Examples

It’s rare that a graphical interface can be completely generic, each system is unique and will require special handling for something or other. Possibly several somethings. One of the strengths of using Python with PyQt is that the complexity can be handled at the application level, or by decorating simple PyQt widgets, as opposed to implementing custom widgets for every new custom function. Keeping the widgets simple is an important design decision for this approach.

Looking at the code for existing interfaces will help answer questions that are not answered here. The approach in this documentation is just the starting point; the list of deployed interfaces below is in rough order of increasing complexity, referenced by their location in the kroot source code repository:

kss/uno/departure/gui
kss/pcs/pcsmotor/qt
kss/lris/qt
kss/nirspec/qt
kss/hires/qt
kss/K1DM3/gui
kss/deimos/qt
kss/uno/vwa/gui

Minimal example

The code included here is only a starting point. The goal is to show the boilerplate needed to create the GUI at all, and to point out where customization occurs. The example used here is a cut-down version of the GUI for the lredccd KTL service, which uses the pyuic preprocessed output from Qt Designer to generate its layout. This Qt Designer .ui file can be used with this code example.

So first order of business, the boilerplate:

#! @KPYTHON3@
#
# kpython safely sets RELDIR, KROOT, LROOT, and PYTHONPATH before invoking
# the actual Python interpreter.

import kPyQt                    # provided by kroot/kui/kPyQt
import ktl                      # provided by kroot/ktl/keyword/python
import lrisQt
from PyQt5 import QtWidgets
import sys


def main():

    application = QtWidgets.QApplication(sys.argv)
    main_window = MainWindow()
    main_window.setupUi()
    main_window.show()

    return kPyQt.run(application)

Using kpython3 is important, as all of our PyQt infrastructure assumes you’re using Python 3, and we want to import Python modules installed as part of kroot– including kPyQt and ktl. The lrisQt module is also installed in kroot, after the pyuic preprocessing referred to above. Note the use of the @KPYTHON3@ substitution pattern here; this sample GUI is intended to use the .sin substitution system common throughout kroot; here, the Makefile macro KPYTHON3 will (probably) be expanded to /kroot/rel/default/bin/kpython3.

The main() method defined here is invoked by a check at the very tail end of the file:

if __name__ == '__main__':
    status = main()
    sys.exit(status)

Having a main() method defined at the top allows the reader to see the overall application flow right after opening up the code, as opposed to digging for it after all the classes and functions have been defined. In particular, the MainWindow class referenced here needs to be defined; for this simple interface it will contain all of our custom logic. The calls in main() instantiate the application and the MainWindow instances, set up all the widgets in the MainWindow, request that the MainWindow be rendered (shown), and lastly, enters the Qt main loop that will handle all other GUI operations until the application exits.

On to the MainWindow:

class MainWindow(QtWidgets.QMainWindow):

    def __init__(self, *args, **kwargs):

        self.template = None
        QtWidgets.QMainWindow.__init__(self, *args, **kwargs)

The initialization method is simple. The self.template reference is where we will store the Qt Designer template when we set up the interface:

def setupUi(self):

    template = lrisQt.RedDetector.Ui_MainWindow()
    template.setupUi(self)
    self.template = template

We execute the method giving us the full set of widgets from the lrisQt module, ask that set of widgets to set itself up with our MainWindow instance as the parent for all those widgets, then save a reference to the template because we will use it whenever we need a reference to an active (or inactive) widget in the interface.

Still in that same method, we begin customization that ties KTL keywords to actual widgets in the interface:

lredccd = ktl.cache('lredccd')

object_name = kPyQt.kFactory(lredccd['OBJECT'])
object_name.stringCallback.connect(template.current_object.setText)
template.apply_object.clicked.connect(self.applyObject)

This interaction covers two widgets on the interface. We want to display the current object name, which is given here by the lredccd.OBJECT KTL keyword, and we want it to be displayed by the template.current_object QtLabel that we established in our Qt Designer layout. The current_object widget name was set directly in Qt Designer; every widget has a unique name, the author has complete control over how they are set. We are connecting the object_name.stringCallback _signal_ to the current_object.setText _slot_, in effect, asking the current_object.setText() method to be called every time the object name changes.

