Sunday, June 24, 2012

Adding a PyQt GUI to your program

I am all in for command line interface (CLI) programs. I'll admit that. They work in terminals. You can run them without an X Server and usually option flags are easy to use and obvious*. But sometimes I'd like to just hit a button. Click and magic happens. No more 200 character command line statement to get the ball rolling. A good example of this is from my colleague Anders Christensen who made a graphical user interface (GUI) for the Phaistos program.

In this post, we'll take the first small steps towards actually making a GUI for a simulation to run the ideal gas in and not just watch some numbers printed in a terminal. I'll be using Qt4 and the PyQt4 bindings. If you have a fairly recent linux distribution, you can apt-get install yourself to success very fast. Remember to also install the Qt-Designer app so you visually can layout your controls.

We'll start out simple by constructing buttons, textedits and a frame. I use a regular QWidget for my main form. The end goal for a user is to push the 1) setup button to initialize particles and 2) push the run button and make the simulation run. The simulation (i.e. the particles) should be displayed in the frame via matplotlib. It looks like this



If you want the resulting simulator.ui code its listed as part the gist for this blog post.

To move on from here, there is a nifty little tool called pyuic4 which will convert your .ui file into a python class with the name Simulator_UI. I always just parse it down to a file called simulator_ui.py. The code is

$ pyuic4 simulator.ui > simulator_ui.py

The simulator_ui class contains the framework for the QWidget form we will be using. The most clever approach I could think of was to subclass the simulator_ui class to separate the "setting up the form" code with the "what happens when I push a button" code. The QWidget I'll use to represent the window the user will see and I call it Simulator. It imports the Simulator_UI class, subclasses it and sets up the GUI. The two buttons are hooked up to either initialize new particles or run for some steps

from PyQt4 import QtGui, QtCore
from simulator_ui import Ui_main
from particlecanvas import ParticleCanvas
class MainForm(QtGui.QMainWindow):
def __init__(self, parent=None):
QtGui.QWidget.__init__(self,parent)
self.ui = Ui_main()
self.ui.setupUi(self)
QtCore.QObject.connect(self.ui.btnSetup, QtCore.SIGNAL("clicked()"), self.on_setup_clicked)
QtCore.QObject.connect(self.ui.btnRun, QtCore.SIGNAL("clicked()"), self.on_run_clicked)
self.c = ParticleCanvas(self.ui.frame)
def on_setup_clicked(self):
self.c.reinitialize(int(self.ui.txtNPart.text()))
def on_run_clicked(self):
for i in range(int(self.ui.txtNSteps.text())):
self.c.move()
self.c.redraw()
view raw simulator.py hosted with ❤ by GitHub

The bread and butter of this application is the ParticleCanvas. Its tailored to draw and move the particles around. Look at its code

from numpy import array, where, abs
from numpy.random import random
from canvas import Canvas
class ParticleCanvas(Canvas):
def __init__(self,parent,dpi=100.0):
Canvas.__init__(self,parent,dpi)
self.pos = None
self.vel = None
def reinitialize(self, npart):
self.pos = 2.0*(random((2,npart))-0.5)
self.vel = 2.0*(random((2,npart))-0.5)
self.particle_plot = None
self.redraw()
def move(self):
dt = 0.01
# change velocity if they go beyond box
self.vel[where(abs(self.pos + dt*self.vel)>1.0)] *= -1.0
self.pos += dt*self.vel
def on_draw(self):
if self.pos is None or self.vel is None: return
if self.particle_plot is None:
self.particle_plot, = self.axes.plot(self.pos[0],self.pos[1], 'ro', animated=True)
self.particle_plot.set_xdata(self.pos[0])
self.particle_plot.set_ydata(self.pos[1])
self.axes.draw_artist(self.particle_plot)

You see that it subclasses the Canvas class which is my best bet on how one should make the code for fast blitting of a matplotlib canvas. A class which subclasses the Canvas class is only responsible for redrawing the actual plot (and do it fast). However,  I would gladly appreciate comments of you have a better way to implement it

from time import sleep
import matplotlib
import matplotlib.pyplot as plt
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
matplotlib.rcParams.update({'font.size': 8})
class Canvas(FigureCanvas):
def __init__(self,parent,dpi=100.0):
size = parent.size()
self.dpi = dpi
self.width = size.width() / dpi
self.height = size.height() / dpi
self.figure = Figure(figsize=(self.width, self.height), dpi=self.dpi, facecolor='w', edgecolor='k', frameon=False)
self.figure.subplots_adjust(left=0.05, bottom=0.05, right=0.95, top=0.95, wspace=None, hspace=None)
self.axes = self.figure.add_subplot(111)
self.axes.axis((-1,1,-1,1))
FigureCanvas.__init__(self, self.figure)
self.updateGeometry()
self.draw()
self.cc = self.copy_from_bbox(self.axes.bbox)
self.particle_plot = None
self.setParent(parent)
self.blit(self.axes.bbox)
def on_pre_draw(self):
self.restore_region(self.cc)
def on_draw(self):
raise NotImplementedError
def on_post_draw(self):
self.blit(self.axes.bbox)
sleep(0.005)
def redraw(self):
self.on_pre_draw()
self.on_draw()
self.on_post_draw()
view raw canvas.py hosted with ❤ by GitHub

Finally, the code that makes it all run is just my main executable which is defined like this

#!/usr/bin/env python
import sys
from PyQt4.QtGui import QApplication
from simulator import MainForm
def main():
app = QApplication(sys.argv)
form = MainForm()
form.show()
app.exec_()
if __name__ == "__main__":
main()
view raw main.py hosted with ❤ by GitHub

The final result looks like this

We now have a finished simulation GUI. However, the main problem is that the GUI and the simulation code is intimately hooked up so the GUI will freeze at some point. Fortunately, it leaves a new post on the horizon.

*I never understood anything of the find command, however.

1 comment: