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!