Restoring and Saving the Main Windows State

PC Repair Tools

Advanced Registry Cleaner PC Diagnosis and Repair

Get Instant Access

Now that the main window's user interface has been fully set up, we are almost ready to finish the initializer method, but before we do we will restore the application's settings from the previous run (or use default settings if this is the very first time the application has been run).

Before we can look at application settings, though, we must make a quick detour and look at the creation of the application object and how the main window itself is created. The very last executable statement in the imagechang-er.pyw file is the bare function call:

As usual, we have chosen to use a conventional name for the first function we execute. Here is its code:

app = QApplication(sys.argv)

app.setOrganizationName("Qtrac Ltd.")

app.setOrganizationDomain("qtrac.eu")

app.setApplicationName("Image Changer")

app.setWindowIcon(QIcon(":/icon.png"))

form = MainWindow()

form.show()

The function's first line is one we have seen many times before. The next three lines are new. Our primary use of them is for loading and saving application settings. If we create a QSettings object without passing any arguments, it will use the organization name or domain (depending on platform), and the application name that we have set here. So, by setting these once on the application object, we don't have to remember to pass them whenever we need a QSettings instance.

But what do these names mean? They are used by PyQt to save the application's settings in the most appropriate place—for example, in the Windows registry, or in a directory under $HOME/.config on Linux, or in $HOME/Library/ Preferences on Mac OS X. The registry keys or file and directory names are derived from the names we give to the application object.

We can tell that the icon file is loaded from the qrc_resources module because its path begins with :/.

After we have set up the application object, we create the main window, show it, and start off the event loop, in the same way as we have done in examples in previous chapters.

Now we can return to where we got up to in the MainWindow._init_() method, and see how it restores system settings.

settings = QSettings()

self.recentFiles = settings.value("RecentFiles").toStringList() size = settings.value("MainWindow/Size",

QVariant(QSize(600, 500))).toSize()

self.resize(size)

