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 |
#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" |
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))); |
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; |
view= new QMultiLineEdit(this, "Main View"); setView(view); |
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); |
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; |
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 '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; |
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; } |
case ID_ABOUT: QMessageBox::about(this, "About Edit", "This is a simple text editor example program."); break; |
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); } |
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))); } |
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); } |
void Edit::textChanged() { modified= true; enableCommand(ID_SAVE); enableCommand(ID_SAVEAS); } |
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; } |
Forthcoming...
- Adding kdehelp support
- QToolTipGroup
- Using KConfig
- Drag and drop
- Adding network file access
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.