Making Python GUIs

Reading time ~15 minutes

Sometimes I want to make a simple (or complex) graphical user interface (GUI) for exploratory data analysis. I use Python, but there are probably better ways to do this. This tutorial serves as a basic walkthrough on how to produce a Python GUI that contains an interactive matplotlib figure.

A caveat before we start: There are a few different options available for GUIs in Python and elsewhere (Tkinter, Qt, Enthought Traits, etc). I’ve tried all of these at some point, but you shouldn’t take my word on what is described below as being “the best way” to do things; this is just what I have found to work best for me. Here is my setup (e.g., things you may need to make this work):

  1. Anaconda Python installation. Both Python 2.7 and 3.x will work, but with 3.x you may need to do conda install pyside after you have installed Anaconda.

  2. Qt 4.8.7

If you have these requirements already and just want to test the GUI, here is the “TL;DR”:

git clone git@github.com:andycasey/pyside-intro.git
cd pyside-intro/
python my_gui.py

Let’s create a GUI

If you have Qt installed, then you should also have Qt Designer. Load it and create a new widget:

pyside-1

On the left you can see widgets, which you can drag and drop the background. Double-click buttons or text to edit them.

pyside-2

In Qt we use layouts and size policies to make things sit in the right spot. First let’s put our buttons into a “horizontal layout” and drop a horizontal spacer before the last button:

pyside-3

To put everything in the right spot (eventually..), right-click on the parent window and give it a “vertical layout”:

pyside-4

pyside-5

Now let’s add a default “widget” at the top, which will later become our matplotlib figure.

pyside-6

Let’s set the size policies so that the widget has MinimumExpanding policy for the vertical direction.

pyside-7

Produce Python code from your widget

In the Qt Designer, save your widget to a file with a .ui extension: my_gui.ui. Now from the terminal we will create a Python script called my_gui.py from our my_gui.ui file:

pyuic4 my_gui.ui > my_gui.py

The my_gui.py file looks like this:

# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file 'my_gui.ui'
#
# Created by: PyQt4 UI code generator Unknown
#
# WARNING! All changes made in this file will be lost!

from PyQt4 import QtCore, QtGui

try:
    _fromUtf8 = QtCore.QString.fromUtf8
except AttributeError:
    def _fromUtf8(s):
        return s

try:
    _encoding = QtGui.QApplication.UnicodeUTF8
    def _translate(context, text, disambig):
        return QtGui.QApplication.translate(context, text, disambig, _encoding)
except AttributeError:
    def _translate(context, text, disambig):
        return QtGui.QApplication.translate(context, text, disambig)

class Ui_Form(object):
    def setupUi(self, Form):
        Form.setObjectName(_fromUtf8("Form"))
        Form.resize(640, 480)
        self.verticalLayout = QtGui.QVBoxLayout(Form)
        self.verticalLayout.setObjectName(_fromUtf8("verticalLayout"))
        self.widget = QtGui.QWidget(Form)
        sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.MinimumExpanding)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(self.widget.sizePolicy().hasHeightForWidth())
        self.widget.setSizePolicy(sizePolicy)
        self.widget.setObjectName(_fromUtf8("widget"))
        self.verticalLayout.addWidget(self.widget)
        self.horizontalLayout = QtGui.QHBoxLayout()
        self.horizontalLayout.setObjectName(_fromUtf8("horizontalLayout"))
        self.pushButton = QtGui.QPushButton(Form)
        self.pushButton.setObjectName(_fromUtf8("pushButton"))
        self.horizontalLayout.addWidget(self.pushButton)
        self.pushButton_3 = QtGui.QPushButton(Form)
        self.pushButton_3.setObjectName(_fromUtf8("pushButton_3"))
        self.horizontalLayout.addWidget(self.pushButton_3)
        spacerItem = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)
        self.horizontalLayout.addItem(spacerItem)
        self.pushButton_2 = QtGui.QPushButton(Form)
        self.pushButton_2.setObjectName(_fromUtf8("pushButton_2"))
        self.horizontalLayout.addWidget(self.pushButton_2)
        self.verticalLayout.addLayout(self.horizontalLayout)

        self.retranslateUi(Form)
        QtCore.QMetaObject.connectSlotsByName(Form)

    def retranslateUi(self, Form):
        Form.setWindowTitle(_translate("Form", "Form", None))
        self.pushButton.setText(_translate("Form", "Show data", None))
        self.pushButton_3.setText(_translate("Form", "Change color", None))
        self.pushButton_2.setText(_translate("Form", "OK", None))

