Getting Started with dip.ui

In this section we work through a number of examples of building simple user interfaces using the dip.ui module. First of all we will briefly introduce some terms and concepts that we will expand on later.

dip.ui uses the well known model-view-controller design pattern. The data being displayed by the user interface is held in a model, the user interface is implemented as a view, and the controller updates the model as the user interacts with the view.

A model is implemented either as a Python dictionary or, more usually, as a sub-class of Model which is part of the dip.model module.

A view is implemented as a PyQt GUI, i.e. a hierarchy of QWidget and QLayout instances. A view can be created programmatically, using Qt Designer, or declaratively using the view factories provided by dip.ui.

The process of associating a view with a model is called binding.

A view that is bound to a particular attribute of a model is an editor.

The controller decides if and when an editor updates the model attribute it is bound to.

A toolkit is used, usually by declarative view factories, to do the work of creating specific widgets when required.

The Smallest Example

The following is a complete example (which you can download from here).

import sys

from PyQt4.QtGui import QApplication

from dip.ui import Form
from dip.ui.toolkits.qt import QtToolkit


# Every PyQt GUI application needs a QApplication.
app = QApplication(sys.argv)

# We need a toolkit to create the widgets.
toolkit = QtToolkit()

# Create the model.
model = dict(name='')

# Define the view.
view = Form()

# Create an instance of the view bound to the model.
ui = view(model, toolkit)

# Make the instance of the view visible.
ui.show()

# Enter the event loop.
app.exec_()

# Show the value of the model.
print("Name:", model['name'])

If you run the example from a command line prompt then the following is displayed.

_images/simple.png

If you enter some text and click on the close button then the value you entered will be printed.

We will now walk through the code a section at a time.

import sys

from PyQt4.QtGui import QApplication

from dip.ui import Form
from dip.ui.toolkits.qt import QtToolkit

The above lines are the imports needed by the example.

app = QApplication(sys.argv)

The above line creates the QApplication instance needed by every PyQt GUI application.

toolkit = QtToolkit()

dip uses a toolkit to actually create widgets and layouts. The above line creates an instance of the toolkit included with dip that creates standard Qt widgets and layouts. Alternative toolkits may be provided, one using KDE would be an obvious example.

model = dict(name='')

The above line creates the model. In this simple case we are using a Python dictionary to implement the model. The model has a single attribute, called name and its initial value is an empty string.

view = Form()

The above line defines the view (or, more accurately, the view factory). Using the Qt toolkit, Form will create a QFormLayout to layout its contents as a form (i.e. fields with associated labels) according to the user interface guidelines of the current style.

ui = view(model, toolkit)

The above line calls the view factory to create the actual implementation of the view and bind it to the model. A view factory will always make sure that a QWidget instance is created.

ui.show()

The above line is the standard PyQt call that makes a user interface visible to the user. It is important to realise that instances of views are ordinary PyQt layouts and widgets and can be used as such in applications. There is no need to implement complete user interfaces using the dip.ui module. An application may use a mixture of handwritten code, designs created with Qt Designer and view factories defined declaratively.

app.exec_()

The above line is the standard PyQt call that enters the application’s event loop. The call will return when the user clicks on the close button.

print("Name:", model['name'])

The above line displays the updated value of the model.

In this example we have created a usable user interface while specifying the absolute minimum amount of information about the model and view. In particular the view has inferred what it should contain by looking at the model. This is very powerful, but you will usually want to exert some control over the view in a real-life situation. The following examples describe how to do this.

Extending the Model

In this example we will build on the previous one to add an age attribute to the model.

The complete example can be downloaded from here and is displayed as shown below.

_images/extended_model.png

We will now walk through the significant changes to the previous example.

model = dict(name='', age=0)

The above line shows the new model definition. Strictly speaking that’s all we need to do. The view will infer from the model that a new editor is needed to handle the additional attribute and that editor should be able to handle integers. However, there is a problem. The model is unordered so there is no way for the view to infer the correct order in which the editors should be displayed.

view = Form('name', 'age')

