Richard Marks' Allegro Techniques


Want to learn some cool new shit that you can do with Allegro?

Creating a custom GUI with Allegro

Part I: Basic Buttons

I'm going to get right to the point.
I'm not one for long introductions and meaningless chitchat.
In fact this is the only intro you're going to see in the whole series.
Let's get started.

First you need to design the Button class

Create the file Button.h and toss in an empty class like this:

<<begin code>>
#ifndef BUTTON_H
#define BUTTON_H

class Button
{
public:
    // constructor
    Button();

    // destructor
    ~Button();
};

#endif
<<end code>>

Now you need to think about how you are going to use buttons.
I have chosen what I think is a simple and easy design.

We will have a ButtonManager class that will handle all our Buttons.
Our Button class needs a few methods for drawing, updating, and creating Buttons.

add them under the destructor:

<<begin code>>
    // creates the button image
    void Create();

    // recreates the button image
    void Recreate();

    // updates the button
    void Update();

    // renders the button
    void Render(BITMAP* destination);
<<end code>>

Notice we are using the BITMAP structure from Allegro in the Render method.
We need to forward-declare that structure so that we can use it.
Above your class and below the #define, add this line:
<<begin code>>
struct BITMAP;
<<end code>>

We need to have a way to detect clicking on a button so we are going to use a
callback function.

A callback function is simply a function that gets called automatically when
something happens. In our Button's case, we will handle 3 events.

Mouse Over
Mouse Out
Click

Our Buttons may need to modify itself or other data in our program.
We will pass a pointer to the button and a void pointer to the button callback
functions. You can see how it is used later on in the tutorial.

Add the callback function pointers after the Render method:

<<begin code>>
    // callback functions
    void (*OnMouseOver)(Button* object, void* data);
    void (*OnMouseOut)(Button* object, void* data);
    void (*OnClick)(Button* object, void* data);
<<end code>>

Next I think we need to add some member variables to our class. But what?

Our buttons will need a caption, a position, a size, and an Allegro BITMAP.
We might want to have different colored buttons, so we will toss in some
members for that as well. Since our buttons might handle some external data,
we have a void pointer that points to such data.

Add this code in out Button class after the methods
<<begin code>>
private:

    // void data pointer for use with callbacks
    void* data_;

    // the button image
    BITMAP* image_;

    // the text on the button
    char* caption_;

    // the coloring of the button
    int faceColor_;
    int borderColor_;
    int shadowColor_;
    int textColor_;

    // the dimension of the button
    int w_;
    int h_;

    // the position of the button
    int x_;
    int y_;
<<end code>>

We need some more methods to manipulate the members we added.
So add these methods to the Button class before the code we just added.

<<begin code>>

    void SetCaption(char* caption);
    void SetSize(int width, int height);
    void SetPosition(int x, int y);
    void SetPosition(BITMAP* anchorBitmap, int x, int y);
    void SetTextColor(int color);
    void SetShadowColor(int color);
    void SetBorderColor(int color);
    void SetFaceColor(int color);

    // repositions the button in the center of the surface
    void CenterOn(BITMAP* surface);
   
    void SetMiscData(void* data);
    void* GetMiscData();

    int GetX();
    int GetY();
    int GetWidth();
    int GetHeight();
    int GetTextColor();
    int GetShadowColor();
    int GetBorderColor();
    int GetFaceColor();
    char* GetCaption();

<<end code>>

That should all make sense to you, so I will not go into much detail except for
the Set/GetMiscData methods.

Now we need to create Button.cpp and write the code that implements all the methods.

First things first.

Include the headers we need. Which are?. .. Allegro and our Button class definition!

<<begin code>>
#include <allegro.h>
#include "Button.h"
<<end code>>

So that we can create buttons that don't do anything yet (placeholders) we will
implement a dummy callback function that will be used as a default for all button
events.

So, this code goes in Button.cpp after the #includes.

