How To code with PyQt and TAU
Steps to create the minimum PyQt gui
How to create a TAU and PyQt GUI or Widgets
Resources
[Controls website, gui framworks]
[Qt4 Documentation]
[Qt4 Classes reference]
ᅵ see Thiago's internal presentation http://www.cells.es/CELLS/Intranet/Divisions/Computing/Controls/Applications/tau/Tau_Internal_Presentation
Create a GUI
Copied from http://www.cells.es/Members/srubio/howto/pyqt
- open qt designer
- create your widget
- save the ui_XXX.ui file
- > pyuic4 ui_XXX.ui > ui_XXX.py
- edit your XXX.py file
- Create a new class inheriting from the object that you want to create (QMainWindow or QDialog)
- in the __init__ method create an ui_XXX object and call to setupUi and retranslateUi methods
- This self.ui object will contain all the widgets and other objects that you added on QtDesigner.
- Add to your class the methods (Slots) that you want to execute for each Widget event (Signal)
- Connect Signals and Slots
- Browse to http://www.riverbankcomputing.com/static/Docs/PyQt4/html/classes.html to see what each PyQt class offers to you.
# This is the contents of the MyGui.py file
# It assumes that you have created ui_MyGui.ui with QtDesigner
# You have to execute >/pyuic4 ui_MyGui.ui > ui_MyGui.py to generate the file to import
from PyQt4 import QtGui,QtCore
from ui_MyGui import ui_MyGui
class MyGUI(QtGui.QMainWindow):
def __init__(self):
QtGui.QMainWindow.__init__(self)
self.ui = ui_MyGui()
self.ui.setupUi(self)
self.connect(self.ui.push_button,QtCore.SIGNAL('clicked(bool)'),self.DoAction)
def DoAction(self,arg = None):
print 'What a %s action.' % ('wonderful' if arg else 'sad')
app = QtGui.QApplication(sys.argv[0:1])
gui = MyGui()
gui.show()
sys.exit(app.exec_())
How to create a TAU Widget
Writing your own widgets from a template
- goto widget/utils
- type:
python widgetgen.py <classname> <superclass> <outputfile> [<qtfile>]
But it needed some patches (DbWidget example using svn tau.widget.utils package)!!! :
- First the template is generated:
python ../widget/utils/widgetgen.py DbWidget QtGui.QTextEdit dbwidget.py
- Editing dbwidget.py :
- Added taubase to imports
7c7
< from tau.widget import TauBaseWidget
---
> from tau.widget import TauBaseWidget,taubase - Modified init to avoit non-TAU parents to be passed to TauBaseWidget?.init
it caused exceptions in designer23c23,28
< self.call__init__(TauBaseWidget, name, parent, designMode=designMode)
---
> if isinstance(parent,TauBaseWidget): #It was needed to avoid exceptions in TauDesigner!
> print 'in DbWidget->TauBaseWidget __init__(parent)'
> self.call__init__(TauBaseWidget, name, parent, designMode=designMode)
> else:
> print 'in DbWidget->TauBaseWidget __init__()'
> self.call__init__(TauBaseWidget, name, designMode=designMode)
50c55,56
< raise RuntimeError("Forgot to overwrite %s.getModelClass" % str(self))
---
> return tau.core.TauDatabase
> #raise RuntimeError("Forgot to overwrite %s.getModelClass" % str(self)) - Instead of eventReceived I overwrote eventHandle;
because in that method appeared the execution exceptions related to setToolTip() and setText(value)112a119,141
> def eventHandle(self, value):
> """ eventHandle, called from setChangeEvent and setConfigEvent
> param @value set previously to self.getDisplayValue() if nothing specific received
> """
> #update text/title #Commented because it was setting text = "controls01:10000"
> #if not self.getShowText(): value = ''
> #if self._setText: self._setText(value)
>
> #update tooltip #Commented because there's no tooltip for Db; it caused exception!
> #self.setToolTip(self.getFormatedToolTip())
>
> print 'in DbWidget eventHandle'
> obj = self.getModelObj()
> if obj:
> print 'obj is not null'
> # sergi: this is the command I added to update the info displayed
> text = obj.get_info()
> self.setText(text)
> print 'text set as "%s"' % text
> ##update appearance
> self.updateStyle()
> print 'out of DbWidget eventHandle' - getModelClass modified to return TauDatabase
50c55,56
< raise RuntimeError("Forgot to overwrite %s.getModelClass" % str(self))
---
> return tau.core.TauDatabase
> #raise RuntimeError("Forgot to overwrite %s.getModelClass" % str(self))
- Added taubase to imports
Now all execution/designer problems are solved
Customization of the Widget
- Setup the signals to be subscribable from Qt:
__pyqtSignals__ = ("modelChanged(const QString &)",)
- Modify methods needed for your widget:
- Model initialization:
@QtCore.pyqtSignature("setModel(QString)")
def setModel(self,model):
self.setModelCheck(model) ##It must be included - Rewrite event Handling (launched by changeEvent(QString) and configEvent()):
@QtCore.pyqtSignature("changeEvent(QString)")
def setChangeEvent(self, value):
self.eventHandle(value)
@QtCore.pyqtSignature("configEvent()")
def setConfigEvent(self):
self.eventHandle(self.getDisplayValue() or self.getNoneValue())
def eventHandle(self, value):
# update text/title
if not self.getShowText(): value = ''
if self._setText: self._setText(value) ##It must be included
#update tooltip
self.setToolTip(self.getFormatedToolTip()) ##It must be included
#TODO: update whatsThis
#update appearance
self.updateStyle() ##It must be included
- Model initialization:
Any of these methods could be rewritten; those decorated will appear in QtDesigner connections view.
- Rewrite updateStyle : use it to customize look&feel, call it from eventHandle!
NOTE: Not event-driven objects can be customized using this method.
- Signals to connect with:
- getChangeSignal : returns QtCore.SIGNAL('changeEvent(QString)')
- getConfigSignal : QtCore.SIGNAL('configEvent()')
QtCore.QObject.connect(self, self.getChangeSignal(), self.setChangeEvent)
About declaring functions available to Qt Signals/Slots connection see #SignaturesusedbyQttorecognizeSignalsSlots
More things …
from unfinished Thiago's tutorial
- Must inherit from TauBaseWidget
- Must inherit from QWidget or any of its subclasses
- Must provide the following constructor:
def __init__(self, parent = None, designMode = False)
- must call the TauBaseWidget Constructor:
self.call__init__(TauBaseWidget, name, designMode=designMode)
- Must define two properties:
- model
- parentModel
- Should implement getModelClass
- Should implement updateStyle
Event Sequence
e.g.: two ways of launching fireEvent(...):
- setModel(QString) -> setModelCheck(QString)
- eventReceived : handle for events received (from Tango or also from Qt?)
Once fireEvent is called it notifies all listeners (included the widget itself!):
- fireEvent(type)
- emit(self.getConfigSignal() or (self.getChangeSignal(),getDisplayValue())
Note: This signals are connected to setConfigEvent and setChangeEvent Note: To control what is send with ChangeSignal the DisplayValue must be set first.
Once event is received the sequence is:
- setChangeEvent(value) / setConfigEvent() #available slots for events:
@QtCore.pyqtSignature("changeEvent(QString)") / @QtCore.pyqtSignature("configEvent()") - eventHandle(value or getDisplayValue) #it performs real event management.
It must be reloaded and include a call for updateStyle() or TauBaseWidget.eventHandle(self,value). - updateStyle() #It should always contain a self.update() call
Notes:
- getDisplayValue(): by default returns getModelObj().getDisplayValue()
- ...
Accessing Models from ipython
Minimal example (take care of how getFactory is used!)
#! python
import tau.core
tfact = tau.core.TauManager().getFactory()()
mks2 = tfact.getDevice('alba02:10000/LAB/VC/MKS-02')
<tau.core.TauDevice Object>
attr = mks2.getAttribute('Status')
attr.read()
<PyTango AttributeValue Object>
print attr.read().value
...
mks2.getHWObj()
<PyTango DeviceProxy Object>
print mks2.getHWObj().get_property(['SerialLine'])
...
Reading/Writing properties of a TauDevice
import tau.core
#From inside any widget using a TauDevice as Model:
model = self.getModel()
#A TauDatabase method is used to read properties list
all_properties = tau.core.TauManager().getFactory()().getDatabase().get_device_property_list(model.name(),'*')
#Any PyTango.DeviceProxy method can be used
all_props_values = model.get_property(all_properties) #it returns a dict object
#For updating the value of a property:
model.put_property({'prop_name':prop_value})
#OR
model.put_property(all_props_values)
Adding a widget to TAU packages and/or Qt Designer
- Widget plugin files must be placed at $PYTHONPATH/tau/widget/designer or any other folder in PYQTDESIGNERPATH
- Must inherit from TauWidgetPlugin and implement a few methods:
import tauplugin
from tau.widget.dbwidget import DbWidget
class DbWidgetPlugin(tauplugin.TauWidgetPlugin):
"""TauPlotPlugin(tauplugin.TauWidgetPlugin)
Provides a Python custom plugin for Qt Designer by implementing the
QDesignerCustomWidgetPlugin via a PyQt-specific custom plugin class.
"""
def getWidgetClass(self):
return DbWidget
def getIconName(self):
return 'columnview.png'
def includeFile(self):
return "tau.widget.dbwidget"
tau.widget Remarks:
- widget module is for generic tango widgets only
- there will be modules for gl, qwt, etc
- if you implement a widget that has reference to any system or interprets tango data in a very particular way it is not a widget. Just part of your application
Some Recipes
Standard Dialogs and Widgets
QMessageBox(parent,Title,Question,FlagsForButtons)
v = QtGui.QMessageBox.warning(None,'SplitterTester', \
'You must introduce or select an IP from IPAddress Combo Box', \
QtGui.QMessageBox.Ok|QtGui.QMessageBox.Cancel);
if v == QtGui.QMessageBox.Cancel:
return
QInputDialog(parent,title,question,QLineEdit,defaultText)
returns (text,OkPressed)
In [29]:QtGui.QInputDialog.getText(None,'hola','dame argo',QtGui.QLineEdit.Normal,'una moneiya')
Out[29]:(<PyQt4.QtCore.QString object at 0x83ad2ac>, True)
QFont: Setting a monospaced font in a QTextEdit panel
font = QtGui.QFont()
font.setStyleHint(QtGui.QFont.TypeWriter)
font.setFamily("unexistentfont")
self.ui.TextPanel.setFont(font)
Focus: .setFocus(), hasFocus() available on most of widgets
QDialog vs QMainWindow (KeyEvents)
- QDialog automatically connects Return and Escape keys to Ok and Cancel buttons.
- In QMainWindow it must be hacked using keyPressEvent methods
Managing Key Events
def keyPressEvent(self,event):
if event.key() == QtCore.Qt.Key_Escape:
self.reject()
elif event.key() in [QtCore.Qt.Key_Return,QtCore.Qt.Key_Enter]:
buttons = [b for b in \
[self.ui.ScanButton,self.ui.buttonBox.buttons()[1],self.ui.AddressEdit,self.ui.RangeEdit] \
if b.hasFocus()]
if buttons:
for b in buttons:
if hasattr(b,'click'): b.click()
else:
self.accept()
return
Signals and Slots
Connecting signals and slots
Connections between signals and slots (and other signals) are made using the QtCore.QObject.connect() method. For example:
QtCore.QObject.connect(a, QtCore.SIGNAL("QtSig()"), b, QtCore.SLOT("QtSlot()"))
QtCore.QObject.connect(a, QtCore.SIGNAL("PySig()"), b, QtCore.SLOT("QtSlot()"))
QtCore.QObject.connect(a, QtCore.SIGNAL("QtSig()"), pyFunction) #A python function with NO arguments
QtCore.QObject.connect(a, QtCore.SIGNAL("QtSig()"), pyClass.pyMethod) #A python function with NO arguments
QtCore.QObject.connect(a, QtCore.SIGNAL("PySig"), pyFunction) #A python signal WITH arguments
"modelChanged(const QString &)": it means that an string is passes as argument to the slot
Emitting signals
Any instance of a class that is derived from the QtCore.QObject class can emit a signal using its emit() method. This takes a minimum of one argument which is the signal. Any other arguments are passed to the connected slots as the signal arguments. For example:
a.emit(QtCore.SIGNAL("clicked()")) #Connect it to a python function with NO arguments
a.emit(QtCore.SIGNAL("pySig"), "Hello", "World") #To be connected to a python function WITH arguments
Signatures used by Qt to recognize Signals/Slots
Signals emitted by the class must be added to __pyqtSignals__ member
__pyqtSignals__ = ("modelChanged(const QString &)",)
Slots available must be decorated providing a header for Qt
@QtCore.pyqtSignature("setModel(QString)")
def setModel(self,model):
self.setModelCheck(model)
QThreads
QThreads object cannot access directly to GUI objects ... All access must be done using Signals and Slots; so any change to GUI (including displaying messages) must be done through emit and declared functions with arguments.
class QPinger(QtCore.QThread):
def __init__(self,parent,ipstart,iprange):
QtCore.QThread.__init__(self)
...
if hasattr(parent,'scan_finished'):
QtCore.QObject.connect(self, QtCore.SIGNAL("scan_finished"), parent.scan_finished)
def run(self):
...
self.emit(QtCore.SIGNAL("scan_finished"), founds)
Random remarks
Note: it works with milliseconds instead of seconds'':
- QThread.start() to launch QThread.run() method
- QThread.wait() equals to threading.Thread.join()
- QThread.wait(millis): equals to threading.Event.wait(seconds)
- QMutex.lock() ; QMutex.unlock ... typical mutex
- QWaitCondition: Like threading.Event but a mutex is unlocked during wait and locked again when the wait finishes.
- QWaitCondition.wait(QMutex): Forces the Thread to wait until QWaitCondition.wakeOne() or .wakeAll() is called in another thread.
- QWaitCondition.wait(QMutex, time=int(millis)): This works like threading.Event.wait(seconds).
- QWaitCondition.wait(QMutex): Forces the Thread to wait until QWaitCondition.wakeOne() or .wakeAll() is called in another thread.
- Signals: QThread.started,.finished,.terminated (launched by terminate(); it is thread unsafe!)
TAU Concepts
from Thiago's internal presentation
- Database
(tango://)?<host>:<port>
- Device
(<database>)?(<devicename>|<devicealias>)
- Attribute
<device>/<attrname>
- Configuration
<attribute>?configuration=<confname>
- Property
<device>?property=<propname>
<attribute>?property=<propname> - Command
<device>?command=<cmdname>

