Adding Functionality To The Push Buttons

Responding to user interaction with Qt widgets is mostly done by connecting signals that these widgets emit to slots. If you have never heard about signals and slots before, please check at least the sidebar or even better read the Qt Tutorial or Programming with Qt.

Signals And Slots: The connection between user interaction and program functionality is done via so-called signals and slots in Qt. A widget emits a slot when something interesting happens. What this means depends on the widget; for a push button, one of the signals is the clicked() signal, which is emitted when the user clicks the push button. Other widgets have other signals.

Slots are just ordinary C++ methods that are marked as slots in the class declaration. Any C++ method can be a slot. Methods that are declared as slots can still be called the ordinary way.

The method QObject::connect() ties signals and slots together. When a widget emits a signal, the Qt runtime system checks whether any slots are connected to this signal and calls these slots.

Not all the signals need the be connected to a slot, in fact, in most applications, only very few signals are. Likewise, there does not have to be a signal connected to each slot; the slot can still be called the ordinary way.

It is a part of the class declaration of any class which signals and slots it emits. For the Qt classes, this is thoroughly documented in the reference documentation, for third-party classes, please see the respective manufacturer's documentation.

The first thing we will do now is connect the Cancel button's clicked() signal which is emitted when this button is clicked to the dialog's reject() method which closes the dialog and sets a negative return code. To do this, start the connection tool by hitting F3, choosing Edit/Connections from the menu or clicking on the connections icon on the toolbar (see Figure 3-2). As soon as you have done this, click on the Cancel button on your form, hold the mouse button pressed and move the mouse cursor to somewhere on the background of the form. Release it there. With this gesture, you indicate that you want to create a connection from the Cancel button to the form as a whole. Pink frames show which widgets you have currently selected while you move the mouse.

Figure 3-2. The connections icon

Once you have released the mouse button, the Connections dialog opens (see Figure 3-3). In this dialog, select clicked() in the Cancel button's signals list to the left and reject() in the dialogs' slot list to the right. The connection will show in the list in the lower half of the dialog.

Figure 3-3. The Connections dialog

Close the dialog. Your first signal-slot-connection is in place, and you did not even have to write a single line of code yet.

Now for the OK button. We want this button to close the dialog with a positive return code, i.e., call the dialog's accept() slot, but we also want to show the aforementioned message box. In order to do the latter, we need to create our own slot.

To do this, select Edit/Slots... from the menu. This shows the Slots dialog (see Figure 3-4). Click on the Add button to add a slot. A slot is added with the default name new_slot(). Go to the edit field labelled Slot: in the Slot properties box and change the name to slotSendOrder().[1]. Leave everything else as it is and click OK.

Figure 3-4. The Slots dialog

Now start the connection tool again, but this time connect the OK button to the dialog. When the Connections dialog opens again, you can see not only the predefined slots, but also your newly created slot sendOrder(). Click on the clicked() signal first and then on the sendOrder() slot so that the connection appears in the lower half. Close the Connections dialog.

When the user now clicks the OK button, both the accept() slot to close the dialog and the sendOrder() slot are invoked. But this time we do not get away as easily as with the Cancel button: We still have to implement the sendOrder() slot.

But wait a minute, this looks like we have run into a problem: In order to implement a slot, we need to change the source code, but you just learned at the beginning of this tutorial that you should not edit the source code generated by uic.

That's true, and to solve this problem, we need to inherit a new class from our PizzaEntry class where we can implement the new slot. This sounds like somewhat of a bother now, but you will get used to it. The whole thing works because Qt Designer declares all user-defined slots as virtual and provides an empty implementation for them. At runtime, then, you do not create an instance of the uic-generated class but an instance of your own implementation class instead. When the OK button is clicked, your own virtual method will be called instead of the generated empty one. If this sounds like black magic to you, you can either simply believe that this works, or (recommended) read in any good C++ book about virtual methods and polymorphy.

