Using Python Qt
The same DNA sequence GUI example will be shown again, but using Qt-based libraries
to compare and contrast with the Tkinter system. There are currently several choices for a
Python-based Qt graphics library, PyQt4, PyQt5 and PySide, though PyQt5 only arrived
after most of this book was written. For the most part these are almost identical and
Python code that works in one will work with the other with only minor adjustments. For
the example we will use PySide because we feel it has a more Pythonic
7
way of dealing
with GUI signals (widget callbacks etc.), and also less restrictive licence conditions.
Unfortunately PyQt5 arrived too late to be considered in this book, though this is the
system the authors would use in the future, given that it has the most active development
and
the
future
of
PySide
is
somewhat
uncertain.
See
http://www.cambridge.org/pythonforbiology
for PySide and PyQt download and install
instructions, as well as links to full documentation.
Compared to Tk the Qt libraries for Python have a larger variety of widgets, although
we will only use a few in the example. Also, Qt naturally exposes a greater variety of
signals, although Tk can be expanded somewhat by the use of .bind() calls, to connect
events to widgets. For example, a Tkinter.Button is generally only used with a single
callback for when the button is pressed. The nearest Qt equivalent, QtGui.QPushButton,
automatically comes with clicked, pressed, released and toggled signals.
Compared to other libraries used in this book the Python bindings to Qt feel a little
different, because of the way the underlying C++ code is wrapped. For example, most Qt
objects are created with few initial arguments, and the object is configured with various
calls
after
it
is
created.
Also
nomenclature
like
‘QtCore.Qt.AlignLeft |
QtCore.Qt.AlignRight’ can seem a bit unfriendly at first. Here the vertical line is the
logical OR operator and is simply a means of combining the two options for left and right
alignment, which are represented as separate bits in a binary number (a typical C++ style
of doing things). This example, albeit using binary, is actually just equivalent to 1 + 2 in
Python.
For the GUI example we make the initial imports. If PyQt4 is used instead the connect()
calls in the class must be adjusted, as mentioned below. Qt is separated into discrete
modules which relate to different aspects of the system and here we import the required
QtCore and QtGui.
import re
from PySide import QtCore, QtGui # or from PyQt4
from Sequences import proteinTranslation, STANDARD_GENETIC_CODE
The SequenceQtGui definition is a subclass of the basic QtGui.QWidget and when we
initialise this class we also initialise the superclass for self. Although QWidget is more
directly comparable to Tk used earlier, the programmer can also consider using
QMainWindow because this comes prepared with slots for a main menu, tool bar and
status bar etc.
class SequenceQtGui(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self, parent=None)
In Qt it is fairly common to use simple horizontal and vertical layouts to arrange sub-
widgets in a window, but here we will use a grid, as in the previous example. Rather than
the grid being inbuilt into the widget system we need to make a layout object,
QGridLayout, and say that this belongs to self, i.e. it is the layout for the main window.
grid = QtGui.QGridLayout(self)
The layout object grid can then be used to configure the size behaviour of the rows and
columns. As before column 5, row 1 and row 4 are set to expand with weight 1.
grid.setColumnStretch(5, 1)
grid.setRowStretch(1, 1)
grid.setRowStretch(4, 1)
As an equivalent to Tkinter.EW, to refer to the left and right edges of a grid cell, we
define leftRight for later use by combining the binary Qt.AlignLeft and Qt.AlignRight
options.
leftRight = QtCore.Qt.AlignLeft | QtCore.Qt.AlignRight
Next we make the actual sub-widgets. A QtGui.QLabel is constructed, which is the
equivalent of Tkinter.Label. Instead of a simple label the QtGui.QGroupBox could be
used, which provides a title and a border to group sub-widgets. Once the widget is made it
is then added to the grid layout at position 0,0; the first row and column.
self.label1 = QtGui.QLabel(text='Enter 1-Letter DNA Sequence:',
parent=self)
grid.addWidget(self.label1, 0, 0)
Next to be constructed is the upper text box. This will be from the
QtGui.QPlainTextEdit class (there is also QTextEdit which supports rich text mark-up).
This goes in the next row of the grid (1,0) and we include the row and column span
arguments (1,6) to cover one row and six columns. The align argument dictates how the
widget sticks to the grid sides. Note that compared to the Tk equivalent the Qt text box has
more functionality, e.g. there is a context menu (right mouse click) with editing options,
which we do not use here.
self.seqTextBox = QtGui.QPlainTextEdit(parent=self)
grid.addWidget(self.seqTextBox, 1, 0, 1, 6, align=leftRight)
The QtGui.QPushButton class is used for simple buttons. Unlike Tk, where we just
specify the callback for the button with command, for Qt we have to select which kind of
user action we want it to respond to. Here we want the callback to be triggered from the
clicked action (technically a slot in Qt). The connecting of an action slot to a callback
function is usually done differently for PyQt4 and PySide libraries. For this PySide
example the self.clearButton.clicked object represents the button clicking, and this has a
connect() function to link it to whichever callback function we desire. Thus the button
click calls self.clearSeq. A comment is included below to illustrate the equivalent for
PyQt4. This involves actually creating a SIGNAL object using a text string (not very
Pythonic) and then the connect() call to link the target of the signal to the function comes
from a Qt widget (self in this case).
8
self.clearButton = QtGui.QPushButton(text='Clear', parent=self)
self.clearButton.clicked.connect(self.clearSeq)
grid.addWidget(self.clearButton, 2, 0)
#PyQt4 uses:
#self.connect(self, QtCore.SIGNAL('clicked()'), self.clearSeq)
The other buttons are made in a similar way, and placed in separate grid columns:
self.loadButton = QtGui.QPushButton(text='Load FASTA', parent=self)
self.loadButton.clicked.connect(self.loadFasta)
grid.addWidget(self.loadButton, 2, 1)
self.transButton = QtGui.QPushButton(text='Translate', parent=self)
self.transButton.clicked.connect(self.seqTranslate)
grid.addWidget(self.transButton, 2, 2)
self.compButton = QtGui.QPushButton(text='Composition', parent=self)
self.compButton.clicked.connect(self.seqComposition)
grid.addWidget(self.compButton, 2, 3)
self.findButton = QtGui.QPushButton(text='Find:', parent=self)
self.findButton.clicked.connect(self.seqFind)
grid.addWidget(self.findButton, 2, 4)
The last widget in row 2 is the small text box for the user to enter a query DNA
sequence. The class used is QtGui.QLineEdit and we will not connect any callback to the
widget, although we could use returnPressed or editingFinished action slots to do
something when the user changes the text.
self.findEntry = QtGui.QLineEdit(parent=self)
grid.addWidget(self.findEntry, 2, 5)
Lastly we add the second label, the lower text area for output and the quit button (which
is connected to the inbuilt QWidget.destroy()). Note that in order to configure the text
widget, in this case to say how the text wraps around at the end of a line, the function
.setLineWrapMode() is used after the object is created.
self.label2 = QtGui.QLabel(text='Text output:', parent=self)
grid.addWidget(self.label2, 3, 0, 1, 6)
self.outTextBox = QtGui.QPlainTextEdit(parent=self)
self.outTextBox.setLineWrapMode(QtGui.QPlainTextEdit.NoWrap)
grid.addWidget(self.outTextBox, 4, 0, 1, 6, align=leftRight)
self.closeButton = QtGui.QPushButton(self, text='Quit')
self.closeButton.clicked.connect(self.destroy)
grid.addWidget(self.closeButton, 5, 5, align=leftRight)
The next functions after the initialisation involve clearing and updating the text areas.
This is a little simpler than with Tk, given that .clear() is inbuilt in to
QtGui.QPlainTextEdit. As you might expect, the names and the actions of the functions
differ between the two systems. For example, setPlainText() replaces all the text, so we
don’t have to clear first. Likewise, using appendPlainText()does not need newline
characters to be added to text.
def clearSeq(self):
self.seqTextBox.clear()
def setSequence(self, text):
self.seqTextBox.setPlainText(text)
def showText(self, text):
self.outTextBox.appendPlainText(text)
def clearOutput(self):
self.outTextBox.clear()
Fetching the DNA sequence is simple and employs .toPlainText() rather than Tk’s
.get(). The tidying of the DNA sequence is done as described earlier.
def getSequence(self):
seq = self.seqTextBox.toPlainText()
seq = re.sub('\s+','',seq)
seq = seq.upper()
return seq
The loadFasta function uses the compound widget QtGui.QFileDialog to get the file
name. This looks somewhat prettier than the Tk equivalent, and will have a style that
represents the native operating system. Note that getOpenFileName does not give back a
file object. Rather it gives back the location of the file, so we use open() to get the actual
file object that BioPython works with.
def loadFasta(self):
msg = 'Choose a FASTA file'
filePath, filtr = QtGui.QFileDialog.getOpenFileName(self, msg)
if filePath: # Something was selected
fileObj = open(filePath, 'rU')
from Bio import SeqIO
for entry in SeqIO.parse(fileObj, 'fasta'):
self.setSequence(str(entry.seq))
break
fileObj.close()
The two functions seqTranslate()and seqComposition() are unchanged compared to the
Tk equivalent, so we will not repeat them. However, it is worth pointing out that the lack
of any difference shows that we have separated the more graphical functions from the
scientific functions, which is generally a good plan.
For finding a query DNA sub-sequence the seqFind function is similar to before. The
differences are that the query entry box uses .text() not .get() and that we have naturally
swapped to QtGui.QMessageBox to display warnings to the user. One extra thing that has
been included here is self.seqTextBox.find(query). This highlights successive instances of
the query string in the upper text area. Although Tkinter.Text has a search() function this
merely finds text, but does not highlight it.
def seqFind(self):
self.clearOutput()
query = self.findEntry.text()
query = query.strip()
if not query:
QtGui.QMessageBox.warning(self, "Warning",
"Search sequence was blank")
return
seq = self.getSequence()
self.seqTextBox.find(query)
if query in seq:
text = "Locations of %s" % (query)
self.showText(text)
win = len(query)
for i in range(len(seq)-win):
if seq[i:i+win] == query:
self.showText(' %d' % i)
else:
text = "Sub-sequence %s not found" % (query)
self.showText(text)
To test the SequenceQtGui class code we have a little work to do. A Qt graphical
interface only works if there is a QApplication instance to control the flow of the GUI
program and carry its main settings (this does some of the job that the Tk root does). The
application object is accordingly made and assigned to the app variable. The window
object is made using the SequenceQtGui class described above and will become the top-
level graphical object. The .show() call is required to actually see something; although this
might seem a little tedious it is really handy to be able to control the visibility of widgets
(all QWidgets have .show()). The final line looks a little odd, but is merely running the
QApplication, and is equivalent to Tk.mainloop(). The function is called ‘exec_’ not
‘exec’ because the latter is an inbuilt keyword of Python. This call is wrapped by
sys.exit(), which is required for the program to exit cleanly when done.
if __name__ == '__main__':
import sys
app = QtGui.QApplication(['Qt Sequence Example'])
window = SequenceQtGui()
window.show()
sys.exit(app.exec_())
Do'stlaringiz bilan baham: |