Displaying tabular data in a GtkTreeView

Hi Community,

As a follow up to Yesterday’s post on Linux development with C++, GTK+ and SQL Server vNext. Today’s post is about GtkTreeView and how we can use it to display tabular data, as we’re used to do with a DataGrid in .NET, but first it’s worthy to start with an introduction to GTK+.

GTK+ is a cross-platform widget toolkit that allows developers to create graphical user interfaces. It’s been around for a few years now, and it supports a variety of languages;  this is long before some companies “introduced” the concept of separating code from visual representation thus decoupling the solution in a more flexible and manageable way, therefore UI can be put together by UX people while developers use their preferred language, Python being one of the most popular but in my personal case I use  C++. Every GTK+ widget inherits from GtkWidget thus it provides an easy to use and succinct abstraction level that enables developers to write generic code. In other words, same Glade file that describes the user interface can easily implement code written in different languages, some screenshots of GTK+ in action can be found here on the project’s website.

 

GtkTreeView was designed having MVC pattern in mind and to illustrate this a bit better, let’s refer to the GtkTreeView I have in my demo application below.

 

image

As mentioned in other post, the information bound to the GtkTreeView is retrieved from SQL Server, so let’s go and dissect the code that binds that resultset to the widget.

 

///////////////////////////////////////////////////////////////////////////////////////////////////

// Method responsible for creating the view, reusing a widget created at runtime from a glade file

///////////////////////////////////////////////////////////////////////////////////////////////////

 

void MainWindowController::CreateView() {

    auto index = 0;

    GtkTreeModel *model;

    std::vector<std::string> columns {"Contact Id", "First Name", "Last Name", "Phone Number", "Email" };

 

    for (auto&& col : columns) {

        auto renderer = gtk_cell_renderer_text_new();

        auto newColumn = gtk_tree_view_column_new_with_attributes(col.c_str(), renderer, "text", index, NULL);

        gtk_tree_view_append_column(GTK_TREE_VIEW(Controls.at("grdRows")), newColumn);

        index++;

    }

 

    if (Context.RowCount_get() > 0) {

        model = FillView();

        gtk_tree_view_set_model(GTK_TREE_VIEW(Controls.at("grdRows")), model);

        g_object_unref(model);

    }

}

 

 

///////////////////////////////////////////////////////////////////////////////////////////////////

// Method responsible for filling the model then bound to the GtkTreeView widget

///////////////////////////////////////////////////////////////////////////////////////////////////

 

GtkTreeModel* MainWindowController::FillView() {

    GtkTreeIter iter;

    GtkTreeStore *store;

    auto hasData = Context.RowCount_get() > 0;

 

    if (hasData) {

        auto colCount = (int)ColumnInformation::ColumnCount;

        store = gtk_tree_store_new (colCount, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING);

        for (auto&& contact : Context.Rows_get()) {

            gtk_tree_store_append (store, &iter, NULL);

 

             gtk_tree_store_set(store, &iter,

                                ColumnInformation::ContactId, contact.Indexer(ColumnInformation::ContactId).c_str(),

                                ColumnInformation::FirstName, contact.Indexer(ColumnInformation::FirstName).c_str(),

                                ColumnInformation::LastName, contact.Indexer(ColumnInformation::LastName).c_str(),

                                ColumnInformation::PhoneNumber, contact.Indexer(ColumnInformation::PhoneNumber).c_str(),

                                ColumnInformation::Email, contact.Indexer(ColumnInformation::Email).c_str(), -1);

        }

 

    }

 

    return  (hasData ? GTK_TREE_MODEL(store) : nullptr);

}

 

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

// Method responsible for handling user selection in GtkTreeView widget, but also storing it in DataContext (bound to UI fields)

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

 

void MainWindowController::OnGridSelectionChanged(GtkTreeSelection *selection, gpointer data) {

    GtkTreeIter iter;

    GtkTreeModel *model;

    Contact currentItem;

    auto selected = gtk_tree_view_get_selection((GtkTreeView*) self->Controls.at("grdRows"));

 

    if (gtk_tree_selection_get_selected(selected, &model, &iter)) {

        for (auto x = 0; x < ColumnInformation::ColumnCount; x++) {

            gchar *column;

            gtk_tree_model_get(model, &iter, x, &column, -1);

            currentItem.SetFieldByIndex(column, x);

            g_free(column);

        }

        self->Context.CurrentContact_set(currentItem, [&](Contact& contact){this->DatabindFields(contact);});

    }

}

 

 

The snippets above are to create the view, better said to format the GtkTreeView that exists in the Controls collection of the window (std::map<std::string, GtkWidget*>), also to populate the model (GtkTreeModel) and to handle any user selection that’s reflected in the other data entry widgets. All of this has been handcrafted unlike the way is done with Windows Forms or WPF, for instance where databinding is sometimes taken for granted.

At this moment in time, you must be wondering when and where the application calls the database and also the status bar how it is updated, both are depicted in snippets below

////////////////////////////////////////////////////////////////////////////

// Method responsible for initializing the application

////////////////////////////////////////////////////////////////////////////

 

void MainWindowController::StartUp(GtkApplication &app, gpointer user_data) {

    application = &app;

    GError *err = nullptr;

    self->builder = gtk_builder_new();

    gtk_builder_add_from_file(self->builder, "./../../ui/SqlTestHarness.glade", &err);

 

    if (err) {

        g_error(err->message);

        g_error_free(err);

        throw std::runtime_error("Unable to load UI. Please ensure glade file exists.");

    }

 

    self->SetUpUI();

    self->ConnectSignals();

    g_object_unref (G_OBJECT (self->builder));

    gtk_application_add_window(&app, (GtkWindow*) self->Controls.at("frmMain"));

    gtk_widget_show ((GtkWidget*) (GtkWindow*) self->Controls.at("frmMain"));

    self->Context = DataContext(app);

 

   // We create a new thread to populate Context (without freezing main UI thread) 

   g_thread_new("RetrieveRecordsThread",

                [&](gpointer data) -> gpointer   {

                        self->Context.Rows_set(self->Context.DataAccess.RetrieveRecords().Data);

                        self->CreateView();

                        self->Refresh();

                        return nullptr;

                }, nullptr);

 

    // Timer that runs every second to update datetime in status bar, and record count (taken from Context)

    g_timeout_add_seconds(1, [&](gpointer data)->gboolean {self->UpdateStatusBar(data);}, self->Controls.at("sbrMain") );

}

 

 

////////////////////////////////////////////////////////////////////////////

// Method responsible for updating the status bar (called from lambda above)

////////////////////////////////////////////////////////////////////////////

 

gboolean MainWindowController::UpdateStatusBar(gpointer data) {

    std::ostringstream oss;

    auto current = std::time(nullptr);

    auto statusBar = (_GtkStatusbar*) data;

    auto localTime = *std::localtime(&current);

    oss << "Date and Time: " <<  std::put_time(&localTime, "%d/%m/%Y  %H:%M:%S") << " - Record Count: " << Context.RowCount_get();

    auto contextId = gtk_statusbar_get_context_id(statusBar, "CurrentInformation");

    gtk_statusbar_remove_all(statusBar, contextId);

    gtk_statusbar_push(statusBar, contextId, oss.str().c_str());

 

    return TRUE;

}

 

Regards,

 

Angel

Leave a Reply

Your email address will not be published. Required fields are marked *