<<begin code>>
// dummy function for callbacks
void dummyCallbackFunction(Button* object, void* data)
{
    if (data && object)
    {
    }
    /* dummy function does nothing */
}
<<end code>>

Next we will implement our constructor and destructor.
We are going to simply use an initializer list to set all the default values.

<<begin code>>
Button::Button():
x_(0), y_(0),
w_(128), h_(24),
textColor_(makecol(0,0,0)),
faceColor_(makecol(192,192,192)),
borderColor_(makecol(96,96,96)),
shadowColor_(makecol(0,0,0)),
image_(NULL),
caption_(NULL),
OnClick(dummyCallbackFunction),
OnMouseOver(dummyCallbackFunction),
OnMouseOut(dummyCallbackFunction),
data_(0)
{
}
<<end code>>

Here is the destructor. Free the memory that our button uses.

<<begin code>>
Button::~Button()
{
    if (image_ != NULL)
    {
        destroy_bitmap(image_);
        image_ = NULL;
    }
}
<<end code>>

Now we are going to implement the code that creates the button image.
We will have a single condition for creation.
The caption MUST be set before the button is created.

Buttons will automatically be sized to have a 4 pixel padding around the caption.

Creating the button image is very simple. We find the width of the caption in pixels
using the text_length function from Allegro.
We also get the height of the font with text_height().
Adding 8 pixels to both gives us the size of our button image.

Next we create the Allegro bitmap, and clear it to the button face color.
We draw a border around the button, then write the caption on it.

And thats it.

Here is the code that you need to implement the Create() method.

<<begin code>>
void Button::Create()
{
    if (caption_ != NULL)
    {
        int width = text_length(font, caption_);
        w_ = width + 8;
        h_ = text_height(font) + 8;

        image_ = create_bitmap(w_, h_);
        clear_bitmap(image_);

        clear_to_color(image_, faceColor_);

        rect(image_, 0, 0, w_-1, h_-1, borderColor_);

        textprintf_ex(image_, font,
            w_ / 2 - width / 2,
            h_ / 2 - text_height(font) / 2,
            textColor_, faceColor_,
            caption_);
    }
}
<<end code>>

To implement the Recreate method we just check to see if the button was created,
and if so, we free the memory that it used, and then call Create again.

<<begin code>>
void Button::Recreate()
{
    if (image_ != NULL)
    {
        destroy_bitmap(image_);
        image_ = NULL;
    }
    this->Create();
}
<<end code>>

Our buttons need to be updated each frame to handle the events.
We poll the Allegro mouse if it needs to be polled.
Next we check for the mouse pointer being over the button, which fires
the OnMouseOver event. if the mouse is not over the button, the OnMouseOut
event will fire. (This is sort of bad because if your event handler puts you
into a loop or does something memory expensive, your computer will lock up)
It works for our purposes, as we will rarely use the OnMouseOut event.

If the left mouse button is pressed while we are over the button, then the
OnClick event will fire.

Here is the code for the Update method.

<<begin code>>
void Button::Update()
{
    if (mouse_needs_poll())
    {
        poll_mouse();
    }

    if (mouse_x >= x_ && mouse_x <= x_ + w_ &&
        mouse_y >= y_ && mouse_y <= y_ + h_)
    {
        this->OnMouseOver(this, data_);

        if (mouse_b & 1)
        {
            this->OnClick(this, data_);
        }
    }
    else
    {
        this->OnMouseOut(this, data_);
    }
}
<<end code>>

Drawing our buttons is very simple.
No explanation should be needed. Here is the code for Render()

<<begin code>>
void Button::Render(BITMAP *destination)
{
    if (image_ == NULL) return;
    blit(image_, destination, 0, 0, x_, y_, image_->w, image_->h);
}
<<end code>>

Here is the code for the set/get methods that manipulate our member variables.
Its all very simple, and no explanations should really be neccessary here.

<<begin code>>
void Button::SetCaption(char* caption)
{
    caption_ = caption;
}

