Programming a simple molecular GUI browser with model-view architecture (MVC) using Python with PySide or PyQt and RDKit

One of the more popular blog post based on monthly visitors is the old Create a Simple Object Oriented GUIDE GUI in MatLAB, but since I don’t program MATLAB at the moment, I thought it could be nice by making an update about how this could be done with Python. One of the GUI widget libraries with binding for Python is QT. It’s a Cross platform toolkit for developing graphical applications and have bindings for many programming including Python. For Python there’s PyQt or PySide. The differences are not big, but I’ll use PySide in this example. It’s a lengthy post, so I hope you’ll stay with me to the end and learn how to write a simple and extensible molecular browser of SD files.

I’ll show how to make a View/Controller – Model (MVC) like architecture by programming a simple RDKit based molecular browser for SD files in Python using PySide. There are many interpretations of the MVC architecture, and various opinions about how the code should be separated. However, the core idea is to separate the core logic, data and capabilities in a model, and then have different views/controllers/widgets that relate to this object, so that communication between widgets is minimized. It may seem a bit cumbersome in the start, but by dividing the code, each widget can be tested/used/reused on its own, and the model can easily be reused in non-GUI scripts. This makes the code easily expandable and are suitable for medium complex applications. The division is illustrated in the diagram:

The main program is a QmainWindow which mixes viewer and a controller elements. During __init__ it will instantiate the SDBrowser class and keep a reference to the object. The Qt widgets will be set to manipulate the object by setting properties or executing functions of this model. When the model changes its properties, it will signal the change (pyqtSignal), and the Qt Widgets which act as viewers will update their content by reading the properties of the model. When the extra view will be needed, it will be launched and passed a reference to the model from the main program. It’s fire and forget. The main program will not interact with the other view, which will have its own routines for reacting to the signaled changes from the model.

A zip file with the source code and all icons is included at the end of the blog post.

The Model

The model need to be a subclass of the PySide Qobject, so that Signals and Slots can be set up. This is part of the PySide.QtCore which contains all the non-GUI stuff from PySide. Additionally some RDKit modules will be imported.

The init function is quite simple. We set a private property _selected, a length and a _status. We also need to run the init of the super class (Qobject), so that the Signals and other Qt related functions get initialized.

Signals, Slots and Python properties

Qt widgets and programs communicate via Signals and Slots. Signals are functions/events that can be connected to zero, one or more Slots (listeners). The signals can pass values with them or not. To make a signal on a class derived from Qobject, it will be set as a property on the Class, not the object. Thus is will be set outside the __init__ function as this using the Signal imported from PySide.QtCore.

#Define a signal that can be emitted when the selected compound is changed, is called pyqtSignal in PyQt
selectedChanged = Signal(int, name = 'selectedChanged')

This signal will pass an integer and have the name selectedChanged. If the name property is not set, it will get the same name as the variable. To Qt’ify the properties of the Python Class, the properties are defined using functions as this:

