Up to this point we have seen only example scripts— Exciting examples, illuminating examples, promising examples, but still just examples. Example scripts are far removed from the realities of a complex GUI application. For a complex application you need a well thought-out modular structure, where each component can find its place. You need an architecture, and you need a design.
Most books on programming languages don't progress much beyond basic examples; indeed, it is not really possible to discuss a complete, complex application. Still, in this part of the book I want to show how BlackAdder and PyQt can help you achieve a well-written, maintainable application, starting with the architecture, and then moving on to the outlines of an application. On the way, I'll show you one useful way of laying out the project structure. In the next few chapters we'll build this framework into a real application.
Let's start with the basics. Applications are, essentially, interfaces that manipulate data. Whether you are handling records from a database, an object tree from a parsed HTML page, a game world, or a stream from a network socket, there is always a reality that is mediated to the user with an interface.
From this, it follows naturally that it would be a good idea to separate bits of software that handle the data from the interface. After all, you might want to manipulate the same data with a different interface. In some development environments this is quite difficult to achieve. Visual Basic, for instance, almost mandates that you entangle your data-mangling code with your GUI code. On the other side of the scale, SmallTalk has explicit support for the most extended form of the celebrated Observer pattern — with the Model/View/Controller framework for the SmallTalk user interface (or, in later versions, the Model-View-Presenter architecture).
The component that represents the data is variously termed model or document; the component that actually shows the data on screen is the view. The model-view-controller framework adds a controller component, which represents the user input.
The controller component receives mouse clicks, key press events and all other user input, and passes those on to the model. The model determines its current state from that input, and notifies the view that its representation of the model should be changed. Sounds like PyQt signals and slots would come in handy, doesn't it?
Model-view-controller architecture
Be aware of the ‘fractal' nature of this architecture. You can envision your entire application divided into two or three parts — one component for the model, one for the view, and perhaps one for the controller. However, the same tripartition can be designed for the lowliest checkbox class. Here, the boolean value is the model, the picture of a box with a cross in it is the view, and the event handler is the controller.
Swing, the Java gui toolkit, does exactly this, and gives you the opportunity to write specialized models and controllers for almost all its widgets (and specialized views, too). PyQt doesn't go quite that far, and its widgets are based on a simpler, more monolithic model. Like all good ideas carried through to their extremes, writing models and controllers for every widget is a bit tiresome. That's why Java's Swing also presents capable default implementations for the controller and model parts.
This chapter is about application architecture, and when speaking of views and models, documents and controllers, I do so only at the application architecture level, not the widget level. However, a complex application could consist of several models and views: for instance, in an application based on a database, you could view every table as a model and every corresponding form as a view.
The most basic architecture in which application model and interface are separated is the document-view paradigm. Here, you have two basic modules: one representing your data (the document), and one representing the interface that shows the data to the user (the view). This architecture is prevalent in the Windows world: the entire Microsoft MFC library is based on its principles, and it is also popular in the Unix world, where many KDE applications are based on it.
The document-view architecture
There must be an interface between the document and the view. Changes made in the view must be passed on to the document, and vice-versa. A simple document-view framework is readily constructed:
The basic application structure consists of three classes: an application class, a view class and a document class. In the next few chapters of this part, we'll work with the framework to build a real application. We'll also extend it to handle multiple document windows: the framework detailed below can only work with one document. The complete framework is in the file docview.py.
Example 12-1. A simple document-view framework
class DocviewDoc(QObject): def __init__(self, *args): apply(QObject.__init__, (self,)+args) self.modified=FALSE def slotModify(self): self.modified = not self.modified self.emit(PYSIGNAL("sigDocModified"), (self.modified,)) def isModified(self): return self.modified
You should always begin with designing the application model - or so the theory goes. Your preferences might lie with first creating a mock-up of the interface using generic widgets, in order to be able to have something concrete to talk about. That's fine with me. Anyway, the DocviewDoc class represents the document or the application model. This can be as complex as you want. This class merely remembers whether it has been modified. The controlling application can query the document using the isModified() function to determine whether the document has changed, and it can hook a QAction to the slotModify() slot to signal user interaction to the model. Separating all code that handles the application data makes it easy to write automated tests using Pyunit. This is the topic of the next chapter.
DocviewView is the view class in the framework. A view is a visual component; in PyQt it must somehow descend from QWidget — either directly, as it is done here, or via a more specialized class, such as QTable or QCanvas. A reference to the application model is passed to the view. This breaks encapsulation somewhat, but it makes initially setting up the display a lot easier.
Warning |
I mentioned earlier, in the Section called QColor in Chapter 10, that the nice people at Trolltech changed the name of the function that is used to set background colors from setBackgroundColor to setEraseColor. This means of course that you, if you want to run this example with PyQt 3, will have to adapt the relevant calls. |
class DocviewView(QWidget): def __init__(self, doc, *args): apply(QWidget.__init__, (self, ) + args) self.doc = doc self.connect(self.doc, PYSIGNAL("sigDocModified"), self.slotDocModified) self.slotDocModified(self.doc.isModified()) def slotDocModified(self, value): if value: self.setBackgroundColor(QColor("red")) else: self.setBackgroundColor(QColor("green"))
The document has to notify the view of changes. This means that the view has to have slots corresponding to all the document signals the view is interested in. A view can thus show changes to the document selectively, and you can create more than one view, each with a specialized function.
The DocviewApp is the controller component. It controls both view and document.
class DocviewApp(QMainWindow): def __init__(self, *args): apply(QMainWindow.__init__,(self, ) + args) self.initActions() self.initMenuBar() self.initToolBar() self.initStatusBar() self.initDoc() self.initView()
The controller keeps a dictionary of actions, making it easier to refer to those actions when populating the menu and toolbars. The dictionary can also be used to export functionality for a macro language, by calling the QAction.activated() slot, which is connected to the relevant slots in the controller. The pixmap is in the form of an inline XPM image, which is not shown here.
def initActions(self): fileQuitIcon=QIconSet(QPixmap(filequit)) self.actions = {} self.actions["fileQuit"] = QAction("Exit", fileQuitIcon, "E&xit", QAccel.stringToKey("CTRL+Q"), self) self.connect(self.actions["fileQuit"], SIGNAL("activated()"), self.slotFileQuit) self.actions["editDoc"] = QAction("Edit", fileQuitIcon, "&Edit", QAccel.stringToKey("CTRL+E"), self) self.connect(self.actions["editDoc"], SIGNAL("activated()"), self.slotEditDoc)
Populating toolbars, menubars and statusbars are always a bit tedious. When BlackAdder is integrated with Qt 3.0, it will be possible to design not only dialogs and widgets, but also menu's and toolbars using a very comfortable action editor. I will discuss the various aspects of creating toolbars and menubars later in Chapter 13.
def initMenuBar(self): self.fileMenu = QPopupMenu() self.actions["fileQuit"].addTo(self.fileMenu) self.menuBar().insertItem("&File", self.fileMenu) self.editMenu = QPopupMenu() self.actions["editDoc"].addTo(self.editMenu) self.menuBar().insertItem("&Edit", self.editMenu) def initToolBar(self): self.fileToolbar = QToolBar(self, "file operations") self.actions["fileQuit"].addTo(self.fileToolbar) QWhatsThis.whatsThisButton(self.fileToolbar) def initStatusBar(self): self.statusBar().message("Ready...")
Here the document, or application model, is initialized.
def initDoc(self): self.doc=DocviewDoc()
The view is created after the document, and then made into the central application widget.
def initView(self): self.view = DocviewView( self.doc, self) self.setCentralWidget(self.view)
This function is called in the slotFileQuit() slot when the document has been modified. Note that we're using a class function, information, from QMessageBox. By passing an empty string after the button labels for "Ok" and "Cancel", the messagebox is created with only two buttons, instead of three.
def queryExit(self): exit = QMessageBox.information(self, "Quit...", "Do you really want to quit?", "&Ok", "&Cancel", "", 0, 1) if exit==0: return TRUE else: return FALSE
The slot functions are called whenever one of the QActions is activated(). Note how the statusbar message is set, before calling the document functions directly.
# # Slot implementations # def slotFileQuit(self): self.statusBar().message("Exiting application...") if self.doc.isModified(): if self.queryExit(): qApp.quit() else: qApp.quit() self.statusBar().message("Ready...") def slotEditDoc(self): self.doc.slotModify() def main(args): app=QApplication(args) docview = DocviewApp() app.setMainWidget(docview) docview.show() app.exec_loop() if __name__=="__main__": main(sys.argv)
This is the stub that starts the application. In contrast with the examples from Part I, such as hello5.py, this framework doesn't check if all windows are closed with:
app.connect(app, SIGNAL("lastWindowClosed()") , app, SLOT("quit()"))
This is because the framework supports only one window, and quitting the app is integrated in the DocviewApp class.
Now the startup bit is done, we can see what docview.py produces when it is run:
A very simple document-view framework application
This framework only supports one window with one view and one document. Another omission is that there is no interaction between view and document. Usually, you will also allow the view component to receive user actions, like mouse clicks. These mostly arrive in the form of events. You can handle these in various ways. The first is to directly call the relevant slot functions in the document. Try adding the following method to the DocviewView class:
def mouseDoubleClickEvent(self, ev): self.doc.slotModify()
This bypasses the controlling application (DocviewApp) and leads to an uncomfortably tight coupling between view and document. Another way to notify the document of the double-click is to let the view emit a signal, which can be caught by the application object and connected to the document slot. Replace the previous function with the following function in the DocviewView class instead:
def mouseDoubleClickEvent(self, ev): self.emit(PYSIGNAL("sigViewDoubleClick"),())
And to the DocviewApp:
def initView(self): self.view = DocviewView( self.doc, self) self.setCentralWidget(self.view) self.connect(self.view, PYSIGNAL("sigViewDoubleClick"), self.slotEditDoc)
As you can see, you can either call the document directly from the view, or via the application controller. The approach you choose depends on the complexity of your application. In the rest of this part we will extend this simple framework to include MDI (multiple document interface) and MTI (multiple top-level windows interface) applications.