void Button::SetSize(int width, int height)
{
    // this function does not do anything if the button has already been created
    if (image_ != NULL) return;
    w_ = width;
    h_ = height;
}

void Button::SetPosition(int x, int y)
{
    x_ = x;
    y_ = y;
}

void Button::SetPosition(BITMAP* anchorBitmap, int x, int y)
{
    if (x < 0)
        x_ = anchorBitmap->w - (w_ - x);
    else
        x_ = x;

    if (y < 0)
        y_ = anchorBitmap->h - (h_ - y);
    else
        y_ = y;
}

void Button::SetTextColor(int color)
{
    textColor_ = color;
}

void Button::SetShadowColor(int color)
{
    shadowColor_ = color;
}

void Button::SetBorderColor(int color)
{
    borderColor_ = color;
}

void Button::SetFaceColor(int color)
{
    faceColor_ = color;
}

void Button::CenterOn(BITMAP* surface)
{
    if (surface == NULL) return; // do nothing if no surface exists
    x_ = surface->w / 2 - w_ / 2;
    y_ = surface->h / 2 - h_ / 2;
}

int Button::GetX()
{
    return x_;
}

int Button::GetY()
{
    return y_;
}

int Button::GetWidth()
{
    return w_;
}

int Button::GetHeight()
{
    return h_;
}

int Button::GetTextColor()
{
    return textColor_;
}

int Button::GetShadowColor()
{
    return shadowColor_;
}

int Button::GetBorderColor()
{
    return borderColor_;
}

int Button::GetFaceColor()
{
    return faceColor_;
}

char* Button::GetCaption()
{
    return caption_;
}

void Button::SetMiscData(void* data)
{
    data_ = data;
}

void* Button::GetMiscData()
{
    return data_;
}
<<end code>>

And that wraps up our Button class!

Now we are going to write the ButtonManager class.
Create ButtonManager.h and again make an empty class.

We are going to need some methods but what do we need?
Well our button manager class just needs to store a list of buttons,
and update and draw them all. Thats simple.

Here is the full code for the ButtonManager class definition.

<<begin code>>

#ifndef BUTTONMANAGER_H
#define BUTTONMANAGER_H

struct BITMAP;
class Button;

#include <vector>

class ButtonManager
{
public:
    // constructor
    ButtonManager();
    
    // destructor
    ~ButtonManager();

    // updates all the buttons
    void Update();

    // renders all the buttons
    void Render(BITMAP* destination);

    // adds a utton  to the button list
    void AddButton(Button* button);

private:
    // the button list
    std::vector<Button*> buttons_;
};

#endif

<<end code>>


We are going to write the ButtonManager class implementation next.
Create ButtonManager.cpp and lets get this finished!

Add the includes we need.
Allegro, the button manager class def, and the button class def.

<<begin code>>
#include <allegro.h>
#include "ButtonManager.h"
#include "Button.h"
<<end code>>

We don't need to do anything in our constructor, so leave it blank.

<<begin code>>
ButtonManager::ButtonManager()
{
}
<<end code>>

Our destructor needs to free the memory used.

<<begin code>>
ButtonManager::~ButtonManager()
{
    for (unsigned int i = 0; i < buttons_.size(); i++)
    {
        delete buttons_[i];
        buttons_[i] = NULL;
    }
    buttons_.clear();
}
<<end code>>

Adding a button to the button list is very easy.

<<begin code>>
void ButtonManager::AddButton(Button *button)
{
    buttons_.push_back(button);
}
<<end code>>

We now need to update & render the buttons in the list.

<<begin code>>
void ButtonManager::Update()
{
    for (unsigned int i = 0; i < buttons_.size(); i++)
    {
        buttons_[i]->Update();
    }
}

void ButtonManager::Render(BITMAP *destination)
{
    for (unsigned int i = 0; i < buttons_.size(); i++)
    {
        buttons_[i]->Render(destination);
    }
}
<<end code>>

And that my friend finishes it up.

Our final thing that we are going to do is write some code to test our cool new
custom gui buttons. :)

