Writing Gui Clients - syue99/Lab_control GitHub Wiki

Introduction

This page describes how to write GUI clients using the PyQt4 Python library. We assume some familiarity with PyQt, please see this tutorial for an excellent introduction for writing PyQt programs. We explain how to create a LabRAD client that encompasses a graphical user interface containing, buttons, spinboxes, dropdown menus, and any other PyQt widgets. The created clients are similar to scripts in that they interface with a server and are able to execute server settings in response to user action i.e pressing a button. In addition, GUI clients are able to subscribe to signals from the server and act in response to these signals being emitted. For these reasons, GUI clients are referred to as asynchronous clients.

First Client

LabRAD is built on top of the twisted Python library. Within twisted, the timing of all events is controlled by an event loop called a reactor. Similarly to twisted, PyQt has its own reactor to control the timing graphical events. For the two libraries to work together, the two event loops must be combined. This is done with a freely available qt4reactor as follows (the file is available in the tutorial folder):

from twisted.internet.defer import inlineCallbacks
from PyQt4 import QtGui

class sampleWidget(QtGui.QWidget):
    def __init__(self, reactor, parent=None):
        super(sampleWidget, self).__init__(parent)
        self.reactor = reactor
        self.setWindowTitle('Sample Widget')
        self.connect()
    
    @inlineCallbacks
    def connect(self):
        #make an asynchronous connection to LabRAD
        from labrad.wrappers import connectAsync
        self.cxn = yield connectAsync(name = 'Sample Widget')
    
    def closeEvent(self, x):
        #stop the reactor when closing the widget
        self.reactor.stop()

if __name__=="__main__":
    #join Qt and twisted event loops
    a = QtGui.QApplication( [] )
    import qt4reactor
    qt4reactor.install()
    from twisted.internet import reactor
    widget = sampleWidget(reactor)
    widget.show()
    reactor.run()

As seen in the example, we first create a QApplication instance and then import and install the qt4reactor, which integrates twisted into the Qt mainloop. Only after this is done, we can import the twisted reactor, create the widget, show it and then start the reactor.

The created sampleWidget inherits from the QWidget class. When constructing sampleWidget, we pass the twisted reactor. This way the widget will know to stop the reactor when it's closed and closeEvent method is executed. The example also shows the connect method, which creates an asynchronous connection to the manager. The syntax is the same as while writing servers: use yield keyword to wait for deferred methods to return a value, and @inlineCallbacks decorators when yielding.

When the widget is running, you should see 'Sample Widget' in the manager's list of connections. Here's the screenshot of the first image: it has a title but not much else.

Sample Widget Screenshot

Reactor already installed error

It is important to run qt4reactor.install() before ever importing the twisted reactor. Otherwise, we will get an error reactor already installed. Note that the twisted reactor is imported whenever we import any other LabRAD classes i.e from labrad.units import WithUnit. To avoid the error, all of such imports must be done after qt4reactor.install().

Advanced

The twisted reactor is installed inside __init__.py within the LabRAD directory. Probably there is no reason to need for it to be automatically imported when accessing WithUnit.

Connecting Layout

Let's now add some buttons to the widget and wire them up to perform a function. The function will be performing a task by accessing a LabRAD server. Our widget will be editing the registry, so it will be helpful to have the Registry Editor running concurrently to monitor what's going on. Here's the screenshot of the running widget:

Connected Layout Widget

On the left is the input line to provide the name of the registry key. The spin box to the right lets the user enter the value. The submit button submits the key and value pair to the registry. The code is below, (the file is also available in the tutorial folder):

from twisted.internet.defer import inlineCallbacks
from PyQt4 import QtGui

