Home
Code & Snippets
Tutorials
GUI Docs
Projects
Drawings & Sketches
Links
Bio & Resume

Kindly hosted by


Support This Project

GUI Tutorial : Part #3

Table of Contents

 

Welcome to the third tutorial in GUI series. This time we will go deeply into element movement and it's resizing using
mouse and some additional functions. But first...

Mouse & Keyboard handler class (CGUIUtility)

At first, we would need some kind of utility class, that will track all mouse, keyboard and possibly joystick
(highly doubt that we need it) actions - for example,
mouse movement and it's buttons, as well as keyboard key presses/releases. So, let's see what our class looks like:

/*************************** GUI Utility class - tracks mouse movements and presses, and keys ****************************/
class CGUIUtility : public CSingleton<CGUIUtility> // Only one must exist!
{
public:
        // Mouse movement handler function - passes mouse movement to required elements and they process 
them in the way they want to.
virtual int OnMouseMove(int x, int y); // Mouse buttons handler functions - passes mouse buttons presses/un-presses to required elements virtual int OnLMouseDown(int x, int y); virtual int OnLMouseUp(int x, int y); virtual int OnRMouseDown(int x, int y); virtual int OnRMouseUp(int x, int y); // Keyboard key handler functions virtual int OnKeyDown(unsigned int CharCode); virtual int OnKeyUp(unsigned int CharCode); // Retrieval functions tVERTEX2f GetMousePos(); tVERTEX2f GetPrevMousePos(); tVERTEX2f GetLMouseDown(); tVERTEX2f GetLMouseUp(); tVERTEX2f GetRMouseDown(); tVERTEX2f GetRMouseUp(); // Retrieve pressed keys keycode by going thru a loop 5-10 times (hardly anyone can press more than
10 keys at a time - 10 fingers?)
unsigned int GetKeyDown(unsigned int index = 0); // Remember, there can be more than one key down
at an instant // Main parsing function - see XML file for more information on it's parsing actions
virtual int Parse(TiXmlNode *node, char *filename); // Mouse.Flag 0 - MouseOver or not // Mouse.Flag 1 - LMouseDown or not // Mouse.Flag 2 - RMouseDown or not // Mouse.Flag 3 - Reserved tFlag m_MouseMessages; tFlag m_KeyboardMessages; // Get an active element - that is, the one that is being dragged or typed in - e.g the one that
has focus
CGUIElement *GetActiveElement(); void SetActiveElement(CGUIElement *element); // Constructor & Destructor CGUIUtility(); virtual ~CGUIUtility(); private: tVERTEX2f m_MousePos, m_PrevMousePos; tVERTEX2f m_LMouseDown; tVERTEX2f m_LMouseUp; tVERTEX2f m_RMouseDown; tVERTEX2f m_RMouseUp; bool m_iKeys[256]; CGUIElement *m_pActiveElement; // Current active element };

Well, its pretty big, but I suppose if you don't need right mouse buttons, you can safely remove them and their
corresponding functions/member variables.
So, let's see now how Windows or Linux GUIs work.


Children Organization

