In the software world, when trying to design a solution to a problem, it is very hard to find a single, all-in-one tool that satisfies our needs. Most of the times we need to make use of a couple of frameworks, several libraries and third-party code. In this article we will show an example of this situation, where we manage to integrate the VTK library into the Qt framework. This combination allows us to create beautiful desktop applications by using QtQuickControls 2 and Material Design, while keeping the 3D processing capabilities and flexibility of VTK.

One of the main advantages of the VTK library is its highly-optimized filters pipeline, which is fairly easy to use.  There are a ton of available filters to choose from, making it also very flexible. Nevertheless, they do not provide appealing UI controls to complement your applications. They do have a Qt integration but it is outdated and uses QtWidgets, which are not as nice as QtQuickControls 2.

On the other hand, the QML module QtQuickControls 2 provides a complete set of multiplatform UI components that will make your applications look awesome. Their state-of-the-art integration of Material Design helps you build smooth, attractive and user-friendly interfaces with very little effort.

In this example we will show you how to build a sample desktop app that is able to open STL files and render them on a 3D canvas which is immersed in a QML window. This window can overlay any kind of component, from toggle switches to progress bars. The UI is defined in QML and the code is written in C++ built using CMake. First, let us start with a simple architectural diagram of the components involved.

The main component that manages the interaction with QML and the other classes in called CanvasHandler. The ProcessingEngine class is in charge of the interaction with the VTK libraries that process the 3D models. This includes opening the file, sanitizing the data and creating and storing Model objects. These Model objects contain the VTK filters to translate the data and the objects that will be used to render it, which are Mappers and Actors. Both command classes, CommandModelAdd and CommandModelTranslate, are used to queue certain actions that the renderer needs to perform. These will be used in a future article to portray the use of the Command design pattern together with an undo/redo stack. The last two classes are the most important ones, QVTKFramebufferObjectItem and QVTKFramebufferObjectRenderer. These are the ones in charge of the interaction between Qt and VTK.

Basically, the QVTKFrambufferObjectItem object (or QVTKFboItem for short) will be a QML component living in the main window with a C++ class to support it. This will be our point of entry from which we will be able to handle what to render and how to do it. This object also provides the possibility of capturing events from the 3D canvas, such as mouse gestures. It is important to point out that this object will live in the application main thread, so we can access it as we do with any other class. On the other hand, the QVTKFramebufferObjectRenderer (or QVTKFboRenderer) will live in what is called the “render” thread. This object will be in charge of handling the render window, the camera and the interaction with the vtkRenderer object. Since it is not thread safe to access the QVTKFboRenderer object directly from the main application thread, we need to make use of the synchronize function provided by the class QQuickFramebufferObject::Renderer. When this method is called, both threads are synchronized and blocked so that they can exchange data and commands safely. In order for the synchronize callback to be executed, we need to call the update method provided by QQuickFramebufferObject from our QVTKFboItem object.

Let us jump to the code now. Although the CanvasHandler class is pretty straightforward, there are still some aspects of it that I wish to emphasize. First of all, we need to register our QVTKFboItem as a QML type so that we can bind the C++ class to the QML component.

qmlRegisterType<QVTKFramebufferObjectItem>(“QtVTK”, 1, 0, “VtkFboItem”);

After loading the main QML file, the QVTKFboItem object will be instantiated. We can recover its reference from QML to C++ to make use of it by using its objectName defined in the main QML file.

QObject *rootObject = engine.rootObjects().first();
m_vtkFboItem = rootObject->findChild<QVTKFramebufferObjectItem*>(“vtkFboItem”);

After the QVTKFboItem is created, the createRenderer function is called automatically. We have overriden this method so that is returns an instance of our QVTKFboRenderer.

QQuickFramebufferObject::Renderer *QVTKFramebufferObjectItem::createRenderer() const
{
return new QVTKFramebufferObjectRenderer();
}

The QVTKFboRenderer is set and the first update is invoked at the end of its constructor, which unchains the first synchronize and first render, leaving everything ready to go.

As we mentioned before, the synchronize method is used to exchange data and commands between both threads. Here we also make sure to update the size of the renderer to match the one of the QVTKFboItem and copy mouse events that will be used to modify the camera position.

// Resize the render window to match the vtkFboItem

int *rendererSize = m_vtkRenderWindow->GetSize();
if (m_vtkFboItem->width() != rendererSize[0] || m_vtkFboItem->height() != rendererSize[1])
{
m_vtkRenderWindow->SetSize(m_vtkFboItem->width(), m_vtkFboItem->height());
}

// Copy mouse events
if (!m_vtkFboItem->getLastMouseLeftButton()->isAccepted())
{
*m_mouseLeftButton = *m_vtkFboItem->getLastMouseLeftButton();
m_vtkFboItem->getLastMouseLeftButton()->accept();
}

After the synchronization is finished both threads go their separate ways. The render thread calls the render method automatically, where mouse events are forwarded to the vtkGenericRenderWindowInteractor so that the camera is moved around accordingly. We also attempt to select a model if it was clicked, by using the vtkCellPicker. Finally, we iterate over the commands queue to execute any commands that were created by the QVTKFboItem.

The rest of the code is self-explanatory or explained with comments within the code. You can find it in the following open-source repository. The code was compiled using Qt 5.9.3 and VTK 8.1.1, tested both in Windows 10 and Ubuntu 16.04. For more detailed building instructions check the README file found in the repository.