Single Document Interface SDI

For some applications, users want to be able to handle multiple documents. This can usually be achieved simply by running more than one instance of an application, but this can consume a lot of resources. Another disadvantage of using multiple instances is that it is not easy to provide a common Window menu that the user can use to navigate between their various documents.

There are three commonly used solutions to this. One is to use a single main window with a tab widget, and with each tab holding one document. This approach is fashionable for Web browsers, but it can be inconvenient when editing documents since it isn't possible to see two or more documents at once. We will not show this approach since the coverage of tab widgets in this chapter's second section is sufficient, and because you'll have the chance to try it for yourself in the exercise. The other two approaches are SDI, which we will cover in this section, and MDI, which we will cover in the next section.

The key to creating an SDI application is to create a window subclass that handles everything itself, including loading, saving, and cleanup, reducing the application to be essentially a collection of one or more such windows.

We will begin by looking at some extracts from the SDI Text Editor's initializer; the application itself is shown in Figure 9.7.

class MainWindow(QMainWindow):

NextId = 1

Si SDr Text Editor - bill-of-right5.txt

4 Pi

File Edit Window

No person shall be held to answer for a capital, or otherwise infamous crime, unless on a presentmgn^ cases arising in the land or r|

4 Pi

Wi SDI Text Editor - human-rights.txt actual service in time of Wai subject for the same offense limb; nor shall be compelled against himself, nor be depr due process of law; nor sha without just compensation.

File Edit Window

Everyone is entitled to all the rights and freedoms set forth in this Declaration, without distinction of any kind, such as race, colour, sex, language, religion, political or other opinion, national or social origin, property, birth or other status.

Furthermore, no distinction shall be made on the basis of the political, jurisdictional or international status of the country or territory to which a person belongs, whether it be independent, trust, non-self-governing or under any other limitation of sovereignty.