Create MainProgram.h and MainProgram.cpp files and add this code:

In MainProgram.h we are going to create a class that will serve as our application.
ButtonDemo is the name I've chosen for my app.

We need a constructor, a destructor, and two methods. Main and Exit.
Main will take the same parameters as the erquired int main() that all C apps use.
Exit is just a void method that we are using to simply exit the main loop.

Our class needs a boolean to track if the app is running (Exit will set this to false)
We need an Allegro bitmap for our back buffer.
We need a button manager too.

<<begin code>>

#ifndef MAINPROGRAM_H
#define MAINPROGRAM_H

// forward declare the allegro bitmap here so we are not doubly including
// the allegro.h header file!
struct BITMAP;

// forward declare the button manager class
class ButtonManager;

// our main class
class ButtonDemo
{
public:
    ButtonDemo();
    ~ButtonDemo();

    int Main(int argc, char* argv[]);

    void Exit();
private:
    bool appRunning_;
    BITMAP* backBuffer_;
    ButtonManager* buttonManager_;
};

#endif
<<end code>>

Our demo will have 2 buttons, one that will open a message box, and the other
will close the application.
We need to write the callback function prototypes here.

<<begin code>>
// button callbacks
void button1_OnClick(Button* object, void* data);
void button1_OnMouseOver(Button* object, void* data);
void button1_OnMouseOut(Button* object, void* data);

void appCloseButton_OnClick(Button* object, void* data);
<<end code>>

I decided to make a support function that will make fancy text for our messagebox.

Add this code too to finish up MainProgram.h so we can fill out MainProgram.cpp
and finally bring this tutorial to an end.

<<begin code>>
// support function for cool shadowy text in the custom message box
void ShadowPrint(BITMAP* destination, const char* text, int x, int y, int textColor, int shadowColor);
<<end code>>

Okay, now for our MainProgram.cpp file we will include what we need.
Allegro, the button manager class def, the button class def, and finally our
main header file.

<<begin code>>

// include allegro first!
#include <allegro.h>

// include the button manager class
#include "ButtonManager.h"

// include the button class
#include "Button.h"

// include the main program header
#include "MainProgram.h"

<<end code>>

Since I am using Microsoft Visual C++ 2008 Express Edition, I need to toss in a
pragma to link to Allegro and I also use another pragma to shut up MSVC about
a stupid warning #C4100.

<<begin code>>
// link to allegro
#pragma comment(lib, "alleg.lib")

// shut visual c++ up about a warning
// warning C4100: 'blah' : unreferenced formal parameter
// its not a problem, but msvc thinks that it is
#pragma warning(disable:4100)
<<end code>>


Next up our application's constructor code.

<<begin code>>
// our constructor is very simple, just use the initializer list syntax
// to init all the members to some default values
ButtonDemo::ButtonDemo():
buttonManager_(NULL),
backBuffer_(NULL),
appRunning_(true)
{
}
<<end code>>

And the destructor code to free all the used memory.

<<begin code>>
ButtonDemo::~ButtonDemo()
{
    // make sure you remember to hide the mouse cursor!
    show_mouse(NULL);

    // free memory used by the back buffer
    if (backBuffer_ != NULL)
    {
        destroy_bitmap(backBuffer_);
    }

    // free memory used by the button manager
    delete buttonManager_;
}
<<end code>>

The Exit method just sets the boolean member to false like I said.
<<begin code>>
void ButtonDemo::Exit()
{
    // a simple bool will track if the program main loop is running or not
    appRunning_ = false;
}
<<end code>>

Now we get to the meat of our application. The main loop. The whole program really.

First thing we need to do is initialize Alegro and install the Allegro drivers.
Then we set the video mode and create the back buffer Allegro bitmap.
We then create the instance of our button manager, and a button.
We set the properties of the button then add it to the button manager.
We create a second button and set its properties and then tell the button
manager about it too.

