Creating a GUI with GTKDialog

by musonio

Last month Leiche provided a bash/Zenity script (now in the repositories) to act as a GUI to several useful command line applications and commands. I had a few hours to spare and play around trying to decorate the script with GTKDIALOG, and the result was the following:

pic1
pic2

GTKDIALOG is similar to ZENITY or KDIALOG, but it is extremely customizable and allows you to arrange the different widgets (buttons, labels, menus, lists, etc.) in infinite combinations.

The use I put it to in the script is absolutely basic. I have only used it to decorate a bash script, with no further features than pushing buttons. Since the possibilities are endless, I will only comment on the alternatives that are actually used in the script. (Further Links are provided below).

The script only aims at providing a handy access to system files that I usually need to read/edit and to command line utilities that I am too lazy to write in a terminal. Since I have chosen Leiche's commands and a few other things I personally find useful, it is the result of personal taste.

The question is, precisely, building it according to one's preferences. It is mostly a question of trial and error, but also a question of being orderly; I guess that the key, for beginners like me, lies in keeping a correct disposition of the tabs (or indentations), which allows to see clearly where each section begins and ends and to check whether there are any unclosed tags.

Buttons, frames and menus can be rearranged as you like, and the only thing you need to understand is how to define the ACTION fields.

To present an example, let's take a look at the lower section of the GUI:

pic3

Structure: a Frame (the line on top; with no label) that contains an Horizontal box that contains three items (button, text, button).

Hence:

<frame>
<hbox>
  <button></button>
  <text></text>
  <button></button>
</hbox>
</frame>
pic4

Structure: a Frame(the line on top)with a Label (Commands Information) that contains two Horizontal boxes (the first of them contains a TEXT and an ENTRY field; the second one contains 6 buttons).

Hence:

<frame Label>
<hbox>
  <text><label></label></text>
  <entry><variable></variable></entry>
</hbox>
<hbox>
  <button></button>      (x6)
</hbox>
</frame>

GTKDIALOGS

Gtkdialogs are like functions: they have to be builtbefore you can actually execute them.

That is why, in our script, it takes the following form:

export MAIN_DIALOG='
<window>
  <vbox>
    .................
  </vbox>
</window>
'
gtkdialog --program=MAIN_DIALOG

It is only this last line that actually executes the dialog. If it were not present, the script would do absolutely nothing.

TAGS

GTKDIALOG is, according to the project's home page, "a small utility for fast and easy GUI building. It can be used to create dialog boxes for almost any interpreted and compiled programs which is a very attractive feature since the developer does not have to learn various GUI languages for the miscellaneous programming languages".

GTKdialogs are written in an XML-like language: every widget is inserted through tags that need its closing pair. If it is not found, or if it not properly placed, the script cannot be executed.

Tags are nested as in the following example:

<window>
<vbox>
  <button>
    <label>Kate</label>
    <action>kate</action>
  </button>
</vbox>
</window>

(Actually, the window and vbox tags are idle in this example, since they contain only one element.)

The real action takes place, in our script, only inside the buttons area; hboxes, vboxes, frames and menus only provide containers for those buttons. How do we specificy what action should take place when we click on an icon? We simply write the command we want to execute inside the ACTION tag. Since this is a bash script, we just use the commands we would use in a bash script with a few slight modifications that will be noted later.

The main widgets we will use in the script are the following:

WINDOW

The window tag, which is not always necessary, allows you to specify some global attributes, such as the size of the window, if it is resizable or not, if it must skip the taskbar, etc.

Some useful options:

window_position="x"
width_request="x"
height_request="x"
title="_"                  (default is "gtk-dialog")
maximize_initially="true"  (default is false)
skip_taskbar_hint="true"   (default is false)

VBOX and HBOX

A vbox (vertical box) and and hbox (horizontal box) which can hold buttons, menus, text, lists, etc., or even other vboxes and hboxes, as we will see in the example.

HBoxes admit two main properties:

homogeneous="true"
States if the other widgets inside the box are distributed evenly (Default is no, which piles up all the widgets on the right of the hbox).
spacing="x"
Specifies the spacing between icons.

Both options have to be specified inside the first tag:

<hbox homogeneous="true">
</hbox>

or:

<hbox spacing="20">
</hbox>

or:

<hbox homogeneous="true" spacing="20">
</hbox>

FRAMES

Frames are useful for grouping together several widgets. The label must be stated inside the first tag. If no label is provided, a simple line is drawn.

MENUS

The simplest form of a Menu has the following structure:

<menubar>
<menu>
  <menuitem>
    <label></label>
    <action></action>
  </menuitem>
  <label></label>
</menu>
</menubar>

As is obvious, you can insert as menu items as you wish, provided you place them correctly.

As in the script, you can also decorate a Menu by creating an horizontal box that contains both the image and the menu:

<hbox>
<pixmap>
  <input file>path_to_file</input>
</pixmap>
<menubar>
  <menu>
    <menuitem>
      <label></label>
      <action></action>
    </menuitem>
    <label></label>
  </menu>
</menubar>
</hbox>

ENTRY

Entries are fields that give the user the possibility on providing an input.

In the case of the Kernel Messages and Commands Information, the value of the entry is assigned to a variable, which is then "processed" by the ACTION defined by the button we choose to press.

BUTTON

There are two main types of buttons:

  1. predefined buttons
  2. custom buttons: text and image buttons; text only buttons; image only buttons

Predefined buttons:

  • <button help></button>
  • <button yes></button>
  • <button no></button>
  • <button ok></button>
  • <button cancel></button>

(see Gtkdialog Examples for screenshots of predefined buttons).

  • Custom text only button
    <button>
    <label>Kate editor</label>
    <action>kate</action>
    </button>
    
    pic5
  • Custom image only button
    <button>
    <input file>/usr/share/pixmaps/VBox.png</input>
    <action>kate</action>
    </button>
    
    pic6
  • Custom text and image button
    <button>
    <input file>/usr/share/pixmaps/VBox.png</input>
    <label>Virtual Box</label>
    <action>kate</action>
    </button>
    
    pic7

As I said before, this is an extremely basic example of GTKDIALOG. Other interesting alternatives are notebooks, radiolists, fully customizable text dialogs, among others. (See the links below for instructions on how to use them).

A few things concerning syntax

  1. Since the syntax and structure of bash scripts have been covered (concerning their basic elements) in previous issues and excellent tutorials on bash scripts can be found on the web, I will only comment on a few things about the script.
  2. As you can see, we have defined a bunch of variables at the beginning of the script. This has two advantages: i) it allows us to write only the name of the variable inside the gtkdialog, and avoid writing the whole path. ii) If we decide to change the file to be shown through the GUI, we just modify the definition of the variable.
  3. One extremely important detail about gtkdialog is that expanded variables sometimes have to be protected, depending on the command string, with single quotes; otherwise, they are not recognized as such.

    In a regular bash script, for example, you would have the following:

    VAR1=~/.kde/share
    VAR2="I'm not a big fan of PCLinuxOS. I just can't live without it"
    echo $VAR1
    echo "$VAR2"
    

    Inside the GTKDIALOG, this may have to be turned into:

    echo '$VAR1'
    echo '"$VAR2"'
    
  4. A couple of lines from the script are worth considering:

    kdesu "dmidecode | head -15 | zenity --text-info --width=700 --height=500 --title \"BIOS information\""

    Three things must considered in this line:

    1. i) The string of commands to be run as root (i.e., after kdesu) must be grouped together or protectedby double quotes.
    2. ii) There is a double piping: dmidecode produces an output, which is piped to head. What does head do? It outputs only the first lines o the input (in this case, the 15 first lines). This last output is then passed on to a zenity window.
    3. iii) The double quotes inside the string that we pass tokdesumust be preceded by a backslash, which is not the case with other commands we have used, such as the zenity text-info dialogs.

    KeyRPM=(`zenity --entry --text "Enter search word:"`) ; rpm -qa | grep "$KeyRPM" | zenity --text-info --width=900 --height=600 --title $""$KeyRPM" in installed RPMS" &

    1. i) The variable is defined inside the action tag itself and expanded in the same line (though linked by a semi-colon).
    2. ii) grep "filters" the output of rpm -qa on the basis of the value of the variable.
    3. iii) The final "&" states that the process will run in the background. This allows you to continue using the main dialog window even if you haven't closed the window launched by the commands that precede that symbol.

