Thursday, October 22, 2015

Building Better Python User Interfaces with PyQt and pyqtgraph

2 ways to build the same user interface

This post illustrates 2 ways to build the same user interface. The first is the 'introductory' level user interface where each component and functionality is in one script. This is fine for a simple user interface like this one. But as one progresses into much larger UI's with many widgets (graphics windows, dock widgets, tables, lists, etc.), the need to separate the components from the functionality becomes critical. So the second way to build the UI is constructed with this in mind.


The user interface we are going to build is a simple one with a plot of a sinc function and a slider that allows the user to change the width of the coda (and the dominant frequency).

What is the function plotted?

$$f(t) = \frac{sin(\alpha \pi t)}{\alpha \pi t}$$

The slider controls the alpha parameter. In the code, this is 'self.a'.

Method 1: a single script to build the UI:

CODE: SincFunction.py

__author__ = 'Anthony Torlucci'
__version__ = '0.0.1'

# import python standard modules
import sys

# import 3rd party libraries
from PyQt4 import QtCore, QtGui
import numpy
import pyqtgraph

#import local python

class Window(QtGui.QMainWindow):
    def __init__(self, parent=None):
        super(Window, self).__init__(parent)
        self.setWindowTitle('Simple Sinc Function UI')
        self.window = pyqtgraph.GraphicsWindow()
        self.window.setBackground('w')
        self.sinc_plot = self.window.addPlot(title='Sinc Function')
        self.sinc_plot.showGrid(x=True, y=True, alpha=0.5)
        self.setCentralWidget(self.window)
        # add a slider to change the coda of the sinc function
        self.slider = QtGui.QSlider(QtCore.Qt.Horizontal)
        # initialize the time axis (this will not change)
        self.t = numpy.linspace(-0.500, 0.500, num=1000, endpoint=True)
        # a will change the coda of the sinc function
        self.a = 1
        #
        # add the menubar with the method createMenuBar()
        self.createMenuBar()
        # add the dock widget with the method createDockWidget()
        self.createDockWidget()
        #
        # first set the default value to a
        self.slider.setValue(self.a)
        # when the slider is changed, it emits a signal and sends an integer value
        # we send that value to a method called slider value changed that updates the value a
        self.slider.valueChanged.connect(self.sliderValueChanged)
        # finally draw the curve



    def createMenuBar(self):
        # file menu actions
        exit_action = QtGui.QAction('&Exit', self)
        exit_action.triggered.connect(self.close)
        # create an instance of menu bar
        menubar = self.menuBar()
        # add file menu and file menu actions
        file_menu = menubar.addMenu('&File')
        file_menu.addAction(exit_action)

    def createDockWidget(self):
        my_dock_widget = QtGui.QDockWidget()
        my_dock_widget.setObjectName('Control Panel')
        my_dock_widget.setAllowedAreas(QtCore.Qt.TopDockWidgetArea | QtCore.Qt.BottomDockWidgetArea)
        # create a widget to house user control widgets like sliders
        my_house_widget = QtGui.QWidget()
        # every widget should have a layout, right?
        my_house_layout = QtGui.QVBoxLayout()
        # add the slider initialized in __init__() to the layout
        my_house_layout.addWidget(self.slider)
        # apply the 'house' layout to the 'house' widget
        my_house_widget.setLayout(my_house_layout)
        # set the house widget 'inside' the dock widget
        my_dock_widget.setWidget(my_house_widget)
        # now add the dock widget to the main window
        self.addDockWidget(QtCore.Qt.BottomDockWidgetArea, my_dock_widget)

    def sliderValueChanged(self, int_value):
        self.a = int_value
        self.drawCurve()

    def drawCurve(self):
        self.sinc_plot.clear()
        self.my_sinc = numpy.sin(self.a * numpy.pi * self.t) / (self.a * numpy.pi * self.t)
        self.sinc_plot.plot(self.t, self.my_sinc, pen='b')

def main():
    app = QtGui.QApplication(sys.argv)
    app.setApplicationName('Simple Sinc Function UI')
    window = Window()
    window.show()
    app.exec_()

