Smart Dialogs

We define a "smart" dialog to be one that initializes its widgets in accordance with data references or data structures that are passed to its initializer, and which is capable of updating the data directly in response to user interaction. Smart dialogs can have both widget-level and form-level validation. Smart dialogs are usually modeless, with "apply" and "close" buttons, although they can also be "live", in which case they may have no buttons, with changes to widgets reflected directly into the data they have access to. Smart modeless

Table 5.3 Selected QDialog Methods

Syntax Description d.accept() Closes (hides) QDialog d, stops its event loop, and causes exec_() to return with a True value. The dialog is deleted if Qt.WA_DeleteOnClose is set d.reject() Closes (hides) QDialog d, stops its event loop, and causes exec_() to return with a False value d.done(i) Closes (hides) QDialog d, stops its event loop, and causes exec_() to return int i d.exec_() Shows QDialog d modally, blocking until it is closed d.show() Shows QDialog d modelessly; inherited from QWidget d.setSizeGrip- Shows or hides QDialog d's size grip depending on bool b Enabled(b)

dialogs that have "apply" buttons notify state changes through signal and slot connections.

The main benefit of using a smart modeless dialog is seen at the point of use. When the dialog is created, it is given references to the calling form's data structures so that the dialog can update the data structures directly with no further code required at the call point. The downsides are that the dialog must have knowledge of the calling form's data structures so that it correctly reflects the data values into its widgets and only applies changes that are valid, and that, being modeless, there is a risk of the data the dialog depends on being changed from under it if the user interacts with some other part of the application.

In this section we are going to continue with the theme of number format dialogs so that we can compare the various approaches.

Modeless Apply/Close-Style Dialogs

If we want our users to be able to repeatedly change the number format and see the results, it will be much more convenient for them if they could do so without having to keep invoking and accepting the number format dialog. The solution is to use a modeless dialog which allows them to interact with the number format widgets and to apply their changes and to see the effect, as often as they like. Dialogs like this usually have an Apply button and a Close button. Unlike a modal OK/Cancel-style dialog, which can be canceled, leaving everything as it was before, once Apply has been clicked the user cannot revert their changes. Of course we could provide a Revert button or a Defaults button, but this would require more work.

Superficially, the only difference between the modeless and the modal versions of the dialog is the button text. However, there are two other important differences: The calling form's method creates and invokes the dialog differently, and the dialog must make sure it is deleted, not just hidden, when it is closed. Let us begin by looking at how the dialog is invoked.

def setNumberFormat2(self):

dialog = numberformatdlg2.NumberFormatDlg(self.format, self) self.connect(dialog, SIGNAL("changed"), self.refreshTable) dialog.show()

We create the dialog in the same way we created the modal version earlier; it is shown in Figure 5.6. We then connect the dialog's changed Python signal to the calling form's refreshTable() method, and then we just call show() on the dialog. When we call show(), the dialog is popped up as a modeless dialog. Application execution continues concurrently with the dialog, and the user can interact with both the dialog and other windows in the application.

Whenever the dialog emits its changed signal, the main form's refreshTable() method is called, and this will reformat all the numbers in the table using

■ Set Number Format (Modeless) 0®

Thousands separator

Decimal marker

Decimal places 12

V

1 1 Red negative numbers

Close

Apply

Figure 5.6 The modeless Set Number Format dialog

Figure 5.6 The modeless Set Number Format dialog the format dictionary's settings. We can imagine that this means that when the user clicks the Apply button the format dictionary will be updated and the changed signal emitted. We will see shortly that this is indeed what happens.

Although the dialog variable goes out of scope, PyQt is smart enough to keep a reference to modeless dialogs, so the dialog continues to exist. But when the user clicks Close, the dialog would normally only be hidden, so if the user invoked the dialog again and again, more and more memory would be needlessly consumed, as more dialogs would be created but none deleted. One solution to this is to make sure that the dialog is deleted, rather than hidden, when it is closed. (We will see another solution when we look at a "live" dialog, shortly.)