Then we get to the main loop that will poll the keyboard if it needs it, and
check for the ESC key to exit the app.
Then we just clear the back buffer, and update the buttons via the button manager,
then draw the buttons via the button manager blit it to the screen and thats it!
Whew!

Here is the code for the Main method ofthe ButtonDemo class.

<<begin code>>
int ButtonDemo::Main(int argc, char* argv[])
{
    // init allegro
    allegro_init();
   
    // install the allegro drivers
    install_timer();
    install_keyboard();
    install_mouse();

    // set the video mode
    set_color_depth(16);
    set_gfx_mode(GFX_AUTODETECT_WINDOWED, 640, 480, 0, 0);

    // create a back buffer bitmap
    backBuffer_ = create_bitmap(640, 480);

    // create a button manager object
    buttonManager_ = new ButtonManager();

    // create a button
    Button* button1 = new Button();

    // set the caption (must be done before Create() is called)
    button1->SetCaption("Click Me!");

    // create the image for the button
    button1->Create();

    // center the button
    button1->CenterOn(backBuffer_);

    // set the button event callback functions
    button1->OnClick        = button1_OnClick;
    button1->OnMouseOver    = button1_OnMouseOver;
    button1->OnMouseOut        = button1_OnMouseOut;
   
    // add the button to the button manager
    buttonManager_->AddButton(button1);

    // create a button
    Button* appCloseButton = new Button();

    // set the caption (must be done before Create() is called)
    appCloseButton->SetCaption("X");

    // set the button text color to red
    appCloseButton->SetTextColor(makecol(255, 0, 0));

    // create the image for the button
    appCloseButton->Create();

    // set the button event callback functions
    appCloseButton->OnClick = appCloseButton_OnClick;

    // position the button in a special way
    // negative positions will be offset from the right and bottom
    // so -8 for the X axis means position the righthand edge of the
    // button 8 pixels from the right edge of the bitmap specified
    // so, here we anchor to the upper right corner of the screen
    appCloseButton->SetPosition(screen, -8, 8);

    // if our button will modify another object, we have to tell it
    // about the data using a void pointer object
    // we need to access the main class to close our program, so
    // we pass the 'this' pointer as our button's data object
    appCloseButton->SetMiscData(this);

    // add the button to the button manager
    buttonManager_->AddButton(appCloseButton);

    // start the program's main loop
    while(appRunning_)
    {
        // poll the keyboard if needed
        if (keyboard_needs_poll())
        {
            poll_keyboard();
        }

        // check for our quit key
        if (key[KEY_ESC])
        {
            // remember this? it simply kills the main loop
            Exit();
        }
       
        // clear the back buffer
        clear_bitmap(backBuffer_);

        // update the buttons via the button manager
        buttonManager_->Update();

        // render (draw) the buttons via the button manager
        buttonManager_->Render(backBuffer_);

        // force the mouse cursor to be visible on the back buffer
        show_mouse(backBuffer_);

        // render (draw) the back buffer on the screen
        blit(backBuffer_, screen, 0, 0, 0, 0, backBuffer_->w, backBuffer_->h);
    }

    // return success
    return 0;
}
<<end code>>

I'm sort of running out of time here, so to make things quick, I'm going to just
quickly tell you what is going on in the next block of code, show it then thats it.

The ShadowPrint function is very simple, and just uses the textprintf_ex Allegro
function multiple times to create a very simple effect of outlining the text.

The button1 Button will change colors when you move your mouse over it, and will
pop up a custom message box at the bottom of the screen when clicked with the
left mouse button.

All the button1_* functions are the calback functions for the events that we
assigned to the event function pointers in the Main method above.

The appCloseButton Button will simply call the main class's Exit method when clicked.

Take note how we use casting to acquire a reference to the data via the void data
pointer that is passed to the button callback functions.
Also notice how we manipulate the button1 button in the OnMouseOver and OnMouseOut
events. Very easy, very clean, very simple.

Finally, here is the code.