The above line is the new view definition where we have told the view what we want to display and in what order. If the model contained other attributes then they would be ignored by the view.

print("Age:", model['age'])

The above line simply displays the extra attribute.

Configuring Views

As has been shown in the previous examples, a view (in actual fact the toolkit) will decide which editor to create for an attribute according to the type of that attribute. If we want to configure a view or an editor, or we wish the view to use a different editor, then we need to specify it explicitly.

In this example we want to configure the editor used to handle the age attribute so that it has a years suffix as shown below.

_images/configure_views.png

The complete example can be downloaded from here. We will now walk through the changes to the previous example.

from dip.ui import Form, SpinBox

The above line now also imports the SpinBox view factory. As the view that the factory creates is actually an editor (because it can be bound to a particular attribute of a model) then we also sometimes refer to it as an editor factory.

view = Form('name', SpinBox('age', suffix=" years"))

The above line shows the new view definition. SpinBox is the default editor factory for integer attributes and creates a spin box widget.

In this case, instead of just giving the name of the age attribute and leaving it to the view to use the default editor factory, we tell it to use a particular editor factory configured the way we want it. Any arguments to an editor factory that aren’t known to the factory are assumed to be the names and values of Qt properties to be applied to the actual editor that is created. In this case the factory doesn’t use the suffix argument itself and so passes it on to the editor when it creates it.

Using a Dialog

In the previous examples the model has always been updated as the user interacts with the view. This isn’t always desirable. For example, you may not want a model to be updated by a dialog view unless the user clicks the Ok button.

Whether or not a model is updated immediately is determined by the controller which manages the interaction between the view and the model. You may provide a controller to implement a particular behaviour, otherwise a controller will be created automatically as required. Controllers are covered in more detail in a later section.

The default controller that is created by a dialog will not automatically update the model when the user changes an editor.

In this section we modify the example, shown below, to use a dialog and to show that the model is only updated when the Ok button is clicked.

_images/dialog.png

The complete example can be downloaded from here. We will now walk through the changes to the previous example.

from dip.ui import Dialog, SpinBox

The above lines import the additional objects used by the example.

view = Dialog('name', SpinBox('age', suffix=" years"))

The above line shows the new view definition based on a dialog rather than a form. Note that the contents of the view have been arranged (under the covers) using a Form. This is the default when the dialog has more than one sub-view.

ui.exec_()

The above line make a standard PyQt call to display the dialog and enter its event loop. If the user clicks the Ok button then the model is updated from the dialog.

Creating Views Programmatically

So far we have created views by calling a view factory that has been defined declaratively. The view factory will also, under the covers, bind the view to a model. This is the most concise way to create a user interface that will automatically update the model, and also will itself be automatically updated when the model is changed. However there are several reasons why you might want to create a view programmatically, i.e. by making normal PyQt calls:

  • to re-use an existing user interface
  • to incorporate widgets that don’t have a corresponding view factory
  • to configure a widget which cannot be done by setting the widget properties
  • you just prefer the traditional programming style.

Even though we create the view programmatically we can still use the view with other dip modules as if it has been created declaratively. For example the view can be automated and tested using the dip.automate module.

If we don’t use view factories to create the view we need to explicitly bind the view to a model. To do this we define the bindings as an instance of the Bindings class. We then call the instance’s bind() method to bind the particular view and model.

We will now look at a version of an earlier example that creates the view programmatically. The complete example can be dowloaded from here.

In the earlier example the view was defined with the following line of code.

view = Form('name', 'age')

The view was then created and bound to the model with the following line of code.

ui = view(model, toolkit)

In the new example we create the view with the following code. This should be very familiar to a PyQt developer. The key thing to note is that the QLineEdit and QSpinBox widgets have each had their objectName property set. This is used to identify the widget when binding it to a particular attribute of the model.

layout = QFormLayout()
layout.addRow("Name", QLineEdit(objectName='name_editor'))
layout.addRow("Age", QSpinBox(objectName='age_editor'))

ui = QWidget()
ui.setLayout(layout)

