The K Desktop Environment

Next Previous Table of Contents

10. Extending the GUI

As we have seen, we have already provided KScribble the ability to open and save pictures with the document class and enabled user interaction by overwriting virtual methods in the view class and we gained the first functionaliy - we can draw pictures as well. But when we created the QPen instance in the document class, we set some pre-defined values for the pen; the color is black and the pen width set to 3 pixels. As you usually want to change these values in a drawing application, we have to enhance the main GUI by providing ways to set these, according to the currently active window and document connected to it. This chapter will therefore introduce you to:

Further, we also add a method to delete the document contents at all with a menubar command.

10.1 Adding the "Pen" Menu

As the title of this section says, we will add a menu for setting the pen values of the documents here. Menus that are inserted into the menubar are instances of QPopupMenu, and you can have a look at how the current menubar is created when you switch to the KScribbleApp class, method initMenubar(). You will see that the menubar items are created in the order they appear on the menubar - but this isn't necessary. There are two things important on how the menubar will look like:

Last but not least you have to create menus first with calling the constructor. The class declaration already contains the pointers to the popup menus, so we will have to add our "Pen" menu here first:


kscribbleapp.h

class KScribbleApp
{
.
.
  private:
    QPopupMenu* pPenMenu;

}

Now we are going to create the menu itself. Change to the implementation of the method KScribbleApp::initMenuBar() and add the lines marked with an arrow:


void KScribbleApp::initMenuBar()
{
..

->  ///////////////////////////////////////////////////////////////////
->  // menuBar entry pen-Menu
->  pPenMenu = new QPopupMenu();
->  pPenMenu->insertItem(i18n("&Color"), ID_PEN_COLOR);
->  pPenMenu->insertItem(i18n("&Brush"), ID_PEN_BRUSH);

    menuBar()->insertItem(i18n("&Edit"), pEditMenu);
->  menuBar()->insertItem(i18n("&Pen"), pPenMenu);
    menuBar()->insertItem(i18n("&View"), pViewMenu);


->  connect(pPenMenu, SIGNAL(activated(int)), SLOT(commandCallback(int)));
->  connect(pPenMenu, SIGNAL(highlighted(int)), SLOT(statusCallback(int)));
}

You see that we first create the menu with new QPopupMenu(). Then we use the insertItem methods to add two menu entries, Color and Brush. The visible commands are inserted with the method i18n(), which ensures that you can internationalize your appliction. So as a general rule, you would declare all visual text that will appear later by the method i18n(). Qt-only programs using Qt > 2.0 would use the according method tr() instead of i18n(), as Qt has it's own ways of internationalizing applications. The second argument is a macro, the ID of the menubar item. This ID is a number that we have to set using #define in the file resource.h, where you will see all other already used ID's declared. There are also other ways to insert menus by directly connecting a slot to the inserted entry, but the application framework uses ID's to select which action has been activated- and highlighted. Therefore each menu entry, independent of the popup menu it appears, has to be a unique number, and as we can hardly remember numbers later, setting a #define for the ID is a nice solution. The popup menu is now inserted into the menubar with insertItem() as well, and with the pointer to the menu as second argument.

Note that we inserted the popup menu after the "Edit" menu and before the "View" menu, so it will appear between those menus later in the menubar. What is also important when creating menus is that they should be available to the user with shortcuts; ususally in menus you will see underlined characters that the user can jump to directly by pressing ALT and the according underlined letter of the menuitem. As a programmer, you have to set this character by a leading ampersand, so the "Pen" menu will later be accessible via the keyboard by pressing ALT+P. Within the menu, the user can press another button to go directly to the command he wants to, so in the menu all items should have this kind of shortcuts as well. Note that you should write item insertions together in groups that have the same visible access, so you can keep a better overview of the characters you already used so that there are no menu accelerators used twice. (this is also important for your translators: in other languages the used accelerator may not be available in the translated word, so they have to set some accelerators again.)

In the last two lines we're connecting the pen menu with two slots: one for when the menu signals that it is activated and the action should be executed, and one for when it is highlighted. That allows making a statusbar help message available for the user. You can have a look at the methods the menu is connected to, they contain switch statements where the sent menu ID is compared and the following action called.

What is left to do is to add the #define statements to the file resource.h:


