|
Kindly hosted by
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!
|