|
|
Home | All Classes | Main Classes | Annotated | Grouped Classes | Functions |
[Prev: The Designer Approach] [Home] [Next: Creating Custom Widgets]
This chapter describes two different approaches that you can take to creating forms with Qt Designer. Subclassing is used to extend the functionality of a form by creating your own class based upon a form you create in Qt Designer. Dynamic dialogs are .ui files which can be executed by a Qt application; this keeps the GUI design and the code separate and is useful in environments where the GUI may have to change more often than the underlying application logic.
We'll start with a general description of how to subclass a form and follow with a short example. Note that subclassing has some disadvantages compared with putting your code into a form directly; see Extending the functionality of a form in The Designer Approach chapter for details.
Qt Designer reads and writes qmake .pro (project) files which are used to record the files used to build the application and from which Makefiles are generated. Qt Designer also reads and writes .ui (user interface) files. These are XML files that record the widgets, layouts, source code and settings you've used for a form. Every .ui file is converted by the uic (user interface compiler) into a C++ .h file and a C++ .cpp file. These C++ files are then read by moc (meta object compiler), and finally compiled by your compiler into a working application.
If you create applications wholly within Qt Designer you only need to create a main.cpp.
If you create the main.cpp file within Qt Designer, it will automatically be added to your project file by Qt Designer. If you create the main.cpp file outside of Qt Designer you must add it to the project file manually by adding the following line at the end of your project's .pro file:
SOURCES += main.cpp
You can then use qmake to generate the Makefile. (For example qmake -o Makefile myproject.pro.) Running make (Linux, Unix or Borland compilers), or nmake (Visual C++), will then call uic, moc and your compiler as necessary to build your application.
If you use Qt Designer to create your main window and dialogs, but also add other C++ files, or if you subclass any of your forms you will need to add these files to the .pro file so that they are compiled with the rest of your application's source files. Each .h file that you create separately from Qt Designer should be added to the HEADERS line, and each .cpp file should be added to the SOURCES line, just as we've done for main.cpp. If you get undefined reference errors it is worth checking that you've added the names of all your header and implementation files to the .pro file.
When subclassing a form it is helpful to use a naming convention to help us identify which files are generated from Qt Designer's .ui files and which are hand coded.
Suppose, for example, that we are developing a dialog and writing the code directly in Qt Designer. We might call our dialog 'OptionsForm' and the .ui file, optionsform.ui. The automatically generated files will be optionsform.h and optionsform.cpp.
If we were developing another dialog, but this time one that we intended to subclass, we want to make it easy to distinguish between the automatically generated files and our hand coded files. For example, we might call our dialog 'SettingsFormBase' and the .ui file settingsformbase.ui. The automatically generated files would then be called settingsformbase.h and settingsformbase.cpp. We would then call our subclass 'SettingsForm' and code it in the files settingsform.h and settingsform.cpp.
Any subclass of a form should include the Q_OBJECT macro so that slots and signals will work correctly. Once you've created your subclass be sure to add the .h and the .cpp files to the .pro project file. For example we would add the following lines for our subclassed 'SettingsForm' at the end of the .pro file:
HEADERS += settingsform.h SOURCES += settingsform.cpp
The simplest way to create a new source file is by clicking File|New to invoke the 'New File' dialog, then click 'C++ Source' or 'C++ Header' as appropriate, then click OK. A new empty source window will appear. You don't need to manually edit the .pro file since Qt Designer will add them for you automatically.
Qt Designer will have added
FORMS = settingsformbase.ui
to the project file. The settingsformbase.h and settingsformbase.cpp files will be generated from the .ui file automatically.
We will write a small example dialog to show the use of subclassing in practice. The dialog will present a choice of customer credit ratings with an option of choosing a 'special' rating for which a specific amount must be given. We'll implement the functionality in a subclass. We'll start by creating the base form and connecting its signals and slots, then we'll create the subclass and a simple main.cpp so that we can test it.
We'll begin by creating a new project. Click File|New, then click the 'C++ Project' icon to invoke the Project Settings dialog. Click the ellipsis button to invoke the Save As dialog; navigate to the project's directory (creating it if necessary). Make sure you're in the project's directory, then enter a project name of 'credit.pro'. Click the Save button to return to the Project Settings dialog, then click OK. Now we'll add a form to the project. Click File|New to invoke the New File dialog. The default form is Dialog which is what we want; click OK. Resize the form to make it smaller; it should be about 2 inches (5 cm) square. Change the form's name to 'CreditFormBase' and the caption to 'Credit Rating'. Save the form as creditformbase.ui.
We'll now add the widgets we need.
Click the Button Group toolbar button, then click near the top left of the form. Resize the button group so that it takes up approximately half the form. Change the button group's name to 'creditButtonGroup' and its title property to 'Credit Rating'.
We'll now add some radio buttons. Double click the Radio Button toolbar button. Click towards the top of the Credit Rating button group and a radio button will appear. Click below this button, to create a second radio button, then click below the second button to create a third. Now we will switch off the effect of the double click by clicking the Pointer (arrow) toolbar button. The pointer will now behave normally, i.e. clicking the form will no longer create more radio buttons. Change the first radio button's name to 'stdRadioButton' and its text to '&Standard'. Change its checked property to True. Change the second button's name to 'noneRadioButton' and its text to '&None'. Change the third radio button's properties to 'specialRadioButton' and 'Sp&ecial' respectively.
If the user chooses the special credit rating they must specify an amount. Click the SpinBox toolbar button and click the form just below the button group. Change the spin box's name to 'amountSpinBox'. Change its prefix to '$ ' (note the space), its maxValue to '100000' and its lineStep to '10000'. Change its enabled property to False.
Click the Push Button toolbar button and click the form below the spin box. Change the button's name to 'okPushButton', its text to 'OK' and its default property to 'True'. Add a second button to the right of the first. Change the second button's name to 'cancelPushButton' and its text to 'Cancel'.
We'll now lay out the widgets and connect up the slots we need.
Click the credit rating group box then press Ctrl+L (lay out vertically).
Click the form so that the button group is no longer selected. Ctrl+Click the OK button and drag the rubber band to touch the Cancel button, then release. Press Ctrl+H.
Click the form, then press Ctrl+L.
The widgets will be laid out vertically, each one stretching to fill up the maximum space both vertically and horizontally. The buttons look rather large since they've expanded to take up the full width of the form. It might look more attractive to make the buttons smaller using spacers. Click the OK button, then press Ctrl+B (break layout). Resize both buttons to make them narrower leaving space on either side of them. Click the Spacer toolbar button then click to the left of the OK button; click Horizontal from the pop up spacer menu. Copy this spacer and place the copy between the two buttons. Copy the spacer again and place the copy to the right of the Cancel button. (For the second and third spacers, click on the first spacer, press Ctrl+C then Ctrl+V. Drag the new spacer to the desired position.) Ctrl+Click the left most spacer and drag the rubber band so that it touches the buttons and the spacers, then release. Press Ctrl+H. Click the form then press Ctrl+L.
We'll now connect the signals and slots. Click Edit|Connections to invoke the View and Edit Connections dialog.
Create a new connection that connects the OK button's clicked() signal to the form's accept() slot. Create a second connection that connects the Cancel button's clicked() signal to the form's reject() slot. (See Creating Signals and Slots Connections.)
We want the amount spin box to be enabled only if the special radio button is checked. Create another connection, this time connecting the special radio button's toggled() signal to the amount spin box's setEnabled() slot.
If the user checks the standard or none radio buttons we want to set the amount accordingly. Connect the credit rating button group's clicked() signal to a new custom setAmount() slot (which you create by clicking the Edit Slots... button).
We'll subclass the form to set the amount in the spin box depending on which radio button is checked. Save the form as 'creditformbase.ui' (press Ctrl+S).
Although we intend our dialog to be used within an application it is useful to create a test harness so that we can develop and test it stand-alone. Click File|New to invoke the 'New File' dialog, then click 'C++ Source', then click OK. In the editor window that pops up, enter the following code:
#include <qapplication.h> #include "creditformbase.h" int main( int argc, char *argv[] ) { QApplication app( argc, argv ); CreditFormBase creditForm; app.setMainWidget( &creditForm ); creditForm.show(); return app.exec(); }
Note that we're including creditformbase.h and instantiating a CreditFormBase object; once we've written our subclass we'll replace the header with our subclass, creditform.h, and instantiate a CreditForm.
We can now generate the application with qmake, e.g. qmake -o Makefile credit.pro, make it and run it. The form should run fine, but doesn't yet have the behaviour we require.
We need to create a header and an implementation file for our subclass. The code for our subclass is minimal. The header file is qt/tools/designer/examples/credit/creditform.h:
#include "creditformbase.h" class CreditForm : public CreditFormBase { Q_OBJECT public: CreditForm( QWidget* parent = 0, const char* name = 0, bool modal = FALSE, WFlags fl = 0 ); ~CreditForm(); public slots: void setAmount(); };
We've declared the slot, setAmount(), that we created in Qt Designer. The Q_OBJECT macro is included because it is essential for classes that use signals and slots.
The implementation in qt/tools/designer/examples/credit/creditform.cpp is simple:
#include <qradiobutton.h> #include <qspinbox.h> #include "creditform.h" CreditForm::CreditForm( QWidget* parent, const char* name, bool modal, WFlags fl ) : CreditFormBase( parent, name, modal, fl ) { setAmount(); } CreditForm::~CreditForm() { /* NOOP */ } void CreditForm::setAmount() { if ( stdRadioButton->isChecked() ) amountSpinBox->setValue( amountSpinBox->maxValue() / 2 ); else if ( noneRadioButton->isChecked() ) amountSpinBox->setValue( amountSpinBox->minValue() ); }
We call setAmount() in the constructor to ensure that the correct amount is shown when the form starts based on whichever radio button we checked in Qt Designer. In setAmount() we set the amount if the standard or none radio button is checked. If the user has checked the special radio button they are free to change the amount themselves.
To be able to test our subclass we change main.cpp to include creditform.h rather than creditformbase.h and change the instantiation of the creditForm object:
#include <qapplication.h> #include "creditform.h" int main( int argc, char *argv[] ) { QApplication app( argc, argv ); CreditForm creditForm; app.setMainWidget( &creditForm ); creditForm.show(); return app.exec(); }
If you created the creditform.h and creditform.cpp files in Qt Designer, they are already in the project file, but if you created them manually you must also update the project file by adding these two new lines at the end:
HEADERS += creditform.h SOURCES += creditform.cpp
To test the form rerun qmake to regenerate the Makefile, then make and run.
The subclassing example we've used is simple, but this reflects subclassing forms in Qt: it is easy to do.
Qt programs are capable of loading Qt Designer .ui files and instantiating the forms represented by the .ui files. Since the .ui file is not compiled it cannot include any C++ code, (e.g. slot implementations). In this section we will explain how to load a dynamic dialog and how to create a class that can be used to implement the dynamic dialog's custom slots.
We will use the credit form that we created in the subclassing section as our example form. We will start by simply instantiating and running the form and then we'll cover how to implement custom slots.
We'll create a main.cpp file to use as a test harness, and manually create a project file.
The project file qt/tools/designer/examples/receiver1/receiver.pro looks like this:
TEMPLATE = app CONFIG += qt warn_on release TARGET = receiver SOURCES += main.cpp unix:LIBS += -lqui win32:LIBS += $(QTDIR)/lib/qui.lib FORMS = mainform.ui LANGUAGE = C++ INCLUDEPATH += $(QTDIR)/tools/designer/uilib
We do not include the creditformbase.ui file since this file will be read at runtime, as we'll see shortly. We must include the qui library since the functionality we require is not part of the standard Qt library.
The main.cpp is quite standard. It will invoke the form we're going to create in Qt Designer as its main form. This form will then load and execute the dynamic dialog.
#include <qapplication.h> #include "mainform.h" int main( int argc, char *argv[] ) { QApplication app( argc, argv ); MainForm *mainForm = new MainForm; app.setMainWidget( mainForm ); mainForm->show(); return app.exec(); }
We create a new instance of our MainForm class, set it to be the main widget, show it and enter the event loop in the app.exec() call.
Open the receiver.pro project file in Qt Designer. We'll create a dialog as our main window which we'll use to invoke the dynamic dialog. Press Ctrl+N to launch the New File dialog and click OK to get the default which is a dialog. Change the dialog's name to 'MainForm' and its caption to 'Main Form'. Add two buttons, one called 'creditPushButton' with the text '&Credit Dialog', and the other called 'quitPushButton' with the text '&Quit'. (For each button click the Push Button toolbar button, then click the form. Change the properties in the property window to those we've just described.)
We will now add a couple of labels so that we can show the settings the user chose in the dynamic dialog. Click the Text Label toolbar button, then click the form below the Credit Dialog button. Change the label's text to 'Credit Rating'. Add another text label below the Quit button. Change its name to 'ratingTextLabel' and its text to 'Unrated'.
We'll now lay out the widgets. Click the form then press Ctrl+G (lay out in a grid).
We'll now handle the signals and slots connections. Invoke the View and Edit Connections dialog and connect the credit dialog button's clicked() signal to a new creditDialog() custom slot (which is created by clicking the Edit Slots... button). Now connect the Quit button's clicked() signal to the dialog's accept() function.
Save the form and call it mainform.ui. (Press Ctrl+S and enter the filename.) In the next section we'll write the code for loading and launching the dynamic dialog directly in Qt Designer.
We'll now add the code to invoke the credit dialog. Before we can do this we need to add the widget factory's header file to the form. Click the Source tab in the Object Hierarchy. Right click Includes (in Implementation), then click New. Type in '<qwidgetfactory.h>', then press Enter. Because we will need to access the spin box in the dynamic dialog we must add its header file. Right click Includes (in Implmentation), then click New. Type in '<qspinbox.h>', then press Enter.
In our main form we created a slot called creditDialog(). We will implement this slot directly in Qt Designer and use it to load and execute the dynamic dialog. The code is taken from qt/tools/designer/examples/receiver1/mainform.ui.h which contains the C++ implementation of mainform.ui's slots.
void MainForm::creditDialog() { QDialog *creditForm = (QDialog *) QWidgetFactory::create( "../credit/creditformbase.ui" ); // Set up the dynamic dialog here if ( creditForm->exec() ) { // The user accepted, act accordingly QSpinBox *amount = (QSpinBox *) creditForm->child( "amountSpinBox", "QSpinBox" ); if ( amount ) ratingTextLabel->setText( amount->text() ); } delete creditForm; }
The create() function is a static QWidgetFactory function. It loads the specified .ui file and returns a pointer to the toplevel QWidget created from the .ui file. We have cast the pointer to QDialog since we know that the creditformbase.ui file defines a QDialog. After creating the dialog we exec() it. If the user clicked OK the dialog returns Accepted and we enter the body of the if statement. We want to know the amount of credit that the user selected. We call the child() function on the dialog passing it the name of the widget we're interested in. The child() function returns a pointer to the widget with the name we passed, or returns 0 if no widget of that name was found. In the example we call child() to get a pointer to the 'amountSpinBox'. If the pointer we get back is not 0 we set the rating text to the amount in the dialog's spin box. At the end we delete the dynamic dialog. Deleting the dialog ensures that we free up its resources as soon as it is no longer required.
We used the child() to gain access to a widget within the dynamic dialog, passing it the name of the widget we were interested in. In some situations we might not know what a widget is called. We can access the first widget of a specified class by calling child() with a null widget name and a classname, e.g. child(0,"QPushButton"). This will return a pointer to the first QPushButton it finds (or 0 if there isn't one). If you want pointers to all the widgets of a given class you can call the QObject::queryList() function, passing it the name of the class. It returns a QObjectList pointer which points to every object in the dialog that is derived from the given class. See the online QObject documentation for further details.
There is one outstanding issue that we haven't addressed: the dynamic dialog does not have the behaviour of the original credit dialog because we have not implemented the setAmount() slot. We can implement slots for dynamic dialogs by creating a QObject subclass. We then create an instance of this subclass and pass a pointer to it to the QWidgetFactory::create() function which will connect the dynamic dialog's signals to the slots implemented in our subclass.
We need to create a QObject subclass and change our creditDialog() to create an instance of our subclass that can be passed to the QWidgetFactory::create() function. Here is the modified creditDialog() function from the qt/tools/designer/examples/receiver2/mainform.ui.h file that contains the code for mainform.ui's slots:
void MainForm::creditDialog() { Receiver *receiver = new Receiver; QDialog *creditForm = (QDialog *) QWidgetFactory::create( "../credit/creditformbase.ui", receiver ); receiver->setParent( creditForm ); // Set up the dynamic dialog here if ( creditForm->exec() ) { // The user accepted, act accordingly QSpinBox *amount = (QSpinBox *) creditForm->child( "amountSpinBox", "QSpinBox" ); if ( amount ) ratingTextLabel->setText( amount->text() ); } delete receiver; delete creditForm; }
We create a new instance of our 'Receiver' subclass. (We'll write the code for this class shortly.) We then create the QDialog using QWidgetFactory::create(). This call differs from our previous example because we pass in the subclass object so that the create() function can set up the signals/slots connections automatically for us. Since our slot must access the widgets in the dynamic form we pass a pointer to the form to the receiver object through our setParent() function. The remainder of the function is the same as before except that we delete our receiver object.
Since we are using the 'Receiver' subclass in our main form we must include its header file. In Object Explorer's Members tab, right click Includes (in Implmentation), then click New. Type in 'receiver.h', then press Enter.
We'll now look at the implementation of our 'Receiver' subclass. The code is taken from qt/tools/designer/examples/receiver2/receiver.h and the corresponding receiver.cpp file. We'll start with the header file.
#include <qobject.h> #include <qdialog.h> class Receiver : public QObject { Q_OBJECT public: void setParent( QDialog *parent ); public slots: void setAmount(); private: QDialog *p; };
Our class must be a QObject subclass and because we're using signals and slots it must include the Q_OBJECT macro. We declare a function and the setAmount() slot that we wish to implement as well as a private QDialog pointer.
The implementation requires the header files of the classes it uses:
#include <qradiobutton.h> #include <qspinbox.h> #include "receiver.h"
We'll discuss the implementation of each function in receiver.cpp separately.
void Receiver::setParent( QDialog *parent ) { p = parent; setAmount(); }
The setParent() function assigns a pointer to the dynamic dialog to our private pointer. We could not do this in a constructor call because we have to construct our Receiver object before we call QWidgetFactory::create(), since we must pass the Receiver object to the create() function. Once we've called create() we then have a pointer to the dynamic dialog which we can then pass via setParent() to our Receiver class. In the subclass version of this example we called setAmount() in the constructor; but we cannot do that here because the implementation of setAmount() depends on knowledge of the dynamic dialog which is not available at construction time. Because of this we call setAmount() in the setParent() function.
void Receiver::setAmount() { QSpinBox *amount = (QSpinBox *) p->child( "amountSpinBox", "QSpinBox" ); QRadioButton *radio = (QRadioButton *) p->child( "stdRadioButton", "QRadioButton" ); if ( radio && radio->isChecked() ) { if ( amount ) amount->setValue( amount->maxValue() / 2 ); return; } radio = (QRadioButton *) p->child( "noneRadioButton", "QRadioButton" ); if ( radio && radio->isChecked() ) if ( amount ) amount->setValue( amount->minValue() ); }
Since we may be updating the amount spin box we need to get a pointer to it. We call child() on the pointer p which points to the dynamic dialog assigned in the setParent() call. We cast the resulting pointer to the correct type so that we can call any functions relevant to that type. In the example we call child() to get a pointer to the amount spin box, and then call child() again to get a pointer to the 'stdRadioButton'. If we get a pointer to the radio button and the button is checked we set the amount providing we have a pointer to the amount spin box. If this radio button was checked we're finished so we return. If the 'stdRadioButton' isn't checked we get a pointer to the 'noneRadioButton' and set the amount if this button is checked. We do nothing if the 'specialRadioButton' is checked because the user is free to enter a value of their choice.
Compiling vs Dynamically Loading Dialogs
The differences between using a 'compiled in' .ui file and a dynamically loaded .ui file are these:
Dynamic dialogs cannot have any C++ code in the .ui file; any custom slots must be implemented via a QObject subclass. Compiled dialogs can contain code either in the .ui file or in a subclass.
Dynamic dialogs will load slower because the .ui file must be read and a QWidget instance instantiated based on the .ui file's parse tree. Compiled code will load much faster because no file reading or parsing is necessary. Note that the user may not notice any difference in speed since the difference may be mere fractions of a second.
Dynamic dialogs allow you to change the .ui file independently of the code so long as none of the changes impact the code. This means that you can change the appearance of the form, e.g. move widgets and lay them out differently. If you want to change a compiled dialog you must change the .ui file and recompile. If you are building an application and want your customers to be able to customize aspects of the user interface you can give them a copy of Qt Designer (if your license permits this) and use dynamic dialogs.
[Prev: The Designer Approach] [Home] [Next: Creating Custom Widgets]
Copyright © 2007 Trolltech | Trademarks | Qt 3.3.8
|