|
Kindly hosted by

GUI Tutorial : Part #1
Welcome to the first tutorial in GUI series. I've updated them to a higher standard (hence, more words?) so that
you enjoy them more and understand a little deeper of what goes behind a custom GUI.
Please don't stop reading it just because it's an OpenGL GUI, because it only needs OpenGL to render widgets and
create textures/materials, but the rest of the code doesn't depend on OpenGL.
Table of Contents
Introduction
What is a GUI? Graphical User Interface. Correct. Anything else?
Well, let's take a look at a static control in Windows. What is it?
It's simply a 2D polygon with some
text on it and additional features
like border, colour and texture. So, let us assume that our GUI will
also be a 2D polygon (quad). What do we need for that? Let's take a
look.
Class Definition & Structure
Our first class will be... yes, CGUIElement, that is the
basic of all other controls. We will be adding
more functionality to
it as we go.
Let's take a look on the class:
class CGUIElement : public CXMLResource, public CDoubleList<CGUIElement *>
{
CREATOR(CGUI);
CREATOR(CGUIBorder);
public:
CGUIElement();
virtual ~CGUIElement();
virtual void Initialize();
virtual int Create(CGUIElement *pParent, tRect WidgetRect, CTexture *pTexture = NULL, CMaterial *pMaterial = NULL, bool bBorder = false);
virtual int LoadXML(TiXmlNode *pDataNode, CString strFilename);
virtual int SaveXML(TiXmlNode *pDataNode, CString strFilename);
virtual void Draw();
virtual void Destroy();
virtual void ProcessMessages();
virtual void ProcessMessage(tGUIMessage& Message);
virtual int Resize(tRect& NewRect);
virtual int OnKeyDown(UINT uiKeyCode);
virtual int OnKeyUp(UINT uiKeyCode);
virtual int OnMove(UINT x, UINT y);
virtual int OnMouseMove(UINT x, UINT y);
virtual int OnLMouseDown(UINT x, UINT y);
virtual int OnLMouseUp(UINT x, UINT y);
virtual int OnRMouseDown(UINT x, UINT y);
virtual int OnRMouseUp(UINT x, UINT y);
UINT GetWidth();
void SetWidth(UINT uiWidth);
UINT GetHeight();
void SetHeight(UINT uiHeight);
CGUIElement *GetChild(UINT uiIndex);
CGUIElement *GetChild(eGUIControlType eWidgetType);
CGUIElement *GetChild(TiXmlElement *pXMLElement);
UINT GetChildCount();
void RemoveChild(CGUIElement *pChild);
void HideSiblings(int iDeep = -1);
void ShowSiblings(int iDeep = -1);
void SetZOrder(UINT uiZOrder);
UINT GetZOrder();
bool Visible();
void Hide();
void Show();
CGUIElement *GetParent();
void SetParent(CGUIElement *pParent);
void SetTexture(CTexture *pTexture);
CTexture *GetTexture();
void SetMaterial(CMaterial *pMaterial);
CMaterial *GetMaterial();
void SetTexCoord(UINT uiIndex, float U, float V);
tVERTEX2f& GetTexCoord(UINT index);
CGUI *GetGUI();
void SetRect(tRect& NewRect);
tRect& GetRect();
void SetGUI(CGUI *pGUI);
eGUIControlType& GetType();
void SetType(eGUIControlType eWidgetType);
void SetFlag(eGUIFlag eFlag, bool bSet);
bool IsFlagSet(eGUIFlag eFlag);
bool IsChild(CGUIElement *pElement);
bool IsAutoCalc();
void SetAutoCalc(bool bAutoCalc);
virtual bool IsOfType(eEntityType eType);
CString GetElementType();
#ifdef GUI_USE_ACTIVE_INACTIVE
bool IsActive() { return m_bActive; };
void Activate() { m_bActive = true; };
void Deactivate() { m_bActive = false; };
#endif
protected:
#ifndef GUI_USE_ACTIVE_INACTIVE
CMaterial *m_pMaterial;
#elif
COLORREF m_ActiveColor;
COLORREF m_InactiveColor;
bool m_bActive;
#endif
eGUIControlType m_eControlType;
bool m_bAutoCalc;
CGUIElement *m_pParent;
CGUI *m_pGUI;
tRect m_Rect;
DWORD m_dwFlags;
CTexture *m_pTexture;
tVERTEX2f m_TexCoord[4];
bool m_bVisible;
};
After all this code, here's an example view of the application of Tutorial #1:
It's not much, but wait for the time when we get to the more advanced controls! Also, you can add your own textures, use alpha masking,
and even with plain simple widgets textured up you can achieve remarkable effects!
Yes, I know it's a big class, lots of code and so on. But hey, it's our base for the rest of our widgets who are just going to
reuse this code to their advantage instead of re-writing it!
So, let's take a look at its functionality.
First of all we need to initialize a widget, which can be done in constructor, but I've chosen to use Initialize() function
to do it. The idea is to have a re-usable widget that can be used again for the other purposes and not deallocated and
then allocated again, thus fragmenting the memory. So, as you might guess, it's a good idea to add a memory manager to
the GUI. We shall do that later. But now...
Now.. oh yeah, now.. As you can see, we are also going to use a messaging system via an Event Handler classand a Message
structure. Each widget will go thru ProcessMessages() function each time it draws (or updates) itself and look for messages
that are related to it. For example, a droplist will look after a button press (since a droplist IS a button at its root) and see
if it is one of its children, and if it is, it sets current selection to it.
Each widget will also implement it's custom loading/saving procedure, but not re-writing it again from scratch each time. Instead,
it will call the derivate class to save it's properties, the derivate class will call it's derivate class to save their properties and so on and on,
and at the end the root class will save it's properties
where all the derivate classes has saved theirs. Hence, the use of inheritance.
Each class will also implement it's miscelaneous key/mouse handling functions to implement their custom events (like with button,
a button press can be implemented using OnLMouseDown and OnLMouseUp events).
Basically, all the functions that are virtual can be re-implemented again by higher order classes (you knew that, didn't you?)
to add extra functionality.
A basic widget also provides a tree-like structure, where it can have any number of children, and each child has a reference to
the parent and also can contain any number of children.
A custom implementation of RTTI is also provided here, but it requres that each new class that derives from base CEntity class
to provide it's own implementation of IsOfType virtual function and the class type in eEntityType enum, where it determines whether
the passed type complies with the object type,
and if it's not, it will be checked by base class, and so on and on until it either founds
the type compliant with passed one, or returns
false. Here's an example of current RTTI for GUI:
Drawing of the Element
Now, moving on to our Draw() function, we are going to have a look
on it's internal code:
void CGUIElement::Draw()
{
if(!Visible())
return;
int left = m_Rect.left;
int right = m_Rect.right;
int top = m_Rect.top;
int bottom = m_Rect.bottom;
if(m_pTexture == NULL)
{
glDisable(GL_TEXTURE_2D);
if(m_pMaterial !=NULL)
{
glDisable(GL_LIGHTING);
glColor4fv(m_pMaterial->GetAmbient().RGBA); }
else
{
glDisable(GL_LIGHTING);
glColor3d(255, 255, 255);
}
glBegin(GL_QUADS);
glVertex3d(left, bottom, 0);
glVertex3d(right, bottom, 0);
glVertex3d(right, top, 0);
glVertex3d(left, top, 0);
glEnd();
}
else
{
m_pTexture->Bind();
if(m_pMaterial !=NULL)
{
glDisable(GL_LIGHTING);
glColor3f(m_pMaterial->GetAmbient().R / 255.0f, m_pMaterial->GetAmbient().G / 255.0f, m_pMaterial->GetAmbient().B / 255.0f); }
else
glColor3d(1, 1, 1);
if(m_pTexture->IsMasked())
{
glAlphaFunc(GL_GREATER, 0.0f);
glEnable(GL_ALPHA_TEST);
}
glBegin(GL_QUADS); glTexCoord2f(m_TexCoord[0].x, m_TexCoord[0].y); glVertex3d(left, bottom, 0); glTexCoord2f(m_TexCoord[1].x, m_TexCoord[1].y); glVertex3d(right, bottom, 0); glTexCoord2f(m_TexCoord[2].x, m_TexCoord[2].y); glVertex3d(right, top, 0); glTexCoord2f(m_TexCoord[3].x, m_TexCoord[3].y); glVertex3d(left, top, 0);
glEnd();
if(m_pTexture->IsMasked())
glDisable(GL_ALPHA_TEST);
}
CGUIElement *Iter = begin();
set_ptr(Iter);
while(Iter !=NULL)
{
Iter->Draw();
Iter = next();
}
}
As you can see, its pretty easy to understand what's going on in here. Firstly, we check if we
have a texture to texture our
quad with,
and if not, we disable texturing and use material/color
to colorize the quad. But if we do have a texture, we
check if it has a mask,
and if it does, we use alpha test to remove pixels specified in texture' alpha
channel.
That's it!
Handling Input
Our basic widget handles only the mouse events, since it's a basic control and only needs to know
when it is being moved
(border elements, for example). Let's take a look at basic workings of mouse input:
int CGUIElement::OnLMouseUp(UINT x, UINT y)
{
CGUIElement *IterElement = end();
set_ptr(IterElement);
while(IterElement !=NULL)
{
if(IterElement->OnLMouseUp(x, y)) return 1;
IterElement = prev();
}
if(GetType() !=GUI)
{
if(PointInRect(GetRect(), x, y))
{
GetGUI()->PostMessage(this, NULL, GUI_Message_AquireFocus);
return 1;
}
}
return 0;
}
As you see, the element firstly propagates the input to it's children in hope that they can process it.
If they can, one of them will
return 1 and the root element doesn't have to process the mouse input.
If all of them return 0, then it can process the mouse input
without the fear if interfering with them.
So, in this case, if the mouse was released over the rectangle of the element (PointInRect()),
the element aquires an active status and sends a message to all notifying them of this change.
You also can notice (if you're good enough! :) ), that we start from last and go towards first child in
our propagation. Why?
Because, if we draw the as usual (from first to last child), the last child will have a highest
Z order (e.g will be on top of all others).
And, because we need to check if the one that is above all processes the message, we don't have to go
any deeper and it will
be a proper way to go.
Easy!
Let's also take a look at the MouseMove procedure:
int CGUIElement::OnMouseMove(UINT x, UINT y)
{
if(x == 0 && y == 0)
return 0;
int iRetValue = 0;
CGUIElement *IterElement = end();
set_ptr(IterElement);
while(IterElement !=NULL)
{
iRetValue|=IterElement->OnMouseMove(x, y);
IterElement = prev();
}
if(GetType() !=GUI)
{
if(Visible())
{ if(GetGUI()->IsLocked())
{
tVERTEX2d CursorPos = CInputEngine::GetSingleton().GetMouse()->GetCursorPos();
tVERTEX2d PrevCursorPos = CInputEngine::GetSingleton().GetMouse()->GetPrevCursorPos();
if(GetGUI()->GetActiveElement() == this)
{
OnMove(CursorPos.x - PrevCursorPos.x, CursorPos.y - PrevCursorPos.y);
return 1;
}
}
if(PointInRect(m_Rect, x, y))
iRetValue = 1;
}
}
return iRetValue;
}
As you see, it also starts from last child and goes towards the first. You also can see that we
check the type of the widget
to prevent unnecessary work being done (in our case we don't
need to move GUI, because it takes the whole screen) and we also check
if the mouse is being
locked on some widget. We lock the mouse in the OnLMouseDown when we're sure that point
is inside the widget'
rectangle and prepare it for being moved around (if it can be moved).
If the mouse IS locked and it IS the widget it's being locked on (e.g
an active one), we
allow it to be moved when the mouse moves. Brilliant!
(Note that we unlock the mouse when it's being released)
Well, that's about it for the first tutorial. Download a GUI Editor, add any simple widgets (Element)
and use this tutorial to rock your world!
Next tutorial we will go into details of saving/loading data from XML files as well as adding a simple border class to ease the
resizing of our beloved widgets. Catch ya then!
EXTRA NOTES
Binary: To run these binaries, you require FreeImage library runtime files - DLL and LIB in the application directory!
Source: To debug the application using the source code, you need a DLL and the FreeImageD.
lib files in the application directory!
|