wGui - Calculator Tutorial

This is a tutorial that will show you how to make a simple calculator using wGui. The final code is only about 200 nicely spaced lines.

The tutorial requires the SDL and FreeType 2 libraries

Project Setup - Win32 (Visual Studio .NET)


Project Setup - *nix

For a *nix system, you will need to create a makefile.

Here's a basic makefile, which will probably need to be modified for your system.


	INCLUDES = -I/usr/include/freetype2 -I../../../includes
	LIBS = -L../../../lib -lwGuid -lfreetype `sdl-config --libs`
	CPPFLAGS = -Wall -O0 -g -DDEBUG `sdl-config --cflags`

	CALC_OBJS = calc_tutorial.o

	.SUFFIXES:
	.SUFFIXES: .cpp .o

	.cpp.o:; g++ $(CPPFLAGS) $(INCLUDES) -c -o $@ $<

	all: CalcApp

	CalcApp: $(CALC_OBJS)
		g++ $(CPPFLAGS) $(INCLUDES) -o CalcTutorial $(CALC_OBJS) $(LIBS)

	clean:
		rm -rf *~ *.o CalcTutorial
	

Makefile


Program Outline

Since the program is rather simple, and only comes out to about 200 lines, all the code will be put into one file (calc_tutorial.cpp).

We'll start with the basic outline of a program.


	#include "wgui.h"
	#include 

	using namespace std;
	using namespace wGui;

	int main(int argc, char **argv)
	{
		try
		{

		}
		catch (...)
		{
			cerr << "Unhandled exception." << endl;
			exit(EXIT_FAILURE);
		}
		exit (EXIT_SUCCESS);
	}
	

Since wGui throws exceptions to indicate that there's a problem, the program will be wrapped in a generic try/catch block.

calc_tutorial1.cpp


Application Class

wGui uses an Application object to handle things like initialization, message passing, and other more mundane tasks. While it can be used as is, we'll derive our own application class.


	class CCalcApp : public CApplication
	{
	public:
		CCalcApp(int argc, char **argv) : CApplication(argc, argv) { }

		virtual void Init(void)
		{
			CApplication::Init();
			SetDefaultFontEngine(GetFontEngine("Vera.ttf"));
		}
	};
	

The only thing that's being done here, is the Init() method has been overridden to set the Default Font Engine. See the Font Rendering page for more information. I've used the common Arial True Type font here, though you can use any font you have. You must copy the font file to the directory you run the program from, or SetDefaultFontEngine() will throw an exception.

Now that we've got an application class, we'll instantiate it in our main() and call the necessary methods.


	int main(int argc, char **argv)
	{
		int iExitCode = EXIT_FAILURE;
		try
		{
			CCalcApp CalcApp(argc, argv);
			CalcApp.Init();
			CalcApp.Exec();

			iExitCode = CalcApp.ExitCode();
		}
		catch (wGui::Wg_Ex_Base& e)
		{
			cerr << "Unhandled wGui exception : " << e.what() << endl;
			exit(EXIT_FAILURE);
		}
		catch (exception& e)
		{
			cerr << "Unhandled std exception : " << e.what() << endl;
			exit(EXIT_FAILURE);
		}
		catch (...)
		{
			cerr << "Unhandled exception." << endl;
			exit(EXIT_FAILURE);
		}
		exit (iExitCode);
	}
	

As you can see, I've also added a few more exception catches so that we can get a little more detailed information if something goes wrong.

calc_tutorial2.cpp


View Class

We'll derive our own view class as well, which is just as simple as our application class.


	class CCalcView : public CView
	{
	public:
		CCalcView(void);
	};


	CCalcView::CCalcView(void) :
		CView(CRect(0, 0, 170, 210), "wGui Calculator Tutorial", false)
	{

	}
	

Now we just have to instantiate one of these in main().


	try
	{
		CCalcApp CalcApp(argc, argv);
		CalcApp.Init();

		CCalcView CalcView;
		CalcApp.Exec();

		iExitCode = CalcApp.ExitCode();
	}
	

It's as simple as adding the one line. The CView constructor will take care of registering itself with the application's message server.

Unfortunately due to a limitation imposed by SDL, wGui only supports one view at a time. Hopefully future versions of SDL will fix this issue.

If you run the application now, you should see an empty window.

calc_tutorial3.cpp


Creating The Interface

Adding controls to our view is just as easy as everything else. First we add some members to our view.


	class CCalcView : public CView
	{
	public:
		CCalcView(void);

	private:
		CButton* m_pBtnNum[10];
		CButton* m_pBtnClear;
		CButton* m_pBtnEquals;
		CButton* m_pBtnPlus;
		CButton* m_pBtnMinus;
		CEditBox* m_pDisplay;
	};
	

