Hello everybody.
In my current project I did quite a lot of GtkTreeView manipulation with filters and since this part of GtkTreeView is quite complex and not described in
GtkTreeView tutorial, I decided to write down this article.
I've contacted the original author of GtkTreeView tutorial, but since I haven't received any response from him yet, I'm posting my tutorial here.
Filtering
Filtering is another powerful tool which can help us reuse our data and display it in number of different ways. To add filtering abilities to your treeview, you need to add another layer in between your
GtkTreeView and it's
GtkTreeModel -
GtkTreeModelFilter.
GtkTreeModelFilter wraps around yound your underlying GtkTreeModel and provides two groups of functionalities:
* limiting the amount of data that is being displayed
* modifying data being displayed
Limiting the amount of data being displayed
We can limit the amount of data being displayed on two levels. First level would be specifying a "virtual root" when creating new filter with
gtk_tree_model_filter_new. This way we can display only one branch of our whole tree.
The second level is filtering data on a row-by-row basis. We can do that by setting "visible column" or "visible function" of the GtkTreeModelFilter. Visible column is a column in filters child model, containing boolean values, and is set with
gtk_tree_model_filter_set_visible_column function. The values in this column determine if the row is getting filtered or not (if the value in this column is set to TRUE, the row is visible, otherwise is hidden). Visible function on the other hand is a
GtkTreeModelFilterVisibleFunc function returning boolean value and is set with
gtk_tree_model_filter_set_visible_func. Visible function gets called with model, iter and user_data as parameters for each row in child model. From that data we determine if the particular row should be filtered or not and according to this return FALSE or TRUE respectively.
Below is an example that demonstrates the usage of GtkTreeModelFilter for limiting the amount of data being displayed. All the methods described above are used, so the code is quite long, but the interesting parts are all included inside create_*_tree family of functions.
Code:
#include <gtk/gtk.h>
#include <string.h>
enum
{
COL_TEXT, /* Model column which holds strings to display. */
COL_FILT, /* Column with boolean values to determine visibility */
N_COLS
};
/* Function declarations */
static GtkWidget *create_non_filtered_tree( GtkTreeModel *model );
static GtkWidget *create_visual_root_tree( GtkTreeModel *model );
static GtkWidget *create_visible_column_tree( GtkTreeModel *model );
static GtkWidget *create_visible_function_tree( GtkTreeModel *model,
GtkEntry *entry );
static gboolean filter_func( GtkTreeModel *model,
GtkTreeIter *iter,
GtkEntry *entry );
static GtkTreeModel *create_model_contents( void );
static void create_treeview_display( GtkTreeView *treeview );
static gboolean cb_entry_changed( GtkEditable *entry,
GtkTreeView *treeview );
int
main( int argc,
char **argv )
{
GtkWidget *window;
GtkWidget *table;
GtkWidget *label;
GtkWidget *entry;
GtkWidget *treeview;
GtkTreeModel *model;
gtk_init( &argc, &argv );
/* Main window */
window = gtk_window_new( GTK_WINDOW_TOPLEVEL );
gtk_container_set_border_width( GTK_CONTAINER( window ), 5 );
gtk_window_set_default_size( GTK_WINDOW( window ), 700, 400 );
g_signal_connect( G_OBJECT( window ), "destroy",
G_CALLBACK( gtk_main_quit ), NULL );
/* Content table */
table = gtk_table_new( 3, 4, FALSE );
gtk_table_set_row_spacings( GTK_TABLE( table ), 5 );
gtk_table_set_col_spacings( GTK_TABLE( table ), 5 );
gtk_container_add( GTK_CONTAINER( window ), table );
/* Labels */
label = gtk_label_new( "Non-filtered content" );
gtk_table_attach( GTK_TABLE( table ), label, 0, 1, 0, 1,
GTK_FILL, GTK_FILL, 0, 0 );
label = gtk_label_new( "Virtual root node" );
gtk_table_attach( GTK_TABLE( table ), label, 1, 2, 0, 1,
GTK_FILL, GTK_FILL, 0, 0 );
label = gtk_label_new( "Visible column" );
gtk_table_attach( GTK_TABLE( table ), label, 2, 3, 0, 1,
GTK_FILL, GTK_FILL, 0, 0 );
label = gtk_label_new( "Visible function" );
gtk_table_attach( GTK_TABLE( table ), label, 3, 4, 0, 1,
GTK_FILL, GTK_FILL, 0, 0 );
/* Entry for last treeview's filter */
entry = gtk_entry_new();
gtk_table_attach( GTK_TABLE( table ), entry, 3, 4, 1, 2,
GTK_FILL, GTK_FILL, 0, 0 );
/* Create and populate model */
model = create_model_contents();
/* Create treeviews */
treeview = create_non_filtered_tree( model );
gtk_table_attach( GTK_TABLE( table ), treeview, 0, 1, 1, 3,
GTK_FILL | GTK_EXPAND, GTK_FILL | GTK_EXPAND, 0, 0 );
treeview = create_visual_root_tree( model );
gtk_table_attach( GTK_TABLE( table ), treeview, 1, 2, 1, 3,
GTK_FILL | GTK_EXPAND, GTK_FILL | GTK_EXPAND, 0, 0 );
treeview = create_visible_column_tree( model );
gtk_table_attach( GTK_TABLE( table ), treeview, 2, 3, 1, 3,
GTK_FILL | GTK_EXPAND, GTK_FILL | GTK_EXPAND, 0, 0 );
treeview = create_visible_function_tree( model, GTK_ENTRY( entry ) );
gtk_table_attach( GTK_TABLE( table ), treeview, 3, 4, 2, 3,
GTK_FILL | GTK_EXPAND, GTK_FILL | GTK_EXPAND, 0, 0 );
/* We need to monitor our entry for changes and refilter the model
* when new text is entered
*/
g_signal_connect( G_OBJECT( entry ), "changed",
G_CALLBACK( cb_entry_changed ),
GTK_TREE_VIEW( treeview ) );
g_object_unref( G_OBJECT( model ) );
gtk_widget_show_all( window );
gtk_main();
return( 0 );
}
/* ********* *
* FUNCTIONS *
* ********* */
static GtkWidget *
create_non_filtered_tree( GtkTreeModel *model )
{
GtkWidget *treeview;
/* Create treeview with model */
treeview = gtk_tree_view_new_with_model( model );
/* Create display components of tree view */
create_treeview_display( GTK_TREE_VIEW( treeview ) );
gtk_tree_view_expand_all( GTK_TREE_VIEW( treeview ) );
return( treeview );
}
static GtkWidget *
create_visual_root_tree( GtkTreeModel *model )
{
GtkWidget *treeview;
GtkTreeModel *filter;
GtkTreePath *path;
/* Create path to set as virtual root */
path = gtk_tree_path_new_from_string( "0" );
/* Create filter and set virtual root */
filter = gtk_tree_model_filter_new( model, path );
gtk_tree_path_free( path );
/* Create treeview with model */
treeview = gtk_tree_view_new_with_model( filter );
/* Create display components of tree view */
create_treeview_display( GTK_TREE_VIEW( treeview ) );
return( treeview );
}
static GtkWidget *
create_visible_column_tree( GtkTreeModel *model )
{
GtkWidget *treeview;
GtkTreeModel *filter;
/* Create filter and set visible column */
filter = gtk_tree_model_filter_new( model, NULL );
gtk_tree_model_filter_set_visible_column( GTK_TREE_MODEL_FILTER( filter ),
COL_FILT );
/* Create treeview with model */
treeview = gtk_tree_view_new_with_model( filter );
/* Create display components of tree view */
create_treeview_display( GTK_TREE_VIEW( treeview ) );
gtk_tree_view_expand_all( GTK_TREE_VIEW( treeview ) );
return( treeview );
}
static GtkWidget *
create_visible_function_tree( GtkTreeModel *model,
GtkEntry *entry )
{
GtkWidget *treeview;
GtkTreeModel *filter;
GtkTreePath *path;
/* Create path to set as virtual root */
path = gtk_tree_path_new_from_string( "1" );
/* Create filter and set visible function */
filter = gtk_tree_model_filter_new( model, path );
gtk_tree_model_filter_set_visible_func(
GTK_TREE_MODEL_FILTER( filter ),
(GtkTreeModelFilterVisibleFunc)filter_func, entry, NULL );
/* Create treeview with model */
treeview = gtk_tree_view_new_with_model( filter );
gtk_tree_path_free( path );
/* Create display components of tree view */
create_treeview_display( GTK_TREE_VIEW( treeview ) );
return( treeview );
}
/* This function is called on every model row. We retrieve the text from
* model and compare it to the one entered in entry. If model row contains
* entry's text as a substring, we return TRUE (showing the row), else FALSE
*/
static gboolean
filter_func( GtkTreeModel *model,
GtkTreeIter *iter,
GtkEntry *entry )
{
const gchar *needle;
gchar *haystack;
gtk_tree_model_get( model, iter, COL_TEXT, &haystack, -1 );
needle = gtk_entry_get_text( entry );
if( strstr( haystack, needle ) != NULL )
return( TRUE );
else
return( FALSE );
}
static GtkTreeModel *
create_model_contents()
{
GtkTreeStore *store;
GtkTreeIter iter, p_iter;
gint i;
/* Create list store to hold our data */
store = gtk_tree_store_new( N_COLS, G_TYPE_STRING,
G_TYPE_BOOLEAN );
/* Populate store with sample data */
gtk_tree_store_append( store, &p_iter, NULL );
gtk_tree_store_set( store, &p_iter, COL_TEXT, "Visible root",
COL_FILT, TRUE,
-1 );
for( i = 1; i < 7; i +=2 )
{
gchar *string;
string = g_strdup_printf( "Leaf %d987", i );
gtk_tree_store_append( store, &iter, &p_iter );
gtk_tree_store_set( store, &iter, COL_TEXT, string,
COL_FILT, TRUE,
-1 );
g_free( string );
string = g_strdup_printf( "Leaf %d789", i + 1 );
gtk_tree_store_append( store, &iter, &p_iter );
gtk_tree_store_set( store, &iter, COL_TEXT, string,
COL_FILT, FALSE,
-1 );
g_free( string );
}
gtk_tree_store_append( store, &p_iter, NULL );
gtk_tree_store_set( store, &p_iter, COL_TEXT, "Hidden root",
COL_FILT, TRUE,
-1 );
for( i = 1; i < 7; i +=2 )
{
gchar *string;
string = g_strdup_printf( "Leaf %d097", i );
gtk_tree_store_append( store, &iter, &p_iter );
gtk_tree_store_set( store, &iter, COL_TEXT, string,
COL_FILT, TRUE,
-1 );
g_free( string );
string = g_strdup_printf( "Leaf %d089", i + 1 );
gtk_tree_store_append( store, &iter, &p_iter );
gtk_tree_store_set( store, &iter, COL_TEXT, string,
COL_FILT, FALSE,
-1 );
g_free( string );
}
return( GTK_TREE_MODEL( store ) );
}
static void
create_treeview_display( GtkTreeView *treeview )
{
GtkTreeViewColumn *col;
GtkCellRenderer *renderer;
/* Create column for displaying text */
col = gtk_tree_view_column_new();
renderer = gtk_cell_renderer_text_new();
gtk_cell_layout_pack_start( GTK_CELL_LAYOUT( col ), renderer, TRUE );
gtk_tree_view_column_set_attributes( col, renderer, "text", COL_TEXT, NULL );
gtk_tree_view_append_column( treeview, col );
/* Remove column header */
gtk_tree_view_set_headers_visible( treeview, FALSE );
}
/* This function gets called whenever the text inside entry changes.
* All that we need to do is to inform our filter that our filtering
* conditions have changed and the display should be recalculated.
*/
static gboolean
cb_entry_changed( GtkEditable *entry,
GtkTreeView *treeview )
{
GtkTreeModelFilter *filter;
filter = GTK_TREE_MODEL_FILTER( gtk_tree_view_get_model( treeview ) );
gtk_tree_model_filter_refilter( filter );
return( FALSE );
}
Modifying data being displayedThis is probably the most powerful capability of the GtkTreeModelFilter. You can transform original data to meet almost any need. All you need to do is write a function that does the transformation. But this flexibility comes at a price: since transformation function is called whenever the data needs to be displayed, it may slow your application down a bit (or a lot;). Modify function is set with
gtk_tree_model_filter_set_modify_func.
Using this functionality can be quite hard at first, but if you follow three simple steps, you'll be able to create something useful without driving yourself crazy. The first step is to determine what do you have (starting point), second step is to determine what would you like to have and the third step is to create a transformation from first to second step.
In example below, the steps would look something like this:
1. Model with one G_TYPE_STRING column, containing strings of length 3.
2. Model with three G_TYPE_INT columns, containing numeric representations of characters.
3. Split string into characters and determine their numeric values.
Now we are ready to set the modify function of filter with gtk_tree_model_filter_set_modify_func. The first parameter in this function call is filter. The second parameter is number of columns in filter (in our case this would be 3, since we want to have 3 integer columns). Third parameter is array of
GTypes that we want to see in our filter (in our example we need array of 3 G_TYPE_INTs). The fourth parameter is function that will do the actual transformation. The fifth parameter is user data and sixth parameter is destroy notifier (you can safely ignore the last two parameters).
All that is left to do now is to write a
modify function. The important thing to understand is that all of the parameters that you get are related to filter and not to underlying model. You must convert them to access underlying model.
That's it. After you get through this piece of code, you should be armed to tackle almost any GtkTreeView transformation task.
Code:
#include <gtk/gtk.h>
#include <string.h>
enum
{
COL_TEXT, /* Model column which holds strings to display. */
N_COLS
};
/* Function declarations */
static GtkWidget *create_non_transformed_tree( GtkTreeModel *model );
static GtkWidget *create_transformed_tree( GtkTreeModel *model );
static void modify_func( GtkTreeModel *model,
GtkTreeIter *iter,
GValue *value,
gint column,
gpointer data );
static GtkTreeModel *create_model_contents( void );
static void create_treeview_display( GtkTreeView *treeview,
gint text_col );
int
main( int argc,
char **argv )
{
GtkWidget *window;
GtkWidget *table;
GtkWidget *label;
GtkWidget *treeview;
GtkTreeModel *model;
gtk_init( &argc, &argv );
/* Main window */
window = gtk_window_new( GTK_WINDOW_TOPLEVEL );
gtk_container_set_border_width( GTK_CONTAINER( window ), 5 );
gtk_window_set_default_size( GTK_WINDOW( window ), 400, 300 );
g_signal_connect( G_OBJECT( window ), "destroy",
G_CALLBACK( gtk_main_quit ), NULL );
/* Content table */
table = gtk_table_new( 2, 2, FALSE );
gtk_table_set_row_spacings( GTK_TABLE( table ), 5 );
gtk_table_set_col_spacings( GTK_TABLE( table ), 5 );
gtk_container_add( GTK_CONTAINER( window ), table );
/* Labels */
label = gtk_label_new( "Non-transformed content" );
gtk_table_attach( GTK_TABLE( table ), label, 0, 1, 0, 1,
GTK_FILL, GTK_FILL, 0, 0 );
label = gtk_label_new( "Transformed contend" );
gtk_table_attach( GTK_TABLE( table ), label, 1, 2, 0, 1,
GTK_FILL, GTK_FILL, 0, 0 );
/* Create and populate model */
model = create_model_contents();
/* Create treeviews */
treeview = create_non_transformed_tree( model );
gtk_table_attach( GTK_TABLE( table ), treeview, 0, 1, 1, 2,
GTK_FILL | GTK_EXPAND, GTK_FILL | GTK_EXPAND, 0, 0 );
treeview = create_transformed_tree( model );
gtk_table_attach( GTK_TABLE( table ), treeview, 1, 2, 1, 2,
GTK_FILL | GTK_EXPAND, GTK_FILL | GTK_EXPAND, 0, 0 );
g_object_unref( G_OBJECT( model ) );
gtk_widget_show_all( window );
gtk_main();
return( 0 );
}
/* ********* *
* FUNCTIONS *
* ********* */
static GtkWidget *
create_non_transformed_tree( GtkTreeModel *model )
{
GtkWidget *treeview;
/* Create treeview with model */
treeview = gtk_tree_view_new_with_model( model );
/* Create display components of tree view */
create_treeview_display( GTK_TREE_VIEW( treeview ), COL_TEXT );
return( treeview );
}
static GtkWidget *
create_transformed_tree( GtkTreeModel *model )
{
GtkWidget *treeview;
GtkTreeModel *filter;
GType types[3];
/* Create column types that will be exposed to treeview */
types[0] = G_TYPE_INT;
types[1] = G_TYPE_INT;
types[2] = G_TYPE_INT;
/* Create filter and set virtual root */
filter = gtk_tree_model_filter_new( model, NULL );
gtk_tree_model_filter_set_modify_func(
GTK_TREE_MODEL_FILTER( filter ), 3, types,
(GtkTreeModelFilterModifyFunc)modify_func, NULL, NULL );
/* Create treeview with model */
treeview = gtk_tree_view_new_with_model( filter );
/* Create display components of tree view */
create_treeview_display( GTK_TREE_VIEW( treeview ), 0 );
create_treeview_display( GTK_TREE_VIEW( treeview ), 1 );
create_treeview_display( GTK_TREE_VIEW( treeview ), 2 );
return( treeview );
}
/* This function is called for every value in model. We retrieve that value
* from model and transform it to our liking. (In this sample code, we populate
* right tree view with sum of numeric representation of chars on the left.)
*/
static void
modify_func( GtkTreeModel *filter,
GtkTreeIter *iter,
GValue *value,
gint column,
gpointer data )
{
GtkTreeModel *model;
GtkTreeIter citer;
gchar *string;
/* We need to obtain filter's child model and convert filter's iter
* to child model iter.
*/
g_object_get( G_OBJECT( filter ), "child-model", &model, NULL );
gtk_tree_model_filter_convert_iter_to_child_iter(
GTK_TREE_MODEL_FILTER( filter ), &citer, iter );
/* We need to handle all columns in our destinetion model */
gtk_tree_model_get( model, &citer, COL_TEXT, &string, -1 );
g_value_set_int( value, string[column] );
g_free( string );
g_object_unref( G_OBJECT( model ) );
}
static GtkTreeModel *
create_model_contents()
{
GtkListStore *store;
GtkTreeIter iter;
gint i;
gchar ch = 'a';
/* Create list store to hold our data */
store = gtk_list_store_new( N_COLS, G_TYPE_STRING );
/* Populate store with sample data */
for( i = 0; i < 8; i++ )
{
gchar *string;
string = g_strdup_printf( "%c%c%c", ch, ch + 1, ch + 2 );
gtk_list_store_append( store, &iter );
gtk_list_store_set( store, &iter, COL_TEXT, string, -1 );
g_free( string );
ch += 3;
}
return( GTK_TREE_MODEL( store ) );
}
static void
create_treeview_display( GtkTreeView *treeview,
gint text_col )
{
GtkTreeViewColumn *col;
GtkCellRenderer *renderer;
/* Create column for displaying text */
col = gtk_tree_view_column_new();
renderer = gtk_cell_renderer_text_new();
gtk_cell_layout_pack_start( GTK_CELL_LAYOUT( col ), renderer, TRUE );
gtk_tree_view_column_set_attributes( col, renderer, "text", text_col, NULL );
gtk_tree_view_append_column( treeview, col );
}
Any comments, corrections and constructive critics are welcome.