The property is decorated with @property. Then we can read the private property _selected as x = model.selected, because we get the return from the selected function. The next line is a setter for the property selected. This function will be run if we assign some value to the selected property in the pythonic way: model.selected = 1. However, as we will soon see, with Qt its easier to use a setter function, here setSelected. Some sanitizing is done in the setting function here to prevent negative selections, and also preventing selections larger than the property self.length (which will be updated when we read in the SD file with the molecules. Additionally a check is done to see if the property actually changed at all. This is necessary as there will otherwise be unnecessary round tripping of the property, if we start to connect multiple signals and slots together from the controllers and the viewers. Last, the Signal that we added to the Class is emitted with the new value.

Slots

Slots can be “just” python functions. But to tell QT what kind of variable the function expects, the @Slot() decorator imported from PySide.QtCore can be used. Here the Slot will not be accepting any extra input.

The slot simply updates the status with the selected (+1 for natural numbers) and the length. We want this updated each time we change the selection. This is easily done by connection the Signal selectedChanged to the slot setCounter. So an extra line is added to the __init__ function:

Now, each time the Signal selectedChanged is emitted, the Slot setCounter will be run. But what about the INT sent with together with the Signal? Because the Slot definition did not define an INT as input, it will be ignored, and the function will just be called without any variables. A statusChanged signal needed later is added along the lines of the selected property. The full source code for the model.py file can be found in the end of the Blog post.

The model also need to be able to load an SD file, render the molecule currently selected as SVG and return the current molecules molblock. So these functions are added. The only Qt related stuff here is that when the Sdfile is loaded, it will emit a selectedChanged signal.

Programming the main window

The main window is programmed as a subclass to QmainWindow, which will allow us to have menus, tool bar, status bar and a central widget. I’ll just show examples of each segment, the full code can be found below the blog post.

The __init__ function has to init the super class and then the model is instantiated as a property on the object itself. Lastly the __init__ function calls the initUI function which will be written to set up the graphical elements. It may be easier to start with the functions that will interact with the model.

The update_mol function handles the updating of the central SVG widget. It loads the SVG it gets from the models getMolSvg() function into the central widget.

The openFile function uses a QfileDialog to get a filename selected by the user, and then calls the models loadSDfile function with this filename.

The two functions nextMol and prevMol increments or decrement the number for the selected molecule. The model already has the validation code that prevents selecting negative numbers or numbers exceeding the length of the SD file.

Returning to the init function, it starts be setting some properties of the window object, set up the central SVG widget to show the molecules, define a statusbar and add a permanent widget to show the molecule counting.

Instead of triggering the functions directly from the widgets, they will be used in QActions. QActions bundles together name, icon, statusbar tip, shortcut and action. As an example is shown the openAction below. All actions are collected in a function CreateActions to bundle them. There are addtionally defined actions for exit, about, previous and next molecule (not shown, but included in the full code below the blog post).

After setting up the components, it goes on to connect some Signals from the model to the relevant functions defined previously. Each time the selectedChanged Signal is emited, the self.update_mol function will be run, which updates the central widget. If the statusChanged signal is emitted, the permanent widget of the statusbar will be updated with the Signals sent string.

This completes the circle and we are now back to the __init__ function which called the initGUI function. Theres also a exit confirmation dialogue and an about message box, but to skip to the interesting part where the actual application is launched.

The important lines are the sdBrowser = Qapplication(sys.argv), which creates a QT application and must be run before creation of any QT widgets. Next the widget is created with mainWindow = MainWindow(), optionally with a filename read from the command line. Lastly, sdBrowser is set to enter the main loop, which will keep it alive until exiting.

Running the application with a test SD file will bring up a simple application to browse the molecules. There’s a menu bar and a tool bar above the molecule view. It can load an SD file and clicking the arrows or keyboard left/right arrowkeys switches between the molecules. Below is a status bar with tips that updates when the mouse over toolbar and menu items and a counter. A cute little Qt app.

Adding another view

The additional complexity of the modularized MVC model starts to pay back when we extent the functionality of the program with new views. As a simple example, a view that will show the raw MolFile information from the SD file will be added.

The QtextBrowser widget is subclassed. As before the super class’s __init__ function must be called. Additionally the DeleteOnClose attribute is set. Otherwise the widget will continue to work and query the model in the background, even if the window is closed. The reference to the model we get on initialization is saved as self.model and the font and size is defined. Then the model’s Signal selectedChanged is connected to a function that updates the text. It simply gets the molblock for the currently selected molecule from the model and updates the text.

Opening the new window is “fire and forget” for the main window. The widget itself connects the relevant Signal from the model to the widgets update function. No need to update the nextMol function of the main application.

Let’s follow the trace of events. When the nextMol action is triggered by clicking on the icon in the toolbar, it will call the nextMol function. This function increases the selected property in the model by one. Changing the property triggers the Signal selectedChanged from the model. This signal has been bound to various Slots.

The Status is changed in the model (nonGUI connection). This updates the status and triggers the statusChanged signal.

The statusChanged signal is bound to the Slot setText of the permanent widget in the toolbar, which then get updated.

The update_mol Slot in the main window will also receive the selectedChanged Signal, as they have been connected. This updates the SVG widget with the SVG rendering of the new molecule.

The updateText Slot of the seperate molblock viewer is also triggered by the selectedChanged Signal. This in turn updates the text of the molblock view.

The main advantages is that the model do not care about the GUI elements. It only emits signals in response to changes in the model. The two views also do not need to interact, they just respond to changes in the model. The controllers (as an example the nextMol action), do not need to update the views, it just changes the property of the model.

For simple applications its probably faster and less complex to directly connect the signals of the widgets to update other widgets, but as the applications grow in size and complexity the model-view/controller design makes it much easier to structure the code. The loose binding between the controllers and the views and between the various views prevents a lot of updating of previously written code to accommodate the new functionality.

Going further

A critique of the model-view/controller architecture is that it leads to multi window applications, which can be quite annoying when the number of windows gets too large. But there is nothing that prevents the widgets and views to share the same window, even though they do not interact directly. The screenshot below shows a slightly more evolved molbrowser. Here the grid View and the SVG view are both placed in the main window, but are each contained as their own view that only relates to the model. The browser has additionally been extended with a matplotlib based interactive view of the properties extracted from the SD file. The plot is interactive and clicks on dots, selects the molecule from the dataset in the model. This in turn updates the grid view and the SVG view, making it easy to explore a dataset of molecules.

Conclusion

So to summarize. First a non-GUI model that contained the core logic, data and number crunching of the app was written. Some functions that presents the internal data and some Qt Signals that tells when something happened with the status of the model were added. Properties that needs to be linked to emitting of signals was encapsulated in getter and setter functions.

The main app was made as a subclass to QmainWindow. Custom functions were added to handle the interaction with the model, wrapped in QActions, which were used in menu and the main toolbar. The views update functions were connected with the relevant signals from the model.

Another widget was programmed which used the same model. It could then be launched from the main application, simply by feeding a reference to the model. Fire and forget, as the views did not need to interact directly.