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

Esben Jannik Bjerrum/ June 2, 2017/ Blog, Cheminformatics, RDkit/ 2 comments

rdkit based molecule browserOne 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:
rdkit browser MVC model
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.

#QtCore is the nonGUI stuff.
from PySide.QtCore import QObject, Slot
from PySide.QtCore import Signal
#RDKit stuff
from rdkit import Chem
from rdkit.Chem import rdDepictor
from rdkit.Chem.Draw import rdMolDraw2D
#The model holding the SDfile data
class SDmodel(QObject): #Inherit from QObject so that Signals can be emitted
    def __init__(self):
        super(SDmodel, self).__init__() #Init the super QObject class so that it works with QT stuff etc.
        self._selected = 0
        self._status = "Ready"
        self.length = 0

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:

    @property
    def selected(self):
        return self._selected
    #This enables us to do model.selected = 2
    @selected.setter
    def selected(self, value):
        self.setSelected(value)
    #this is more easy to use from with PyQt signals
    def setSelected(self, selected):
        #Prevent setting a selected that doesn't exist
        if selected < 0: selected = 0
        if selected > self.length -1: selected = self.length -1
        #Only set the selected if its changed, we get round tripping otherwise
        if selected != self._selected:
            self._selected = selected
            print "in model: selected set for ", selected
            #Emit the signal that selected has changed
            self.selectedChanged.emit(self._selected)

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.

:
    #Decorate the function setCounter
    @Slot()
    def setCounter(self):
        self.setStatus('%s/%s'%(self._selected + 1, self.length))

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:

        #Set the counter when the selection is changed (non GUI signal slot example)
        self.selectedChanged.connect(self.setCounter)

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.

    def loadSDfile(self, filename):
        self.filename = filename
        self.SDMolSupplier = Chem.SDMolSupplier(filename)
        self.length = len(self.SDMolSupplier)
        if self.selected == 0:
            self.selectedChanged.emit(self._selected)
        else:
            self.setSelected(0)
    #Better rendering with SVG
    def getMolSvg(self, kekulize=True, calc2Dcoords=True):
        mol = self.SDMolSupplier[self._selected]
        mc = Chem.Mol(mol.ToBinary())
        if kekulize:
            try:
                Chem.Kekulize(mc)
            except:
                mc = Chem.Mol(mol.ToBinary())
        if not mc.GetNumConformers() or calc2Dcoords:
            rdDepictor.Compute2DCoords(mc)
        drawer = rdMolDraw2D.MolDraw2DSVG(300,300)
        drawer.DrawMolecule(mc)
        drawer.FinishDrawing()
        svg = drawer.GetDrawingText().replace('svg:','')
        return svg
    def getMolBlock(self):
        print "Getting MolBlock"
        return self.SDMolSupplier.GetItemText(self.selected)

Testing the model in nonGUI mode

Using the model interactively. Note the difference in counting between 0 counting (selected) and natural counting (status).

In [1]: from model import SDmodel
In [2]: sdmodel = SDmodel()
In [3]: sdmodel.loadSDfile('tester.sdf')
In [4]: print sdmodel.length
2
In [5]: print sdmodel.status
1/2
In [6]: sdmodel.setSelected(1)
in model: selected set for  1
In [7]: print sdmodel.status
2/2
In [8]: print sdmodel.getMolSvg()[0:100]
&amp;lt;?xml version='1.0' encoding='iso-8859-1'?&amp;gt;
&amp;lt;svg version='1.1' baseProfile='full'
              xmln
In [9]: print sdmodel.getMolBlock()[0:100]
Getting MolBlock
NSC 2
 ccorina  04030319103D 1   1.00000     0.00000
CORINA 2.61 0041  25.10.2001
 28 31  0  0  0

So that was the model. 91 lines including blanks and comments.

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 Main browser windows
class MainWindow(QMainWindow):
	def __init__(self,  fileName=None):
		super(MainWindow,self).__init__()
		self.fileName = fileName
		self.filter = "SD files (*.sdf *.sd)"
		self.model = SDmodel()
		#Setup the user interface
		self.initUI()
		#If we get a filename, load it into the model
		if self.fileName != None:
			self.model.loadSDfile(fileName)

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.

	#Update the central widget with SVG from the model
	def update_mol(self):
		self.center.load(QByteArray(self.model.getMolSvg()))
	#Open a new file
	def openFile(self):
		self.fileName, self.filter = QFileDialog.getOpenFileName(self, filter=self.filter)
		self.model.loadSDfile(str(self.fileName))
	#Increment the selected mol with 1
	def nextMol(self):
		#Increment the selected molecule in the model by 1
		self.model.setSelected(self.model.selected + 1)
	#Decrement the selected mol with 1
	def prevMol(self):
		#Decrement the selected molecule in the model by 1
		self.model.setSelected(self.model.selected - 1)

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.

		#Set Window properties
		self.setWindowTitle("A Simple SD file browser")
		self.setWindowIcon(QIcon('Peptide.png'))
		self.setGeometry(100, 100, 200, 150)
		#Set Central Widget
		self.center = QtSvg.QSvgWidget()
		self.center.setFixedSize(350,350)
		self.setCentralWidget(self.center)
		#Setup the statusbar
		self.myStatusBar = QStatusBar()
		#A permanent widget is right aligned
		self.molcounter = QLabel("-/-")
		self.myStatusBar.addPermanentWidget(self.molcounter, 0)
		self.setStatusBar(self.myStatusBar)
		self.myStatusBar.showMessage('Ready', 10000)

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).

		self.openAction = QAction( QIcon('Open Folder.png'), 'O&pen',
								  self, shortcut=QKeySequence.Open,
								  statusTip="Open an SD file",
								  triggered=self.openFile)

Next are functions that create the menu and toolbar. Two menu’s are created and added to the main windows menubar, and a main toolbar is created.

		#Setup the menu
		self.fileMenu = self.menuBar().addMenu("&File")
		self.helpMenu = self.menuBar().addMenu("&Help")
		#Setup the Toolbar
		self.mainToolBar = self.addToolBar('Main')

With the actions, menu and toolbars ready, the menus and toolbar are ready to be populated with the defined actions. Note how the openAction is added to the menu and also reused for the toolbar.

		#Populate the Menu with Actions
		self.fileMenu.addAction(self.openAction)
		self.fileMenu.addSeparator()
		self.fileMenu.addAction(self.exitAction)
		self.helpMenu.addAction(self.aboutAction)
		self.helpMenu.addSeparator()
		self.helpMenu.addAction(self.aboutQtAction)
		#Populate the Toolbar with actions.
		self.mainToolBar.addAction(self.openAction)
		self.mainToolBar.addSeparator()
		self.mainToolBar.addAction(self.prevAction)
		self.mainToolBar.addAction(self.nextAction)
		self.mainToolBar.addSeparator()
		self.mainToolBar.addAction(self.molblockAction)

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.

		#Connect model signals to UI slots
		#Update central widget if the selected molecule changes
		self.model.selectedChanged.connect(self.update_mol)
		#Update the permanent widget in the status bar, if status
changes
		self.model.statusChanged.connect(self.molcounter.setText)
		#Finally! Show the UI!
		self.show()

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.

if __name__ == '__main__':
	# Exception Handling
	try:
		sdBrowser = QApplication(sys.argv)
		#Load with file if provided
		if len(sys.argv) > 1:
			mainWindow = MainWindow(fileName = sys.argv[1])
		else:
			mainWindow = MainWindow()
		sdBrowser.exec_()
		sys.exit(0)
	#Basic Exception handling
	except NameError:
		print("Name Error:", sys.exc_info()[1])
	except SystemExit:
		print("Closing")
	except Exception:
		print(sys.exc_info()[1])

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.
rdkit based molecule browser

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.

