Developer Guide - Tutorials - Making a Calculator

This tutorial will explain the steps needed to create a calculator app that is similar to the one included in the demo distribution of Woopsi. Creating a calculator involves creating a screen, a window, buttons, a textbox, and wiring everything up via the event system, so it is a good introduction to building applications with Woopsi.

Note that the calculator we will develop in this tutorial won't perform any calculations - we're solely concerned with building the interface here.

Creating the Calculator Class

The first thing we need to do is to create a class that contains the functionaliy of our calculator. For small applications like a calculator, it's useful to think of a class as a self-contained program that Woopsi will run.

As our class will need to handle button presses, it needs to inherit from the EventHandler class, and it needs to implement the various handler functions. The functions are virtual, so our class only needs to implement the ones we will need.

The code below illustrates a skeleton template of a class that inherits from the EventHandler:

class Calculator : EventHandler {
public:
	// Constructor signature
	Calculator();

	// Event handling signatures
	void handleEvent(EventArgs& e);
};

Note that we haven't declared a destructor - as Woopsi manages everything for us, we don't need one.

This template is missing some important elements. We're all set up to handle events now, but we haven't got any gadgets to fire events, a window or a screen to contain it. We're also missing the preprocessor directives that identify whether or not we've already included this file, and the Woopsi include.

Let's address the gadget problem first. We need a pointer to a screen, and we also need a pointer to a window. We know that we need to output the calculator's value somewhere, so we need a pointer to a textbox gadget. Finally, we need a dozen or so buttons, but we don't need to store pointers to any of these. As we won't need to actually perform any operations on the buttons once they have been created, we can allow Woopsi to manage the button pointers for us. The only interaction we have with the buttons is via the event system, and again, Woopsi will handle all of that.

Adding the preprocessor directives is a simple matter of following standard C++ conventions, as is including the Woopsi header file.

The code below contains the complete class with the new additions in place. If you wanted to add this to a project, you would need to copy the code into a file called "calculator.h" and add it to your project. I've added a few other variables and functions in that we will need later; don't worry about these at the moment.

#ifndef _CALCULATOR_H_
#define _CALCULATOR_H_

#include "woopsiheaders.h"

class Calculator : EventHandler {
private:
	// Woopsi gadget pointers
	AmigaScreen* screen;
	AmigaWindow* window;
	Textbox* output;

	// Variables for other functionality
	bool wipeNeeded;

	// Functions for other functionality
	void addText(char* text);
	
public:
	// Constructor signature
	Calculator();

	// Event handling signature
	void handleEvent(EventArgs& e);
};

#endif

Creating the GUI

Now that we have the class definition, we need to start fleshing out the functions. The constructor will need to create the new screen, window and gadgets, and it will need to wire up the button click events. The constructor looks like this:

Calculator::Calculator() {
	
	// Create the screen
	screen = new AmigaScreen("Calculator");
	woopsiApplication->addGadget(this->screen);

	// Create the window
	window = new AmigaWindow(0, 90, 60, 97, "Calc", GADGET_DRAGGABLE | GADGET_CLOSABLE);
	screen->addGadget(window);

	// Create the textbox
	output = new Textbox(0, 0, 52, 16, "0");
	window->addGadget(output);
	
	// Align the text right
	output->setTextPositionHoriz(Textbox::TEXT_POSITION_HORIZ_RIGHT);

	// Create a temporary vector to store button pointers
	DynamicArray buttons;
	
	// Create all buttons
	buttons.push_back(new Button(0, 16, 13, 16, "7"));
	buttons.push_back(new Button(13, 16, 13, 16, "8"));
	buttons.push_back(new Button(26, 16, 13, 16, "9"));
	buttons.push_back(new Button(39, 16, 13, 16, "*"));

	buttons.push_back(new Button(0, 32, 13, 16, "4"));
	buttons.push_back(new Button(13, 32, 13, 16, "5"));
	buttons.push_back(new Button(26, 32, 13, 16, "6"));
	buttons.push_back(new Button(39, 32, 13, 16, "-"));

	buttons.push_back(new Button(0, 48, 13, 16, "1"));
	buttons.push_back(new Button(13, 48, 13, 16, "2"));
	buttons.push_back(new Button(26, 48, 13, 16, "3"));
	buttons.push_back(new Button(39, 48, 13, 16, "+"));

	buttons.push_back(new Button(0, 64, 13, 16, "0"));
	buttons.push_back(new Button(13, 64, 13, 16, "C"));
	buttons.push_back(new Button(26, 64, 13, 16, "="));
	buttons.push_back(new Button(39, 64, 13, 16, "/"));

	// Wire up button events add add them to the window
	for (u8 i = 0; i < buttons.size(); i++) {
		buttons[i]->setEventHandler(this);
		buttons[i]->setRefCon(i);
		window->addGadget(buttons[i]);
	}
}

