4.12. Creating dialogs in SMITER

There are prepared classes for writing dialogs in SMITER that may ease the writing of a dialog. Otherwise to create a dialog simply use the PyQt5 module for creating the GUI and provide corresponding functions that are called when using the dialog.

In this howto we will write a simple dialog that accepts a input field and upon clicking Apply it will open a message box and printing the input string. We will also use the prepared base classes for writing simple dialogs.

4.12.1. Creating the file

Create a file example_dialog.py in the smiter/SMITER/src/Dialogs directory. The naming convention for the files is Snake Case.

4.12.2. Writing the code

The base code:

from SMITER_dialogs import Dialog


class ExampleDialog(Dialog):
    def __init__(self, parent=None):
        super(ExampleDialog, self).__init__(parent)

if __name__ == '__main__':
    from qtsalome import QApplication

    # Check if there already is an instance of QApplication
    app = QApplication.instance()
    execute = False

    if app is None:
        # Create an instance of QApplication
        execute = True
        app = QApplication([])

    # Instance the dialog and show it.
    d = ExampleDialog()
    d.show()

    if execute:
        # Run only if there was no QApplication instance found.
        app.exec_()

At the top we import the SMITER_dialogs.Dialog class from which our ExampleDialog will inherit.

The initialize function always takes first argument as a parent (optional) and immediately initializes the inherited class by calling super, which takes our dialog class name as argument and a reference to itself. So if you change the class name to SomeDifferentDialog the super function would be super(SomeDifferentDialog, self).__init__(parent).

That’s it for the dialog and on the bottom we have a conditional code block used for testing the dialog by running it outside or inside of SMITER. So far this takes most of the example and should be of no concern to us.

4.12.3. Running the code outside of SMITER

4.12.3.1. Linux

In Linux you can run the dialog, to see if the layout is correct. To do that the SMITER environment must be loaded, which is done by sourcing the env_launch.sh file in SMITER top directory.

# Go to SMITER top directory
cd smiter
# Generate the env_launch.sh file
./sat environ smiter
# Load the env_launch.sh
source env_launch.sh
# Run the example_dialog.py
python SMITER/src/Dialogs/example_dialog.py

4.12.3.2. Windows

In Windows, if you wish to test how the Dialog looks, you will have to run it from SMITER. Run SMITER and then in the python console write

exec(open('C:\path\to\smiter\SMITER\src\Dialogs\example_dialog.py').read())

4.12.3.3. Result

The following dialog should pop up.

../_images/dialogs_1.png

We can click on Apply, Cancel, Help, and only the Cancel will actually do something, that is, closing the dialog.

4.12.4. Populating the code further

Now we change the dialogs name to Example dialog. After the line with the super statement, we will use self.setWindowTitle('Example dialog').

def __init__(self, parent=None):
   super(ExampleDialog, self).__init__(parent)
   self.setWindowTitle('Example dialog')

Next step we will add an input field. For this we will use a method called addSpecialWidgets to our ExampleDialog class.

def addSpecialWidgets(self):
    self.inputField = self.addLineEdit('Please input:')

If we rerun the dialog, we will get

../_images/dialogs_2.png

Next step is to process what has been given to the dialog. For this purpose, adding parameters to the dialog and clicking Apply, we use the method onAppl.

def onApply(self):
    # Using the variable self.inputField we extract the text.
    text = self.inputField.text()

    # Now we will display the message with the use of
    # ``self.displayMessage``, which takes a title and text for its
    # arguments.
    self.displayMessage('Message!', text)

Now if we rerun the dialog and write something to the input field and click on Apply, we should get

../_images/dialogs_3.png

4.12.5. Resulting code

from SMITER_dialogs import Dialog

class ExampleDialog(Dialog):
    def __init__(self, parent=None):
        super(ExampleDialog, self).__init__(parent)
        self.setWindowTitle('Example dialog')

    def addSpecialWidgets(self):
        self.inputField = self.addLineEdit('Please input:')

    def onApply(self):
        # Using the variable self.inputField we extract the text.
        text = self.inputField.text()

        # Now we will display the message with the use of
        # ``self.displayMessage``, which takes a title and text for its
        # arguments.
        self.displayMessage('Message!', text)


if __name__ == '__main__':
    from qtsalome import QApplication

    # Check if there already is an instance of QApplication
    app = QApplication.instance()
    execute = False

    if app is None:
        # Create an instance of QApplication
        execute = True
        app = QApplication([])

    # Instance the dialog and show it.
    d = ExampleDialog()
    d.show()

    if execute:
        # Run only if there was no QApplication instance found.
        app.exec_()

Note

For more information on the base dialog check SMITER dialog

4.12.6. Writing more specific dialogs

If the base dialog does not provide enough functionality, i.e. you wish multiple layouts, showing images and so on, we can of course write our own dialogs or extend the base class.

4.12.6.1. Minimum requirements for a dialog

The dialog must be written as a python class that inherits the QDialog or any other derivites of the QDialog class. It must contains the standard buttons box, for example Apply, Cancel and Help. The dialog must contain a function for each of the before mentioned buttons.

An example of such a dialog:

from qtsalome import (QDialog, QGridLayout, QHBoxLayout, QVBoxLayout,
    QFormLayout, QDialogButtonBox, QLineEdit, QPlainTextEdit, QDialogButtonBox,
    QLabel, QPixmap, pyqtSlot, QMessageBox)
# Do not import salome.geom or salome.smesh here. Not even numpy. This avoids
# the initialization of the said modules when SMITER starts and prolongs the
# activation time.


