Skip to main content

skip to main content

developerWorks  >  Linux  >

GNOMEnclature:: Making application programming easy with GNOME libraries, Part 2

Building a sample genealogy program

developerWorks
Document options

Document options requiring JavaScript are not displayed


Rate this page

Help us improve this content


Level: Intermediate

George Lebl (jirka@5z.com), developerWorks columnist, Eazel, Inc.

01 Nov 1999

Welcome to the first edition of GNOMEnclature, the first installment of GNOME project member George Lebl's new monthly column for the Linux Zone! George will be covering all things GNOME (MDI development using GnomeMDI, writing GNOME Panel applets, using Glade and libglade, using the GNOME Canvas, and more) and a few things that aren't. This month, George takes you step by step through the process of building a simple genealogy program, expanding on his previous article detailing the construction of a simple "hello world" application, and explaining everything along the way.

In my last article you found out how to program a very useless "hello world" application. Now it's time to see how to develop a real-world application. We'll start by developing a somewhat real-world application (kept simple for the sake of documentation) and extend it in subsequent articles. The application we'll be writing is a genealogy application for entering family trees. As the articles progress, we'll intermingle discussions of GLib, GTK+, and GNOME as needed.

First let's look at the data structure we'll be using. A real-world genealogy application deals with a complex structure, but we'll deal only with a simple tree. We won't allow any siblings in the tree, so each node will have only two other nodes as parents. The usual concept of what is child and what is parent in binary trees is reversed in this particular application, which can be confusing, so let's use this convention: Unless I specify otherwise, I'm referring to the real world rather than to the abstraction. In other words, a "parent" is the parent of the person specified by the node, not the parent node of a binary tree.

So, a person will have two parents, which are stored as the children of the node. For this tree we will use the GNode, n-ary tree implementation by glib. It's fairly simple to use, and we use only a small subset of its functionality. Also sometimes in one dataset we might want to have several unrelated persons. As a result, there will be several trees, and thus we will store them as a GList linked list. To define a person, we will define a Person structure type, which has three character fields: one for name, one for date of birth, and one for date of death. The whole structure will look like this:


The structure of our family tree

The crosses indicate NULL pointers, and an ellipse with a structure name directly above another one has its pointer in the data field of the lower structure. You can see that we have a normal, doubly linked list of GList structures, which in their data field have a pointer to a GNode structure, which in turn has in its data field a pointer to the Person structure. The GNode is an n-ary tree implementation, and so the node has only a single pointer to its children, which in turn have a "next" pointer that points to their next sibling. They also contain a "parent" pointer, which allows you to browse the entire tree no matter where you start. We will store a global pointer to the linked list of trees in a variable named "trees."

See the complete listing for this program.

Building the UI

Now let's design the user interface. The basic parts were described in the last article, so we won't cover them again. As in the hello world example of the last article, we'll make a single-document, single-window application. This model allows you to do many quick solutions, which are fine for a small application, but can prove deadly when you try to extend the application, if it wasn't designed correctly. We should always keep the data and the UI separate. We should also keep in mind that we might want to have an application with multiple views of the same data and multiple windows. This means we should try to avoid storing any state as data or content of any of the widgets.

Now we need to define what actions we want to do and put those into the menu. I suggest looking into the suggestions.txt document in the devel-docs directory in the gnome-libs distribution tarball (see Resources later in this article) as this document gives the proper menu structures for application and other UI suggestions. We need to be able to add a completely new tree (Add Person) and add a new parent to an existing person (Add Parent). We also need to be able to exit, and we will have an "about" box. I have put several unimplemented commands into the menu definitions as well. I have also put a tool bar definition in, which is basically the same as the menu definition, but you use the gnome_app_create_toolbar function instead of the gnome_app_create_menus function.

The "about" box is slightly more complicated -- and more correct than it was in our hello world example from the last article. Here we define a static pointer to the dialog widget inside the about_cb callback function. In case the dialog is still around and it hasn't been destroyed, we do the following:



		gdk_window_show(dialog->window);
	gdk_window_raise(dialog->window);
	

where dialog is a GtkWidget pointer, and we assume that it has been already realized and shown before. This will show the dialog window if it has been minimized and raise it if it has been obscured. If neither is the case, we create a new about box dialog, and do the following:



	gtk_signal_connect(GTK_OBJECT(dialog), "destroy",
			   GTK_SIGNAL_FUNC(gtk_widget_destroyed),
			   &dialog);
	

This will connect the destroy callback to gtk_widget_destroyed function, which is a convenience function for this purpose. We pass the pointer to our static dialog pointer, and when gtk_widget_destroyed gets called, it will set it to NULL for us.

Also notice another new thing we add:



	gnome_dialog_set_parent(GNOME_DIALOG(dialog), GTK_WINDOW(app));
	

where app is our main application window. This is a useful thing to do as it will associate the dialog to the app as a transient window (a temporary dialog), and window managers can now handle it appropriately.