Here we have created the screen, added a window to it, added an output textbox, and added 16 buttons. The buttons represent the numbers and calculator functions. We have also set the Calculator class to be the event handler for all of the buttons, by:

You may notice that we allow the button vector to go out of scope without deleting the buttons. This is intentional. We don't need to retain pointers to the buttons outside of Woopsi - we will rely on Woopsi to handle all of the button functionality. Woopsi automatically deletes any gagdets when it is itself deleted.

This constructor should be copied into a new file called "calculator.cpp" and included in your project. You will need to include the header file we've already created by adding this directive to the top of the file:

#include "calculator.h"

Event Handling

At this point, we have a complete GUI for our calculator. All of the visual elements are in place. However, the GUI doesn't actually do anything yet - clicking a button has no effect. Before the GUI will work, and before the code will compile, we need to fill out the event handler functions.

The only event we are interested in for the calculator project is the button click. The "handleEvent()" function will get called when any of the buttons triggers any event, so we need a way to identify which event has been fired, and which button has fired the event. Once we know that information, we can take the appropriate action.

We determine the type of event by checking the values of the EventArgs struct:

void Calculator::handleEvent(EventArgs& e) {
	switch (e.type) {
		case EVENT_CLICK:
			// Click has occurred
			break;
		default:
			return;
	}
}

We can check for any other of Woopsi's supported events by adding extra cases into the switch statement.

Determining which button was clicked is also very easy. As we gave each button a unique refcon, we can embed another switch statement into the handleEvent method:

void Calculator::handleEvent(EventArgs& e) {
	switch (e.type) {
		case EVENT_CLICK:
		
			// Click has occurred
			switch (e.gadget->getRefCon()) {
				case 14:
					// Handle equals button
					break;
				case 13:
					// Handle clear button
					break;
				case 11:
					// Handle add button
					break;
				case 7:
					// Handle subtract button
					break;
				case 3:
					// Handle multiply button
					break;
				case 15:
					// Handle divide button
					break;
				default:
					// Handle numeric button
					break;
			}
			break;
			
		default:
			return;
	}
}

The gadget that fired the event is sent to the function as the EventArgs' "gadget" member. We determine its refcon and act appropriately for that button. If the button is one of the operators we handle its action; otherwise, the button is numeric and we handle it as such.

Although we now know which button has been pressed, and can handle it accordingly, we aren't actually doing anything with the event yet. Let's handle the numeric buttons first.

When a numeric button is clicked, we want to display that number in the output box. The method of output is dependent on the last button that was clicked. If we're clicking a number for the first time, or have just clicked an operator button, we need to wipe the display and set it to the number of the current button. If the last button to be pressed was a number, we know that this press represents another digit in a larger number, so we want to append this new number to the display instead of wiping it. The "addText()" function that we previously prototyped can be filled out to handle this:

void Calculator::addText(char* text) {
	if ((strcmp(output->getText(), "0") == 0) || (wipeNeeded)) {
		wipeNeeded = false;
		output->setText(text);
	} else {
		if (strlen(output->getText()) < 5) {
			output->addText(text);
		}
	}
}

This function works out whether we need to append to or reset the output text. Ignoring the logic of the function, which isn't particularly important here, the three most interesting functions are all members of the TextBox class. "setText()" will wipe the text and replace it with the supplied value. "addText" appends text to the right-hand side of the textbox.

We need to call this new function in our "handleEvent()" function. We also need to make sure that any clicks on the operator buttons inform the calculator that we will need to wipe the display the next time a number is clicked. Finally, we need to make the "clear" button reset the display using the "setText()" TextBox function. Our "handleEvent()" function now looks like this:

void Calculator::handleEvent(EventArgs& e) {
	switch (e.type) {
		case EVENT_CLICK:
		
			// Click has occurred
			switch (e.gadget->getRefCon()) {
				case 14:
					// Handle equals button
					wipeNeeded = true;
					break;
				case 13:
					// Handle clear button
					output->setText("0");
					break;
				case 11:
					// Handle add button
					wipeNeeded = true;
					break;
				case 7:
					// Handle subtract button
					wipeNeeded = true;
					break;
				case 3:
					// Handle multiply button
					wipeNeeded = true;
					break;
				case 15:
					// Handle divide button
					wipeNeeded = true;
					break;
				default:
					// Handle numeric button
					addText(((Button*)e.gadget)->getText());
					break;
			}
			break;
			
		default:
			return;
	}
}