Next we define the bindings as shown in the following line of code. The name of each keyword argument corresponds to the objectName of a widget in the view. The value of the keyword argument is the name of the attribute in the model that the widget is to be bound to.

bindings = Bindings(name_editor='name', age_editor='age')

Finally, the following line of code uses the bindings we have defined to bind a particular view to a particular model. Note that we still need to pass a toolkit even though we have already created the view.

bindings.bind(ui, model, toolkit)

In the above we have bound the name of a widget to the name of an attribute. We may also bind the name of a widget to an instance of a view factory. The view factory itself is bound to the name of the attribute in the usual way, i.e. by passing it as the first argument to the factory. This allows us to provide additional information about a widget and to add specific behaviour to it.

For example, say we have a model which contains a list of integer values:

model = dict(values=[])

We create a view containing a QListWidget with a QPushButton below it:

layout = QVBoxLayout()
layout.addWidget(QListWidget(objectName='values_editor'))
layout.addWidget(QPushButton("Add", objectName='add_button'))

ui = QWidget()
ui.setLayout(layout)

We want to add a new entry to the list when the button is pressed.

Now we define the bindings:

bindings = Bindings(
        values_editor=ListWidget(
                'values', column=ListColumn(column_type=Int())),
        add_button=AddButton('values'))

First the QListWidget is bound to the ListWidget view factory. The view factory is itself bound to the values attribute of the model. In addition we have used the ListColumn to specify the type of an element of the list. We have to do this because there is no way to infer it from a dict-based model.

Secondly the QPushButton is bound to the AddButton view factory which is itself linked to the QListWidget. As a result, when the bindings’s bind() method is called dip is able to connect everything up to achieve the desired behaviour.

Creating Views with Qt Designer

You can incorporate views created with Qt Designer by creating them as you would normally do and explicitly binding the relevant widgets to attributes of a model as described in the previous section.

You may also use the Designer view factory to do this for you by giving it the name of the .ui file created using Qt Designer.

For example, say we have a model which contains a single string:

model = dict(name='')

We have a file called name_editor.ui created with Qt Designer that contains a widget that can edit a string and which has an objectName of lineEdit.

We then create a view factory specifiying the name of the .ui file and the bindings that define the relationship between the lineEdit widget and the name attribute of the model:

view = Designer('name_editor.ui', bindings=Bindings(lineEdit='name'))

We then call the view factory and show the user interface it creates as normal:

ui = view(model, toolkit)
ui.show()

Using a Real Model

So far in our examples we have implemented the model using a Python dictionary. Normally models are implemented as sub-classes of Model. Doing so has two particular advantages when using views.

  • A view can automatically use any meta-data provided by an attribute type that correspond to the names of properties that can be applied to an editor.
  • A view will automatically update itself if the value of any of the model’s attributes changes. Model changes can be made programmatically or by the user using another view connected to the same model.

In this section we modify the example, shown below, to demonstrate both of these features.

_images/real_model.png

When running the example you will see that the age is limited to the values defined by the meta-data in the model, and that changes to either view are instantly reflected in the other.

The complete example can be downloaded from here. We will now walk through the changes to the previous example.

from PyQt4.QtGui import QApplication, QHBoxLayout, QWidget

from dip.model import Int, Model, Str

The above lines import the additional objects used by the example.

class ExampleModel(Model):

    # The name.
    name = Str(toolTip="The person's full name")
    
    # The age in years.
    age = Int(minimum=0, maximum=120)

The above lines define the model. Hopefully this is fairly self-explanatory.

Note that all editors will use any statusTip, toolTip or whatsThis text found in a type’s meta-data.

model = ExampleModel()

The above line simply creates an instance of the model.

ui_left = view(model, toolkit)
ui_right = view(model, toolkit)

The above lines define two identical instances of the view. Both views share the same model.

layout = QHBoxLayout()
layout.addWidget(ui_left)
layout.addWidget(ui_right)

ui = QWidget()
ui.setLayout(layout)