Extra tips

  1. You can group together with { } any number of commands before sending their output to a Zenity text dialog.

    For instance, the section on Command Information, which looks like this:

    pic8

    can be turned into the following:

    pic9

    The "Help..." button groups together the rest of the commands (except for manual):

    <action>{ echo "WHATIS" && whatis $VAR1 && echo "WHEREIS" && whereis $VAR1 && echo "WHICH" && which $VAR1 && echo "VERSION" && $VAR1 --version && echo "HELP" && $VAR1 --help ; } | zenity --text-info --width=600 --height=600 --title $"Help..." &</action>

  2. You can turn the main window into a notebook, with as many label as you want:

    pic10

    All you need to do is define the notebook in the first line of the GTKDIALOG, and create a VBox for each label:

    <notebook labels="System|System Files|Notebook">
      <vbox></vbox>
      <vbox></vbox>
      <vbox></vbox>
    </notebook>
    

Further Links

My Script

#! /bin/bash
Encoding=UTF-8

FILE1=/etc/X11/xorg.conf
FILE2=/etc/fstab
FILE3=/boot/grub/menu.lst
FILE4=/etc/apt/sources.list
FILE5=/etc/conky/conky.conf
FILE6=/etc/rc.local
FILE7=/etc/sudoers
FILE8=~/.bashrc
FILE9=/root/.bashrc

export MAIN_DIALOG='
<window window_position="1" title="System Tools">