#Import required modules
from PySide import QtCore, QtGui
#Import model
from model import SDmodel
#The Molblock viewer class
class MolBlockView(QtGui.QTextBrowser):
    def __init__(self, model, parent=None):
        #Also init the super class
        super(MolBlockView, self).__init__(parent)
        #This sets the window to delete itself when its closed, so it doesn't keep querying the model
        self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
        #We set the model we got on initiation
        self.model = model
        #Set font
        self.font = QtGui.QFont("Monospace")
        self.font.setStyleHint(QtGui.QFont.Monospace)
        self.setFont(self.font)
        self.setGeometry(600, 100, 620, 800)
        #connect the signal to an update of the text
        self.model.selectedChanged.connect(self.updateText)
        #Update text first time
        self.updateText()
    def updateText(self):
        #Function to get the molblock text from the model
        molblock = self.model.getMolBlock()
        self.setText(str(molblock))

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.
It can be tested by separately from the main application.

import sys
from model import SDmodel
from molblockview import MolBlockView
from PySide import QtGui
model = SDmodel()
model.loadSDfile('tester.sdf')
myApp = QtGui.QApplication(sys.argv)
molblockview = MolBlockView(model)
molblockview.show()
myApp.exec_()

To integrate it in the main application there’s needed a function that launches the widget, a QAction and addition of the QAction to the toolbar.

:
    def viewMolBlock(self):
        self.molblockBrowser = MolBlockView(self.model)
        self.molblockBrowser.show()

Create the QAction.

        self.molblockAction = QAction( QIcon('molblock.png'), 'V&iew MolBlock',
                                  self, shortcut="Ctrl+M",
                                  statusTip="View MolBlock",
                                  triggered=self.viewMolBlock)

Add the action to the toolbar.

        self.mainToolBar.addSeparator()
        self.mainToolBar.addAction(self.molblockAction)

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.
rdkit based molecule browser and molblock viewer
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.

  1. The Status is changed in the model (nonGUI connection). This updates the status and triggers the statusChanged signal.
  2. The statusChanged signal is bound to the Slot setText of the permanent widget in the toolbar, which then get updated.
  3. 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.
  4. 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.
rdkit based molecule browser with grid and graphs

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.

Full model.py

#QtCore is the nonGUI stuff.
from PySide.QtCore import QObject, Slot
from PySide.QtCore import Signal
#RDKit stuff
from rdkit import Chem
from rdkit.Chem import rdDepictor
from rdkit.Chem.Draw import rdMolDraw2D
#The model holding the SDfile data
class SDmodel(QObject): #Inherit from QObject so that Signals can be emitted
	def __init__(self):
		super(SDmodel, self).__init__() #Init the super QObject class so that it works with QT stuff etc.
		self._selected = 0
		self._status = "Ready"
		self.length = 0
		#Set the counter when the selection is changed (non GUI signal slot example)
		self.selectedChanged.connect(self.setCounter)
	#Define a signal that can be emitted when the selected compound is changed, is called pyqtSignal in PyQt
	selectedChanged = Signal(int, name = 'selectedChanged')
	@property
	def selected(self):
		return self._selected
	#This enables us to do model.selected = 2
	@selected.setter
	def selected(self, value):
		self.setSelected(value)
	#this is more easy to use from with PyQt signals
	def setSelected(self, selected):
		#Prevent setting a selected that doesn't exist
		if selected < 0: selected = 0
		if selected > self.length -1: selected = self.length -1
		#Only set the selected if its changed, we get roundtripping otherwise
		if selected != self._selected:
			self._selected = selected
			print "in model: selected set for ", selected
			#Emit the signal that selected has changed
			self.selectedChanged.emit(self._selected)
	#Decorate the function setCounter
	@Slot()
	def setCounter(self):
		self.setStatus('%s/%s'%(self._selected + 1, self.length))
	#A status signal and property
	statusChanged = Signal(str, name = 'statusChanged')
	@property
	def status(self):
		return self._status
	def setStatus(self, status):
		self._status = status
		self.statusChanged.emit(self._status)
	def loadSDfile(self, filename):
		self.filename = filename
		self.SDMolSupplier = Chem.SDMolSupplier(filename)
		self.length = len(self.SDMolSupplier)
		if self.selected == 0:
			self.selectedChanged.emit(self._selected)
		else:
			self.setSelected(0)
	#Better rendering with SVG
	def getMolSvg(self, kekulize=True, calc2Dcoords=True):
		mol = self.SDMolSupplier[self._selected]
		mc = Chem.Mol(mol.ToBinary())
		if kekulize:
		    try:
		        Chem.Kekulize(mc)
		    except:
		        mc = Chem.Mol(mol.ToBinary())
		if not mc.GetNumConformers() or calc2Dcoords:
		    rdDepictor.Compute2DCoords(mc)
		drawer = rdMolDraw2D.MolDraw2DSVG(300,300)
		drawer.DrawMolecule(mc)
		drawer.FinishDrawing()
		svg = drawer.GetDrawingText().replace('svg:','')
		return svg
	def getMolBlock(self):
		print "Getting MolBlock"
		return self.SDMolSupplier.GetItemText(self.selected)