The above lines are standard PyQt calls that create a QWidget containing the two views side by side.

print("Name:", model.name)
print("Age:", model.age)

The above lines display the model’s attribute values.

Controllers and Validation

We have already briefly mentioned that a view has a controller that is responsible for the interaction between the view and the model. The specific responsibilities of the controller are:

  • to determine if the data in the view is valid
  • to update the model with valid data from the view
  • to update the view if the model changes
  • to enable and disable the editors in the view, typically in response to changes made by the user to other editors
  • to provide easy programatic access to the editors in the view.

If a controller is not explicitly set for a view then a default controller is automatically created. The default controller considers a view to be valid if all the enabled editors contain valid data. It will update the model when any valid data changes. As we have already mentioned the default controller created for a Dialog will only update the model with valid data when the Ok button is clicked.

You will usually want to create and explicitly set a controller for a view when you need to provide more sophisticated validation of a view, or to disable certain editors when other editors contain particular values.

In this section we will walk through a more complicated example, shown below, of a view that demonstrates the use of validation, help and the creation of an explicit controller.

_images/validate.png

The example has the following features.

  • Tool tip and What’s This help is provided for some editors.
  • The Name editor ensures any entered data matches a regular expression.
  • The Children editor is disabled if the gender is inappropriate.
  • The view is invalid if the values of the Age and Driving license are incompatible.
  • A user friendly explanation of why the view is invalid is provided.
  • The Ok button is only enabled when the view contains valid data.

The following is the complete example (which you can download from here).

import sys

from PyQt4.QtGui import QApplication

from dip.model import Enum, Int, Model, Str
from dip.ui import Controller, Dialog, GroupBox, MessageArea, SpinBox
from dip.ui.toolkits.qt import QtToolkit


class Person(Model):
    """ The Person class encapsulates the data held about an individual. """

    # The person's name.
    name = Str(validator=r'\w[\w\s]*\w',
            toolTip="The person's name",
            whatsThis="The name of the person. It must be at least two "
                    "characters and can include spaces.")

    # The person's gender.
    gender = Enum('female', 'male')

    # The person's age.
    age = Int(1, minimum=1)

    # The number of the children the person has had.  It is only applicable if
    # the person is female.
    children = Int()

    # The person's driving license number.  A person must be 17 or older to
    # hold a driving license.
    driving_license = Str(toolTip="The person's driving license number")


class PersonController(Controller):
    """ The PersonController class implements a controller for a view that
    allows the update of a Person instance.
    """

    def validate_view(self):
        """ Validate the data in the view.

        :return:
            a string which will be empty if the view is valid, otherwise it
            will explain why the view is invalid.
        """

        # The 'children' editor is only enabled if the gender is female.
        self.children_editor.enabled = (self.gender_editor.value == 'female')

        # The super-class will handle the basic editor validation.
        invalid_reason = super(PersonController, self).validate_view()

        if invalid_reason == '':
            if self.driving_license_editor.value != '' and \
                    self.age_editor.value < 17:
                invalid_reason = "A person must be at least 17 years old " \
                        "to hold a driving license."

        return invalid_reason


# Every PyQt GUI application needs a QApplication.
app = QApplication(sys.argv)

# We need a toolkit to create the widgets.
toolkit = QtToolkit()

# Create the model.
model = Person()

# Define the view.
view = Dialog(
        GroupBox(
            'name',
            SpinBox('age', suffix=" years"),
            'gender',
            'children',
            'driving_license',
            title="Person Record"
        ),
        MessageArea()
    )

# Create the controller.
controller = PersonController(auto_update_model=False)

# Create an instance of the view bound to the model.
ui = view(model, toolkit, controller=controller)

# Enter the dialog's modal event loop.
ui.exec_()

# Show the attributes of the model.
print("Name:", model.name)
print("Gender:", model.gender)
print("Age:", model.age)
print("Children:", model.children)
print("Driving license:", model.driving_license)

We will now walk through the significant parts of the code.

