Journal Articles
Browse in : |
All
> Journals
> CVu
> 165
(13)
All > Topics > Programming (877) Any of these categories - All of these categories |
Note: when you create a new publication type, the articles module will automatically use the templates user-display-[publicationtype].xt and user-summary-[publicationtype].xt. If those templates do not exist when you try to preview or display a new article, you'll get this warning :-) Please place your own templates in themes/yourtheme/modules/articles . The templates will get the extension .xt there.
Title: Rapid Dialog Design Using Qt
Author: Administrator
Date: 03 October 2004 13:16:08 +01:00 or Sun, 03 October 2004 13:16:08 +01:00
Summary:
In this third installment of our series on GUI programming with the Qt C++ toolkit, we're going to show how to design dialog boxes (or "dialogs") using Qt.
Body:
In this third installment of our series on GUI programming with the Qt C++ toolkit, we're going to show how to design dialog boxes (or "dialogs") using Qt. Dialogs can be created entirely from source code, or with Qt Designer, a visual GUI design tool. Whichever approach is chosen, the result is invariably good looking, resizable, platform-independent dialogs.
Writing dialogs entirely in code using Qt isn't the chore you'd expect if you're familiar with other toolkits such as Swing, GTK+ or MFC. Qt's layout manager classes take care of positioning widgets on screen. Qt provides a horizontal box layout, a vertical box layout and a grid layout. These can be nested to create arbitrarily complex layouts.
Qt's layouts feature automatic positioning and resizing of child widgets, sensible minimum and default sizes for top-level widgets, and automatic repositioning when the contents, language or font changes. For cross-platform applications, Qt's layouts are a huge time-saver.
Layouts are also useful for internationalization. With fixed sizes and positions, the translation text is often truncated; with layouts, the child widgets are automatically resized. Furthermore, if you translate your application to a right-to-left language such as Arabic or Hebrew, layouts will automatically reverse themselves to follow the direction of writing.
To see how this works in practice, we will implement the "Login to Database" dialog shown above. This is achieved by deriving from QDialog (which in turn derives from QWidget) and writing the code for a few functions. Let's start with the header file:
// include guards omitted #include <qdialog.h> class QLabel; class QLineEdit; class QPushButton; class LoginDialog : public QDialog { Q_OBJECT public: LoginDialog(QWidget *parent = 0); private slots: void enableLoginButton(); private: QLineEdit *dbNameLineEdit; QLineEdit *userNameLineEdit; QLineEdit *passwordLineEdit; QLineEdit *hostNameLineEdit; QLineEdit *portLineEdit; QLabel *dbNameLabel; QLabel *userNameLabel; QLabel *passwordLabel; QLabel *hostNameLabel; QLabel *portLabel; QPushButton *loginButton; QPushButton *cancelButton; };
The LoginDialog class has a typical Qt widget constructor that accepts a parent widget (or window), a slot called enableLoginButton(), and a dozen data member that keep track of the dialog's child widgets. The Q_OBJECT macro at the top of the class definition is necessary because we are using Qt's "signals and slots" mechanism in the class.
Let's now review the implementation file:
#include <qlabel.h> #include <qlayout.h> #include <qlineedit.h> #include <qpushbutton.h> #include "logindialog.h" LoginDialog::LoginDialog(QWidget *parent) : QDialog(parent) { dbNameLabel = new QLabel(tr("&Database name:"), this); userNameLabel = new QLabel(tr("&User name:"), this); passwordLabel = new QLabel(tr("&Password:"), this); hostNameLabel = new QLabel(tr("&Host name:"), this); portLabel = new QLabel(tr("P&ort:"), this); // more follows
The constructor passes on the parent parameter to the base class constructor. If parent is non-null, the dialog automatically centers itself on top of the parent window and shares that window's taskbar entry. In addition, if the dialog is modal (which is achieved by calling setModal() or exec() on the dialog), the user won't be allowed to interact with the parent window until the user closes the dialog.
Next, the constructor creates five QLabel widgets showing the texts "Database name:", "User name:", "Password:", "Host name:" and "Port:". The ampersand character ('&') indicates which letter is the shortcut key. The tr() function that surrounds the string literals marks the strings as translatable.
The second argument to the QLabel constructor is the parent widget or window, in this case the dialog (this). Child widgets are shown on screen inside their parent.
dbNameLineEdit = new QLineEdit(this); userNameLineEdit = new QLineEdit(this); passwordLineEdit = new QLineEdit(this); passwordLineEdit->setEchoMode( QLineEdit::Password); hostNameLineEdit = new QLineEdit(this); portLineEdit = new QLineEdit(this); dbNameLabel->setBuddy(dbNameLineEdit); userNameLabel->setBuddy(userNameLineEdit); passwordLabel->setBuddy(passwordLineEdit); hostNameLabel->setBuddy(hostNameLineEdit); portLabel->setBuddy(portLineEdit); // more follows
We create five QLineEdit widgets and set the "Password" widget's echo mode to QLineEdit::Password, so that characters typed by the user are replaced by asterisks or bullets. After creating the widgets, we call setBuddy() to create associations between the labels and the line editors. When the user presses a label's shortcut key (for example, Alt+P for "Password:"), the associated line editor gets the focus.
connect(dbNameLineEdit, SIGNAL(textChanged(const QString &)), this, SLOT(enableLoginButton())); connect(userNameLineEdit, SIGNAL(textChanged(const QString &)), this, SLOT(enableLoginButton())); connect(passwordLineEdit, SIGNAL(textChanged(const QString &)), this, SLOT(enableLoginButton())); connect(hostNameLineEdit, SIGNAL(textChanged(const QString &)), this, SLOT(enableLoginButton())); connect(portLineEdit, SIGNAL(textChanged(const QString &)), this, SLOT(enableLoginButton())); // more follows
We connect the textChanged() signal of each line editor to the dialog's enableLoginButton() slot. Whenever the user types in some text, the textChanged() signal is emitted and the enableLoginButton() slot is called. Based on the contents of the line editors, enableLoginButton() enables or disables the dialog's "Login" button. Disabled widgets are typically greyed out.
loginButton = new QPushButton(tr("Login"), this); cancelButton = new QPushButton(tr("Cancel"), this); loginButton->setDefault(true); loginButton->setEnabled(false); connect(loginButton, SIGNAL(clicked()), this, SLOT(accept())); connect(cancelButton, SIGNAL(clicked()), this, SLOT(reject())); // more follows
We create the "Login" and "Cancel" buttons, make "Login" the default button (meaning that pressing Enter will effectively click that button), and disable it. Then we connect the "Login" button's clicked() signal and QDialog's accept() slot, and connect the "Cancel" button's clicked() signal and QDialog's reject() slot. Both slots close the dialog, but they set QDialog's return code to a different value, which applications can query afterwards.
Now that we're done with creating the child widgets, we must set their positions and sizes relative to the parent widget. This could be done using QWidget::setGeometry(), but the result would be a hard-coded, unresizable dialog. Furthermore, determining pixel coordinates for the dialog's widgets is a tedious task that is better performed by a machine.
To obtain the desired result, we need two layout managers, one nested into the other. The dialog's main layout (the outer layout) is a grid layout with six rows and two columns. The inner layout is a horizontal box layout that contains the "Login" and "Cancel" buttons. The inner layout occupies the bottom row of the grid.
Here comes the code:
QHBoxLayout *buttonLayout = new QHBoxLayout; buttonLayout->addStretch(1); buttonLayout->addWidget(loginButton); buttonLayout->addWidget(cancelButton); QGridLayout *mainLayout = new QGridLayout(this); mainLayout->setMargin(10); mainLayout->setSpacing(5); mainLayout->addWidget(dbNameLabel, 0, 0); mainLayout->addWidget(dbNameLineEdit, 0, 1); mainLayout->addWidget(userNameLabel, 1, 0); mainLayout->addWidget(userNameLineEdit, 1, 1); mainLayout->addWidget(passwordLabel, 2, 0); mainLayout->addWidget(passwordLineEdit, 2, 1); mainLayout->addWidget(hostNameLabel, 3, 0); mainLayout->addWidget(hostNameLineEdit, 3, 1); mainLayout->addWidget(portLabel, 4, 0); mainLayout->addWidget(portLineEdit, 4, 1); mainLayout->addMultiCellLayout(buttonLayout, 5, 5, 0, 1); // more follows
We start by creating the QHBoxLayout that contains the buttons. We insert a stretch item, the "Login" button and the "Cancel" button into the layout. The layout will place them side by side. The stretch item is there to fill the space on the left of the buttons; without it, QHBoxLayout would stretch the "Login" and "Cancel" buttons to fill the entire width of the dialog.
Then we create a QGridLayout. We set the layout's margin to 10 pixels and the spacing between widgets in the layout to 5 pixels. Then we add the widgets to the layout. The addWidget() function takes a widget, a row and a column as parameters. At the very end, we insert the QHBoxLayout into the QGridLayout using addMultiCellLayout(), and specify that it should extend from row 5 to row 5 and from column 0 to column 1; i.e. occupy cells (5, 0) and (5, 1).
Here comes the rest of the constructor, where we set the window title:
setCaption(tr("Login to Database")); }
The constructor code might have felt a bit long. The good news is that we're pretty much finished now. The only missing part is the enableLoginButton() slot:
void LoginDialog::enableLoginButton() { loginButton->setEnabled( !dbNameLineEdit->text().isEmpty() && !userNameLineEdit->text().isEmpty() && !passwordLineEdit->text().isEmpty() && !hostNameLineEdit->text().isEmpty() && !portLineEdit->text().isEmpty()); }
When the user edits the contents of one of the line editors, the enableLoginButton() slot is called. The slot sets the button's state to enabled if and only if all the QLineEdits contain some text.
At this point, you might wonder why LoginDialog has no destructor. After all, who will delete all the objects created with new in the constructor? The answer is that when you create a widget or layout with a parent, the parent assumes ownership for the child. There is therefore no need for a LoginDialog destructor that simply deletes the child widgets and layouts; this is exactly what the QWidget destructor does. (Recall that LoginDialog inherits QDialog, which inherits QWidget.)
Qt Designer is a visual GUI design tool included with Qt. Although Qt's nice API makes it easy to write dialogs purely in code, most Qt developers find that Qt Designer is faster to use and allows them to make prototypes very quickly. In addition, if you work in an organisation where the user interface design is done by a team of designers, the designers can use Qt Designer themselves to create the dialogs instead of producing specifications that the developers then need to implement.
To show how Qt Designer works, we will use it to redo the "Login to Database" dialog.
Creating a dialog in Qt Designer usually consists of the following steps:
-
Put child widgets on the form.
-
Set up their properties.
-
Group them into layouts.
-
Specify the tab order.
The first step, putting the required child widgets on the form, is accomplished by clicking the desired widget from the toolbox on the left of Qt Designer's main window followed by clicking the desired position on the form. For the moment, we don't need to worry too much about the precise position and size of the child widgets; soon enough, we will put them in layouts, which will take care of those aspects automatically.
We also need a stretch item to fill the extra space in the buttons' layout. It is represented by a blue "spring" in Qt Designer. Next, we must set the child widgets' properties using the property editor located on the right side of Qt Designer's main window. Start by renaming all the widgets so that they have the same names as in the previous example. Then click the background of the form and set the form's "name" property to "LoginDialogBase" and its "caption" property to "Login to Database".
The following table summarises the properties to set for each widget:
Widget | Property | Value |
---|---|---|
dbNameLabel | text | "&Database name:" |
userNameLabel | text | "&User name:" |
passwordLabel | text | "&Password:" |
hostNameLabel | text | "&Host name:" |
portLabel | text | "P&ort:" |
passwordLineEdit | echoMode | Password |
loginButton | text | "Login" |
default | Truex | |
enabled | Falsex | |
cancelButton | text | "Cancel" |
We need to set the labels' "buddies". This is done by setting the "buddy" property of each label to the corresponding widget. Once the properties are set, the dialog should look like the one shown in Figure 4.
The next step is to put the widgets inside layouts. This is done by selecting multiple widgets and choosing "Lay Out Horizontally", "Lay Out Vertically" or "Lay Out in a Grid" from the "Layout" menu.
First, we select the stretch item and the two buttons, and click "Lay Out Horizontally". The resulting layout is rendered as a red frame in Qt Designer, to make it tangible. Then we click the background of the form and click "Lay Out in a Grid". This will produce the layout shown in Figure 5.
If a layout doesn't turn out quite right, we can always click "Undo", then roughly reposition the widgets being laid out and try again. When everything else is done, we are ready to set the dialog's tab order. This is done by pressing F4, clicking the widgets in the order we want them to be in the tab chain, and pressing Esc to terminate. Qt Designer will display the tab order as numbers in blue circles.
We can now save the dialog as a .ui file that contains the dialog in an XML format that Qt Designer can load and save. This file is converted to C++ using a separate tool called uic (User Interface Compiler). Assuming the .ui file is called logindialogbase.ui, the resulting C++ code would appear in the logindialogbase.h and logindialogbase.cpp files.
The dialog looks identical to the one we developed earlier purely in code, but right now if the user fills in the line editors or presses "Cancel", nothing happens! This is solved by subclassing the uic-generated class and adding the missing functionality there, as follows.
// Header file: // include guards omitted #include "logindialogbase.h" class LoginDialog : public LoginDialogBase { Q_OBJECT public: LoginDialog(QWidget *parent = 0); private slots: void enableLoginButton(); };
// Implementation file: #include <qlabel.h> #include <qlayout.h> #include <qlineedit.h> #include <qpushbutton.h> #include "logindialog.h" LoginDialog::LoginDialog(QWidget *parent) : LoginDialogBase(parent) { connect(dbNameLineEdit, SIGNAL(textChanged(const QString &)), this, SLOT(enableLoginButton())); connect(userNameLineEdit, SIGNAL(textChanged(const QString &)), this, SLOT(enableLoginButton())); connect(passwordLineEdit, SIGNAL(textChanged(const QString &)), this, SLOT(enableLoginButton())); connect(hostNameLineEdit, SIGNAL(textChanged(const QString &)), this, SLOT(enableLoginButton())); connect(portLineEdit, SIGNAL(textChanged(const QString &)), this, SLOT(enableLoginButton())); connect(loginButton, SIGNAL(clicked()), this, SLOT(accept())); connect(cancelButton, SIGNAL(clicked()), this, SLOT(reject())); }
The enableLoginButton() slot is not listed here, since it's identical to the slot of the same name in the original version of the LoginDialog class.
One of the main advantages of Qt Designer is that the code generated by uic is kept totally separate from the application's hand-written code. This gives you the flexibility to change your user interface without needing to rewrite the code or fearing that your modifications to generated code will be lost.
This completes our review of creating dialogs with Qt. In the next article, we will see how to create custom widgets with any look and behaviour we want.
Notes:
More fields may be available via dynamicdata ..