#Simple unit testing
if __name__ == "__main__":
	sdmodel = SDmodel()
	sdmodel.loadSDfile('tester.sdf')
	print sdmodel.length
	sdmodel.setSelected(1)
	print sdmodel.status
	print sdmodel.getMolSvg()[0:100]
	print sdmodel.getMolBlock()[0:100]&lt;/pre&gt;

Full sdbrowser.py

#!/usr/bin/python
#SDbrowser viewer
import sys, time
from PySide.QtGui import *
from PySide.QtCore import QByteArray
from PySide import QtSvg
#Import model
from model import SDmodel
from molblockview import MolBlockView
#The Main browser windows
class MainWindow(QMainWindow):
	def __init__(self,  fileName=None):
		super(MainWindow,self).__init__()
		self.fileName = fileName
		self.filter = "SD files (*.sdf *.sd)"
		self.model = SDmodel()
		#Setup the user interface
		self.initUI()
		#If we get a filename, load it into the model
		if self.fileName != None:
			self.model.loadSDfile(fileName)
	#Update the central widget with SVG from the model
	def update_mol(self):
		self.center.load(QByteArray(self.model.getMolSvg()))
	#Open a new file
	def openFile(self):
		self.fileName, self.filter = QFileDialog.getOpenFileName(self, filter=self.filter)
		self.model.loadSDfile(str(self.fileName))
	#Increment the selected mol with 1
	def nextMol(self):
		#Increment the selected molecule in the model by 1
		self.model.setSelected(self.model.selected + 1)
	#Decrement the selected mol with 1
	def prevMol(self):
		#Decrement the selected molecule in the model by 1
		self.model.setSelected(self.model.selected - 1)
	#Launch the molblockviewer
	def viewMolBlock(self):
		self.molblockBrowser = MolBlockView(self.model)
		self.molblockBrowser.show()
	#Setup the user interface
	def initUI(self):
		#Set Window properties
		self.setWindowTitle("A Simple SD file browser")
		self.setWindowIcon(QIcon('Peptide.png'))
		self.setGeometry(100, 100, 200, 150)
		#Set Central Widget
		self.center = QtSvg.QSvgWidget()
		self.center.setFixedSize(350,350)
		self.setCentralWidget(self.center)
		#Setup the statusbar
		self.myStatusBar = QStatusBar()
		#A permanent widget is right aligned
		self.molcounter = QLabel("-/-")
		self.myStatusBar.addPermanentWidget(self.molcounter, 0)
		self.setStatusBar(self.myStatusBar)
		self.myStatusBar.showMessage('Ready', 10000)
		#Make the Actions
		self.openAction = QAction( QIcon('Open Folder.png'), 'O&pen',
								  self, shortcut=QKeySequence.Open,
								  statusTip="Open an SD file",
								  triggered=self.openFile)
		self.molblockAction = QAction( QIcon('Page Overview 3.png'), 'V&iew MolBlock',
								  self, shortcut="Ctrl+M",
								  statusTip="View MolBlock",
								  triggered=self.viewMolBlock)
		self.exitAction = QAction( QIcon('Exit.png'), 'E&xit',
								   self, shortcut="Ctrl+Q",
								   statusTip="Close the Application",
								   triggered=self.exit)
		self.prevAction = QAction( QIcon('Left.png'),'Previous', self,
								   shortcut=QKeySequence.MoveToPreviousChar,
								   statusTip="Previous molecule",
								   triggered=self.prevMol)
		self.nextAction = QAction( QIcon('Right.png'),'Next', self,
								   shortcut=QKeySequence.MoveToNextChar,
								   statusTip="Next molecule",
								   triggered=self.nextMol)
		self.aboutAction = QAction( QIcon('Info.png'), 'A&;bout',
									self, statusTip="Displays info about SDbrowser",
								   triggered=self.aboutHelp)
		self.aboutQtAction = QAction("About &Qt", self,
								statusTip="Qt library About box",
								triggered=qApp.aboutQt)
		#Setup the menu
		self.fileMenu = self.menuBar().addMenu("&File")
		self.helpMenu = self.menuBar().addMenu("&Help")
		#Setup the Toolbar
		self.mainToolBar = self.addToolBar('Main')
		#Populate the Menu with Actions
		self.fileMenu.addAction(self.openAction)
		self.fileMenu.addSeparator()
		self.fileMenu.addAction(self.exitAction)
		self.helpMenu.addAction(self.aboutAction)
		self.helpMenu.addSeparator()
		self.helpMenu.addAction(self.aboutQtAction)
		#Populate the Toolbar with actions.
		self.mainToolBar.addAction(self.openAction)
		self.mainToolBar.addSeparator()
		self.mainToolBar.addAction(self.prevAction)
		self.mainToolBar.addAction(self.nextAction)
		self.mainToolBar.addSeparator()
		self.mainToolBar.addAction(self.molblockAction)
		#Connect model signals to UI slots
		#Update central widget if the selected molecule changes
		self.model.selectedChanged.connect(self.update_mol)
		#Update the permanent widget in the status bar, if status changes
		self.model.statusChanged.connect(self.molcounter.setText)
		#Finally! Show the UI!
		self.show()
	def exit(self):
		response = QMessageBox.question(self,"Confirmation","This will exit the SD browser\nDo you want to Continue",
										QMessageBox.Yes | QMessageBox.No)
		if response == QMessageBox.Yes:
			sdBrowser.quit()
		else:
			pass
	def aboutHelp(self):
		QMessageBox.about(self, "A Basic SD browser",
				"A Simple SD browser where you can see molecules\nIcons from icons8.com")