class Person(Model):
    """ The Person class encapsulates the data held about an individual. """

    # The person's name.
    name = Str(validator=r'\w[\w\s]*\w',
            toolTip="The person's name",
            whatsThis="The name of the person. It must be at least two "
                    "characters and can include spaces.")

    # The person's gender.
    gender = Enum('female', 'male')

    # The person's age.
    age = Int(1, minimum=1)

    # The number of the children the person has had.  It is only applicable if
    # the person is female.
    children = Int()

    # The person's driving license number.  A person must be 17 or older to
    # hold a driving license.
    driving_license = Str(toolTip="The person's driving license number")

The above code defines the Person model. The main thing to note are the keyword arguments to the different types. These are all meta-data, i.e. they are ignored by the types themselves but are available to other code. In this case they are used by the view we create later on. We could just as easily pass them as property values to the corresponding editor factory, but including them with the model means that they will be used automatically by any view that uses the model. If an editor factory does explicitly specify a property value of the same name then it will be used in preference to that in the model.

The meta-data themselves should be fairly self-explanatory. The validator value, because it is a string, is used by the LineEdit editor factory to create a QRegExpValidator. It may also be a callable that is passed a parent object as its only argument and returns a QValidator.

class PersonController(Controller):
    """ The PersonController class implements a controller for a view that
    allows the update of a Person instance.
    """

    def validate_view(self):
        """ Validate the data in the view.

        :return:
            a string which will be empty if the view is valid, otherwise it
            will explain why the view is invalid.
        """

        # The 'children' editor is only enabled if the gender is female.
        self.children_editor.enabled = (self.gender_editor.value == 'female')

        # The super-class will handle the basic editor validation.
        invalid_reason = super(PersonController, self).validate_view()

        if invalid_reason == '':
            if self.driving_license_editor.value != '' and \
                    self.age_editor.value < 17:
                invalid_reason = "A person must be at least 17 years old " \
                        "to hold a driving license."

        return invalid_reason

The above code is the implementation of the controller. This is a sub-class of dip’s default Controller class that reimplements the validate_view() method. This method is called whenever the user changes an editor or the application changes the model.

A controller automatically provides attributes that correspond to each editor. The name of the attribute is the name of the corresponding attribute in the model with _editor appended.

Using these attributes the following code disables the Children editor if the Gender editor has an inappropriate value.

        self.children_editor.enabled = (self.gender_editor.value == 'female')

Normally a reimplementation of validate_view() will call the base class implementation to handle the validation of the individual editors as shown in the following code.

        invalid_reason = super(PersonController, self).validate_view()

If the individual editors are valid then the controller checks to see that the Age and Driving license editors have compatible values as shown in the following code.

            if self.driving_license_editor.value != '' and \
                    self.age_editor.value < 17:
                invalid_reason = "A person must be at least 17 years old " \
                        "to hold a driving license."

The next significant code to look at is that which defines the view.

view = Dialog(
        GroupBox(
            'name',
            SpinBox('age', suffix=" years"),
            'gender',
            'children',
            'driving_license',
            title="Person Record"
        ),
        MessageArea()
    )

The main point of interest in the above code is the use of MessageArea. If this is specified then the controller uses it to display any messages to the user. It can be configured via its properties. For example a message area defined as follows will show white text on a red background:

MessageArea(styleSheet='background-color: red; color: white')

The following line creates the instance of the controller. By passing the auto_update_model=False argument we ensure that the model is only updated when the Ok button is clicked.

controller = PersonController(auto_update_model=False)

Finally, the following line creates the instance of the view and uses the controller instance we have just created rather than a default one.

ui = view(model, toolkit, controller=controller)

Actions

An action is an abstraction of a user operation that triggers some change in an application. It is implemented in PyQt by the QAction class. An action can be visualised in a GUI as an item in a QMenu or a QToolBar amongst others. An action will often be visualised more than once allowing the user to perform the same operation using, for example, a menu, a tool bar and a context menu. The great advantage of actions is that when their state changes, the widgets being used to visualise them update their appearence automatically.