class connectedLayoutWidget(QtGui.QWidget):
    def __init__(self, reactor, parent=None):
        super(connectedLayoutWidget, self).__init__(parent)
        self.reactor = reactor
        self.setupLayout()
        self.connect()
    
    def setupLayout(self):
        #setup the layout and make all the widgets
        self.setWindowTitle('Connected Layout Widget')
        #create a horizontal layout
        layout = QtGui.QHBoxLayout()
        #name of the parameter
        self.lineedit = QtGui.QLineEdit()
        #value entry
        self.spin = QtGui.QDoubleSpinBox()
        #buttons for submitting
        self.submit = QtGui.QPushButton('Submit')
        #add all the button to the layout
        layout.addWidget(self.lineedit)
        layout.addWidget(self.spin)
        layout.addWidget(self.submit)
        self.setLayout(layout)
        
    @inlineCallbacks
    def connect(self):
        #make an asynchronous connection to LabRAD
        from labrad.wrappers import connectAsync
        from labrad.errors import Error
        self.Error = Error
        cxn = yield connectAsync(name = 'Connected Layout Widget')
        self.registry = cxn.registry
        self.lineedit.editingFinished.connect(self.on_editing_finished)
        self.submit.pressed.connect(self.on_submit)
    
    @inlineCallbacks
    def on_submit(self):
        '''
        when the submit button is pressed, submit the value to the registry
        '''
        text = self.lineedit.text()
        value = self.spin.value()
        key = str(text) #convert QString to python string
        yield self.registry.set(key, value)
        
    @inlineCallbacks
    def on_editing_finished(self):
        '''
        called when the user is finished edintg the parameter name
        tries to load the value from the registry, if it's there
        '''
        text = self.lineedit.text()
        key = str(text) #convert QString to python string
        try:
            value = yield self.registry.get(key)
        except self.Error as e:
            print e
        else:
            self.spin.setValue(value)
        
    def closeEvent(self, x):
        #stop the reactor when closing the widget
        self.lineedit.editingFinished.disconnect()
        self.reactor.stop()

if __name__=="__main__":
    #join Qt and twisted event loops
    a = QtGui.QApplication( [] )
    import qt4reactor
    qt4reactor.install()
    from twisted.internet import reactor
    widget = connectedLayoutWidget(reactor)
    widget.show()
    reactor.run()

The main difference with the first client is we create a number of widgets inside the setupLayout method. When the user interacts with the widgets, they emit a signal. For example pressing the submit button emits the pressed signal and editingFinished signal is emitted when the user finished entering something in the input line.

These emitted signals are connected to slots in the connect method. The slots execute programatic tasks in response to the signals. Upon receiving the pressed signal, we set the key with the name written on the input line to the value entered in the spin box. When the name of the parameter is changed in the input line, we attempt to look up the parameter value of from the registry.

Receiving Signals From Servers

In the previous example, we communicate with a server when the user pressed a button. Here we show how to update a widget when the signal comes from the server. For the example we use 'Server Connected' and 'Server Disconnected' signals from the managers. These are emitted when the corresponding events take place. This GUI presents a simple log of the events:

Signal Wdiget

The source code is available in the tutorial folder. As you play with the widget, it's best to use the node server to easily start and stop servers, prompting signals to be emitted.


from twisted.internet.defer import inlineCallbacks
from PyQt4 import QtGui

class signalWidget(QtGui.QWidget):
    
    SERVER_CONNECT_ID = 9898989
    SERVER_DISCONNECT_ID = 9898990
    
    def __init__(self, reactor, parent=None):
        super(signalWidget, self).__init__(parent)
        self.reactor = reactor
        self.setupLayout()
        self.connect()
    
    def setupLayout(self):
        #setup the layout and make all the widgets
        self.setWindowTitle('Signal Widget')
        #create a horizontal layout
        layout = QtGui.QHBoxLayout()
        #create the text widget 
        self.textedit = QtGui.QTextEdit()
        self.textedit.setReadOnly(True)
        layout.addWidget(self.textedit)
        self.setLayout(layout)
        
    @inlineCallbacks
    def connect(self):
        #make an asynchronous connection to LabRAD
        from labrad.wrappers import connectAsync
        cxn = yield connectAsync(name = 'Signal Widget')
        manager = cxn.manager
        #subscribe to 'Server Connect' message
        yield manager.subscribe_to_named_message('Server Connect', self.SERVER_CONNECT_ID, True)
        yield manager.addListener(listener = self.followServerConnect, source = None, ID = self.SERVER_CONNECT_ID)
        #subscribe to 'Server Disconnect' message
        yield manager.subscribe_to_named_message('Server Disconnect', self.SERVER_DISCONNECT_ID, True)
        yield manager.addListener(listener = self.followServerDisconnect, source = None, ID = self.SERVER_DISCONNECT_ID)
    
    def followServerConnect(self, cntx, server_name):
        #executed when a server connects to the manager
        server_name = server_name[1]
        text =  'Server Connected: {}'.format(server_name)
        self.textedit.append(text)
    
    def followServerDisconnect(self, cntx, server_name):
        #executed when the server disconnected from the manager
        server_name = server_name[1]
        text = 'Server Disconnected: {}'.format(server_name)
        self.textedit.append(text)
        
    def closeEvent(self, x):
        #stop the reactor when closing the widget
        self.reactor.stop()

