3D-Visualisierung mit VTK und Avalonia UI
Bilder sagen bekanntlich mehr als tausend Worte und manchmal sind sie sogar unumgänglich, bspw. bei medizinischen Anwendungen und der Darstellung von CT-Bildern im dreidimensionalen Raum. Sowohl in der Forschung als auch in der Praxis hat sich dafür das „Visualization Toolkit“ (VTK) etabliert, eine leistungsstarke Open-Source-C++-Bibliothek für die 2D- und 3D-Visualisierung wissenschaftlicher Daten. [1] Außerhalb der Forschung muss diese Visualisierung jedoch auch in modernen grafischen Benutzeroberflächen (GUI) eingebettet sein. Avalonia UI ist eine solche moderne GUI-Bibliothek und noch dazu plattformübergreifend. [2]
Früher habe ich das Schlagwort „plattformübergreifend“ häufig abgetan, bis ich zwei Berufsjahre lang in einem Projekt mitgewirkt habe, in dem eine Tool-Landschaft für Windows UND Linux lauffähig gemacht werden sollte. Nun stellt sich die Frage, wie sich die Plattformunabhängigkeit von Avalonia UI mit den vielfältigen Möglichkeiten von VTK kombinieren lässt.
Die Einbettung von VTK in ein Avalonia-UI-Control
VTK und Avalonia UI miteinander zu kombinieren, stellt heutzutage kein Problem mehr da, denn dies funktioniert mithilfe von ActiViz [3] sehr einfach; ActiViz bietet die Möglichkeit, VTK-Ansichten als Avalonia-UI-Control einzubinden. Punkt. Zudem ist der Support schnell und freundlich, und durch die Zusammenarbeit mit anderen Produkten wie CMake (dem Quasi-Standard zur Beschreibung von C++-Bau-Umgebungen) [4] ist eine langfristige und verlässliche Weiterentwicklung gesichert.
Wie funktioniert die Verwendung im Detail? Schauen wir uns dazu ein kleines Beispiel, bestehend aus einer Avalonia-UI-Anwendung und einer einfachen VTK-Visualisierung, an. [5] Das gesamte GUI besteht ausschließlich aus dem Avalonia-UI-Control:
<Window x:Class="BlogVTKUndAvalonia.MainWindow" someNotImportantBoilerPlateCode
Title="VTK_Avalonia">
<AvaloniaControls:RenderWindowControl Name="VTKControl" />
</Window>
Wir haben eine Datenquelle (vtkSphereSource erzeugt ein Polygonnetz, das wie eine Kugel aussieht) und einen Mapper (vtkPolyDataMapper), der die geometrischen Daten für die Anzeige aufbereitet.
Optional könnten wir hier weitere Verarbeitungsschritte wie z. B. Filter, Transformationen etc. einfügen. Anschließend wird ein Actor (vtkActor) erzeugt, der die Mapper-Daten enthält und im Renderer (vtkRenderer) dargestellt wird. Der Renderer übernimmt die Szene, zeigt das Actor-Objekt an und übergibt es an das Avalonia-Control. [6]
public partial class MainWindow : Window
{
public MainWindow()
{
AvaloniaXamlLoader.Load(this);
var mainView = this.FindControl<RenderWindowControl>("VTKControl");
var renderWindow = mainView?.RenderWindow;
var renderer = vtkRenderer.New();
renderWindow?.AddRenderer(renderer);
var interactorStyle = vtkInteractorStyleTrackballCamera.New();
renderWindow?.GetInteractor().SetInteractorStyle(interactorStyle);
var src = vtkSphereSource.New();
var mapper = vtkPolyDataMapper.New();
mapper.SetInputConnection(src.GetOutputPort());
var actor = vtkActor.New();
actor.SetMapper(mapper);
renderer.AddActor(actor);
}
}
Für diejenigen die sich Avalonia-UI-Code und VTK-Pipeline nicht im Kopf selbst übersetzen können, hier das visuelle Ergebnis: 😉
Abbildung 1: Polygonnetz, das wie eine Kugel aussieht
Anmerkung: Komplette Plattformunabhängigkeit lässt sich in der realen Praxis meist etwas schwerer herstellen, als so mancher Anbieter gerne behauptet. Für unser Beispiel bedeutet dies, dass die Anwendung zusätzliche Informationen darüber benötigt, wie das Rendering unter speziellen Betriebssystemen benutzt werden muss:
AppBuilder.Configure<App>()
.UsePlatformDetect()
.WithInterFont()
.LogToTrace()
.With(new Win32PlatformOptions {
RenderingMode = new[] { Win32RenderingMode.Wgl }
});
Von der Einbettung zur Kombination mit wiederverwendbaren UserControls
Wirklich interessant wird die Interaktion zwischen VTK-Komponenten und Avalonia-UI-Controls, wenn man wiederverwendbare UserControls baut. Nutzen wir also unsere 3D-Kugel, fügen einen Slider ein und binden dessen Wertänderungen an eine Funktion:
.axaml
<UserControl x:Class="ReusableVTKControl" oneNotImportantBoilerPlateCode>
<Grid RowDefinitions="Auto,*">
<Slider Name="Slider"
Grid.Row="0"
Maximum="100"
Minimum="0"
Orientation="Horizontal"
ValueChanged="OnSliderValueChangedFromSlider" />
<AvaloniaControls:RenderWindowControl Name="VTKControl" Grid.Row="1" />
</Grid>
</UserControl>
.axaml.cs
private void OnSliceNumberChangedFromScene(double slicerPosition)
=> SliderPosition = slicerPosition;
An dem InteractorStyle, der mit unserem RenderWindow verbunden ist, lassen sich Benutzereingaben individuell anpassen, indem wir EventHandler registrieren (Bonus-Punkte, wenn man sie auch wieder abmeldet und Speicherlecks vermeidet):
style.MouseWheelBackwardEvt += (_, _) => { SliderPosition += 0.1; };
Die SliderPosition ist an eine selbst erstellte StyledProperty von Avalonia UI gebunden und wir können das OnPropertyChangedEvent überschreiben:
public static readonly StyledProperty<double> SliderPositionProperty =
AvaloniaProperty.Register<ReusableVTKControl, double>(
nameof(SliderPosition),
0.5);
public double SliderPosition
{
get => GetValue(SliderPositionProperty);
set => SetValue(SliderPositionProperty, value);
}
protected override void OnPropertyChanged
(AvaloniaPropertyChangedEventArgs change)
{
_renderer.SetBackground(0.0, SliderPosition, 0.0);
//Constructor: this.FindControl<Slider>("Slider")
_slider.Value = SliderPosition * 100;
_renderWindow.GetInteractor().Render();
base.OnPropertyChanged(change);
}
Nun haben wir ein UserControl, bei dem wir einen Slider oder das synchronisierte Mausrad bewegen können und sich die Hintergrundfarbe bei unserer 3D-Kugel verändert.
Beliebige Avalonia-UI-Controls (hier der Slider) können mit der VTK-Visualisierung (Mausrad drehen, über dem VTK-Fenster) einander bidirektional beeinflussen und damit komplexe in sich geschlossene UserControls implementieren. Mit weiteren StyledProperties und der change.Property innerhalb des OnPropertyChanged können diese Controls dann einfach von außen konfiguriert werden.
Am Ende könnte unsere Anwendung dreimal unser UserControl einbinden und jedes Mal die Farbänderung auswählen, was dann wie folgt aussähe:
<Grid ColumnDefinitions="*,*" RowDefinitions="*,*">
<AvaloniaControls:RenderWindowControl Name="VTKControl"
Grid.Row="0"
Grid.Column="0" />
<BlogVtkUndAvalonia:ReusableVTKControl Grid.Row="1"
Grid.Column="0"
ColorChange="Red" />
<BlogVtkUndAvalonia:ReusableVTKControl Grid.Row="0"
Grid.Column="1"
ColorChange="Green" />
<BlogVtkUndAvalonia:ReusableVTKControl Grid.Row="1"
Grid.Column="1"
ColorChange="Blue" />
</Grid>
Abbildung 2: Polygonnetz mit Farbänderungen
Datenfluss in einer sauberen Architektur
Komplexere Applikationen nutzen mit Avalonia-UI häufig die Model–View–ViewModel (MVVM) Architektur, in die sich unsere UserControls gut einpassen:
- Eine Domain-Logik berechnet im Modell einen Wert (z. B. die Farbe grün für Ergebnisse unter einem Schwellwert, die Farbe rot für einen Wert darüber), der an das ViewModel übergeben wird.
- Das ViewModell bleibt mittels Styled Properties up to date, zumal die Views über die Properties auch Änderungen ans ViewModel melden können, die dann Änderungen im Model auslösen.
Als nächsten Schritt könnte man die VTK-Szene als Teil des UserControls in einer eigenen Klasse kapseln. Das UserControl hätte lediglich das Wissen, dass es ein VTKRendererWindow beinhaltet, wüsste aber nicht mehr, was die VTK-Szene wie darstellt. Über Callbacks könnte die Szene komplett entkoppelt werden, was wichtig ist, denn eine VTK-Szene kann bspw. durch dutzende Aktoren beliebig komplex werden.
Der Vorteil liegt auf der Hand: dem Single-Response-Prinzip folgend, ist die VTK-Szene damit wirklich unabhängig; ein Austausch des Sliders bspw. gegen ein numerisches Eingabefeld, oder das Ändern der initialen Kamera-Ansicht, löst nur Änderungen an einer Komponente aus.
Abbildung 3: Datenfluss in einer sauberen Architektur
Fazit: 3D-Visualisierung mit VTK und Avalonia UI
Wie Sie anhand des kleinen Beispiels gesehen haben, harmonieren VTK und Avalonia UI gut miteinander. Das ist an sich wenig überraschend, denn die Macht aus etlichen Jahren C++ Bibliotheksentwicklung trifft auf moderne UI-Technik. In unserer gelebten Praxis ist dies von großem Vorteil, denn Kunden, die bspw. im medizinischen Kontext mit CT-Bildern arbeiten und axiale, sagittale und koronale Ansichten benötigen, profitieren stark davon. [7]
Hinweise:
[1] VTK: Das Visualization Toolkit
[2] Avalonia UI: The Future of .NET UI
[3] ActiViz: 3D Visualization Library for .NET C# and Unity
[4] CMake: Plattformübergreifendes Open-Source-Tool zur Automatisierung des Erstellungsprozesses von Software
[5] Hier finden Sie den gesamten Code der Beispielanwendung; Sie benötigen jedoch eine Lizenz von ActiViz.
[6] 3D-Visulaisierung ist ein eigenes Kompetenzfeld, für den tieferen Einstieg in das Thema empfiehlt sich das VTK Textbook.
[7] Wie eine solche Anwendung in der Praxis entsteht, zeigt die gemeinsame Erfolgsgeschichte von Dr. Sennewald Medizintechnik und t2informatik.
Hier finden Sie einen Beitrag über Avalonia UI und die Entwicklung einer Cross-Plattform WPF-Anwendung mit .NET Core.
Suchen Sie nach einem Team für Ihre Softwareentwicklung oder -modernisierung? Dann laden Sie sich den t2informatik Steckbrief herunter.
Wollen Sie als Meinungsmacherin oder Kommunikator über das Thema diskutieren? Dann teilen Sie den Beitrag gerne in Ihrem Netzwerk.

Jan Kasper
Jan Phillip Kasper ist Softwareentwickler. Er startete im Embedded Bereich mit C++ und wechselte später wegen des starken Toolings zu C#. Seine erste technische Liebe gilt den Unit Tests, im Systemtest spürt er mit leiser Schadenfreude skurrile Fehler auf. Und privat? Da baut er mit seinen Söhnen Technikspielzeug und lässt sie gelegentlich sogar selbst damit spielen.
Im t2informatik Blog veröffentlichen wir Beträge für Menschen in Organisationen. Für diese Menschen entwickeln und modernisieren wir Software. Pragmatisch. ✔️ Persönlich. ✔️ Professionell. ✔️ Ein Klick hier und Sie erfahren mehr.