|
|
|
| GUI Clinic Home |
||||||||||||||||||||||||||||||||
| How Can I Upgrade the GUI? | Messages Enhancing Existing Controls | Creating New Controls Creating New Flags & Messages |
||||||||||||||||||||||||||||||||
Previous: Modifying GUI Behavior So you're getting a handle on the Allegro GUI. You've created some demos that have helped you understand it better. You're becoming quite good. But there's one problem: you absolutely can't stand the way it looks. Isn't there a way to change it? Or try this: you have this program you've been working on. Things are going great. Your new knowledge is enabling you to do bigger and better things. But having only sixteen controls is just so darn limiting! Isn't there a way to add to the system without hacking the library? Thankfully, the answer to both questions is a resounding yes! The GUI is quite extensible and modifiable. You only need to know how to extend and modify it. Well, that's what this document is for. Upgrading the GUI is not hard once somebody tells you how to do it. It's really quite simple. Basically, you extend the GUI by creating new dialog procedures. Remember, d_button_proc, d_keyboard_proc, and so on are only functions that adhere to a certain interface standard: the "dialog procedure standard." There's nothing stopping anyone from creating new dialog procedures. In fact, it isn't always that drastic. Many times, all you need to so is respond to a new message (or handle an existing message in a new way). In such a case, you don't need to create a whole new object; you can simply build upon an existing one. But I'm getting ahead of myself. What are messages? How do you respond to them? Let's find out...
Messages are Allegro's way of communicating with the dialog objects. The GUI system runs through a loop, processing input. When it decides that something needs to be done, it tells any objects concerned what they need to do by passing them different messages. The objects then respond to that message by executing whatever code is attached to that message. This is what the dialog procedure is for. It is the interface between an object and the GUI. Generally, this function isn't much more than a big switch statement that looks at the message that the object is receiving, then takes any necessary action. But what data does the dialog procedure operate on? Well, remember, it must follow the form: int my_func_name(int msg, DIALOG *d, int c) This was discussed more thoroughly in the section, Building and Using Dialogs, but to quickly review: the parameters, from left to right, are as follows: msg, which contains the message; d, which is a pointer to the DIALOG object concerned (not the whole dialog array); and c, which is usually ignored, but holds message-dependent information such as the key that was pressed (for a keypress message) or the radio button group (for a radio button). The possible return values from the dialog procedure are also discussed in Building and Using Dialogs. So, what messages does the GUI send? Well, there are fifteen messages documented in the Allegro documentation. Here's the rundown (paraphrased from the docs): MSG_START MSG_END MSG_DRAW MSG_CLICK MSG_DCLICK MSG_KEY MSG_CHAR MSG_XCHAR MSG_WANTFOCUS MSG_GOTFOCUS MSG_LOSTFOCUS MSG_GOTMOUSE MSG_LOSTMOUSE MSG_IDLE MSG_RADIO There you have it. All fifteen predefined GUI messages. Every dialog procedure you build will probably respond to one or more of these messages. The GUI sends them, and you catch them. But can you send your own messages? Yes! You can manually throw messages around by calling: SEND_MESSAGE(d, msg, c) The first parameter is the dialog object (not a whole array) that you wish to send the message to. The second parameter is the message, and the third parameter is the "c" parameter to the dialog procedure for the object (which can be anything if the object won't be using it). For example, to make an object named my_button draw itself, you'd say: SEND_MESSAGE(my_button, MSG_DRAW, 0); Easy as pie! Easier, in fact. You don't need to worry about the crust getting flaky and brittle... But I promised that this section would cover methods of extending the GUI. With your (possibly new) knowledge of messages, we'll dive right in. Let's learn how to enhance the prepackaged controls.
Since most of the predefined GUI controls do everything right already, the usual thing people do when they extend a control is simply make it look different. That way, it fits with the look of their application. That's probably one of the simplest ways to extend a control, so let's look at how to do that. I wonder if there's an example of that somewhere... Well, it turns out that the check box control (d_check_proc) is the absolute perfect example of what we're trying to do here. Remember the description of the d_button_proc control? It was simply a button that toggles on and off when it's clicked. But that's what a check box does! Isn't that just useless duplication of code? Actually, it's not. Take a look at the dialog procedure for the standard check box (formatted to fit the window size, which makes it really ugly; don't worry about completely understanding it: just try to figure out what it does in general), from allegro/src/guiproc.c:
/* d_check_proc:
* Who needs C++ after all? This is derived
* from d_button_proc, but overrides the drawing
* routine to provide a check box.
*/
int d_check_proc(int msg, DIALOG *d, int c)
{
int x;
int fg;
if (msg==MSG_DRAW) {
fg = (d->flags & D_DISABLED) ? gui_mg_color
: d->fg;
text_mode(d->bg);
x = d->x +
gui_textout(screen, d->dp, d->x,
d->y+(d->h-(text_height(font)-
gui_font_baseline))/2,
fg, FALSE) + text_height(font)/2;
rectfill(screen, x+1, d->y+1,
x+d->h-1, d->y+d->h-1, d->bg);
rect(screen, x, d->y, x+d->h, d->y+d->h, fg);
if (d->flags & D_SELECTED) {
line(screen, x, d->y,
x+d->h, d->y+d->h, fg);
line(screen, x, d->y+d->h,
x+d->h, d->y, fg);
}
if (d->flags & D_GOTFOCUS)
dotted_rect(x+1, d->y+1, x+d->h-1,
d->y+d->h-1, fg, d->bg);
return D_O_K;
}
return d_button_proc(msg, d, 0);
}
As you can see, this is a pretty short dialog procedure. But look at what it does: first, it tests whether or not the message it is receiving is a MSG_DRAW. If it is, it draws itself. It's appearance is based on the flags, positional, etc... characteristics given in its DIALOG structure (pointed to by the "d" parameter). Notice that its appearance changes somewhat when it is disabled, selected, or has the input focus. If the message isn't MSG_DRAW, however, it simply chains itself to the d_button_proc dialog procedure. This way, it handles clicks, mouse movement, queries for focus, and so on exactly the way a button would. After all, they're using the same code! And since MSG_DRAW messages are already trapped (after which the procedure returns D_O_K), we never have to worry about the normal button-drawing code conflicting with our own! This is a powerful way to work. It results in the ability to derive objects from other objects, superficially like the way a C++ class can derive from another class (only it also works with C). It's quite cool; the possibilities are almost endless.
Sometimes, though, simply extending the functionality of existing controls isn't enough for what you want to do. In these cases, you need to create your own controls. This isn't a difficult process. As with most programming, the key is preparation. Know exactly how your control will act under any circumstances. Know what it must do in response to the messages that the GUI will send its way. Knowing your control's behavior to a tee will make writing the code easy. The process for creating a new controls is a lot like the process for extending an existing control. First, you design it. Then, you create the new dialog procedure. The only difference here is that the new procedure won't chain itself to an existing one; the control will be a self-contained unit. Sounds like time for another example. So, what should we create? How about a drop-down list box? Designing the Drop-Down List So what should this control do? Its position, size, and colors are all given in the DIALOG structure already, so we don't need to worry about those. We will want to have a list of items, and some way to keep track of which item is selected. It would probably be a good idea to create a table of possible messages and decide what this control should do in response to each.
As you can see, not every message is acted upon. In fact, it is a rare object that cares about every single message. Coding the Drop-Down List The basic skeleton for our control will look like this. Notice that we only check for the messages that we're worried about: int d_drop_down_proc(int msg, DIALOG *d, int c)
{
switch (msg)
{
case MSG_DRAW:
break;
case MSG_CLICK:
break;
case MSG_CHAR:
break;
case MSG_WANTFOCUS:
break;
}
return D_O_K;
}
As you can see, most messages are ignored. It's a rare object that actually cares about every message. The object needs a list of items. Let's make that an array of strings. We'll terminate the array with a NULL string. The dp field seems like a good way to access this data, so we'll use it. Also, let's use d1 as an index into the array. It'll tell us which item is selected. We'll also need to reserve d2 for our own use as well. You'll see why in a minute. So, here's how the object will operate: when it initializes, we won't need to do anything special. When it draws itself, we'll use the D_SELECTED flag to tell us if the list is dropped down or not, and we'll draw the object accordingly. When the user clicks the control, it'll set this flag and tell itself to redraw (it'll know which item to highlight based on the contents of the d1 field). When the user presses the up arrow, the down arrow, Home, or End, the list selection will change without needing to drop down the list. Good enough. Let's take the procedure apart and see how it works. You'll probably find the source easier to read than the code that follows, because of formatting. Before we decide what to do with any GUI messages we receive, we run through some calculations that a few of the messages need, so we don't duplicate code needlessly: /* d_drop_down_proc
*
* A drop-down list box. The dp parameter should
* point to an array of strings to use for the
* list items. The array is terminated by a NULL
* entry. d1 holds the current selection. d2 is
* reserved.
*/
int d_drop_down_proc(int msg, DIALOG *d, int c)
{
int num_items = 0;
char **items = d->dp;
int fgcol, bgcol;
int old_x=-1, old_y=-1;
int mx, my;
int bot;
int text_y, ry;
int ht;
int i;
/* get the number of items */
if (items != NULL) {
while (items[num_items++] != NULL)
;
}
num_items--;
/* clamp the value of the selection index */
if (d->d1 >= num_items)
d->d1 = num_items - 1;
ht = text_height(font) + 4;
bot = d->y + d->h + (ht*num_items);
fgcol = (d->flags & D_DISABLED) ? gui_mg_color
: d->fg;
bgcol = d->bg;
As you can see, we find the number of items in the list and figure out a few other commonly-used values. There's really not much here. Notice that the "items" pointer is a pointer-to-pointer-to-char, and is set to whatever dp points to. This lets us iterate through the list of strings. Next, let's look at the drawing code for our object, which is executed when it receives a MSG_DRAW message: switch (msg)
{
case MSG_DRAW:
if ( !(d->flags & D_HIDDEN) ) {
rect(screen, d->x, d->y,
d->x+d->w, d->y+d->h, fgcol);
/* invert the colors */
if (d->flags & D_GOTFOCUS) {
i = fgcol;
fgcol = bgcol;
bgcol = i;
}
rectfill(screen, d->x+1, d->y+1,
d->x+d->w-1, d->y+d->h-1, bgcol);
text_mode(-1);
text_y = d->y + (d->h/2) -
(text_height(font)/2)+1;
gui_textout(screen, items[d->d1],
d->x+6, text_y, fgcol, 0);
/* if we've got the focus */
if (d->flags & D_GOTFOCUS) {
for (i=d->x+2; i
This is the most complicated part of this object. First, it checks that the object isn't hidden (for obvious reasons). Then it draws the control by drawing a box and drawing the text for the selected item. If the object has the input focus, it draws a dotted "focus rectangle" on the object. Having the focus also inverts the colors. Following that, the control decides whether or not to draw itself in dropped-down style by checking the D_SELECTED flag. If it is dropped down, it draws each list item, selecting the one in d2 (you'll see why next). Okay. Let's handle mouse clicks: case MSG_CLICK:
if ( !(d->flags & D_HIDDEN) &&
!(d->flags & D_DISABLED) ) {
d->d2 = -10000; /* below lowest possible */
/* only works with the left mouse button */
while (gui_mouse_b() & 0x01) {
mx = gui_mouse_x();
my = gui_mouse_y();
if (((mx != old_x) || (my != old_y)) &&
((mx > d->x) && (mx < d->x+d->w) &&
(my > d->y) && (my < bot ))) {
old_x = mx;
old_y = my;
if (d->d2 != ((my-d->y-d->h)/ht)) {
if (d->flags & D_SELECTED) {
if (my >= d->y+d->h) {
d->d2 = ((my-d->y-d->h)/ht);
}
} else {
d->d2 = d->d1;
}
/* signals a dropped-down state */
d->flags |= D_SELECTED;
scare_mouse();
/* redraw in the dropped-down position */
SEND_MESSAGE(d, MSG_DRAW, 0);
unscare_mouse();
}
}
}
/* return to non-dropped-down state */
d->flags &= ~D_SELECTED;
/* finalize the selection */
if ((mx > d->x) && (mx < d->x+d->w) &&
(my > d->y) && (my < bot ))
d->d1 = d->d2;
return D_REDRAW;
}
break;
Again, the control checks that it is visible. This time, it also verifies that it isn't disabled. We don't want it to be actived when it is disabled! Next, it sets the d2 parameter to an impossibly low value. Why? Well, the drop-down list box uses d1 to hold the index of the currently selected item. But, when the mouse is being dragged on a dropped-down list, we also need to keep track of which list item is currently highlighted. That's why we've reserved d2. While the left mouse button is being held down, the object continually checks for mouse movement inside the dropped-down part. We don't care about any movement outside the list, since it won't change anything as far as our control is concerned. When we do get movement, we calculate which item should be highlighted and set the D_SELECTED flag. Then, we call the SEND_MESSAGE macro on ourselves to tell us to redraw in the dropped-down position, with correct highlighting. Since we're sending the MSG_DRAW message ourselves, we need to hide the mouse first. Once the mouse button is let go, we clear the D_SELECTED flag and if the user let go of the button while on top of the object, change the selection. Then we return D_REDRAW to let the GUI know it needs to redraw the entire dialog. Finally, we handle keypresses and accept the input focus: case MSG_CHAR:
if ( !(d->flags & D_HIDDEN) &&
!(d->flags & D_DISABLED)) {
i=FALSE;
if ((c >> 8) == KEY_UP) {
if (d->d1 > 0)
d->d1--;
i=TRUE;
} else if ((c >> 8) == KEY_DOWN) {
if (d->d1 < num_items)
d->d1++;
i=TRUE;
} else if ((c >> 8) == KEY_HOME) {
d->d1 = 0;
i=TRUE;
} else if ((c >> 8) == KEY_END) {
d->d1 = num_items;
i=TRUE;
}
d->d2 = d->d1;
scare_mouse();
SEND_MESSAGE(d, MSG_DRAW, 0);
unscare_mouse();
if (i)
return D_USED_CHAR;
}
break;
case MSG_WANTFOCUS:
return D_WANTFOCUS;
}
return D_O_K;
}
This last part of the dialog procedure is pretty simple. If the control isn't hidden or disabled, it checks the high byte of the key code (which holds the key's scan code) for acceptable values (Up, Down, Home, or End). If one is found, we update the selection index (in d1) and redraw the drop-down list box with the new selection. To show that we used the key, we return D_USED_CHAR. For MSG_WANTFOCUS, we just return D_WANTFOCUS. This allows Allegro to give our object the input focus. If the routine hasn't already returned, the last thing it does is to return D_O_K. Then, it can get on with its life. So there you have it. A drop-down list box. It has its flaws, but this should be enough of an example to show how to create a new dialog object. All a dialog procedure needs to do is respond to different messages by manipulating the data encapsulated in a DIALOG structure. For the source to an example program that uses d_drop_down_proc, you can download dropdown.c. Now you know how to extend the Allegro GUI to the limits of your imagination. But wait! There's more! Not only can you create new objects, but you can also create new flags and messages for your objects to use! But how?
Creating new flags and messages is probably one of the simplest things you can do with the GUI, so this shouldn't take long. Flags Say your object has a couple of different ways to display itself--say "with shadow" and "without shadow." Instead of using up one of your precious few DIALOG structure members, you can create a D_HAS_SHADOW flag, and check its value to know whether or not to draw the shadow. Or maybe you have a routine that will move a dialog around on the screen, by iterating through an array of DIALOG structures and offsetting the x and y variables of each object. The problem is, certain objects need to stay in the same place. Simple! Create a D_UNMOVABLE flag, and have the movement function check its value first. As you can see, there are lots of possibilities here. Any boolean values that can be thought of as characteristics of an object can be changed into a flag. So how is this done? Allegro defines a special flag called D_USER. It is the first flag available for all-purpose use. All it takes to create a new flag is a simple #define: #define D_HAS_SHADOW D_USER Every new flag you want to create after that is simply another power of two above D_USER: #define D_UNMOVABLE (D_USER << 1) #define D_ANOTHER_FLAG (D_USER << 2) #define D_THIRD_FLAG (D_USER << 3) You get the idea. Then, with an object, you can manipulate the new flag just as you would any other flag: /* set the flag */
object->flags |= D_HAS_SHADOW;
/* clear the flag */
object->flags &= ~D_HAS_SHADOW;
/* test the flag and branch accordingly */
if (object->flags & D_HAS_SHADOW)
draw_with_shadow();
else
draw_without_shadow();
See how easy it is? Just make sure that you don't create too many flags, or else they'll overflow the flags variable. Messages For instance, you may have an object that can scroll up and down. Normally, it scrolls in response to a mouse click on the scrollbar on its side. But it would also be nice for the object to scroll when the user presses one of the arrow keys. It would be easy to just paste the scrolling code into the MSG_CLICK and MSG_CHAR handlers, but that's an unnecessary waste. Why not create a MSG_SCROLL handler? Then, whenever you want your object to scroll, send it this message. No waste of code! This concept comes in handy if you change the internals of the object. Say you want to consolidate a few DIALOG fields into a structure that dp points to. Instead of having to re-write every piece of code that uses those variables, you'll just need a recompile. If all of your communication with an object is by messages, it doesn't really matter what's inside the object--you can change it all you want, and the interface to the object is the same (like the object-oriented concept of encapsulation). The object is truly self-contained. So how do you create these messages? You create new messages much in the same way you create new flags; just #define it: #define MSG_SCROLL MSG_USER Other messages are created by continually adding 1 to MSG_USER: #define MSG_SAVE (MSG_USER+1) #define MSG_LOAD (MSG_USER+2) #define MSG_ANOTHER (MSG_USER+3) And so on. To make an object respond to a new message, simply handle that message the same way you handle MSG_DRAW or MSG_CLICK: test for it in the dialog procedure. Piece of cake. Whew! There's a lot to extending the GUI system, isn't there? Don't worry if you don't understand it all right now; practicing with small objects helps tremendously. And a look at the source (allegro/src/guiproc.c) never hurt anyone... The GUI system also contains a few helper functions that make life a little easier. We'll look at those in the next section. Next: Completing the Journey |