How to add my own module widget to another module?

Hi all,
I have been using Slicer for some basic tasks in the past and just now have started developing my own module. My concept was to simplify the workflow for a clinician to annotate several images. The module should do the communication with an XNAT server to pull the images and push the annotation files (markup JSON files).

Initially, my concept was to create a new GUI that mirrors and wraps the relevant functionality of the Markups module. Learning more about the concept of reusable widgets, I tried to include the widgets I need from Markups. The widget I am missing would be what is in the Control Points collapsible button - mainly the functions on the activeMarkupTableWidget. It seems to have not been made available as a widget or the function logic made accessible from outside so that I could easily replicate the functionality.

I then used slicer.modules.markups.createNewWidgetRepresentation() to include the Markups module into my module but still was missing the functionality of the activeMarkupTableWidget. I tried replicating the setup according to the .cxx file but seem to not understand enough of it to connect the data node and actually display the control points in the table.

My next approach was to add my new functionality as a widget into the markups module (and hide all the UI elements not needed). This might actually make the most sense design wise, according to my current understanding. I managed to clean up and simplify the markups module as desired but struggle to see what needs to be done to instantiate my module as a widget.

Trying this command:

slicer.util.getModuleGui("Markups").layout().addWidget(MyModuleWidget())

I get the following error:

'MyModuleWidget' object has no attribute 'createNewWidgetRepresentation'

I am a bit confused here. Starting from the wizard to create a scriptable module, I end up with a python file containing the following classes: MyModule, MyModuleWidget, MyModuleLogic, MyModuleTest, as well as the GUI file. I would have expected to be able to intantiate the widget as it is. Do I need to implement createNewWidgetRepresentation or add some other bits? I couldn’t find examples for this.

I mentioned my path to arrive at this question as any advice/ comment on this would be appreciated as well and might allow a better approach.
The main question is though: How to add my own module widget to another module?

Here seems to be a related issue with the Markups module:

slicer.qSlicerSimpleMarkupsWidget() is a reusable widget for managing markup control points. For example, it is used in Fiducial registration wizard module of SlicerIGT extension.

image

You can also create simple custom widget for landmarking, such as in OrthodonticAnalysis extension:

We plan to factor out the control point list that you see in Markups module (with all the landmarking features):

I’m not sure if there is any funded project that requires this feature, so you need to wait for a while - or contribute time or funding to make it happen sooner.

1 Like

Thank you a lot, Andras. These are very useful recommendations! :slight_smile:

Do you also have an advice or a pointer towards how to make one’s widget available to be added to other modules? What has to be done to create an actual widget, not just a full module, from the MyModuleWidget() class?

You can import the widget in a separate class (if you work in Python you can put it into a separate file, but it may also stay in the single module .py file) and import and use it in another module.

Most of the reusable widgets are implemented in Slicer core in C++, but there are a few examples in Python in extensions, for example in Cardiac Device Simulator module in SlicerHeart.

1 Like

I wanted to create two widgets in my module, one widget with the main functionality and the second widget that modifies the markup module GUI (to only show the control points table) and attaches the first widget to the markup layout. This is a hack to go around re-implementing the markup control point table.

The problem with this approach is, that I cannot create two widgets using ScriptedLoadableModuleWidget and .ui files. When setting up the module and two widget classes in the following way:

class MyModule(ScriptedLoadableModule):...

class MyModuleWidget(ScriptedLoadableModuleWidget, VTKObservationMixin):...

class MyModuleSecondWidget(ScriptedLoadableModuleWidget, VTKObservationMixin):...

I get the following error when calling ScriptedLoadableModuleWidget.setup(self) for the second widget:

AttributeError: module 'modules' has no attribute 'mymodulesecond'

I didn’t manage to understand how to create the second widget without slicer expecting a corresponding module.

I then tried to follow the example of the Cardiac Device Simulator module and used qt.QWidget instead of ScriptedLoadableModuleWidget. To be able to load and use the second .ui file, I had to replace the function self.layout.addWidget(uiWidget) with

layout = qt.QVBoxLayout()
layout.addWidget(uiWidget)
self.setLayout(layout)

This has worked for me but I wanted to report my process for future reference and to mention the issue with using two widget classes based on ScriptedLoadableModuleWidget.

Only the <ModuleName>Widget class is special in the sense that Slicer discovers and instantiates that automatically. You can create all other widgets as regular Python classes and instantiate them as usual. If you create a new widget MyCustomWidget then you can access it from another module like this:

import MyModule
w = MyModule.MyCustomWidget()
1 Like

Ok, understood.

I think it threw me off, that it doesn’t seem possible to have multiple widget classes using ScriptedLoadableModuleWidget in the same module. It took me a while to identify the problem (though I still don’t understand why it behaves like this) and to find the workaround going back to QWidget. If this is the intended behaviour of widgets, it could be useful including a second widget setup in the module wizard/ demo module.

You can add any number of widgets from .ui files using loadUI - see documentation and example here.

When creating two widget classes (in the same module file) as in the wizard demo file using
class MyModuleWidget(ScriptedLoadableModuleWidget, VTKObservationMixin):...,
I am not able to add both widgets to the module. Both widgets would be expected to be in a accordingly named module.
Thus, following the module wizard and tutorial, I am not offered the tools for creating a widget that can be reused. I think it would be great extending the demo module by a second widget (that is not a ScriptedLoadableModuleWidget) and which is then added to the main ScriptedLoadableModuleWidget. For me, as someone new to Slicer development and without experience in QT, it took quite some effort to find a way to do what I wanted.

You provided a lot of useful information that helped me move forward, but the essential step was using qt.QWidget for the widget creation which I haven’t seen directly in other modules I was looking at. This knowledge seems key to developing reusable widgets which is a core aim for the widget concept as I understand it.

You have only one module widget, i.e., the widget that is shown in the left panel when you switch to the module. But you can create many widgets in Qt designer, save each in a ui file, and at runtime instantiate the widgets from the ui files and add them to the module widget.

Creating reusable widgets is an advanced topic, not covered by wizards or tutorials. If you are new to Qt then I would recommend to not start with creating reusable widgets, but use and customize the already existing reusable widgets provided by Qt, CTK, and Slicer.

It is usually not needed to create reusable widgets in Python, because scripting is just used for putting together the end-user workflow from existing components. You rarely if ever need to develop new GUI components in Python.

Can you write a bit about your overall task and plan to implement your solution? We could then probably provide more directly useful answers.