GUI Building with KDE

Richard J. Moore


Introduction

Creating GUIs with the KDE Desktop Environment is very easy thanks to the large number of preexisting GUI components that are available for you to use, and to the framework Qt provides for developers. Unfortunately while there is a large body of public domain source code (for example the entire KDE), and detailed class documentation to learn from, there has not been a tutorial that explains how to put things together to create a GUI. The aim of this document is to provide such a tutorial. To make this discussion concrete I will develop a very simple KDE application adding such details as `Drag and Drop', URL support and online help. This document does not try to explain C++ or basic compilation issues, it is only concerned with how to put a GUI together.

To begin with we'll just create a very simple GUI consisting only of a menubar containing a single menu. The only item in the menu will be the `Exit' command. From this rather humble beginning we'll extend our application into a simple but functional text editor. This may seem like a rather formidable task for a tutorial, but as you will see by reusing the existing components provided by Qt and KDE it is made very simple in practice.  I'll give the code first then go through it in detail afterwards. All of the example code is available free for download from the KDE development website at http://developer.kde.org/examples.tar.gz.
 

A Minimal Application

We'll now examine in detail a minimal KDE Application. The application only provides a single function `Exit', but already supports floating menubars, session management and internationalisation.
 
#ifndef EDIT_H
#define EDIT_H

#include <ktopwidget.h>

class Edit : public KTopLevelWidget
{
  Q_OBJECT

public:
  Edit();

  ~Edit();

public slots:
 void commandCallback(int id_);

private:
  // Child widgets
  KMenuBar *menu;
};
#endif // EDIT_H
Edit class declaration 
 
  The first thing to look at is the class declaration, as you can see our application is implemented as the class Edit. Edit is a subclass of KTopLevelWidget (KTLW) which is the baseclass of all KDE applications. KTLW manages an applications menu bar, status bar and toolbars, and provides support for session management. The next thing in the class declaration is the `Q_OBJECT' macro, this is needed in order to support the important Qt features of `signals' and `slots'. The only slot in the Edit class is the commandCallback() method, it has been declared as a slot with `public' access by the line that reads `public slots:'. A slot is essentially a sort of typesafe callback method, it will be called by objects that the Edit class has expressed an interest in using the connect() method. The rest of the class declaration is like any other C++ class.
 
#include <kapp.h>
#include <kmenubar.h>
#include "edit.h"

const int ID_EXIT= 111;

Edit::Edit()
{
  QPopupMenu *file = new QPopupMenu;

  file->insertItem(klocale->translate("Exit"), ID_EXIT);

  menu = new KMenuBar( this );
  CHECK_PTR( menu );
  menu->insertItem( klocale->translate("File"), file );
  menu->show();
  setMenu(menu);

  // Connect things together
  connect (file, SIGNAL (activated (int)), SLOT (commandCallback (int)));
}

Edit::~Edit()
{

}

void Edit::commandCallback(int id_)
{
  switch(id_) {
  case ID_EXIT:
    exit(0);
    break;
  }
}

#include "edit.moc"
Edit class implementation

