GTK GUI development in Genie
Introduction and Disclaimer
I am a beginner with genie - and finding it tough going due to the absence of materials like this to get going. In the spirit of open-source having been given all these great development tools, the least I can do is document my journey and discoveries! The text is therefore written from the standpoint of a raw beginner and may well be wrong! This is my take having pieced together information from:
- GTK documentation
- valadoc bindings documentation - www.valadoc.org for the GTK implementation in vala/genie
- vala Gtk tutorial
- studying other genie and vala examples
I am trying this stuff out as I write - I hope some of it is right and a review of this text would be great! Please gurus, feel free to edit the text at will and make this resource useful to us newbies.
It is possible to create windows with drag and drop in a nice GUI with a program called Glade which produces an XML file that can then be read by the GtkBuilder class at run time to construct the GUI. However, this to my mind defeats the concept of a compiled language as the XML has to be parsed and "interpreted" into a GUI at run time. Instead then, this tutorial focuses on the concepts required for hand coding of a precompiled GUI.
Gtk - stands for the Gimp Tool Kit as it had its origins as a library of functions in the development of the famous GIMP - Graphics and Image Manipulation Program. Gtk is one of a number of libraries that form the basis of the Gnome desktop environment the lighter weight Linux desktop than the main alternative KDE. Since Gtk exists on 99% of linux distributions and is even available for Windows it presents as a persuasive toolkit for portable and lightweight (requiring only minimal computer resources) GUIs.
Windows are created normally in a new class and an instance created from the init section of the main program. Typically:
init Gtk.init (ref args) // Initialise the Gtk library var AppWindow = new MainAppWindow() //make an instance of the window AppWindow.show_all() Gtk.main() //pass control to the GTK main loop which polls for signals/events class MainAppWindow : Window init title = "My Application Main Window" default_height = 200 //set the height of the window default_width = 350 //set the width of the window window_position = WindowPosition.CENTER destroy.connect(Gtk.main_quit) //add the Gtk.main_quit to the close
There are many examples to get us this far on the internet – I found moving from this point much tougher. Some perspective of the overall structure of Gtk objects is required.
Gtk documentation refers to the various “controls” (perhaps this is the MS word for widgets – apologies in advance) that can be placed in windows such as buttons, entry boxes, labels, tables, scroll bars etc as “widgets”. A window can have any number of widgets.
In Gtk, there is a widget class in itself and the items referred to as widgets, the labels, buttons, TreeView, Entry, ComboBox etc.... are all descendants inheriting all the methods, properties and signals (events) from the Widget class - in looking at the documentation of a particular class of widget, it is important to realise that all the widget class properties/methods/signals also apply to these child widgets.
New label widgets can be added to a window with:
var A_Label = new Label(“The text for the label”)
The properties of the label can be modified to get almost any effect see:
Text Boxes – or Entry Boxes
New entry boxes can be added to a window with:
var An_Entry = new Entry() An_Entry.set_width_chars(20) An_Entry.set_text(“some entry”) An_Entry.set_editable(true)
To get the program to look at the text of the Entry - the get_text() method of the Entry widget is required e.g.
TextBoxValue:string TextBoxValue = An_Entry.get_text() print "The entry made was: %s", TextBoxValue
To do some action based on the users input into the Entry - a function needs to be "connected" to the "changed" signal or if the action is only to be performed when the user hits the enter key in the Entry then the "activate" signal needs to be connected to.
Getting the Focus
To get the focus for a specific widget the grab_focus() method does the trick (this is a method for the parent widget class so it works on any widget which inherits all the properties and methods of its parent - see comments above about widgets)
Buttons are the clickable interface widgets that then go off and do some action/process. It is often convenient to make a keyboard short cut for performing the action. It is convention to indicate the keyboard shortcut using an underlined character in the button’s label. In Gtk this is called a “mnemonic” (in contrast to the actual meaning of the word!). So most often New buttons will be added to a window with:
Var A_Button = new Button.with_mnemonic(“Process Some _Stuff”)
The ”_S” indicates that the S of “Stuff” should be underlined and that the key “S” is the keyboard shortcut to click the button. Button’s also need to be connected to a function which does the action/process when it is clicked. Gtk refers to the signals that widgets “emit” and the clicked signal is emitted when it is clicked. To run the ProcessStuff function when the button is clicked:
Gtk provides many standard buttons with a nice icon on the button for the usual functions that buttons are used for opening a file, closing a window etc... these are created with
Var OK_Button = new Button.from_stock(STOCK.OK)
There is quite an extensive list of choices of stock items http://library.gnome.org/devel/gtk/unstable/gtk3-Stock-Items.html#GTK-STOCK-OK:CAPS
The TreeView Widget
This widget is really powerful. It produces a grid for displaying lists of data, or lists with sub-lists in a “tree” of information. There is quite a lot that can be done with a TreeView and a series of sub-widgets that will be introduced in this section to alter the behaviour of the TreeView. A TreeView really works with a whole collection of widgets (TreeStore or ListStore, TreeViewColumn, CellRenderers) not as a widget on it's own. There are some concepts to understand before you can get going.
Initially, having come from a background of PHP web-scripting, I was looking for ways to create widgets on the fly to render the GUI based on the data returned from SQL calls. The data returned may contain any number of rows and how to display a variable number of rows without being able to create widgets on the fly confused me.
Instead the TreeView widget allows any number of data rows to be displayed and I suspect is a key part of any non-trivial GUI application.
To create a new TreeView widget - almost predictably based on the creation of other widgets above: var MyTreeView = new TreeView()
The data for the TreeView is stored in the TreeStore (or ListStore where the data is not in a tree format i.e. there is no child data rows to be linked to each row of data. This is effectively a sub-set of the TreeStore functionality and hence easier to implement). These “stores” are called the (data) “Model” by the Gtk documentation. To set up a ListStore for the TreeView the code is:
//set up a new data “Model” for the TreeView in a ListStore containing 3 columns two strings and a double Var MyListStore = new ListStore(3, typeof (string), typeof (string), typeof(double)) MyTreeView.set_store = MyListStore //the data for the TreeView is set
A TreeView is made up of a number of TreeViewColumns. Each Column has a clickable header (the column title) that can be used to sort the list and for resizing the column width. In each column there is a CellRenderer whose properties determine how the data in the entire column is displayed (not just a single cell - but all cells in that column). There are different cell renderers for different types of data. The CellRenderers are packed into TreeViewColumns (much like traditional widgets are packed into a HBox or VBox - see positioning Widgets below). NB CellRenderer is not actually a widget though
- The CellRenderer for each column needs to be initiated and
- Then the TreeViewColumn created
Var Col1CellRenderer = new CellRendererText() Var Column1 = new TreeViewColumn.with_attributes(“Column One”, Col1CellRenderer) MyTreeView.append_columns(Column1) // add column 1 to the TreeView
It is the CellRenderer object that has the events (signals) that can be “connect”ed to with a function to perform an action following a user input to change a field in the TreeView widget. There are a series of CellRenderer objects for differing purposes - all of them are derived from the CellRenderer class though and inherit all the signals and properties. Setting CellRender properties against derived classes is therefore appropriate and has the desired effect.
The CellRenderer only has limited capability in configuring just how the output should look. It would have been wonderful to have found a CellRendererNumeric or similar whose properties and methods could be manipulated to render doubles/floats sensibly. Unfortunately, it is a little messy to do this and still be able to edit the data in the column. The column needs to be created with a cell data function
Var SellPrice_CellRend = new CellRendererText() //add a new cell renderer for the column /* Insert a column with the special data function method of TreeVeiw */ MyTreeView.insert_column_with_data_func(-1, "Sell Price", SellPrice_CellRend, FormatSellPriceDecimalPlaces) </snip> /*Now how to render the data */ def FormatSellPriceDecimalPlaces(Column:TreeViewColumn, Renderer:CellRenderer, Model:TreeModel, Iter:TreeIter) NumberToFormat:float TempDouble: double FormattedNumber:string /*First have to get the data to format COLS is an enum with english names for each column number*/ Model.get(Iter, COLS.SELLPRICE, out TempDouble,-1) NumberToFormat = (float) TempDouble /* The to_string() function only takes a parameter for the float version */ FormattedNumber = NumberToFormat.to_string("%.2f") /* Now set the "text" property of the CellRenderer with the formatted data to display*/ Renderer.set("text", FormattedNumber)
In the snippet above since when get or set methods are used of the TreeStore it is necessary to refer to the column numbers (integers) of the TreeStore to get and set the data for. This makes for code which is quickly difficult to read and I saw in my googling a neat trick to avoid having random numbers sprinkled throughout the TreeView code. The trick is to define an enum (enumerated list) I called it COLS each member of the list takes the values incremented by 1 - to make an enum:
enum COLS ITEMCODE=0 QUANTITY SELLPRICE
Now referring to COLS.SELLPRICE will return 2 but is much easier to read.
Referring to rows of data in a TreeStore is done using TreePath the address of the row of data is given by a series of integers separated by a colon eg. 4:2:3 refers to the 5th (as counting starts at 0) row, and the third row of the child data at the 5th row and the 4th row of data from the child of the child row. For a ListStore of course only a single integer is required as there is no tree structure to contend with.
Scroll Bars on a TreeView
To add scroll bars to a TreeView a "container" widget called a ScrolledWindow is required. The TreeView is then placed inside the ScrolledWindow and the ScrolledWindow added to the main window using a packing widget - see below. By default the TreeView widget resizes itself based on the number of rows of data in the ListStore or TreeStore that are to be displayed in the widget.
A scrolled window widget is created with two parameters which are "Adjustment" objects. These adjustment objects determine the upper and lower bounds of the horizontal and vertical scrollbars together with the increments and page up/down increments. If they are not specified then the take on the Adjustment parameters of their contained widgets.
var MyScrolledWindow = new ScrolledWindow(null,null) //create the scrolled window MyScrolledWindow.set_policy(PolicyType.ALWAYS,PolicyType.AUTOMATIC)
The .set_policy method of the ScrolledWindow sets the horizontal scroll bar to always show and the vertical scroll bar to only show when the window is too small to display all the data in the window. This parameter could also be set to PolicyType.NEVER.
This won't make sense until you have read the positioning widgets section: When a ScrolledWindow containing a TreeView is put inside a Vbox or HBox there is very little control over what area the TreeView/ScrolledWindow takes up - it basically finished after the last row of data is displayed. I wanted the TreeView to take up a section where new rows would display and for it to show the blank space when the TreeView is empty of data. After some experimentation with Layout widgets, I found that using a table to position widgets is the answer as the ScrolledWindow/TreeView can be allocated a chunk of space on the table and it will display the empty space you specify.
TreeStore and TreeIter
TreeStore or ListStore are the widgets that stores the data for display in a TreeView. TreeStore is effectively a multi-dimensional array (a ListStore being just a single dimension array) which can be manipulated and data set/deleted using a series of it's own methods. The current position in the TreeStore is given by the TreeIter. To add data to a TreeStore you first need to add a blank record and get back the TreeIter of where to update the values. The append method of a TreeStore (or a ListStore) returns an "out" parameter which is a TreeIter to tell the program where the new row has been added. Consider first the root level of the Tree - i.e. just the top level of data (as a TreeStore can have child rows of data)
MyRootTreeIter:TreeIter MyTreeStore.append(out MyRootTreeIter, null) //add a new row to the root top level TreeStore MyTreeStore.set(MyRootTreeIter, 0, "Some Data",1,"Next Column Data",-1)
The code above defines a TreeIter - MyRootTreeIter, then it appends a blank row in the root of MyTreeStore TreeStore. If the row was to be appended to child of one of the rows, then the TreeIter for that row needs to be got first and is then the second parameter to the append method. Since the second parameter is null then this means that the row is to be added to the root(top level) of the tree's data.
Having appended a blank row the TreeIter referencing the new blank row is returned into the out parameter MyRootTreeIter. The data in this row is set using the set method, the first parameter of which is the TreeIter referencing the row, then the column number to be set and the data that the column number should be set to. It seems that any number of columns of data can be set using the .set method of TreeStore, the column number followed by the data to enter into that column. Once the last column number/data to set has been entered as a parameter the final parameter must be -1.
Retrieving Data From a TreeStore
It took me a little while to figure out how to get the data back from the TreeStore or ListStore. These widgets are sub-classes of TreeModel and the TreeModel has some methods (which are inherited by the child classes of TreeStore and ListStore) to get the values back - when you have a TreeIter referencing the row you wish to retrieve the data from:
Column1Data:GLib.Value Column1DataString: string MyTreeStore.get_value(MyTreeIter, 1, out Column1Data) Colum1nDataString = (string) Column1Data print "The data in column1 of the TreeStore is: %s", Column1DataString
This should show "Next Column Data" in the terminal window
Note that the data returned as an out parameter from the get_value method is a GLib.Value - I have cast it to a normal string. If you don't wish to mess with GValues then you can use the .get method of the TreeStore (or ListStore) TreeModel derivate classes to get data back into variables. The same format as the .set methods is used, i.e. the column number to get the data for followed by an out variable to receive the data back into.
The TreeModel also has methods (and hence so too do the derivative objects of TreeStore and ListStore) to navigate around the rows of the data model - there is a foreach function and a MyTreeStore.get_iter_first(out MyTreeIter) and MyTreeStore.iter_next(MyTreeIter) methods (no previous?) to navigate around the data. There is also a foreach method of the TreeModel.
Capturing a Key Press Event in a TreeView
To process key_press_event in a TreeView the key_press_event signal (a generic widget signal inherited by the TreeView Widget) must be enabled in the TreeView by setting the eventmask of the widget to allow the key press to be captured.
MyTreeView.set_events(Gdk.EventMask.KEY_PRESS_MASK) MyTreeView.key_press_event.connect(ProcessKeyPress) . <<snip>> . def ProcessKeyPress(CurrentTreeView:Widget, KeyPressed:Gdk.EventKey ) :bool /*What to do when the user presses a key while focus is on the TreeView */ CurrentTreePath:TreePath SelectedTreeCol:TreeViewColumn Position_TreeIter:TreeIter Quantity:double this.MyTreeView.get_cursor(out CurrentTreePath, out SelectedTreeCol) if CurrentTreePath != null //the CurrentTreePath is null if the focus was not on a line in the TreeView this.MyTreeStore.get_iter (out Position_TreeIter, CurrentTreePath) this.MyTreeStore.get(Position_TreeIter,COLS.QUANTITY,out Quantity,-1) case KeyPressed.str when "+" Quantity +=1 this.MyTreeStore.set_value(Position_TreeIter,COLS.QUANTITY,Quantity) when "-" Quantity -=1 this.MyTreeStore.set_value(Position_TreeIter,COLS.QUANTITY,Quantity) if Quantity <=0 this.MyTreeStore.remove(Position_TreeIter) else this.MyTreeStore.set(SaleEntryGrid_TreeIter, COLS.QUANTITY, Quantity, -1) return true
Note a thing that foxed me was that the function used by the key_press_event has the widget that called it passed to it and the KeyEvent structure - this structure appears to be defined in the Gdk package - which is required by Gtk. It is the KeyEvent.str element of the structure that holds the string of the character pressed. The function is also required to return a bool (not a void) so you must return true or false. If you return false then I was getting some weird output so returning true worked for me! Very little of this is well documented and there was a lot of trial and error.
There is no placing of widgets in a particular position, instead widgets are “packed in boxes”. There are horizontal boxes and vertical boxes.
You can set the spacing of the packing and choose to pack the boxes from the left or right in the case of horizontal boxes) or from the top or the bottom in the case of vertical boxes. Using this box system, Gtk can reshuffle widgets appropriately given any window resizing or maximising the user does.
A horizontal box object is created with two parameters – the first is called homogenous – expected to be true or false – if homogenous is true it looks as though the widgets are spread evenly across the line and the second parameter refers to the amount of spacing between widgets eg:
var A_Horizontal_Box = new Hbox(false, 0)
the defined widget objects are then packed into the boxes using:
A_Horizontal_Box.pack_start (A_Widget_Object, false, true, 0)
again the parameters used determine the spacing/alignment of the widgets within the box. Further it is possible to place already packed boxes inside other boxes in much the same way as any other widget.
Tables provide an alternative method of placement or widgets – or packing widgets into the window. They provide a grid where widgets can be placed – a widget can be placed to cover any range of cells. To make a table 4 rows down with 6 columns across:
Var A_Table = new Table(4,6,true)
The third parameter is the homogenous parameter, all cells are the same size if it is true. If false then the cells will be the same height as the tallest widget in the row and the widest widget in the column. Widgets are placed into the table using:
This would "attach" the button so that it adjoins to the left of column 2 and the right adjoins column 3 and the top adjoins row 4 and the bottom adjoins row 6. The columns are measured from the left and the rows counted from the top as 1. The top cell is therefore denoted by 0,1,0,1 - adjoins the left boundary (0) and the right of the cell adjoins column 1, the top of the cell adjoins the very top (0) and the bottom adjoins row 1. Using this syntax it is possible for widgets to consume any specified area of the table.
Are created using a heirarchy of GTK objects:
The components of a menu are illustrated above (well it was in the document but not sure how to put the image up) The whole menu structure and master object off which all the subsidiary menu objects hang is called a MenuBar object – created with code such as:
var MyAppMenuBar = new MenuBar()
Each word along the menu bar is a menu item object which launches a sub menu “Menu” object with any number of MenuItem objects of it's own which show below it when the MenuBar MenuItem label is clicked. Or put another way, the MenuItems created and appended to the MenuBar object are the ones that show on the MenuBar. Each of these MenuItems invokes a sub menu of it's own.
There are MenuItem objects and associated Menu objects for the sub-menu created for each of the File, Edit, Search, View, Document …. etc above
var ViewMenuLauncherItem = new MenuItem.with_label("_View") ViewMenuLauncherItem.set_submenu(ViewMenu) MyAppMenuBar.append(ViewMenuLauncherItem)
Perhaps the “View” sub-menu was created with the code:
var ViewMenu = new Menu()
To each menu any number of menu item objects are created. The “Show Toolbar” menu item above might have been created with:
ShowToolbar_menu_item = new CheckMenuItem.with_label("Show _Toolbar")
It then needs to be added to the ViewMenu Menu with:
Finally you need to attach a function to run when the menu item is checked:
The function – on_check needs to be defined in the same window class.
There are a number of other menu_item types that can be added to menus:
- CheckMenuItem as seen above – where the menu item can be checked when it is enabled
- SeperatorMenuItem – this is simply a line that separates menu items – these are created and added to our View Menu using code:
separator_menu_item = new SeparatorMenuItem() ViewMenu.append(separator_menu_item)
- RadioMenuItem – when there are a series of options only one of which can be chosen out of the selection
- ImageMenuItem – these have images to the left of the menu item text as per the zoom-in/zoom-out/normal-size menu items in the illustration above. These are created and added to our View Menu with code:
ZoomInMenu_item = new ImageMenuItem.from_stock(STOCK_ZOOM_IN, null) ViewMenu.append(ZoomInMenu_item)