├── .gitignore ├── 02__Interact_Debug_&_Pipeline ├── CMakeLists.txt ├── README.md ├── SL_Tutorials.png ├── sl_02__Summary.png └── t_ApplyThreshold │ ├── CMakeLists.txt │ ├── Resources │ ├── Icons │ │ └── t_ApplyThreshold.png │ └── UI │ │ └── t_ApplyThreshold.ui │ ├── Testing │ ├── CMakeLists.txt │ └── Python │ │ └── CMakeLists.txt │ └── t_ApplyThreshold.py ├── 03__DeveloperFoci_NeatBlanks_&_GUI_HelloWorld ├── CMakeLists.txt ├── README.md ├── SL_Tutorials.png ├── sl_03__Summary.png └── sl__GUI_HelloWorld │ ├── CMakeLists.txt │ ├── Resources │ ├── Icons │ │ └── sl__GUI_HelloWorld.png │ └── UI │ │ └── sl__GUI_HelloWorld.ui │ ├── Testing │ ├── CMakeLists.txt │ └── Python │ │ └── CMakeLists.txt │ └── sl__GUI_HelloWorld.py ├── 04__CodeStyle_MethodGroups_&_US_SeqViewer ├── CMakeLists.txt ├── README.md ├── SL_Tutorials.png ├── sl_04__Summary.png └── sl__US_SeqViewer │ ├── CMakeLists.txt │ ├── Resources │ ├── Icons │ │ └── sl__US_SeqViewer.png │ └── UI │ │ └── sl__US_SeqViewer.ui │ ├── Testing │ ├── CMakeLists.txt │ └── Python │ │ └── CMakeLists.txt │ └── sl__US_SeqViewer.py ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Large Figures' Folder (all saved on GoogleDrive) 2 | _Figures/ 3 | 4 | 5 | 99__Test__ToBeReduced/ 6 | 7 | 8 | 9 | # PyCharm Files 10 | .idea/ 11 | 12 | 13 | # Byte-compiled / optimized / DLL files 14 | __pycache__/ 15 | *.py[cod] 16 | *$py.class 17 | 18 | # C extensions 19 | *.so 20 | 21 | # Distribution / packaging 22 | .Python 23 | build/ 24 | develop-eggs/ 25 | dist/ 26 | downloads/ 27 | eggs/ 28 | .eggs/ 29 | lib/ 30 | lib64/ 31 | parts/ 32 | sdist/ 33 | var/ 34 | wheels/ 35 | pip-wheel-metadata/ 36 | share/python-wheels/ 37 | *.egg-info/ 38 | .installed.cfg 39 | *.egg 40 | MANIFEST 41 | 42 | # PyInstaller 43 | # Usually these files are written by a python script from a template 44 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 45 | *.manifest 46 | *.spec 47 | 48 | # Installer logs 49 | pip-log.txt 50 | pip-delete-this-directory.txt 51 | 52 | # Unit test / coverage reports 53 | htmlcov/ 54 | .tox/ 55 | .nox/ 56 | .coverage 57 | .coverage.* 58 | .cache 59 | nosetests.xml 60 | coverage.xml 61 | *.cover 62 | *.py,cover 63 | .hypothesis/ 64 | .pytest_cache/ 65 | 66 | # Translations 67 | *.mo 68 | *.pot 69 | 70 | # Django stuff: 71 | *.log 72 | local_settings.py 73 | db.sqlite3 74 | db.sqlite3-journal 75 | 76 | # Flask stuff: 77 | instance/ 78 | .webassets-cache 79 | 80 | # Scrapy stuff: 81 | .scrapy 82 | 83 | # Sphinx documentation 84 | docs/_build/ 85 | 86 | # PyBuilder 87 | target/ 88 | 89 | # Jupyter Notebook 90 | .ipynb_checkpoints 91 | 92 | # IPython 93 | profile_default/ 94 | ipython_config.py 95 | 96 | # pyenv 97 | .python-version 98 | 99 | # pipenv 100 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 101 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 102 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 103 | # install all needed dependencies. 104 | #Pipfile.lock 105 | 106 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 107 | __pypackages__/ 108 | 109 | # Celery stuff 110 | celerybeat-schedule 111 | celerybeat.pid 112 | 113 | # SageMath parsed files 114 | *.sage.py 115 | 116 | # Environments 117 | .env 118 | .venv 119 | env/ 120 | venv/ 121 | ENV/ 122 | env.bak/ 123 | venv.bak/ 124 | 125 | # Spyder project settings 126 | .spyderproject 127 | .spyproject 128 | 129 | # Rope project settings 130 | .ropeproject 131 | 132 | # mkdocs documentation 133 | /site 134 | 135 | # mypy 136 | .mypy_cache/ 137 | .dmypy.json 138 | dmypy.json 139 | 140 | # Pyre type checker 141 | .pyre/ 142 | -------------------------------------------------------------------------------- /02__Interact_Debug_&_Pipeline/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.16.3...3.19.7 FATAL_ERROR) 2 | 3 | project(SL_Tutorials) 4 | 5 | #----------------------------------------------------------------------------- 6 | # Extension meta-information 7 | set(EXTENSION_HOMEPAGE "https://www.slicer.org/wiki/Documentation/Nightly/Extensions/SL_Tutorials") 8 | set(EXTENSION_CATEGORY "Tutorials") 9 | set(EXTENSION_CONTRIBUTORS "Sen Li (ETS)") 10 | set(EXTENSION_DESCRIPTION "This is an example of a simple extension") 11 | set(EXTENSION_ICONURL "https://www.example.com/Slicer/Extensions/SL_Tutorials.png") 12 | set(EXTENSION_SCREENSHOTURLS "https://www.example.com/Slicer/Extensions/SL_Tutorials/Screenshots/1.png") 13 | set(EXTENSION_DEPENDS "NA") # Specified as a list or "NA" if no dependencies 14 | 15 | #----------------------------------------------------------------------------- 16 | # Extension dependencies 17 | find_package(Slicer REQUIRED) 18 | include(${Slicer_USE_FILE}) 19 | 20 | #----------------------------------------------------------------------------- 21 | # Extension modules 22 | add_subdirectory(t_ApplyThreshold) 23 | ## NEXT_MODULE 24 | 25 | #----------------------------------------------------------------------------- 26 | include(${Slicer_EXTENSION_GENERATE_CONFIG}) 27 | include(${Slicer_EXTENSION_CPACK}) 28 | -------------------------------------------------------------------------------- /02__Interact_Debug_&_Pipeline/README.md: -------------------------------------------------------------------------------- 1 | # 3D Slicer Extension Tutorial: Step by Step 2 | 3 | 4 |
5 |
6 | 7 | ## Step 02: Interact, Debug & Pipeline 8 | [![IMAGE ALT TEXT HERE](https://img.youtube.com/vi/Gyd-q4NAI7U/0.jpg)](https://www.youtube.com/watch?v=Gyd-q4NAI7U&list=PLTuWbByD80TORd1R-J7j7nVQ9fot3C2fK) 9 | 10 | #### YouTube Video Tutorial for Step_02 11 | 12 | #### Bilibili Video Tutorial for Step_02 13 | 14 | isolated 15 | -------------------------------------------------------------------------------- /02__Interact_Debug_&_Pipeline/SL_Tutorials.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SenonETS/3DSlicerTutorial_ExtensionModuleDevelopment/c2ed7fd7e94f79953ec38019b16ca90019817bbc/02__Interact_Debug_&_Pipeline/SL_Tutorials.png -------------------------------------------------------------------------------- /02__Interact_Debug_&_Pipeline/sl_02__Summary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SenonETS/3DSlicerTutorial_ExtensionModuleDevelopment/c2ed7fd7e94f79953ec38019b16ca90019817bbc/02__Interact_Debug_&_Pipeline/sl_02__Summary.png -------------------------------------------------------------------------------- /02__Interact_Debug_&_Pipeline/t_ApplyThreshold/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | #----------------------------------------------------------------------------- 2 | set(MODULE_NAME t_ApplyThreshold) 3 | 4 | #----------------------------------------------------------------------------- 5 | set(MODULE_PYTHON_SCRIPTS 6 | ${MODULE_NAME}.py 7 | ) 8 | 9 | set(MODULE_PYTHON_RESOURCES 10 | Resources/Icons/${MODULE_NAME}.png 11 | Resources/UI/${MODULE_NAME}.ui 12 | ) 13 | 14 | #----------------------------------------------------------------------------- 15 | slicerMacroBuildScriptedModule( 16 | NAME ${MODULE_NAME} 17 | SCRIPTS ${MODULE_PYTHON_SCRIPTS} 18 | RESOURCES ${MODULE_PYTHON_RESOURCES} 19 | WITH_GENERIC_TESTS 20 | ) 21 | 22 | #----------------------------------------------------------------------------- 23 | if(BUILD_TESTING) 24 | 25 | # Register the unittest subclass in the main script as a ctest. 26 | # Note that the test will also be available at runtime. 27 | slicer_add_python_unittest(SCRIPT ${MODULE_NAME}.py) 28 | 29 | # Additional build-time testing 30 | add_subdirectory(Testing) 31 | endif() 32 | -------------------------------------------------------------------------------- /02__Interact_Debug_&_Pipeline/t_ApplyThreshold/Resources/Icons/t_ApplyThreshold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SenonETS/3DSlicerTutorial_ExtensionModuleDevelopment/c2ed7fd7e94f79953ec38019b16ca90019817bbc/02__Interact_Debug_&_Pipeline/t_ApplyThreshold/Resources/Icons/t_ApplyThreshold.png -------------------------------------------------------------------------------- /02__Interact_Debug_&_Pipeline/t_ApplyThreshold/Resources/UI/t_ApplyThreshold.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | t_ApplyThreshold 4 | 5 | 6 | 7 | 0 8 | 0 9 | 279 10 | 286 11 | 12 | 13 | 14 | 15 | 16 | 17 | Inputs 18 | 19 | 20 | 21 | 22 | 23 | Input volume: 24 | 25 | 26 | 27 | 28 | 29 | 30 | Pick the input to the algorithm. 31 | 32 | 33 | 34 | vtkMRMLScalarVolumeNode 35 | 36 | 37 | 38 | false 39 | 40 | 41 | false 42 | 43 | 44 | false 45 | 46 | 47 | 48 | 49 | 50 | 51 | Image threshold: 52 | 53 | 54 | 55 | 56 | 57 | 58 | Set threshold value for computing the output image. Voxels that have intensities lower than this value will set to zero. 59 | 60 | 61 | 0.100000000000000 62 | 63 | 64 | -100.000000000000000 65 | 66 | 67 | 500.000000000000000 68 | 69 | 70 | 0.500000000000000 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | Outputs 81 | 82 | 83 | 84 | 85 | 86 | Thresholded volume: 87 | 88 | 89 | 90 | 91 | 92 | 93 | Pick the output to the algorithm. 94 | 95 | 96 | 97 | vtkMRMLScalarVolumeNode 98 | 99 | 100 | 101 | false 102 | 103 | 104 | true 105 | 106 | 107 | true 108 | 109 | 110 | true 111 | 112 | 113 | 114 | 115 | 116 | 117 | Inverted volume: 118 | 119 | 120 | 121 | 122 | 123 | 124 | Result with inverted threshold will be written into this volume 125 | 126 | 127 | 128 | vtkMRMLScalarVolumeNode 129 | 130 | 131 | 132 | false 133 | 134 | 135 | true 136 | 137 | 138 | true 139 | 140 | 141 | true 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | Advanced 152 | 153 | 154 | true 155 | 156 | 157 | 158 | 159 | 160 | Invert threshold: 161 | 162 | 163 | 164 | 165 | 166 | 167 | If checked, values above threshold are set to 0. If unchecked, values below are set to 0. 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | false 181 | 182 | 183 | Run the algorithm. 184 | 185 | 186 | Apply 187 | 188 | 189 | 190 | 191 | 192 | 193 | Qt::Vertical 194 | 195 | 196 | 197 | 20 198 | 40 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | ctkCollapsibleButton 208 | QWidget 209 |
ctkCollapsibleButton.h
210 | 1 211 |
212 | 213 | ctkSliderWidget 214 | QWidget 215 |
ctkSliderWidget.h
216 |
217 | 218 | qMRMLNodeComboBox 219 | QWidget 220 |
qMRMLNodeComboBox.h
221 |
222 | 223 | qMRMLWidget 224 | QWidget 225 |
qMRMLWidget.h
226 | 1 227 |
228 |
229 | 230 | 231 | 232 | t_ApplyThreshold 233 | mrmlSceneChanged(vtkMRMLScene*) 234 | inputSelector 235 | setMRMLScene(vtkMRMLScene*) 236 | 237 | 238 | 122 239 | 132 240 | 241 | 242 | 248 243 | 61 244 | 245 | 246 | 247 | 248 | t_ApplyThreshold 249 | mrmlSceneChanged(vtkMRMLScene*) 250 | outputSelector 251 | setMRMLScene(vtkMRMLScene*) 252 | 253 | 254 | 82 255 | 135 256 | 257 | 258 | 220 259 | 161 260 | 261 | 262 | 263 | 264 | t_ApplyThreshold 265 | mrmlSceneChanged(vtkMRMLScene*) 266 | invertedOutputSelector 267 | setMRMLScene(vtkMRMLScene*) 268 | 269 | 270 | 161 271 | 8 272 | 273 | 274 | 173 275 | 176 276 | 277 | 278 | 279 | 280 |
281 | -------------------------------------------------------------------------------- /02__Interact_Debug_&_Pipeline/t_ApplyThreshold/Testing/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_subdirectory(Python) 2 | -------------------------------------------------------------------------------- /02__Interact_Debug_&_Pipeline/t_ApplyThreshold/Testing/Python/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | 2 | #slicer_add_python_unittest(SCRIPT ${MODULE_NAME}ModuleTest.py) 3 | -------------------------------------------------------------------------------- /02__Interact_Debug_&_Pipeline/t_ApplyThreshold/t_ApplyThreshold.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | import vtk 5 | 6 | import slicer 7 | from slicer.ScriptedLoadableModule import * 8 | from slicer.util import VTKObservationMixin 9 | 10 | '''==================================================================================================================''' 11 | '''==================================================================================================================''' 12 | # 13 | # t_ApplyThreshold 14 | # 15 | class t_ApplyThreshold(ScriptedLoadableModule): 16 | 17 | def __init__(self, parent): 18 | ScriptedLoadableModule.__init__(self, parent) 19 | self.parent.title = "t_ApplyThreshold" 20 | self.parent.categories = ["SL_Tutorials"] 21 | self.parent.dependencies = [] # TODO: add here list of module names that this module requires 22 | self.parent.contributors = ["Sen Li (École de Technologie Supérieure)"] 23 | # TODO: update with short description of the module and a link to online module documentation 24 | self.parent.helpText = """This is t_ApplyThreshold ! """ 25 | # TODO: replace with organization, grant and thanks 26 | self.parent.acknowledgementText = """ 27 | This file was originally developed by Jean-Christophe Fillion-Robin, Kitware Inc., Andras Lasso, PerkLab, 28 | and Steve Pieper, Isomics, Inc. and was partially funded by NIH grant 3P41RR013218-12S1. 29 | """ 30 | 31 | # Additional initialization step after application startup is complete 32 | slicer.app.connect("startupCompleted()", registerSampleData) 33 | 34 | print("t_ApplyThreshold(ScriptedLoadableModule): __init__(self, parent)") 35 | 36 | '''==================================================================================================================''' 37 | '''==================================================================================================================''' 38 | # 39 | # t_ApplyThresholdWidget 40 | # 41 | class t_ApplyThresholdWidget(ScriptedLoadableModuleWidget, VTKObservationMixin): 42 | 43 | def __init__(self, parent=None): 44 | """ Called when the user opens the module the first time and the widget is initialized. """ 45 | ScriptedLoadableModuleWidget.__init__(self, parent) 46 | VTKObservationMixin.__init__(self) # needed for parameter node observation 47 | self.logic = None 48 | self._parameterNode = None # SingleTon initialized through self.setParameterNode(self.logic.getParameterNode()) 49 | self._updatingGUIFromParameterNode = False 50 | print("**Widget.__init__(self, parent)") 51 | 52 | # ------------------------------------------------------------------------------------------------------------------ 53 | def setup(self): 54 | print("**Widget.setup(self), \tSL_Developer") 55 | 56 | """ 00. Called when the user opens the module the first time and the widget is initialized. """ 57 | ScriptedLoadableModuleWidget.setup(self) 58 | 59 | # 01. Load widget from .ui file (created by Qt Designer). 60 | # Additional widgets can be instantiated manually and added to self.layout. 61 | uiWidget = slicer.util.loadUI(self.resourcePath('UI/t_ApplyThreshold.ui')) 62 | self.layout.addWidget(uiWidget) 63 | self.ui = slicer.util.childWidgetVariables(uiWidget) 64 | 65 | # 02. Set scene in MRML widgets. Make sure that in Qt designer the 66 | # top-level qMRMLWidget's "mrmlSceneChanged(vtkMRMLScene*)" signal in is connected to 67 | # each MRML widget's "setMRMLScene(vtkMRMLScene*)" slot. 68 | uiWidget.setMRMLScene(slicer.mrmlScene) 69 | 70 | # 03. Create logic class. Logic implements all computations that should be possible to run 71 | # in batch mode, without a graphical user interface. 72 | self.logic = t_ApplyThresholdLogic() 73 | 74 | # 04. Connections, ensure that we update parameter node when scene is closed 75 | self.addObserver(slicer.mrmlScene, slicer.mrmlScene.StartCloseEvent, self.onSceneStartClose) 76 | self.addObserver(slicer.mrmlScene, slicer.mrmlScene.EndCloseEvent, self.onSceneEndClose) 77 | 78 | # 05. SL_Developer. Connect Signal-Slot, ensure that whenever user changes some settings on the GUI, 79 | # that is saved in the MRML scene (in the selected parameter node). 80 | self.ui.inputSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.updateParameterNodeFromGUI) 81 | self.ui.outputSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.updateParameterNodeFromGUI) 82 | self.ui.imageThresholdSliderWidget.connect("valueChanged(double)", self.updateParameterNodeFromGUI) 83 | self.ui.invertOutputCheckBox.connect("toggled(bool)", self.updateParameterNodeFromGUI) 84 | self.ui.invertedOutputSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.updateParameterNodeFromGUI) 85 | 86 | self.ui.applyButton.connect('clicked(bool)', self.onApplyButton) 87 | 88 | # 06. Needed for programmer-friendly Module-Reload where the Module had already been enter(self)-ed; 89 | # Otherwise, will initial through function enter(self) 90 | if self.parent.isEntered: 91 | self.initializeParameterNode() 92 | 93 | # ------------------------------------------------------------------------------------------------------------------ 94 | def cleanup(self): 95 | """ Called when the application closes and the module widget is destroyed. """ 96 | print("**Widget.cleanup(self)") 97 | self.removeObservers() 98 | 99 | # ------------------------------------------------------------------------------------------------------------------ 100 | def enter(self): 101 | """ Called each time the user opens this module. """ 102 | print("\n**Widget.enter(self)") 103 | 104 | # 01. Slicer. SL__Note: Every-Module own a SingleTon ParameterNode that can be identified by 105 | # self._parameterNode.GetAttribute('ModuleName')! Need to initial every Entry! 106 | self.initializeParameterNode() 107 | 108 | # ------------------------------------------------------------------------------------------------------------------ 109 | def exit(self): 110 | """ Called each time the user opens a different module. """ 111 | print("**Widget.exit(self)") 112 | # Slicer. Do not react to parameter node changes (GUI will be updated when the user enters into the module) 113 | self.removeObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromParameterNode) 114 | 115 | # ------------------------------------------------------------------------------------------------------------------ 116 | def onSceneStartClose(self, caller, event): 117 | """ Called just before the scene is closed. """ 118 | print("**Widget.onSceneStartClose(self, caller, event)") 119 | 120 | # Slicer. Parameter node will be reset, do not use it anymore 121 | self.setParameterNode(None) 122 | 123 | # ------------------------------------------------------------------------------------------------------------------ 124 | def onSceneEndClose(self, caller, event): 125 | """ Called just after the scene is closed. """'''''' 126 | print("**Widget.onSceneEndClose(self, caller, event)") 127 | # If this module is shown while the scene is closed then recreate a new parameter node immediately 128 | if self.parent.isEntered: 129 | self.initializeParameterNode() 130 | 131 | # ------------------------------------------------------------------------------------------------------------------ 132 | def initializeParameterNode(self): 133 | """ Ensure parameter node exists and observed. """'''''' 134 | print("\t**Widget.initializeParameterNode(self), \t SL_Developer") 135 | # 01. Slicer-Initial: the SingleTon ParameterNode stores all user choices in param-values, node selections... 136 | # so that when the scene is saved and reloaded, these settings are restored. 137 | self.setParameterNode(self.logic.getParameterNode()) 138 | 139 | # 02. SL_Developer. Select default input nodes if nothing is selected yet to save a few clicks for the user 140 | if not self._parameterNode.GetNodeReference("InputVolume"): 141 | firstVolumeNode = slicer.mrmlScene.GetFirstNodeByClass("vtkMRMLScalarVolumeNode") 142 | if firstVolumeNode: 143 | self._parameterNode.SetNodeReferenceID("InputVolume", firstVolumeNode.GetID()) 144 | 145 | # ------------------------------------------------------------------------------------------------------------------ 146 | def setParameterNode(self, inputParameterNode): 147 | """ SL_Notes: Set and observe the SingleTon ParameterNode. 148 | Observation is needed because when ParameteNode is changed then the GUI must be updated immediately. 149 | """ 150 | print("\t\t**Widget.setParameterNode(self, inputParameterNode)") 151 | if inputParameterNode: 152 | if not inputParameterNode.IsSingleton(): 153 | raise ValueError(f'SL__Allert! \tinputParameterNode = \n{inputParameterNode.__str__()}') 154 | self.logic.setDefaultParameters(inputParameterNode) 155 | 156 | # 01. Unobserve previously selected SingleTon ParameterNode 157 | if self._parameterNode is not None: 158 | self.removeObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromParameterNode) 159 | # 02. Set new SingleTon ParameterNode and Add an observer to the newly selected -> immediate GUI reflection 160 | self._parameterNode = inputParameterNode 161 | if self._parameterNode is not None: 162 | self.addObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromParameterNode) 163 | # 03. Initial GUI update; need to do this GUI update whenever there is a change from the SingleTon ParameterNode 164 | self.updateGUIFromParameterNode() 165 | 166 | # ------------------------------------------------------------------------------------------------------------------ 167 | def updateGUIFromParameterNode(self, caller=None, event=None): 168 | """ This method is called whenever parameter node is changed. 169 | The module GUI is updated to show the current state of the parameter node. """ 170 | # 00. Check self._updatingGUIFromParameterNode to prevent from GUI changes 171 | # (it could cause infinite loop: GUI change -> UpdateParamNode -> Update GUI -> UpdateParamNode) 172 | if self._parameterNode is None or self._updatingGUIFromParameterNode: 173 | return 174 | 175 | # I. Open-Brace: Make sure GUI changes do not call updateParameterNodeFromGUI__ (it could cause infinite loop) 176 | self._updatingGUIFromParameterNode = True 177 | # -------------------------------------------------------------------------------------------------------------- 178 | # II. SL_Developer. In-Brace, Update UI widgets () 179 | print("**Widget.updateGUIFromParameterNode(self, caller=None, event=None), \tSL_Developer") 180 | # II-01. Update node selectors and sliders 181 | self.ui.inputSelector.setCurrentNode(self._parameterNode.GetNodeReference("InputVolume")) 182 | self.ui.outputSelector.setCurrentNode(self._parameterNode.GetNodeReference("OutputVolume")) 183 | self.ui.invertedOutputSelector.setCurrentNode(self._parameterNode.GetNodeReference("OutputVolumeInverse")) 184 | self.ui.imageThresholdSliderWidget.value = float(self._parameterNode.GetParameter("Threshold")) 185 | self.ui.invertOutputCheckBox.checked = (self._parameterNode.GetParameter("Invert") == "true") 186 | # II-02. Update buttons states and tooltips 187 | if self._parameterNode.GetNodeReference("InputVolume") and self._parameterNode.GetNodeReference("OutputVolume"): 188 | self.ui.applyButton.toolTip = "Compute output volume" 189 | self.ui.applyButton.enabled = True 190 | else: 191 | self.ui.applyButton.toolTip = "Select input and output volume nodes" 192 | self.ui.applyButton.enabled = False 193 | # -------------------------------------------------------------------------------------------------------------- 194 | # III. Close-Brace: All the GUI updates are done 195 | self._updatingGUIFromParameterNode = False 196 | 197 | # ------------------------------------------------------------------------------------------------------------------ 198 | # ------------------------------------------------------------------------------------------------------------------ 199 | def updateParameterNodeFromGUI(self, caller=None, event=None): 200 | """ Read GUI Method: Method updateParameterNodeFromGUI__ is called when users makes any change in the GUI. 201 | Changes are saved into the parameter node (so that they are restored when the scene is saved and loaded). 202 | """ 203 | print(f"**Widget.updateParameterNodeFromGUI(self, caller=None, event=None), \t SL_Developer") 204 | if self._parameterNode is None or self._updatingGUIFromParameterNode: 205 | return 206 | 207 | # I. Before updating the SingleTon ParameterNode; Disable Modify events, e.g., vtk.vtkCommand.ModifiedEvent 208 | wasModified = self._parameterNode.StartModify() # Modify all properties in a single batch 209 | 210 | # II. Update the SingleTon ParameterNode; No updateGUIFromParameterNode triggered in this step 211 | self._parameterNode.SetNodeReferenceID("InputVolume", self.ui.inputSelector.currentNodeID) 212 | self._parameterNode.SetNodeReferenceID("OutputVolume", self.ui.outputSelector.currentNodeID) 213 | self._parameterNode.SetParameter("Threshold", str(self.ui.imageThresholdSliderWidget.value)) 214 | self._parameterNode.SetParameter("Invert", "true" if self.ui.invertOutputCheckBox.checked else "false") 215 | self._parameterNode.SetNodeReferenceID("OutputVolumeInverse", self.ui.invertedOutputSelector.currentNodeID) 216 | 217 | # III. After updating the SingleTon ParameterNode; Enable Modify events, e.g., vtk.vtkCommand.ModifiedEvent 218 | self._parameterNode.EndModify(wasModified) 219 | 220 | # ------------------------------------------------------------------------------------------------------------------ 221 | def onApplyButton(self): 222 | """ SL_Developer. Run processing when user clicks "Apply" button. """ 223 | with slicer.util.tryWithErrorDisplay("Failed to compute results.", waitCursor=True): 224 | 225 | # Compute output 226 | self.logic.process(self.ui.inputSelector.currentNode(), self.ui.outputSelector.currentNode(), 227 | self.ui.imageThresholdSliderWidget.value, self.ui.invertOutputCheckBox.checked) 228 | 229 | # Compute inverted output (if needed) 230 | if self.ui.invertedOutputSelector.currentNode(): 231 | # If additional output volume is selected then result with inverted threshold is written there 232 | self.logic.process(self.ui.inputSelector.currentNode(), self.ui.invertedOutputSelector.currentNode(), 233 | self.ui.imageThresholdSliderWidget.value, not self.ui.invertOutputCheckBox.checked, showResult=False) 234 | 235 | 236 | '''==================================================================================================================''' 237 | '''==================================================================================================================''' 238 | # 239 | # t_ApplyThresholdLogic 240 | # 241 | class t_ApplyThresholdLogic(ScriptedLoadableModuleLogic): 242 | """ The Logic class is : to facilitate dynamic reloading of the module without restarting the application. 243 | This class should implement all the actual computation done by your module. 244 | The interface should be such that other python code can import this class 245 | and make use of the functionality without requiring an instance of the Widget. 246 | Uses ScriptedLoadableModuleLogic base class, available at: 247 | https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py 248 | """'''''' 249 | 250 | def __init__(self): 251 | """ Called when the logic class is instantiated. Can be used for initializing member variables. """ 252 | ScriptedLoadableModuleLogic.__init__(self) 253 | print("**Logic.__init__(self)") 254 | 255 | # ------------------------------------------------------------------------------------------------------------------ 256 | def setDefaultParameters(self, parameterNode): 257 | """ SL_Developer: Initialize parameter node, Re-Enter, Re-Load. """'''''' 258 | print("\t\t\t**Logic.setDefaultParameters(self, parameterNode), \tSL_Developer"); 259 | 260 | """ Initialize parameter node with default settings. """ 261 | if not parameterNode.GetParameter("Threshold"): 262 | parameterNode.SetParameter("Threshold", "100.0") 263 | if not parameterNode.GetParameter("Invert"): 264 | parameterNode.SetParameter("Invert", "false") 265 | 266 | # ------------------------------------------------------------------------------------------------------------------ 267 | def process(self, inputVolume, outputVolume, imageThreshold, invert=False, showResult=True): 268 | """ 269 | Run the processing algorithm. 270 | Can be used without GUI widget. 271 | :param inputVolume: volume to be thresholded 272 | :param outputVolume: thresholding result 273 | :param imageThreshold: values above/below this threshold will be set to 0 274 | :param invert: if True then values above the threshold will be set to 0, otherwise values below are set to 0 275 | :param showResult: show output volume in slice viewers 276 | """ 277 | 278 | if not inputVolume or not outputVolume: 279 | raise ValueError("Input or output volume is invalid") 280 | 281 | import time 282 | startTime = time.time() 283 | logging.info('Processing started') 284 | 285 | # Compute the thresholded output volume using the "Threshold Scalar Volume" CLI module 286 | cliParams = { 287 | 'InputVolume': inputVolume.GetID(), 288 | 'OutputVolume': outputVolume.GetID(), 289 | 'ThresholdValue': imageThreshold, 290 | 'ThresholdType': 'Above' if invert else 'Below' 291 | } 292 | cliNode = slicer.cli.run(slicer.modules.thresholdscalarvolume, None, cliParams, wait_for_completion=True, update_display=showResult) 293 | # We don't need the CLI module node anymore, remove it to not clutter the scene with it 294 | slicer.mrmlScene.RemoveNode(cliNode) 295 | 296 | stopTime = time.time() 297 | logging.info(f'Processing completed in {stopTime-startTime:.2f} seconds') 298 | 299 | ''' =================================================================================================================''' 300 | ''' =================================================================================================================''' 301 | # 302 | # t_ApplyThresholdTest 303 | # 304 | class t_ApplyThresholdTest(ScriptedLoadableModuleTest): 305 | """ This is the test case for your scripted module. 306 | Uses ScriptedLoadableModuleTest base class, available at: 307 | https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py """ 308 | 309 | def setUp(self): 310 | """ Do whatever is needed to reset the state - typically a scene clear will be enough. """ 311 | slicer.mrmlScene.Clear() 312 | 313 | # ------------------------------------------------------------------------------------------------------------------ 314 | def runTest(self): 315 | """Run as few or as many tests as needed here. 316 | """ 317 | self.setUp() 318 | self.test_t_ApplyThreshold1() 319 | 320 | # ------------------------------------------------------------------------------------------------------------------ 321 | def test_t_ApplyThreshold1(self): 322 | """ Ideally you should have several levels of tests. At the lowest level 323 | tests should exercise the functionality of the logic with different inputs 324 | (both valid and invalid). At higher levels your tests should emulate the 325 | way the user would interact with your code and confirm that it still works 326 | the way you intended. 327 | One of the most important features of the tests is that it should alert other 328 | developers when their changes will have an impact on the behavior of your 329 | module. For example, if a developer removes a feature that you depend on, 330 | your test should break so they know that the feature is needed. 331 | """ 332 | 333 | self.delayDisplay("Starting the test") 334 | 335 | # Get/create input data 336 | 337 | import SampleData 338 | registerSampleData() 339 | inputVolume = SampleData.downloadSample('t_ApplyThreshold1') 340 | self.delayDisplay('Loaded test data set') 341 | 342 | inputScalarRange = inputVolume.GetImageData().GetScalarRange() 343 | self.assertEqual(inputScalarRange[0], 0) 344 | self.assertEqual(inputScalarRange[1], 695) 345 | 346 | outputVolume = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLScalarVolumeNode") 347 | threshold = 100 348 | 349 | # Test the module logic 350 | 351 | logic = t_ApplyThresholdLogic() 352 | 353 | # Test algorithm with non-inverted threshold 354 | logic.process(inputVolume, outputVolume, threshold, True) 355 | outputScalarRange = outputVolume.GetImageData().GetScalarRange() 356 | self.assertEqual(outputScalarRange[0], inputScalarRange[0]) 357 | self.assertEqual(outputScalarRange[1], threshold) 358 | 359 | # Test algorithm with inverted threshold 360 | logic.process(inputVolume, outputVolume, threshold, False) 361 | outputScalarRange = outputVolume.GetImageData().GetScalarRange() 362 | self.assertEqual(outputScalarRange[0], inputScalarRange[0]) 363 | self.assertEqual(outputScalarRange[1], inputScalarRange[1]) 364 | 365 | self.delayDisplay('Test passed') 366 | 367 | # ====================================================================================================================== 368 | # ====================================================================================================================== 369 | # ------------ Unit-Test: Load Data ----------------------------------------------------------------------- 370 | 371 | # 372 | # Register sample data sets in Sample Data module 373 | # 374 | def registerSampleData(): 375 | """ 376 | Add data sets to Sample Data module. 377 | """ 378 | # It is always recommended to provide sample data for users to make it easy to try the module, 379 | # but if no sample data is available then this method (and associated startupCompeted signal connection) can be removed. 380 | 381 | import SampleData 382 | iconsPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons') 383 | 384 | # To ensure that the source code repository remains small (can be downloaded and installed quickly) 385 | # it is recommended to store data sets that are larger than a few MB in a Github release. 386 | 387 | # t_ApplyThreshold1 388 | SampleData.SampleDataLogic.registerCustomSampleDataSource( 389 | # Category and sample name displayed in Sample Data module 390 | category='t_ApplyThreshold', 391 | sampleName='t_ApplyThreshold1', 392 | # Thumbnail should have size of approximately 260x280 pixels and stored in Resources/Icons folder. 393 | # It can be created by Screen Capture module, "Capture all views" option enabled, "Number of images" set to "Single". 394 | thumbnailFileName=os.path.join(iconsPath, 't_ApplyThreshold1.png'), 395 | # Download URL and target file name 396 | uris="https://github.com/Slicer/SlicerTestingData/releases/download/SHA256/998cb522173839c78657f4bc0ea907cea09fd04e44601f17c82ea27927937b95", 397 | fileNames='t_ApplyThreshold1.nrrd', 398 | # Checksum to ensure file integrity. Can be computed by this command: 399 | # import hashlib; print(hashlib.sha256(open(filename, "rb").read()).hexdigest()) 400 | checksums='SHA256:998cb522173839c78657f4bc0ea907cea09fd04e44601f17c82ea27927937b95', 401 | # This node name will be used when the data set is loaded 402 | nodeNames='t_ApplyThreshold1' 403 | ) 404 | 405 | # t_ApplyThreshold2 406 | SampleData.SampleDataLogic.registerCustomSampleDataSource( 407 | # Category and sample name displayed in Sample Data module 408 | category='t_ApplyThreshold', 409 | sampleName='t_ApplyThreshold2', 410 | thumbnailFileName=os.path.join(iconsPath, 't_ApplyThreshold2.png'), 411 | # Download URL and target file name 412 | uris="https://github.com/Slicer/SlicerTestingData/releases/download/SHA256/1a64f3f422eb3d1c9b093d1a18da354b13bcf307907c66317e2463ee530b7a97", 413 | fileNames='t_ApplyThreshold2.nrrd', 414 | checksums='SHA256:1a64f3f422eb3d1c9b093d1a18da354b13bcf307907c66317e2463ee530b7a97', 415 | # This node name will be used when the data set is loaded 416 | nodeNames='t_ApplyThreshold2' 417 | ) 418 | 419 | -------------------------------------------------------------------------------- /03__DeveloperFoci_NeatBlanks_&_GUI_HelloWorld/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.16.3...3.19.7 FATAL_ERROR) 2 | 3 | project(SL_Tutorials) 4 | 5 | #----------------------------------------------------------------------------- 6 | # Extension meta-information 7 | set(EXTENSION_HOMEPAGE "https://www.slicer.org/wiki/Documentation/Nightly/Extensions/SL_Tutorials") 8 | set(EXTENSION_CATEGORY "Tutorials") 9 | set(EXTENSION_CONTRIBUTORS "Sen Li (ETS)") 10 | set(EXTENSION_DESCRIPTION "This is a series of tutorial for 3D Slicer Extension module development.") 11 | set(EXTENSION_ICONURL "https://www.example.com/Slicer/Extensions/SL_Tutorials.png") 12 | set(EXTENSION_SCREENSHOTURLS "https://www.example.com/Slicer/Extensions/SL_Tutorials/Screenshots/1.png") 13 | set(EXTENSION_DEPENDS "NA") # Specified as a list or "NA" if no dependencies 14 | 15 | #----------------------------------------------------------------------------- 16 | # Extension dependencies 17 | find_package(Slicer REQUIRED) 18 | include(${Slicer_USE_FILE}) 19 | 20 | #----------------------------------------------------------------------------- 21 | # Extension modules 22 | add_subdirectory(sl__GUI_HelloWorld) 23 | ## NEXT_MODULE 24 | 25 | #----------------------------------------------------------------------------- 26 | include(${Slicer_EXTENSION_GENERATE_CONFIG}) 27 | include(${Slicer_EXTENSION_CPACK}) 28 | -------------------------------------------------------------------------------- /03__DeveloperFoci_NeatBlanks_&_GUI_HelloWorld/README.md: -------------------------------------------------------------------------------- 1 | # 3D Slicer Extension Tutorial: Step by Step 2 | 3 | 4 |
5 |
6 | 7 | ## Step 03: Developer Foci, Neat Blanks & GUI Hello World 8 | 9 | [![IMAGE ALT TEXT HERE](https://img.youtube.com/vi/tOldfUkSecI/0.jpg)](https://www.youtube.com/watch?v=tOldfUkSecI&list=PLTuWbByD80TORd1R-J7j7nVQ9fot3C2fK) 10 | 11 | #### YouTube Video Tutorial for Step_03 12 | 13 | #### Bilibili Video Tutorial for Step_03 14 | 15 | isolated 16 | -------------------------------------------------------------------------------- /03__DeveloperFoci_NeatBlanks_&_GUI_HelloWorld/SL_Tutorials.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SenonETS/3DSlicerTutorial_ExtensionModuleDevelopment/c2ed7fd7e94f79953ec38019b16ca90019817bbc/03__DeveloperFoci_NeatBlanks_&_GUI_HelloWorld/SL_Tutorials.png -------------------------------------------------------------------------------- /03__DeveloperFoci_NeatBlanks_&_GUI_HelloWorld/sl_03__Summary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SenonETS/3DSlicerTutorial_ExtensionModuleDevelopment/c2ed7fd7e94f79953ec38019b16ca90019817bbc/03__DeveloperFoci_NeatBlanks_&_GUI_HelloWorld/sl_03__Summary.png -------------------------------------------------------------------------------- /03__DeveloperFoci_NeatBlanks_&_GUI_HelloWorld/sl__GUI_HelloWorld/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | #----------------------------------------------------------------------------- 2 | set(MODULE_NAME sl__GUI_HelloWorld) 3 | 4 | #----------------------------------------------------------------------------- 5 | set(MODULE_PYTHON_SCRIPTS 6 | ${MODULE_NAME}.py 7 | ) 8 | 9 | set(MODULE_PYTHON_RESOURCES 10 | Resources/Icons/${MODULE_NAME}.png 11 | Resources/UI/${MODULE_NAME}.ui 12 | ) 13 | 14 | #----------------------------------------------------------------------------- 15 | slicerMacroBuildScriptedModule( 16 | NAME ${MODULE_NAME} 17 | SCRIPTS ${MODULE_PYTHON_SCRIPTS} 18 | RESOURCES ${MODULE_PYTHON_RESOURCES} 19 | WITH_GENERIC_TESTS 20 | ) 21 | 22 | #----------------------------------------------------------------------------- 23 | if(BUILD_TESTING) 24 | 25 | # Register the unittest subclass in the main script as a ctest. 26 | # Note that the test will also be available at runtime. 27 | slicer_add_python_unittest(SCRIPT ${MODULE_NAME}.py) 28 | 29 | # Additional build-time testing 30 | add_subdirectory(Testing) 31 | endif() 32 | -------------------------------------------------------------------------------- /03__DeveloperFoci_NeatBlanks_&_GUI_HelloWorld/sl__GUI_HelloWorld/Resources/Icons/sl__GUI_HelloWorld.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SenonETS/3DSlicerTutorial_ExtensionModuleDevelopment/c2ed7fd7e94f79953ec38019b16ca90019817bbc/03__DeveloperFoci_NeatBlanks_&_GUI_HelloWorld/sl__GUI_HelloWorld/Resources/Icons/sl__GUI_HelloWorld.png -------------------------------------------------------------------------------- /03__DeveloperFoci_NeatBlanks_&_GUI_HelloWorld/sl__GUI_HelloWorld/Resources/UI/sl__GUI_HelloWorld.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | t_ApplyThreshold 4 | 5 | 6 | 7 | 0 8 | 0 9 | 314 10 | 331 11 | 12 | 13 | 14 | 15 | 16 | 17 | QPushButton#pushButton_HelloWorld { 18 | background-color: none; 19 | border-style: outset; 20 | border-width: 1.2px; 21 | border-radius: 10px; 22 | border-color: black; 23 | font: bold; 24 | min-width: 8em; 25 | padding: 3px; 26 | } 27 | QPushButton#pushButton_HelloWorld:pressed { 28 | background-color: rgb(110, 125, 0); 29 | border-style: outset; 30 | border-width: 1.2px; 31 | border-radius: 10px; 32 | border-color: black; 33 | font: bold; 34 | min-width: 8em; 35 | padding: 3px; 36 | } 37 | QPushButton#pushButton_HelloWorld:hover { 38 | background-color: rgb(225, 255, 0); 39 | border-style: outset; 40 | border-width: 1.2px; 41 | border-radius: 10px; 42 | border-color: black; 43 | font: bold; 44 | min-width: 8em; 45 | padding: 3px; 46 | } 47 | 48 | QPushButton#pushButton_HelloWorld:hover:pressed{ 49 | background-color: rgb(110, 125, 0); 50 | border-style: outset; 51 | border-width: 1.2px; 52 | border-radius: 10px; 53 | border-color: black; 54 | font: bold; 55 | min-width: 8em; 56 | padding: 3px; 57 | } 58 | 59 | 60 | 61 | Say hello by clicking QPushButton 62 | 63 | 64 | 65 | 66 | 67 | 68 | Qt::Vertical 69 | 70 | 71 | 72 | 20 73 | 40 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | qMRMLWidget 83 | QWidget 84 |
qMRMLWidget.h
85 | 1 86 |
87 |
88 | 89 | 90 |
91 | -------------------------------------------------------------------------------- /03__DeveloperFoci_NeatBlanks_&_GUI_HelloWorld/sl__GUI_HelloWorld/Testing/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_subdirectory(Python) 2 | -------------------------------------------------------------------------------- /03__DeveloperFoci_NeatBlanks_&_GUI_HelloWorld/sl__GUI_HelloWorld/Testing/Python/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | 2 | #slicer_add_python_unittest(SCRIPT ${MODULE_NAME}ModuleTest.py) 3 | -------------------------------------------------------------------------------- /03__DeveloperFoci_NeatBlanks_&_GUI_HelloWorld/sl__GUI_HelloWorld/sl__GUI_HelloWorld.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | import vtk 5 | 6 | import slicer, qt 7 | from slicer.ScriptedLoadableModule import * 8 | from slicer.util import VTKObservationMixin 9 | 10 | '''==================================================================================================================''' 11 | '''==================================================================================================================''' 12 | # 13 | # sl__GUI_HelloWorld 14 | # 15 | class sl__GUI_HelloWorld(ScriptedLoadableModule): 16 | 17 | def __init__(self, parent): 18 | ScriptedLoadableModule.__init__(self, parent) 19 | self.parent.title = "sl__GUI_HelloWorld" 20 | self.parent.categories = ["SL_Tutorials"] 21 | self.parent.dependencies = [] # TODO: add here list of module names that this module requires 22 | self.parent.contributors = ["Sen Li (École de Technologie Supérieure)"] 23 | # TODO: update with short description of the module and a link to online module documentation 24 | self.parent.helpText = """This is sl__GUI_HelloWorld ! """ 25 | self.parent.acknowledgementText = 'Step-by-step tutorial on 3D Slicer extension development. ' \ 26 | '\nThis file was originally developed by Sen Li, LATIS, École de techonologie supérieure. ' \ 27 | '\nSen.Li.1@ens.etsmtl.ca' 28 | 29 | print("sl__GUI_HelloWorld(ScriptedLoadableModule): __init__(self, parent)") 30 | 31 | '''==================================================================================================================''' 32 | '''==================================================================================================================''' 33 | # 34 | # sl__GUI_HelloWorldWidget 35 | # 36 | class sl__GUI_HelloWorldWidget(ScriptedLoadableModuleWidget, VTKObservationMixin): 37 | 38 | def __init__(self, parent=None): 39 | """ Called when the user opens the module the first time and the widget is initialized. """ 40 | ScriptedLoadableModuleWidget.__init__(self, parent) 41 | VTKObservationMixin.__init__(self) # needed for parameter node observation 42 | self.logic = None 43 | self._parameterNode = None # SingleTon initialized through self.setParameterNode(self.logic.getParameterNode()) 44 | self._updatingGUIFromParameterNode = False 45 | print("**Widget.__init__(self, parent)") 46 | 47 | # ------------------------------------------------------------------------------------------------------------------ 48 | def setup(self): 49 | print("**Widget.setup(self), \tSL_Developer") 50 | 51 | """ 00. Called when the user opens the module the first time and the widget is initialized. """ 52 | ScriptedLoadableModuleWidget.setup(self) 53 | 54 | # 01. Load widget from .ui file (created by Qt Designer). 55 | # Additional widgets can be instantiated manually and added to self.layout. 56 | uiWidget = slicer.util.loadUI(self.resourcePath('UI/sl__GUI_HelloWorld.ui')) 57 | self.layout.addWidget(uiWidget) 58 | self.ui = slicer.util.childWidgetVariables(uiWidget) 59 | 60 | # 02. Set scene in MRML widgets. Make sure that in Qt designer the 61 | # top-level qMRMLWidget's "mrmlSceneChanged(vtkMRMLScene*)" signal in is connected to 62 | # each MRML widget's "setMRMLScene(vtkMRMLScene*)" slot. 63 | uiWidget.setMRMLScene(slicer.mrmlScene) 64 | 65 | # 03. Create logic class. Logic implements all computations that should be possible to run 66 | # in batch mode, without a graphical user interface. 67 | self.logic = sl__GUI_HelloWorldLogic() 68 | 69 | # 04. Connections, ensure that we update parameter node when scene is closed 70 | self.addObserver(slicer.mrmlScene, slicer.mrmlScene.StartCloseEvent, self.onSceneStartClose) 71 | self.addObserver(slicer.mrmlScene, slicer.mrmlScene.EndCloseEvent, self.onSceneEndClose) 72 | 73 | # 05. SL_Developer. Connect Signal-Slot, ensure that whenever user changes some settings on the GUI, 74 | # that is saved in the MRML scene (in the selected parameter node). 75 | self.ui.pushButton_HelloWorld.clicked.connect(self.onButtonHelloWorld_Clicked) 76 | 77 | # 06. Needed for programmer-friendly Module-Reload where the Module had already been enter(self)-ed; 78 | # Otherwise, will initial through function enter(self) 79 | if self.parent.isEntered: 80 | self.initializeParameterNode() 81 | 82 | # ------------------------------------------------------------------------------------------------------------------ 83 | def cleanup(self): 84 | """ Called when the application closes and the module widget is destroyed. """ 85 | print("**Widget.cleanup(self)") 86 | self.removeObservers() 87 | 88 | # ------------------------------------------------------------------------------------------------------------------ 89 | def enter(self): 90 | """ Called each time the user opens this module. """ 91 | print("\n**Widget.enter(self)") 92 | 93 | # 01. Slicer. SL__Note: Every-Module own a SingleTon ParameterNode that can be identified by 94 | # self._parameterNode.GetAttribute('ModuleName')! Need to initial every Entry! 95 | self.initializeParameterNode() 96 | 97 | # ------------------------------------------------------------------------------------------------------------------ 98 | def exit(self): 99 | """ Called each time the user opens a different module. """ 100 | print("**Widget.exit(self)") 101 | # Slicer. Do not react to parameter node changes (GUI will be updated when the user enters into the module) 102 | self.removeObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromParameterNode) 103 | 104 | # ------------------------------------------------------------------------------------------------------------------ 105 | def onSceneStartClose(self, caller, event): 106 | """ Called just before the scene is closed. """ 107 | print("**Widget.onSceneStartClose(self, caller, event)") 108 | 109 | # Slicer. Parameter node will be reset, do not use it anymore 110 | self.setParameterNode(None) 111 | 112 | # ------------------------------------------------------------------------------------------------------------------ 113 | def onSceneEndClose(self, caller, event): 114 | """ Called just after the scene is closed. """'''''' 115 | print("**Widget.onSceneEndClose(self, caller, event)") 116 | # If this module is shown while the scene is closed then recreate a new parameter node immediately 117 | if self.parent.isEntered: 118 | self.initializeParameterNode() 119 | 120 | # ------------------------------------------------------------------------------------------------------------------ 121 | def initializeParameterNode(self): 122 | """ Ensure parameter node exists and observed. """'''''' 123 | print("\t**Widget.initializeParameterNode(self), \t SL_Developer") 124 | # 01. Slicer-Initial: the SingleTon ParameterNode stores all user choices in param-values, node selections... 125 | # so that when the scene is saved and reloaded, these settings are restored. 126 | self.setParameterNode(self.logic.getParameterNode()) 127 | 128 | # 02. SL_Developer. To update ParameterNode and attach observers 129 | pass 130 | 131 | # ------------------------------------------------------------------------------------------------------------------ 132 | def setParameterNode(self, inputParameterNode): 133 | """ SL_Notes: Set and observe the SingleTon ParameterNode. 134 | Observation is needed because when ParameteNode is changed then the GUI must be updated immediately. 135 | """ 136 | print("\t\t**Widget.setParameterNode(self, inputParameterNode)") 137 | if inputParameterNode: 138 | if not inputParameterNode.IsSingleton(): 139 | raise ValueError(f'SL__Allert! \tinputParameterNode = \n{inputParameterNode.__str__()}') 140 | self.logic.setDefaultParameters(inputParameterNode) 141 | 142 | # 01. Unobserve previously selected SingleTon ParameterNode 143 | if self._parameterNode is not None: 144 | self.removeObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromParameterNode) 145 | # 02. Set new SingleTon ParameterNode and Add an observer to the newly selected -> immediate GUI reflection 146 | self._parameterNode = inputParameterNode 147 | if self._parameterNode is not None: 148 | self.addObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromParameterNode) 149 | # 03. Initial GUI update; need to do this GUI update whenever there is a change from the SingleTon ParameterNode 150 | self.updateGUIFromParameterNode() 151 | 152 | # ------------------------------------------------------------------------------------------------------------------ 153 | def updateGUIFromParameterNode(self, caller=None, event=None): 154 | """ This method is called whenever parameter node is changed. 155 | The module GUI is updated to show the current state of the parameter node. """ 156 | # 00. Check self._updatingGUIFromParameterNode to prevent from GUI changes 157 | # (it could cause infinite loop: GUI change -> UpdateParamNode -> Update GUI -> UpdateParamNode) 158 | if self._parameterNode is None or self._updatingGUIFromParameterNode: 159 | return 160 | 161 | # I. Open-Brace: Make sure GUI changes do not call updateParameterNodeFromGUI__ (it could cause infinite loop) 162 | self._updatingGUIFromParameterNode = True 163 | # -------------------------------------------------------------------------------------------------------------- 164 | # II. SL_Developer. In-Brace, Update UI widgets () 165 | print("**Widget.updateGUIFromParameterNode(self, caller=None, event=None), \tSL_Developer") 166 | # II-01. Update buttons states and tooltips 167 | self.ui.pushButton_HelloWorld.toolTip = "Click Me and join SL Slicer Tutorial World" 168 | self.ui.pushButton_HelloWorld.enabled = True 169 | # -------------------------------------------------------------------------------------------------------------- 170 | # III. Close-Brace: All the GUI updates are done 171 | self._updatingGUIFromParameterNode = False 172 | 173 | # ------------------------------------------------------------------------------------------------------------------ 174 | # ------------------------------------------------------------------------------------------------------------------ 175 | def updateParameterNodeFromGUI(self, caller=None, event=None): 176 | """ Read GUI Method: Method updateParameterNodeFromGUI__ is called when users makes any change in the GUI. 177 | Changes are saved into the parameter node (so that they are restored when the scene is saved and loaded). 178 | """ 179 | print(f"**Widget.updateParameterNodeFromGUI(self, caller=None, event=None), \t SL_Developer") 180 | if self._parameterNode is None or self._updatingGUIFromParameterNode: 181 | return 182 | 183 | # I. Before updating the SingleTon ParameterNode; Disable Modify events, e.g., vtk.vtkCommand.ModifiedEvent 184 | wasModified = self._parameterNode.StartModify() # Modify all properties in a single batch 185 | 186 | # II. SL_Developer. Update the Singleton ParameterNode; No updateGUIFromParameterNode triggered in this step 187 | # self._parameterNode.SetNodeReferenceID("InputVolume", self.ui.inputSelector.currentNodeID) 188 | 189 | # III. After updating the SingleTon ParameterNode; Enable Modify events, e.g., vtk.vtkCommand.ModifiedEvent 190 | self._parameterNode.EndModify(wasModified) 191 | 192 | # ------------------------------------------------------------------------------------------------------------------ 193 | def onButtonHelloWorld_Clicked(self): 194 | """ SL_Developer. Run processing when user clicks "Hello World" button. """ 195 | with slicer.util.tryWithErrorDisplay("Failed to compute results.", waitCursor=True): 196 | 197 | str_Result = self.logic.obtainStr_HelloWorld() 198 | qt.QMessageBox.information(slicer.util.mainWindow(), 'SL Tutorial: GUI HelloWorld', str_Result) 199 | 200 | 201 | '''==================================================================================================================''' 202 | '''==================================================================================================================''' 203 | # 204 | # sl__GUI_HelloWorldLogic 205 | # 206 | class sl__GUI_HelloWorldLogic(ScriptedLoadableModuleLogic): 207 | """ The Logic class is : to facilitate dynamic reloading of the module without restarting the application. 208 | This class should implement all the actual computation done by your module. 209 | The interface should be such that other python code can import this class 210 | and make use of the functionality without requiring an instance of the Widget. 211 | Uses ScriptedLoadableModuleLogic base class, available at: 212 | https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py 213 | """'''''' 214 | 215 | def __init__(self): 216 | """ Called when the logic class is instantiated. Can be used for initializing member variables. """ 217 | ScriptedLoadableModuleLogic.__init__(self) 218 | print("**Logic.__init__(self)") 219 | 220 | # ------------------------------------------------------------------------------------------------------------------ 221 | def setDefaultParameters(self, parameterNode): 222 | """ SL_Developer: Initialize parameter node, Re-Enter, Re-Load. """'''''' 223 | print("\t\t\t**Logic.setDefaultParameters(self, parameterNode), \tSL_Developer"); 224 | 225 | # To initial ModuleSingleton ParameterNode 226 | pass 227 | 228 | # ------------------------------------------------------------------------------------------------------------------ 229 | def obtainStr_HelloWorld(self): 230 | """ To be called without GUI **Widget. """ 231 | return "SL: Hello World!" 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | ''' =================================================================================================================''' 247 | ''' =================================================================================================================''' 248 | ''' =================================================================================================================''' 249 | # 250 | # sl__GUI_HelloWorldTest 251 | # 252 | class sl__GUI_HelloWorldTest(ScriptedLoadableModuleTest): 253 | """ This is the test case for your scripted module. 254 | Uses ScriptedLoadableModuleTest base class, available at: 255 | https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py """ 256 | 257 | def setUp(self): 258 | """ Do whatever is needed to reset the state - typically a scene clear will be enough. """ 259 | slicer.mrmlScene.Clear() 260 | 261 | # ------------------------------------------------------------------------------------------------------------------ 262 | def runTest(self): 263 | """Run as few or as many tests as needed here. 264 | """ 265 | self.setUp() 266 | self.test_sl__GUI_HelloWorld1() 267 | 268 | # ------------------------------------------------------------------------------------------------------------------ 269 | def test_sl__GUI_HelloWorld1(self): 270 | """ Ideally you should have several levels of tests. At the lowest level 271 | tests should exercise the functionality of the logic with different inputs 272 | (both valid and invalid). At higher levels your tests should emulate the 273 | way the user would interact with your code and confirm that it still works 274 | the way you intended. 275 | One of the most important features of the tests is that it should alert other 276 | developers when their changes will have an impact on the behavior of your 277 | module. For example, if a developer removes a feature that you depend on, 278 | your test should break so they know that the feature is needed. 279 | """ 280 | 281 | self.delayDisplay("Starting the test") 282 | 283 | # Test the module logic 284 | logic = sl__GUI_HelloWorldLogic() 285 | str_Result = logic.obtainStr_HelloWorld() 286 | self.assertIsNotNone(str_Result) 287 | self.delayDisplay('Test passed') 288 | -------------------------------------------------------------------------------- /04__CodeStyle_MethodGroups_&_US_SeqViewer/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.16.3...3.19.7 FATAL_ERROR) 2 | 3 | project(SL_Tutorials) 4 | 5 | #----------------------------------------------------------------------------- 6 | # Extension meta-information 7 | set(EXTENSION_HOMEPAGE "https://www.slicer.org/wiki/Documentation/Nightly/Extensions/SL_Tutorials") 8 | set(EXTENSION_CATEGORY "Tutorials") 9 | set(EXTENSION_CONTRIBUTORS "Sen Li (ETS)") 10 | set(EXTENSION_DESCRIPTION "This is a series of tutorial for 3D Slicer Extension module development.") 11 | set(EXTENSION_ICONURL "https://www.example.com/Slicer/Extensions/SL_Tutorials.png") 12 | set(EXTENSION_SCREENSHOTURLS "https://www.example.com/Slicer/Extensions/SL_Tutorials/Screenshots/1.png") 13 | set(EXTENSION_DEPENDS "NA") # Specified as a list or "NA" if no dependencies 14 | 15 | #----------------------------------------------------------------------------- 16 | # Extension dependencies 17 | find_package(Slicer REQUIRED) 18 | include(${Slicer_USE_FILE}) 19 | 20 | #----------------------------------------------------------------------------- 21 | # Extension modules 22 | add_subdirectory(sl__US_SeqViewer) 23 | ## NEXT_MODULE 24 | 25 | #----------------------------------------------------------------------------- 26 | include(${Slicer_EXTENSION_GENERATE_CONFIG}) 27 | include(${Slicer_EXTENSION_CPACK}) 28 | -------------------------------------------------------------------------------- /04__CodeStyle_MethodGroups_&_US_SeqViewer/README.md: -------------------------------------------------------------------------------- 1 | # 3D Slicer Extension Tutorial: Step by Step 2 | 3 |
4 |
5 | 6 | ## Step 04: Index SequenceBrowser 7 | 8 | [![IMAGE ALT TEXT HERE](https://img.youtube.com/vi/f_gsm0GJ4_8/0.jpg)](https://www.youtube.com/watch?v=f_gsm0GJ4_8&list=PLTuWbByD80TORd1R-J7j7nVQ9fot3C2fK) 9 | 10 | 11 | #### YouTube Video Tutorial for Step_04 12 | 13 | #### Bilibili Video Tutorial for Step_04 14 | 15 | 16 | isolated 17 | -------------------------------------------------------------------------------- /04__CodeStyle_MethodGroups_&_US_SeqViewer/SL_Tutorials.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SenonETS/3DSlicerTutorial_ExtensionModuleDevelopment/c2ed7fd7e94f79953ec38019b16ca90019817bbc/04__CodeStyle_MethodGroups_&_US_SeqViewer/SL_Tutorials.png -------------------------------------------------------------------------------- /04__CodeStyle_MethodGroups_&_US_SeqViewer/sl_04__Summary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SenonETS/3DSlicerTutorial_ExtensionModuleDevelopment/c2ed7fd7e94f79953ec38019b16ca90019817bbc/04__CodeStyle_MethodGroups_&_US_SeqViewer/sl_04__Summary.png -------------------------------------------------------------------------------- /04__CodeStyle_MethodGroups_&_US_SeqViewer/sl__US_SeqViewer/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | #----------------------------------------------------------------------------- 2 | set(MODULE_NAME sl__US_SeqViewer) 3 | 4 | #----------------------------------------------------------------------------- 5 | set(MODULE_PYTHON_SCRIPTS 6 | ${MODULE_NAME}.py 7 | ) 8 | 9 | set(MODULE_PYTHON_RESOURCES 10 | Resources/Icons/${MODULE_NAME}.png 11 | Resources/UI/${MODULE_NAME}.ui 12 | ) 13 | 14 | #----------------------------------------------------------------------------- 15 | slicerMacroBuildScriptedModule( 16 | NAME ${MODULE_NAME} 17 | SCRIPTS ${MODULE_PYTHON_SCRIPTS} 18 | RESOURCES ${MODULE_PYTHON_RESOURCES} 19 | WITH_GENERIC_TESTS 20 | ) 21 | 22 | #----------------------------------------------------------------------------- 23 | if(BUILD_TESTING) 24 | 25 | # Register the unittest subclass in the main script as a ctest. 26 | # Note that the test will also be available at runtime. 27 | slicer_add_python_unittest(SCRIPT ${MODULE_NAME}.py) 28 | 29 | # Additional build-time testing 30 | add_subdirectory(Testing) 31 | endif() 32 | -------------------------------------------------------------------------------- /04__CodeStyle_MethodGroups_&_US_SeqViewer/sl__US_SeqViewer/Resources/Icons/sl__US_SeqViewer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SenonETS/3DSlicerTutorial_ExtensionModuleDevelopment/c2ed7fd7e94f79953ec38019b16ca90019817bbc/04__CodeStyle_MethodGroups_&_US_SeqViewer/sl__US_SeqViewer/Resources/Icons/sl__US_SeqViewer.png -------------------------------------------------------------------------------- /04__CodeStyle_MethodGroups_&_US_SeqViewer/sl__US_SeqViewer/Resources/UI/sl__US_SeqViewer.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | sl__US_SeqViewer 4 | 5 | 6 | 7 | 0 8 | 0 9 | 485 10 | 556 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 14 19 | 75 20 | true 21 | 22 | 23 | 24 | Ultrasound Sequence Viewer 25 | 26 | 27 | 28 | 29 | 30 | 31 | Qt::Vertical 32 | 33 | 34 | QSizePolicy::Fixed 35 | 36 | 37 | 38 | 20 39 | 10 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | Sequence browser: 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | vtkMRMLSequenceBrowserNode 58 | 59 | 60 | 61 | false 62 | 63 | 64 | false 65 | 66 | 67 | false 68 | 69 | 70 | true 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | N/A 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 0 90 | 0 91 | 92 | 93 | 94 | 1 95 | 96 | 97 | 99 98 | 99 | 100 | 50 101 | 102 | 103 | Qt::Horizontal 104 | 105 | 106 | 107 | 108 | 109 | 110 | FrameIndex (from 1): 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | Qt::Vertical 120 | 121 | 122 | 123 | 20 124 | 40 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | qMRMLNodeComboBox 134 | QWidget 135 |
qMRMLNodeComboBox.h
136 |
137 | 138 | qMRMLWidget 139 | QWidget 140 |
qMRMLWidget.h
141 | 1 142 |
143 |
144 | 145 | 146 | 147 | sl__US_SeqViewer 148 | mrmlSceneChanged(vtkMRMLScene*) 149 | sequenceSelector 150 | setMRMLScene(vtkMRMLScene*) 151 | 152 | 153 | 289 154 | 439 155 | 156 | 157 | 338 158 | 19 159 | 160 | 161 | 162 | 163 | slider_SeqFrame 164 | valueChanged(int) 165 | label_FrameIndex 166 | setNum(int) 167 | 168 | 169 | 333 170 | 45 171 | 172 | 173 | 559 174 | 45 175 | 176 | 177 | 178 | 179 |
180 | -------------------------------------------------------------------------------- /04__CodeStyle_MethodGroups_&_US_SeqViewer/sl__US_SeqViewer/Testing/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_subdirectory(Python) 2 | -------------------------------------------------------------------------------- /04__CodeStyle_MethodGroups_&_US_SeqViewer/sl__US_SeqViewer/Testing/Python/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | 2 | #slicer_add_python_unittest(SCRIPT ${MODULE_NAME}ModuleTest.py) 3 | -------------------------------------------------------------------------------- /04__CodeStyle_MethodGroups_&_US_SeqViewer/sl__US_SeqViewer/sl__US_SeqViewer.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import vtk, qt 3 | 4 | import slicer 5 | from slicer.ScriptedLoadableModule import * 6 | from slicer.util import VTKObservationMixin 7 | 8 | import numpy as np 9 | 10 | '''==================================================================================================================''' 11 | '''==================================================================================================================''' 12 | '''------------------------- STRING Macro of sl__US_SeqViewer ------------------------------------------------------''' 13 | '''------------------------------------------------------------------------------------------------------------------''' 14 | INT_SliderFrameIndex_Min = 1 # StartingValue of slider_FrameIndex, increase from 1 15 | INT_FRAME_INDEX_SLIDER_DEFAULT = 50 # Default slider_FrameIndex value 16 | INT_FRAME_INDEX_SLIDER_DEFAULT_MAX = 99 # Default slider_FrameIndex maximum 17 | 18 | # ReferenceRole 19 | STR_SeqBrowserNode_RefRole_Selected = 'SeqBrowser_Ref_CurSelected' 20 | 21 | 22 | 23 | '''==================================================================================================================''' 24 | # 25 | # sl__US_SeqViewer 26 | # 27 | class sl__US_SeqViewer(ScriptedLoadableModule): 28 | 29 | def __init__(self, parent): 30 | ScriptedLoadableModule.__init__(self, parent) 31 | self.parent.title = "sl__US_SeqViewer" 32 | self.parent.categories = ["SL_Tutorials"] # Set categories (the module shows up in the module selector) 33 | self.parent.dependencies = ["Markups"] # Add here list of module names that this module requires 34 | self.parent.contributors = ["Sen Li (École de Technologie Supérieure)"] 35 | # TODO: 10. update with a link to online module Tutorial 36 | self.parent.helpText = """This is sl__US_SeqViewer ! """ 37 | self.parent.helpText += self.getDefaultModuleDocumentationLink() 38 | self.parent.acknowledgementText = 'Step-by-step tutorial on 3D Slicer extension development. ' \ 39 | '\nThis file was originally developed by Sen Li, LATIS, École de techonologie supérieure. ' \ 40 | '\nSen.Li.1@ens.etsmtl.ca' 41 | 42 | print("sl__US_SeqViewer(ScriptedLoadableModule): __init__(self, parent)") 43 | 44 | '''==================================================================================================================''' 45 | class sl__US_SeqViewerWidget(ScriptedLoadableModuleWidget, VTKObservationMixin): 46 | 47 | def __init__(self, parent=None): 48 | """ Called when the user opens the module the first time and the widget is initialized. """ 49 | ScriptedLoadableModuleWidget.__init__(self, parent) 50 | VTKObservationMixin.__init__(self) # needed for parameter node observation 51 | self.logic = None 52 | self._parameterNode = None # Singleton initialized through self.setParameterNode(self.logic.getParameterNode()) 53 | self._updatingGUIFromParameterNode = False 54 | 55 | print("**Widget.__init__(self, parent)") 56 | 57 | def setup(self): 58 | print("**Widget.setup(self), \tSL_Developer") 59 | """ 00. Called when the user opens the module the first time and the widget is initialized. """ 60 | ScriptedLoadableModuleWidget.setup(self) 61 | 62 | # 01. Load widget from .ui file (created by Qt Designer). 63 | # Additional widgets can be instantiated manually and added to self.layout. 64 | uiWidget = slicer.util.loadUI(self.resourcePath('UI/sl__US_SeqViewer.ui')) 65 | self.layout.addWidget(uiWidget) 66 | self.ui = slicer.util.childWidgetVariables(uiWidget) 67 | 68 | # 02. Set scene in MRML widgets. Make sure that in Qt designer the 69 | # top-level qMRMLWidget's "mrmlSceneChanged(vtkMRMLScene*)" signal in is connected to 70 | # each MRML widget's "setMRMLScene(vtkMRMLScene*)" slot. 71 | uiWidget.setMRMLScene(slicer.mrmlScene) 72 | 73 | # 03. Create logic class. Logic implements all computations that should be possible to run 74 | # in batch mode, without a graphical user interface. 75 | self.logic = sl__US_SeqViewerLogic() 76 | 77 | # 04. Connections, ensure that we update parameter node when scene is closed 78 | self.addObserver(slicer.mrmlScene, slicer.mrmlScene.StartCloseEvent, self.onSceneStartClose) 79 | self.addObserver(slicer.mrmlScene, slicer.mrmlScene.EndCloseEvent, self.onSceneEndClose) 80 | 81 | # 05. SL_Developer. Connect Signal-Slot, ensure that whenever user changes some settings on the GUI, 82 | # that is saved in the MRML scene (in the selected parameter node). 83 | self.ui.sequenceSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.onSelectedNodeChanged) 84 | slicer.modules.sequences.toolBar().activeBrowserNodeChanged.connect(self.onSelectedNodeChanged) 85 | 86 | self.ui.slider_SeqFrame.connect("valueChanged(int)", self.onSliderFrameIndex_ValueChanged) 87 | 88 | 89 | # 06. Needed for programmer-friendly Module-Reload where the Module had already been enter(self)-ed; 90 | # Otherwise, will initial through function enter(self) 91 | if self.parent.isEntered: 92 | self.initializeParameterNode() # Every-Module own a Singleton ParameterNode track by **Logic.moduleName! 93 | 94 | # ------------------------------------------------------------------------------------------------------------------ 95 | def cleanup(self): 96 | """ Called when the application closes and the module widget is destroyed. """ 97 | print("**Widget.cleanup(self)") 98 | self.removeObservers() 99 | 100 | # ------------------------------------------------------------------------------------------------------------------ 101 | def enter(self): 102 | """ Called each time the user opens this module. """ 103 | print("**Widget.enter(self)") 104 | 105 | # 01. Slicer. SL__Note: Every-Module own a Singleton ParameterNode that can be identified by 106 | # self._parameterNode.GetAttribute('ModuleName')! Need to initial every Entry! 107 | self.initializeParameterNode() 108 | 109 | # ------------------------------------------------------------------------------------------------------------------ 110 | def exit(self): 111 | """ Called each time the user opens a different module. """ 112 | print("**Widget.exit(self)") 113 | # Slicer. Do not react to parameter node changes (GUI will be updated when the user enters into the module) 114 | self.removeObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromParameterNode) 115 | 116 | # ------------------------------------------------------------------------------------------------------------------ 117 | def onSceneStartClose(self, caller, event): 118 | """ Called just before the scene is closed. """ 119 | print("**Widget.onSceneStartClose(self, caller, event)") 120 | 121 | # Slicer. Parameter node will be reset, do not use it anymore 122 | self.setParameterNode(None) 123 | 124 | # ------------------------------------------------------------------------------------------------------------------ 125 | def onSceneEndClose(self, caller, event): 126 | """ Called just after the scene is closed. """ 127 | print("**Widget.onSceneEndClose(self, caller, event)") 128 | # If this module is shown while the scene is closed then recreate a new parameter node immediately 129 | if self.parent.isEntered: 130 | self.initializeParameterNode() 131 | 132 | # ------------------------------------------------------------------------------------------------------------------ 133 | def initializeParameterNode(self): 134 | """ Ensure parameter node exists and observed. """ 135 | # 01. Slicer-Initial: the Singleton ParameterNode stores all user choices in param-values, node selections... 136 | # so that when the scene is saved and reloaded, these settings are restored. 137 | self.setParameterNode(self.logic.getParameterNode()) 138 | 139 | # 02. SL_Developer. To update ParameterNode and attach observers 140 | pass 141 | 142 | 143 | # ------------------------------------------------------------------------------------------------------------------ 144 | def setParameterNode(self, inputParameterNode): 145 | """ SL_Notes: Set and observe the Singleton ParameterNode. 146 | Observation is needed because when ParameterNode is changed then the GUI must be updated immediately. 147 | """ 148 | print("**Widget.setParameterNode(self, inputParameterNode)") 149 | if inputParameterNode: 150 | if not inputParameterNode.IsSingleton(): 151 | raise ValueError(f'SL__Allert! \tinputParameterNode = \n{inputParameterNode.__str__()}') 152 | self.logic.setDefaultParameters(inputParameterNode) 153 | 154 | # 01. Unobserve previously selected Singleton ParameterNode; 155 | if self._parameterNode is not None: 156 | self.removeObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromParameterNode) 157 | # 02. Set new Singleton ParameterNode and Add an observer to the newly selected 158 | self._parameterNode = inputParameterNode 159 | if self._parameterNode is not None: 160 | self.addObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromParameterNode) 161 | # 03. Initial GUI update; need to do this GUI update whenever there is a change from the Singleton ParameterNode 162 | self.updateGUIFromParameterNode() 163 | 164 | # ================================================================================================================== 165 | # ================================================================================================================== 166 | # =========== SL_Developer, Section I: get, set, obtain ================================ 167 | # ------------------------------------------------------------------------------------------------------------------ 168 | def getSelectedItemNumber_FromGUI_Slider(self): 169 | # Slider FrameIndex starts from 1, but idx_SelectedItemNumber starts 0. 170 | idx_CurSeqBrowser_SelectedItemNumber = self.ui.slider_SeqFrame.value - INT_SliderFrameIndex_Min 171 | return idx_CurSeqBrowser_SelectedItemNumber 172 | 173 | # ------------------------------------------------------------------------------------------------------------------ 174 | # ================================================================================================================== 175 | # ================================================================================================================== 176 | # =========== SL_Developer, Section II-A: updateGUIFromParameterNode__ & Slots that call uiUpdate = 177 | # ------------------------------------------------------------------------------------------------------------------ 178 | def updateGUIFromParameterNode(self, caller=None, event=None): 179 | """ This method is called whenever parameter node is changed. 180 | The module GUI is updated to show the current state of the parameter node. """ 181 | # 00. Check self._updatingGUIFromParameterNode to prevent from GUI changes 182 | # (it could cause infinite loop: GUI change -> UpdateParamNode -> Update GUI -> UpdateParamNode) 183 | if self._parameterNode is None or self._updatingGUIFromParameterNode: 184 | return 185 | 186 | # I. Open-Brace: Make sure GUI changes do not call updateParameterNodeFromGUI__ (it could cause infinite loop) 187 | self._updatingGUIFromParameterNode = True 188 | # -------------------------------------------------------------------------------------------------------------- 189 | # II. SL_Developer, C: In-Brace, Update UI widgets () 190 | print("**Widget.updateGUIFromParameterNode(self, caller=None, event=None), \tSL_Developer") 191 | # II-01. Update Values of Node-Selectors (qMRMLNodeComboBox) 192 | nodeSeqBrowser_Selected = self._parameterNode.GetNodeReference(STR_SeqBrowserNode_RefRole_Selected) 193 | self.ui.sequenceSelector.setCurrentNode(nodeSeqBrowser_Selected) 194 | # II-02. Update Status of slider_SeqFrame, and label_FrameIndex: QLabel, Sliders (ctkSliderWidget) 195 | self.uiUpdate_Slider_SeqFrame__by__nodeSeqBrowser_Selected(nodeSeqBrowser_Selected) 196 | 197 | # -------------------------------------------------------------------------------------------------------------- 198 | # III. Close-Brace: All the GUI updates are done; 199 | self._updatingGUIFromParameterNode = False 200 | 201 | # ------------------------------------------------------------------------------------------------------------------ 202 | # ------------------------------------------------------------------------------------------------------------------ 203 | def onSliderFrameIndex_ValueChanged(self, caller=None, event=None): 204 | ''' SL_Notes: Not UserOnly function, can be called when a target_ControlPoint is selected! ''' '''''' 205 | # 00. Check Singleton ParameterNode: in case of enter() or onSceneStartClose() 206 | if self._parameterNode is None or self._updatingGUIFromParameterNode: 207 | return 208 | 209 | # 01. LogicUpdate: nodeSeqBrowser's Current-SelectedItemNumber 210 | idx_CurFrame = self.getSelectedItemNumber_FromGUI_Slider() 211 | self.logic.logicUpdate_SwitchSelection_SelectedSeqBrowser_ChangeFrameIndex(idx_CurFrame) 212 | print(f'\t**Widget.onSliderFrameIndex_ValueChanged,\tidx_CurFrame = {idx_CurFrame}') 213 | 214 | # 02. uiUpdate: LandmarkPositionLabels 215 | self._updatingGUIFromParameterNode = True # I. Open-Brace: Avoid updateParameterNodeFromGUI__ (infinite loop) 216 | self.uiUpdate_SwitchSelection_SelectedSeqBrowser_ChangeFrameIndex() # II. In-Brace: uiUpdate 217 | self._updatingGUIFromParameterNode = False # III. Close-Brace: All the GUI updates are done; 218 | 219 | # ------------------------------------------------------------------------------------------------------------------ 220 | def onSelectedNodeChanged(self, node_NewActiveBrowser=None, event=None): 221 | ''' SL_Notes: Not UserOnly function, can be called when a target_ControlPoint is selected! ''' '''''' 222 | print(f"\nBeginning of **Widget.onSelectedNodeChanged(): \tnode_NewActiveBrowser =" 223 | f" {node_NewActiveBrowser.GetID() if node_NewActiveBrowser else type(node_NewActiveBrowser)}") 224 | # 00-A. Check Singleton ParameterNode: important test for every NodeChange Slot, in case of onSceneStartClose() 225 | # Check _updatingGUIFromParameterNode: avoid bugs introduced by Slicer (PointAdded, PointPositionDefined) 226 | if self._parameterNode is None or self._updatingGUIFromParameterNode: 227 | return 228 | # 00-B. Check the validity of node_NewActiveBrowser 229 | if not node_NewActiveBrowser: 230 | return 231 | 232 | # 01. LogicUpdate 233 | self.updateParameterNodeFromGUI__Set_RefRoleNodeID(STR_SeqBrowserNode_RefRole_Selected, node_NewActiveBrowser.GetID()) 234 | 235 | # 02. uiUpdate: update slider_SeqFrame 236 | if self.parent.isEntered: 237 | # I. Open-Brace: Make sure GUI changes do not call updateParameterNodeFromGUI__ (it could cause infinite loop) 238 | self._updatingGUIFromParameterNode = True 239 | # -------------------------------------------------------------------------------------------------------------- 240 | # II-02-A. Re-Set sequenceSelector, just in case the Signal sender is Sequences.toolBar() 241 | self.ui.sequenceSelector.setCurrentNode(node_NewActiveBrowser) 242 | # II-02-B. Re-Set modules.sequences active SeqBrowser, just in case the Signal sender is Laminae-Labeling 243 | slicer.modules.sequences.widgetRepresentation().setActiveBrowserNode(node_NewActiveBrowser) 244 | # II-02-C. Push Slicer Screen refresh before uiUpdate 245 | self.uiUpdate_PushSlicerScreenUpdate_by_ShakeTargetSeqBrowser(node_NewActiveBrowser) 246 | # II-02-D. Start uiUpdate 247 | self.uiUpdate_SwitchSelection_ChangeSeqBrowser_RemainFrameIndex(node_NewActiveBrowser) 248 | # -------------------------------------------------------------------------------------------------------------- 249 | # III. Close-Brace: All the GUI updates are done; 250 | self._updatingGUIFromParameterNode = False 251 | 252 | # ------------------------------------------------------------------------------------------------------------------ 253 | # ------------------------------------------------------------------------------------------------------------------ 254 | # ================================================================================================================== 255 | # ----------- Section II-B: Sub-Functions called by updateGUIFromParameterNode__ or Slot functions --- 256 | # ----- 1. All sub-functions starts with uiUpdate ---------------------------------------------------- 257 | # ----- 2. All uiUpdate functions canNOT set self._updatingGUIFromParameterNode ---- 258 | # ----- 3. The superior function who call uiUpdate function MUST set self._updatingGUIFromParameterNode ---- 259 | # ------------------------------------------------------------------------------------------------------------------ 260 | # ------------------------------------------------------------------------------------------------------------------ 261 | def uiUpdate_Slider_SeqFrame__by__nodeSeqBrowser_Selected(self, nodeSeqBrowser_Selected): 262 | ''' **Widget.uiUpdate_Slider_SeqFrame__by__nodeSeqBrowser_Selected(self, nodeSeqBrowser_Selected) ''' '''''' 263 | if nodeSeqBrowser_Selected: 264 | str_CurSeqBrowser_ID = 'nodeSeqBrowser_Selected.GetID() = ' + nodeSeqBrowser_Selected.GetID() 265 | str_NumberOfItems = '.GetNumberOfItems() = ' + str(nodeSeqBrowser_Selected.GetNumberOfItems()) 266 | str_idxFrame = f', \tidxFrame = {self.logic.obtain_idxSliderCurFrame_from_TargetSeqBrowser(nodeSeqBrowser_Selected)}' 267 | else: 268 | str_CurSeqBrowser_ID = 'CurSeqBrowser.GetID() = ' + str(type(nodeSeqBrowser_Selected)) 269 | str_NumberOfItems = '' 270 | str_idxFrame = '' 271 | print(f"\t**Widget.uiUpdate_Slider_SeqFrame__by__nodeSeqBrowser_Selected(), {str_CurSeqBrowser_ID}, {str_NumberOfItems}{str_idxFrame}") 272 | 273 | if nodeSeqBrowser_Selected and nodeSeqBrowser_Selected.GetNumberOfItems() > 0: 274 | self.ui.slider_SeqFrame.enabled = True 275 | self.ui.slider_SeqFrame.minimum = INT_SliderFrameIndex_Min 276 | self.ui.slider_SeqFrame.maximum = nodeSeqBrowser_Selected.GetNumberOfItems() 277 | self.ui.slider_SeqFrame.value = self.logic.obtain_idxSliderCurFrame_from_TargetSeqBrowser(nodeSeqBrowser_Selected) 278 | self.ui.label_FrameIndex.setText(str(self.ui.slider_SeqFrame.value)) 279 | else: 280 | # No SequenceBrowser_Node available, so we disable the slider_SeqFrame, and set label_FrameIndex 'N/A' 281 | self.ui.slider_SeqFrame.enabled = False 282 | self.ui.slider_SeqFrame.minimum = INT_SliderFrameIndex_Min 283 | self.ui.slider_SeqFrame.maximum = INT_FRAME_INDEX_SLIDER_DEFAULT_MAX 284 | self.ui.slider_SeqFrame.value = INT_FRAME_INDEX_SLIDER_DEFAULT 285 | self.ui.label_FrameIndex.setText('N/A') 286 | 287 | # ------------------------------------------------------------------------------------------------------------------ 288 | # ================================================================================================================== 289 | # ------------------------------------------------------------------------------------------------------------------ 290 | def uiUpdate_SwitchSelection_ChangeSeqBrowser_RemainFrameIndex(self, node_NewActiveBrowser): 291 | ''' **Widget.uiUpdate_SwitchSelection_ChangeSeqBrowser_RemainFrameIndex(self, nodeTarget_SeqBrowser) ''' '''''' 292 | # 00-A. Check if the module isEntered 293 | if not self.parent.isEntered: return 294 | # 00-B. Check the validity of nodeTarget_SeqBrowser 295 | if not node_NewActiveBrowser: return 296 | 297 | # 01. Update slider_SeqFrame 298 | if node_NewActiveBrowser: 299 | str_CurSeqBrowser_ID = 'node_NewActiveBrowser.GetID() = ' + node_NewActiveBrowser.GetID() 300 | str_NumberOfItems = 'idx_SeqBrowserSelectedItem = ' + str(node_NewActiveBrowser.GetNumberOfItems()) 301 | else: 302 | str_CurSeqBrowser_ID = 'node_NewActiveBrowser.GetID() = ' + str(type(node_NewActiveBrowser)) 303 | str_NumberOfItems = '' 304 | print(f"\t**Widget.uiUpdate_SwitchSelection_ChangeSeqBrowser_RemainFrameIndex(), {str_CurSeqBrowser_ID}, {str_NumberOfItems}") 305 | self.uiUpdate_Slider_SeqFrame__by__nodeSeqBrowser_Selected(node_NewActiveBrowser) 306 | 307 | 308 | # ------------------------------------------------------------------------------------------------------------------ 309 | def uiUpdate_SwitchSelection_SelectedSeqBrowser_ChangeFrameIndex(self): 310 | ''' **Widget.uiUpdate_SwitchSelection_SelectedSeqBrowser_ChangeFrameIndex(self) 311 | There are two modes to trigger this uiUpdate: UI modified / Non-UI (node) modified. 312 | To guarantee the Non-UI mode, we will update all UI widgets (including the possible TriggerMan UI widget). 313 | All uiUpdate can be done by logicUpdated nodeSeqBrowser_Selected, thus argument idx_TargetFrame NotRequired. 314 | ''' '''''' 315 | # 00-A. Check if the module isEntered 316 | if not self.parent.isEntered: return 317 | # 00-B. Check the validity of nodeSeqBrowser_Selected 318 | nodeSeqBrowser_Selected = self._parameterNode.GetNodeReference(STR_SeqBrowserNode_RefRole_Selected) 319 | if not nodeSeqBrowser_Selected: return 320 | 321 | # 01. Update the uiSlider 322 | self.uiUpdate_Slider_SeqFrame__by__nodeSeqBrowser_Selected(nodeSeqBrowser_Selected) 323 | 324 | 325 | # ------------------------------------------------------------------------------------------------------------------ 326 | def uiUpdate_PushSlicerScreenUpdate_by_ShakeTargetSeqBrowser(self, nodeTarget_SeqBrowser): 327 | print(f' **Widget.uiUpdate_PushSlicerScreenUpdate_by_ShakeTargetSeqBrowser()') 328 | if nodeTarget_SeqBrowser: 329 | # Let's push Slicer to update by Setting current selected frame back and forth 330 | idx_curFrame = nodeTarget_SeqBrowser.GetSelectedItemNumber() 331 | 332 | nodeTarget_SeqBrowser.SetSelectedItemNumber(max(idx_curFrame - 1, 0)) 333 | nodeTarget_SeqBrowser.SetSelectedItemNumber(min(idx_curFrame + 1, nodeTarget_SeqBrowser.GetNumberOfItems() - 1)) 334 | nodeTarget_SeqBrowser.SetSelectedItemNumber(idx_curFrame) 335 | 336 | # ================================================================================================================== 337 | # ================================================================================================================== 338 | # =========== SL_Developer, Section IV: updateParameterNodeFromGUI__ ============================== 339 | # ------------------------------------------------------------------------------------------------------------------ 340 | def updateParameterNodeFromGUI__Set_RefRoleNodeID(self, STR_RefRole, str_NodeID): 341 | """ Read GUI Method: Method updateParameterNodeFromGUI__ is called when users makes any change in the GUI. 342 | Changes are saved into the parameter node (so that they are restored when the scene is saved and loaded). 343 | **Widget.updateParameterNodeFromGUI__Set_RefRoleNodeID(self, STR_RefRole, str_NodeID) """ 344 | if self._parameterNode is None or self._updatingGUIFromParameterNode: 345 | return 346 | 347 | # I. Before updating the Singleton ParameterNode; Disable Modify events, e.g., vtk.vtkCommand.ModifiedEvent 348 | wasModified = self._parameterNode.StartModify() # Modify all properties in a single batch 349 | 350 | # II. Update the Singleton ParameterNode; No updateGUIFromParameterNode triggered in this step 351 | node_BeforeChange = self._parameterNode.GetNodeReference(STR_RefRole) 352 | if node_BeforeChange: str_NodeBeforeChange = self._parameterNode.GetNodeReference(STR_RefRole).GetID() 353 | else: str_NodeBeforeChange = "" 354 | print(f'\tBefore Update: {str_NodeBeforeChange}') 355 | self._parameterNode.SetNodeReferenceID(STR_RefRole, str_NodeID) 356 | print(f'\tAfter Update: {self._parameterNode.GetNodeReference(STR_RefRole).GetID()}') 357 | 358 | # III. After updating the Singleton ParameterNode; Enable Modify events, e.g., vtk.vtkCommand.ModifiedEvent 359 | self._parameterNode.EndModify(wasModified) 360 | 361 | 362 | # ------------------------------------------------------------------------------------------------------------------ 363 | # ------------------------------------------------------------------------------------------------------------------ 364 | ''' =================================================================================================================''' 365 | # 366 | # sl__US_SeqViewerLogic 367 | # 368 | class sl__US_SeqViewerLogic(ScriptedLoadableModuleLogic): 369 | """ The Logic class is : to facilitate dynamic reloading of the module without restarting the application. 370 | This class should implement all the actual computation done by your module. 371 | The interface should be such that other python code can import this class 372 | and make use of the functionality without requiring an instance of the Widget. 373 | Uses ScriptedLoadableModuleLogic base class, available at: 374 | https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py 375 | """ 376 | 377 | def __init__(self): 378 | """ Called when the logic class is instantiated. Can be used for initializing member variables. """ 379 | ScriptedLoadableModuleLogic.__init__(self) 380 | 381 | self._isSwitchingSeqBrowser = False 382 | 383 | # ================================================================================================================== 384 | # ================================================================================================================== 385 | # =========== SL_Developer, Section VI: get, set, obtain, & createNewNode ===================== 386 | # ------------------------------------------------------------------------------------------------------------------ 387 | def obtain_idxSliderCurFrame_from_TargetSeqBrowser(self, nodeTarget_SeqBrowser): 388 | ''' **Logic.obtain_idxSliderCurFrame_from_TargetSeqBrowser(self, nodeTarget_SeqBrowser) ''' '''''' 389 | idx_SliderCurFrame = nodeTarget_SeqBrowser.GetSelectedItemNumber() + INT_SliderFrameIndex_Min 390 | return idx_SliderCurFrame 391 | 392 | 393 | # ------------------------------------------------------------------------------------------------------------------ 394 | # ================================================================================================================== 395 | # ================================================================================================================== 396 | # =========== SL_Developer, Section VII-A: logicUpdate & Functions that call paramNodeUpdate ==== 397 | # ------------------------------------------------------------------------------------------------------------------ 398 | def setDefaultParameters(self, parameterNode): 399 | """ SL_Developer, B: Initialize parameter node, Re-Enter, Re-Load. """ 400 | print("**Logic.setDefaultParameters(self, parameterNode), \tSL_Developer, B"); 401 | # I. Before updating the Singleton ParameterNode; Disable Modify events, e.g., vtk.vtkCommand.ModifiedEvent 402 | wasModified = parameterNode.StartModify() # Modify all properties in a single batch 403 | # -------------------------------------------------------------------------------------------------------------- 404 | # II. Update the Singleton ParameterNode; No updateGUIFromParameterNode triggered in this step 405 | # II-01. Set NodeRef for curSelected SeqBrowser, select the first if not selected 406 | if not parameterNode.GetNodeReference(STR_SeqBrowserNode_RefRole_Selected): 407 | node_SeqBrowser_First = slicer.mrmlScene.GetFirstNodeByClass("vtkMRMLSequenceBrowserNode") 408 | if node_SeqBrowser_First: 409 | # II-01-A. Set NodeRefID for paramNode 410 | parameterNode.SetNodeReferenceID(STR_SeqBrowserNode_RefRole_Selected, node_SeqBrowser_First.GetID()) 411 | # II-01-B. Synchronize with modules.sequences's SequenceBrowser active node 412 | slicer.modules.sequences.widgetRepresentation().setActiveBrowserNode(node_SeqBrowser_First) 413 | else: 414 | # II-01-C. Already got NodeRefID for paramNode, we only need to Synchronize with modules.sequences 415 | nodeSeqBrowser_Selected = parameterNode.GetNodeReference(STR_SeqBrowserNode_RefRole_Selected) 416 | slicer.modules.sequences.widgetRepresentation().setActiveBrowserNode(nodeSeqBrowser_Selected) 417 | 418 | # -------------------------------------------------------------------------------------------------------------- 419 | # III. After updating the Singleton ParameterNode; Enable Modify events, e.g., vtk.vtkCommand.ModifiedEvent 420 | parameterNode.EndModify(wasModified) 421 | 422 | 423 | # ------------------------------------------------------------------------------------------------------------------ 424 | # ------------------------------------------------------------------------------------------------------------------ 425 | # ----------- Section VII-B: Sub-Functions with prefix/surfix paramNodeUpdate ---------------------- 426 | # ----- 1. All sub-functions prefix/surfix with paramNodeUpdate; -------------------------------------- 427 | # ------2. All paramNodeUpdate functions canNOT self.getParameterNode().StartModify() --- 428 | # ------3. The superior function who call paramNodeUpdate function MUST self.getParameterNode().StartModify() --- 429 | # ------------------------------------------------------------------------------------------------------------------ 430 | 431 | 432 | # ------------------------------------------------------------------------------------------------------------------ 433 | # ================================================================================================================== 434 | # ================================================================================================================== 435 | # =========== SL_Developer, Section VIII: Other Logic Functions =============================== 436 | # ------------------------------------------------------------------------------------------------------------------ 437 | # --------------- Section VIII-01: Boolean (is_) Functions --------------------------------------- 438 | # ------------------------------------------------------------------------------------------------------------------ 439 | def isValid_idxTargetFrame(self, nodeSeqBrowser, idx_TargetFrame): 440 | ''' **Logic.isValid_idxTargetFrame(self, nodeSeqBrowser, idx_TargetFrame) ''' '''''' 441 | if nodeSeqBrowser and idx_TargetFrame >=0 and idx_TargetFrame < nodeSeqBrowser.GetNumberOfItems(): 442 | return True; 443 | else: 444 | return False 445 | 446 | 447 | # ------------------------------------------------------------------------------------------------------------------ 448 | # --------------- Section VIII-02: Set / Update Functions --------------------------------------- 449 | # ------------------------------------------------------------------------------------------------------------------ 450 | # ------------------------------------------------------------------------------------------------------------------ 451 | # ------------------------------------------------------------------------------------------------------------------ 452 | def logicUpdate_SwitchSelection_SelectedSeqBrowser_ChangeFrameIndex(self, idx_TargetFrame): 453 | ''' **Logic.logicUpdate_SwitchSelection_SelectedSeqBrowser_ChangeFrameIndex(self, idx_TargetFrame) ''' '''''' 454 | # 00-A. Check the validity of nodeSeqBrowser_Selected and idx_TargetFrame 455 | nodeSeqBrowser_Selected = self.getParameterNode().GetNodeReference(STR_SeqBrowserNode_RefRole_Selected) 456 | if not nodeSeqBrowser_Selected: return 457 | # 00-B. Check the validity of idx_TargetFrame 458 | if not self.isValid_idxTargetFrame(nodeSeqBrowser_Selected, idx_TargetFrame): 459 | raise ValueError(f'SL_Alert! Invalid idx_TargetFrame = {idx_TargetFrame}'); return 460 | 461 | # 01. Update nodeSeqBrowser along with its the Current-SelectedItemNumber 462 | nodeSeqBrowser_Selected.SetSelectedItemNumber(idx_TargetFrame) 463 | print(f"\t\t**Logic.logicUpdate_SwitchSelection_SelectedSeqBrowser_ChangeFrameIndex()\t" 464 | f"nodeSeqBrowser_Selected.GetID() = {nodeSeqBrowser_Selected.GetID()}, idx_TargetFrame = {idx_TargetFrame}") 465 | 466 | 467 | 468 | 469 | ''' =================================================================================================================''' 470 | ''' =================================================================================================================''' 471 | ''' =================================================================================================================''' 472 | # 473 | # sl__US_SeqViewerTest 474 | # 475 | class sl__US_SeqViewerTest(ScriptedLoadableModuleTest): 476 | """ This is the test case for your scripted module. 477 | Uses ScriptedLoadableModuleTest base class, available at: 478 | https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py """ 479 | def setUp(self): 480 | """ Do whatever is needed to reset the state - typically a scene clear will be enough. """ 481 | slicer.mrmlScene.Clear() 482 | 483 | def runTest(self): 484 | """Run as few or as many tests as needed here. """ 485 | self.setUp() 486 | self.test_sl__US_SeqViewer1() 487 | 488 | def test_sl__US_SeqViewer1(self): 489 | """ Ideally you should have several levels of tests. At the lowest level 490 | tests should exercise the functionality of the logic with different inputs 491 | (both valid and invalid). At higher levels your tests should emulate the 492 | way the user would interact with your code and confirm that it still works 493 | the way you intended. 494 | One of the most important features of the tests is that it should alert other 495 | developers when their changes will have an impact on the behavior of your 496 | module. For example, if a developer removes a feature that you depend on, 497 | your test should break so they know that the feature is needed. 498 | """ 499 | 500 | self.delayDisplay("Starting the test") 501 | 502 | pass 503 | 504 | self.delayDisplay('Test passed') 505 | 506 | 507 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Step-by-step tutorial of 3D Slicer Extension Module Development 2 | 3 | [![IMAGE ALT TEXT HERE](https://img.youtube.com/vi/rr0Q9GUuz7E/0.jpg)](https://www.youtube.com/watch?v=rr0Q9GUuz7E&list=PLTuWbByD80TORd1R-J7j7nVQ9fot3C2fK) 4 | 5 | 6 | 7 | 8 | 9 | ## Step 02: Interact, Debug & Pipeline 10 | 11 | isolated 12 | 13 | #### YouTube Video Tutorial for Step_02 14 | 15 | #### Bilibili Video Tutorial for Step_02 16 | 17 | 18 | 19 | 20 | 21 | 22 | ## Step 03: Developer Foci, Neat Blanks & GUI Hello World 23 | 24 | isolated 25 | 26 | #### YouTube Video Tutorial for Step_03 27 | 28 | #### Bilibili Video Tutorial for Step_03 29 | 30 | 31 | 32 | 33 | 34 | ## Step 04: Code Style, Method Groups & US Sequence Viewer 35 | 36 | isolated 37 | 38 | #### YouTube Video Tutorial for Step_04 39 | 40 | #### Bilibili Video Tutorial for Step_04 41 | --------------------------------------------------------------------------------