The second widget is the template.apply_object button. Every time it gets clicked we want to send the current contents of the template.new_object widget, a text entry widget that is not displayed here, to the lredccd.OBJECT KTL keyword. Similar to the text callback above, what we’re doing is requesting that the self.applyObject() method be invoked any time the apply_object.clicked signal fires– which only happens when the user clicks on the button.

If this were a normal user interface there would be a long list of similar interactions; instantiate a Keyword.Keyword instance, tweak it if necessary, and tie it in as necessary to different widgets on the interface. From a maintainability perspective it helps to make all the connections for related sets of widgets in small batches like this, grouping the connections by function on the interface rather than by some other classification.

This is also where you connect function calls to individual menu options. In the case of this interface we have only one:

template.actionQuit.setShortcut('Ctrl+Q')
template.actionQuit.setStatusTip('Quit application')
template.actionQuit.triggered.connect(QtWidgets.qApp.quit)

Again, grouping calls by function, we also set up the keyboard shortcut (which is also displayed in the menu) for the quit action. Like the buttons, labels, and other interface widgets, the menu options are defined for this interface in Qt Designer. For this menu option we want the application to quit if it is ever selected.

Having set up the base interface we now need to define the methods that will be called for the interactions defined above. For simplicity’s sake these are often defined as part of the MainWindow class. The only method used in this example is applyObject(), which writes the user-specified value out to the appropriate KTL keyword:

def applyObject(self, *ignored, **also_ignored):

    object_box = self.template.new_object
    object_value = object_box.toPlainText()

    if object_value == '':
        return

    ccd_object = ktl.cache('lredccd', 'OBJECT')
    ccd_object.write(object_value, wait=False)

    object_box.setPlainText('')

Remember, the context for being in this method at all is because the user clicked on an “Apply” button. The first order of business is to retrieve the current value of the template.new_object text field; we cowardly refuse to send the value if it is the empty string. We continue processing if it is not; we retrieve the ktl.Keyword instance for the keyword we want to write to, and write out the value, explicitly stating that we don’t want to block until the write operation is complete. Having written out the new value we clear the text entry box as a positive affirmation that the value was sent successfully; when the actual KTL keyword brodcast comes back, it will independently trigger the update to the displayed value as we established earlier in MainWindow.setupUi().

There you have it, a fully functional GUI!

Minimal example, uninterrupted

For convenience, here is the full body of code shown above, but without wordy explanations interleaved:

#! @KPYTHON3@
#
# kpython safely sets RELDIR, KROOT, LROOT, and PYTHONPATH before invoking
# the actual Python interpreter.

import kPyQt                    # provided by kroot/kui/kPyQt
import ktl                      # provided by kroot/ktl/keyword/python
import lrisQt
from PyQt5 import QtWidgets
import sys


def main():

    application = QtWidgets.QApplication(sys.argv)
    main_window = MainWindow()
    main_window.setupUi()
    main_window.show()

    return kPyQt.run(application)


class MainWindow(QtWidgets.QMainWindow):

    def __init__(self, *args, **kwargs):

        self.template = None
        QtWidgets.QMainWindow.__init__(self, *args, **kwargs)


    def setupUi(self):

        template = lrisQt.RedDetector.Ui_MainWindow()
        template.setupUi(self)
        self.template = template

        lredccd = ktl.cache('lredccd')

        object_name = kPyQt.kFactory(lredccd['OBJECT'])
        object_name.stringCallback.connect(template.current_object.setText)
        template.apply_object.clicked.connect(self.applyObject)

        template.actionQuit.setShortcut('Ctrl+Q')
        template.actionQuit.setStatusTip('Quit application')
        template.actionQuit.triggered.connect(QtWidgets.qApp.quit)


    def applyObject(self, *ignored, **also_ignored):

        object_box = self.template.new_object
        object_value = object_box.toPlainText()

        if object_value == '':
            return

        ccd_object = ktl.cache('lredccd', 'OBJECT')
        ccd_object.write(object_value, wait=False)

        object_box.setPlainText('')

# end of class MainWindow


if __name__ == '__main__':
    status = main()
    sys.exit(status)

