GUI Programming with Python: QT Edition | ||
---|---|---|
Prev | Chapter 15. A More Complex Framework: Multiple Documents, Multiple Views | Next |
Let's first take stock of the requirements for a document/view manager, and write a little testcase.The document/view manager will have to take care of the mapping between documents and views. Every document can have more than one view, but a view can only show one document.
The document/view manager must be able to create new documents, together with new views.
The document/view manager will have to make sure the document gets closed when the last view is closed.
The document/view manager must be able to create new views for existing documents.
The document/view manager must not be forced to know about exact document or view classes, but work against a standard set of methods, i.e., an interface.
A guideline during implementation is that the document/view manager should not make GUI calls directly. There are two reasons for this: it is easier to write good testcases if there is no GUI involved, and denying the document/view manager access to the GUI forces us to place all the actual GUI code in one place, namely the application or controller object.
Here's the testcase:
Example 15-1. A testcase for a document manager
import unittest from docmanager import * from qt import * class TestViewManager(QObject): def activeWindow(self): return None def width(self): return 100 def height(self): return 100 class TestParent(QObject): def queryCloseDocument(self, document): return QMessageBox.Yes def querySaveDocument(self, document): return QMessageBox.No def queryDiscardDocument(self, document): return QMessageBox.Yes def queryFileName (self, document =None): return "fileName" class TestDocument(QObject): def modified(self): return TRUE def save(self): pass def close(self): pass def title(self): return "title" def pathName(self): return "pathname" def setPathName(self, pathname): pass class TestView(QObject): def __init__(self, parent, document, *args): QObject.__init__(self, parent) self._document = document def show(self): pass def showMaximized(self): pass def setCaption(self, caption): pass def resize(self, x, y): pass def close(self, destroy): return TRUE
The purpose of this testcase is to test the documentmanager. An interesting side effect is that the development of the testcase necessitates the development of fake versions of the other necessary components. Creating these fake components for the view, document and application makes clear which functions they must support.
The TestViewManager class is an interesting object. It will manage the different views (windows, tabs, or splitpanes) for the application. As such, it will become the visual counterpart of the DocManager class.
The TestParent represents the application itself — that is, the central class that manages all QActions, menus, toolbars and so on. As you can see, we need four methods in the application, to ask the user whether she wants to close, save or discard the document, and what the filename should be. By not calling QMessageBox or QFileDialog directly, we again get a stronger separation of GUI and application logic. But life is messy, and a complete separation is not attainable.
This is most apparent in the TestView class. Here we need to create stubs for a number of functions that are part of QWidget, such as setCaption().
The TestDocument class also shows a clear interface: but more than that, it is also clearly meant for file-oriented applications. A database application would in all likelihood not concern itself with obscurities like pathnames. On the other hand, with a database application it is even more important to allow more than one view on more than one document at a time — if we simply equate document with query.
class DocManagerTestCase(unittest.TestCase): def setUp(self): self.parent = TestParent() self.viewManager = TestViewManager() def checkInstantiate(self): try: docManager = DocManager(self.parent, self.viewManager) except Exception, e: self.fail("Could not instantiate docmanager: " + str(e)) def checkCreateDocument(self): docManager = DocManager(self.parent, self.viewManager) numberOfDocs = docManager.numberOfDocuments() + 1 numberOfViews = docManager.numberOfViews() + 1 try: document = docManager.createDocument(TestDocument, TestView) except Exception, e: self.fail("Could not add a new document: " + str(e)) assert document, "No document created" assert numberOfDocs == docManager.numberOfDocuments(),\ "No document added" assert numberOfViews == docManager.numberOfViews(), \ "No view added" assert docManager.views(document),\ "Document does not have a view" def checkAddView(self): docManager = DocManager(self.parent, self.viewManager) document = docManager.createDocument(TestDocument, TestView) numberOfDocs = docManager.numberOfDocuments() numberOfViews = docManager.numberOfViews() + 1 numberOfDocViews = len(docManager.views(document)) +1 try: view = docManager.addView(document, TestView) except DocManagerError, e: self.fail(e) except Exception, e: self.fail("Could not add a view to a document " + str(e)) assert view is not None,\ "No view created" assert numberOfDocs == docManager.numberOfDocuments(),\ "Document added" assert numberOfViews == docManager.numberOfViews(), \ "No view added" assert numberOfDocViews == len(docManager.views(document)), \ "No view added to document" view = None document = TestDocument() try: view = docManager.addView(document, TestView) fail("Should not have been able to add a view " + "to an unmanaged document") except DocManagerError, e: pass assert view == None,\ "View created" def checkCloseView(self): docManager = DocManager(self.parent, self.viewManager) document = docManager.createDocument(TestDocument, TestView) view = docManager.addView(document, TestView) numberOfViews = docManager.numberOfViews() docManager.closeView(view) assert numberOfViews > docManager.numberOfViews(), \ "No view removed: was %i, is %i" % (docManager.numberOfViews(), numberOfViews) def doNotCheckCloseDocument(self): docManager = DocManager(self.parent, self.viewManager) document = docManager.createDocument(TestDocument, TestView) docManager.closeDocument(document) assert docManager.numberOfDocuments() == 0,\ "docManager still manages a document" def suite(): testSuite=unittest.makeSuite(DocManagerTestCase, "check") return testSuite def main(): runner = unittest.TextTestRunner() runner.run(suite()) if __name__=="__main__": main()
A look at the testcases shows how the documentmanager is intended to be used. When a document is created, one view is automatically created. More views can be added to a document. Views can be removed, and when the last view is removed, the document is closed. Creating documents and views is the job of the documentmanager; this is why we pass the classes of the view and document to the manager, and not to complete objects.
As I said, life is messy, and if you look at the last test, you will see one bit of unavoidable mess. During the implementation of the document manager it became clear that in order to ‘catch' events (such as closing the application or window with the close button in the title bar) it was necessary to install an event filter in every view. This meant that the original implementation of closeDocument(), which called closeView(), had to be changed to one where closeDocument() called view.close() — which fires the event filter, which fires the closeView(). This, however, is only possible if you use actual QWidget-derived objects; it cannot be done with the fakes we created for the test. This means that the checkCloseDocument() needs to be renamed doNotCheckCloseDocument()(It is a convention to prefix tests that don't work with doNot)—the test will never work.