Intermitten crash with qMRMLTreeView index's in python

I’m working on a custom application that has a qMRMLTreeView in a scripted python module. I’m using this to view models and hierarchies and fibers.

I want to see when users enable/disable a model/hierarchy/fiber, and maybe do something with the mrmlnode.
So I’ve connected the decorationClicked(QModelIndex) signal to a signal handler.

When my signal handler fires, sometimes it generates a crash.

Most frustratingly try/except didn’t catch the exception. It appears that if I could catch the exception that would be close enough for my purposes.
Alternatively any hints on avoiding it would be great.

Here are my more specific details.
I found this crash when getting the mrml node and have now traced it back.
I can replicate the problem when the QModelIndex.data, or the QModelIndex.flags accessors are used. The exact crash is difficult for me to find due to it being buried in qt.
I can mostly reliably replicate the problem using a few models, and a few model hierarchy nodes in the core “Models” module, with slicer nightly 2018-07-12(which my project is derived from).

I don’t know how to create models/model hierarchies programmatically right now, so that part of my testing is manual.
I load up slicer, switch to the models module, and add at least three hierarchy nodes, with at least one nested in the other.
I then load up any two models into the scene, attaching at least one to the deepest hierarchy node.
The crash is most reliable when moving hierarchies around, but simply clicking on the show/hide eye will cause it also. My first thought was some sort of race condition, but adding an arbitrary pause(up to seconds) didn’t resolve the issue in my signal handler.

Models set which should crash

scene
    \HierarchyA
        \HierarchyB
           ModelA
    \HierarchyC
           ModelB

I run the following code in the slicer python terminal
to find the tree view, and add the signal handler.

import re
import time
def findWidget( widget, objectRegex ):
  name_reg=re.compile(objectRegex)
  if name_reg.match(widget.objectName):
    return widget
  else:
    children = []
    for w in widget.children():
      resulting_widget = findWidget(w, objectRegex)
      if resulting_widget:
        return resulting_widget
  return None

# consider slicer.util.findChild of findChildren instead.

def indexDumper(index):
  treeView.disconnect('decorationClicked(QModelIndex)');
  try:
    # printing index always works... and never crashes.
    # when we crash, none of the prints make it through, have to switch to logging to see them.
    print(index)
    # index is both not none, and marked valid yet it still crashes on accessing data or flags
    if ( index is None ):
        print("error, Index is none!")
    if ( index.IsValid()):
       print("index is valid")
    print("row"+str(index.row()))
    print("col"+str(index.column()))
    print("sizof"+str(index.__sizeof__()))
    # Both index.data, and index.flags can generate a crash here.
    # it is intermittent.
    #print("flags"+str(index.flags()))
    #time.sleep(0.1)
    print("data"+str(index.data()))
  except:
    print("Exception on index access!!!")
    return
  print("full dump complete")
  treeView.connect('decorationClicked(QModelIndex)',indexDumper)
  return

mW=slicer.modules.models.widgetRepresentation()
treeView=findWidget(mW,".*[Mm]odel.*[Tt]ree.*")
t=treeView.connect('decorationClicked(QModelIndex)',indexDumper)

# now click wildly on hierarchy nodes

I would suggest to use subject hierarchy tree widget instead. You can create groups of arbitrary nodes, drag-and-drop is very robust (does not crash), and highly customizable (you can define custom icons, custom context menu items, etc. with plug-ins written in Python or C++).

1 Like

Thanks for the hint, that looks like it could be a drop in replacement, am I reading that right?

Yes. You can see it in action in Data module’s subject hierarchy tab.

1 Like

Thank you for this. I’ve gotten over most of the hurdles of integrating qMRMLSubjectHierarchyView now.

I’ve gotten stuck detecting when the visibility is toggled and cant seem to get past it.

I’ve settled on using the clicked(QModelIndex) signal, and only performing actions when the QModelIndex is the visibilityColumn.
It seems that the QModelIndex returned is for the scene instead of the the expected position in the tree as expected.
This is okay because subjecthierarchy.currentItem() gets the item, and can be used to get the data node.
This works if the object clicked is a leaf node but it doesnt work for branches.

Can you offer any advice on how to tell when branch visibility is toggled?

Using QModelIndex is quite fragile and I’d advise against it - these indices are not persistent and may change.

If I had to observe visibility changes then I’d observe the Modified event of display node(s). That said I don’t know exactly what changes visibility, so this may not be a good solution.

1 Like

Thank you both for all the hints. I believe I now have a functional solution. I’ll share it in case anyone tries this in the future.

A simplified overview, when my scripted module sets up, it finds all vtkMRMLModelHierarchyNodes’s,
For any node which has a valid vtkMRMLModelNode, it gets the vtkMRMLModelDisplayNode and adds my custom observer, to watch that displaynode’s modified event. It also adds an attribute to the displaynode so I can find my modelnode. I didn’t see a way to get the modelnode given the displaynode, so there may be room for improvement on that.

Here is my test code which should work when pasted into slicer python terminal.

def modelDisplayModified(mrmlNode,event):
  # hacky event handler for mrmlscene that is fired any time a modeldisplaynode is modified.
  # finds the modelnode using an attribute which was added in setup
  model=slicer.mrmlScene.GetNodeByID(mrmlNode.GetAttribute("vtkMRMLModelNodeID"))
  print(mrmlNode.GetName(),mrmlNode.GetClassName(), model.GetName(),model.GetClassName())
  return

setConnections=True
if setConnections:
  #set connections,
  mrmlNodes=slicer.mrmlScene.GetNodesByClass('vtkMRMLModelHierarchyNode')
  for mn in range(mrmlNodes.GetNumberOfItems()):
    model=mrmlNodes.GetItemAsObject(mn).GetModelNode()
    if model is not None:
      obsNum=model.GetDisplayNode().AddObserver(vtk.vtkCommand.ModifiedEvent, modelDisplayModified )
      model.SetAttribute('mdObserver',str(obsNum))
      model.GetDisplayNode().SetAttribute("vtkMRMLModelNodeID",model.GetID())
else:
  #remove connections,
  mrmlNodes=slicer.mrmlScene.GetNodesByClass('vtkMRMLModelHierarchyNode')
  for mn in range(mrmlNodes.GetNumberOfItems()):
    model=mrmlNodes.GetItemAsObject(mn).GetModelNode()
    if model is not None:
      model.GetDisplayNode().RemoveAttribute("vtkMRMLModelNodeID")
      model.GetDisplayNode().RemoveObserver(int(model.GetAttribute('mdObserver')))
      model.RemoveAttribute('mdObserver')
1 Like

You can use modelNode = modelDisplayNode.GetDisplayableNode().

1 Like

That is much cleaner, thank you.