<<begin code>>
void ShadowPrint(BITMAP* destination, const char* text, int x, int y, int textColor, int shadowColor)
{
    /*

    makes prettier text by creating a fake shadow

    sss
    sns  s - shadow color text
    sss  n - normal color text

    */
    textprintf_ex(destination, font, x-1, y-1, shadowColor, -1, text);
    textprintf_ex(destination, font, x, y-1, shadowColor, -1, text);
    textprintf_ex(destination, font, x+1, y-1, shadowColor, -1, text);
    textprintf_ex(destination, font, x-1, y, shadowColor, -1, text);
    textprintf_ex(destination, font, x+1, y, shadowColor, -1, text);
    textprintf_ex(destination, font, x-1, y+1, shadowColor, -1, text);
    textprintf_ex(destination, font, x, y+1, shadowColor, -1, text);
    textprintf_ex(destination, font, x+1, y+1, shadowColor, -1, text);

    textprintf_ex(destination, font, x, y, textColor, -1, text);
}

// a button event callback function
void button1_OnClick(Button* object, void* data)
{
    // alert("Button clicked!", "", "Yay!", "OK", NULL, KEY_ENTER, KEY_ESC);

    BITMAP* backBuffer = create_bitmap(420, 96);
    BITMAP* messageBox = create_bitmap(420, 96);
   
    clear_bitmap(backBuffer);
    clear_bitmap(messageBox);
    rectfill(messageBox,0,0,messageBox->w, messageBox->h, makecol(192,192,192));
    rect(messageBox,0,0,messageBox->w, messageBox->h, makecol(64,64,64));

    ShadowPrint(messageBox, "The button was clicked. Press spacebar to close.",
        8, 8, makecol(0,128,255), makecol(0,0,64));

    // messagebox loop
    while(!key[KEY_SPACE])
    {
        clear_bitmap(backBuffer);
        blit(messageBox, backBuffer, 0, 0, 0, 0, messageBox->w, messageBox->h);
        show_mouse(backBuffer);
       
        blit(backBuffer, screen, 0, 0,
            SCREEN_W/2 - messageBox->w/2,
            SCREEN_H - 96, backBuffer->w, backBuffer->h);
    }

    // clean up
    show_mouse(NULL);
    destroy_bitmap(messageBox);
    destroy_bitmap(backBuffer);
}

// a button event callback function
void button1_OnMouseOver(Button* object, void* data)
{
    // change border color to green
    object->SetBorderColor(makecol(0,255,0));

    // change face color to dark green
    object->SetFaceColor(makecol(0,64,0));

    // change text color to yellow
    object->SetTextColor(makecol(255,255,0));

    // apply changes by recreating the button image
    object->Recreate();
}

// a button event callback function
void button1_OnMouseOut(Button* object, void* data)
{
    // change border color to default
    object->SetBorderColor(makecol(96,96,96));

    // change face color to default
    object->SetFaceColor(makecol(192,192,192));

    // change text color to default
    object->SetTextColor(makecol(0,0,0));

    // apply changes by recreating the button image
    object->Recreate();
}

// a button event callback function
void appCloseButton_OnClick(Button* object, void* data)
{
    // access the main program class
    ButtonDemo* app = (ButtonDemo*)data;

    // and call Exit()
    app->Exit();
}
<<end code>>

The last thing you need is the required int main() function to compile.
We make an instance of our application class and return the return value of the Main
method that we created. Simple.

<<begin code>>
// program entry point - good ole main()
int main(int argc, char* argv[])
{
    // create an instance of our main class
    ButtonDemo program;

    // return the result of our Main method
    return program.Main(argc, argv);
}
END_OF_MAIN(); // allegro needs this to work its magic
<<end code>>

Reread this tutorial, download the source, look over it, download and run the demo.
And contact me if you have questions, comments, etc.

Note: You will need alleg42.dll to run the demo.

Thanks for reading!

Sincerely, Richard Marks

ccpsceo@gmail.com

Downloads:

Download Source Code (.zip)

Download Binary Executable (.zip)