(I should note here that you can actually just import .ui files directly into Python through the QUiLoader class, but I have not shown that here just so you can see the “bare bones” of what the Python code looks like.)

Clean up the automatically-generated code

The code in our my_gui.py file is not quite ready yet. This is because the pyuic4 function is not perfectly suited for our needs, so we will have to change some of the code that it has generated. Specifically we should replace the PyQt imports to PySide, remove the translation function, and re-name our widgets. To save time, below is the complete updated file (you can check the line-by-line differences yourself). You should be able to run this code and show a blank widget by just typing python my_gui.py from the terminal.

#!/usr/bin/env python
# -*- coding: utf-8 -*-

""" My awesome GUI! """

from __future__ import (division, print_function, absolute_import,
                        unicode_literals)


from PySide import QtCore, QtGui


class MyGUI(QtGui.QDialog):

    def __init__(self, **kwargs):
        super(MyGUI, self).__init__(**kwargs)


        self.setGeometry(600, 480, 600, 480)
        self.move(QtGui.QApplication.desktop().screen().rect().center() \
            - self.rect().center())
        self.setWindowTitle("My awesome GUI")

        vertical_layout = QtGui.QVBoxLayout(self)
        self.figure_widget = QtGui.QWidget(self)
        sizePolicy = QtGui.QSizePolicy(
            QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.MinimumExpanding)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(
            self.figure_widget.sizePolicy().hasHeightForWidth())
        self.figure_widget.setSizePolicy(sizePolicy)
        vertical_layout.addWidget(self.figure_widget)

        horizontal_layout = QtGui.QHBoxLayout()
        self.btn_show_data = QtGui.QPushButton(self)
        self.btn_show_data.setText("Show data")
        horizontal_layout.addWidget(self.btn_show_data)

        self.btn_change_color = QtGui.QPushButton(self)
        self.btn_change_color.setText("Change color")
        horizontal_layout.addWidget(self.btn_change_color)
        spacer = QtGui.QSpacerItem(
            40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)
        horizontal_layout.addItem(spacer)
        self.btn_ok = QtGui.QPushButton(self)
        self.btn_ok.setText("OK")
        horizontal_layout.addWidget(self.btn_ok)
        vertical_layout.addLayout(horizontal_layout)

        return None


if __name__ == "__main__":

    import sys

    app = QtGui.QApplication(sys.argv)
    window = MyGUI()
    window.exec_()

Integrate matplotlib functionality

Now we are going to add a matplotlib widget and get Python code to be executed when we click buttons. First create a file called mpl.py that contains the following code:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

""" Functionality to use matplotlib figures in PySide GUIs. """

from __future__ import (division, print_function, absolute_import,
                        unicode_literals)

import os
import matplotlib
from warnings import simplefilter

# Ignore warnings from matplotlib about fonts not being found.
simplefilter("ignore", UserWarning)

# Load our matplotlibrc file.
matplotlib.rc_file(os.path.join(os.path.dirname(__file__), "matplotlibrc"))

from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure

from PySide import QtCore, QtGui


class MPLWidget(FigureCanvas):
    """
    A widget to contain a matplotlib figure.
    """

    def __init__(self, parent=None, toolbar=False, tight_layout=True,
        autofocus=False, background_hack=True, **kwargs):
        """
        A widget to contain a matplotlib figure.

        :param autofocus: [optional]
            If set to `True`, the figure will be in focus when the mouse hovers
            over it so that keyboard shortcuts/matplotlib events can be used.
        """
        super(MPLWidget, self).__init__(Figure())
        
        self.figure = Figure(tight_layout=tight_layout)
        self.canvas = FigureCanvas(self.figure)
        self.canvas.setParent(parent)

        # Focus the canvas initially.
        self.canvas.setFocusPolicy(QtCore.Qt.WheelFocus)
        self.canvas.setFocus()

        self.toolbar = None 

        if autofocus:
            self._autofocus_cid = self.canvas.mpl_connect(
                "figure_enter_event", self._focus)


        self.figure.patch.set_facecolor([v/255. for v in 
            self.palette().color(QtGui.QPalette.Window).getRgb()[:3]])

        return None


    def _focus(self, event):
        """ Set the focus of the canvas. """
        self.canvas.setFocus()

And create a matplotlibrc file in the same folder, which lets you style your figures.

backend     : qt4agg
backend.qt4 : PySide

axes.titlesize  : 9.0
axes.labelsize  : 9.0 

xtick.labelsize : 9.0
ytick.labelsize : 9.0

legend.fontsize : 9.0  

font.family     : serif  
font.serif      : Computer Modern Roman  