We shall start with the dialog's_init_() method.

def _init_(self, format, parent=None):

super(NumberFormatDlg, self)._init_(parent)

self.setAttribute(Qt.WA_DeleteOnClose)

After calling super(), we call setAttribute() to make sure that when the dialog is closed it will be deleted rather than merely hidden.

thousandsLabel = QLabel("&Thousands separator") self.thousandsEdit = QLineEdit(format["thousandsseparator"]) thousandsLabel.setBuddy(self.thousandsEdit) self.thousandsEdit.setMaxLength(1) self.thousandsEdit.setValidator(

QRegExpValidator(punctuationRe, self))

decimalMarkerLabel = QLabel("Decimal &marker") self.decimalMarkerEdit = QLineEdit(format["decimalmarker"]) decimalMarkerLabel.setBuddy(self.decimalMarkerEdit) self.decimalMarkerEdit.setMaxLength(1) self.decimalMarkerEdit.setValidator(

QRegExpValidator(punctuationRe, self)) self.decimalMarkerEdit.setInputMask("X")

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.Apply|

QDialogButtonBox.Close)

The creation of the form's widgets is very similar to what we did before, but this time we are using preventative validation almost exclusively. We set a one-character maximum length on the thousands separator and decimal marker line edits, and in both cases we also set a QRegExpValidator. A validator will only allow the user to enter valid characters, and in the case of a regular expression validator, only characters that match the regular expression.* PyQt uses a regular expression syntax that is essentially a subset of the syntax offered by Python's re module.

The QRegExpValidator's initializer requires both a regular expression and a parent, which is why we have passed self in addition to the regular expression.

In this case, we have set the validation regular expression to be "[,;:.]". This is a character class and means that the only characters that are valid are those contained in the square brackets, that is, space, comma, semicolon, colon, and period. Notice that the regular expression string is preceded by "r". This signifies a "raw" string and means that (almost) all of the characters inside the string are to be taken as literals. This considerably reduces the need to escape regular expression special characters such as "\", although here it does not matter. Nonetheless, we always use "r" with regular expression strings as a matter of good practice.

Although we are happy to accept an empty thousands separator, we require a decimal marker. For this reason we have used an input mask. A mask of "X" says that one character of any kind is required—we don't have to concern ourselves with what the character will be because the regular expression validator will ensure that it is valid. Format masks are explained in the QLineEd-it.inputMask property's documentation.®

The only other difference to the way we created the widgets in the modal version of the dialog is that we create Apply and Close buttons rather than OK and Cancel buttons.

★ The QRegExp documentation provides a brief introduction to regular expressions. For in-depth coverage, see Mastering Regular Expressions by Jeffrey E. Friedl.

® Every PyQt QObject and QWidget has "properties". These are similar in principle to Python properties, except that they can be accessed using the property!) and setProperty() methods.

self.format = format

In the modal dialog we took a copy of the caller's format dictionary; here we take a reference to it, so that we can change it directly from within the dialog.

We will not show the dialog's layout since it is identical to the layout used in the modal dialog shown earlier.

self.connect(buttonBox.button(QDialogButtonBox.Apply),

SIGNAL("clicked()"), self.apply) self.connect(buttonBox, SIGNAL("rejected()"), self, SLOT("reject()")) self.setWindowTitle("Set Number Format (Modeless)")

We create two signal-slot connections. The first one is between the Apply button's clicked() signal and the apply() method. To make this connection, we must retrieve a reference to the button from the button box using its button() method, passing the same argument, QDialogButtonBox.Apply, that we used to create the button in the first place.

The connection to reject() will cause the dialog to close, and because of the Qt.WA_DeleteOnClose attribute, the dialog will be deleted rather than hidden. There is no connection to the dialog's accept() slot, so the only way to get rid of the dialog is to close it. If the user clicks the Apply button, the apply() slot, shown next, will be called. Naturally, we also set a window title.

