Data driven application with GTK+ and APR DBD routines
I have been trying to find a reason to study c programming for a long time, now I found a reason to do it, I wrote a very small application that uses APR and GTK+ where I can manager costumers and suppliers contact info.
Before start coding you must pay attention to the following requirements:
- Apache APR
- GTK+
- pkg-config
All the requirements above can be easily installed in your system by using the linux distribution package management system, you can also visit their respective sites to download the sources, compile them and install the binaries. The Apache APR sources can be found at apr.apache.org and GTK+ sources at gtk.org
Now we can start coding our application, let’s start with main window, the main window only contains a menu with few options, one of these menu options will open the suppliers contact info management window. In the main window we also do the APR initialization and termination, this step is very important since all structures used by APR must be initialized to be properly used.
Creating the main window
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | #include <apr_general.h> #include <gtk/gtk.h> #include "suppliers/management.h" //The callback function to be called when the main window is closed static gboolean close_application_event( GtkWidget *widget, GdkEvent *event, gpointer data) { //Exit application gtk_main_quit(); return FALSE; } //The callback function that is called when the main window is destroyed static void destroy(GtkWidget *widget, gpointer data) { //Exit application gtk_main_quit(); } int main( int argc, char *argv[] ) { GtkWidget *main_window; //Initialize apr and its internal data structures apr_initialize(); //Initializing GTK gtk_init(&argc, &argv); //Configuring the window main_window = gtk_window_new(GTK_WINDOW_TOPLEVEL); g_signal_connect(main_window, "delete-event", G_CALLBACK(close_application_event), NULL); gtk_container_set_border_width(GTK_CONTAINER(main_window), 10); //Placing the window at the top on the left corner of window gtk_widget_set_uposition(main_window, 0, 0); gtk_window_set_title(GTK_WINDOW(main_window), "Registration of Suppliers"); //Configuring menubar GtkWidget *menu_bar = gtk_menu_bar_new(); gtk_container_add (GTK_CONTAINER(main_window), menu_bar); //Configuring registration menu GtkWidget *register_menu = gtk_menu_new(); GtkWidget* register_item = gtk_menu_item_new_with_label("Register"); gtk_menu_item_set_submenu(GTK_MENU_ITEM(register_item), register_menu); gtk_menu_bar_append(GTK_MENU_BAR(menu_bar), register_item); //Configuring suppliers management submenu GtkWidget *suppliers_management_item = gtk_menu_item_new_with_label("Suppliers"); gtk_menu_shell_append(GTK_MENU_SHELL(register_menu), suppliers_management_item); g_signal_connect(management_suppliers_item, "activate", G_CALLBACK(suppliers_management), (gpointer) main_window); //Configuring quit menu item GtkWidget* quit_item = gtk_menu_item_new_with_label("Quit"); g_signal_connect_swapped(quit_item, "activate", G_CALLBACK(destroy), NULL); gtk_menu_bar_append(GTK_MENU_BAR(menu_bar), quit_item); gtk_widget_show_all(main_window); //Start the application main loop gtk_main (); //Shutting down APR apr_terminate(); return 0; } |
Once the main window is done we are ready to code the suppliers contact info management window.
Creating the suppliers window
The file below contains few pieces of code related with supplier contact info window, this window has some callback functions for its controls, when the user press the ENTER key on the first entry widget in this window the persistence layer code is called to retrieve data from database.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 | #include <gtk/gtk.h> #include <gdk/gdk.h> #include <gdk/gdkkeysyms.h> #include <stdlib.h> #include "management.h" #include "persistence.h" //The flag used to mark the record as new gboolean NEW; GtkToolItem *button_new; GtkWidget *id; GtkWidget *name; GtkWidget *address; //This function does some adjustements on window and its input fields void edit_mode(gboolean editing) { //Enabling/disabling toolbar buttons and entry widgets gtk_widget_set_sensitive(GTK_WIDGET(button_new), ! editing); gtk_widget_set_sensitive(GTK_WIDGET(id), ! editing); gtk_widget_set_sensitive(GTK_WIDGET(name), editing); gtk_widget_set_sensitive(GTK_WIDGET(address), editing); //If we are working on a new record or we aren't editing data if(! editing || NEW) { //Clean all input fields gtk_entry_set_text(GTK_ENTRY(name), ""); gtk_entry_set_text(GTK_ENTRY(address), ""); if(! NEW) { gtk_entry_set_text(GTK_ENTRY(id), ""); gtk_widget_grab_focus(id); } else { gtk_widget_grab_focus(name); } } else { gtk_widget_grab_focus(name); } } //The callback function to be called when the user clicks on "New" button of toolbar void button_new_supplier_clicked(GtkWidget *widget, gpointer data) { NOVO = TRUE; edit_mode(FALSE); } //The callback function to be called when the user presses any key in a input field gboolean id_keypress_event( GtkWidget *widget, GdkEventKey *event, gpointer user_data) { supplier_t *supplier; gboolean ok; const gchar *value; //If the user press the [Enter] or [Return] keys load the supplier info and show to the user if(event->keyval == GDK_Return || event->keyval == GDK_KP_Enter) { value = gtk_entry_get_text(GTK_ENTRY(id)); ok = get_supplier(value, &supplier); NOVO = ! ok; edit_mode(TRUE); if(ok) { if(! NEW) { gtk_entry_set_text(GTK_ENTRY(name), supplier->name); gtk_entry_set_text(GTK_ENTRY(address), supplier->address); } free(supplier); } return TRUE; } return FALSE; } //The callback function to be called when the supplier contact info window is closed gboolean close_supplier_management_window_event( GtkWidget *widget, GdkEvent *event, gpointer data) { //We must return FALSE in order to let GTK close the window and free its resources return FALSE; } //The function that setup the window used for manage suppliers contact info void suppliers_management(GtkWidget *widget, gpointer data) { GtkWidget *window; GtkWidget *label; GtkWidget *entry; GtkWidget *alignment; GtkWidget *table; GtkWidget *toolbar; GtkToolItem *toolbar_item; window = gtk_window_new(GTK_WINDOW_TOPLEVEL); //Hide the taskbar button for this window in window manager gtk_window_set_skip_taskbar_hint(GTK_WINDOW(window), TRUE); g_signal_connect(window, "delete-event", G_CALLBACK(close_supplier_management_window_event), NULL); gtk_container_set_border_width (GTK_CONTAINER (window), 10); gtk_window_set_transient_for(GTK_WINDOW(window), (GtkWindow *) data); //Set the window title gtk_window_set_title(GTK_WINDOW(window), "Registration of Suppliers"); //Adding table layout to the window table = gtk_table_new(4, 2, FALSE); gtk_table_set_row_spacings(GTK_TABLE(table), 4); gtk_table_set_col_spacings(GTK_TABLE(table), 4); //Configuring toolbar toolbar = gtk_toolbar_new(); gtk_toolbar_set_orientation(GTK_TOOLBAR(toolbar), GTK_ORIENTATION_HORIZONTAL); gtk_toolbar_set_style(GTK_TOOLBAR(toolbar), GTK_TOOLBAR_BOTH); gtk_container_set_border_width(GTK_CONTAINER(toolbar), 5); //Adding the "New" button to toolbar toolbar_item = gtk_tool_button_new(NULL, "New"); gtk_toolbar_insert(GTK_TOOLBAR(toolbar), toolbar_item, -1); g_signal_connect(toolbar_item, "clicked", G_CALLBACK(button_new_supplier_clicked), NULL); botao_novo = toolbar_item; //Adicionando toolbar dentro da tabela gtk_table_attach(GTK_TABLE(table), toolbar, 0, 2, 0, 1, GTK_FILL, GTK_SHRINK, 0, 0); //Adding fields to the table label = gtk_label_new("Id"); alignment = gtk_alignment_new(0.9, 0.5, 0.0, 0.0); gtk_container_add(GTK_CONTAINER(alignment), label); gtk_table_attach(GTK_TABLE(table), alignment, 0, 1, 1, 2, GTK_FILL, GTK_SHRINK, 0, 0); entry = gtk_entry_new(); gtk_entry_set_max_length(GTK_ENTRY(entry), 14); gtk_entry_set_width_chars(GTK_ENTRY(entry), 14); alignment = gtk_alignment_new(0.0, 0.5, 0.0, 0.0); gtk_container_add(GTK_CONTAINER(alignment), entry); gtk_table_attach_defaults(GTK_TABLE(table), alignment, 1, 2, 1, 2); g_signal_connect(entry, "key-press-event", G_CALLBACK(id_keypress_event), NULL); id = entry; label = gtk_label_new("Name"); alignment = gtk_alignment_new(0.9, 0.5, 0.0, 0.0); gtk_container_add(GTK_CONTAINER(alignment), label); gtk_table_attach(GTK_TABLE(table), alignment, 0, 1, 2, 3, GTK_FILL, GTK_SHRINK, 0, 0); entry = gtk_entry_new(); gtk_entry_set_max_length(GTK_ENTRY(entry), 40); gtk_entry_set_width_chars(GTK_ENTRY(entry), 40); alignment = gtk_alignment_new(0.0, 0.5, 0.0, 0.0); gtk_container_add(GTK_CONTAINER(alignment), entry); gtk_table_attach_defaults(GTK_TABLE(table), alignment, 1, 2, 2, 3); name = entry; label = gtk_label_new("Address"); alignment = gtk_alignment_new(0.9, 0.5, 0.0, 0.0); gtk_container_add(GTK_CONTAINER(alignment), label); gtk_table_attach(GTK_TABLE(table), alignment, 0, 1, 3, 4, GTK_FILL, GTK_SHRINK, 0, 0); entry = gtk_entry_new(); gtk_entry_set_max_length(GTK_ENTRY(entry), 11); gtk_entry_set_width_chars(GTK_ENTRY(entry), 11); alignment = gtk_alignment_new(0.0, 0.5, 0.0, 0.0); gtk_container_add(GTK_CONTAINER(alignment), entry); gtk_table_attach_defaults(GTK_TABLE(table), alignment, 1, 2, 3, 4); address = entry; //Adding the table to the window gtk_container_add (GTK_CONTAINER(window), table); //Centering the window on the screen gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER); //Set editing mode of window edit_mode(FALSE); //Showing window and all controls inside it gtk_widget_show_all(window); } |
This a partial the definition of contact management window, we have several functions implemented above, most of them are callback functions, the most important is suppliers_management, this function is called from the menu on main window, in line 97 we are initializing the window as top level window, the lines 99 to 104 does a basic setup of window on window manager.
This window also has a table layout added to it lines 107 to 110, this widget allow us to align some widgets in this window, the first of them is the toolbar widget, it takes space of two columns in this layout as we can see on gtk_table_attach function call on line 125, we also include some labels and entry widgets to this table to align them in the window lines 128 to 166. Now we can finish the implementation of the window by just placing it on the center of screen line 172 and set the default state of its widgets line 175.
I also wrote a small abtraction layer around APR DBD routines.
Writing functions that does some database abstraction on top of APR DBD
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | #include <stdarg.h> #include <stdlib.h> #include <glib/gprintf.h> #include <gtk/gtk.h> #include "apr_lib.h" #include "apr_dbd.h" #include "database.h" //Open database connection gboolean db_open_connection(const char* driver_name, const char* parameters, db_connection_t **connection) { const apr_dbd_driver_t *driver; apr_pool_t *pool; apr_dbd_t *handle; apr_status_t status; //Initializing a new memory pool status = apr_pool_create(&pool, NULL); if(status == APR_SUCCESS) { //Initializing the memory pool status = apr_dbd_init(pool); if(status == APR_SUCCESS) { //Loading the database driver status = apr_dbd_get_driver(pool, driver_name, &driver); if(status == APR_SUCCESS) { //Open the connection to database status = apr_dbd_open(driver, pool, parameters, &handle); if(status == APR_SUCCESS) { *connection = apr_pcalloc(pool, sizeof(db_connection_t)); (*connection)->pool = pool; (*connection)->handle = handle; (*connection)->driver = driver; return TRUE; } } } } return FALSE; } //Count the number of parameters in a query int db_count_sql_parameters(const char* query) { int args = 0; const char *q; for (q = query; *q; q++) { if (q[0] == '%') { if (apr_isalpha(q[1])) { args++; } else if (q[1] == '%') { q++; } } } return args; } //Convert a va_list to a binary list of values const void** db_binary_args_from_var_args(apr_pool_t *pool, int nargs, va_list args) { const void **values; int i; values = apr_palloc(pool, sizeof(*values) * nargs); for (i = 0; i < nargs; i++) { values[i] = va_arg(args, const void*); } return values; } //Execute a query on database and store the results on db_resultset_t structure. gboolean db_execute_query(db_connection_t *connection, db_resultset_t **resultset, const char* query, ...) { apr_dbd_prepared_t *prepared_statement; apr_dbd_results_t *results; const void **values; int result; va_list args; va_start(args, query); //Converting a va_list holding a variable number of arguments to binary arguments values = db_binary_args_from_var_args(connection->pool, db_count_sql_parameters(query), args); va_end(args); results = NULL; prepared_statement = NULL; //Prepare the sql statement result = apr_dbd_prepare(connection->driver, connection->pool, connection->handle, query, NULL, &prepared_statement); if(result == 0) { //Pass the values to the prepared statement and execute it result = apr_dbd_pbselect(connection->driver, connection->pool, connection->handle, &results, prepared_statement, 0, values); if(result == 0) { //Allow the db_resultset_t structure and pass the apr resultset resultset to it *resultset = apr_pcalloc(connection->pool, sizeof(db_resultset_t)); (*resultset)->data = results; return TRUE; } } return FALSE; } //Move to the next row on resultset gboolean db_next_row(db_connection_t *connection, db_resultset_t *resultset) { apr_dbd_row_t *row; int result; row = NULL; //Call the apr function to move to the next row on resultset result = apr_dbd_get_row(connection->driver, connection->pool, resultset->data, &row, -1); if(result == 0) { //If the row is successfully retrieved set it on our resultset resultset->row = row; return TRUE; } //Print any error occurred while moving to the next row g_printf("%s", apr_dbd_error(connection->driver, connection->handle, result)); return FALSE; } //Read data from column in resultset const char* db_read_value(db_connection_t *connection, db_resultset_t *resultset, int column) { const char *value; //Read the value in a column at the row pointed by resultset structure value = apr_dbd_get_entry(connection->driver, resultset->row, column); if(value != NULL) { return value; } return ""; } //Close database connection gboolean db_close_connection(db_connection_t *connection) { apr_status_t status; //Close database connection status = apr_dbd_close(connection->driver, connection->handle); if(status == APR_SUCCESS) { //Do apr pool cleanup and free all pointers apr_pool_destroy(connection->pool); return TRUE; } return FALSE; } |
The file above contains a small portion of our database abstraction layer, both memory management and database connection handling are done with help of APR and its DBD routines, please put special focus on db_open_connection function, in this function we do the abstraction of the database connection, first we create the memory pool line 17, if the pool is successfully created we can now init it line 20, now we are read to load the database driver at line 23, once driver is successfully loaded we are able to open the database connection and then create an internal structure containing a pointer to memory pool, connection handle and database driver structures.
The majority of APR DBD routines takes a pool as argument, this means that all memory allocated by these methods will be hold by this memory pool and will released when the database connection is closed and the pool is destroyed as we can see in db_close_connection at line 149.
Now we can finish our application code with a persistence abtraction layer to help us to load/save data on database.
Writing few functions that does persistence abstraction
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | #include <gtk/gtk.h> #include <stdio.h> #include <stdlib.h> #include "persistence.h" #include "../util/database.h" gboolean get_supplier(const char *id, supplier_t **supplier) { db_connection_t *connection = NULL; db_resultset_t *resultset = NULL; gboolean ok; //Open database connection ok = db_open_connection("pgsql", "dbname=contacts user=dbuser password=dbpassword", &connection); if(ok) { //Execute query on database ok = db_execute_query(connection, &resultset, "SELECT \"ID\", \"NAME\"," \ "\"ADDRESS\" FROM SUPPLIER WHERE \"ID\" = %s", id); if(ok) { //Move to the first row of resultset ok = db_next_row(connection, resultset); if(ok) { //Initialize the memory for structure *supplier = malloc(sizeof(supplier_t)); (*supplier)->id = db_read_value(connection, resultset, 0); (*supplier)->name = db_read_value(connection, resultset, 1); (*supplier)->address = db_read_value(connection, resultset, 2); //Close database connection db_close_connection(connection); return TRUE; } } //Close database connection db_close_connection(connection); } return FALSE; } |
The function get_supplier is one of several functions that defines our persistence layer, this function just do a SQL statement in a table and them map the data retrieved from database to an allocated struct. Note the lines 13 to 21 where we are calling the functions of our database abstraction layer, in each call we have a boolean value to be returned, we use this approach to check if the function has finished with a error, if the function call returns TRUE we can move on and do another call to the database layer.
Writing the makefile
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | #Using gcc as c compiler CC=gcc #Defining compiler flags, making use of pkg-config to retrieve compiler flags for gtk and apr also CFLAGS=-Wall -g -O2 -I. `pkg-config gtk+-2.0 --cflags` `pkg-config apr-1 --cflags` `pkg-config apr-util-1 --cflags` #Defining libs to be used for compile this programin, using pkg-config to discover the libs used by gtk and apr LIBS=`pkg-config gtk+-2.0 --libs` `pkg-config apr-1 --libs` `pkg-config apr-util-1 --libs` #The default target will call program compilation all:registration #Linking all libraries into main program registration: registration.o util.la customers.la suppliers.la $(CC) $(LIBS) -o $@ registration.o customers/customers.la suppliers/suppliers.la util/util.la registration.o: registration.c $(CC) $(CFLAGS) -c -o $@ $< #Target to compile util code as library util.la: cd util && $(MAKE) #Target to compile customers code as library customers.la: cd customers && $(MAKE) #Target to compile suppliers code as library suppliers.la: cd suppliers && $(MAKE) #Defining the target to clean a previous build clean: cd util && $(MAKE) $@ cd customers && $(MAKE) $@ cd suppliers && $(MAKE) $@ rm -f cadastro cadastro.o # This is GNU Makefile extension to notify that roughly means: 'clean' does # not depend on any files in order to call it. .PHONY: clean |
Compiling the application
Just type:
make
Running application
Just type:
./supplier_registration