resource.h

///////////////////////////////////////////////////////////////////
// Pen-menu entries
#define ID_PEN_COLOR                14010
#define ID_PEN_BRUSH                14020

You will see that the numbers are unique for these entries- you have to watch out not to set the same number for two entries- but if it happens by accident, there's still the compiler that informs you about redefining.

This is currently all you have to do to add a new menu for your menubar. The actions they will execute are: "Color" will call a color selection dialog, "Brush" will call a dialog (which we still have to create) to select the brush width.

But first we'll extend the toolbar as well by two icons for these actions in the next section.

10.2 Adding Toolbar Buttons

Whenever you think that some new commands should be made available by toolbar buttons as well because they are often used and you want to offer additional functionality, you can easily do that by adding buttons in the framework's initToolBar() method of the App class. Here, we decide to add a button for both menu entries in the Pen-menu, but those need icons - which you can either find in the KDE directory /toolbar or, when you don't find an icon that matches your action, have to create yourself. KIconEdit is very suitable to paint icons, so we will first create them. Choose "New" from the KDevelop "File" menu and select "Icon" as the filetype. The first icon will be named "pencolor.xpm". Now we have to select where we want to have the icon created in our project directory. Press the directory selection button and change to your project directory containing the KScribble sources. Then create a new directory "toolbar". Change to that directory and press "OK". The new icon will then be created in the new directory "toolbar" and will be opened by KIconEdit within KDevelop automatically. Paint something that will signalize the user what the button is intended to do, save the pixmap and then switch to the RFV / LFV in KDevelop. Select the icon by a right mouse button press and select "Properties" from the popup menu. You will see that the icon is included in the distribution, but for your program to find the icon again later, you have to set the installation destination as well. Check the "install" option and enter into the line now active below:

$(kde_datadir)/kscribble/toolbar/pencolor.xpm

This will install the pixmap in the KDE file system hierarchy's data directory, where each application has its subdirectory containing additional files needed by the application. Icons have to be installed into another subdirectory "toolbar", so the application's icon loader can find the pixmaps for your program.

After you're finished, repeat all these above steps with the second icon for selecting the pen width. Name this pixmap "penwidth.xpm".

Now we only have to insert the buttons into the toolbar; add the lines marked with the arrow into your code:


void KScribbleApp::initToolBar()
{
..
    toolBar()->insertButton(BarIcon("editcopy"), ID_EDIT_COPY, true, i18n("Copy"));
    toolBar()->insertButton(BarIcon("editpaste"), ID_EDIT_PASTE, true, i18n("Paste"));
    toolBar()->insertSeparator();
->  toolBar()->insertButton(BarIcon("pencolor"), ID_PEN_COLOR, true, i18n("Color") );
->  toolBar()->insertButton(BarIcon("penwidth"), ID_PEN_BRUSH, true, i18n("Width") );
->  toolBar()->insertSeparator();
    toolBar()->insertButton(BarIcon("help"), ID_HELP_CONTENTS, SIGNAL(clicked()),
..
}

Here, we use the methods of KToolBar to insert buttons. The first argument, BarIcon(), tells the method to load the icon for the button. What seems unusual is that we don't have to care for the file extension. The preferred format for KDE 2 is *.PNG, but it works with xpm's as well. (You could use ImageMagick for that as well which can do that- or use KScribble in a later step to convert your icons to PNG !)

The second argument is again the ID. The commands are then automatically activated, as the toolBar() is already connected to the same methods as the menubar is for signal activated(). The third argument stands for "available" when true, "deactivated" when false; as we want to have these available, we set this to true. At last, we add a tooltip for the the buttons, which we also embrace with i18n() to allow internationalization.

Now you're done for now- the GUI is extended at least visually. You can compile and run KScribble again and see how it looks like- of course the new items in the menubar and toolbar can't execute any action - that is what we're going to add in the next section. You will also note that the toolbar icons we added are not displayed - which is because we didn't install KScribble and so they can't be found. All other used icons are already shipped with the KDE libraries, so these are already visible.

10.3 Creating the Pen Width Dialog

As we´ve already created the according menubar and toolbar commands, we now have to build the first dialog to set the pen width. For this, select "New" from the KDevelop "File" menu and select "Qt/KDE Dialog". Then enter the dialog file name as kpenbrushdlg, the extension will be automatically added. Enter "OK" and the dialogeditor opens an empty widget that will be our dialog background. When constructing a dialog, we have to think about what is really needed by the user; here, we need a label to display what will be set; a spinbox with up and down buttons to set the pen width value and three buttons, one for resetting the pen width to the default value, one to cancel the dialog and one for taking over the new value - the OK button. In this order we will add the items to the dialog - which is important because the tab-focus follows the order by which the widgets are created. So if you´re starting with the OK button, then the spinbox and then the cancel button, the input focus will change from the ok button to the spinbox and then to the cancel button - which is not what the user expects. The tab focus should follow the widget´s items top-down from left to right, so we have to construct the dialog in this order as well. To add items to the dialog, select the "Widgets" tab on the left pane. There you have all available widgets present by icons to construct your dialog. Pressing a widget button will create the new item and place it at the top-left corner of the widget. From there, you can place it with the mouse to the position you would like it to show up. Further, when a widget item is selected, you can set the according values in the "Widget Properties" pane on the right.

The Label: press the "QLabel" button on the "Widgets" tab and place it at position x:50, y:20. Then select the "General" section in the widget properties pane. Change the text in properity "Text" from "Label" to "Pen Width:". Adjust the width of the label to a width that matches the label contents in x-direction; a width of 120 should last. You can do this either by using the mouse or set the value in the "Geometry" section of the properties.

The Spinbox: press the "QSpinBox" button on the "Widgets" tab and place it at the right of the label we created in the last step. Now set the variable name in section "C++Code" to "width_spbox". The minimum and maximum values are 1 and 100, which should last for setting the brush width.

The Buttons: finally, we need the mentioned three buttons. The leftmost button will be the default button. Create a QPushbutton and place it somewhere nicely on the bottom of the dialog, set the variable name to "default_btn" and the button text to "Default". Proceed with the OK button with variable name "ok_btn" and the cancel button with variable name "cancel_btn" and set the button text to "&OK" and "&Cancel".

If you´re fine with the layout of the dialog, choose "Generate complete sources" from the Build menu and set the classname to "KPenBrushDlg", the inheritance to QDialog. After pressing "OK", the sources for the dialog are created and added to the project. Now you can return to the editor view in KDevelop and we can add the code needed to give the dialog some execution purpose.

10.4 Connections and Setting Up

After we have created the GUI of the dialog, we have to add some functionality to the buttons and provide ways to set and retrieve the selected value of the spinbox - because we want the dialog to display the current value when it gets called and to access the selected value when the user pressed the OK button to quit the dialog.

In the generated class for the dialog, KPenBrushDlg, you can see one method besides the constructor and the destructor, initDialog(). This method implements the whole GUI construction, so we don´t have to care for that anymore and we can go directly to add the usual connections for the push buttons first. Add the lines marked by arrows to the constructor of the dialog:


KPenBrushDlg::KPenBrushDlg(int curr, QWidget *parent, const char *name) : QDialog(parent,name,true){
        initDialog();

->      connect(default_btn, SIGNAL(clicked()), this, SLOT(slotDefault()));
->      connect(ok_btn, SIGNAL(clicked()), this, SLOT(accept()));
->      connect(cancel_btn, SIGNAL(clicked()), this, SLOT(reject()));
}

This provides the functionality for the buttons on the bottom of the dialog when the user clicks the button. First, we set the default button to execute a slot called slotDefault(). This slot is still to be implemented below, where we will set the default value of the spinbox directly.

The second connect() call connects the ok button to call the slot accept() provided by QDialog, as well as the cancel button gets connected to QDialog´s slot reject(). This will both close the dialog and will set the result value which we will use later when we implement the method that calls the dialog to determine if we want to use the value set or to ignore any changes.

Now we have to add two methods to set and retrieve the spinbox value:


void setCurrent(int curr){ width_spbox->setValue(curr); }
int width() { return width_spbox->value(); };

Add these methods to the class declaration with the modifier "public", as we want to set and retrieve the values when we call the dialog to show up. The setCurrent() method will be used to set the current value the pen has, the width() method returns us the selected with when the user presses OK and we want to know which value has been chosen.

Last but not least, we need to implement the slotDefault() method:


//kpenbrushdlg.h:
//method declaration:

public slots:
  void slotDefault();

//kpenbrushdlg.cpp:
//method implementation:

void KPenBrushDlg::slotDefault()
{
  width_spbox->setValue(3);
}

This will set the default value to 3 pixels for the pen.

Now we´re ready with our first dialog and we can turn to over to the other application classes to adapt some things and add the method calls to invoke the dialog.

10.5 Calling the Dialogs

As you may guess, calling the dialogs means that we will not only implement calling our width selection dialog but also add the method for selecting the pen color, but one after another. First, create a method slotPenBrush() in the class KScribbleApp:


void KScribbleApp::slotPenBrush()
{
  slotStatusMsg(i18n("Setting brush width..."));

  // get one window with document for a current pen width
  QWidgetList windows = pWorkspace->windowList();
  KScribbleView* m = (KScribbleView*)windows.at(0);
        KScribbleDoc* pDoc = m->getDocument();
  int curr_width=pDoc->penWidth();

  // create the dialog, get the new width and set the pen width for all documents
  KPenBrushDlg* dlg= new KPenBrushDlg(this);
  dlg->setCurrent(curr_width);
  if(dlg->exec()){
    int width=dlg->width();
        for ( int i = 0; i < int(windows.count()); ++i )
        {
        m = (KScribbleView*)windows.at(i);
        if ( m )
        {
                pDoc = m->getDocument();
        pDoc->setPenWidth(width);
        }
        }
  }
  slotStatusMsg(i18n("Ready."));
}

Here, we first have to access the window list and retrieve a pointer to a document - which can be a document of any window, because all documents should have the same current pen width. Then we create an integer variable curr_width that stores the current pen width.

Now we can call the dialog by creating the dlg instance of KPenBrushDlg. Then we set the current pen width by calling dlg->setCurrent(), which method we added to the dialog. By calling dlg->exec() we invoke the dialog. The if() statement ensures that the following code is only executed when the result code of the dialog has the accept flag set - which means, the code is executed if the user pressed the OK button on the dialog.

Assuming the user changed the value and pressed OK, we have to set all documents to use the new pen width. For that we use the for() loop and set every document´s pen width to the width variable we retrieved before with dlg->width().

We don´t have implemented the method setPenWidth() in the document class, so we´ll do this right now:



kscribbledoc.h:

public:
  void setPenWidth( int w ){ pen.setWidth( w ); }

What is missing to execute any action is to add the methods that shall be called when the menu items are activated or the toolbar buttons pressed. For this, we have to add the ID´s to the slot commandCallback(), which selects and executes the according methods we want to call if a menu or toolbar item was chosen:


void KScribbleApp::commandCallback(int id_)
{
  switch (id_)
  {
    case ID_PEN_BRUSH:
      slotPenBrush();
      break;

    case ID_PEN_COLOR:
      slotPenColor();
      break;
....
  }
}

This addition also adds the slotPenColor() method to the execution list to set the pen color, which we will implement now:


void KScribbleApp::slotPenColor()
{
  slotStatusMsg(i18n("Selecting pen color..."));

  QColor myColor;
  int result = KColorDialog::getColor( myColor, this );
  if ( result == KColorDialog::Accepted )
  {
    QWidgetList windows = pWorkspace->windowList();
    KScribbleDoc* pDoc;
    KScribbleView* m;
    for ( int i = 0; i < int(windows.count()); ++i )
    {
      m = (KScribbleView*)windows.at(i);
      if ( m )
      {
        pDoc = m->getDocument();
        pDoc->setPenColor(myColor);
      }
    }
  }
  slotStatusMsg(i18n("Ready."));
}

When looking at the code, we see that we use another new method of KScribbleDoc to set the pen color. This one has to be implemented as well:
kscribbledoc.h:

    /** sets the pen color */
    void setPenColor( const QColor &c ){ pen.setColor( c ); }

Watch out for adding the declaration of the two new methods slotPenBrush() and slotPenColor() to the class KScribbleApp, so our class knows about these methods.

Now you´re ready ! Let´s summarize what we´ve done in this chapter:

By this structure, you are provided the general way how to extend your application with more functionality and manipulating settings that influence the behavoir of the document and view interaction.

Next Previous Table of Contents