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)