Chapter 16. User Interface Paradigms

Table of Contents
Tabbed documents
Back to the MDI windows
A row of split windows
A stack of documents
A more complex view management solution
Conclusion

In Chapter 15, we created a general framework to handle the complexities of applications that have more than one document open at the same time — with possibly more than one view on the same document, too. We also discussed the various paradigms for representing those views in the application.

In this chapter, we will explore the actual implementation of some of those paradigms, starting with one of the most useful and modern paradigms: the tabbed document model.

Tabbed documents

Like most user interface paradigms, the tabbed document paradigm has been popularized by current integrated development environments. A tabbed document collects all open documents in one window, with a row of tabs to facilitate easy navigation of documents. This paradigm has become so prevalent that even the old stalwart of user interface conservatism, XEmacs, supports it.

It turns out to be remarkably easy to implement a tabbed document interface. First, let's determine what we want to get out of this component. It is the first of several generic components that can take views — i.e. QWidget's— and show them in an application workspace. All view managers should have the same API. That allows the user to choose his favorite way of working without giving us lots of work — because, from the point of view of the application, all view managers are exactly the same.

We will provisionally call the component that manages tabbed views TabManager. The TabManager is meant to be almost a drop-in replacement for the QWorkspace we used in the Chapter 15. Therefore, it should support most of the same functionality: adding, removing and listing actual views. Other capabilities of QWorkspace don't make sense: you cannot tile or cascade tabbed windows. There must be some way to indicate to the wrapping application whether the view manager supports these capabilities.

PyQt offers a QTabWidget, which fits the basics of our needs perfectly. However, in contrast with the QWorkspace, where merely creating a widget with the workspace as parent widget was enough to let it be managed, QTabWidget wants us to explicitly add pages, and thus widgets, to its list of tabs. Finally, it also allows the addition and removal of pages. We can also request a reference to the active view, and ask to be notified of page changes.

QTabWidget is used in the QTabDialog dialog window class, and makes use of QWidgetStack and QTabBar. QWidgetStack keeps a stack of widgets of which only one is shown at a time. QTabBar, which keeps a row of tabs. Tabs can be square or triangular (the latter is seldom seen nowadays, for it is very ugly), and shown on the top or bottom of the window.

Applications that handle documents that consist of several (but not many) pages often show a row of triangular tabs at the bottom of the window. You cannot set the tabs to appear at the side of the window. That's a pity, since it is a position that is quite often preferred by users.

Let us take a look at the implementation of a tabbed document manager:

"""
tabmanager.py - tabbed document manager for the mdi framework

copyright: (C) 2001, Boudewijn Rempt
email:     boud@rempt.xs4all.nl
"""
from qt import *
from resources import TRUE, FALSE

class TabManager(QTabWidget):

    def __init__(self, *args):
        apply(QTabWidget.__init__,(self, ) + args)
        self.views=[]
        self.setMargin(10)
    

The TabManager is derived from QTabWidget. A simple python list of views is kept, otherwise we would not be able to retrieve a list of all open views for the ‘windows' menu. The margin between tab and document should really be a user-settable property, but we won't develop a user preferences framework until chapter Chapter 18.

    def addView(self, view):
        if view not in self.views:
            self.views.append(view)
            self.addTab(view, view.caption())
            self.showPage(view)
    

Adding a new view is a simple exercise. However, note that until you actually call showPage() on your view, the QTabWidget appears to be innocent of your addition, and won't manage the layout of the page. This means that when you create a new window and resize the application window, the contents won't resize with it. Simply drawing the tab widget's attention to the page will suffice, however.

With PyQt's QWorkspace it was enough to create a widget with the workspace as its parent—the widget was automatically managed shown. This is no longer enough when we use QTabWidget. This means that we will have to adapt the DocManager class to work with addView. This is done in the private _createView() function:

    def _createView(self, document, viewClass):
        view = viewClass(self._viewManager,
                         document,
                         None,
                         QWidget.WDestructiveClose)
        if self._docToViewMap.has_key(document):
            index = len(self._docToViewMap[document]) + 1
        else:
            index = 1
        view.setCaption(document.title() + " %s" % index)

        self._viewManager.addView(view)

        view.installEventFilter(self._parent)

        if self._viewToDocMap == {}:
            view.showMaximized()
        else:
            view.show()

        return view
    

To return to the TabManager class:

    def removeView(self, view):
        if view in self.views:
            self.views.remove(view)
            self.removePage(view)

    def activeWindow(self):
        return self.currentPage()

    def windowList(self):
        return self.views
    

The first of these three functions is new. Simply closing a widget was enough to remove it when it was managed by the QWorkspace object; now we must explicitly remove it. This, too, demands a change in the DocManager class, but fortunately, it's a simple change:

    def _removeView(self, view, document):
        try:
            self._docToViewMap[document].remove(view)
            self._viewManager.removeView(view)
            del self._viewToDocMap[view]
        except ValueError, e:
            pass # apparently already deleted
    

Both activeWindow() and windowList have been included to make the interface of the tabmanager more similar to that of QWorkspace. If you want to have transparently interchangeable components, they must have the same functions.

    def cascade(self): pass

    def tile(self): pass

    def canCascade(self):
        return FALSE

    def canTile(self):
        return FALSE
    

You cannot cascade nor tile a set of tab pages. The functions are included, but merely to avoid runtime exceptions when the application inadvertently does try to call them. The functions canCascade() and canTile() can be used to determine whether this component supports this functionality.