Dumb Dialogs

We define a "dumb" dialog to be a dialog whose widgets are set to their initial values by the dialog's caller, and whose final values are obtained directly from the widgets, again by the dialog's caller. A dumb dialog has no knowledge of what data its widgets are used to present and edit. We can apply some basic validation to a dumb dialog's widgets, but it is not common (or always possible) to set up validation that incorporates interdependencies between widgets; in other words, form-level validation is not usually done in dumb dialogs. Dumb dialogs are normally modal dialogs with an "accept" button (e.g., OK) and a "reject" button (e.g., Cancel).

The main advantages of using dumb dialogs are that we do not have to write any code to provide them with an API, nor any code for additional logic. Both of these benefits are a consequence of all their widgets being publically accessible. The main disadvantages are that the code that uses them is tied to their user interface (because we access the widgets directly), so we cannot easily implement complex validation—and they are much less convenient than a standard or smart dialog if needed in more than one place.

We will begin with a concrete example. Suppose we have a graphics application and we want to let the user set some pen properties—for example, the pen's width, style, and whether lines drawn with it should have beveled edges. Figure 5.1 shows what we want to achieve.

Figure 5.1 The Pen Properties dialog

In this case, we don't need "live" or interactive updating of the pen's properties, so a modal dialog is sufficient. And since the validation required is quite simple, we can use a dumb dialog in this situation.

We would use the dialog by popping it up modally in a slot that is connected to a menu option, toolbar button, or dialog button. If the user clicked OK, we would then update our pen properties; if they clicked Cancel, we would do nothing. Here is what the calling slot might look like:

def setPenProperties(self):

dialog = PenPropertiesDlg(self) dialog.widthSpinBox.setValue(self.width) dialog.beveledCheckBox.setChecked(self.beveled) dialog.styleComboBox.setCurrentIndex(

dialog.styleComboBox.findText(self.style)) if dialog.exec_():

self.width = dialog.widthSpinBox.value() self.beveled = dialog.beveledCheckBox.isChecked() self.style = unicode(dialog.styleComboBox.currentText()) self.updateData()

We begin by creating a PenPropertiesDlg dialog—we will see the details of this shortly; all we need to know now is that it has a width spinbox, a beveled checkbox, and a style combobox. We pass a parent, self (the calling form) to the dialog, to take advantage of the fact that by default, PyQt centers a dialog over its parent, and also because dialogs that have a parent do not get a separate entry in the taskbar. We then access the widgets directly, setting their values to those held by the calling form. The QComboBox.findText() method returns the index position of the item with the matching text.

When we call exec_() on a dialog, the dialog is shown modally. This means that the dialog's parent windows and their sibling windows are blocked until the dialog is closed. Only when the user closes the dialog (either by "accepting" or by "rejecting" it) does the exec_() call return. The return value evaluates to True if the user accepted the dialog; otherwise, it evaluates to False. If the user accepted the dialog we know that they want their settings to take effect, so we read them out of the dialog's widgets and update our application's data. The updateData() call at the end is just one of our own custom methods that makes the application show the pen properties in the main window.

At the end of the setPenProperties() method the PenPropertiesDlg will go out of scope and will become a candidate for garbage collection. For this reason, we must always create a new dialog and populate its widgets whenever setPen-Properties() is called. This approach saves memory, at the price of some speed overhead. For tiny dialogs like this, the overhead is too small for the user to notice, but later on we will show an alternative approach that avoids creating and destroying dialogs every time.

Using a dumb dialog means that the dialog is quite loosely coupled to the application. We could completely decouple it by making the labels accessible as instance variables. Then we could use the PenPropertiesDlg to edit any kind of data that required a spinbox, a checkbox, and a combobox, simply by changing the labels. For example, we could use it to record a weather reading with a "Temperature" spinbox, an "Is raining" checkbox, and a "Cloud cover" combobox.

Now that we have seen how we can use the dialog, let's look at the code that implements it. The PenPropertiesDlg has a single method,_init_(), which we will look at in parts.

class PenPropertiesDlg(QDialog):

def_init_(self, parent=None):

super(PenPropertiesDlg, self)._init_(parent)

Not surprisingly, our dialog is a QDialog subclass, and we initialize it in the way we have seen a few times already.

widthLabel = QLabel("&Width:") self.widthSpinBox = QSpinBox() widthLabel.setBuddy(self.widthSpinBox)

self.widthSpinBox.setAlignment(Qt.AlignRight|Qt.AlignVCenter)

self.widthSpinBox,setRange(0, 24)

self.beveledCheckBox = QCheckBox("&Beveled edges")

styleLabel = QLabel("&Style:")