position = settings.value("MainWindow/Position",

self.move(position) self.restoreState(

settings.value("MainWindow/State").toByteArray())

self.setWindowTitle("Image Changer") self.updateFileMenu()

QTimer.singleShot(0, self.loadInitialFile)

We begin by creating a QSettings object. Since we passed no arguments, the names held by the application object are used to locate the settings information. We begin by retrieving the recently used files list. The QSettings.value() method always returns a QVariant, so we must convert it to the data type we are expecting.

Next, we use the two-argument form of value(), where the second argument is a default value. This means that the very first time the application is run, it has no settings at all, so we will get a QSize() object with a width of 600 pixels and a height of 500 pixels.* On subsequent runs, the size returned will be whatever the size of the main window was when the application was terminated—so long as we remember to save the size when the application terminates. Once we have a size, we resize the main window to the given size. After getting the previous (or default) size, we retrieve and set the position in exactly the same way.

There is no flickering, because the resizing and positioning are done in the main window's initializer, before the window is actually shown to the user.

Qt 4.2 introduced two new QWidget methods for saving and restoring a top-level window's geometry. Unfortunately, a bug meant that they were not reliable in all situations on X11-based systems, and for this reason we have restored the window's size and position as separate items. Qt 4.3 has fixed the bug, so with Qt 4.3 (e.g., with PyQt 4.3), instead of retrieving the size and position and calling resize() and move(), everything can be done using a single line:

self.restoreGeometry(settings.value("Geometry").toByteArray())

This assumes that the geometry was saved when the application was terminat- close-ed, as we will see when we look at the closeEvent(). Event()

The QMainWindow class provides a restoreState() method and a saveState() 185 method; these methods restore from and save to a QByteArray. The data they save and restore are the dock window sizes and positions, and the toolbar positions—but they work only for dock widgets and toolbars that have unique object names.

After setting the window's title, we call updateFileMenu() to create the File menu. Unlike the other menus, the File menu is generated dynamically; this is so that it can show any recently used files. The connection from the File menu's aboutToShow() signal to the updateFileMenu() method means that the File menu is created afresh whenever the user clicks File in the menu bar, or presses Alt+F. But until this method has been called for the first time, the File menu does not exist—which means that the keyboard shortcuts for actions that have not been added to a toolbar, such as Ctrl+Q for "file quit", will not work. In view of this, we explicitly call updateFileMenu() to create an initial File menu and to activate the keyboard shortcuts.

★ PyQt's documentation rarely gives units of measurement because it is assumed that the units are pixels, except for QPrinter, which uses points.

Doing Lots of Processing at Start-Up

If we need to do lots of processing at start-up—for example, if we need to load in lots of large files, we always do so in a separate loading method. At the end of the main form's constructor, the loading method is called through a zero-timeout single-shot timer.

What would happen if we didn't use a single-shot timer? Imagine, for example, that the method was loadInitialFiles() and that it loaded lots of multimegabyte files. The file loading would be done when the main window was being created, that is, before the show() call, and before the event loop (exec_()) had been started. This means that the user might experience a long delay between launching the application and actually seeing the application's window appear on-screen. Also, if the file loading might result in message boxes being popped up—for example, to report errors—it makes more sense to have these appear after the main window is shown, and when the event loop is running.

We want the main window to appear as quickly as possible so that the user knows that the launch was successful, and so that they can see any long-running processes, like loading large files, through the main window's user interface. This is achieved by using a single-shot timer as we did in the Image Changer example.

This works because a single-shot timer with a timeout of zero does not execute the slot it is given immediately. Instead, it puts the slot to be called in the event queue and then simply returns. At this point, the end of the main window's initializer is reached and the initialization is complete. The very next statement (in main() )isa show() call on the main window, and this does nothing except add a show event to the event queue. So, now the event queue has a timer event and a show event. A timer event with a timeout of zero is taken to mean "do this when the event queue has nothing else to do", so when the next statement, exec_(), is reached and starts off the event loop, it always chooses to handle the show event first, so the form appears, and then, with no other events left, the single-shot timer's event is processed, and the loadInitialFiles() call is made.

The initializer's last line looks rather peculiar. A single-shot timer takes a timeout argument (in milliseconds), and a method to call when the timeout occurs. So, it looks as though the line could have been written like this instead:

self.loadInitialFile()

In this application, where we load at most only one initial file, and where that file is very unlikely to be as big even as 1 MB, we could use either approach without noticing any difference. Nonetheless, calling the method directly is not the same as using a single-shot timer with a zero timeout, as the Doing Lots of Processing at Start-Up sidebar explains.

We have finished reviewing the code for initializing the main window, so now we can begin looking at the other methods that must be implemented to provide the application's functionality. Although the Image Changer application is just one specific example, to the greatest extent possible we have made the code either generic or easily adaptable so that it could be used as the basis for other main-window-style applications, even ones that are completely different.

In view of the discussions we have just had, it seems appropriate to begin our coverage with the loadInitialFile() method.

def loadInitialFile(self): settings = QSettings()

fname = unicode(settings.value("LastFile").toString()) if fname and QFile.exists(fname): self.loadFile(fname)

This method uses a QSettings object to get the last image that the application used. If there was such an image, and it still exists, the program attempts to load it. We will review loadFile() when we cover the file actions.

We could just as easily have written if fname and os.access(fname, os.F_OK): It makes no noticable difference here, but for multiperson projects, it may be wise to have a policy of preferring PyQt over the standard Python libraries or vice versa in cases like this, just to keep things as simple and clear as possible.

We discussed restoring the application's state a little earlier, so it seems appropriate to cover the close event, since that is where we save the application's state.

def closeEvent(self, event): if self.okToContinue(): settings = QSettings()

filename = QVariant(QString(self.filename)) \

if self.filename is not None else QVariant() settings.setValue("LastFile", filename) recentFiles = QVariant(self.recentFiles) \ if self.recentFiles else QVariant() settings.setValue("RecentFiles", recentFiles) settings.setValue("MainWindow/Size", QVariant(self.size())) settings.setValue("MainWindow/Position",

QVariant(self.pos())) settings.setValue("MainWindow/State", QVariant(self.saveState()))

else:

event.ignore()

If the user attempts to close the application, by whatever means (apart from killing or crashing it), the closeEvent() method is called. We begin by calling our own custom okToContinue() method; this returns True if the user really

Table 6.2 Selected QMainWindow Methods

Syntax m.addDockWidget(a, d)

m.addToolBar(s) m.menuBar()

m.restoreGeometry(ba)

m.restoreState(ba)

m.saveGeometry()

m.saveState()

m.setCentralWidget(w) m.statusBar()

m.setWindowIcon(i)

m.setWindowTitle(s)

Description

Adds QDockWidget d into Qt.QDockWidgetArea a in QMainWindow m

Adds and returns a new QToolBar called string s Returns QMainWindow m's QMenuBar (which is created the first time this method is called) Restores QMainWindow m's position and size to those encapsulated in QByteArray ba Restores QMainWindow m's dock widgets and toolbars to the state encapsulated in QByteArray ba Returns QMainWindow m's position and size encapsulated in a QByteArray Returns the state of QMainWindow m's dock widgets and toolbars, that is, their sizes and positions, encapsulated in a QByteArray

Sets QMainWindow m's central widget to be QWidget w Returns QMainWindow m's QStatusBar (which is created the first time this method is called) Sets QMainWindow m's icon to QIcon i; this method is inherited from QWidget

Sets QMainWindow m's title to string s; this method is inherited from QWidget wants to close, and False otherwise. It is inside okToContinue() that we give the user the chance to save unsaved changes. If the user does want to close, we create a fresh QSettings object, and store the "last file" (i.e., the file the user has open), the recently used files, and the main window's state. The QSettings class only reads and writes QVariant objects, so we must be careful to provide either null QVariants (created with QVariant()), or QVariants with the correct information in them.

If we take this approach, we do not need to save the main window's size or position separately.

def okToContinue(self): if self.dirty:

If the user chose not to close, we call ignore() on the close event. This will tell PyQt to simply discard the close event and to leave the application running.

If we are using Qt 4.3 (e.g., with PyQt 4.3) and have restored the main window's geometry using QWidget.restoreGeometry(), we can save the geometry like this:

settings.setValue("Geometry", QVariant(self.saveGeometry()))

reply = QMessageBox.question(self,

"Image Changer - Unsaved Changes", "Save unsaved changes?", QMessageBox.Yes|QMessageBox.No| QMessageBox.Cancel) if reply == QMessageBox.Cancel:

return False elif reply == QMessageBox.Yes: self.fileSave() return True

This method is used by the closeEvent(), and by the "file new" and "file open" actions. If the image is "dirty", that is, if it has unsaved changes, we pop up a message box and ask the user what they want to do. If they click Yes, we save the image to disk and return True. If they click No, we simply return True, so the unsaved changes will be lost. If they click Cancel, we return False, which means that the unsaved changes are not saved, but the current image will remain current, so it could be saved later.

All the examples in the book use yes/no or yes/no/cancel message boxes to give the user the opportunity to save unsaved changes. An alternative favored by some developers is to use Save and Discard buttons (using the QMessageBox.Save and QMessageBox.Discard button specifiers), instead.

The recently used files list is part of the application's state that must not only be saved and restored when the application is terminated and executed, but also kept current at runtime. Earlier we connected the fileMenu's aboutToShow() signal to a custom updateFileMenu() slot. So, when the user presses Alt+F or clicks the File menu, this slot is called before the File menu is shown.

def updateFileMenu(self): self.fileMenu.clear()

self.addActions(self.fileMenu, self.fileMenuActions[:-1]) current = QString(self.filename) \

if self.filename is not None else None recentFiles = [] for fname in self.recentFiles:

if fname != current and QFile.exists(fname): recentFiles.append(fname) if recentFiles:

self.fileMenu.addSeparator()

for i, fname in enumerate(recentFiles):

action = QAction(QIcon(":/icon.png"), "&%d %s" % ( i + 1, QFileInfo(fname).fileName()), self) action.setData(QVariant(fname)) self.connect(action, SIGNAL("triggered()"), self.loadFile) self.fileMenu.addAction(action)

The Static QMessageBox Methods

The QMessageBox class offers several static convenience methods that pop up a modal dialog with a suitable icon and buttons. They are useful for offering users dialogs that have a single OK button, or Yes and No buttons, and similar.

The most commonly used QMessageBox static methods are critical(), infor-mation(), question(), and warning(). The methods take a parent widget (over which they center themselves), window title text, message text (which can be plain text or HTML), and zero or more button specifications. If no buttons are specified, a single OK button is provided.

The buttons can be specified using constants, or we can provide our own text. In Qt 4.0 and Qt 4.1, it was very common to bitwise or QMessageBox.Default with OK or Yes buttons—this means the button will be pressed if the user presses Enter, and to bitwise or QMessageBox.Escape with the Cancel or No buttons, which will then be pressed if the user presses Esc. For example:

reply = QMessageBox.question(self,

"Image Changer - Unsaved Changes", "Save unsaved changes?",

QMessageBox.Yes|QMessageBox.Default,

QMessageBox.No|QMessageBox.Escape)

The methods return the constant of the button that was pressed.

From Qt 4.2, the QMessageBox API has been simplified so that instead of specifying buttons and using bitwise ors, we can just use buttons. For example, for a yes/no/cancel dialog we could write:

reply = QMessageBox.question(self,

"Image Changer - Unsaved Changes", "Save unsaved changes?", QMessageBox.Yes|QMessageBox.No|QMessageBox.Cancel)

In this case, PyQt will automatically make the Yes (accept) button the default button, activated by the user pressing Enter, and the Cancel (reject) button the escape button, activated by the user pressing Esc. The QMessageBox methods also make sure that the buttons are shown in the correct order for the platform. We use the Qt 4.2 syntax for the examples in this book.

The message box is closed by the user clicking the "accept" button (often Yes or OK) or the "reject" button (often No or Cancel). The user can also, in effect, press the "reject" button by clicking the window's close button, X, or by pressing Esc.

If we want to create a customized message box—for example, using custom button texts and a custom icon—we can create a QMessageBox instance. We can then use methods such as QMessageBox.addButton() and QMessage-Box.setIcon(), and pop up the message box by calling QMessageBox.exec_().

self.fileMenu.addSeparator() self.fileMenu.addAction(self.fileMenuActions[-1])

We begin by clearing all the File menu's actions. Then we add back the original list of file menu actions, such as "file new" and "file open", but excluding the last one, "file quit". Then we iterate over the recently used files list, creating a local list which only contains files that still exist in the filesystem, and excluding the current file. Although it does not seem to make much sense, many applications include the current file, often showing it first in the list.

Now, if there are any recently used files in our local list we add a separator to the menu and then create an action for each one with text that just contains the filename (without the path), preceded by a numbered accelerator: 1, 2, • •, 9. PyQt's QFileInfo class provides information on files similar to some of the functions offered by Python's os module. The QFileInfo.fileName() method is equivalent to os.path.basename(). For each action, we also store an item of "user data"—in this case, the file's full name, including its path. Finally, we connect each recently used filename's action's triggered() signal to the loadFile() slot, and add the action to the menu. (We cover loadFile() in the next section.) At the end, we add another separator, and the File menu's last action, "file quit".

But how is the recently used files list created and maintained? We saw in the form's initializer that we initially populate the recentFiles string list from the application's settings. We have also seen that the list is correspondingly saved in the closeEvent(). New files are added to the list using addRecentFile().

def addRecentFile(self, fname): if fname is None: return if not self.recentFiles.contains(fname): self.recentFiles.prepend(QString(fname)) while self.recentFiles.count() > 9: self.recentFiles.takeLast()

This method prepends the given filename, and then pops off any excess files from the end (the ones added longest ago) so that we never have more than nine filenames in our list. We keep the recentFiles variable as a QStringList, which is why we have used QStringList methods rather than Python list methods on it.

The addRecentFile() method itself is called inside the fileNew(), fileSaveAs(), and loadFile() methods; and indirectly from loadInitialFile(), fileOpen(), and updateFileMenu(), all of which either call or connect to loadFile(). So, when we save an image for the first time, or under a new name, or create a new image, or open an existing image, the filename is added to the recently used files list. However, the newly added filename will not appear in the File menu, unless we subsequently create or open another image, since our updateFileMenu() method does not display the current image's filename in the recently used files list.

J Edit Help

^ New,,,

Ctrl+N

j. Open...

Ctrl+O

1 5a ve

Ctrl+S

j Save As...

Print

Ctrl+P

lChristrnas_Island, png

2 USA.png

3 iss013-e-14802.jpg

4 gimp.png

[¡¡J Quit

Ctrl+Q

Figure 6.8 The File menu with some recently used files

Figure 6.8 The File menu with some recently used files

The approach to handling recently used files that we have taken here is just one of many possibilities. An alternative is to create the File menu just once, with a set of actions at the end for recently used files. When the menu is updated, instead of being cleared and re-created, the actions set aside for recently used files are simply hidden or shown, in the latter case having had their filenames updated to reflect the current set of recently used files. From the user's point of view, there is no discernable difference whichever approach we take under the hood, so in either case the File menu will look similar to the one shown in Figure 6.8.

Both approaches can be used to implement recently used files in a File menu, adding the list at the end as we have done in the Image Changer application, just before the Quit option. They can also both be used to implement the Open Recent File menu option that has all the recent files as a submenu, as used by OpenOffice.org and some other applications. The benefits of using a separate Open Recent File option is that the File menu is always the same, and full paths can be shown in the submenu—something we avoid when putting recently used files directly in the File menu so that it doesn't become extremely wide (and therefore, ugly).

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


Responses

  • Tamara
    How to save & restore QDockWidget in registry?
    9 years ago
  • Vappu
    How to save appliction sttingsLpyqt?
    8 years ago

Post a comment