text.antialiased : True
text.dvipnghack  : None

figure.figsize  : 7.3, 4.2  
figure.dpi      : 80

Add matplotlib functionality to our widget

Now let’s change our Widget to be a matplotlib widget (MPLWidget). In my_gui.py add import mpl at the top of the file and change this:

        self.figure_widget = QtGui.QWidget(self)

To this:

        self.figure_widget = mpl.MPLWidget(tight_layout=True)

And we’ll set up the axes in the end of our __init__ function:

        # Create a matplotlib axes.
        ax = self.figure_widget.figure.add_subplot(111)
        ax.set_xlabel(r"$x$")
        ax.set_ylabel(r"$y$")
        ax.scatter([], [])

Connect signals to widgets All widgets have signals that they emit when something happens in the GUI. For example, when a button is clicked, it emits a clicked signal. We just need to conncet these signals to a function we have written. Here’s one example:

        # Connect the signals.
        self.btn_ok.clicked.connect(self.close)
        self.btn_show_data.clicked.connect(self.show_data)


    def show_data(self):
        """ A function to show data. """

        x = np.random.uniform(size=100)
        y = np.random.uniform(size=100)

        self.figure_widget.figure.axes[0].collections[0].set_offsets(
            np.array([x, y]).T)
        self.figure_widget.draw()

Putting this all together, here is our completed my_gui.py file:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

""" My awesome GUI! """

from __future__ import (division, print_function, absolute_import,
                        unicode_literals)

import numpy as np

from PySide import QtCore, QtGui

import mpl


class MyGUI(QtGui.QDialog):

    def __init__(self, **kwargs):
        super(MyGUI, self).__init__(**kwargs)

        self.setGeometry(600, 480, 600, 480)
        self.move(QtGui.QApplication.desktop().screen().rect().center() \
            - self.rect().center())
        self.setWindowTitle("My awesome GUI")

        vertical_layout = QtGui.QVBoxLayout(self)
        self.figure_widget = mpl.MPLWidget(tight_layout=True)
        sizePolicy = QtGui.QSizePolicy(
            QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.MinimumExpanding)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(
            self.figure_widget.sizePolicy().hasHeightForWidth())
        self.figure_widget.setSizePolicy(sizePolicy)
        vertical_layout.addWidget(self.figure_widget)

        horizontal_layout = QtGui.QHBoxLayout()
        self.btn_show_data = QtGui.QPushButton(self)
        self.btn_show_data.setText("Show data")
        horizontal_layout.addWidget(self.btn_show_data)

        self.btn_change_color = QtGui.QPushButton(self)
        self.btn_change_color.setText("Change color")
        horizontal_layout.addWidget(self.btn_change_color)
        spacer = QtGui.QSpacerItem(
            40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)
        horizontal_layout.addItem(spacer)
        self.btn_ok = QtGui.QPushButton(self)
        self.btn_ok.setText("OK")
        horizontal_layout.addWidget(self.btn_ok)
        vertical_layout.addLayout(horizontal_layout)

        # Create a matplotlib axes.
        ax = self.figure_widget.figure.add_subplot(111)
        ax.set_xlabel(r"$x$")
        ax.set_ylabel(r"$y$")
        ax.set_xlim(0, 1)
        ax.set_ylim(0, 1)
        ax.scatter([], [])

        # Connect the signals.
        self.btn_ok.clicked.connect(self.close)
        self.btn_show_data.clicked.connect(self.show_data)
        self.btn_change_color.clicked.connect(self.change_color)

        # Create a matplotlib event listener.
        self.figure_widget.mpl_connect("button_press_event", self.button_press)

        return None


    def show_data(self):
        """ A function to show data. """

        x = np.random.uniform(size=100)
        y = np.random.uniform(size=100)

        self.figure_widget.figure.axes[0].collections[0].set_offsets(
            np.array([x, y]).T)
        self.figure_widget.draw()


    def change_color(self):
        """ Change the color of the data points. """

        colors = "rgbmky"
        self.figure_widget.figure.axes[0].collections[0].set_color(
            colors[np.random.randint(0, len(colors))])
        self.figure_widget.draw()

        return None


    def button_press(self, event):
        """ A function for when a button has been pressed in the figure. """

        print("Button press event!", event)


if __name__ == "__main__":

    import sys

    app = QtGui.QApplication(sys.argv)
    window = MyGUI()
    window.exec_()

Which should look something like this:

pyside-8

That’s it!

Hiatus

End hiatus.… Continue reading

Best and Brightest EMP Stars

Published on September 18, 2014

Binary Population Properties

Published on July 11, 2014