Basically, every GUI element is a rectangle (we assumed that from Tutorial #1), and each of it's children have
the defined Z order. Let's say we have a dialog box with some buttons on it. The Z order for dialog is 1, and
each of the buttons will have a higher Z order than the previous element. After all, we cannot draw some rectangles
at once on the screen, they will look that they have the same Z order if and only if they do not overlap each other.
So, we will define our Z order in a simple way:

<List of children>
Child 1 - Lowest Z order
Child 2 - Higher than Child 1 Z Order
Child 3 - Higher than Child 2 Z Order
...
</end list of children>

We will begin our drawing from the last one up to the first one. But, we will be passing mouse messages
(presses and movement) as well as keyboard presses/releases in the opposite way, thus ensuring that the
control with highest Z order will process that message first (if it is within certain boundaries - like for
the mouse press the mouse position must be within element' rectangle boundaries), and only then if it
doesn't found a use for it, pass it to the lower Z order children.

Being more practical, we will take a look at our OnDraw() code:

void CGUIElement::OnDraw()
{
	int x1, x2, y1, y2;
x1 = m_Rect.m_iLeft; x2 = m_Rect.m_iRight; y1 = m_Rect.m_iTop; y2 = m_Rect.m_iBottom; if(m_pBackgroundMaterial == NULL) { glColor3d(m_Color.m_fR, m_Color.m_fG, m_Color.m_fB); glBegin(GL_QUADS); glVertex3d(x1, y1, 0); glVertex3d(x1, y2, 0); glVertex3d(x2, y2, 0); glVertex3d(x2, y1, 0); glEnd(); } else { ApplyMaterial(m_pBackgroundMaterial, TEX_COLOR, NULL); if(m_pBackgroundMaterial->m_pTexPtr->m_MaskTexID == 1) { glAlphaFunc(GL_GREATER,0.0f); glEnable(GL_ALPHA_TEST); glBegin(GL_QUADS); glTexCoord2f(m_fTexCoord[0].x, m_fTexCoord[0].y); glVertex3d(x1, y2, 0); glTexCoord2f(m_fTexCoord[1].x, m_fTexCoord[1].y); glVertex3d(x2, y2, 0); glTexCoord2f(m_fTexCoord[2].x, m_fTexCoord[2].y); glVertex3d(x2, y1, 0); glTexCoord2f(m_fTexCoord[3].x, m_fTexCoord[3].y); glVertex3d(x1, y1, 0); glEnd(); glDisable(GL_ALPHA_TEST); } else { glBegin(GL_QUADS); glTexCoord2f(m_fTexCoord[0].x, m_fTexCoord[0].y); glVertex3d(x1, y2, 0); glTexCoord2f(m_fTexCoord[1].x, m_fTexCoord[1].y); glVertex3d(x2, y2, 0); glTexCoord2f(m_fTexCoord[2].x, m_fTexCoord[2].y); glVertex3d(x2, y1, 0); glTexCoord2f(m_fTexCoord[3].x, m_fTexCoord[3].y); glVertex3d(x1, y1, 0); glEnd(); } } CGUIElement *tmp = m_lstChildren.begin(); m_lstChildren.set_ptr(tmp); while(tmp !=NULL) { tmp->OnDraw(); tmp = m_lstChildren.next(); }

As you may see, we drawing our children from start of list to the end. So, our last child we draw
will be overlapping the ones drawn before, thus having a highest Z order. By going thru that list in
the other way, we ensure that the one with highest Z order will process the message first,
thus eliminating the need for the lower Z order elements from processing it needlessly and taking
valuable CPU/processing time.


Message Processing Example (OnLMouseDown)

Here is an example of a Left Mouse Button Pressed function:

int CGUIElement::OnLMouseDown(int x, int y) 
{
	CGUIElement *tmp = m_lstChildren.end();
	m_lstChildren.set_ptr(tmp);
	
	while(tmp !=NULL)
	{
		if(tmp->OnLMouseDown(x, y))
			return 1;
		tmp = m_lstChildren.prev();
	}

	if(m_bVisible)
	{	
		if(PointInRect(&m_Rect, x, y))
		{
			CGUIUtility::GetSingleton().SetActiveElement(this);
			// Activate here and bring it to m_iTop!
			CGUIUtility::GetSingleton().m_MouseMessages.m_iFlag1 = LMouseDown;
			return 1;
		}
	}

	return 0;
}

As I have said, the message processing goes from end to start. For each and every mesage, be it
LMB Down/Up, RMB Down/Up, or any other.

Top class - CGUI

OK, moving on, we come to the concept of a GUI. We will have a single instance of a CGUI class
(Singleton, anyone?) that we have to create in order to use it and its underlying children' GUIs.

Let's take a look at the class definition:

class CGUI : public CSingleton<CGUI>
{
public:
	// Drawing function
	void OnDraw();
	// Main parsing function - see XML file for more information on it's parsing actions
	virtual int Parse(TiXmlNode *node, char *filename);
	// Destruction function
	void OnDestroy();

	// Get/Set for current active GUI
	CGUIElement *GetGUI(int index = -1);	// If index is -1, it returns currently active GUI, 
otherwise returns GUI by index
void SetGUI(unsigned int index); CGUI(); virtual ~CGUI(); private: CDoubleList <CGUIElement *> m_lstGUI; CGUIElement *m_pDesktop; // Current active element that takes whole screen (and more)
that receives mouse/kbd messages
};

Well, not so hard as it seems. We create a single instance of it in the same way as we create a
pointer (CPointer *pPointer = new CPointer;), and after that we can acess it by calling
CGUI::GetSingleton().AnyFunctionHere(). Pretty easy. But first, in order to have it to do something, we have to:

1. Initialize it properly.
2. Parse some file with GUI descriptions.
3. Add OnDraw() call to RenderGLScene() in-between our setup for Orthogonal mode.


So, let's do it!
Firstly, let's take a look at our Parse() function. I promise, this one is easier than the one used in CGUIElement -
but wait, it will get harder soon:

int CGUI::Parse(TiXmlNode *this_node, char *filename)
{
	
	TiXmlElement* element = NULL;
	TiXmlNode *node = NULL;
	TiXmlDocument doc;
	bool parse_again = false;

	// If a user passed a filename, use it as an external link to our data for this element
	if(filename != NULL)
	{
		if(!doc.LoadFile(filename))	// Try to parse data using given filename
			return 0;
		
		node = doc.FirstChild();	// Aquire first data node
	}
	
	if(this_node !=NULL)
	{
		element = this_node->ToElement();
		
		if(element->Attribute("Filename") !=NULL)
		{
			if(!doc.LoadFile(element->Attribute("Filename")))
				return 0;
			
			node = doc.FirstChild();
			parse_again = true;
		}
	}

	if(parse_again)
		Parse(node, NULL);
	else
	{
		int viewport[4];
		glGetIntegerv(GL_VIEWPORT, viewport);
		
		TiXmlNode *child_node = NULL;
		child_node = node->FirstChild();

		while(child_node !=NULL)
		{
			CGUIElement *newElement = NULL;
			if(stricmp(child_node->Value(), "element") == 0)	// We got a simple element
				newElement = new CGUIElement;
			
			if(newElement->Parse(child_node, NULL))
			{
				// Each desktop will have a full screen for its needs
				newElement->SetRect(tRect(0, viewport[2], viewport[3], 0));
				newElement->m_RestrictionFlags.m_iFlag2 = RestrictMoveX;
				newElement->m_RestrictionFlags.m_iFlag3 = RestrictMoveY;

				m_lstGUI.push_back(newElement);
				m_pDesktop = newElement;
			}
			else
				delete newElement;

			child_node = node->IterateChildren(child_node);
		}
	}
	return 1;
}

Well, easy as it seems (for some of you). My basic GUI in XML file will look like this:

<GUI>
<Element Name="SomeGUI" Color="0,255,0">
<Element Rect="200,216,500,400" ID="15" MatByRefID="1" 
UV="(0.00,0.00),(1.00,0.00),(1.00,1.00),(0.00,1.00)"
BorderFile="default_border.xml"/> </Element> </GUI>

Basically, a GUI XML file will consist of:
1. GUI Header - so our app will know that it is a GUI XML file
2. A list of sub-GUIs
2.1 A list of sub-elements
2.1.1 etc

Well, a few words about our CGUIUtility class. In OpenGL a screen is set up so that the top
of it has a bigger Y value than the bottom, and the right side has the bigger value of X then
left side. For this, we have to add a little modification to the mouse code, so that it will be correct
in our GUI processing. As you may know, in Windows, top has lesser Y value than bottom, compared to OpenGL.
So, to correct that little-big mistake, we have to substract current Y value from the height of the window,
that can be aquired using glGetIntegerv(GL_VIEWPORT, viewport) function, and only after correcting that
we pass X and Y values to mouse processing functions.

Moving to some updates from previous tutorial, you might notice that Parse function has changed a bit,
and also the CGUIElement has some additional functions for children accessibility, and also the major
functions that we will need for element' dragging and resizing. Also, we would need to somehow identify
our element amongst tohers, so we will give it member integer variable (m_iElementID) and also the variable identifying
it's type (m_eElementType). I will not go into basic Get/Set functions for them, but instead let me take a look at our mouse functions:

Other messages (OnLMouseDown/Up, OnMouseMove) intrinsics

We will look only to the basic functions - left mouse button press/release, and its movement. For mouse movement,
the mouse coordinates must be passed to each element (or they might not be, depends on your taste).
For example, in OnMouseMove the element will track whether the mouse is over or not, and then modify
the global flags accordingly. Those flags are required by the actual element (for example, by button element) to
handle different mouse states. IN button, for example, this may mean the different texture (for mouse over).
In LMouseDown we also track global flag - the reason is the same as described above. Same goes for the LMouseUp.
In each element' mouse function in calls children' mouse functions. If some of them returns 1 (or true), it means that
it has processed it and that no other child will have to process that message. This shortens up processing time in overall.

int CGUIElement::OnLMouseDown(int x, int y) 
{
	CGUIElement *tmp = m_lstChildren.end();
	m_lstChildren.set_ptr(tmp);
	
	while(tmp !=NULL)
	{
		if(tmp->OnLMouseDown(x, y))
			return 1;
		tmp = m_lstChildren.prev();
	}

	if(m_bVisible)
	{	
		if(PointInRect(&m_Rect, x, y))
		{
			CGUIUtility::GetSingleton().SetActiveElement(this);
			// Activate here and bring it to m_iTop!
			CGUIUtility::GetSingleton().m_MouseMessages.m_iFlag1 = LMouseDown;
			return 1;
		}
	}

	return 0;
}

int CGUIElement::OnLMouseDown(int x, int y) 
{
	CGUIElement *tmp = m_lstChildren.end();
	m_lstChildren.set_ptr(tmp);
	
	while(tmp !=NULL)
	{
		if(tmp->OnLMouseDown(x, y))
			return 1;
		tmp = m_lstChildren.prev();
	}

	if(m_bVisible)
	{	
		if(PointInRect(&m_Rect, x, y))
		{
			CGUIUtility::GetSingleton().SetActiveElement(this);
			// Activate here and bring it to m_iTop!
			CGUIUtility::GetSingleton().m_MouseMessages.m_iFlag1 = LMouseDown;
			return 1;
		}
	}

	return 0;
}


int CGUIElement::OnLMouseUp(int x, int y)
{
	CGUIElement *tmp = m_lstChildren.end();
	m_lstChildren.set_ptr(tmp);
	
	while(tmp !=NULL)
	{
		if(tmp->OnLMouseUp(x, y))
			return 1;
		tmp = m_lstChildren.prev();
	}

	if(m_bVisible)
		CGUIUtility::GetSingleton().m_MouseMessages.m_iFlag1 = Default;

	return 0;
}


Well, that's pretty much it for the CGUIElement. Now, let's take a look at our moving and sizing code.

Moving the Element with mouse

When we press a mouse over an element, the global flag (mousedown) is raised, so that we know that while
we have that mouse down, we can move the element. Then, we displace the element by the amount it has
moved since the last mouse position - this is where m_PrevMousePos and m_MousePos comes into play.
We simply substract one from the other to get the actual displacement, and then move the rectangle X/Y units
we got from making the substraction. So, here's our OnMove function:

int CGUIElement::OnMove(int x, int y)
{
	int mx = Default, my = Default;
	if(m_bVisible)
	{	
		// Resize this element according to its restriction flags
		if(m_RestrictionFlags.m_iFlag2 !=RestrictMoveX && x !=0)
		{
			m_Rect.m_iLeft+=x;
			m_Rect.m_iRight+=x;
			mx = MoveX;
		}

		if(m_RestrictionFlags.m_iFlag3 !=RestrictMoveY && y !=0)
		{
			m_Rect.m_iTop+=y;
			m_Rect.m_iBottom+=y;
			my = MoveY;
		}
	}

	CGUIElement *border = FindChild(Border);
	if(border !=NULL)
		border->OnSize(m_Rect);

	if(mx != Default && my != Default)
		return MoveXY;
	else if(mx == MoveX && my == Default)
		return MoveX;
	else if(my == MoveY && mx == Default)
		return MoveY;
}

Here we first of all check for restriction flags (maybe the user cannot even move it in some direction!) and only
after we add the displacement values to rectangle coordinates. Also, do not forget to resize the border (if there's any).
Then we return the code appropriate to the movement of the rectangle
(if only moved in X direction, return MoveX, if only Y, return MoveY, if both - MoveXY).
Easy. But sizing code wouldn't look so easy as moving.

Border class

Let's take a look at CGUIBorder class:

/****************************** Border Class ***********************************/
class CGUIBorder : public CGUIElement
{
public:
	
        virtual void OnDraw(); // Drawing function
	virtual int Parse(TiXmlNode *node, char *filename);
	virtual int OnSize(tRect newSizeRect); // Border needs a custom OnSize() routine, since it has to assign rectangles to it's border elements
	
	// A cursor is changed when a mouse is over the border to allow the user to resize it
	virtual int OnLMouseDown(int x, int y);
	virtual int OnLMouseUp(int x, int y);
	virtual int OnMouseMove(int x, int y);
	
	// Get/Set functions
	void SetBorderWidth(unsigned int width);
	unsigned int GetBorderWidth();

	void SetBorderSpacing(unsigned int spacing);
	unsigned int GetBorderSpacing();
	// Get/Set functions

	virtual void OnDestroy(); // Destruction function

	// Constructor & Destructor
	CGUIBorder();
	virtual ~CGUIBorder();

private:
	unsigned int m_iBorderWidth, m_iBorderSpacing;	// Border width & spacing (height can be added)
	unsigned int m_iBorderElmntDrag;				// An index pointing to element that is being dragged
	CGUIElement	m_BorderElmnt[8];					// 8 border elements

tRect FindInnerRect(int x, int y); // Utility function for finding inner rectangle };

Well, here we go. The m_iBorderWidth and m_iBorderSpacing are holding border width (there can be height as
well if you wanted to) and spacing between border elements (in units, in our case - OpenGL).m_BorderElmnt[8]
are simply children of the border - the rectangles that the border has. I've chosen to do it this way and not to add
them into list of children, since it's much easier to handle them this way, and also saves time by going thru list of children
in search of particular border element requested by index, for example.

There are only a few functions worth taking a look at in CGUIBorder. They are OnSize(), OnLMouseDown() and OnMouseMove().

int CGUIBorder::OnSize(tRect newSizeRect)
{
	SetRect(newSizeRect);

	tRect r[8];
	
	// Left
	r[0].m_iLeft = newSizeRect.m_iLeft - m_iBorderWidth - m_iBorderSpacing;
	r[0].m_iRight = newSizeRect.m_iLeft - m_iBorderSpacing;
	r[0].m_iTop = newSizeRect.m_iTop + m_iBorderSpacing;
	r[0].m_iBottom = newSizeRect.m_iBottom - m_iBorderSpacing;
	
	// Right
	r[1].m_iLeft = newSizeRect.m_iRight + m_iBorderSpacing;
	r[1].m_iRight = newSizeRect.m_iRight + m_iBorderWidth + m_iBorderSpacing;
	r[1].m_iTop = newSizeRect.m_iTop + m_iBorderSpacing;
	r[1].m_iBottom = newSizeRect.m_iBottom - m_iBorderSpacing;
	
	// Top
	r[2].m_iLeft = newSizeRect.m_iLeft - m_iBorderSpacing;
	r[2].m_iRight = newSizeRect.m_iRight + m_iBorderSpacing;
	r[2].m_iTop = newSizeRect.m_iTop + m_iBorderWidth + m_iBorderSpacing;
	r[2].m_iBottom = newSizeRect.m_iTop + m_iBorderSpacing;

	// Bottom
	r[3].m_iLeft = newSizeRect.m_iLeft - m_iBorderSpacing;
	r[3].m_iRight = newSizeRect.m_iRight + m_iBorderSpacing;
	r[3].m_iTop = newSizeRect.m_iBottom - m_iBorderSpacing;
	r[3].m_iBottom = newSizeRect.m_iBottom - m_iBorderWidth - m_iBorderSpacing;

	// Top Left
	r[4].m_iLeft = r[0].m_iLeft;
	r[4].m_iRight = r[0].m_iRight;
	r[4].m_iTop = r[0].m_iTop + m_iBorderWidth;
	r[4].m_iBottom = r[0].m_iTop;

	// Top Right
	r[5].m_iLeft = r[1].m_iLeft;
	r[5].m_iRight = r[1].m_iRight;
	r[5].m_iTop = r[1].m_iTop + m_iBorderWidth;
	r[5].m_iBottom = r[1].m_iTop;

	// Bottom Left
	r[6].m_iLeft = r[0].m_iLeft;
	r[6].m_iRight = r[0].m_iRight;
	r[6].m_iTop = r[0].m_iBottom;
	r[6].m_iBottom = r[0].m_iBottom - m_iBorderWidth;

	// Bottom m_iRight
	r[7].m_iLeft = r[1].m_iLeft;
	r[7].m_iRight = r[1].m_iRight;
	r[7].m_iTop = r[1].m_iBottom;
	r[7].m_iBottom = r[1].m_iBottom - m_iBorderWidth;

	for (int i=0; i<8; i++)
		GetChild(i)->SetRect(r[i]);

	return 1;
}

The OnSize() function accepts the given rectangle (the parent' rectangle) and recalculates the border
rectangles according to it.

int CGUIBorder::OnLMouseDown(int x, int y)
{
	for (int i=0; i<8; i++)
	{
		CGUIElement *border_element = GetChild(i);
		int ret = border_element->OnLMouseDown(x, y);
		if(ret == 1)
		{
			m_iBorderElmntDrag = i;
			return 1;
		}
	}
	return 0;
}


Dragging border' elements

The OnLMouseDown() function tracks which border element the mouse was pressed over (and held) and assigns a
member variable to track which border bard is being dragged - thus showing us which way we should resize
our parent element.
The last function, OnMouseMove() tracks movement of border children, and at the same time resizing parent rectangle
according to the dragged element index. See the 'if' switches?
That's the thing that checks which element is dragged, and in which way the parent' rectangle must be resized in:

int CGUIBorder::OnMouseMove(int x, int y)
{
	if(m_iBorderElmntDrag !=-1)
	{
		CGUIElement *dragged_element = GetChild(m_iBorderElmntDrag);
		tRect oldDraggedRect = dragged_element->GetRect();
		tRect oldParentRect = GetParent()->GetRect();

		CGUIElement::OnMouseMove(x, y); // Do general movement of dragged element border (if there's any)
		// Now, let's check if parent can accept the actual movement of that dragged border element that is resizing it
		// Firstly, let's find the displacement of the rectangle...

		tRect newRect = dragged_element->GetRect();
		tRect diff_rect;
		diff_rect.m_iLeft = newRect.m_iLeft - oldDraggedRect.m_iLeft;
		diff_rect.m_iRight = newRect.m_iRight - oldDraggedRect.m_iRight;
		diff_rect.m_iTop = newRect.m_iTop - oldDraggedRect.m_iTop;
		diff_rect.m_iBottom = newRect.m_iBottom - oldDraggedRect.m_iBottom;

		tRect newParentRect = oldParentRect;

		if(m_iBorderElmntDrag == 0 || m_iBorderElmntDrag == 4 || m_iBorderElmntDrag == 6)	
// User drags leftmost elements, this decreasing m_iLeft
newParentRect.m_iLeft = GetParent()->GetRect().m_iLeft + diff_rect.m_iLeft; if(m_iBorderElmntDrag == 1 || m_iBorderElmntDrag == 5 || m_iBorderElmntDrag == 7)
// User drags rightmost elements, thus decreasing m_iRight newParentRect.m_iRight = GetParent()->GetRect().m_iRight + diff_rect.m_iRight; if(m_iBorderElmntDrag == 2 || m_iBorderElmntDrag == 4 || m_iBorderElmntDrag == 5)
// User drags topmost elements, thus increasing m_iTop newParentRect.m_iTop = GetParent()->GetRect().m_iTop + diff_rect.m_iTop; if(m_iBorderElmntDrag == 3 || m_iBorderElmntDrag == 6 || m_iBorderElmntDrag == 7)
// User drags topmost elements, thus decreasing m_iBottom
newParentRect.m_iBottom = GetParent()->GetRect().m_iBottom + diff_rect.m_iBottom; if(!GetParent()->OnSize(newParentRect)) return OnSize(oldParentRect); else return OnSize(newParentRect); } return 0; }

Well, that's about it! Now, let's take a look what our code will do for us and be proud of yourselves!