The final method in this class is apply(), which we will review in two parts.

def apply(self):

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

QMessageBox.warning(self, "Format Error",

"The thousands separator and the decimal marker " "must be different.") self.thousandsEdit.selectAll() self.thousandsEdit.setFocus() return if len(decimal) == 0:

QMessageBox.warning(self, "Format Error",

"The decimal marker may not be empty.") self.decimalMarkerEdit.selectAll() self.decimalMarkerEdit.setFocus() return

Form-level validation is normally necessary when two or more widgets' values are interdependent. In this example, we do not want to allow the thousands separator to be the same as the decimal place marker, so we check for this situation in the apply() method, and if it has occurred we notify the user, put the focus in the thousands separator line edit, and return without applying the user's edits.

We could have avoided this by connecting both line edits' textEdited() signals to a "check and fix" slot—we will do this in the next example.

We must also check that the decimal marker isn't empty. Although the decimal place marker's line edit regular expression validator wants a single character, it allows the line edit to be empty. This is because an empty string is a valid prefix for a string that has a valid character. After all, the line edit may have been empty when the user switched the focus into it.

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

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

self.redNegativesCheckBox.isChecked() self.emit(SIGNAL("changed"))

If there are no validation problems, neither of the return statements is executed and we fall through to the end of the accept() slot. Here we update the format dictionary. The self.format variable is a reference to the caller's format dictionary, so the changes are applied directly to the caller's data structure. Finally, we emit a changed signal, and as we have seen, this causes the caller's refreshTable() method to be called, which in turn formats all the numbers in the table using the caller's format dictionary.

This dialog is smarter than the standard one we created in the preceding section. It works directly on the caller's data structure (the format dictionary), and notifies the caller when the data structure has changed so that the changes can be applied. We could have made it smarter still and given it a reference to the caller's refreshTable() method and had the dialog execute it directly: We will use this approach in the next example.

In situations where the user wants to repeatedly apply changes, it may be inconvenient for them to keep having to click an Apply button. They may just want to manipulate a dialog's widgets and see the effects immediately. We will see how to do this next.

Modeless "Live" Dialogs

For our last number format example, we will review a smart modeless "live" dialog—a dialog that works very similarly to the one we have just seen, but which has no buttons, and where changes are applied automatically and immediately. The dialog is shown in Figure 5.7.

In the modal version of the dialog we used post-mortem validation, and in the smart modeless version we used a mixture of post-mortem and preventative validation. In this example, we will use preventative validation exclusively. Also, instead of creating a signal-slot connection so that the dialog can notify

Figure 5.7 The "live" Set Number Format dialog

the caller of changes, we give the dialog the method to call when there are changes to be applied so that it can call this method whenever necessary.

We could create this dialog in exactly the same way as the previous dialog, but we will instead demonstrate a different approach. Rather than creating the dialog when it is needed and then destroying it, creating and destroying on every use, we will create it just once, the first time it is needed, and then hide it when the user is finished with it, showing and hiding on every use.

def setNumberFormat3(self):

if self.numberFormatDlg is None:

self.numberFormatDlg = numberformatdlg3.NumberFormatDlg( self.format, self.refreshTable, self) self.numberFormatDlg.show() self.numberFormatDlg.raise_() self.numberFormatDlg.activateWindow()

In the calling form's initializer, we have the statement self.numberFormatDlg = None. This ensures that the first time this method is called the dialog is created. Then, we show the dialog as before. But in this case, when the dialog is closed it is merely hidden (because we do not set the Qt.WA_DeleteOnClose widget attribute). So when this method is called, we may be creating and showing the dialog for the first time, or we may be showing a dialog that was created earlier and subsequently hidden. To account for the second possibility, we must both raise (put the dialog on top of all the other windows in the application) and activate (give the focus to the dialog); doing these the first time is harmless.*

Also, we have made the dialog even smarter than the previous one, and instead of setting up a signal-slot connection, we pass the bound refreshTable() method to the dialog as an additional parameter.

The_init_() method is almost the same as before, with just three differences.

First, it does not set the Qt.WA_DeleteOnClose attribute so that when the dialog is closed, it will be hidden, not deleted. Second, it keeps a copy of the method it is passed (i.e., it keeps a reference to self.refreshTable() in self.callback), and

*PyQt uses raise_() rather than raise() to avoid conflict with the built-in raise statement.

third, its signal and slot connections are slightly different than before. Here are the connection calls:

self.connect(self.thousandsEdit,

SIGNAL("textEdited(QString)"), self.checkAndFix) self.connect(self.decimalMarkerEdit,

SIGNAL("textEdited(QString)"), self.checkAndFix) self.connect(self.decimalPlacesSpinBox,

SIGNAL("valueChanged(int)"), self.apply) self.connect(self.redNegativesCheckBox,

SIGNAL("toggled(bool)"), self.apply)

As before, we can rely on the decimal places spinbox to ensure that only a valid number of decimal places is set, and similarly the "red negatives" checkbox can only be in a valid state, so changes to either of these can be applied immediately.

But for the line edits, we now connect their textEdited() signals. These signals are emitted whenever the user types in a character or deletes a character from them. The checkAndFix() slot will both ensure that the line editshold valid text and apply the change immediately. There are no buttons in this dialog: The user can close it by pressing Esc, which will then hide it. The dialog will be deleted only when its calling form is deleted, because at that point the caller's self.numberFormatDlg instance variable will go out of scope, and with no other reference to the dialog, it will be scheduled for garbage collection.

def apply(self):

self.format["thousandsseparator"] = \

unicode(self.thousandsEdit.text()) self.format["decimalmarker"] = \

unicode(self.decimalMarkerEdit.text()) self.format["decimalplaces"] = \

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

self.redNegativesCheckBox.isChecked() self.callback()

The apply() method is the simplest we have seen so far. This is because it is called only when all the editing widgets hold valid data, so no postmortem validation is required. It no longer emits a signal to announce a state change—instead, it calls the method it was given and this applies the changes directly to the caller's form.

def checkAndFix(self):

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

self.thousandsEdit.clear() self.thousandsEdit.setFocus()

self.decimalMarkerEdit.setText(".") self.decimalMarkerEdit.selectAll() self.decimalMarkerEdit.setFocus() self.apply()

This method applies preventative validation as the user types in either of the line edits. We still rely on the line edit validators, maximum length properties, and in the case of the decimal place marker line edit, an input mask, with all of these combining to provide almost all the validation that we need. But it is still possible for the user to set the same text in both—in which case we delete the thousands separator and move the focus to its line edit, or (if the user tries hard) for the decimal place marker to be empty—in which case we set a valid alternative, select it, and give it the keyboard focus. At the end we know that both line edits are valid, so we call apply() and apply the changes.

One benefit of using the show/hide approach is that the dialog's state is maintained automatically. If we have to create the dialog each time it is used we must populate it with data, but for this dialog, whenever it is shown (after the first time), it already has the correct data. Of course, in this particular example we have three dialogs that are all used to edit the same data, which means that this dialog could become out of sync; we ignore this issue because having multiple dialogs editing the same data is not something we would do in a real application.

By passing in both the data structure (the format dictionary) and the caller's update method (refreshTable(), passed as self.callback), we have made this dialog very smart—and very tightly coupled to its caller. For this reason, many programmers prefer the "middle way" of using standard dialogs—dumb dialogs are too limited and can be inconvenient to use, and smart dialogs can be more work to maintain because of the tight coupling their knowledge of their callers' data structures implies.

Was this article helpful?

0 0
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