The basic dip action is defined by the IAction interface. It has a string identifier and can be positioned relative to other actions. It is recommended that a naming convention be adopted for action identifiers.

The ActionCollection class, which implements the IAction interface, is used to define a collection of actions, possibly organised in a hierarchy, which can be turned into an appropriate widget such as a QMenu or QToolBar.

An ActionCollection has an optional title, and a list of members. The title is normally used when the collection is visualised. If there is no title then the collection may be visualised differently.

Each member of a collection may be either an object that implements the IAction interface or a string. A string may be either empty or be the identifier of an action. An empty string is usually interpreted as being some sort of separator when the collection is visualised. If the string is the identifer of an action then it acts as a place holder and will be replaced by the action when the collection is visualised.

The Action class, which also implements the IAction interface, is a convenient wrapper around a QAction so that it can be positioned within an ActionCollection.

More on Toolkits and View Properties

dip uses a toolkit to create all the widgets and layouts that it needs itself. A toolkit also defines the factory used to create the editor for an attribute type. Toolkits are lightweight objects that should not contain state information. Therefore they can be created and destroyed as and when required.

It is important to understand that a toolkit is not a mechanism primarily designed for writing toolkit-independent applications. However, with a little care, it is possible to do so.

A toolkit is not required to create widgets that have a consistent API. For example the QtToolkit included with dip will create an instance of QSpinBox when asked to by the SpinBox editor factory. Another toolkit may create an instance of a completely different class when asked to do the same thing. In other words, applications cannot assume that the API of a widget created by one toolkit is compatible with the API created by another.

This is particularly an issue when configuring a view with property names and values when specifying a view factory.

This all makes it sound very difficult to create toolkit-independent applications. However:

  • tookits will not be fundamentally different to each other. All widgets will be instances of QWidget, and in most cases a toolkit will create a sub-class of a standard Qt widget. For example a KDE toolkit will create an instance of KIntSpinBox which is a sub-class of QSpinBox. Therefore if an application restricts itself to the QSpinBox API it will be portable but still look like a KDE application
  • properties that are invalid for a particular view will be ignored and will not raise an exception.

In summary, making an application work with multiple toolkits isn’t as simple as just using an instance of the new toolkit in place of the old. The application needs to be checked against any documented toolkit incompatibilites. However it is expected that there will be very few of these.

A Pattern for Simple GUI Utilities

Although dip contains lots of functionality intended to support the development of large, complex applications it is also very suitable for writing simple GUI utilities. In this section we walk through the code of a line counting utility using a pattern that can be used whenever there is a need to gather some information from the user, perform some task using that information as input and then displaying the output.

The complete source code (which you can download from here) is shown below.

import glob
import os
import sys

from PyQt4.QtGui import QApplication, QDialog, QWizard

from dip.model import Instance, List, Model, Str
from dip.ui import (CollectionButtons, DirectoryValidator, FileSelector, Form,
        IToolkit, ListWidget, MessageArea, Stretch, VBox, Wizard, WizardPage)