Let's call our inherited class PizzaEntryImpl, because we will put some implementation in there. For other naming suggestions, please see the sidebar “The Name Of The Game”.

The Name Of The Game: In a larger project, you will have a possibly large number of dialog (or other) classes generated by Qt Designer and uic as well as implementation classes for many (if not all) of these dialogs. In such a situation, it is best to think of a smart naming scheme right away in order to avoid confusion later.

There two demands on such a naming scheme: It should be easy, obvious, and unique to get from the generated class' name to the implementation class name or the other way around, and the class names should not be too cumbersome to type. One naming scheme that fulfills these conditions is the one used in this tutorial: The generated dialog class gets the name that seems “natural” (like PizzaEntry in this case), the implementation class the same name with Impl attached.

Another option is the one chosen by the TrollTech developers: Choose the “natural” name for the implementation class and append Base to this name for the generated class.

Both schemes described here and many others are equally well. The important thing is that you pick one consistent scheme and stick to it throughout your project.

All we need in this implementation is class is an empty constructor so that we can pass the usual parameters, and the slot we want to implement. Here's what the header file PizzaEntryImpl.h for our implementation class PizzaEntryImpl looks like:

#ifndef PIZZAENTRYIMPL_H
#define PIZZAENTRYIMPL_H

#include "PizzaEntry.h"

class PizzaEntryImpl : public PizzaEntry
{
  Q_OBJECT

public:
    PizzaEntryImpl( QWidget* parent = 0, const char* name = 0, 
					 bool modal = FALSE, WFlags f = 0 ) :
	  PizzaEntry( parent, name, modal, f ) {}

public slots:
	virtual void sendOrder();
};
#endif

The implementation file is even simpler, since all we want to do for now is showing a message box that tells the user that his order has been sent. In a real pizza order application, you would of course package the data somehow and send them over the network. Here is PizzaEntryImpl.cpp:

#include "PizzaEntryImpl.h"

#include <qmessagebox.h>

void PizzaEntryImpl::sendOrder()
{
  QMessageBox::information( this, "Pizza Entry Information",
							"Your order has been sent. Your pizza will arrive in 30-45 minutes", QMessageBox::Ok );
}

Finally, since by using the slots accept() and reject(), we have committed to using the dialog as a modal dialog (instead of as a modeless dialog like in the previous tutorial), which means that we need to stretch the notion of “clean Qt programming” a bit in order to keep our test bed simple. Usually, you start the event processing by calling QApplication::exec(), but since modal dialogs have an event loop of their own, we can get away with just calling exec() on our dialog class. Here is the new file PizzaEntryTest.cpp:

#include <qapplication.h>
#include "PizzaEntryImpl.h"

int main( int argc, char* argv[] )
{
  QApplication app( argc, argv );

  PizzaEntryImpl* pizzaEntry = new PizzaEntryImpl( 0, "pizza entry", true);
  app.setMainWidget( pizzaEntry );
  int ret = pizzaEntry->exec();
  delete pizzaEntry;
  return ret;
}

That's all for now. You can now save your dialog in Qt Designer, compile and run it. Here are the command lines you need. For Unix with the g++ compiler:

uic -o PizzaEntry.h pizza.ui
uic -i PizzaEntry.h -o PizzaEntry.cpp pizza.ui
moc -o moc_PizzaEntry.cpp PizzaEntry.h
moc -o moc_PizzaEntryImpl.cpp PizzaEntryImpl.h
moc -o moc_PizzaEntryTest.cpp PizzaEntryTest.cpp
g++ -I$QTDIR/include PizzaEntry.cpp PizzaEntryTest.cpp \
	PizzaEntryImpl.cpp moc_PizzaEntry.cp moc_PizzaEntryImpl.cpp \ 
	-L$QTDIR/lib -lqt

and for Windows with Visual C++:

Now run the program and see what happens when you click either Cancel or OK.

Notes

[1]

It is by no means a requirement that names of slots start with slot but many people like to do this because it makes the purpose more explicit.