Qt/.NET — Using QML in a .NET WPF application

Qt/.NET is a proposed toolkit for interoperability between C++ and .NET, including a Qt-based custom native host for managed assemblies, as well as a native-to-managed adapter module that provides higher-level interoperability services such as object life-cycle management, instance method invocation and event notification.

In a previous post, we demonstrated how Qt/.NET can be used to create QObject-based wrapper classes for managed types, including the possibility to access properties of .NET objects as QObject properties, and also to convert .NET events to QObject signals. We showed how a Qt application can seamlessly integrate with assets in managed assemblies, and presented an example of an application that provides a QML user interface for a C# backend module.

qtdotnet_02_010

[Object Model] Using a Ping object from the .NET framework through a QObject-based wrapper.

In this post we will continue describing our proposal for Qt and .NET interoperability, including how to implement C# interfaces in C++ and how to define .NET types that extend Qt classes. We will conclude with an example of how to use these and other features of Qt/.NET to embed a QML UI into a .NET WPF application.

Implementing C# interfaces in C++

An interface in C# is a specification of a design contract composed of names and signatures of member functions. Types that are declared as implementations of a given interface are required to provide a publicly accessible implementation for each of the members of that interface. C# interfaces thus provide a standard mechanism for decoupling the use of abstract types from their implementations.

public interface IStringTransformation
{
    string Transform(string s);
}

