Changing the background of 3D view at startup

Hi,

i have a class derived from qSlicerMainWindow (according to https://github.com/KitwareMedical/SlicerCustomAppTemplate).

In the method setupUi, after the line this->Superclass::setupUi(mainWindow); I am setting a custom layout. This works so far. The custom layout contains a new 3d view, which is generated correctly.
After that, the scene contains three 3d views, which I find odd, I would have thought that the scene would only contain two of them after the change to my layout. One is called ‘View’, one is called ‘View1’, and one is called ‘View’.

When I then (still in setupUi) do the following:

  vtkMRMLViewLogic* ViewLogic = layoutManager->threeDWidget("My3dView")->viewLogic();
  vtkMRMLViewNode* ViewNode = layoutManager->threeDWidget("My3dView")->mrmlViewNode();
  ViewLogic->StartViewNodeInteraction(vtkMRMLViewNode::BackgroundColorFlag);
  int wasModifying = ViewNode->StartModify();
  ViewNode->SetBackgroundColor(0.0f, 0.0f, 0.0f);
  ViewNode->SetBackgroundColor2(0.0f, 0.0f, 0.0f);
  ViewNode->EndModify(wasModifying);
  ViewLogic->EndViewNodeInteraction();

the background color is unchanged when the main window appears.

Is there something wrong with my code or do I have to change the background color of the 3d views at a different place (i.e. somewhere later in program execution)?

Any advice would be appreciated.
Robert

Default view properties are read from application settings, written to default view and slice nodes in the scene, and then views are updated from the default nodes.

Here is an example of how default nodes are set for slice views.

I would recommend to attach a debugger and step through the view initialization process to get familiar how it works.

Thank you for the hint Andras, that helped me solve it.

I found out the following sequence of vtkMRMLViewNode creations:

  1. Creation of qSlicerApplication - RegisterNodeClass (vtkMRMLScene constructor)
  2. qSlicerMainWindow initialization - layout manager creates new node - no default node yet present, thus node->Reset(defaultNode); is not called
  3. Initialization of the window object derived from qSlicerMainWindow - I set a new layout here - the layout manager creates a new node - no default node yet present, thus node->Reset(defaultNode); is not called
  4. In vtkMRMLLayoutLogic::CreateMissingViews the node is set to the scene, which leads to vtkMRMLViewLogic::UpdateViewNode() that tries to get the view node of the scene which fits to its name, however does not find one. So it creates a new vtkMRMLViewNode for itself and also adds this node to the scene. The name is auto-generated: “View”.
  5. A new scene for vtkSlicerunitsLogic is created ( RegisterNodeClass (vtkMRMLScene constructor) )
  6. postInitializeApplication: default 3d view node is created while loading the module ‘ViewControllers’ and set to the scene. Only the settings BoxVisibility, AxisLabelsVisibility, UseOrthographicProjection, UseDepthPeeling, OrientationMarkerType, OrientationMarkerSize, RulerType are read from QSettings and set to the default node. Maybe it would be worth to read also other settings from the QSettings. All present views are then reset to the default node.

What I did to solve my problem was to make the following connection in my main window:

Q_Q(MyMainWindow);
QObject::connect(q, SIGNAL(initialWindowShown()), q, SLOT(startupCompleted()));

with

void MyMainWindow::startupCompleted()
{
    qSlicerApplication * app = qSlicerApplication::application();
    std::vector<vtkMRMLNode*> nodes;
    app->mrmlScene()->GetNodesByClass("vtkMRMLViewNode", nodes);
    nodes.push_back(app->mrmlScene()->GetDefaultNodeByClass("vtkMRMLViewNode"));
    
    for (auto&& node : nodes)
    {
        vtkMRMLViewNode* viewNode = vtkMRMLViewNode::SafeDownCast(node);
        int wasModifying = viewNode->StartModify();
        viewNode->SetBackgroundColor(0.0f, 0.0f, 0.0f);
        viewNode->SetBackgroundColor2(0.0f, 0.0f, 0.0f);
        viewNode->SetBoxVisible(false);
        viewNode->SetAxisLabelsVisible(false);
        viewNode->EndModify(wasModifying);
    }
}

This works now, the settings are correctly taken over. However, I suspect a malfunction here in the Slicer code. The standard vtkMRMLViewNode that is created at Slicer startup is called View1, so this is clearly step 2 in the list above. The third (step 3) is created by my layout change request. The fourth creation, however, is suspicious to me. Maybe there is a mixup between Name, SingletonTag, and LayoutLabel? Here the fields of the nodes for comparison:

NodeFromStep3
Name: Viewcx3dView (probably generated based on the SingletonTag)
SingletonTag: cx3dView (correct according to my layout definition)
LayoutLabel: 3D view (correct according to my layout definition)

NodeFromStep4
Name: View (autogenerated)
SingletonTag: 3D view (from my LayoutLabel?)
LayoutLabel: 1 (autogenerated?)

Moreover, when I do the following:

vtkMRMLViewLogic* ViewLogic = layoutManager->threeDWidget("Viewcx3dView")->viewLogic();
vtkMRMLViewNode* ViewNode1 = layoutManager->threeDWidget("Viewcx3dView")->mrmlViewNode();
vtkMRMLViewNode* ViewNode2 = ViewLogic->GetViewNode();

then ViewNode1 is NodeFromStep3 and ViewNode2 is NodeFromStep4. Is this how it should be?

Best,
Robert

As soon as the scene is created, you can define default view nodes with modified properties (see and follow this pattern). So, you should be able to do just before you register and set your custom layout, no need to add a callback function for this.

I agree that this is somewhat complicated but it probably works correctly. threeDWidget(QString) is a convenience method that returns widget by name. If you have multiple view nodes by the same name then you will get the first view with matching name. To get view nodes that are used in the current layout, you can iterate through all view nodes in the scene and only use those that are mapped in current layout.

This kind of works. It sets the background black, but for the Box and AxisLabels the result is as follows:
Unbenannt
Not sure why this happens.

Well, this is not the case. I looked at the following parts of the code:


and

and encountered that for the threeDView the LayoutLabel is taken as the name of the logic (it is set in the method setViewLabel) whereas for the slice view, there is a separate method (setSliceViewName in line 376) which sets the LayoutName to the logic. In both cases, however, the logic tries to find the respective node by its name:


and

So, as an experiment, I implemented also for the qMRMLThreeDWidget a separate method setViewName, which is used by the qMRMLLayoutThreeDViewFactory to set the name of the logic according to the LayoutName and removed setting the logic name from setViewLabel. Now, the previously created additional view node (and also an additional camera node that was created) is not created anymore. This is how I would have expected it to be. Of course I am not sure if I miss side effects here or if there was some reason for the different implementations.

Thank you for digging into this!

You are right, it is indeed incorrect in qMRMLThreeDViewControllerWidget that view logic name is set based on layout label instead of layout name. Probably it did not come up as an error before because in built-in view layouts, layout name and label are always the same for 3D views. I’ve fixed the issue and I’ll push a fix today if no complications come up during testing.

1 Like