Qt/.NET: .NET WPFアプリケーションでQMLを使用

本稿は「Qt/.NET — Using QML in a .NET WPF application」の抄訳です。
 

Qt/.NETは、C++と.NETの間の相互運用性を提供する提案されたツールキットであり、マネージアセンブリ用のQtベースのカスタムネイティブホストと、オブジェクトのライフサイクル管理、インスタンスメソッドの呼び出し、イベント通知などの高度な相互運用サービスを提供するネイティブからマネージアダプターモジュールが含まれています。

前の投稿では、Qt/.NETを使用して、マネージ型のQObjectベースのラッパークラスを作成する方法を示しました。これにより、.NETオブジェクトのプロパティにQObjectのプロパティとしてアクセスしたり、.NETイベントをQObjectのシグナルに変換したりすることが可能になります。また、Qtアプリケーションがマネージアセンブリ内のアセットとシームレスに統合できる方法を示し、C#バックエンドモジュール用のQMLユーザーインターフェースを提供するアプリケーションの例を紹介しました。

qtdotnet_02_010

[オブジェクトモデル] .NETフレームワークのPingオブジェクトをQObjectベースのラッパーを介して使用

 

この記事では、Qtと.NETの相互運用性に関する提案を続けて説明します。具体的には、C++でC#インターフェースを実装する方法や、Qtクラスを拡張する.NET型を定義する方法について述べます。最後に、Qt/.NETのその他の機能を使用して、QML UIを.NET WPFアプリケーションに埋め込む方法の例を紹介します。

C# インターフェースの C++ での実装

C#におけるインターフェースとは、メンバ関数の名前とシグネチャで構成される設計契約の仕様です。 特定のインターフェースの実装として宣言された型は、そのインターフェースの各メンバに対して、外部からアクセス可能な実装を提供することが求められます。 このように、C#のインターフェースは、抽象型の使用をその実装から切り離すための標準的なメカニズムを提供します。

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