class LineCounter(Model):
    """ The LineCounter class is a combined model and view for counting the
    total number of lines in a set of files.
    """

    # The name of the root directory.
    root_directory = Str(required='yes', validator=DirectoryValidator)

    # The list of glob-like filters to apply to names of files in the directory
    # hierachy.  If the list is empty then all files are processed.
    filters = List(Str(required='stripped'))

    # The toolkit to use.
    toolkit = Instance(IToolkit)

    # The wizard used to get the input from the user.  Note that this is an
    # ordinary class attribute.
    wizard = Wizard(
            WizardPage(
                VBox (
                    Form(FileSelector('root_directory', mode='directory')),
                    Stretch(),
                    MessageArea()
                ),
                title="Directory name",
                subTitle="Enter the name of the directory containing the "
                        "files whose lines are to be counted."
            ),
            WizardPage(
                VBox(
                    CollectionButtons('filters'),
                    ListWidget('filters'),
                    Stretch(),
                    MessageArea()
                ),
                title="File filters",
                subTitle="Enter the glob-like filters to apply to the file "
                        "names."
            ),
            options=QWizard.HaveFinishButtonOnEarlyPages,
            windowTitle="Line counter"
        )

    def populate(self):
        """ Populate the model with input from the user.

        :return:
            ``True`` if the user didn't cancel.
        """

        ui = self.wizard(self, self.toolkit)

        return ui.exec_() == QDialog.Accepted

    def perform(self):
        """ Count the number of lines and display the total. """

        filepaths = []

        for dirpath, _, filenames in os.walk(self.root_directory):
            if len(self.filters) == 0:
                # There are no filters so look at each file.
                for filename in filenames:
                    filepaths.append(os.path.join(dirpath, filename))
            else:
                # Apply the filters.
                for filter in self.filters:
                    filepaths.extend(glob.glob(os.path.join(dirpath, filter)))

        # Count the files in each file.
        line_count = 0

        for filepath in filepaths:
            try:
                with open(filepath) as fh:
                    line_count += len(fh.readlines())
            except UnicodeDecodeError:
                # Assume it is a binary file and ignore it.
                pass
            except EnvironmentError as err:
                self.toolkit.warning("Line counter",
                        "There was an error reading the file <tt>{0}</tt>."
                                .format(filepath),
                        detail=str(err))

        # Tell the user.
        self.toolkit.information("Line counter",
                "There were {0} lines in {1} files.".format(line_count,
                        len(filepaths)))

    @toolkit.default
    def toolkit(self):
        """ Create the default toolkit. """

        from dip.ui.toolkits.qt import QtToolkit

        return QtToolkit()


# Every PyQt GUI application needs a QApplication.
app = QApplication(sys.argv)

# Create the model/view.
line_counter = LineCounter()

# Populate the model with input from the user.
if line_counter.populate():
    # Perform the task, i.e. count the lines.
    line_counter.perform()

The code comprised three sections:

  • the import statements
  • a combined model/view class that represents the input data, declaratively defines the GUI used to gather the data from the user and actually counts the lines
  • the code to create the model/view instance and call its methods to create the GUI and count the lines.

Note that we have chosen to use a wizard to gather the input data from the user. However the amount of data doesn’t really justify this (a simple dialog would be sufficient) but we wanted to demonstrate how easy it is to create a wizard.

The import statements are shown below.

import glob
import os
import sys

from PyQt4.QtGui import QApplication, QDialog, QWizard

from dip.model import Instance, List, Model, Str
from dip.ui import (CollectionButtons, DirectoryValidator, FileSelector, Form,
        IToolkit, ListWidget, MessageArea, Stretch, VBox, Wizard, WizardPage)

The model/view class is called LineCounter and is a sub-class of Model as you would expect.

class LineCounter(Model):
    """ The LineCounter class is a combined model and view for counting the
    total number of lines in a set of files.
    """

The input data we want to gather from the user is the name of a directory and an optional list of glob-like patterns that are used to filter the names of files in the directory. These are represented as attributes in the model. The directory name is shown below.

    root_directory = Str(required='yes', validator=DirectoryValidator)

The directory name is stored as a string. By specifying the required meta-data we are telling any editor that dip creates to modify the value that it must consist of at least one character. By specifying the validator meta-data we are also saying that the value must be the name of an existing directory.

The declaration of the list of filters is shown below.

    filters = List(Str(required='stripped'))

Here the meta-data means that each filter must contain at least one non-whitespace character.

Next we declare an attribute that holds the toolkit that will be used to create the GUI.

    toolkit = Instance(IToolkit)

The final attribute is the declaration of the wizard used to gather the input data. Note that this is an ordinary class attribute.

    wizard = Wizard(
            WizardPage(
                VBox (
                    Form(FileSelector('root_directory', mode='directory')),
                    Stretch(),
                    MessageArea()
                ),
                title="Directory name",
                subTitle="Enter the name of the directory containing the "
                        "files whose lines are to be counted."
            ),
            WizardPage(
                VBox(
                    CollectionButtons('filters'),
                    ListWidget('filters'),
                    Stretch(),
                    MessageArea()
                ),
                title="File filters",
                subTitle="Enter the glob-like filters to apply to the file "
                        "names."
            ),
            options=QWizard.HaveFinishButtonOnEarlyPages,
            windowTitle="Line counter"
        )