Designer-less example

It’s entirely possible, and in some cases preferred, to establish all of the PyQt widgets programatically, without the use of Qt Designer or an established template. The example shown here doesn’t match all the cosmetic elements present in the example above, but it does implement the functional widgets. One structural variance between the two approaches is the use of a grid layout in this example, instead of fixed pixel placement of widgets within the window.

#! @KPYTHON3@
#
# kpython safely sets RELDIR, KROOT, LROOT, and PYTHONPATH before invoking
# the actual Python interpreter.

import kPyQt                    # provided by kroot/kui/kPyQt
import ktl                      # provided by kroot/ktl/keyword/python
from PyQt5 import QtWidgets
import sys


def main():

    application = QtWidgets.QApplication(sys.argv)
    main_window = MainWindow()
    main_window.setupUi()
    main_window.show()

    return kPyQt.run(application)


class MainWindow(QtWidgets.QMainWindow):

    def __init__(self, *args, **kwargs):

        self.template = None
        QtWidgets.QMainWindow.__init__(self, *args, **kwargs)


    def setupUi(self):

        self.setWindowTitle('LRIS red object')
        mainframe = QtWidgets.QFrame(self)
        self.setCentralWidget(mainframe)
        mainframe.setStyleSheet(default_stylesheet)

        grid = QtWidgets.QGridLayout()
        mainframe.setLayout(grid)
        self.main_grid = grid

        apply_object = QtWidgets.QPushButton(mainframe)
        current_object = QtWidgets.QLabel(mainframe)
        label_object = QtWidgets.QLabel(mainframe)
        new_object = QtWidgets.QLineEdit(mainframe)

        self.apply_object = apply_object
        self.current_object = current_object
        self.label_object = label_object
        self.new_object = new_object

        grid.addWidget(label_object, 0, 0)
        grid.addWidget(current_object, 0, 1)
        grid.addWidget(new_object, 1, 1)
        grid.addWidget(apply_object, 1, 2)

        label_object.setText('Object:')
        apply_object.setText('Apply')

        lredccd = ktl.cache('lredccd')

        object_name = kPyQt.kFactory(lredccd['OBJECT'])
        object_name.stringCallback.connect(current_object.setText)
        apply_object.clicked.connect(self.applyObject)

        self.menubar = QtWidgets.QMenuBar(self)
        self.setMenuBar(self.menubar)
        self.menuFile = QtWidgets.QMenu(self.menubar)
        self.menuFile.setTitle('File')
        self.menubar.addAction(self.menuFile.menuAction())

        self.actionQuit = QtWidgets.QAction(self)
        self.actionQuit.setMenuRole(QtWidgets.QAction.QuitRole)
        self.actionQuit.setText('Quit')
        self.actionQuit.setShortcut('Ctrl+Q')
        self.actionQuit.setStatusTip('Quit application')
        self.actionQuit.triggered.connect(QtWidgets.qApp.quit)
        self.menuFile.addAction(self.actionQuit)


    def applyObject(self, *ignored, **also_ignored):

        object_box = self.new_object
        object_value = object_box.text()

        if object_value == '':
            return

        ccd_object = ktl.cache('lredccd', 'OBJECT')
        ccd_object.write(object_value, wait=False)

        object_box.setText('')

# end of class MainWindow



default_stylesheet = '''
QWidget {
    font: 12pt "Cantarell";
}

QCheckBox {
    background-color: #333333;
    color: #EEEEEE;
}

QDialog {
    background-color: #333333;
}

QFrame {
    background-color: #333333;
}

QLabel {
    color: #EEEEEE;
}

QLineEdit {
    color: #EEEEEE;
    background-color: #555555;
}

QPlainTextEdit {
    font: 12pt "Consolas";
}

QPushButton {
    border: 1px solid #8f8f91;
    border-radius: 7px;
    background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
                                      stop: 0 #c3c4c7, stop: 1 #a7a8ab);
}

QPushButton:pressed {
    background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
                                      stop: 0 #d4d5d8, stop: 1 #b8b9bc);
    border-style: inset;
}
'''


if __name__ == '__main__':
    status = main()
    sys.exit(status)