<vbox>
  <hbox homogeneous="True">
    <frame>
    <hbox homogeneous="True">
      <hbox>
        <button>
          <input file>'$HOME'/.kde/share/icons/ananke/konsole_60.png</input>
          <action>konsole &</action>
        </button>  
      </hbox>

      <vbox homogeneous="True">
        <button>
          <label>TOP</label>
          <action>konsole -e top &</action>
        </button>

        <button>
          <input file>'$HOME'/.kde/share/icons/ananke/root.png</input>
          <action>kdesu "konsole -e top" &</action>
        </button>
      </vbox>
    </hbox>
    </frame>

    <vbox>
      <frame Kernel messages>

        <button>
          <label>View messages</label>
          <action>dmesg | zenity --text-info  --width=700 --height=500 --title $"View kernel messages" &</action>
        </button>

        <entry><variable>VAR2</variable></entry>

        <hbox>
          <button>
            <label>search</label>
            <input file>'$HOME'/.kde/share/icons/ananke/search.png</input>
            <action>dmesg | grep $VAR2 | zenity --text-info  --width=700 --height=500 --title $"$VAR2 in kernel messages" &</action>
          </button>

          <button>
            <label>search & save</label>
            <input file>'$HOME'/.kde/share/icons/ananke/save.png</input>
            <action>dmesg | grep $VAR2 > '$HOME'/dmesg_output_$VAR2.txt &</action>
            <action>kate '$HOME'/dmesg_output_$VAR2.txt</action>
          </button>
        </hbox>
        </frame>
      </vbox>
    </hbox>

    <frame Commands Information>
    <hbox>
      <text> <label>Command:</label> </text>
      <entry><variable>VAR1</variable></entry>
    </hbox>

    <hbox>
      <button>
        <label>Help</label>
        <action>$VAR1 --help | zenity --text-info --width=600 --height=600 --title $"Help" &</action>
      </button>

      <button>
        <label>Whereis</label>
        <action>whereis $VAR1 | zenity --text-info  --width=400 --height=20 --title $"Whereis" &</action>
      </button>

      <button>
        <label>Which</label>
        <action>which $VAR1 | zenity --text-info --width=200 --height=200 --title $"Version" &</action>
      </button>

      <button>
        <label>Version</label>
        <action>$VAR1 --version | zenity --text-info --width=200 --height=200 --title $"Version" &</action>
      </button>

      <button>
        <label>Manual</label>
        <action>man $VAR1 | zenity --text-info --width=400 --height=500 --title $"Manual" &</action>
      </button>

      <button>
        <label>Kate Manual</label>
        <action>man $VAR1 >>'$HOME'/tmp/temp_man.txt</action>
        <action>kate '$HOME'/tmp/temp_man.txt</action>
        <action>rm -f '$HOME'/tmp/temp_man.txt</action>
      </button>
    </hbox>
    </frame>
    <hbox homogeneous="True">

    <frame>
      <vbox>
        <button>
          <input file>'$HOME'/.kde/share/icons/ananke/gkrellm.png</input>
          <label>Sensors</label>
          <action>sensors | zenity --text-info  --width=700 --height=500 --title $"Sensors" &</action>
        </button>

        <button>
          <input file>'$HOME'/.kde/share/icons/ananke/ethernet.png</input>
          <label>Ethernet Interfaces</label>
          <action>ifconfig | zenity --text-info  --width=700 --height=500 --title $"View an ethernet network interface" &</action>
        </button>

        <button>
          <input file>'$HOME'/.kde/share/icons/ananke/wifi.png</input>
          <label>Wireless Interfaces</label>
          <action>iwconfig | zenity --text-info  --width=700 --height=500 --title $"Current wireless network interface" &</action>
        </button>

        <button>
          <input file>'$HOME'/.kde/share/icons/ananke/synaptic.png</input>
          <label>Synaptic logs</label>
          <action>kdesu "konqueror /root/.synaptic/log" &</action>
        </button>

        <button>
          <input file>'$HOME'/.kde/share/icons/ananke/hardware.png</input>
          <label>Hardware information </label>
          <action>kdesu "lshw | zenity --text-info  --width=700 --height=500 --title \"View detailed information about the hardware\"" &</action>
        </button>

        <button>
          <input file>'$HOME'/.kde/share/icons/ananke/bios.png</input>
          <label>BIOS information</label>
          <action>kdesu "dmidecode | head -15 | zenity --text-info  --width=700 --height=500 --title \"BIOS information\"" &</action>
        </button>

        <button>
          <input file>'$HOME'/.kde/share/icons/ananke/sys1.png</input>
          <label>PCI devices</label>
          <action>lspci | zenity --text-info  --width=700 --height=500 --title $"PCI devices" &</action>
        </button>
      </vbox>
    </frame>

    <frame>
      <vbox>
        <hbox>
          <pixmap>
            <input file>'$HOME'/.kde/share/icons/ananke/kde.png</input>
          </pixmap>

          <menubar>
          <menu>
            <menuitem>
              <label>Running kernel</label>
              <action>uname -a | zenity --text-info  --width=900 --height=20 --title $"Current running kernel" &</action>
            </menuitem>

            <menuitem>
              <label>KDE Version</label>
              <action>kde-config --version | grep KDE | zenity --text-info  --width=60 --title $"KDE Version" &</action>
            </menuitem>

            <menuitem>
              <label>Show Path</label>
              <action>echo '$PATH' | zenity --text-info  --width=900 --height=10 --title $"PATH" &</action>
            </menuitem>
          <label>System Information</label>
          </menu>
          </menubar>
        </hbox>

        <hbox>
          <pixmap>
            <input file>'$HOME'/.kde/share/icons/ananke/rpm.png</input>
          </pixmap>

          <menubar>
          <menu>
            <menuitem>
              <label>View Installed Applications</label>
              <action>rpm -qa | sort | zenity --text-info  --width=900 --height=600 --title $"Installed RPMS" &</action>
            </menuitem>

            <menuitem>
              <label>Open list of Installed Applications</label>
              <action>zenity --info --text "I will open a list of the installed RPMs in a temporary text file. \\nIf you want to keep it, save it in a different location, since the temporary file will be removed."</action>
              <action>rpm -qa | sort >> '$HOME'/tmp/installed_rpms.txt</action>
              <action>kate '$HOME'/tmp/installed_rpms.txt</action>
            </menuitem>

            <menuitem>
              <label>Search in list of installed Applications</label>
              <action>KeyRPM=(`zenity --entry --text "Enter search word:"`) ; rpm -qa | grep "$KeyRPM" | zenity --text-info  --width=900 --height=600 --title $""$KeyRPM" in installed RPMS" &</action>
            </menuitem>

          <label>Installed Applications</label>
          </menu>
          </menubar>
        </hbox>

        <hbox>
          <pixmap>
            <input file>'$HOME'/.kde/share/icons/ananke/devices.png</input>
          </pixmap>

          <menubar>
          <menu>
            <menuitem>
              <label>Hard drive partitions</label>
              <action>kdesu "fdisk -l | zenity --text-info  --width=700 --height=500 --title $"Hard drive partitions"" &</action>
            </menuitem>

            <menuitem>
              <label>Hard drive UUID</label>
              <action>blkid | zenity --text-info  --width=700 --height=500 --title $"Hard Drive UUID" &</action>
            </menuitem>

              <menuitem>
              <label>Current mount points</label>
              <action>mount | zenity --text-info  --width=700 --height=500 --title $"Current mount points" & </action>
            </menuitem>

            <menuitem>
              <label>Available disk space</label>
              <action>df -h | zenity --text-info  --width=700 --height=500 --title $"Available disk space"  &</action>
            </menuitem>

            <menuitem>
              <label>Connected USB devices</label>
              <action>lsusb | zenity --text-info  --width=700 --height=500 --title $"Connected USB devices" &</action>
            </menuitem>

          <label>Devices</label>
          </menu>
          </menubar>
        </hbox>

        <hbox>
          <pixmap>
            <input file>'$HOME'/.kde/share/icons/ananke/x.png</input>
          </pixmap>

          <menubar>
          <menu>
            <menuitem>
              <label>X-Server information</label>
              <action>xdpyinfo | zenity --text-info  --width=700 --height=500 --title $"Information about the X-server" &</action>
            </menuitem>

            <menuitem>
              <label>GLX/OpenGL Information</label>
              <action>glxinfo | zenity --text-info  --width=700 --height=500 --title $"Information about glx and opengl" & </action>
            </menuitem>

          <label>X-Server</label>
          </menu>
          </menubar>
        </hbox>

        <hbox>
          <pixmap>
            <input file>'$HOME'/.kde/share/icons/ananke/groups.png</input>
          </pixmap>

          <menubar>
          <menu>
            <menuitem>
              <label>Group memberships</label>
              <action>groups | zenity --text-info  --width=700 --height=100 --title $"View group memberships" &</action>
            </menuitem>

            <menuitem>
              <label>Groups</label>
              <action>cat /etc/group | zenity --text-info  --width=700 --height=500 --title $"View Groups" &</action>
            </menuitem>

          <label>Groups</label>
          </menu>
          </menubar>
        </hbox>

        <button>
          <label>Loaded modules</label>
          <action>lsmod | zenity --text-info  --width=700 --height=500 --title $"View loaded modules" &</action>
        </button>

        <button>
          <label>Services</label>
          <action>chkconfig --list | zenity --text-info  --width=900 --height=600 --title $"View Services" &</action>
        </button>

      </vbox>
    </frame>
    </hbox>

    <hbox>
      <frame System Files>
        <hbox>
          <button><label>'"$FILE1"'</label><action>zenity --title='"$FILE1"' --text-info --width 500 --height 400 --filename='"$FILE1"' &</action></button>
          <button><input file>'$HOME'/.kde/share/icons/ananke/root.png</input><action>kdesu "kate '"$FILE1"'" &</action></button>
        </hbox>

        <hbox>
          <button><label>'"$FILE2"'</label><action>zenity --title='"$FILE2"' --text-info --width 500 --height 400 --filename='"$FILE2"' &</action></button>
          <button><input file>'$HOME'/.kde/share/icons/ananke/root.png</input><action>kdesu "kate '"$FILE2"'" &</action></button>
        </hbox>

        <hbox>
          <button><label>'"$FILE3"'</label><action>zenity --title='"$FILE3"' --text-info --width 500 --height 400 --filename='"$FILE3"' &</action></button>
          <button><input file>'$HOME'/.kde/share/icons/ananke/root.png</input><action>kdesu "kate '"$FILE3"'" &</action></button>
        </hbox>

        <hbox>
          <button><label>'"$FILE4"'</label><action>zenity --title='"$FILE4"' --text-info --width 500 --height 400 --filename='"$FILE4"' &</action></button>
          <button><input file>'$HOME'/.kde/share/icons/ananke/root.png</input><action>kdesu "kate '"$FILE4"'" &</action></button>
        </hbox>
      </frame>

      <frame>
        <hbox>
          <button><label>'"$FILE5"'</label><action>zenity --title='"$FILE5"' --text-info --width 500 --height 400 --filename='"$FILE5"' &</action></button>
          <button><input file>'$HOME'/.kde/share/icons/ananke/root.png</input><action>kdesu "kate '"$FILE5"'" &</action></button>
        </hbox>

        <hbox>
          <button><label>'"$FILE6"'</label><action>zenity --title='"$FILE6"' --text-info --width 500 --height 400 --filename='"$FILE6"' &</action></button>
          <button><input file>'$HOME'/.kde/share/icons/ananke/root.png</input><action>kdesu "kate '"$FILE6"'" &</action></button>
        </hbox>

        <hbox>
          <button>
            <label>'"$FILE7"'</label>
            <action>kdesu "zenity --title='"$FILE7"' --text-info --width 500 --height 400 --filename='"$FILE7"'" &</action>
          </button>

          <button>
            <input file>'$HOME'/.kde/share/icons/ananke/root.png</input>
            <action>kdesu "kate '"$FILE7"'" &</action>
          </button>

        </hbox>

        <hbox>
          <button>
            <label>'"$FILE8"'</label>
            <action>kate '"$FILE8"' &</action>
          </button>

          <button>
            <input file>'$HOME'/.kde/share/icons/ananke/root.png</input>
            <action>kdesu "kate '"$FILE9"'" &</action>
          </button>

        </hbox>
      </frame>
    </hbox>

    <frame>
    <hbox homogeneous="True">
      <button>
        <input file>'$HOME'/.kde/share/icons/ananke/info.png</input>
        <action>zenity --question --text "To be able to perform all the operations, the following apps must be installed: \\n\\ndmidecode lm_sensors lshw mesa-demos xdpyinfo \\n\\nDo you want to install them if they are not already installed?"; if [ "$?" = 0 ]; then kdesu "konsole --noclose -e apt-get install mesa-demos xdpyinfo lm_sensors dmidecode lshw"; fi</action>
      </button>

      <text use-markup="true"><label>"<span color='"'blue'"' font-family='"'purisa'"' weight='"'bold'"' size='"'large'"'><small>System Tools</small></span>"</label></text>

      <button>
        <input file>'$HOME'/.kde/share/icons/ananke/exit.png</input>
        <action type="exit">exit 0</action>
      </button>
    </hbox>
    </frame>
  </vbox>
  </window>
  '

  gtkdialog --program=MAIN_DIALOG