Making Pure Python Displays

Important

  • Make sure the PCASpy tutorial server is running

As we saw in the A Word About Python Display section, it is possible to make displays using Python code and a .ui file from Qt Designer. It is also possible to make displays without using the Qt Designer at all, and write the user interface entirely in code.

To demonstrate this capability we will describe the steps to create the “All Motors” screen described at the Components Section.

This screen will have a QLineEdit and a QPushButton that will invoke a method to filter our list of motors and present a list of PyDMEmbeddedDisplays in the frame below pointing to the inline_motor.ui file that was created in the Inline Motor Screen section of this tutorial.

Here is how it will look once we are done:

All Motors Screen

Important

In order to simplify this tutorial, instead of using a database or other type of service, the data to populate the list of motors will come from a simple text file named motor_db.txt that can be downloaded here.

  • Step 1.

    The first thing that we will do is add the imports needed for the code that will follow.

    import os
    import json
    from qtpy import QtCore
    from pydm import Display
    from qtpy.QtWidgets import (QVBoxLayout, QHBoxLayout, QGroupBox,
        QLabel, QLineEdit, QPushButton, QScrollArea, QFrame,
        QApplication, QWidget)
    
    from pydm.widgets import PyDMEmbeddedDisplay
    from pydm.utilities import connection
    
  • Step 2.

    Let’s create our Python Class that will inherit from Display (See Subclassing Display).

    class AllMotorsDisplay(Display):
        def __init__(self, parent=None, args=[], macros=None):
            super(AllMotorsDisplay, self).__init__(parent=parent, args=args, macros=None)
            # Placeholder for data to filter
            self.data = []
            # Reference to the PyDMApplication
            self.app = QApplication.instance()
            # Load data from file
            self.load_data()
            # Assemble the Widgets
            self.setup_ui()
    
        def minimumSizeHint(self):
            # This is the default recommended size
            # for this screen
            return QtCore.QSize(750, 120)
    
        def ui_filepath(self):
            # No UI file is being used
            return None
    

    Breaking it down into pieces:

    1. The constructor of the class will call the load_data method that is responsible for opening our database and adding the information to our placeholder, self.data, for later filtering, as well as the setup_ui method in which the widgets be constructed and configuered.

    2. minimumSizeHint returns the suggested minimum dimensions for the display.

    3. ui_filepath will return None, as no ui file is being used in this case.

    • Step 2.1.

      Add the code to the load_data method.

      Note

      Look at the comments over the lines for explanation on what they do.

      def load_data(self):
          # Extract the directory of this file...
          base_dir = os.path.dirname(os.path.realpath(__file__))
          # Concatenate the directory with the file name...
          data_file = os.path.join(base_dir, "motor_db.txt")
          # Open the file so we can read the data...
          with open(data_file, 'r') as f:
              # For each line in the file...
              for entry in f.readlines():
                  # Append to the list of data...
                  self.data.append(entry[:-1])
      
    • Step 2.2.

      Add the code to the setup_ui method.

      Note

      Look at the comments over the lines for explanation on what they do.

      def setup_ui(self):
          # Create the main layout
          main_layout = QVBoxLayout()
          self.setLayout(main_layout)
      
          # Create a Label to be the title
          lbl_title = QLabel("Motors Diagnostic")
          # Add some StyleSheet to it
          lbl_title.setStyleSheet("\
              QLabel {\
                  qproperty-alignment: AlignCenter;\
                  border: 1px solid #FF17365D;\
                  border-top-left-radius: 15px;\
                  border-top-right-radius: 15px;\
                  background-color: #FF17365D;\
                  padding: 5px 0px;\
                  color: rgb(255, 255, 255);\
                  max-height: 25px;\
                  font-size: 14px;\
              }")
      
          # Add the title label to the main layout
          main_layout.addWidget(lbl_title)
      
          # Create the Search Panel layout
          search_layout = QHBoxLayout()
      
          # Create a GroupBox with "Filtering" as Title
          gb_search = QGroupBox(parent=self)
          gb_search.setTitle("Filtering")
          gb_search.setLayout(search_layout)
      
          # Create a label, line edit and button for filtering
          lbl_search = QLabel(text="Filter: ")
          self.txt_filter = QLineEdit()
          self.txt_filter.returnPressed.connect(self.do_search)
          btn_search = QPushButton()
          btn_search.setText("Search")
          btn_search.clicked.connect(self.do_search)
      
          # Add the created widgets to the layout
          search_layout.addWidget(lbl_search)
          search_layout.addWidget(self.txt_filter)
          search_layout.addWidget(btn_search)
      
          # Add the Groupbox to the main layout
          main_layout.addWidget(gb_search)
      
          # Create the Results Layout
          self.results_layout = QVBoxLayout()
          self.results_layout.setContentsMargins(0, 0, 0, 0)
      
          # Create a Frame to host the results of search
          self.frm_result = QFrame(parent=self)
          self.frm_result.setLayout(self.results_layout)
      
          # Create a ScrollArea so we can properly handle
          # many entries
          scroll_area = QScrollArea(parent=self)
          scroll_area.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
          scroll_area.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
          scroll_area.setWidgetResizable(True)
      
          # Add the Frame to the scroll area
          scroll_area.setWidget(self.frm_result)
      
          # Add the scroll area to the main layout
          main_layout.addWidget(scroll_area)
      
    • Step 2.3.

      Add the code to connect the QPushButton click and perform the search on our data.

      Note

      Look at the comments over the lines for explanation on what they do.

      def do_search(self):
          # For each widget inside the results frame, lets destroy them
          for widget in self.frm_result.findChildren(QWidget):
              widget.setParent(None)
              widget.deleteLater()
      
          # Grab the filter text
          filter_text = self.txt_filter.text()
      
          # For every entry in the dataset...
          for entry in self.data:
              # Check if they match our filter
              if filter_text.upper() not in entry.upper():
                  continue
              # Create a PyDMEmbeddedDisplay for this entry
              disp = PyDMEmbeddedDisplay(parent=self)
              disp.macros = json.dumps({"MOTOR":entry})
              disp.filename = 'inline_motor.ui'
              disp.setMinimumWidth(700)
              disp.setMinimumHeight(40)
              disp.setMaximumHeight(100)
              # Add the Embedded Display to the Results Layout
              self.results_layout.addWidget(disp)
      

      Important

      Since PyDM v1.6.0 it is no longer required to call pydm.utilities.connection.establish_widget_connections and pydm.utilities.connection.close_widget_connections.

  • Step 3.

    Save this file as all_motors.py.

    Warning

    For this tutorial it is important to use this file name as it will be referenced at the other sections. If you change it please remember to also change at the other steps when referenced.

  • Step 4.

    Test the All Motors Screen:

    pydm all_motors.py
    
    ../../_images/all_motors.gif

Note

You can download this file using this link.