Back to top


The main window

Now let's look at our main window's body. We will use the GtkCList widget for the main window's view of the data. The GtkCList is a multiple-column list widget. We need to discuss another feature of the GTK+ widget system before going any further. There are many types of widgets that need scrolling ability, so it would make sense to put the scrollbars and related functionality in a widget. However, it would place too many limitations on our design if we just derived all widgets that require scrolling from some generic scrolling widget. Instead we use the container-based nature of GTK+ and place the widget that needs to be scrolled into a GtkScrolledWindow. All GtkScrolledWindow does is connect to the internals of the widget being scrolled and set up scrollbars for it. The widget itself takes care of scrolling. Some widgets that support scrolling are GtkCList, GtkList, GtkText, and GnomeCanvas. If you want to put arbitrary widgets into an area that is scrolled, you place them first inside a GtkViewport and then put that inside GtkScrolledWindow, but we digress.

We will create the GtkCList with the gtk_clist_new_with_titles function, which takes the number of columns as the first argument and an array of character strings to use as the titles for the different columns. We also need to create a scrolled window and place the clist inside that. The code would look something like:



	char *clist_titles[] = {
		"Name",
		"Date of birth",
		"Date of death",
		"Parents"
	};

	...

	/* make new scrolled window for clist */
	w = gtk_scrolled_window_new(NULL,NULL);
	/* set a size */
	gtk_widget_set_usize(w,300,200);
	
	/* create a clist for the main body of the window */
	clist = gtk_clist_new_with_titles(4,clist_titles);
	gtk_container_add(GTK_CONTAINER(w), clist);

	/* set the contents of the app window to the clist */
	gnome_app_set_contents(GNOME_APP(app), w);

The gtk_widget_set_size function sets the default size of the scrolled window to be 300 x 200 pixels. We do this because there is no way for GTK+ to guess a correct size as the whole purpose of scrolling is to put more information inside than you can fit. The NULLs in gtk_scrolled_window_new call are GtkAdjustment pointers, but that is a more advanced topic. For now just leave them NULL.



Back to top


Adding a new person

Next let's look at how we plan to add a completely new person. This is done in the add_person_cb function. Here we create a new instance of the structure "Person" with the g_new macro (the first argument is the type; the second is the number of instances, which is 1 if it's not an array). We then use g_strdup_printf function to use a printf-style function, but put the result in a newly allocated string, and g_strdup, which just duplicates the string it gets as an argument in memory and returns a pointer to it. When we finish creating the Person structure, we need to make a new GNode node for it. This is done with g_node_new function. Because this is a completely new tree, we need to append it onto the "trees" linked list. We do this by:

trees = g_list_append(trees, node);

This is what we need to do to add the person to the data structure. Now we also need to update the GtkCList widget to contain this person. We do this in the add_person_to_clist function to which we pass the pointer to the node.

To add a line to a GtkCList, we use gtk_clist_append function, which takes an array of character strings with as many entries as the listing has columns. We get this by writing the following:



	/* an array of strings for the gtk_clist_append function*/
	char *texts[4];

	texts[0] = person->name;
	texts[1] = person->dob;
	texts[2] = person->dod;
	/* make a parents string for us */
	texts[3] = make_parents_string(node);

The make_parents_string will be explained later. The gtk_clist_append function will return a number of the row that we have just added. We need to be able to later get a pointer to the node that describes this person. GtkCList fortunately provides a way to store a single arbitrary data pointer on each row of the list. This is done by the gtk_clist_set_row_data function:

	gtk_clist_set_row_data(GTK_CLIST(clist),row_number,node);

Now let's explain how make_parents_string function works. It returns a newly allocated string with the name of the parents, or of a single parent, or just an empty string. Inside we have:



Back to top


We first check whether there are any parents at all by checking the node->children pointer. If there are, the data pointer of that node points to the Person structure of the first parent. Then we check if that node has a "next" sibling. If it does, then we have two parents. Note the g_strconcat function, which takes an arbitrary number of string arguments followed by a NULL. It then concatenates all of these, returning a newly allocated string.



Back to top


Adding parents and changing properties


Now in get_selection, we check whether we got a -1 and return NULL in that case; otherwise we return the row data, which we set to the node of that particular person in add_person_to_clist. We do this with:



	return gtk_clist_get_row_data(GTK_CLIST(clist),row_number);

Now we can explain how we add a parent to an item in add_parent_cb. We first get the GNode of the currently selected line. If we get a NULL from get_selection, we use a GnomeApp function to display a warning to the user:



	gnome_app_warning(GNOME_APP(app),"Select a person first");

Note that there are also gnome_app_message, gnome_app_error, and gnome_app_flash. Flash is only for low-priority status updates.

We now need to check whether there are already two parents (we check for two or more just for the sake of sanity). We do this by the GLib function g_node_n_children, which returns the number of children (in our case parents) of a node.

Now if we get this far, we do pretty much the same thing we did with add_person_cb, but instead of appending the GNode to the list of trees, we need to append it to the list of children (parents in our case) of the node returned by get_selection. This is done with the g_node_append function as in:



	g_node_append(selection,node);

We also call add_person_to_clist as that is the same as for the add_person_cb. However, we must also call another function that will update the parent field in the list for the selected person, because we have just added a parent for him. We do this in update_parent_string function to which we pass the pointer to the GNode of the person we wish to update. In the update parent string we just use the make_parents_string function to make us a new parent string as before.

However, we now face a problem because we no longer know the row number of the node we wish to update. Thus, we create a helper function, find_clist_row, which goes through all the rows in the clist and gets their data pointer and returns the row number for which the returned data matches our row. This done, we are ready to update the parents field for this node. We call the gtk_clist_set_text function with the row number, the column number, and the string that we wish to put there, in that order.



Back to top


Editing node properties

Finally, for this example program, we want to allow the user to edit the properties of a node. We have defined a "Properties" item in our "Edit" menu to run properties_cb. We have already seen how to get the currently selected item. You have also seen some basics for treating dialogs. But unlike the about box dialog, we need to provide our own body for the dialog which will be three text entry boxes, into which the user can type the name and the dates for that particular person.


Our genealogy program in action

We also need to specify which buttons should be on the bottom row of the dialog. This is done during the call to gnome_dialog_new, which creates a new generic dialog box, an object called GnomeDialog. We pass the title of the dialog as the first argument, and then as many extra arguments as there are buttons, followed by a NULL. The arguments for buttons are simply strings with the button names, but you can also specify a "stock" button by using the definitions from gnome-stock.h. This will result in nicer and more consistent buttons. In our example, we use the following:



	dialog = gnome_dialog_new("Person properties",
				  GNOME_STOCK_BUTTON_OK,
				  GNOME_STOCK_BUTTON_CANCEL,
				  NULL);

Our dialog still needs some kind of body. A GnomeDialog contains a vertical box (GtkVBox) container for you to put your dialog body into. Because there are three entries that will be relatively similar in how they work, we define a new function, make_hbox_with_label_and_entry, which will create a horizontal box for us, and put a text label on one side of the box and a text entry on the other side. We need two things from this function: the pointer to the horizontal box, and a pointer to the text entry so that we may read out its contents later. We can make the box be returned in the return value of the function so that we may use this function directly when "packing" the vertical box of the dialog as in:



	gtk_box_pack_start(GTK_BOX(GNOME_DIALOG(dialog)->vbox),
			   make_hbox_with_label_and_entry("Name: ",
							  person->name,
							  &name_entry),
			   FALSE,FALSE,0);

Now that we have created our dialog's body, we can go on to show the dialog and get a response from the user. To make things simple we will use a modal dialog. By that we mean that the user will have to respond to our dialog before we allow him to continue using the application. This can be done with the gnome_dialog_run function, to which we pass the pointer to the dialog object. It returns the number of the button pressed, starting with 0, or a negative number if the user destroyed the window with the window manager. We then simply check for a return of 0 as that would be the "OK" button. If so, we need to get the texts from the entries. This is done with the gtk_entry_get_text function, which will return a pointer to the internal buffer, so make sure you do not free the returned string. Then we just update the structure by freeing the old strings and allocating new ones, and calling our already defined functions for updating an entry.



Back to top


Things to note

Throughout the source code I use statements to check arguments to functions. There are two macros for this purpose: g_return_if_fail, which just acts as an assert -- but if the condition fails, it just does a return -- and g_return_val_if_fail, which tests a condition and returns a value that was the second argument to the macro if the condition fails. These functions are very useful for debugging as you will get a nice warning on the terminal with the line number and function name if the condition fails, rather than some segfault or wrong behavior later on. We should also add, however, that these things should not be required to run your program. They are just for debugging purposes, and the user can compile the code without them to expedite the process. This means that they should only check for things that should always be true, just like asserts do, and they should not be used for, say, checking the user's input.

Now we have a simple real-world application that we can build upon. In my next article, we'll concentrate on file operations with libxml to make our program load and save the tree.



Resources



About the author

George (Jiri or Jirka in Czech) Lebl was born in Prague, Czechoslovakia (now Czech Republic), and lives in San Diego, California where he is trying to finish his degree. After several years using non-UNIX operating systems, he started using UNIX. He became a UNIX bigot about four years ago, and a Free Software bigot about two years ago. He joined the GNOME project in the fall of 1997, and finally became a C bigot as well. Nowadays he works at Eazel, Inc. on the next generation of GNOME. Most importantly, he's a VI user. You can contact George Lebl at jirka@5z.com.




Rate this page


Please take a moment to complete this form to help us better serve you.



 


 


Not
useful
Extremely
useful
 


Share this....

digg Digg this story del.icio.us del.icio.us Slashdot Slashdot it!



Back to top