Standard Dialogs

We consider a dialog to be a "standard" dialog if it initializes its widgets in accordance with the values set through its initializer or through its methods, and whose final values are obtained by method calls or from instance variables—not directly from the dialog's widgets. A standard dialog can have both widget-level and form-level validation. Standard dialogs are either modal, with "accept" and "reject" buttons, or (less commonly) modeless, in which case they have "apply" and "close" buttons and notify state changes through signal and slot connections.

One key advantage of standard dialogs is that the caller does not need to know about their implementation, only how to set the initial values, and how to get the resultant values if the user clicked OK. Another advantage, at least for modal standard dialogs, is that the user cannot interact with the dialog's parent windows and their sibling windows, so the relevant parts of the application's state will probably not change behind the dialog's back. The main drawback of using a standard dialog is most apparent when it must handle lots of different data items, since all the items must be fed into the dialog and the results retrieved on each invocation, and this may involve many lines of code.

As with the previous section, we will explain by means of an example. In this case, the example will be used both in this section and in the next section so that we can see the different approaches and trade-offs between standard and smart dialogs more clearly.

Let us imagine that we have an application that needs to display a table of floating-point numbers, and that we want to give users some control over the format of the numbers. One way to achieve this is to provide a menu option, toolbar button, or keyboard shortcut that will invoke a modal dialog which the user can interact with to set their formatting preferences. Figure 5.3 shows a number format dialog that has been popped up over a table of numbers.

J

K

L M N

0

16

2,205.70

2,311.90

5470,59

2,503,70

■ Set Number Format (Modal)

17

3,693.38

2,404.92

:,479.08

3,340.29

18

-9510.54

3,133.07

Thousands separator ,| Decimal marker Decimal places 2 1 1 Red negative numbers

¡,562.17

4,349.04

19

-3,7880.19

2,661.66

¡,215.60

-3,7610.05

20

-1,7280.27

4,059.87

9590.78

1,643.36

21

-2,3690.62

-3,4650.37