class MyCustomDialog(QDialog):
    def __init__(self, parent=None):
        super(MyCustomDialog, self).__init__(parent)
        self.setWindowTitle('Example custom dialog')
        self.setupUi()

    def setupUi(self):
        # Setting up the main layout
        layout = QGridLayout()

        # Having 3 sublayouts.
        # +-------------------+---------------------+
        # |    Upper left     |     Upper right     |
        # |       layout      |       layout        |
        # |                   |                     |
        # |-------------------+---------------------+
        # |                                         |
        # |              Lower layout               |
        # |                                         |
        # +-----------------------------------------+

        # Using form layout for simple ``Label : Input field`` widgets.
        # Also contains the ``Apply``, ``Cancel`` and ``Help`` buttons.
        layoutUpperLeft = QFormLayout()

        self.inputLineEdit = QLineEdit('')
        layoutUpperLeft.addRow('Your input', QLineEdit())

        # Use the QDialogButtonBox for adding standard buttons
        buttons = QDialogButtonBox(QDialogButtonBox.Apply |
            QDialogButtonBox.Cancel | QDialogButtonBox.Help)

        # Connect the standard buttons signals
        buttons.accepted.connect(self.onApply)
        buttons.rejected.connect(self.onCancel)
        buttons.helpRequested.connect(self.onHelp)

        layoutUpperLeft.addWidget(buttons)

        # Vertical layout for stacking widgets vertically
        layoutUpperRight = QVBoxLayout()

        self.textEdit = QPlainTextEdit()
        layoutUpperRight.addWidget(self.textEdit)

        # Horizontal layout for stacking widgets horizontally
        layoutLower = QHBoxLayout()

        # Adding two QLabel widgets with images
        image1 = QLabel()
        # Load the image into QPixMap. The image is loaded from Qt resources.
        # To see how to add images
        pixmap = QPixmap(':/SMITER/example_dialog_image1.png')
        image1.setPixmap(pixmap)

        # Do the same
        image2 = QLabel()
        pixmap = QPixmap(':/SMITER/example_dialog_image2.png')
        image2.setPixmap(pixmap)

        layoutLower.addWidget(image1)
        layoutLower.addWidget(image2)

        # Now put the layouts into the main layout

        layout.addLayout(layoutUpperLeft, 0, 0)
        layout.addLayout(layoutUpperRight, 0, 1)

        # The lower layout will expand over columns, thats why we add the
        # additional 1, -1 argument
        layout.addLayout(layoutLower, 1, 0, 1, -1)

        # Set the main layout
        self.setLayout(layout)

    @pyqtSlot()
    def onHelp(self):
        """Display a help message regarding this dialog.
        """
        helpMsg = "This is an example of a custom dialog"
        QMessageBox.information(self, 'Information', helpMsg)

    @pyqtSlot()
    def onApply(self):
        """Do something with the inputs of the dialog here. If you need to
        import modules, such as salome.geom or salome.smesh, do it here and
        not on the top of the file.

        It is preferred to get the input values from the widgets and then call
        a separate function which accepts the input values and do something
        with them. Also, again, it is mandatory if you need certain modules
        such as geompy, smesh or even numpy, import them in the function and
        not on top of the file!

        .. code-block:: python

           from salome.geom import geomBuilder
           geompy = geomBuilder.New()
           # or
           from salome.smesh import smeshBuilder
           sb = smeshBuilder.New()

           # Now ei
        """
        # Do something with the inputs of the dialog
        lineEditText = self.inputLineEdit.text()
        print(lineEditText)

        plainTextEditText = self.textEdit.toPlainText()
        print(plainTextEditText)

    @pyqtSlot()
    def onCancel(self):
        """Either just close the dialog or perform additional actions.
        """
        self.close()

if __name__ == '__main__':
    from qtsalome import QApplication

    # Check if there already is an instance of QApplication
    app = QApplication.instance()
    execute = False

    if app is None:
        # Create an instance of QApplication
        execute = True
        app = QApplication([])

    # Instance the dialog and show it.
    d = MyCustomDialog()
    d.show()

    if execute:
        # Run only if there was no QApplication instance found.
        app.exec_()

If we run this dialog in SMITER as desribed in SMITER dialog the dialog will look like this.

../_images/dialogs_4.png

Note

The code snippets are also available in smiter/SMITER/src/Dialogs directory.

4.12.7. Adding images to Qt resource system

Images and other multimedia files can be added to the Qt resource system.

Specifically, if we want to use images in our dialogs, we should avoid embedding them into the python files itself, but rather add them to the smiter/SMITER/src/Dialogs/images directory and editing the dialog_images.qrc.

The .qrc file is an XML file, in which we can specify a namespace and assign images to the namespace. If we look at the content of smiter/SMITER/src/Dialogs/images/dialog_images.qrc.

<RCC>
  <qresource prefix="SMITER">
    <file>example_dialog_image1.png</file>
    <file>example_dialog_image2.png</file>
  </qresource>
</RCC>

We see that there is a prefix SMITER then a list of image file names, enclosed with the <file></file> markups. That is all what we need to do, and if we want to access or display these images inside PyQt, we simple write the path of the image inside the Qt resource system, i.e. :/SMITER/example_dialog_image1.png. Example code:

from qtsalome import QLabel, QPixmap

label = QLabel()
# Load the image
pixmap = QPixmap(':/SMITER/example_dialog_image1.png')
# Set the pixmap
label.setPixmap(pixmap)
# The image is ready to be displayed.