[C# コード]C#インターフェース定義の例

Qt/.NETでは、C#インターフェースをC++クラスで実装することができ、インターフェースと実装の分離をネイティブな相互運用性のコンテキストにまで拡張することができます。C#インターフェースのネイティブ実装クラスは、(1) QDotNetInterfaceを継承し、(2) 対象のインターフェースの完全修飾名を指定し、(3) インターフェースのすべてのメンバの実装を提供する必要があります。インターフェースのメンバの実装は、コールバックとして登録し、適切な名前とシグネチャに関連付ける必要があります。

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

[C++ コード] C#インターフェースの実装

ネイティブオブジェクトカプセル化

QDotNetInterface を拡張することで、C++ オブジェクトを C# インターフェイスの実装として .NET からアクセス可能にすることができます。 Qt/.NET アダプタは、ネイティブ実装のプロキシとして機能するマネージドオブジェクトを提供することで、これを実現します。 このプロキシは QDotNetInterface コンストラクタによって作成され、インターフェイスメンバーの実装として提供されたコールバックのリストを含んでいます。 マネージドコードの観点では、インターフェイスを実装し、そのメンバーが他の .NET オブジェクトから呼び出されるのはこのプロキシです。

インターフェースプロキシをネイティブコードへのコールバックの集合として定義することは、QDotNetInterfaceを拡張するクラスが実際のインターフェース実装を提供するものである場合にはうまく機能します。しかし、既存のネイティブ型 (例えば、Qt API) を.NETコードに公開することが目的である場合には、もはや十分ではありません。この場合、インターフェース実装のベースクラスとしてQDotNetNativeInterface<T>を使用する必要があります。この汎用クラスは、実際に公開される型 T のインスタンスへのポインタをカプセル化します。

例えば、QModelIndex クラスをマネージコードに公開し、このクラスのインスタンスを C++ での操作と同じ方法で C# でも操作できるようにすることを目的とするとします。 最初のステップは、QModelIndex の C# インターフェースを定義することです。

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

[C# コード] QModelIndexインターフェース定義 (抜粋)

次に、C#インターフェースのネイティブ実装を提供する必要があります。この場合、QDotNetNativeInterface<QModelIndex>を拡張するC++クラスとなります。このベースクラスのコンストラクタには、インターフェースの完全修飾名、カプセル化するQModelIndexのインスタンスへのポインタ、およびブール値を引数として渡します。この最後の値は、プロキシオブジェクトのファイナライザがネイティブポインタの破棄をどのように処理するかを決定します。値が true の場合、ポインタはコールバックによって破棄され、ターゲットタイプ T (この場合は QModelIndex) のデストラクタが呼び出されます。値が false の場合、プロキシのファイナライザ実行中に破棄処理は行われません。

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++コード] QModelIndexのC#インターフェースの実装(抜粋)。

これで、ターゲット型 T のインスタンスをインターフェースの実装にカプセル化し、マネージコードに公開することができます。ネイティブ実装のコンストラクタは、マネージドプロキシの作成をトリガーします。マネージドプロキシには、実装コールバックのリストと、カプセル化されたネイティブオブジェクトへのポインタが含まれています。インターフェースのメンバーがプロキシ上で呼び出されると、今度はネイティブ実装上の対応するコールバックが呼び出され、ネイティブポインタが引数として渡されます。コールバックはポインタを適切なターゲット型Tにキャストし、カプセル化されたオブジェクト上の適切な関数を呼び出します。

qtdotnet_02_021[オブジェクトモデル] C#コードに公開されたQModelIndexオブジェクト

.NETでQtクラスを拡張

特定のシナリオで Qt API を使用するには、呼び出しコードで抽象 Qt クラスに拡張機能を提供する必要があります。これは、たとえば、モデル/ビューデザインパターンに従うクラスで必要となります。Qt ビュークラスを使用するアプリケーションでは、QAbstractItemModel などの抽象モデルクラスの実装、または QAbstractListModel などのその特殊化の 1 つを提供する必要があります。.NETアプリケーションの場合、これはこれらのQt C++クラスのいずれかを拡張するC#クラスを持つことを意味します。Qt/.NETでは、QObjectベースのラッパーとC#インターフェースの実装を組み合わせることで、これを実現しています。

例えば、Qtビューを使用して.NETアプリケーション内のアイテムのリストを表示したいとします。これを行うには、QAbstractListModelクラスを拡張する新しいモデルを定義する必要があります。この新しいモデルは、QAbstractListModelのC++メンバー関数の一部をオーバーライドする独自のC#メソッドを提供します。ただし、必要に応じてベースの実装を呼び出すことも必要になります。この目的のために、ベースの実装を表すC#インターフェースIQAbstractListModelを定義し、C#モデルクラスでオーバーライド可能な抽象クラスと併せて使用します。簡潔にするために、QtクラスQModelIndexQVariantは、IQModelIndexIQVariantインターフェースの実装としてC#で既に利用可能であると仮定します。

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#コード] QAbstractListModel をオーバーライドするためのC#基本定義

C#のインターフェースおよび抽象クラスの定義に対するネイティブな対応は、Qt APIのQAbstractListModelを拡張する新しいC++クラスであり、IQAbstractListModel C#インターフェースのネイティブ実装と、C#モデルのネイティブラッパーの両方として機能します。そのため、この新しい C++ クラスは QDotNetInterface QDotNetObject の両方を拡張する必要があります。 繰り返しになりますが、簡潔にするために、IQModelIndex および IQVariant の C# インターフェースの実装は、すでに同名の C++ クラスによって提供されているものとします。

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++コード] C#でQAbstractListModelをオーバーライド用ネイティブ定義

 

必要な基本定義がすべて整ったので、マネージドとネイティブの両方で、QAbstractListModelを拡張し、アプリケーションに実際のモデルを提供するC#クラスを定義することができます。

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#コード] QAbstractListModelを継承するC#クラスの例