if __name__ == '__main__':
    main()

What does the code do?

If you ran the code above, you will see an empty plot window at first. Move the slider and the plot appears. As you move the slider to the right, the alpha parameter (self.a) increases and the function becomes narrower (an increase in the dominant frequency).

Method 2: Breaking up the UI into components / objects

First, a disclaimer. I am a self taught, intermediate level python programmer. My primary area of study is computational geophysics and I began programming to better understand the concepts. That being said, I would like to share a little bit of what I have learned building some user interfaces that illustrate basic concepts in computational geophysics.

Below is a tree structure of the code:

  • /ui
    • __init__.py
    • ui_dock_widget.py
    • ui_main_window.py
    • ui_menubar.py
  • __init__.py
  • main.py
  • main_window.py

We start with the main function. This is required for all pyqt apps. For all my simple user interfaces, I re-use this exact same code. The only thing I have to change is the application name.

CODE: main.py

__author__ = 'Anthony Torlucci'
__version__ = '0.0.1'

# import python standard modules
import sys

# import 3rd party libraries
from PyQt4.QtGui import QApplication

# import local python
from main_window import Window

def main():
    app = QApplication(sys.argv)
    app.setApplicationName('Simple Sinc Function UI')
    window = Window()
    window.show()
    app.exec_()

if __name__ == '__main__':
    main()

In the code above, we import Window from main_window. Before looking at main_window.py, lets look at the ui components which are constructed in ui_main_main_window and ui_dock_widget.

CODE: ui_main_window.py

__author__ = 'Anthony Torlucci'
__version__ = '0.0.1'

# import python standard modules


# import 3rd party libraries
from PyQt4 import QtGui, QtCore
import pyqtgraph

# import local python
from ui_menubar import Ui_Menubar

class Ui_MainWindow(QtCore.QObject):

    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        self.ui_central_widget = QtGui.QWidget(MainWindow)
        #
        self.ui_window = pyqtgraph.GraphicsWindow()
        self.ui_window.setBackground('w')
        self.ui_sinc_plot = self.ui_window.addPlot(title='Sinc Function')
        self.ui_sinc_plot.showGrid(x=True, y=True, alpha=0.5)
        # LAYOUT
        self.ui_central_layout = QtGui.QVBoxLayout()
        self.ui_central_layout.addWidget(self.ui_window)
        self.ui_central_widget.setLayout(self.ui_central_layout)
        #
        MainWindow.setCentralWidget(self.ui_central_widget)
        #
        # MENUBAR
        self.ui_menubar = Ui_Menubar()
        self.ui_menubar.setupUi(self)

The components of our user interface are a graphics window for plotting the function (from which we will use pyqtgraph), and a slider. The graphics window (self.ui_window) will be the central component (and for this simple case, the only component) of our main window. Every main window in qt has a central widget. What I have done here is create a widget, gave it a layout, and added the graphics window to that widget. If you would like to add more windows and widgets, it's easy. Just create an instance of the widget here and add it to the central widget layout. Next, we add a menubar to our main window.


CODE: ui_menubar.py

__author__ = 'Anthony Torlucci'
__version__ = '0.0.1'

# import python standard modules

# import 3rd party libraries
from PyQt4 import QtGui

# import local python

class Ui_Menubar(QtGui.QMenuBar):
    def __init__(self, parent=None):
        super(Ui_Menubar, self).__init__(parent)
        self.exit_action = QtGui.QAction(QtGui.QIcon('exit.png'), '&Exit', self)

    def setupUi(self, Ui_Menubar):
        self.ui_menubar = QtGui.QMenuBar()
        #
        # file menu actions:
        # add file menu and file menu actions
        self.file_menu = self.ui_menubar.addMenu('&File')
        self.file_menu.addAction(self.exit_action)

This menubar only has one menu 'File' and one menu action for that menu 'Exit'. The reason it is constructed this way is so I can add many more menus and actions and keep them contained in this file. We would still have to add the functionality to the main window, but that would be only a few lines of code. Next, lets look at the dock widget that holds the slider.


CODE: ui_dock_widget.py

__author__ = 'Anthony Torlucci'
__version__ = '0.0.1'

# import python standard modules

# import 3rd party libraries
from PyQt4 import QtCore, QtGui

# import local python scripts

class Ui_ControlsBoxDockWidget(QtCore.QObject):

    def setupUi(self, ControlsBox):
        ControlsBox.setObjectName('Controls Box')
        self.ui_controls_box_widget = QtGui.QDockWidget(ControlsBox)
        self.ui_controls_box_widget.setAllowedAreas(QtCore.Qt.BottomDockWidgetArea)
        #
        self.slider = QtGui.QSlider(QtCore.Qt.Horizontal)
        self.slider.setRange(1,100) # this avoids dividing by zero
        #
        self.house_layout = QtGui.QVBoxLayout()
        self.house_layout.addWidget(self.slider)
        self.house_widget = QtGui.QWidget()
        self.house_widget.setLayout(self.house_layout)
        #
        self.ui_controls_box_widget.setWidget(self.house_widget)

So I'm not sure if this is the best way to do it, but I always create a 'house' widget. This is the widget that will hold all my other widgets. The only widget here is a slider. But if I wanted to create a dial, labels, or any other widgets, I could just add them to my house widget layout. You may have to change to a grid layout and add spacers, but all that is applied and contained within the house widget. Then just set the dock widget's widget to house widget.

Now for the meat and potatoes. We give our widgets functionality in main_window.py. That's it. Notice we create all of our main window components in 2 lines: self.ui = Ui_MainWindow() and self.ui.setupUi(self). Same for the menubar and the dock widget. I think everything else is self explanatory.

CODE: main_window.py

__author__ = 'Anthony Torlucci'
__version__ = '0.0.1'

# import python standard modules


# import 3rd party libraries
from PyQt4 import QtGui, QtCore
import numpy
import pyqtgraph

# import local python
from ui.ui_main_window import Ui_MainWindow
from ui.ui_dock_widget import Ui_ControlsBoxDockWidget

class Window(QtGui.QMainWindow):
    def __init__(self, parent=None):
        super(Window, self).__init__(parent)
        self.setWindowTitle('Simple Sinc Function UI')
        #
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)
        #
        self.mbar = self.setMenuBar(self.ui.ui_menubar.ui_menubar)
        self.ui.ui_menubar.exit_action.triggered.connect(self.close)
        #
        self.dck_widget = Ui_ControlsBoxDockWidget()
        self.dck_widget.setupUi(self)
        self.addDockWidget(QtCore.Qt.BottomDockWidgetArea, self.dck_widget.ui_controls_box_widget)
        #
        # DATA
        # initialize the time axis (this will not change)
        self.t = numpy.linspace(-0.500, 0.500, num=1000, endpoint=True)
        # a will change the coda of the sinc function
        self.a = 1
        #
        # CONTROLS
        # first set the default value to a
        self.dck_widget.slider.setValue(self.a)
        # when the slider is changed, it emits a signal and sends an integer value
        # we send that value to a method called slider value changed that updates the value a
        self.dck_widget.slider.valueChanged.connect(self.sliderValueChanged)
        # finally draw the curve
        self.drawCurve()

    def sliderValueChanged(self, int_value):
        self.a = int_value
        self.drawCurve()

    def drawCurve(self):
        self.ui.ui_sinc_plot.clear()
        self.my_sinc = numpy.sin(self.a * numpy.pi * self.t) / (self.a * numpy.pi * self.t)
        self.ui.ui_sinc_plot.plot(self.t, self.my_sinc, pen='b')

I hope this helps some of you learning to develop with python, pyqt, and pyqtgraph to build your own user interfaces to explore computational science. Good luck!

3 comments:

  1. Very nice tutorial for a python beginner like me. Many thanks!

    ReplyDelete
  2. This is a good starting point for me. How do you display the value of slider?

    ReplyDelete