Then we initialize them in our constructor. Now you'll note that we don't do anything with the destructor to clean these objects up. This is intentional. All of the controls are derived from CWindow, which get automatically cleaned up when their parent is destroyed. See the CWindow class for more information.


	CCalcView::CCalcView(void) :
		CView(CRect(0, 0, 170, 210), "wGui Calculator Tutorial", false)
	{
		m_pDisplay = new CEditBox(CRect(10, 10, 150, 30), this);
		m_pDisplay->SetWindowText("0");
		m_pDisplay->SetReadOnly(true);
		m_pBtnNum[7] = new CButton(CRect(10, 50, 40, 80), this, "7");
		m_pBtnNum[8] = new CButton(CRect(50, 50, 80, 80), this, "8");
		m_pBtnNum[9] = new CButton(CRect(90, 50, 120, 80), this, "9");
		m_pBtnClear = new CButton(CRect(130, 50, 160, 80), this, "C");
		m_pBtnNum[4] = new CButton(CRect(10, 90, 40, 120), this, "4");
		m_pBtnNum[5] = new CButton(CRect(50, 90, 80, 120), this, "5");
		m_pBtnNum[6] = new CButton(CRect(90, 90, 120, 120), this, "6");
		m_pBtnPlus = new CButton(CRect(130, 90, 160, 120), this, "+");
		m_pBtnNum[1] = new CButton(CRect(10, 130, 40, 160), this, "1");
		m_pBtnNum[2] = new CButton(CRect(50, 130, 80, 160), this, "2");
		m_pBtnNum[3] = new CButton(CRect(90, 130, 120, 160), this, "3");
		m_pBtnMinus = new CButton(CRect(130, 130, 160, 160), this, "-");
		m_pBtnNum[0] = new CButton(CRect(10, 170, 40, 200), this, "0");
		m_pBtnEquals = new CButton(CRect(50, 170, 120, 200), this, "=");
	}
	

calc_tutorial4.cpp


Getting Control Input

We've now got something that looks like a calculator, but it doesn't do anything. The first step here, is to respond to the input from the controls.

In a similar fashion to the Windows API, wGui uses messages to indicate when a user does something with a control. Since all of the controls we're using that take user input are buttons (the EditBox doesn't count since it's set to read-only), this is very simple.

To indicate to the message server that we want to get Button Click messages, we have to add the following line to the view constructor.


		CMessageServer::Instance().RegisterMessageClient(this, CMessage::CTRL_SINGLELCLICK);
	

Then we have to add a HandleMessage() method to the view.


	class CCalcView : public CView
	{
	public:
		CCalcView(void);
		bool HandleMessage(CMessage* pMessage);

	private:
		CButton* m_pBtnNum[10];
		CButton* m_pBtnClear;
		CButton* m_pBtnEquals;
		CButton* m_pBtnPlus;
		CButton* m_pBtnMinus;
		CEditBox* m_pDisplay;
	};


	bool CCalcView::HandleMessage(CMessage* pMessage)
	{
		bool bHandled = false;

		if (pMessage)
		{
			switch (pMessage->MessageType())
			{
			case CMessage::CTRL_SINGLELCLICK:
				if (pMessage->Destination() == this)
				{
					bHandled = true;
					const wGui::CMessageClient* pSource = pMessage->Source();
					if (pSource == m_pBtnClear)
					{
					}
					else if (pSource == m_pBtnPlus)
					{
					}
					else if (pSource == m_pBtnMinus)
					{
					}
					else if (pSource == m_pBtnEquals)
					{
					}
					else if (pSource != 0)
					{
						bHandled = false;
						for (int i = 0; i <= 9; ++i)
						{
							if (pSource == m_pBtnNum[i])
							{
								bHandled = true;
								break; //exit for loop
							}
						}
					}
				}
				break;
			default:
				bHandled = CView::HandleMessage(pMessage);
				break;
			}
		}

		return bHandled;
	}
	

Here the message handler doesn't actually do anything except recieve the messages and acknowledge them to the message server.

Read the Messaging Framework page for more information on how this all works.

calc_tutorial5.cpp


Finishing Things Up

Now we just need to do something when the buttons are pressed, and we'll be done.

We'll expand the view class so that it can do the various operations our calculator can do, and can update the calculator's display.


	class CCalcView : public CView
	{
	public:
		CCalcView(void);
		bool HandleMessage(CMessage* pMessage);

	private:
		void UpdateDisplay(void);
		void DoCalc(void);

		CButton* m_pBtnNum[10];
		CButton* m_pBtnClear;
		CButton* m_pBtnEquals;
		CButton* m_pBtnPlus;
		CButton* m_pBtnMinus;
		CEditBox* m_pDisplay;
		int m_iDisplay;
		int m_iRegister;
		enum ECalcMode {
			MODE_NOP,
			MODE_ADD,
			MODE_SUB
		} m_eCalcMode;
	};


	CCalcView::CCalcView(void) :
		CView(CRect(0, 0, 170, 210), "wGui Calculator Tutorial", false),
		m_iDisplay(0),
		m_iRegister(0),
		m_eCalcMode(MODE_NOP)
	{
		CMessageServer::Instance().RegisterMessageClient(this, CMessage::CTRL_SINGLELCLICK);
		m_pDisplay = new CEditBox(CRect(10, 10, 150, 30), this);
		m_pDisplay->SetWindowText("0");
		m_pDisplay->SetReadOnly(true);
		m_pBtnNum[7] = new CButton(CRect(10, 50, 40, 80), this, "7");
		m_pBtnNum[8] = new CButton(CRect(50, 50, 80, 80), this, "8");
		m_pBtnNum[9] = new CButton(CRect(90, 50, 120, 80), this, "9");
		m_pBtnClear = new CButton(CRect(130, 50, 160, 80), this, "C");
		m_pBtnNum[4] = new CButton(CRect(10, 90, 40, 120), this, "4");
		m_pBtnNum[5] = new CButton(CRect(50, 90, 80, 120), this, "5");
		m_pBtnNum[6] = new CButton(CRect(90, 90, 120, 120), this, "6");
		m_pBtnPlus = new CButton(CRect(130, 90, 160, 120), this, "+");
		m_pBtnNum[1] = new CButton(CRect(10, 130, 40, 160), this, "1");
		m_pBtnNum[2] = new CButton(CRect(50, 130, 80, 160), this, "2");
		m_pBtnNum[3] = new CButton(CRect(90, 130, 120, 160), this, "3");
		m_pBtnMinus = new CButton(CRect(130, 130, 160, 160), this, "-");
		m_pBtnNum[0] = new CButton(CRect(10, 170, 40, 200), this, "0");
		m_pBtnEquals = new CButton(CRect(50, 170, 120, 200), this, "=");
	}


	void CCalcView::UpdateDisplay(void)
	{
		char cMode = m_eCalcMode == MODE_ADD ? '+' :
			m_eCalcMode == MODE_SUB ? '-' : ' ';
		char buffer[100];
		sprintf(buffer, "%i %c", m_iDisplay, cMode);
		m_pDisplay->SetWindowText(buffer);
	}


	void CCalcView::DoCalc(void)
	{
		switch (m_eCalcMode)
		{
		case MODE_ADD:
			m_iDisplay = m_iRegister + m_iDisplay;
			break;
		case MODE_SUB:
			m_iDisplay = m_iRegister - m_iDisplay;
			break;
		default:
			break;
		}
	}
	

Then we add a few lines of code to the message handler so that it calls our new methods


	bool CCalcView::HandleMessage(CMessage* pMessage)
	{
		bool bHandled = false;

		if (pMessage)
		{
			switch (pMessage->MessageType())
			{
			case CMessage::CTRL_SINGLELCLICK:
				if (pMessage->Destination() == this)
				{
					bHandled = true;
					const wGui::CMessageClient* pSource = pMessage->Source();
					if (pSource == m_pBtnClear)
					{
						m_iDisplay = 0;
						m_iRegister = 0;
						m_eCalcMode = MODE_NOP;
					}
					else if (pSource == m_pBtnPlus)
					{
						DoCalc();
						m_iRegister = m_iDisplay;
						m_iDisplay = 0;
						m_eCalcMode = MODE_ADD;
					}
					else if (pSource == m_pBtnMinus)
					{
						DoCalc();
						m_iRegister = m_iDisplay;
						m_iDisplay = 0;
						m_eCalcMode = MODE_SUB;
					}
					else if (pSource == m_pBtnEquals)
					{
						DoCalc();
						m_iRegister = 0;
						m_eCalcMode = MODE_NOP;
					}
					else if (pSource != 0)
					{
						bHandled = false;
						for (int i = 0; i <= 9; ++i)
						{
							if (pSource == m_pBtnNum[i])
							{
								m_iDisplay = m_iDisplay * 10 + i;
								bHandled = true;
								break; //exit for loop
							}
						}
					}
					UpdateDisplay();
				}
				break;
			default:
				bHandled = CView::HandleMessage(pMessage);
				break;
			}
		}

		return bHandled;
	}
	

And now you're done!

calc_tutorial6.cpp


Final Notes

This tutorial was based on the CalcApp test program that you'll find in the tests directory. The CalcApp test does a few more things, such as registering for keystrokes (so you can type numbers in), so you may want to take a look at that.