まとめると、新しいモデルは以下の3つの要素で構成されます。

  1. オーバーライドオブジェクト(C#、上記の例ではFooListModelのインスタンス)
  2. ベースインターフェースプロキシ(C#、Proxy_IQAbstractListModel)
  3. ネイティブラッパーおよびベースインターフェース実装(C++、QDotNetAbstractListModel)

これら3つのオブジェクトは同期されたライフサイクルを持ち、ネイティブおよびマネージコードの両方の観点から、ネイティブのQAbstractListModelクラスを拡張するという概念的な目標を達成するために連携して動作します。3つのオブジェクトは、親オブジェクトのコンストラクタ (例では示されていません) によって同時に作成されます。C#オブジェクトは親オブジェクトとしてモデルを「参照」し、C++オブジェクトはネイティブラッパーとして参照します。ガベージコレクションと最終化の後、適切なデストラクタを介して、ネイティブ実装/ラッパーを含むすべてのオブジェクトが適切に破棄されます。

qtdotnet_02_052[オブジェクトモデル] C#クラスで拡張されたQAbstractListModel。

ツールについて一言

上述の通り、ネイティブラッパーやインターフェースの実装に必要なコードの多くは、自動生成可能な定型コードです。Qt/.NETプロジェクトでは、このようなネイティブ定型コード用のコード生成ツールの開発が進められています。これはまだ進行中の作業であり、今後の記事でさらに詳しく説明します。

WPF アプリケーションで QML の使用

これまで説明してきたすべての相互運用機能を使用して、Qt/.NET を使用して WPF アプリケーションに QML ユーザーインターフェイスを追加する方法を説明します。 QML UI は、3D アニメーションを表示する View3D で構成されます。 重ね合わせられた ListView には、カメラの配置に関連する技術的プロパティのリストが表示されます。このリストの内容は C# バックエンドで管理され、C# で実装されたリストモデルを通じて QML UI に提供されます。メインウィンドウの WPF 仕様には、以下の要素が含まれます。

  • QML UI のプレースホルダー
  • カメラの位置を操作するスライダー
  • フレームレート表示 (100% が 60 fps に相当するプログレスバー)

qtdotnet_02_013

[モックアップ] WPF + QML デモアプリケーションのデザイン

スライダーはC#バックエンドのプロパティ(CameraPositionXCameraRotationYなど) にバインドされており、スライダーが移動するとPropertyChangedイベントが発行されます。このイベントはプロパティ通知信号に変換され、View3D上のバインドされたプロパティが適切に更新されます。

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プロパティ(抜粋)は、バックエンド(mainWindow)のプロパティにバインド

C#のバックエンドには、QAbstractListModelを拡張するCameraモデルクラスのインスタンスが含まれます。カメラコントロールのスライダーを変更すると、Cameraモデルの更新がトリガーされ、その結果、ListViewの内容が更新されます。

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# コード] Camera モデル

WPFウィンドウにQQuickViewの埋め込み

WPF ウィンドウ内に QML UI を表示するには、アプリケーションのメインウィンドウの XAML 指定で WindowsFormsHost要素を使用します。 この要素により、WPF に Windows Forms コントロールを埋め込むことができます。 このコントロールは、WPF コントロールとは異なり、HWND ハンドル経由でアクセスできます。 静的関数 fromWinId() を呼び出し、引数として埋め込みコントロールのハンドルを渡すことで、QWindowを取得できます。埋め込み QWindow のコンテンツとして、QQmlEngineと共に QML コンテンツのレンダリングを担当する QQuickViewを使用します。最後に、WindowsFormsHost 要素の使用は、このデモアプリケーションの目的のための一時的な回避策にすぎません。正式リリース時には、Qt/.NET には独自の QWindow ベースの WPF 要素が含まれます。

<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 マークアップ] WPF メインウィンドウ(抜粋)のホスト要素に 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++コード] WPFホスト要素内にQQuickViewの表示

 

デモアプリケーションのソースコードは、Qt/.NETリポジトリに提出された最新のパッチで入手できます。

qtdotnet_02_app

[アプリのスクリーンショット] 実行中のWPF + QML デモ


Blog Topics:

Comments