[C# Code] Example of C# interface definition.

Qt/.NET allows C# interfaces to be implemented by C++ classes, further extending the decoupling of interface and implementation to the context of native interoperability. A native implementation class for a C# interface must: (1) extend QDotNetInterface, (2) specify the fully qualified name of the target interface, and (3) provide implementations for all the interface members. The implementations of interface members must be registered as callbacks and associated to the corresponding names and signatures.

struct ToUpper : public QDotNetInterface
{
    ToUpper() : QDotNetInterface("FooLib.IStringTransformation, FooLib")
    {
        setCallback<QString, QString>("Transform",
            [](void *, const QString &bar) { return bar.toUpper(); });
    }
};

[C++ Code] Implementing a C# interface.

Native object encapsulation

By extending QDotNetInterface, C++ objects can thus become accessible to .NET as implementations of C# interfaces. The Qt/.NET adapter achieves this by providing a managed object that will serve as a proxy of the native implementation. This proxy is created by the QDotNetInterface constructor, and contains the list of callbacks that are provided as interface member implementations. From the perspective of managed code, it's the proxy that implements the interface and whose members are invoked by other .NET objects.

The definition of the interface proxy as a collection of callbacks to native code works well in the case where the class extending QDotNetInterface is also the one providing the actual interface implementation. But  it is no longer adequate if the goal is to expose an existing native type (e.g. from the Qt API) to .NET code. In that case, the QDotNetNativeInterface<T> must be used instead as the base class for interface implementations. This generic class encapsulates a pointer to an instance of some type T, which is the one actually being exposed.

As an example, let's assume the goal is to expose the QModelIndex class to managed code, such that instances of that class can be manipulated in C# in the same way as they are in C++. The first step would be to define a C# interface for QModelIndex.

public interface IQModelIndex
{
    bool IsValid();
    int Row();
    int Column();
}

[C# Code] Interface definition for QModelIndex (excerpt).

We would then need to provide a native implementation of the C# interface. In this case, that would be a C++ class extending QDotNetNativeInterface<QModelIndex>. The constructor for this base class, apart from the fully qualified name of the interface, will take as arguments a pointer to the instance of QModelIndex to encapsulate, as well as a bool value. This last value will determine how the finalizer for the proxy object handles disposal of the native pointer: if the value is true, the pointer will be disposed by a callback that invokes the destructor for the target type T – in this case, QModelIndex; if the value is false, no disposal action will be taken during the proxy's finalizer.

struct IQModelIndex : public QDotNetNativeInterface<QModelIndex>
{
    IQModelIndex(const QModelIndex &idx) : QDotNetNativeInterface<QModelIndex>(
    	"Qt.DotNet.IQModelIndex, Qt.DotNet.Adapter", new QModelIndex(idx), true)
    {
        setCallback<bool>("IsValid", [this](void *data)
            { return reinterpret_cast<QModelIndex *>(data)->isValid(); });
        setCallback<int>("Column", [this](void *data)
            { return reinterpret_cast<QModelIndex *>(data)->column(); });
        setCallback<int>("Row", [this](void *data)
            { return reinterpret_cast<QModelIndex *>(data)->row(); });
    }
};

[C++ Code] Implementing the C# interface for QModelIndex (excerpt).

An instance of the target type T can now be encapsulated in the the interface implementation and exposed to managed code. The constructor for the native implementation will trigger the creation of the managed proxy, which will contain the list of implementation callbacks, as well as a pointer to the encapsulated native object. When an interface member is invoked on the proxy it will in turn invoke the associated callback on the native implementation, passing as argument the native pointer. The callback will cast the pointer to the appropriate target type T and call the corresponding function on the encapsulated object.

qtdotnet_02_021[Object Model] QModelIndex object exposed to C# code.

Extending Qt classes in .NET

Using the Qt API, in certain scenarios, requires that calling code provide extensions to abstract Qt classes. This is the case,  for example, with classes that adhere to the model/view design pattern. An application that uses a Qt view class will need to provide an implementation of an abstract model class, such as QAbstractItemModel or one of its specializations, like QAbstractListModel. In the case of a .NET application, that means having a C# class that extends one of these Qt C++ classes. Qt/.NET makes this possible by combining a QObject-based wrapper with the implementation of a C# interface.

As an example, let's suppose that we want to use a Qt view to display a list of items in a .NET application. To that end, we will need to define a new model that extends the QAbstractListModel class. This new model will provide its own C# methods to override some of the C++ member functions of QAbstractListModel. However, it will also need to invoke the base implementations when needed. For that purpose, we will define an IQAbstractListModel C# interface to represent the base implementation, alongside an abstract class that can be overridden by the C# model class. For brevity's sake, we'll assume that the QModelIndex and QVariant Qt classes are already accessible in C# as implementations of the IQModelIndex and IQVariant interfaces. 

public interface IQAbstractListModel
{
    int Flags(IQModelIndex index);
    IQModelIndex CreateIndex(int arow, int acolumn, IntPtr adata);
    void EmitDataChanged(IQModelIndex topLeft, IQModelIndex bottomRight, int[] roles);
}

public abstract class QAbstractListModel
{
    public IQAbstractListModel Base { get; protected set; }
    public virtual int Flags(IQModelIndex index) => Base.Flags(index);
    public abstract int RowCount(IQModelIndex parent);
    public abstract IQVariant Data(IQModelIndex index, int role);
}

[C# Code] Base definitions to override QAbstractListModel in C#.

The native counterpart to the C# definitions of interface and abstract class is a new C++ class that extends QAbstractListModel from the Qt API, and that will function as both native implementation for the IQAbstractListModel C# interface as well as native wrapper for C# models. As such, the new C++ class must also extend both QDotNetInterface and QDotNetObject. Again for the sake of brevity we will assume that implementations for the IQModelIndex and IQVariant C# interfaces are already provided by C++ classes with the same name.

class QDotNetAbstractListModel
    : public QDotNetObject
    , public QDotNetInterface
    , public QAbstractListModel
{
public:
    QDotNetAbstractListModel()
    {
        setCallback<int, IQModelIndex>("Flags",
            [this](void *, IQModelIndex index)
            {
            	return QAbstractListModel::flags(index);
            });
        setCallback<IQModelIndex, int, int, void *>("CreateIndex",
            [this](void *, int row, int col, void *ptr)
            {
                return IQModelIndex(QAbstractListModel::createIndex(row, col, ptr));
            });
        setCallback<void, IQModelIndex, IQModelIndex, QDotNetArray<int>>("EmitDataChanged",
            [this](void *, IQModelIndex idx0, IQModelIndex idx1, QDotNetArray<int> roles)
            {
                emit QAbstractListModel::dataChanged(idx0, idx1, roles);
            }
    }
    Qt::ItemFlags flags(const QModelIndex &index) const override
    {
        return Qt::ItemFlags::fromInt(method("Flags", fnFlags).invoke(*this, index));
    }
    int rowCount(const QModelIndex &parent = QModelIndex()) const override
    {
        return method("RowCount", fnRowCount).invoke(*this, parent);
    }
    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override
    {
        return method("Data", fnData).invoke(*this, index, role);
    }
private:
    mutable QDotNetFunction<int, IQModelIndex> fnFlags = nullptr;
    mutable QDotNetFunction<int, IQModelIndex> fnRowCount = nullptr;
    mutable QDotNetFunction<IQVariant, IQModelIndex, int> fnData = nullptr;
};

[C++ Code] Native definitions required to override QAbstractListModel in C#.

With all the necessary base definitions in place, both managed and native, we can now define the C# class that extends QAbstractListModel and provides the actual model for the application.

public class FooListModel : QAbstractListModel
{
    private string[] Values = ["Foo", "Bar", "Foobar"];
    public override int RowCount(IQModelIndex parent = null)
    {
        if (parent?.IsValid() == true)
            return 0;
        return Values.Length;
    }
    public override int Flags(IQModelIndex index = null)
    {
    	if (index == null)
            return Base.Flags(index);
    	int row = index.Row();
        if (row < 0 || row >= Values.Length)
            return Base.Flags(index);
        return (int)(ItemFlag.ItemIsEnabled | ItemFlag.ItemNeverHasChildren);
    }
    public override IQVariant Data(IQModelIndex index, int role = 0)
    {
    	if (index == null)
            return null;
    	int row = index.Row();
        if (row < 0 || row >= Values.Length)
            return null;
        if ((ItemDataRole)role != ItemDataRole.DisplayRole)
            return null;
        return Values[row];
    }
    public void SetFoo(string foo)
    {
    	Values[0] = foo;
        Values[2] = foo + Values[1].ToLower();
        var idx0 = Base.CreateIndex(0, 0, IntPtr.Zero);
        var idx1 = Base.CreateIndex(2, 0, IntPtr.Zero);
        Base.EmitDataChanged(idx0, idx1, [(int)ItemDataRole.DisplayRole]);
    }
}

[C# Code] Example C# class that extends QAbstractListModel.

In summary, the new model will be composed by the following triple:

  1. Overriding object (C#, in the example above, an instance of FooListModel);
  2. Base interface proxy (C#, Proxy_IQAbstractListModel);
  3. Native wrapper and base interface implementation (C++, QDotNetAbstractListModel).

These three objects will have a synchronized life-cycle, and will cooperate to achieve the conceptual goal of extending the native Qt class QAbstractListModel, in the perspective of both native and managed code. The three objects will be created at the same time, triggered by the constructor of the overriding object (not shown in the example code). C# objects will "see" the model as the overriding object, whereas C++ objects will "see" it as the native wrapper. After garbage collection and finalization, all objects will be adequately disposed, including the native implementation/wrapper through the appropriate destructor.

qtdotnet_02_052[Object Model] QAbstractListModel extended by C# class.

A word about tooling

Much of the code required to set up native wrappers and interface implementations, as described above, is boilerplate code that can be automatically generated. The on-going work on the Qt/.NET project includes the development of code-generation tools for such boilerplate native code. This topic is still a work in progress, and we'll discuss it in further detail in subsequent posts.

Using QML in a WPF application

Making use of all the interop features discussed so far, we'll now demonstrate how to use Qt/.NET to add a QML user interface to a WPF application. The QML UI will consist of a View3D that will display a 3D animation. An overlaid ListView will show a list of technical properties related to camera placement. The contents of this list is managed by the C# backend, and provided to the QML UI through a list model implemented in C#. The WPF specification of the main window will contain the following elements:

  • Placeholder for the QML UI;
  • Group of slider controls to manipulate camera position;
  • Frame rate indicator (progress bar with 100% corresponding to 60 fps).

qtdotnet_02_013

[Mockup] WPF + QML demo application design.

The sliders will be bound to properties in the C# backend (e.g. CameraPositionX, CameraRotationY, etc.), and will trigger PropertyChanged events when moved. These events will be converted to property notification signals that will allow the bound properties on the View3D to be updated accordingly.

View3D {
    id: view
    anchors.fill: parent
    PerspectiveCamera {
        position: Qt.vector3d(
            mainWindow.cameraPositionX,
            mainWindow.cameraPositionY + 200,
            mainWindow.cameraPositionZ + 300)
        eulerRotation.x: (mainWindow.cameraRotationX - 30) % 360
        eulerRotation.y: mainWindow.cameraRotationY
        eulerRotation.z: mainWindow.cameraRotationZ
    }
}

[QML UI] View3D properties (excerpt) bound to backend (mainWindow) properties.

The C# backend will include an instance of a Camera model class that extends QAbstractListModel. Changes to the camera control sliders will also trigger updates to the Camera model, which will in turn result in an update to the ListView contents.

public class Camera : QAbstractListModel
{
    private static string[] Names = ["Truck", "Pedestal", "Zoom", "Tilt", "Pan", "Roll"];
    private double[] Values { get; set; } = new double[Names.Length];
    private IQModelIndex[] Index { get; } = new IQModelIndex[Names.Length];

    private IQModelIndex IndexOf(Settings setting)
    {
        if (Index[(int)setting] is not { } idx)
            idx = Index[(int)setting] = Base.CreateIndex((int)setting, 0, 0);
        return idx;
    }

    public double this[Settings setting]
    {
        get { return Values[(int)setting]; }
        set
        {
            Values[(int)setting] = value;
            var idx = IndexOf(setting);
            Base.EmitDataChanged(idx, idx, [(int)ItemDataRole.DisplayRole]);
        }
    }

    public override int RowCount(IQModelIndex parent = null)
    {
        if (parent?.IsValid() == true)
            return 0;
        return Values.Length;
    }

    public override int Flags(IQModelIndex index = null)
    {
        return (int)(ItemFlag.ItemIsEnabled | ItemFlag.ItemNeverHasChildren);
    }

    public override IQVariant Data(IQModelIndex index, int role = 0)
    {
        if ((ItemDataRole)role != ItemDataRole.DisplayRole)
            return null;
        var row = index.Row();
        if (row is < 0 or > 5)
            return null;
        return $@"{Names[row]}: {Values[row]:0.00}";
    }
}

[C# Code] Camera model.

Embedding a QQuickView in a WPF window

To show the QML UI inside of the WPF window we will use a WindowsFormsHost element in the XAML specification for the app's main window. This element allows embedding a Windows Forms control in WPF, which, unlike WPF controls, can be accessed through a HWND handle. We can then obtain a QWindow by calling the fromWinId() static function, passing as argument the handle to the embedded control. As the content of the embedded QWindow, we will use a QQuickView that, together with a QQmlEngine, will be responsible for rendering the QML contents. As a final note: the use of the WindowsFormsHost element is only a makeshift solution, exclusively for the purposes of this demo app. When officially released, Qt/.NET will include its own QWindow-based WPF element.

<Window x:Class="WpfApp.MainWindow" Title="WPF + QML Embedded Window"
  ...
  xmlns:wf="clr-namespace:System.Windows.Forms;assembly=System.Windows.Forms">
  <Grid>
    ...
    <WindowsFormsHost Name="EmbeddedAppHost" Grid.Column="1">
      <wf:Panel Name="HwndHost" BackColor="#AAAAAA" />
    </WindowsFormsHost>
  </Grid>
</Window>

[XAML Markup] WPF main window (excerpt) with host element for the QML UI.

void EmbeddedWindow::show()
{
    embeddedWindow = QWindow::fromWinId((WId)mainWindow->hwndHost.handle());
    quickView = new QQuickView(qmlEngine, embeddedWindow);
    quickView->setSource(QUrl(QStringLiteral("qrc:/main.qml")));
    quickView->show();
}

[C++ Code] Showing a QQuickView inside of the WPF host element.

The full source code of the demo application is available in the latest patch submitted to the Qt/.NET repository.

qtdotnet_02_app

[App Screenshot] WPF + QML demo running.


Blog Topics:

Comments