 | Level: Intermediate George Lebl (jirka@5z.com), developerWorks columnist, Eazel, Inc.
01 Jan 2000 GNOME Canvas is a powerful graphics tool that can help you present data, build games, and much more. Following up his previous columns in which he showed you how to build the code and a GUI for a basic genealogy program, developerWorks columnist George Lebl introduces you to GNOME Canvas by constructing a simple graphical game in this tutorial.
Last month you learned how to store things on disk with XML (see Resources later in this article). In this month's installment, we look at how to present your data graphically using the GNOME Canvas. But the canvas doesn't have to be used for graphics only; if the standard widgets don't fit your needs, you may want to use the canvas. For example, the gnumeric spreadsheet uses the canvas instead of a widget to draw the actual spreadsheet. However, we will work with a slightly more fun example: a game called Piskvorky. OK, that's not fair -- that's the Czech name. In English it's usually called Gomoku. It's basically tic-tac-toe played on a larger board, and you have to connect 5 of your marks to win. We will not put any intelligence into the code, and we'll let the user play both sides. Here is how the game looks: Piskvorky game

Before we turn to the game, I need to explain what the GNOME Canvas is and how it works. The GNOME Canvas is a flicker-free, fast, and easy means to use widgets for drawing high-quality graphics; it is based partly on the Tk canvas widget. It uses the GTK+ object system for everything, so every graphical element is a GTK+ object. It can work with arbitrary units, but we'll just use pixels as they're easier to manage for most things. The canvas works somewhat like the GTK+ widget set. You divide canvas items into two categories: groups and items. Groups are invisible objects that just know their top left coordinate. You put other items inside groups, and those items use the origin of that group as their coordinate system. This makes it easy to write a routine that draws something at set coordinates onto a group, and then when you want to draw it somewhere you just make a group where you want the drawing to be done, and then put your items onto that group. You can also move the group later, or destroy it, thus destroying every element inside it. You can nest groups as well. The canvas items use the GTK+ argument system for setting their parameters. (The full listing is available at the end of this paper.) The GTK+ argument system works by having named arguments that you pass along with your value to a function using variable argument lists. So, for example, to set the "fill_color" argument to "red" and the "width_units" argument to 8, you would use:
gnome_canvas_item_set(GNOME_CANVAS_ITEM(item),
"fill_color","red",
"width_units",(double)8,
NULL); |
This way you can pass any number of arguments to the function, and what you are doing is immediately clear from the call. However, because there is no way that C can tell what types the function requires, it cannot cast your values. This means that you should always cast your values unless you are sure that you are passing the correct type. It is wise to check the documentation to make sure that you have the correct types. A good rule of thumb is that whenever you need to pass a number (unless that number specifies pixels), it will almost certainly be a double-- however, it is still better to check. You can find the list of the arguments in the documentation of the particular item in the Gnome User Interface Library Reference Manual. Be sure to check the arguments that come from inheritance too. For example, the rectangle (GnomeCanvasRect) is inherited from GnomeCanvasRE, which is a base class for the rectangle and the ellipse, and it provides all the arguments that you will use for a rectangle or an ellipse. To see which objects a particular object inherits from, look at the "Object Hierarchy" section of the item documentation page in the Gnome User Interface Library Reference Manual. Creating a new item
Let's see how to create a new item with the gnome_canvas_item_new function. You pass it the group to which this item will belong to. This is unlike GTK+, where an item is only later put into a container. You also pass the GTK+ type number of the object you are creating and a list of arguments just as with the gnome_canvas_item_set above. The GTK+ type number is obtained by calling the _get_type function of the particular object you want to create; for example, for a rectangle (GnomeCanvasRect), you call gnome_canvas_rect_get_type(). For the group, if you want to put the item directly onto the canvas, or if you are creating a group that will be directly on the canvas, you use the root group. To get the root group pointer, you call the gnome_canvas_root method. As groups are just another canvas items, you treat them as such. So to create a new group at x,y, you would do the following:
GnomeCanvasItem *item;
item = gnome_canvas_item_new(gnome_canvas_root(canvas),
gnome_canvas_group_get_type(),
"x",(double)x,
"y",(double)y,
NULL); |
Note that you don't have to "show" the item either, as it will be shown by default; this is again unlike GTK+ widgets. We are now ready to start creating the application. We have already seen how to do the basic GUI in the first couple of examples, so we don't need to worry about that. All we need to worry about is the canvas and the game details. I have defined all the constants at the top of the source, so that you can fiddle with them to change the look of the game board if you don't like the defaults. We also make an enum of 3 things, NOTHING, CROSS, and CIRCLE. This is used to indicate the current player and the contents of any cell on the board. The board is just a BOARD_SIZE by BOARD_SIZE array of integers, where BOARD_SIZE is 19 by default. We also store the current_player, the move_number, and the last_winner (I think those variable names are self explanatory). We also store a few more things as globals. We store a pointer to our main window (app), the pointer to the canvas (canvas), a pointer to a canvas group where we put all the marks, the crosses and the circles (boardgroup), and a pointer to the status text (status_text) canvas item and the status image group (status_image). Now we come to our first function, draw_a_line. This is just a convenience function for drawing simple lines instead of complex shapes or polylines, since all the lines we draw are just such simple lines. The thickness is uniform throughout, so we don't pass it as a parameter. Its prototype is:
static void draw_a_line(GnomeCanvasGroup *group,
int x1, int y1, int x2, int y2,
char *color); |
To draw lines with the canvas, you don't just specify beginning and end points. You can specify an array of points that will be connected. However, we want only single lines. You generate a new array by calling gnome_canvas_points_new with the number of points you want as the argument. Then you use the 'coords' member of the resulting structure as an array, putting your coordinates as x1,y1,x2,y2,x3,y3,... The code that draws a single line on the group 'group', from x1,y1 to x2,y2 of color 'color', which is a string, and of thickness THICKNESS, which is a number, looks like this:
GnomeCanvasPoints *points;
/* allocate a new points array */
points = gnome_canvas_points_new (2);
/* fill out the points */
points->coords[0] = x1;
points->coords[1] = y1;
points->coords[2] = x2;
points->coords[3] = y2;
/* draw the line */
gnome_canvas_item_new(group,
gnome_canvas_line_get_type(),
"points", points,
"fill_color", color,
"width_units", (double)THICKNESS,
NULL);
/* free the points array */
gnome_canvas_points_free(points); |
Note that we don't bother to store the pointer to the line object as we will not be needing it. When we wish to destroy it, we will always destroy its group, which will destroy all its children including this line. Also, you could reuse the points array if you'd like. You might choose to do this if, for instance, you are drawing a point in a loop and that would make a more efficient code. But often efficiency is not really necessary, and it sometimes makes the code harder to read, so don't worry about optimizing away things like this unless you know that they are done frequently or that they represent a significant runtime cost. We also need to draw the background of the board, which includes the lines dividing the cells, a white back drop for the board, and a status text item. This is done in the draw_background function, which is called from our main function. The white rectangle is drawn first by using:
/* white background */
gnome_canvas_item_new(gnome_canvas_root(canvas),
gnome_canvas_rect_get_type(),
"x1",(double)BOARD_BORDER,
"y1",(double)BOARD_BORDER,
"x2",(double)(BOARD_BORDER +
BOARD_SIZE*CELL_SIZE),
"y2",(double)(BOARD_BORDER +
BOARD_SIZE*CELL_SIZE),
"fill_color", "white",
NULL); |
We draw this on the root group, and the rectangle will be filled (and without an outline as we have only set "fill_color"). Now to draw the grid, we loop through 0 to BOARD_SIZE and draw lines using the above draw_a_line function. We again draw these on the root group, and the code looks like:
int i;
/* draw horizontal lines */
for(i=0;i<=BOARD_SIZE;i++) {
draw_a_line(gnome_canvas_root(canvas),
BOARD_BORDER,
BOARD_BORDER + i*CELL_SIZE,
BOARD_BORDER + BOARD_SIZE*CELL_SIZE,
BOARD_BORDER + i*CELL_SIZE,
"black");
}
/* draw vertical lines */
for(i=0;i<=BOARD_SIZE;i++) {
draw_a_line(gnome_canvas_root(canvas),
BOARD_BORDER + i*CELL_SIZE,
BOARD_BORDER,
BOARD_BORDER + i*CELL_SIZE,
BOARD_BORDER + BOARD_SIZE*CELL_SIZE,
"black");
} |
Next we need to draw a status text item. For now we want it to be empty, but we can create it and set all its attributes now. The code looks like:
/* draw the status text */
status_text = gnome_canvas_item_new(gnome_canvas_root(canvas),
gnome_canvas_text_get_type(),
"x",(double)BOARD_BORDER,
"y",(double)(BOARD_SIZE*CELL_SIZE +
BOARD_BORDER +
BOARD_STATUS/2),
"anchor",GTK_ANCHOR_WEST,
"font","fixed",
"fill_color", "black",
"text","",
NULL); |
The x is set to the border size so that the text is flush with the board, and the y is the bottom edge of the board, plus half of BOARD_STATUS, which defines the bottom border. We set the y to be in the middle of the area where we want the text to appear. The "anchor" tells us what point we actually specified above. It can be all the directions, including the corners such as GTK_ANCHOR_NW, or it can also be GTK_ANCHOR_CENTER, and it specifies the point on an imaginary box surrounding the text. We want the text to be centered on the y coordinate and to start on the x coordinate so we define the anchor to be on the west side of the imaginary box. We choose the font to be "fixed" and the color of the text (the "fill_color") to be black. We also set the text to be an empty string. We then set status_image to be NULL, as this will be used later in the set_status function, and it will be used to display the current player mark.
Drawing the game pieces
We now come to functions that draw a cross and a circle. We also draw a drop shadow that is dark gray and offset by DROP_SHADOW pixels in both x and y directions. Using the draw_a_line function, the code that draws the cross including the drop shadow is below:
/* draw the drop shadow */
draw_a_line(group,
CELL_PAD + DROP_SHADOW,
CELL_PAD + DROP_SHADOW,
CELL_SIZE - CELL_PAD + DROP_SHADOW,
CELL_SIZE - CELL_PAD + DROP_SHADOW,
"dark gray");
draw_a_line(group,
CELL_PAD + DROP_SHADOW,
CELL_SIZE - CELL_PAD + DROP_SHADOW,
CELL_SIZE - CELL_PAD + DROP_SHADOW,
CELL_PAD + DROP_SHADOW,
"dark gray");
/* draw the cross itself */
draw_a_line(group,
CELL_PAD,
CELL_PAD,
CELL_SIZE - CELL_PAD,
CELL_SIZE - CELL_PAD,
"blue");
draw_a_line(group,
CELL_PAD,
CELL_SIZE - CELL_PAD,
CELL_SIZE - CELL_PAD,
CELL_PAD,
"blue"); |
As you see, we don't care where the cross will actually go: we draw in absolute coordinates for the group. Later, the group can be placed anywhere on the canvas. The CELL_PAD is a constant defining the space between the edge of a cell and the cross itself, and CELL_SIZE is the total cell size. Drawing the circle is even simpler. A circle is really just an ellipse where the width and the height are the same, so we just use GnomeCanvasEllipse object:
/* draw the drop shadow */
gnome_canvas_item_new(group,
gnome_canvas_ellipse_get_type(),
"x1",(double)(CELL_PAD + DROP_SHADOW),
"y1",(double)(CELL_PAD + DROP_SHADOW),
"x2",(double)(CELL_SIZE - CELL_PAD + DROP_SHADOW),
"y2",(double)(CELL_SIZE - CELL_PAD + DROP_SHADOW),
"outline_color", "dark gray",
"width_units", (double)THICKNESS,
NULL);
/* draw the circle itself */
gnome_canvas_item_new(group,
gnome_canvas_ellipse_get_type(),
"x1",(double)CELL_PAD,
"y1",(double)CELL_PAD,
"x2",(double)(CELL_SIZE - CELL_PAD),
"y2",(double)(CELL_SIZE - CELL_PAD),
"outline_color", "dark green",
"width_units", (double)THICKNESS,
NULL); |
Note the "width_units" argument. We could also use "width_pixels", which is a guint type argument and always specifies the width in pixels, no matter what your units. Since we use pixels as units, the relation is one-to-one. But note that "width_units" takes a double, while "width_pixels" takes an unsigned int. Another drawing function is the draw_strike function. This draws a red line between centers of two cells by calling the draw_a_line function. This is used to mark a winning line of 5 cells.
The game
Now that we are done with the simple drawing routines, we can go on to the game stuff. We define a function make_mark_at, which takes the mark type as an integer (CROSS or CIRCLE) and an x and a y. The function creates a group located at x*CELL_SIZE and y*CELL_SIZE. It then calls the draw_cross or draw_circle with this group to draw the proper mark into the group. It also sets the location in the board array to the mark. This will, of course, work correctly only if that location was previously empty, but we do make sure of this in our caller function below, so we can assume that it was empty. The function looks like this:
GnomeCanvasItem *group;
/* update our data */
board[x][y] = mark;
/* make a new group for the mark */
group = gnome_canvas_item_new(boardgroup,
gnome_canvas_group_get_type(),
"x",(double)x*CELL_SIZE,
"y",(double)y*CELL_SIZE,
NULL);
if(mark == CROSS)
draw_cross(GNOME_CANVAS_GROUP(group));
else
draw_circle(GNOME_CANVAS_GROUP(group)); |
Now that we are this far, we should look at the play_location function, which actually calls make_mark_at. |