We've now achieved everything we set out to. We have a fully-functional calculator GUI that can identify when buttons are clicked and which button has been clicked. Based on that information, we can update the calculator's display appropriately.

As stated earlier, we haven't actually written the calculation code; if you are interested in that, you can have a look at the calculator class in the Woopsi sourcecode.

Adding the calculator into an existing Woopsi project is easy. The code to do this, which you should include somewhere in your "main()" function in "main.cpp" (or somewhere appropriate to your project) is:

Calculator *calc = new Calculator();

To recap on the sourcecode, this is the full "calculator.h" code:

#ifndef _CALCULATOR_H_
#define _CALCULATOR_H_

#include "woopsiheaders.h"

class Calculator : EventHandler {
private:
	// Woopsi gadget pointers
	AmigaScreen* screen;
	AmigaWindow* window;
	Textbox* output;

	// Variables for other functionality
	bool wipeNeeded;

	// Functions for other functionality
	void addText(char* text);
	
public:
	// Constructor signature
	Calculator();

	// Event handling signature
	void handleEvent(EventArgs& e);
};

#endif

This is the full "calculator.cpp" sourcecode:

#include "calculator.h"
	
Calculator::Calculator() {
	
	// Create the screen
	screen = new AmigaScreen("Calculator");
	woopsiApplication->addGadget(this->screen);

	// Create the window
	window = new AmigaWindow(0, 90, 60, 97, "Calc", GADGET_DRAGGABLE | GADGET_CLOSABLE);
	screen->addGadget(window);

	// Create the textbox
	output = new Textbox(0, 0, 52, 16, "0");
	window->addGadget(output);
	
	// Align the text right
	output->setTextPositionHoriz(Textbox::TEXT_POSITION_HORIZ_RIGHT);

	// Create a temporary vector to store button pointers
	DynamicArray buttons;
	
	// Create all buttons
	buttons.push_back(new Button(0, 16, 13, 16, "7"));
	buttons.push_back(new Button(13, 16, 13, 16, "8"));
	buttons.push_back(new Button(26, 16, 13, 16, "9"));
	buttons.push_back(new Button(39, 16, 13, 16, "*"));

	buttons.push_back(new Button(0, 32, 13, 16, "4"));
	buttons.push_back(new Button(13, 32, 13, 16, "5"));
	buttons.push_back(new Button(26, 32, 13, 16, "6"));
	buttons.push_back(new Button(39, 32, 13, 16, "-"));

	buttons.push_back(new Button(0, 48, 13, 16, "1"));
	buttons.push_back(new Button(13, 48, 13, 16, "2"));
	buttons.push_back(new Button(26, 48, 13, 16, "3"));
	buttons.push_back(new Button(39, 48, 13, 16, "+"));

	buttons.push_back(new Button(0, 64, 13, 16, "0"));
	buttons.push_back(new Button(13, 64, 13, 16, "C"));
	buttons.push_back(new Button(26, 64, 13, 16, "="));
	buttons.push_back(new Button(39, 64, 13, 16, "/"));

	// Wire up button events add add them to the window
	for (u8 i = 0; i < buttons.size(); i++) {
		buttons[i]->setEventHandler(this);
		buttons[i]->setRefCon(i);
		window->addGadget(buttons[i]);
	}
}

void Calculator::handleEvent(EventArgs& e) {
	switch (e.type) {
		case EVENT_CLICK:
		
			// Click has occurred
			switch (e.gadget->getRefCon()) {
				case 14:
					// Handle equals button
					wipeNeeded = true;
					break;
				case 13:
					// Handle clear button
					output->setText("0");
					break;
				case 11:
					// Handle add button
					wipeNeeded = true;
					break;
				case 7:
					// Handle subtract button
					wipeNeeded = true;
					break;
				case 3:
					// Handle multiply button
					wipeNeeded = true;
					break;
				case 15:
					// Handle divide button
					wipeNeeded = true;
					break;
				default:
					// Handle numeric button
					addText(((Button*)e.gadget)->getText());
					break;
			}
			break;
			
		default:
			return;
	}
}

void Calculator::addText(char* text) {
	if ((strcmp(output->getText(), "0") == 0) || (wipeNeeded)) {
		wipeNeeded = false;
		output->setText(text);
	} else {
		if (strlen(output->getText()) < 5) {
			output->addText(text);
		}
	}
}