The Main Menu
So, you've probably read all the Beginner Guide to X tutorials (if not then go back and read them !), your sitting there still feeling lost, and you can't find exactly how to bring all the stuff you've read together to make a simple GUI. I too found it specially hard to wrap everything together when starting out and hopefully this article should help address this problem for others, I'm still a beginner so there might be a few errors, but for the most part the code has been tested. This tutorial will be rather Ogre oriented since thats what I use, I'll try to keep things a little generic though.
Contents
Getting started
For tutorial purposes, we will quickly run over the Initialisation part (as this has been addressed in other tutorials more thoroughly and is usually engine specific). I will not litter the tutorial with unlrelated code like framelisteners, We will be making a main menu which is basicly just a collection of buttons and a StaticImage for background, but it is cruical that you have read the following tutorials and understood them, specially the file types. [CrazyEddie's Beginner Guides ] [Ogre's Basic Tutorial 6]
A plan of action
There are 3 ways to go about this, one way is to hardcode the menu (which is a bad idea unless you don't want your menu to be customisable), the 2nd way is to use although uses an xml based approach by writing layout files, imageset files..etc. The 3rd way is to use the falagard system which is subset of the 2nd approach, but expands even more on the functionality by offering the .looknfeel files which allow you to skin any existing scheme or even create your own.
Prerequisites
Ok, before we get started we need to have a few things initialised first, other then Ogre root, scene manager, framelistener....etc. If you've read [basic tutorial 6] you'll see that we need to have the following defiendone of the looks loaded as well as a font. I'll be using TaharezLook and tahoma-12 for this tutorial, but I'll load the font at a later stage in the tutorial. I'll only use CEGUI:: in this area, but I'll drop using it since it'll be obvious where to use it after a while. Just assume I've used using namespace CEGUI;
// Initialisation Area CEGUI::OgreCEGUIRenderer* mGUIRenderer = new CEGUI::OgreCEGUIRenderer(Root::getSingletonPtr()->getAutoCreatedWindow(), Ogre::RENDER_QUEUE_OVERLAY, false, 3000); CEGUI::System* mGUISystem = new CEGUI::System(mGUIRenderer); CEGUI::Logger::getSingleton().setLoggingLevel(CEGUI::Informative); // this is recommended to help with debugging, but not neccessary CEGUI::SchemeManager::getSingleton().loadScheme((CEGUI::utf8*)"TaharezLook.scheme"); mGUISystem->setDefaultMouseCursor((CEGUI::utf8*)"TaharezLook", (CEGUI::utf8*)"MouseArrow"); CEGUI::DefaultWindow* mRootWindow= CEGUI::WindowManager::getSingleton().createWindow((CEGUI::utf8*)"DefaultWindow", (CEGUI::utf8*)"RootWindow"); mGUISystem->setGUISheet(mRootWindow); // set active Window
- Note 1: Casting string params to UTF8* isn't neccessary, if your application will only use english (latin based languages), it shouldn't be of any use. But if you want to use other languages which are not covered by the ascii chart, you can use the utf8 cast. I won't be using it for this tutorial, but it should be obvious where you can use it
- Note 2: While its totally legal to use different windows and have children assigned to them, I prefer to have a single root window in which all other windows are childs to it. I initialise it as DefaultWindow* to set it apart from other window types
- Note 3: You can start your app without initialising a font, so long as you use a Window that doesn't use fonts (e.g: StaticImages)
The Code Approach
There are a few things you'll constantly need when writing a menu, assuming your gui code is in a different class, we'll attempt to get pointers to them at the beginning of the code.
WindowManager* Wmgr = WindowManager::getSingletonPtr(); System* GUISys = System::getSingletonPtr(); Window* myRoot = System::getSingletonPtr()->getWindow("DefaultWindow"); // get default window
The Menu code
Anyways, now that we got that out of the way, lets get started. I'll write code, then explain it below
// Menu Background StaticImage* MenuBackground = (StaticImage*)Wmgr->createWindow("TaharezLook/StaticImage", "Background"); myRoot->addChildWindow( MenuBackground ); MenuBackground->setPosition( Point( 0.0f, 0.0f ) ); MenuBackground->setSize( Size( 1.0f, 1.0f ) ); // full screen // New game Button PushButton* NewGame = (PushButton*)Wmgr->createWindow("TaharezLook/Button", "NewGame"); MenuBackground->addChildWindow( NewGame ); NewGame->setPosition( Point( 0.2f, 0.2f ) ); NewGame->setSize( Size( 0.4f, 0.2f ) ); // Load game Button PushButton* LoadGame= (PushButton*)Wmgr->createWindow("TaharezLook/Button", "LoadGame"); MenuBackground->addChildWindow( LoadGame ); LoadGame->setPosition( Point( 0.2f, 0.45f ) ); LoadGame->setSize( Size( 0.4f, 0.2f ) ); // Quit game Button PushButton* QuitGame= (PushButton*)Wmgr->createWindow("TaharezLook/Button", "QuitGame"); MenuBackground->addChildWindow( QuitGame ); QuitGame->setPosition( Point( 0.2f, 0.7f ) ); QuitGame->setSize( Size( 0.4f, 0.2f ) ); GUISys->setGUISheet(myRoot); // this line is redundant since you didn't change gui sheets, but its here to make sure
This will create a menu with 3 empty buttons positioned below each other with an empty background. You always need to define the starting position of the window (the top left edge) and how big the window will be. If you don't you'll never see the window. You'll see that the 3 buttons are childs of the Background Menu window, which is a child of the RootWindow.
Changing the look
But this is no fun, we want to set Images to the buttons, display text on them, perhaps even use tooltips. The menu isn't really usable this way. But before we start jumping into things, we must do a few things first.
Using Fonts
Before we use text, we'll need to define a font. If you've already done that then skip forward. If not then please add this line to your initialisation code in Prerequisites area.
FontManager::getSingletonPtr()->createFont("Tahoma-12.font");
To add a text line for a button you use:
ButtonPtr->setText("foo");
Using Tooltips
Now that we've defined a font, we should define tooltips as well in initialisation area.
myGUISystem->setTooltip("TaharezLook/Tooltip");
You should usually just leave this with initialisation along with initialisation. You'll need to inject time pulses each frame though, that was explained here already ToolTips. To add a tooltip for a button you use:
ButtonPtr->setTooltipText("foo");
Using Images
Before using images, we need to define an imageset. You can skip forward to the imageset explanation in the XML area to understand what it means.
Note that you could easily create a full image from a texture using the line below, the created image will be called "full_image"
Imageset* foo = ImagesetManager::getSingletonPtr()->createImagesetFromImageFile("NameOfImageset", "Image.jpg");
But if you want to split it up it'll be alittle harder. You'll need to define a texture
Texture* texturePtr = System::getSingletonPtr()->getRenderer()->createTexture("ImageFile.jpg"); Imageset* MenuImageset = ImagesetManager::getSingletonPtr()->createImageset("ImageName", texturePtr);
Since I'm no artist, we'll just assume we have 2 images, one with the background, and one with the different button states (make sure they're loaded in resources.cfg). While there are 4 different states( Normal, Hover, pushed & Disabled) we'll just use 2 different looks, one for normal & the other will be shared amongst Hover, Pushed & disabled. The 2nd image will basicly look a box with 2 different textures to indicate it being pushed in or out. Each of the 2 images will have its own imageset.
Imageset* MenuImageset = ImagesetManager::getSingletonPtr()->createImagesetFromImageFile("Background","MenuBackground.jpg"); Texture* texturePtr = System::getSingletonPtr()->getRenderer()->createTexture("MenuButtons.jpg"); Imageset* MenuImageset = ImagesetManager::getSingletonPtr()->createImageset("Buttons", texturePtr);
Now we have our imagesets, we'll need define images in each. The first we'll need to define an the Background image, which will be full size. For the 2nd imageset we'll need to split it up and define 2 images inside of it. One for Button Up, and one for Button Down.
MenuImageset->defineImage("Background", Point(0.0f,0.0f), Size( 1.0f, 1.0f ), Point(0.0f,0.0f)); // Whole Image ButtonsImageset->defineImage("ButtonUp", Point(0.0f,0.0f), Size( 0.5f, 0.5f ), Point(0.0f,0.0f)); // First half of image ButtonsImageset->defineImage("ButtonDown", Point(0.0f,0.5f), Size( 0.5f, 0.5f ), Point(0.0f,0.0f)); // Second Half
now that we have images define, we need to turn them into RenderableImage before using them.
RenderableImage foo; foo.setImage(ImagesetFOO->getImage("ImageName"); Window->setImage(foo);
Wrapping It all up
Now lets write the code above again with the new adjustments
/*** Stuff you need to do in the initialisation phase ***/ FontManager::getSingletonPtr()->createFont("Tahoma-12.font"); myGUISystem->setTooltip("TaharezLook/Tooltip"); // Creating Imagesets and defining images Imageset* MenuImageset = ImagesetManager::getSingletonPtr()->createImagesetFromImageFile("Background","MenuBackground.jpg"); Texture* texturePtr = System::getSingletonPtr()->getRenderer()->createTexture("MenuButtons.jpg"); Imageset* MenuImageset = ImagesetManager::getSingletonPtr()->createImageset("Buttons", texturePtr); ButtonsImageset->defineImage("ButtonUp", Point(0.0f,0.0f), Size( 0.5f, 0.5f ), Point(0.0f,0.0f)); ButtonsImageset->defineImage("ButtonDown", Point(0.0f,0.5f), Size( 0.5f, 0.5f ), Point(0.0f,0.0f)); // Create RenderableImages RenderableImage ButtonUp, ButtonDown, Background; Background.setImage( MenuImageset->getImage("full_image") ); ButtonUp.setImage( ButtonsImageset->getImage("ButtonUp") ); ButtonDown.setImage( ButtonsImageset->getImage("ButtonDown") ); /*** the menu code ***/ StaticImage* MenuBackground = (StaticImage*)Wmgr->createWindow("TaharezLook/StaticImage", "Background"); myRoot->addChildWindow( MenuBackground ); MenuBackground->setPosition( Point( 0.0f, 0.0f ) ); MenuBackground->setSize( Size( 1.0f, 1.0f ) ); // full screen MenuBackground->setImage(Background); PushButton* NewGame = (PushButton*)Wmgr->createWindow("TaharezLook/Button", "NewGame"); MenuBackground->addChildWindow( NewGame ); NewGame->setPosition( Point( 0.2f, 0.2f ) ); NewGame->setSize( Size( 0.4f, 0.2f ) ); NewGame->setText("New Game"); NewGame->setNormalImage(ButtonUp); NewGame->setHoverImage(ButtonDown); NewGame->setPushedImage(ButtonDown); PushButton* LoadGame = (PushButton*)Wmgr->createWindow("TaharezLook/Button", "LoadGame"); MenuBackground->addChildWindow( LoadGame ); LoadGame->setPosition( Point( 0.2f, 0.45f ) ); LoadGame->setSize( Size( 0.4f, 0.2f ) ); LoadGame->setText("Load Game"); LoadGame->setTooltipText("Disabled, not implemented yet"); LoadGame->Disable(); LoadGame->setNormalImage(ButtonUp); LoadGame->setHoverImage(ButtonDown); LoadGame->setPushedImage(ButtonDown); LoadGame->setDisabledImage(ButtonDown); PushButton* QuitGame= (PushButton*)Wmgr->createWindow("TaharezLook/Button", "QuitGame"); MenuBackground->addChildWindow( QuitGame ); QuitGame->setPosition( Point( 0.2f, 0.7f ) ); QuitGame->setSize( Size( 0.4f, 0.2f ) ); QuitGame->setText("Quit Game"); QuitGame->setNormalImage(ButtonUp); QuitGame->setHoverImage(ButtonDown); QuitGame->setPushedImage(ButtonDown);
Now go ahead and test, doesn't that look much better ? It looks more like a proper menu now. You could start handling events from here on (explained later). But this way isn't really very good. Should you want to change anything in the menu, you'll have to edit several lines of code & recompile your application. Besides, just making a menu as simple as that took about 50 lines of code, there must be a simpler way.
XML based approach (non-falagard way)
This way is much more efficient then hard-coding the menu. Its less code, easier to edit and use as well. I personally think this way is even better then using the falagard system if you're just after doing small and simple menu's, despite the fact falagard is supposed to be better performance wise and more flexible. Before we start we need
- Note 1: The .xsd files included in the cegui datafiles aren't needed if you're using Ogre. Ogre uses a different XML parser (tinyXML) which doesn't use these files so feel free to remove them, but if you're not using ogre and you didn't specify a different xml parser, you'll need those files.
XML Crash Course
If you're familiar with xml, feel free to skip this area (or better yet, rewrite it to make it more helpful).
A code line is usually encapsulated in < >. To start a declare a new type you can use <Type/> or <Type> </Type>. The first way should be used to declare objects that don't have too many settings. e.g:
<Image Name="Background" XPos="0.0" YPos="0.0" Width="800" Height="600" />
As you've probably noticed, you can set its settings in the same line. As for the second way, it should be used to open up bigger types. e.g:
<Window Type="TaharezLook/Button" Name="NewGame"> <Property Name="AbsoluteRect" Value="l:224.000000 t:216.000000 r:416.000000 b:264.000000" /> </Window>
You can start nesting types in other types, you should be able to pickup the rest of the basics from xml files included with CEGUI and the ones in this tutorial.
- Note 1: Please use a proper text editor and not notepad or wordpad, there are many available for free online, using google would be a good time here :).
XML File types
Now I'll explain the filetypes (copied from the beginner tutorial to loading data) and thier uses, there are 3 types. .layout .imageset .scheme ( there is a 4th one .looknfeel used with falagard) While you can just call them .xml and be done with it, this way makes it more clearer.
Imagesets
Effectively, an Imageset is just a collection of defined regions upon the source image / texture file (which is also specified in the Imageset definition). Each of these defined regions has a unique name and is known within the system as an Image. An Image as defined in an Imageset is the basic level of imagery used by CEGUI. By modifying the source image / texture file and also the position and size of the defined regions within the Imageset files you can easily change the appearance of what gets drawn by CEGUI.
Since each imageset cannot contain more then one Image file. We'll have to make 2 imageset files. For both images, we'll assume the width and hieght are 512x512. But for the second image, we'll assume it has 4 different button images inside instead of just 2 like the code part of tutorial, each button image is 1/4 the size of the full image. The only reason behind this is its much easier to assign & define images now.
- MenuBackground.imageset
<?xml version="1.0" ?> <Imageset Name="Background" Imagefile="MenuBackground.jpg" NativeHorzRes="800" NativeVertRes="600" AutoScaled="true" ResourceGroup="General"> <Image Name="Background" XPos="0.0" YPos="0.0" Width="512" Height="512" /> </Imageset>
- MenuButtons.imageset
<?xml version="1.0" ?> <Imageset Name="Buttons" Imagefile="MenuButtons.jpg" NativeHorzRes="800" NativeVertRes="600" AutoScaled="true"> <Image Name="ButtonUp" XPos="0.0" YPos="0.0" Width="512" Height="128" /> <Image Name="ButtonDown" XPos="0.0" YPos="128.0" Width="512" Height="128" /> <Image Name="ButtonDisabled" XPos="0.0" YPos="256.0" Width="512" Height="128" /> <Image Name="ButtonHighlighted" XPos="0.0" YPos="384.14" Width="512" Height="128" /> </Imageset>
While the ResourceGroup="" line isn't neccessary, I just added it so you can see where to use it. To load Imagesets by code you do
ImagesetManager::getSingletonPtr()->createImageset( "MenuBackground.imageset");
Layouts
A layout file contains an XML representation of a window layout. Each nested 'Window' element defines a window or widget to be created, the 'Property' elements define the desired settings and property values for each window defined.
- Menu.layout
<?xml version="1.0" ?> <GUILayout> <Window Type="WindowsLook/StaticImage" Name="Menu/Background"> <Property Name="Size" Value="w:1 h:1" /> <Property Name="Image" Value="set:Background image:Background" /> <Window Type="WindowsLook/Button" Name="Menu/NewGame"> <Property Name="UnifiedAreaRect" Value="{{0.2,0.0},{0.2,0.0},{0.6,0.0},{0.4,0.0}}" /> <Property Name="NormalImage" Value="set:Buttons image:ButtonUp" /> <Property Name="HoverImage" Value="set:Buttons image:ButtonDisabled" /> <Property Name="PushedImage" Value="set:Buttons image:ButtonHighlighted" /> <Property Name="DisabledImage" Value="set:Buttons image:ButtonDisabled" /> <Property Name="Tooltip" Value="Start a new game"/> </Window> <Window Type="WindowsLook/Button" Name="Menu/LoadGame"> <Property Name="UnifiedAreaRect" Value="{{0.2,0.0},{0.45,0.0},{0.6,0.0},{0.65,0.0}}" /> <Property Name="NormalImage" Value="set:Buttons image:ButtonUp" /> <Property Name="HoverImage" Value="set:Buttons image:ButtonDisabled" /> <Property Name="PushedImage" Value="set:Buttons image:ButtonHighlighted" /> <Property Name="DisabledImage" Value="set:Buttons image:ButtonDisabled" /> <Property Name="Tooltip" Value="Not implemented yet"/> </Window> <Window Type="WindowsLook/Button" Name="Menu/QuitGame"> <Property Name="UnifiedAreaRect" Value="{{0.2,0.0},{0.7,0.0},{0.6,0.0},{0.9,0.0}}" /> <Property Name="NormalImage" Value="set:Buttons image:ButtonUp" /> <Property Name="HoverImage" Value="set:Buttons image:ButtonDisabled" /> <Property Name="PushedImage" Value="set:Buttons image:ButtonHighlighted" /> <Property Name="DisabledImage" Value="set:Buttons image:ButtonDisabled" /> <Property Name="Tooltip" Value="Exit To Desktop"/> </Window> </Window> </GUILayout>
This does basicly all the stuff we did in the code portion of the tutorial. The only new term here is UnifiedAreaRect, the first pair defines where the XPos starts, the 3rd defines where it ends. Same goes for 2nd & 4th but for YPos
To load a .layout file in code you do
Window* Menu = WindowManager::getSingletonPtr()->loadWindowLayout("Menu.layout");
- Note 1: There is an editor file for layouts written by scriptkid, its preferable if you get it from CVS and build it, but its possible to get the binaries from the downloads area in the cegui website. While its rather limited atm, it allows you to create and place the various widgets and export into a .layout file. It doesn't support custom images as of yet (8/12/2005).
- Note 2: Since the layout file is usually the most error prone, I usually encase it in a try catch statement to catch any parsing exceptions and pause on them. If you don't do that you won't get informed of errors when they happen, which could lead to other errors happening and crashing ultimatley.
try { Window* Menu = WindowManager::getSingletonPtr()->loadWindowLayout("Menu.layout"); } catch(CEGUI::Exception &e) { OGRE_EXCEPT(Ogre::Exception::ERR_INTERNAL_ERROR, string(e.getMessage().c_str()), "Error Parsing Menu"); }
Schemes
A Scheme is a means to group other data files together, it's also the most convenient way to load and register widget types. A Scheme can contain one or more of the following (which will be loaded and initialised when the scheme is loaded): Imageset Window Set Window Alias A 'Window Set' basically specifies the name of a loadable module or skin(.dll , .so, .looknfeel), and a set of widgets contained within that modules that you wish to register with the system. A 'Window Alias' provides a means to refer to a window / widget type by alternative names, it can also be used to 'hide' an already registered widget type with another widget type (so that other widget type gets used instead).
This file isn't totally needed in the current approach, we're just using it here as a means of grouping imagesets for loading. However this file will be used in greater detail in the Falagard system.
- GameGUI.scheme
<?xml version="1.0" ?> <GUIScheme Name="GameGUI"> <Imageset Name="Background" Filename="MenuButtons.imageset" ResourceGroup="General"/> <Imageset Name="Buttons" Filename="MenuButtons.imageset"/> </GUIScheme>
This simply just loads the 2 imagesets. To load the scheme by code you use
SchemeManager::getSingleton().loadScheme("GameGUI.scheme);
Wrapping It all up
Save the xml files above and add them somewhere in your Resource hierarchy (which should be your media folder unless you changed it).
Now for the code portion, the initialisation area will remain the same. However, you'll need to have this line before your menu code
SchemeManager::getSingleton().loadScheme("GameGUI.scheme); // Or you could do this if you skip the scheme part //ImagesetManager::getSingletonPtr()->createImageset( "MenuBackground.imageset"); //ImagesetManager::getSingletonPtr()->createImageset( "MenuButtons.imageset");
Now its time to write the menu code
WindowManager* Wmgr = WindowManager::getSingletonPtr(); System* GUISys = System::getSingletonPtr(); Window* myRoot = System::getSingletonPtr()->getWindow("DefaultWindow"); // get default window try { Window* Menu = Wmgr->loadWindowLayout("Menu.layout"); } catch(CEGUI::Exception &e) { OGRE_EXCEPT(Ogre::Exception::ERR_INTERNAL_ERROR, string(e.getMessage().c_str()), "Error Parsing Menu"); } myRoot->addChildWindow(Menu); GUISys->setGUISheet(myRoot); // this line is redundant since you didn't change gui sheets, but its here to make sure
And thats it ! 50 lines of code are now alittle less then 10 lines. The menu is now easily editable by editing the xml files without the need of recompiling. Should any errors occur, checking the cegui.log should be very handy.
Falagard Way & the looknfeel system
TO DO
Ok, so we've seen how to make our menu. But so far we can only look at it, we can't use it. Here we'll learn how to start subscribing events and handle them. But CEGUI doesn't detect keyboard & mouse input itself, we'll need to inject Input as we get it. So you'll have to modify your framelistener to do so. Check basic tutorial 6 on how to do that.
To start handling events we need to have pointers for each menu button you want handled (even the background if you want to do anything special). If you've followed the code approach, you should already have them. Otherwise, we'll need to start getting them. Simply do this for each button
PushButton* Button1 = (PushButton*)m_WindowManager->getWindow("Widget Reference Name");
Now that we have pointers to the buttons we wan't to handle. You need to subscribe it to a function that will do the "handling". If you're function is a global function (or not part of a class). you do this
Button1 ->subscribeEvent(PushButton::EventClicked, HandleButton1);
if it is part of a class you do this
Button1->subscribeEvent(PushButton::EventClicked, Event::Subscriber(&ClassName::HandleButton1, this));
What this does is tell the Window to trigger function HandleButton1() should EventClicked happen. There are much more events which you can find out about them from the API reference.
Now here is a typical action
bool HandleButton1(const EventArgs& e) { //Code you want Button1 to do return true; }
Once the event is triggered, HandleButton1 is called and an EventArgs is passed to it. This has some useful info you might need to operate on,
- Note 1 : the function needs to be of type bool and return true. it also needs to take one parameter only and thats const CEGUI::EventArgs&
Handling events for the layout approach
I'll show an example based on the layout approach, you could easily adapt it to any of the other approaches.
// declaring functions bool handleNewGame(const EventArgs& e); bool handleQuitButton(const EventArgs& e); bool handleHover(const EventArgs& e);