Now we'll study the implementation of our Edit class, looking first at the constructor. The constructor creates a QPopupMenu object, this is the File menu. We add the `Exit' item using the insertItem() method. The first parameter is the string to be displayed in the menu, the second is an integer id (you can find out about all the methods of a class by looking at it's class documentation). In order to prepare the applet for translation we call klocale->translate() rather than just setting the label to the literal string 'Exit', this should be done for any string the user will see. We don't have to create our own KLocale object as KApplication (the base class of all KDE application objects) creates one for us. We access the KLocate object using the klocale macro provided by kapp.h.

The next thing we need to do is create a menubar to contain our menu, we do this using a KMenuBar widget. The CHECK_PTR macro used to ensure the widget was created successfully is a Qt provided macro that checks it's parameter is not 0 (if the parameter is 0 it terminates the application with an error message). We create the 'File' item in almost the same way as we created the menu entry (the similarity should come as no suprise once you know that QPopupMenu and KMenuBar have a common baseclass QMenuData). Again we need to call KLocale as string 'File' will be visible to the user. We display our menu by calling its show() method then tell KTLW to manage our menu with setMenu().

The final stage in making our applet work is to connect the menubar entry to some code to exit the application, that's what the connect() call does. Qt applications (and hence KDE applications) use signals and slots to wire one widget to another. A widget (or any other QObject) will emit a signal whenever it's state changes, our applet uses the activated() signal that a QPopupMenu emits when a user selects a menu item. This signal must be connected to the commandCallback() slot we declared in the Edit class. As you can see the commandCallback() slot looks no different to a normal method, the only difference is the declaration that this method is a slot in the header file. commandCallback() exits the program if the id_ parameter it was passed is the constand ID_EXIT - the same constant we used to identify the 'Exit' menu item. Signals and slots can have parameters and they are treated like those of any other method. Qt checks that the type signature of the signal matches that of the slot (and reports the error if they do not match). Now we can take a look at the connect() method call in the Edit constructor.
 
  connect (file, SIGNAL (activated (int)), SLOT (commandCallback (int)));
Connecting a signal to a slot

The first parameter is the object that will omit the signal, in our case it's the QPopupMenu file. The next parameter is the signal to be connected, you must specify the name of the signal and the types of it's parameters (as they appear in the class header file or class documentation) using the SIGNAL() macro. The last parameter connect() takes is the slot specified using the SLOT() macro and specifying it's parameters as before. Qt will inform you at run time if the connection is invalid. QObject provides a number of other overloaded versions of the connect() method, you should look at the Qt class documentation for more details. It is perfectly acceptable to connect a signal to more than one slot or to connect more than one signal to a single slot.

The last feature to note about this simple applet is that we include the file edit.moc at the end of the class implementation. The `moc' file is the output of the moc preprocessor that must must be applied to any class that inherits QObject. You normally invoke moc via a makefile rather than directly and you must ensure that its output is compiled and linked to your application. The easiest way to ensure this is to simply include the moc output in the class implementation which is the approach used here. It is important to ensure that moc is rerun if you change the class declaration or you may get strange errors.

You may think that this was a lot of work just to create a completely useless applet, but we've come a long way already: We've learned about session management, creating menus, internationalisation and the basis of Qt, signals and slots. The next stage will be to make our applet actually do something!

Adding functionality

Now that we know the basics we can move on a bit more quickly, so I won't be covering the code in quite as much detail as in the last section. Our aim now is to make the applet do something, in our case we want to be able to edit text.

You can see the complete code for this version of Edit as example2 in the source files for this tutorial.

Qt provides us with QMultiLineEdit, a widget for editing text, this will form the core of our editor applet. We declare a member variable to hold the widget in the Edit class declaration like this:
 
  QMultiLineEdit *view;
Declaring the editor widget
All we need to do now is create the widget in Edits constructor and tell KTLW to manage it.
 
  view= new QMultiLineEdit(this, "Main View");
  setView(view);
Creating the editor widget

We already know how to add menu items, so adding some new ones for load and save is easy. The code is almost the same as that for the 'Exit' item (note that we now need some more constants like ID_SAVE to identify the new commands).
 
  file->insertItem(klocale->translate("Open..."), ID_OPEN );
  file->insertItem(klocale->translate("Save"), ID_SAVE);
  file->insertItem(klocale->translate("Save As..."), ID_SAVEAS);
Adding some more menu items
Notice that these entries all end in an ellipsis ('...'), this indicates that selecting this item will cause a dialog to be displayed for the user to give more information. Using an ellipsis in this way is almost universal across all GUIs and you should follow this convention religiously.

Now that we've created the menu items, all that remains is to make them do something. The code required is very straight forward. We'll look first at the `Open' command. All we need to do is create a new case in the switch statement of the commandCallback() slot, and then add the code to open a file.
 
  case ID_OPEN:
    name= QFileDialog::getOpenFileName();
    if (!name.isEmpty()) {
      load(name);
    }
    break;
Opening a file in Edit
The new code makes use of a static method of the QFileDialog class to pop up a modal file dialog and get a filename. If the user pressed the `Cancel' button then the empty string is returned so we test for this case and do nothing. If the user selected a filename then we read the file into the editor widget by calling the load method.

The load method uses the QFile class to read the contents of the selected file. The QFile class is provided by Qt and provides an easy to use platform independent way to access files. It is used here in combination with QTestStream which lets us read the contents of the file a line at time. Each line is appended to the text widget as it is read. It is important to keep track of the name of the current file name (so we can implement `Save') so we add a new instance variable filename_ to the Edit class. This value is set by the load method.
 
  void Edit::load(const char *filename) {
      QFile f( filename );
      if ( !f.open( IO_ReadOnly ) )
	  return;

      view->setAutoUpdate( FALSE );
      view->clear();

      QTextStream t(&f);
      while ( !t.eof() ) {
  	QString s = t.readLine();
	view->append( s );
      }
      f.close();

      view->setAutoUpdate( TRUE );
      view->repaint();
      filename_= filename;
  }
The implementation of the load() method.
One feature of this method to note is the use of setAutoUpdate() to prevent flickering and improve performance. The effect is to disable redraws for the text widget until the entire file is read in - otherwise the widget would be repainted for each line. It is very important not to leave a widget in this state, so when we have finished reading the file we call setAutoUpdate() again to re-enable updates. We also call repaint() to ensure the widget is properly redrawn.

The 'Save As' command is almost the same as the 'Open' command. We now use the getSaveFileName() which unlike getOpenFileName() does not require that the filename choosen should be that of an existing file. The save command is also easy: if we already know the filename then save the text, if not then call the saveAs() command.
 
    case ID_SAVE:
      if (!filename_.isEmpty())
        saveAs(filename_);
      else {
        name= QFileDialog::getSaveFileName();
        if (!name.isEmpty())
          saveAs(name);
      }
      break;
    case ID_SAVEAS:
      name= QFileDialog::getSaveFileName();
      if (!name.isEmpty())
        saveAs(name);
      break;
Saving a file in Edit
The code to actually save the file is simple, we use QFile as before, but this time we open the file with the IO_WriteOnly flag to indicate we want to open the file for output. We write the whole text as a single (multiline) string obtained with the text() method of QMultiLineEdit;
 
  void Edit::saveAs(const char *filename) {
      QFile f( filename );
      if ( !f.open( IO_WriteOnly ) )
	  return;

      QTextStream t(&f);
      t << view->text();
      f.close();

      setHint(filename);
      filename_= filename;
  }
The implementation of the saveAs() method.
The last feature of this version of the example is an `About' box. This is a dialog box usually accessed using the `About...' entry in the `Help' menu. This is implemented here by calling the static method QMessageBox::about(). `About' boxes are usually used to display information such as the version number, author and vendor of an application.


 
    case ID_ABOUT:
      QMessageBox::about(this, "About Edit",
	  	         "This is a simple text editor example program.");
      break;
The implementation of the saveAs() method.

Improving the user interface

Our applet is now capable of performing useful tasks, but its user interface is rather poor. There are a number of deficiencies such as the lack of a warning if you exit without saving changes, the lack of any display of the name of the currently open file, and a lack of keyboard shortcuts. We'll now try to rectify some of these defects.

The first thing we'll tackle is adding a status bar to our applet, we'll use it to display the name of the currently open file. KDE provides a status bar widget, KStatusBar, and KTLW knows how to manage it, so there's not much left for us to do. KStatusBar has an API very similar to that of the menu widgets so you should be able to figure out what's going on. Note that after showing our status bar we tell KTLW to manage it. We can change the string displayed on the status bar using the id constant ID_HINTTEXT, this is implemented in the slot setHint().
 
  void Edit::initStatusBar()
  {
    statusbar= new KStatusBar(this);
    statusbar->insertItem("Welcome to Edit", ID_HINTTEXT);
    statusbar->show();
    setStatusBar(statusbar);
  }

  void Edit::setHint(const char *text)
  {
    statusbar->changeItem(text, ID_HINTTEXT);
  }

Adding a status bar.

We set the string in the status bar when the user invokes any method that changes the filename, for example loading and saving.

We'll now add a toolbar to Edit, this is slightly more complicated as we need to assign icons to the buttons and setup the tooltips. We make use of the KIconLoader class to handle loading (and caching) of the toolbar icons. This class searches in the directories specified by the KDE File System Standard for an icon, which as we're only using standard icons, suits us fine. We use a macro provided by kapp.h to access the icon loader created by KApplication.
 
  void Edit::initToolBar()
  {
    KIconLoader *loader = kapp->getIconLoader();
    toolbar = new KToolBar(this);

    toolbar->insertButton(loader->loadIcon("filenew.xpm"),
			  ID_NEW, TRUE,
			  klocale->translate("Create a new file"));
    toolbar->insertButton(loader->loadIcon("fileopen.xpm"),
	 		  ID_OPEN, FALSE,
			  klocale->translate("Open a file"));
    toolbar->insertButton(loader->loadIcon("filefloppy.xpm"),
			  ID_SAVE, FALSE,
			  klocale->translate("Save the current file"));

    addToolBar(toolbar);
    toolbar->setBarPos(KToolBar::Top);
    toolbar->show();
    connect(toolbar, SIGNAL(clicked(int)), SLOT(commandCallback(int)));
  }

Adding a toolbar.
Each toolbar item is created by a call to insertButton(). This method takes the icon (accessed using KIconLoader), the command id, a flag to indicated if the button is enabled and the text for the tooltip as parameters. As usual we allow for i18n support and wrap the tooltip in a call to klocale->translate().

We connect the toolbars clicked signal to the same slot as we use for the menus. This is perfectly safe as we are using the same id numbers for commands in each, and we are free to connect as many signals as we want to a slot.

It is generally good practice when designing a user interface to prevent the user invoking an operation that cannot be performed. We do this in Qt by enabling and disabling widgets depending on the state of the application. For example if you have made no changes to a document then saving to its original file makes no sense (though saving it to a different file is often useful). All Qt and KDE widgets have a method setEnabled() which is used to `grey out' the widget and disable it.

KMenuBar and KToolbar both make the process of enabling and disabling their entries easy by providing a method which will set the state of an entry specified by its command id. We call both of these in the implementation of enableCommand().
 
  void Edit::enableCommand(int id) //SLOT
  {
    toolbar->setItemEnabled(id, true);
    menu->setItemEnabled(id, true);
  }

  void Edit::disableCommand(int id) // SLOT
  {
    toolbar->setItemEnabled(id, false);
    menu->setItemEnabled(id, false);
  }
Enabling and disabling commands.
We shall now use these methods to implement context sensitive enabling and disabling of commands. To make the `Save' command disabled when there are no changes to save we need to keep track of when the text is modified, this is dome using the textChanged() signal emitted by QMultiLineEdit when the user edits the text. Using this signal we can create a flag to indicate if the text has been changed. This is handled by the textChanged() slot defined by the Edit class. This slot also updates the state of the commands.
 
void Edit::textChanged()
{
  modified= true;
  enableCommand(ID_SAVE);
  enableCommand(ID_SAVEAS);
}
The definition of the textChanged() slot.
Note that the load and save methods must also be modified to keep the flag up to date, and to enable or disable commands. You can see these changes for yourself by looking at the example programs.

The final thing we will add to our editor in this section is a confirmation dialog to be shown when the user tries to exit when there are unsaved changes in the editor. The dialog is a standard Qt warning dialog.

The dialog is implemented using the exit() method. This method simply tests the flag we created to indicate modifications, and uses another static method of QMessageBox to display a warning dialog.


 
  int Edit::exit()
  {
    int die= 0;

    if (!modified)
      die= 1;
    else
      if (QMessageBox::warning(this, klocale->translate("Unsaved Changes"),
  			       "You have unsaved changes, you will loose "
			       "them if you exit now.",
			       "Exit", "Cancel",
			       0, 1, 1))
         die= 0;
      else
        die= 1;

    return die;
  }
The definition of the exit() method.

Forthcoming...


This document is Copyright Richard J. Moore 1997-1998 (rich@kde.org)
You are free to distribute the examples given here in any form what so ever, and under any license terms. You are however NOT free to do the same with the text of the tutorial itself. You are permitted to distribute the text of the tutorial in electronic formats as long as it is distributed in its entirity. You may not charge for this document, though you may recoup packaging costs. You may make paper copies for personal use only. If you want to distribute this under other terms then drop me a mail.