self.styleComboBox = QComboBox()

styleLabel.setBuddy(self.styleComboBox)

self.styleComboBox.addItems(["Solid", "Dashed", "Dotted",

"DashDotted", "DashDotDotted"])

okButton = QPushButton("&OK") cancelButton = QPushButton("Cancel")

For each editing widget, we also create a corresponding label so that the user can tell what they are editing. When we put an ampersand (&) in a label's text it can have two possible meanings. It can simply be a literal ampersand. Or it can signify that the ampersand should not be shown, but instead the letter following it should be underlined to show that it represents a keyboard accelerator. For example, in the case of the widthLabel, its text of "&Width:" will appear as Width: and its accelerator will be Alt+W. On Mac OS X the default behavior is to ignore accelerators; for this reason, PyQt does not display the underlines on this platform.

What distinguishes between a literal ampersand and an accelerator ampersand is if the label has a "buddy": If it does, the ampersand signifies an accelerator. A buddy is a widget that PyQt will pass the keyboard focus to when the corresponding label's accelerator is pressed. So, when the user presses Alt+W, the keyboard focus will be switched to the widthSpinBox. This in turn means that if the user presses the up or down arrow keys or PageUp or PageDown, these will affect the widthSpinBox since it has the keyboard focus.

In the case of buttons, an underlined letter in the button's text is used to signify an accelerator. So in this case, the okButton's text, "&OK", appears as OK, and the user can press the button by clicking it with the mouse, by tabbing to it and pressing the spacebar, or by pressing Alt+O. It is not common to provide an accelerator for Cancel (or Close) buttons since these are normally connected to the dialog's reject() slot, and QDialog provides a keyboard shortcut for that, the Esc key.* Checkboxes and radio buttons are somewhat similar to buttons in that they have text that can have an accelerator. For example, the beveled checkbox has an underlined "B", so the user can toggle the checkbox's checked state by pressing Alt+B.

One disadvantage of creating buttons like this is that when we come to lay them out we will do so in one particular order. For example, we might put OK to the left of Cancel. But on some windowing systems this order is wrong. PyQt has a solution for this, covered in the Dialog Button Layout sidebar.

We have aligned the spinbox's number to the right, vertically centered, and set its valid range to be 0-24. In PyQt, a pen width (i.e., a line width) of 0 is allowed and signifies a 1-pixel-wide width regardless of any transformations. Pen widths of 1 and above are drawn at the given width, and respect any transformations, such as scaling, that are in force.

By using a spinbox and setting a range for it, we avoid the possibility of invalid pen widths that might have been entered had we used, for example, a line edit. Very often, simply choosing the right widget and setting its properties appropriately provides all the widget-level validation that is needed. This is also shown by our use of the beveled checkbox: Either the pen draws lines with beveled edges or it doesn't. And the same is true again with our use of a combobox of line styles—the user can choose only a valid style, that is, a style from a list that we have provided.

buttonLayout = QHBoxLayout() buttonLayout.addStretch() buttonLayout.addWidget(okButton) buttonLayout.addWidget(cancelButton) layout = QGridLayout() layout.addWidget(widthLabel, 0, 0) layout.addWidget(self.widthSpinBox, 0, 1) layout.addWidget(self.beveledCheckBox, 0, 2) layout.addWidget(styleLabel, 1, 0) layout.addWidget(self.styleComboBox, 1, 1, 1, 2) layout.addLayout(buttonLayout, 2, 0, 1, 3) self.setLayout(layout)

We have used two layouts, one nested inside the other, to get the layout we want. We begin by laying out the buttons horizontally, beginning with a "stretch". The stretch will consume as much space as possible, which has the effect of pushing the two buttons as far to the right as they can go, and still fit.

* We use the terms "keyboard accelerator" and "accelerator" for the Alt+Leffer key sequences that can be used to click buttons and switch focus in dialogs, and to pop up menus. We use the term "keyboard shortcut" for any other kind of key sequence—for example, the key sequence Ctrl+S, which is often used to save files. We will see how to create keyboard shortcuts in Chapter 6.

Dialog Button Layout

In some of our early examples, we have put the buttons on the right of the dialogs, with the OK button first and then the Cancel button next. This is the most common layout on Windows, but it is not always correct. For example, for Mac OS X or for the GNOME desktop environment, they should be swapped.

If we want our applications to have the most native look and feel possible and expect to deploy them on different platforms, issues like button ordering and positioning will matter to us. Qt 4.2 (PyQt 4.1) provides a solution for this particular problem: the QDialogButtonBox class.

Instead of creating OK and Cancel buttons directly, we create a QDialogBut-tonBox. For example:

buttonBox = QDialogButtonBox(QDialogButtonBox.Ok|

QDialogButtonBox.Cancel)

To make a button the "default" button, that is, the one that is pressed when the user presses Enter (assuming that the widget with keyboard focus does not handle Enter key presses itself), we can do this:

buttonBox.button(QDialogButtonBox.Ok).setDefault(True)

Since a button box is a single widget (although it contains other widgets), we can add it directly to the dialog's existing layout, rather than putting it in its own layout and nesting that inside the dialog's layout. Here is what we would do in the PenPropertiesDlg example's grid layout:

layout.addWidget(buttonBox, 3, 0, 1, 3)

And instead of connecting from the buttons' clicked() signals, we can make connections from the button box, which has its own signals that correspond to user actions:

self.connect(buttonBox, SIGNAL("accepted()"), self, SLOT("accept()")) self.connect(buttonBox, SIGNAL("rejected()"), self, SLOT("reject()"))

We are still free to connect to individual buttons' clicked() signals, though, and often do so for dialogs that have many buttons.

The QDialogButtonBox defaults to using a horizontal layout, but can be set to use a vertical layout by passing Qt.Vertical to its constructor, or by calling setOrientation().

We use QDialogButtonBox for most of the examples, but it could always be replaced by individual QPushButtons if backward compatibility was an issue.

widthLabel

widthSpinBox beveledCheckBox

styleLabel

styleComboBox

1 stretch okButton cancelButton

Figure 5.2 The Pen Properties dialog's layout

Figure 5.2 The Pen Properties dialog's layout

The width label, width spinbox, and bevel checkbox are laid out side by side in three columns using a grid layout. The style label and style combobox are put on the next row, with the style combobox set to span two columns. The arguments to the QGridLayout.addWidget() method are the widget, the row, the column, and then optionally, the number of rows to span, followed by the number of columns to span. We add the button layout as a third row to the grid layout, having it span all three columns. Finally, we set the layout on the dialog. The layout is shown schematically in Figure 5.2; the grid layout is shown shaded.

self.connect(okButton, SIGNAL("clicked()"), self, SLOT("accept()")) self.connect(cancelButton, SIGNAL("clicked()"), self, SLOT("reject()")) self.setWindowTitle("Pen Properties")

At the end of_init_() we make the necessary connections. We connect the OK button's clicked() signal to the dialog's accept() slot: This slot will

Table 5.1 Selected Layout Methods

Syntax b.addLayout(l)

b.addSpacing(i) b.addStretch(i)

b.addWidget(w) b.setStretchFactor(x, i)

g.setRowStretch(r, i) g.setColumnStretch(c, i)

Description

Adds QLayout I to QBoxLayout b, which is normally a QHBoxLayout or a QVBoxLayout

Adds a QSpacerItem of fixed size int i to layout b

Adds a QSpacerItem with minimum size 0 and a stretch factor of int i to layout b Adds QWidget w to layout b

Sets the stretch factor of layout b's layout or widget x to int i

Adds QLayout I to QGridLayout g at row int r and column int c; additional row span and column span arguments can be given Adds QWidget w to QGridLayout g at row int r and column int c; additional row span and column span arguments can be given Sets QGridLayout g's row r's stretch to int i Sets QGridLayout g's column c's stretch to int i close the dialog and return a True value. The Cancel button is connected in a corresponding way. Finally, we set the window's title.

For small dumb dialogs that are only ever called from one place, it is possible to avoid creating a dialog class at all. Instead, we can simply create all the widgets in the invoking method, lay them out, connect them, and call exec_(). If exec_() returns True, we can then extract the values from the widgets and we are done. The file chap05/pen.pyw contains the Pen Properties dialog and a dummy program with two buttons, one to invoke the PenPropertiesDlg we have just reviewed and another that does everything inline. Creating dialogs inline is not an approach that we would recommend, so we will not review the code for doing it, but it is mentioned and provided in the example's setPenInline() method for completeness.

Dumb dialogs are easy to understand and use, but setting and getting values using a dialog's widgets is not recommended except for the very simplest dialogs, where only one, two, or at most, a few values are involved. We have shown them primarily as a gentle introduction to dialogs, since creating, laying out, and connecting the widgets is the same in any kind of dialog. In the next section, we will look at standard dialogs, both modal and modeless ones.

+1 0

Responses

  • Abe Pazos
    Great tutorials! Suggestion: make source code different from article text. Just changing the background color would help. I think it would be much more readable. Thanks!
    6 years ago
  • PETRONILLA
    How to call dialog from menu item pyqt5?
    2 years ago

Post a comment