Create a simple UI

Hi to everyone!

version: 5.6.2
OS: windows11

I’m working in create a robust (but also simple) interface for our python module named SegmentTracer. This code contains several classes and create/remove buttons begins to be hard.

Watching some Scripted Modules as could be Endoscopy I see that the most common way to do that is using a classUI to manage all the functions related with the UI. I am not familiarized with ScriptedLoadableModuleWidget but I have made a complete Qt integration:

import slicer
import qt

class SegmentTracerUI:
    def __init__(self):
        
        
        self.dock_widget = slicer.util.mainWindow().findChild(qt.QDockWidget,"SegmentTracerDockWidget")
        
        if self.dock_widget:
            self.dock_layout = self.dock_widget.findChild(qt.QWidget).layout()
            self.label = self.dock_widget.findChild(qt.QLabel)

       
        else:
            self.main_window.setStatusBar(None)
            self.create_dock_widget()

    def create_dock_widget(self, title="ST Assistant"):
        """Crea el dock widget y lo agrega a la interfaz."""
        self.dock_widget = qt.QDockWidget(title)
        self.dock_widget.setObjectName("SegmentTracerDockWidget")
        
        # Contenedor y layout
        dock_content = qt.QWidget()
        self.dock_layout = qt.QVBoxLayout(dock_content)
        
        # Label de bienvenida
        self.label = qt.QLabel("Welcome to SegmentTracer.")
        self.label.setFont(qt.QFont("Times", 11))
        self.dock_layout.addWidget(self.label)
        
        self.dock_widget.setWidget(dock_content)
        self.main_window.addDockWidget(qt.Qt.LeftDockWidgetArea, self.dock_widget)
        
        

    def create_button(self, function, text):
        """Crea un botón y lo añade al dock."""
        button = qt.QPushButton(text)
        button.clicked.connect(function)
        self.dock_layout.addWidget(button)
        slicer.app.processEvents()
        return button

    def clear_buttons(self):
        """Elimina todos los botones del dock, preservando solo el QLabel."""
        for i in reversed(range(self.dock_layout.count())):
            widget = self.dock_layout.itemAt(i).widget()
            if widget and widget is not self.label:
                widget.setParent(None)
                widget.deleteLater()
        slicer.app.processEvents()

    def show_message(self, text):
        """Cambia el texto del QLabel dentro del dock."""
        self.label.setText(text)
        print(text)
        slicer.app.processEvents()

What I am wondering is:

1º Can I modify something to guarantee a better user experience? (i.e. not blocking the Slicer window)

2º Also, to link one function to a specific button I use:

self._continue_action = action_creator(myFunction, event_type = 'Continue', args = [myFunctionArg1,myFunctionArg2,...])
self.button_continue = ui.create_button(self.button_continue_action, 'Continue')

where the function action_creator is the following function:

    def action_creator(self, function, event_type, args = []):

        if event_type == 'Continue':
            slicer.app.processEvents()
            def action():
                if len(args) > 0:
                    function(*args)
                else:
                    function()
                return None
            return action

Which looks a little bit difficult… Has anyone any idea to associate the button with the functions and their args?

Thanks a lot! :slight_smile:

Update: I use this class now to manage all the UI:


class SegmentTracerUI:
    def __init__(self):
        slicer.util.mainWindow().setStatusBar(None)
 
        

    def createDockInterface(self, title="ST Assistant"):
        """Crea el dock widget y lo agrega a la interfaz."""
        self.dock_widget = qt.QDockWidget(title)
        self.dock_widget.setObjectName("SegmentTracerDockWidget")
        
        # Contenedor y layout
        dock_content = qt.QWidget()
        self.dock_layout = qt.QVBoxLayout(dock_content)
        
        # Label de bienvenida
        self.label = qt.QLabel("Welcome to SegmentTracer.")
        self.label.setFont(qt.QFont("Times", 11))
        self.dock_layout.addWidget(self.label)
        
        self.dock_widget.setWidget(dock_content)
        slicer.util.mainWindow().addDockWidget(qt.Qt.LeftDockWidgetArea, self.dock_widget)

    def create_button(self, function, text, type, args=[]):
        """Crea un botón y lo añade al dock con la opción de pasarle argumentos a la función."""
        button = qt.QPushButton(text)
        
        # Asignación de la función que actualiza el estado global
        button.clicked.connect(lambda: self.on_button_pressed(type, function, args))
        
        self.dock_layout.addWidget(button)
        slicer.app.processEvents()
        return button

    def on_button_pressed(self, button_type, function, args):
        """Maneja la lógica cuando un botón es presionado, actualiza el estado global y llama a la función."""
        self.choice = button_type
        print(self.choice)
        function(*args)
        
    def fastRemoveButtons(self): # Más rápida que la antigua removeButtons
        """Elimina todos los botones del dock, preservando solo el QLabel, de manera más eficiente."""
       
        widgets_to_remove = [self.dock_layout.itemAt(i).widget() for i in range(self.dock_layout.count())]
        
        for widget in widgets_to_remove:
            if widget and widget is not self.label:
                widget.delete()
    
       
        slicer.app.processEvents()
        

    def removeButtons(self):
        
        """Elimina todos los botones del dock, preservando solo el QLabel."""
        for i in reversed(range(self.dock_layout.count())):
            widget = self.dock_layout.itemAt(i).widget()
            if widget and widget is not self.label:
                widget.setParent(None)
                widget.deleteLater()
        slicer.app.processEvents()

    def showMessage(self, text):
        """Cambia el texto del QLabel dentro del dock."""
        self.label.setText(text)
        print(text)
        slicer.app.processEvents()

This works fine most of the times but, sometimes (creating/deleting buttons) it crashes and slicer app closes suddenly. The crash is not related with an specific part of the code, because it not always fail in the same place or even fail…

I have reading something about delete/deleteLater functions in qt and the related dangers when you try to delete a button in some specific thread, but I’m not sure if this can affect in this case.

Can anybody tell me how to remove/ create the buttons in a better way?

Thanks a lot.

Hi,

It isn’t clear from the description above if the buttons are truly dynamic (come and go as the module is being used) or removing them is just part of your module cleanup.

Either way, my (non-expert) advice is to use the extension wizard, which is probably less simple (in the sense that it introduces more components and concepts) but easier.

Hi @shai-ikko , the buttons are dynamic in the sense of I have other classes that calls the UI and this functions in the new class creates/deletes buttons during the process allowing the user take decisions. Usually a function in the main class execute some code (for example segment something in a volume) and then creates a button to ask if the segment is OK or it should be re-done.

When the user clicks the button, the interface has to remove all the buttons and continue the execution (for example, saving the segment or create other buttons…)

Example use:

 def startQuestioningReSegmentation(self):

   
        self.stUI.showMessage('What do you want to do now?')

        # Button generation: 
        self.button_left = self.stUI.create_button(self.startAddNewPointsToSegmentationByEvent, text = 'Add points at Left', type = True, args = []) # Botón para poner puntos en la rama izq., equivalente a la pregunta: Ponemos puntos en la izq.?-> True
        self.button_right  = self.stUI.create_button(self.startAddNewPointsToSegmentationByEvent, text = 'Add points at Right', type = False, args = []) # Botón para poner puntos en la rama der., equivalente a la pregunta: Ponemos puntos en la izq.?-> False
        



    def startAddNewPointsToSegmentationByEvent(self):

        self.stUI.showMessage('Select first point')
        # Delete Buttons:
        self.stUI.fastRemoveButtons()
        
        # Register Button:

        self.button_register = self.stUI.create_button(self.addNewPointsToSegmentationByEvent, text = 'Register Point', type = 'Continue', args = [])

In other things, where I can find some examples of extensions-wizard?

The Extension Wizard is a tool that is included in Slicer:

Re dynamics – unless your requirements are truly exceptional, the more common practice is to create all the buttons in advance, and disable or hide those which aren’t relevant, rather than create and destroy them on the go. If you do it that way, you will be more likely to find similar examples, and answers from people working in similar ways.

That said, of course you know your requirements and I don’t.

1 Like