if __name__=="__main__":
    #join Qt and twisted event loops
    a = QtGui.QApplication( [] )
    import qt4reactor
    qt4reactor.install()
    from twisted.internet import reactor
    widget = signalWidget(reactor)
    widget.show()
    reactor.run()

Every signal we subscribe to requires a unique integer id, in our case we have two: SERVER_CONNECT_ID and SERVER_DISCONNECT_ID. One then subscribes to the signal by calling the corresponding settings e.g yield manager.subscribe_to_named_message('Server Connect', self.SERVER_CONNECT_ID, True). Here the first argument is the name of the messaged we're subscribing to. The second argument is the unique id. The boolean true indicates that we are subscribing rather than unsubscribing.

After subscribing to the message, we need to connect the message to the method which will be executed when the message is received. This is done with the line yield manager.addListener(listener = self.followServerConnect, source = None, ID = self.SERVER_CONNECT_ID). Here self.followServerConnect is the method which will be called when the Server Connect message is received. The ID is the same integer used to subscribe to the message.

Open the widget and start and stop a server to make sure everything works. It's best to use the node server: then you can repeatedly start and stop the 'Python Test Server'. Also try starting multiple clients simultaneously, all of them should get the messages.

#Combining Widgets

Now that we have written several GUI clients, how do we combined them into a single experimental control window? The good news is that the QWidgets we have written are completely modular and reusable. To combine the connectedLayoutWidget and the signalWidget, we create a new widget called combinedWidget and add the previously written widgets to its layout. The result looks like this:

Combined Widget

and the code is available in the tutorial folder.

from PyQt4 import QtGui
from connectedLayoutWidget import connectedLayoutWidget
from signalWidget import signalWidget

class combinedWidget(QtGui.QWidget):
    def __init__(self, reactor, parent=None):
        super(combinedWidget, self).__init__(parent)
        self.reactor = reactor
        self.create_layout()
    
    def create_layout(self):
        '''
        creates a vertical layout of two widgets
        '''
        self.setWindowTitle('Combined Widget')
        layout = QtGui.QVBoxLayout()
        connected_w = connectedLayoutWidget(reactor)
        signaling_w = signalWidget(reactor)
        layout.addWidget(connected_w)
        layout.addWidget(signaling_w)
        self.setLayout(layout)
        
    def closeEvent(self, x):
        self.reactor.stop()

if __name__=="__main__":
    a = QtGui.QApplication( [] )
    clipboard = a.clipboard()
    import qt4reactor
    qt4reactor.install()
    from twisted.internet import reactor
    combinedWidget = combinedWidget(reactor)
    combinedWidget.show()
    reactor.run()

Note that the widgets share the same reactor, which is passed to them from combinedWidget. Currently each of the subwidgets make its own asynchronous connection to the LabRAD manager. This is simplest, but is unnecessary as the widgets could share the same connection and work in different contexts. This will be discussed in the next sections.

In this example combinedWidget inherited from the QWidget class. Try changing it to QTabWidget and adding each of the subwidgets into a separate tab. You can also create a nice border around the subwidgets by having those inherit from the QFrame class.

#Connection Sharing

#Automatically reconnecting

Other

Configuration in the registry

Algorithmic Layout

Combining GUIs to make a centralized control window

Connection Sharing

When multiple GUI clients are combined into a single window, it is advantageous for them to share a single LabRAD connection in order to conserve resources. The connection can be shared with the multiple clients operating in different contexts, maintaining their independence. Connection sharing is simplified by utilizing the connection object in /clients/connection.py. For details on how to use it, see the dedicated page.

Disables and Enables itself when servers are connected

guidelines

Back to Main Wiki