OK J [ Cancel

9340,12

4,987,43

22

-3,9900.38

-3,4430,89 v

981.94

UJUU1_UII_U |

11

""

1>

Figure 5.3 The modal Set Number Format dialog in context

Figure 5.3 The modal Set Number Format dialog in context

The data that we want the dialog to make available to the user is held in a dictionary in the main form. Here is how the dictionary is initialized:

self.format = dict(thousandsseparator=",", decimalmarker=".", decimalplaces=2, rednegatives=False)

Using a dictionary like this is very convenient, and makes it easy to add additional items.

We have put the dialog in its own file, numberformatdlg1.py, which the application, numbers.pyw, imports. The number "1" in the filename distinguishes it from the other two versions of the dialog covered in the next section.

Modal OK/Cancel-Style Dialogs

Let us begin by seeing how the dialog is used; we assume that the setNumber-Format1() method is called in response to some user action.

def setNumberFormat1(self):

dialog = numberformatdlg1.NumberFormatDlg(self.format, self) if dialog.exec_():

self.format = dialog.numberFormat() self.refreshTable()

We start by creating the dialog and passing it the format dictionary from which the dialog will initialize itself, and self so that the dialog is tied to the calling form—centered over it and not having its own taskbar entry.

As we mentioned earlier, calling exec_() pops up the dialog it is called on as a modal dialog, so the user must either accept or reject the dialog before they can interact with the dialog's parents and their siblings. In the next section, we will use modeless versions of the dialog that don't impose this restriction.

If the user clicks OK, we set the format dictionary to have the values set in the dialog, and update the table so that the numbers are displayed with the new format. If the user cancels, we do nothing. At the end of the method, the dialog goes out of scope and is therefore scheduled for garbage collection.

To save space, and to avoid needless repetition, from now on we will not show any import statements, unless their presence is not obvious. So, for example, we will no longer show from PyQt4.QtCore import * or the PyQt4.QtGui import.

We are now ready to see the implementation of the dialog itself. class NumberFormatDlg(QDialog):

def_init_(self, format, parent=None):

super(NumberFormatDlg, self)._init_(parent)

The_init_() method begins in the same way as all the other dialogs we have seen so far.

thousandsLabel = QLabel("&Thousands separator") self.thousandsEdit = QLineEdit(format["thousandsseparator"]) thousandsLabel.setBuddy(self.thousandsEdit) decimalMarkerLabel = QLabel("Decimal &marker") self.decimalMarkerEdit = QLineEdit(format["decimalmarker"]) decimalMarkerLabel.setBuddy(self.decimalMarkerEdit) decimalPlacesLabel = QLabel("&Decimal places") self.decimalPlacesSpinBox = QSpinBox() decimalPlacesLabel.setBuddy(self.decimalPlacesSpinBox) self.decimalPlacesSpinBox,setRange(0, 6) self.decimalPlacesSpinBox.setValue(format["decimalplaces"]) self.redNegativesCheckBox = QCheckBox("&Red negative numbers") self.redNegativesCheckBox.setChecked(format["rednegatives"])

buttonBox = QDialogButtonBox(QDialogButtonBox.Ok|

QDialogButtonBox.Cancel)

For each aspect of the format that we want the user to be able to change we create a label so that they know what they are editing, and a suitable editing widget. Since the format argument is mandatory, we assume that it has all Buddies the values we need, so we use it to initialize the editing widgets. We also use 143« setBuddy() calls to support keyboard users since not all users are able to use the mouse.

Table 5.2 Selected QDialogButtonBox Methods and Signals

Syntax d.addButton(b, r)

d.addButton(t, r)

d.addButton(s)

d.setOrientation(o)

d.button(s)

d.accepted()

d.rejected()

Description

Adds QPushButton b, with QDialogButtonBox.ButtonRole r, to QDialogButtonBox d

Adds a QPushButton with text t and with button role r to QDialogButtonBox d, and returns the added button Adds a QPushButton, specified as QDialogButton-Box.StandardButton s, to QDialogButtonBox d and returns the added button Sets the QDialogButtonBox's orientation to Qt.Orientation o (vertical or horizontal) Returns the QDialogButtonBox's QPushButton specified as StandardButton s, or None if there isn't one This signal is emitted if a button with the QDialogBut-tonBox.Accept role is clicked

This signal is emitted if a button with the QDialogBut-tonBox.Reject role is clicked

The only validation we have put in place is to limit the range of the decimal places spinbox. We have chosen to do "post-mortem" validation, that is, to validate after the user has entered values, at the point where they click OK to accept their edits. In the next section, we will see "preventative" validation, which prevents invalid edits in the first place.

self.format = format.copy()

We need to take a copy of the format dictionary that was passed in, since we want to be able to change the dictionary inside the dialog without affecting the original dictionary.

grid = QGridLayout() grid.addWidget(thousandsLabel, 0, 0) grid.addWidget(self.thousandsEdit, 0, 1) grid.addWidget(decimalMarkerLabel, 1, 0) grid.addWidget(self.decimalMarkerEdit, 1, 1) grid.addWidget(decimalPlacesLabel, 2, 0) grid.addWidget(self.decimalPlacesSpinBox, 2, 1) grid.addWidget(self.redNegativesCheckBox, 3, 0, 1, 2) grid.addWidget(buttonBox, 4, 0, 1, 2) self.setLayout(grid)

The layout is very similar in appearance to the one we used for the Pen Properties dialog, except that this time we have a QDialogButtonBox widget rather than a layout for the buttons. This makes it possible to create the entire layout using a single QGridLayout.

thousandsLabel

self.thousandsEdit

decimalMarkerLabel

self.decimalMarkerEdit

decimalPlacesLabel

self.decimalPlacesSpinBox

self.redNegativesCheckBox

okButton cancelButton

Figure 5.4 The Set Number Format dialog's layout

Figure 5.4 The Set Number Format dialog's layout

Both the "red negatives" checkbox and the button box are laid out so that they each span one row and two columns. Row and column spans are specified by the last two arguments to the QGridLayout's addWidget() and addLayout() methods. The layout is shown in Figure 5.4, with the grid shown shaded.

self.connect(buttonBox, SIGNAL("accepted()"), self, SLOT("accept()")) self.connect(buttonBox, SIGNAL("rejected()"), self, SLOT("reject()")) self.setWindowTitle("Set Number Format (Modal)")

The code for making the connections and setting the window's title is similar to what we used for the Pen Properties dialog, only this time we use the button box's signals rather than connecting directly to the buttons themselves.

def numberFormat(self): return self.format

If the user clicks OK, the dialog is accepted and returns a True value. In this case, the calling form's method overwrites its format dictionary with the dialog's dictionary, by calling the numberFormat() method. Since we have not made the dialog's self.format attribute very private (i.e., by calling it_format), we could have accessed it from outside the form directly; we will take that approach in a later example.

When the user clicks OK, because we are using post-mortem validation, it is possible that some of the editing widgets contain invalid data. To handle this, we reimplement QDialog.accept() and do our validation there. Because the method is quite long, we will look at it in parts.

def accept(self):

class ThousandsError(Exception): pass class DecimalError(Exception): pass Punctuation = frozenset(" ,;:.")

We begin by creating two exception classes that we will use inside the accept() method. These will help to keep our code cleaner and shorter than would otherwise be possible. We also create a set of the characters that we will allow to be used as thousands separators and decimal place markers.

The only editing widgets we are concerned with validating are the two line edits. This is because the decimal places spinbox is already limited to a valid range, and because the "red negatives" checkbox can only be checked or unchecked, both of which are valid.

thousands = unicode(self.thousandsEdit.text()) decimal = unicode(self.decimalMarkerEdit.text()) try:

raise DecimalError, ("The decimal marker may not be " "empty.")

raise ThousandsError, ("The thousands separator may " "only be empty or one character.")

raise DecimalError, ("The decimal marker must be " "one character.")

if thousands == decimal:

raise ThousandsError, ("The thousands separator and " "the decimal marker must be different.") if thousands and thousands not in Punctuation:

raise ThousandsError, ("The thousands separator must " "be a punctuation symbol.") if decimal not in Punctuation:

raise DecimalError, ("The decimal marker must be a " "punctuation symbol.")

except ThousandsError, e:

QMessageBox.warning(self, "Thousands Separator Error", unicode(e)) self.thousandsEdit.selectAll() self.thousandsEdit.setFocus() return except DecimalError, e:

QMessageBox.warning(self, "Decimal Marker Error", unicode(e)) self.decimalMarkerEdit.selectAll() self.decimalMarkerEdit.setFocus() return

We begin by getting the text from the two line edits. Although it is acceptable to have no thousands separator, a decimal marker must be present, so we begin by checking that the decimalMarkerEdit has at least one character. If it doesn't, we raise our custom DecimalError with suitable error text. We also raise exceptions if either of the texts is longer than one character, or if they are the same character, or if either contains a character that is not in our Punctuation set. The if statements differ regarding punctuation because the thousands separator is allowed to be empty, but the decimal place marker is not.

Figure 5.5 A QMessageBox warning

We have used parentheses around the error strings that are in two parts to turn them into single expressions; an alternative syntax would have been to drop the parentheses, and instead concatenate the two parts and escape the newline.

Depending on whether we get a ThousandsError or a DecimalError, we display a "warning" message box with appropriate error text, as illustrated in Figure 5.5. We must convert the exception object e to be a string (we have used unicode() to do this) so that it is suitable as an argument to the QMessageBox's static warning() method. We will make more use of the QMessageBox static methods, including the use of additional arguments, both in this chapter and throughout the book.

Once the user has acknowledged the error message by closing the message box, we select the text in the invalid line edit and give the focus to the line edit, ready for the user to make their correction. Then we return—so the dialog is not accepted and the user must either fix the problem or click Cancel to close the dialog and abandon their changes.

self.format["thousandsseparator"] = thousands self.format["decimalmarker"] = decimal self.format["decimalplaces"] = \

self.decimalPlacesSpinBox.value() self.format["rednegatives"] = \

self.redNegativesCheckBox.isChecked() QDialog.accept(self)

If no exception is raised, neither of the return statements is executed and execution falls through to this final part of the accept() method. Here we update the dialog's format dictionary with the values from the editing widgets, and call the base class's accept() method. The form will be closed (i.e., hidden) and a True value returned from the exec_() statement. As we saw earlier, the caller, on receiving a True value from exec_(), goes on to retrieve the dialog's format using the numberFormat() method.

Why didn't we use super() to call the base class's accept() at the end instead of naming QDialog explicitly? The short answer is that using super() in this context won't work. PyQt tries to be as efficient as possible by using lazy attribute lookup, but the result is that super() does not work as we would expect in PyQt

QMessageBox sidebar

subclasses. (For an explanation, see the PyQt pyqt4ref.html documentation, under "super and PyQt classes".)

Although the dialog is hidden only when it is accepted (or rejected), once it goes out of scope, that is, at the end of the caller's setNumberFormat1() method, the dialog is scheduled for garbage collection.

Creating modal dialogs like this one is usually straightforward. The only complications involved concern whether we have layouts and validation that require some care to get right, as we do here.

In some cases the user will want to be able to see the results of their choices, perhaps changing their choices a few times until they are satisfied. For these situations modal dialogs can be inconvenient since the user must invoke the dialog, perform their edits, accept, see the results, and then repeat the cycle until they are happy. If the dialog was modeless and was able to update the application's state without being closed, the user could simply invoke the dialog once, perform their edits, see the effects, and then do more edits, and so on: a much faster cycle. We will see how to achieve this in the next section; we will also look at a much simpler and more active validation strategy—preventative validation.

Was this article helpful?

0 -2
Tuberminator

Tuberminator

The main focus of this report is to show how to get involved in video marketing on the run, how to rank quickly on YouTube and Google using FREE semi-automatic tools and services. QUICKLY AND FREE. I will show methods and techniques I use to rank my videos, as well as free resources and tools to make video clips, to get backlinks and free traffic.

Get My Free Ebook


Post a comment