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: 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 :ref:`pyuic ` preprocessed output from Qt Designer to generate its layout. :download:`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 :mod:`kPyQt` and :mod:`ktl`. The ``lrisQt`` module is also installed in kroot, after the :ref:`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 :class:`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 :class:`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. .. include:: ./standalone.sin :literal: