PySide Recipes
This page covers some simple design patterns that can be used to create PySide gui's based on VisIt. For deployment of PySide applications, see PySide Application Deployment.
Main Application
You can create an application class that derives from QObject in order to house the logic for your application. We use QObject so our normal Python class can contain slot functions that we can call in response to widget signals. The example below shows a basic skeleton of what it looks like to create an application based on QObject, though widget creation is not shown.
class MyCustomApp(QObject):
def __init__(self):
super(MyCustomApp,self).__init__()
self.ui = None
self.resourcedir = ""
self.__parse_command_line()
self.__init_widgets()
def show(self):
self.ui.show()
self.ui.raise_()
def __parse_command_line(self):
i = 0
while i < len(sys.argv):
if sys.argv[i] == "-resourcedir":
self.resourcedir = sys.argv[i+1]
i = i + 1
i = i + 1
def __resource(self, filename):
if self.resourcedir != "":
return os.path.join(self.resourcedir, filename)
return filename
def __init_widgets(self):
# Create widgets here with the top level widget being self.ui
return
# Create and show our custom window.
main = MyCustomApp()
main.show()
Creating a Window From Qt Designer
Qt Designer is very useful in creating windows and it can save an XML description of the window called a UI file. It is possible to make your PySide application create its entire user interface based on the UI file that you made in Qt Designer. There are some important points to remember when creating your window in Qt designer:
- You will be accessing widgets by name by looking them up using the findChild method, so give your widgets memorable names.
- Pay attention to layout when you make your window so your window will be size independent.
- Use QWidget to stand in for visualization windows. You will be able to add real visualization windows under the QWidget that you added.
Let's fill in your application's __init_widgets method so it sets the self.ui widget based on the controls that were created by dynamically loading our user interface from a UI file.
def __init_widgets(self):
# Load the UI from a Qt designer file.
loader = QUiLoader()
file = QFile(self.__resource("myapp.ui"))
file.open(QFile.ReadOnly)
self.ui = loader.load(file, None)
file.close()
Window Icon
You can override VisIt's default window icon to further customize your application. You can create a 256x256 PNG file to contain your icon and then open it via QIcon and set the main window's windowIcon property. You can follow the same pattern for any other windows that you create. Be sure to distribute the myapp.png file when you bundle your application.
def __init_widgets(self):
# ... code to create self.ui (omitted) ...
self.icon = QIcon(self.__resource("myapp.png"))
self.ui.setWindowIcon(self.icon)
Showing windows
It is important to create a custom show() method for your application. This lets you not only show the main UI but also raise it up above other application windows. Finally, since you will have created all of the VisIt vis windows and embedded them by this point, you can call ShowAllWindows() to tell VisIt's viewer that windows are mapped and on the screen. If you skip this step then things like playing animation will not work.
class MyCustomApp(QObject):
def show(self):
self.ui.show()
self.ui.raise_()
ShowAllWindows() # needed for animation
In Qt Designer, menus are created using QActions. Typically, the action's name will be based on the text that you typed for the action's menu text. This gives rise to action object names like actionQuit, or actionSave_Image.
In order to customize or manipulate the widgets that we created via our UI file, we can call the findChild method on self.ui to look for QActions that have specific names.
# Connect the quit action
actionQuit = self.ui.findChild(QAction,"actionQuit")
actionQuit.triggered.connect(self.close)
Note that we obtained a handle to the actionQuit action and then we connected its triggered signal to our application's close method. The close method looks like this:
def close(self):
# sys.exit(0) is not working because of some interpreter lock or timer issues on exit.
# Force kill instead.
if sys.platform == "win32":
# This works on Windows
os.kill(os.getpid(), -9)
else:
# This works on Mac/Linux
os.system("kill -9 %d" % os.getpid())
Adding visualization windows
In this example, we have 3 visualization windows embedded into a window that we created using Qt Designer. In order to have 3 visualization windows, we need to add 2 more windows since VisIt starts with 1 window. In the Qt Designer file, we used QWidget to stand in for the widgets that we wanted to replace with visualization windows. We gave them names: CURVE1_WIDGET, CURVE2_WIDGET, and CITY_WIDGET to make it easy to remember what we'd be using them for (2 curve plots and a plot of 3D city geometry, in this case).
We could add the following code snippet into our __init_widgets method:
# Add 2 windows so we have 3 windows.
AddWindow()
AddWindow()
# Get the render window widgets from VisIt and put them into our window
self.rwindows = []
parents = ("CURVE1_WIDGET", "CURVE2_WIDGET", "CITY_WIDGET")
for i in range(3):
self.rwindows.append(pyside_support.GetRenderWindow(i+1))
parent = self.ui.findChild(QWidget, parents[i])
glout = QGridLayout(parent)
glout.setContentsMargins(0,0,0,0)
glout.addWidget(self.rwindows[i],0,0)
Once we add visualization windows into our application, it's best to clear them when we first show our window too. We can extend the show method that we wrote for our application to easily do this:
def show(self):
self.ui.show()
for i in (1,2,3):
SetActiveWindow(i)
RedrawWindow()
self.ui.raise_()
CheckBox
Using check boxes is straightforward. You can create the QCheckBox in Qt Designer and give it a name. You can then connect the check box up to a slot function defined in your class.
checkbox = self.ui.findChild(QCheckBox, "SHOW_SOURCE")
checkbox.toggled.connect(self.showSourceToggled)
The slot function:
def showSourceToggled(self, checked):
# Set the internal class variable that we're using to hold the state from the check box.
self.showSource = checked
# This will cause our plots to change so call routines to update plots (if there is data).
if self.__have_data():
self.__setup_city_plot()
ComboBox
Adding items to a combo box can probably be done from Qt Designer but there are uses for dynamically adding elements to a combo box. In this case, we'll add the variables that we support.
# Add some variable names to the results
self.var = "flux"
vars = self.ui.findChild(QComboBox,"VARIABLE")
vars.addItem("area")
vars.addItem("flux")
vars.addItem("summedFlux")
vars.setCurrentIndex(vars.findText(self.var))
vars.activated.connect(self.varChanged)
In this case, we're using a combo box to determine which variable is being plotted. We can connect a slot to the combo box's activated signal. Our slot function will change the plot we care about to the new variable. Note the use of DisableRedraw and RedrawWindow to prevent unnecessary redraws when multiple plot attributes are changed.
def varChanged(self, value):
vars = self.ui.findChild(QComboBox,"VARIABLE")
self.var = vars.itemText(value)
if self.__have_data():
SetActiveWindow(3)
SetActivePlots(0)
DisableRedraw()
ChangeActivePlotsVar(self.var)
self.__set_plot_atts()
RedrawWindow()
Slider
This section shows how to use a QSlider widget. The example here uses the slider as a time slider.
Connect the slot:
slider = self.ui.findChild(QSlider, "TIME_SLIDER")
slider.sliderReleased.connect(self.timeSliderChanged)
Call when opening a database:
nStates = GetMetaData(self.database).numStates
# Set the valid range on the time slider.
slider = self.ui.findChild(QSlider, "TIME_SLIDER")
slider.setMinimum(0)
slider.setMaximum(nStates-1)
The slot:
def timeSliderChanged(self):
slider = self.ui.findChild(QSlider, "TIME_SLIDER")
self.timeState = slider.value()
SetActiveWindow(3)
SetTimeSliderState(self.timeState)
Opening files
The first step is to hook up the menu action to an openFiles method in our class. This gets done in the __init_widgets method.
# Connect the open file action
action = self.ui.findChild(QAction,"actionOpen")
action.triggered.connect(self.openFile)
When actionOpen is activated, we want to use the stock Qt file dialog to locate the files that we want to open. Currently, VisIt's file dialog is not usable.
def openFile(self):
# Use the stock Qt dialog to look for VTK files.
filename, _ = QFileDialog.getOpenFileName(self.ui, 'Open file', os.curdir, "*.vtk")
if filename != "":
self.database = filename
# Do something with the new database
self.__setup_databases()
self.__setup_plots()
def __setup_databases(self):
SetActiveWindow(3)
OpenDatabase(self.database)
def __setup_plots(self):
SetActiveWindow(3)
DeleteAllPlots()
AddPlot("Pseudocolor", "flux")
DrawPlots()
Saving images
As with other menu-based actions, we can hook up image saving to work in a similar fashion. This example shows how to use a Qt file dialog to pick the output name of an image that we're saving. This code assumes that you have a counter in your class called self.saveIndex.
def saveImage(self):
suggestedName = os.path.join(os.curdir, "output%04d.png" % self.saveIndex)
filename, _ = QFileDialog.getSaveFileName(self.ui, 'Save image', suggestedName, "*.png")
if filename != "":
if filename == suggestedName:
self.saveIndex = self.saveIndex + 1
SetActiveWindow(3)
atts = GetSaveWindowAttributes()
atts.outputToCurrentDirectory = 0
atts.outputDirectory, atts.fileName = os.path.split(filename)
atts.screenCapture = 0
atts.format = atts.PNG
atts.resConstraint = atts.ScreenProportions
atts.width = 2000
atts.family = 0
SetSaveWindowAttributes(atts)
SaveWindow()
About Window
Qt provides a QMessageBox that can be used for an about window.
# Connect the about action
actionAbout = self.ui.findChild(QAction,"actionAbout")
actionAbout.triggered.connect(self.about)
The slot:
def about(self):
QMessageBox.about(self.ui, "About MyApp", "<center><h1>MyApp</h1><br>Copyright 2006-2013<br>LLNS Corporation</center>")
Help Window
With just a little code, you can set up a custom Qt window that can show an HTML help file. There are probably better WebKit ways to do this but presently, VisIt's PySide installation does not seem to provide WebKit widgets.
Connect an action to show the window:
# Connect the help action
actionHelp = self.ui.findChild(QAction,"actionHelp")
actionHelp.triggered.connect(self.showHelpWindow)
self.help = None
Create the window:
def __create_help_window(self):
win = QMainWindow()
win.setMinimumWidth(600);
win.setMinimumHeight(600);
central = QWidget()
win.setCentralWidget(central)
vLayout = QVBoxLayout(central)
browser = QTextBrowser(central)
browser.setSource(QUrl().fromLocalFile(self.__resource("help.html")))
vLayout.addWidget(browser)
hLayout = QHBoxLayout()
vLayout.addLayout(hLayout)
hLayout.addStretch(5)
dismiss = QPushButton(central)
dismiss.setText("Dismiss")
dismiss.clicked.connect(win.hide)
hLayout.addWidget(dismiss)
return win
The slot to show the window:
def showHelpWindow(self):
if self.help == None:
self.help = self.__create_help_window()
self.help.show()
Animation Controls
VisIt's GUI already provides animation controls that you can steal for your PySide application. The good part of taking VisIt's animation controls is that their behavior for changing time steps is already hooked up. You can even customize the behavior!
The following code snippet assumes that we're loading a UI file that contains a QWidget called "ANIMATION". We'll use that widget as the parent for the animation controls that we'll reparent into our PySide application. In this example, we take VisIt's time slider and VCR controls and reparent them into the PySide application under the ANIMATION widget. We save references to the slider and to the timecontrols so we can do some fancier things later.
class App(QObject):
def __init_widgets(self):
# Code omitted
# Steal the time slider and VCR controls from the main window. We have to
# check the title of the group box because posted windows can disrupt the
# widget ordering of the group boxes in the returned list.
animation = self.ui.findChild(QWidget, "ANIMATION")
aLayout = QVBoxLayout(animation)
aLayout.setContentsMargins(0,0,0,0)
w = GetUIWindow()
g = w.findChildren(QGroupBox)
for wd in g:
if wd.title() == "Time":
self.timecontrols = wd
tc = self.timecontrols.children()
self.visitslider = tc[3]
vcrcontrols = tc[-1]
self.visitslider.setParent(animation)
aLayout.addWidget(self.visitslider)
vcrcontrols.setParent(animation)
aLayout.addWidget(vcrcontrols)
Customization
VisIt's animation controls will call SetTimeSliderState() on the active time slider. In a PySide application, we're often dealing with multiple windows or have internal state that requires extra coordination. We can hook into VisIt's existing time slider behavior and make it call our own behavior instead.
Disconnect VisIt's time slider behavior and call our application's sliderWasReleased slot instead. In our slot, we'll call the old slot and execute our own custom code.
class App(QObject):
def __init_widgets(self):
# Code omitted
# Disconnect the current time slider behavior and make it call our routine instead.
self.visitslider.disconnect(SIGNAL('sliderWasReleased()'), self.timecontrols, SLOT('sliderEnd()'))
self.visitslider.connect(SIGNAL('sliderWasReleased()'), self.sliderWasReleased)
Here is our slot. Note that we use the references to the visitslider and timecontrols. We call VisIt's old slot first and then we call our class' own __set_timeState() method, which in this case updates internal state and updates various aspects of our plots in multiple vis windows.
class App(QObject):
def sliderWasReleased(self):
# Call VisIt's old time slider slot function directly.
idx = self.timecontrols.metaObject().indexOfSlot("sliderEnd()")
if idx >= 0:
ret = self.timecontrols.metaObject().method(idx).invoke(self.timecontrols)
# VisIt set the time already. Let's get the value so we can update our stuff.
ts = self.visitslider.value()
self.__set_timeState(ts)
Adding Mouse Input
VisIt's vis windows can be extended to accept more mouse input via event filters. This event filter provides signals that are emitted when certain mouse events occur.
- pressed - emitted when the mouse button is pressed
- moved - emitted when the mouse button is moved after having been pressed
- released - emitted when the mouse is released after having been pressed
Event filter class:
class MouseEventFilter(QObject):
pressed = Signal(QPoint,QSize)
moved = Signal(QPoint,QSize)
released = Signal(QPoint,QSize)
def __init__(self):
super(MouseEventFilter,self).__init__()
self.press = 0
def hit(self):
self.press = 1
def eventFilter(self, obj, event):
if event.type() == QEvent.MouseButtonPress:
if event.button() == Qt.LeftButton:
self.pressed.emit(event.pos(), obj.size())
if self.press:
return 1
elif event.type() == QEvent.MouseMove:
if self.press:
self.moved.emit(event.pos(), obj.size())
return 1
elif event.type() == QEvent.MouseButtonRelease:
if self.press:
self.released.emit(event.pos(), obj.size())
self.press = 0
return 1
return super(MouseEventFilter,self).eventFilter(obj, event)
Hooking up the filter class to application slots:
# We can create the filter object, install it on the vis window's central widget,
# and connect its slots to our app.
self.curve1EF = MouseEventFilter()
self.curve1EF.pressed.connect(self.curve1Pressed)
self.curve1EF.moved.connect(self.curve1Moved)
self.curve1EF.released.connect(self.curve1Released)
self.rwindows[0].centralWidget().installEventFilter(self.curve1EF)
Example slot functions that demonstrate how to add functionality to VisIt windows. In this case, we have a curve plot window that shows a time cue line. We want to know whether we pressed the line and if so then we want the line to move as we move the mouse. On mouse release, we want to set the new time slider state. The examples demonstrate what you would need to do but code to actually determine click locations is omitted.
def curve1Pressed(self, pos, size):
# Check whether we clicked close to the time cue
if self.__mouse_clicked_time_cue(1, pos, size):
# Tell the event filter that we pressed on something. If we don't then the normal
# mouse behavior will apply in the window.
self.curve1EF.hit()
def curve1Moved(self, pos, size):
# The mouse X location in the window will correspond to a time step. Which one?
ts = self.__time_from_curve_release(1, pos, size)
# Update the curve plot's time cue to the selected time step, color it green.
self.__set_curve_timeState(ts, (0,255,0,255))
def curve1Released(self, pos, size):
# The mouse X location in the window will correspond to a time step. Which one?
ts = self.__time_from_curve_release(1, pos, size)
# Change the time step that we're looking at in all windows.
self.__set_timeState(ts)
Debugging
This section shows techniques for debugging PySide applications
Signals and Slots
As with normal Qt applications, PySide applications can have signals and slots. PySide applications can even manipulate existing signal/slot connections that may already be in place within VisIt, allowing you to manipulate VisIt's behavior. It can be helpful to see a listing of the signals and slots for a particular object. Here is some code to show that information:
def print_signals_and_slots(obj):
for i in xrange(obj.metaObject().methodCount()):
m = obj.metaObject().method(i)
if m.methodType() == QMetaMethod.MethodType.Signal:
print "SIGNAL: sig=", m.signature(), "hooked to nslots=",obj.receivers(SIGNAL(m.signature()))
elif m.methodType() == QMetaMethod.MethodType.Slot:
print "SLOT: sig=", m.signature()