nt of the governed,-That whenever any Form of [*■ Iructive of these ends, it is the Right of the i it, and to institute new Government, laying <rs in such form, id Happiness, ablished should dingly all to suffer, while ig the forms to jses and > a design to is their duty, is for their future Colonies; and such former Systems Britain is a direct object tes. To prove this, 1

Figure 9.7 SDI Text Editor with three documents Instances = set()

def_init_(self, filename=QString(), parent=None):

super(MainWindow, self)._init_(parent)

self.setAttribute(Qt.WA_DeleteOnClose) MainWindow.Instances.add(self)

The NextId static variable is used to provide numbers for new empty windows: "Unnamed-1.txt", "Unnamed-2.txt", and so on.

The application consists of one or more MainWindow instances, each of which must be able to act independently. However, there are three common situations where we need to access all of the instances from inside any one of them. One is to provide a "save all" option, another is to provide a Window menu through which the user can switch between the window instances, and another is to provide a "quit" option that the user can use to terminate the application, and which must implicitly close every window. The Instances static variable is what we use to keep track of all the instances.

When a new window instance is created, we set it to delete itself when closed. This means that windows can be closed directly by the user or indirectly by other instances (when the application is terminated, for example). One implication of using Qt.WA_DeleteOnClose is that the window should take care of saving unsaved changes and cleaning up itself. We also add the window to the static set of window instances so that any window instance can gain access to all the other windows. We will look into all of these matters further on.

self.editor = QTextEdit() self.setCentralWidget(self.editor)

The QTextEdit is the ideal widget for our central widget, with some actions create- being able to be passed directly to it, as we will see in a moment. We will now Action() look at just a few of the actions, skipping the createAction() method that we 175 'sa have seen before.

fileSaveAllAction = self.createAction("Save A&ll", self.fileSaveAll, icon="filesave", tip="Save all the files")

This action is similar to almost all the other file actions, with a connection to one of the MainWindow subclass's methods.

fileCloseAction = self.createAction("&Close", self.close, QKeySequence.Close, "fileclose", "Close this text editor")

The "close" action is similar to those we have seen before. As usual, we do not reimplement the close() method, but instead reimplement the closeEvent() handler so that we can intercept any clean closure of the window. What is different is that this action closes the only current window, not the application (unless this is the application's only window).

fileQuitAction = self.createAction("&Quit", self.fileQuit, "Ctrl+Q", "filequit", "Close the application")

The "quit" action terminates the application, and does so by closing each of the SDI Text Editor's windows, as we will see when we review the file-Quit() method.

editCopyAction = self.createAction("&Copy", self.editor.copy, QKeySequence.Copy, "editcopy", "Copy text to the clipboard")

This action connects to the QTextEdit's relevant slot. The same is true of the "cut" and "paste" actions.

The menus, toolbars, and status bar are all created in ways that we have seen previously, except for the Window menu, which we will look at now.

self.windowMenu = self.menuBar().addMenu("&Window") self.connect(self.windowMenu, SIGNAL("aboutToShow()"), self.updateWindowMenu)

We do not add any actions to the Window menu at all. Instead, we simply connect the menu's aboutToShow() method to our custom updateWindowMenu() method which, as we will see, populates the menu with all the SDI Text Editor windows.

self.connect(self, SIGNAL("destroyed(QObject*)"), MainWindow.updateInstances)

When the user closes a window, thanks to the Qt.WA_DeleteOnClose flag, the window will be deleted. But because we have a reference to the window in the static Instances set, the window cannot be garbage-collected. For this reason we connect the window's destroyed() signal to a slot that updates the Instances by removing any windows that have been closed. We will discuss this in more detail when we look at the updateInstances() method.

Since each window is responsible for a single file, we can have a single filename associated with each window. The filename can be passed to the window's initializer, and it defaults to an empty QString. The last lines of the initializer handle the filename.

self.filename = filename if self.filename.isEmpty():

self.filename = QString("Unnamed-%d.txt" % \

MainWindow.NextId)

MainWindow.NextId += 1 self.editor.document().setModified(False) self.setWindowTitle("SDI Text Editor - %s" % self.filename) else:

self.loadFile()

If the window has no filename, either because the application has just been started or because the user has invoked the "file new" action, we create a suitable window title; otherwise, we load the given file.

The closeEvent(), loadFile(), fileSave(), and fileSaveAs() methods are very similar to ones we have seen before, so we will not describe them here. (They are in the source code in chap09/sditexteditor.pyw, of course.) Instead, we will focus on those things that are special for an SDI application.

def fileNew(self):

MainWindow().show()

When the user invokes the "file new" action, this method is called. Another instance of this class is created, and show() is called on it (so it is shown mode-lessly). At the end of the method, we would expect the window to go out of scope and be destroyed since it does not have a PyQt parent and it is not an instance variable. But inside the main window's initializer, the window adds itself to the static Instances set, so an object reference to the window still exists, and therefore, the window is not destroyed.

def fileOpen(self):

filename = QFileDialog.getOpenFileName(self,

"SDI Text Editor -- Open File") if not filename.isEmpty():

if not self.editor.document().isModified() and \ self.filename.startsWith("Unnamed"): self.filename = filename self.loadFile() else:

MainWindow(filename).show()

This method is slightly different from similar ones we have seen before. If the user gives a filename, and the current document is both unmodified and unnamed (i.e., a new empty document), we load the file into the existing window; otherwise, we create a new window, passing it the filename to load.

def fileSaveAll(self): count = 0

for window in MainWindow.Instances: if isAlive(window) and \

window.editor.document().isModified(): if window.fileSave(): count += 1

self.statusBar().showMessage("Saved %d of %d files" % ( count, len(MainWindow.Instances)), 5000)

As a courtesy to users, we provide a Save All menu option. When it is invoked we iterate over every window in the Instances set, and for each window that is "alive" and modified, we save it.

A window is alive if it has not been deleted. Unfortunately, this is not quite as simple as it seems. There are two lifetimes associated with a QWidget: the lifetime of the Python variable that refers to the widget (in this case, the MainWindow instances in the Instances set), and the lifetime of the underlying PyQt object that is the widget as far as the computer's window system is concerned.

Normally, the lifetimes of a PyQt object and its Python variable are exactly the same. But here they may not be. For example, suppose we started the application and clicked File^New a couple of times so that we had three windows, and then we navigated to one of them and closed it. At this point the window that is closed (thanks to the Qt.WA_DeleteOnClose attribute) will be deleted.

Under the hood, PyQt actually calls the deleteLater() method on the deleted window. This gives the window the chance to finish anything it is in the middle of doing, so that it can be cleanly deleted. This will normally be all over in less than a millisecond, at which point the underlying PyQt object is deleted from memory and no longer exists. But the Python reference in the Instances set will still be in place, only now referring to a PyQt object that has gone. For this reason, we must always check any window in the Instances set for aliveness before accessing it.

def isAlive(qobj): import sip try:

sip.unwrapinstance(qobj) except RuntimeError:

return False return True

The sip module is one of PyQt's supporting modules that we do not normally need to access directly. But in cases where we need to dig a bit deeper, it can be useful. Here, the method tries to access a variable's underlying PyQt object. If the object has been deleted, a RuntimeError exception is raised, in which case we return False; otherwise, the object still exists and we return True.* By performing this check, we ensure that a window that has been closed and deleted is not inadvertently accessed, even if we have not yet deleted the variable that refers to the window.

@staticmethod def updateInstances(qobj):

MainWindow.Instances = set([window for window \

in MainWindow.Instances if isAlive(window)])

Whenever a window is closed (and therefore deleted), it emits a destroyed() signal, which we connected to the updateInstances() method in the initializer. This method overwrites the Instances set with a set that contains only those window instances that are still alive.

So why do we need to check for aliveness when we iterate over the instances—for example, in the fileSaveAll() method—since this method ensures that the Instances set is kept up-to-date and is holding only live windows? The reason is that it is theoretically possible that between the time when a window is closed and the Instances set is updated, the window is iterated over in some other method.

Whenever the user clicks the Window menu in any SDI Text Editor window, a menu listing all the current windows appears. This occurs because the windowMenu's aboutToShow() signal is connected to the updateWindowMenu() slot that populates the menu.

def updateWindowMenu(self): self.windowMenu.clear() for window in MainWindow.Instances: if isAlive(window):

self.windowMenu.addAction(window.windowTitle(), self.raiseWindow)

First any existing menu entries are cleared; there will always be at least one, the current window. Next we iterate over all the window instances and add an

★The isAlive() function is based on Giovanni Bajo's PyQt (then PyKDE) mailing list posting, "How to detect if an object has been deleted". The list is used for both PyQt and PyKDE.

action for any that are alive. The action has text that is simply the window's title (the filename) and a slot—raiseWindow()—to be called when the menu option is invoked by the user.

def raiseWindow(self): action = self.sender() if not isinstance(action, QAction): return for window in MainWindow.Instances: if isAlive(window) and \

window.windowTitle() == action.text(): window.activateWindow() window.raise_() break

This method could be called by any of the Window menu's entries. We begin with a sanity check, and then we iterate over the window instances to see which one has a title whose text matches the action's text. If we find a match, we make the window concerned the "active" window (the application's top-level window that has the keyboard focus), and raise it to be on top of all other windows so that the user can see it.

In the MDI section that follows we will see how to create a more sophisticated Window menu, with accelerators and some additional menu options.

def fileQuit(self):

QApplication.closeAllWindows()

PyQt provides a convenient method for closing all of an application's top-level windows. This method calls close() on all the windows, which in turn causes each window to get a closeEvent(). In this event (not shown), we check to see whether the QTextEdit's text has unsaved changes, and if it has we pop up a message box asking the user if they want to save.

app = QApplication(sys.argv)

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

MainWindow().show()

At the end of the sditexteditor.pyw file, we create a QApplication instance and a single MainWindow instance and then start off the event loop.

Using the SDI approach is very fashionable, but it has some drawbacks. Since each main window has its own menu bar, toolbar, and possibly dock windows, there is more resource overhead than for a single main window.* Also, although it is easy to switch between windows using the Window menu, if we wanted

*On Mac OS X there is only one menu bar, at the top of the screen. It changes to reflect whichever window currently has the focus.

more control over window sizing and positioning, we would have to write the code ourselves. These problems can be solved by using the less fashionable MDI approach that we cover in the next section.

Was this article helpful?

0 0
Tube Traffic Ninja

Tube Traffic Ninja

Discover How You Can Quickly And Easily Dominate Google and YouTube... With Simple Cash Generating Videos. Did you know that YouTube is the second largest search website on the entire Internet? YouTube gets more daily searches than Bing and Yahoo. In fact, there is only one search engine that gets more action.

Get My Free Ebook


Responses

  • Ursula
    How to change between windows pyqt4?
    8 years ago
  • fergus mcgregor
    What are the drawbacks of using single document interface?
    8 years ago
  • tim fink
    How to create a multiple document interface using pyqt?
    3 years ago

Post a comment