if __name__ == '__main__':
	# Exception Handling
	try:
		sdBrowser = QApplication(sys.argv)
		#Load with file if provided
		if len(sys.argv) > 1:
			mainWindow = MainWindow(fileName = sys.argv[1])
		else:
			mainWindow = MainWindow()
		sdBrowser.exec_()
		sys.exit(0)
	#Basic Exception handling
	except NameError:
		print("Name Error:", sys.exc_info()[1])
	except SystemExit:
		print("Closing")
	except Exception:
		print(sys.exc_info()[1])

Full molblockview.py

#!/usr/bin/python
#Import required modules
from PySide import QtCore, QtGui
#Import model
from model import SDmodel
#The Molblock viewer class
class MolBlockView(QtGui.QTextBrowser):
	def __init__(self, model, parent=None):
		#Also init the super class
		super(MolBlockView, self).__init__(parent)
		#This sets the window to delete itself when its closed, so it doesn't keep querying the model
		self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
		#We set the model we got on initiation
		self.model = model
		#Set font
		self.font = QtGui.QFont("Monospace")
		self.font.setStyleHint(QtGui.QFont.Monospace)
		self.setFont(self.font)
		self.setGeometry(600, 100, 620, 800)
		#connect the signal to an update of the text
		self.model.selectedChanged.connect(self.updateText)
		#Update text first time
		self.updateText()
	def updateText(self):
		#Function to get the molblock text from the model
		molblock = self.model.getMolBlock()
		self.setText(str(molblock))
if __name__ == "__main__":
	#Import model
	import sys
	from model import SDmodel
	model = SDmodel()
	model.loadSDfile('tester.sdf')
	myApp = QtGui.QApplication(sys.argv)
	molblockview = MolBlockView(model)
	molblockview.show()
	myApp.exec_()
Zipfile:sdBrowser



                                        
                    
                    
                    
Share this Post

2 Comments

  1. Pingback: rdEditor: An open-source molecular editor based using Python, PySide2 and RDKit | Cheminformania

  2. Great Article!

    Thank you so much!

Leave a Comment

Your email address will not be published.

*
*