The wizard has two pages. We have configured the wizard itself by setting the windowTitle and options properties. We have specified that the Finish button will be shown on all pages. This is because the second page of the wizard is completely optional.

The first page of the wizard is shown below as it appears when the utility is started.

_images/line_count_page_1.png

The interesting part of the page is the FileSelector arranged in a Form. The MessageArea towards the bottom of the page reflects the fact that the directory name is currently empty. Because the page is invalid the Next and Finish buttons are disabled. They will be automatically enabled as soon as the user enters the name of an existing directory.

The second wizard page is shown below after we have added a *.py filter.

_images/line_count_page_2.png

The main components in this page are the central ListWidget where the filters can be edited, and the CollectionButtons that are used to manipulate the entries in the list. The tool buttons may also be used individually or not at all. They can also be placed anywhere within the page. For example, as the filters are independent of each other there is no real need to be able to move them around with the up and down buttons. Therefore we could instead do something like:

HBox(Stretch(), AddButton('filters'), RemoveButton('filters')),

The next part of the model/view is the populate() method, the body of which is shown below.

        return ui.exec_() == QDialog.Accepted

The first of the above lines creates the QWizard instance itself by calling the Wizard factory. The two arguments are the model (i.e. self) and the toolkit. A different toolkit may mean that some other implementation of a wizard may be created instead.

The second of the above lines is the standard PyQt call to display the wizard and to allow the user to interact with it. The populate() method then returns True if the user didn’t cancel.

The next part of the model/view is the perform() method which does the actual work of counting the lines in the files. The first section of the method, shown below, builds up the full list of files taking any filters into account.

        filepaths = []

        for dirpath, _, filenames in os.walk(self.root_directory):
            if len(self.filters) == 0:
                # There are no filters so look at each file.
                for filename in filenames:
                    filepaths.append(os.path.join(dirpath, filename))
            else:
                # Apply the filters.
                for filter in self.filters:
                    filepaths.extend(glob.glob(os.path.join(dirpath, filter)))

The next section of the method, shown below, reads each file and adds the number of lines in each to a running total.

        line_count = 0

        for filepath in filepaths:
            try:
                with open(filepath) as fh:
                    line_count += len(fh.readlines())
            except UnicodeDecodeError:
                # Assume it is a binary file and ignore it.
                pass
            except EnvironmentError as err:
                self.toolkit.warning("Line counter",
                        "There was an error reading the file <tt>{0}</tt>."
                                .format(filepath),
                        detail=str(err))

The final section of the method, shown below, displays the total line count and the number of files as a dialog using the toolkit’s information() method.

        self.toolkit.information("Line counter",
                "There were {0} lines in {1} files.".format(line_count,
                        len(filepaths)))

An example of the output produced is shown below.

_images/line_count_output.png

The final part of the model/view is the method that creates the default toolkit, the body of which is shown below.

        from dip.ui.toolkits.qt import QtToolkit

        return QtToolkit()

That completes our walk through of the LineCounter model/view class.

Note

This example uses a controller that is created automatically. What if we needed to use a specialised controller, perhaps to perform some additional validation?

This is most easily done (in a simple utility such as this) by combining it with the existing model/view. In other words, LineCounter should be sub-classed from WizardController instead of Model. All you need to do then is to provide LineCounter with an implementation of validate_page().

The final section of the whole utility is shown below.

app = QApplication(sys.argv)

# Create the model/view.
line_counter = LineCounter()

# Populate the model with input from the user.
if line_counter.populate():
    # Perform the task, i.e. count the lines.
    line_counter.perform()

Here we create the QApplication instance required by every GUI based application, create the instance of our model/view, populate the model, and (if the user didn’t cancel) perform the task of counting the lines of the specified files.