C++/WinRTの始め方


概要

C++ にある程度慣れている人向けに C++/WinRT を紹介する。
開発環境を整え、簡単なプログラムを実行する。

C++/WinRT とは

  • Windows 10 のネイティブアプリを作るためのフレームワーク
  • C++/CX のような独自言語を用いない C++17 のライブラリとして表現される
    • WRL という Template Library もあったらしいが、 C++/WinRT に取って代わられた
  • 小さく、ランタイムが速いのがウリ

C++ で表現されたネイティブなライブラリってことは MFC みたいなもんね、と言ってしまえばそれまでだが、ユニバーサル Windows プラットフォーム (UWP) に対応しているのが大きく異なる。PPI (DPI) の異なる複数のディスプレイの間でウィンドウを移動してもきちんと追従する。

あとは MFC みたいなマクロによるおまじないが登場しないのはとても良い。人間が書くべきコードは大幅に減り、とても簡単に、そしてコードが見やすくなった。ただ、人間が書いていないというだけであって裏で自動生成されているコードはある。Qt や Unity の IL2CPP みたいなやり口だ。

開発環境の準備

はやる気持ちを抑えてまずは環境を整える。

Visual Studio のサポート、C++/WinRT、XAML では、VSIX 拡張機能と、NuGet パッケージ を読めば全て書いてあるが、タイトルからわかる通り翻訳がめちゃくちゃなので英語で読みたくない人のために最低限をここに記す。

以下を用意する:

  • Windows 10 (1803以降)
  • Visual Studio 2017 (可能な限り最新)
  • C++/WinRT VSIX

Windows 10

  1. スタートメニューから 設定更新とセキュリティ開発者向け から 開発者モード になっておくこと

Visual Studio 2017

  1. Visual Studio Installer を起動
  2. から Community, Professional, Enterprise のいずれかをインストールする
  3. ワークロードユニバーサル Windows プラットフォーム開発 を選択
    • 右の インストールの詳細 から C++ ユニバーサル Windows プラットフォーム ツール にチェックを入れる
  4. インストール

C++/WinRT VSIX

VSIX は新規プロジェクトを作成する際の雛形を提供する。すでに存在するプロジェクトには必要ない。

  1. Visual Studio 2017 を起動
  2. メニューの ツール機能拡張と更新プログラム...を開く
  3. 左からオンラインVisual Studio Marketplace を開き、右上の検索フィールドから C++/WinRT で検索して ダウンロード を実行
  4. Visual Studio 2017 を一度終了すると VSIX インストーラーが起動する

プロジェクトの新規作成

ここまで出来てようやくプロジェクトの作成だ。

  1. Visual Studio 2017 を起動
  2. メニューの ファイル新規作成プロジェクト...
  3. Visual C++Windows UniversalBlank App (C++/WinRT) を選択して OK
    • 環境によっては何故か Visual C++テストBlank App (C++/WinRT) に出現することもあった
  4. 最小バージョンWindows 10, version 1803 (10.0; ビルド 17134) にする

実行してみる

実行

メニューからビルドして実行してみよう。Click Me と書かれたボタンが出てきて、クリックするとボタンが Clicked に変化する。

レイアウト

この UI のレイアウトは MainPage.xaml に入っている。開いてみるとすごく縮小されたレイアウトが出てきて面食らうのだが、非常に大きなスクリーンで表示していることになっていることが原因。左上のプルダウンから 4" IoT Device など小さなスクリーンを選ぶとだいぶ見やすくなる。

下には XML で同じ内容が表示されており、こちらを編集しても即座に上の絵に反映される。

コントロールは ツールボックス ウィンドウに並んでおり、ここから新たなコントロールを配置することができる。Visual Basic や MFC 時代を知っていると好き勝手並べたくなるのだが、昨今携帯端末から大型ディスプレイまで様々な画素数、PPI のディスプレイがあるため基本的には自動レイアウトになっている。XML をみると分かるが Button の親にある StackPanel というやつがそれ。

イベントは Button に記述されている Click="ClickHandler" によって ClickHandler メソッドが呼ばれることになる。

MainPage.xaml
<Button x:Name="myButton" Click="ClickHandler">Click Me</Button>

実装

MainPage.xaml にぶら下がっている MainPage.cpp に実装がある。MyProperty は32ビットのプロパティを持たせているだけで何もしていないので、実質 MainPage コンストラクタと ClickHandler だけだ。

改造してみる

クリップボードの中身を監視して表示するプログラムに変えてみよう。

手順としては次の通り:

  1. レイアウトに TextBlock を追加する
  2. ClickHandler を削除する
  3. クリップボードのハンドラを追加する

レイアウトに TextBlock を追加する

MainPage.xamlStackPanel を次のように書き換える:

MainPage.xaml
<StackPanel>
    <TextBlock x:Name="clipboardText" Text="コピーするとここに表示される"/>
</StackPanel>

これによりボタンは消滅する。

ClickHandler を削除する

レイアウトからボタンを削除したため MainPage.hMainPage.cpp から ClickHandler を削除する。

クリップボードのハンドラを追加する

クリップボードは winrt::Windows:ApplicationModel::DataTransfer::Clipboard で操作できる。

ヘッダは名前空間ごとに存在するので、 DataTransfer 名前空間を取り込むために pch.h に次の1行を追加する:

pch.h
#include <winrt/Windows.ApplicationModel.DataTransfer.h>

MainPage.cppusing namespace が並ぶ位置に DataTransfer 名前空間を、ついでにこの後使う IAsyncAction のために Foundation 名前空間も追加する:

MainPage.cpp
using namespace winrt;
using namespace Windows::UI::Xaml;
using namespace Windows::ApplicationModel::DataTransfer; // 追加
using namespace Windows::Foundation; // 追加

MainPage.cppMainPage::MainPage() を次のように書き換える:

MainPage.cpp
MainPage::MainPage()
{
    InitializeComponent();

    // イベントハンドラの登録
    Clipboard::ContentChanged([this](IInspectable const, IInspectable const) -> IAsyncAction
    {
        DataPackageView content = Clipboard::GetContent();
        // クリップボードに入っているのはテキストか
        if (content.Contains(StandardDataFormats::Text())
        {
            // clipboardText に表示
            hstring text = co_await content.GetTextAsync();
            clipboardText().Text(text);
        }
    });
}

クリップボードからテキストを取得するためには GetTextAsync という非同期関数を呼び出すため、非同期待ちの co_await を使用している。co_await を呼び出すとこのラムダ関数はコルーチンになるため、戻り値の型が IAsyncAction になる。ラムダ関数の引数も参照にしたくなるが非同期になるため値渡しにする必要がある。

WinRT を使用すると IAsyncAction, IAsyncOperation, co_await があちこちで登場するようになる。ここはややこしいので C++/WinRT のドキュメントを読んで欲しいが、ボタンを押した後の処理で登場した void MainPage::ClickHandler() のようなやつも、中で非同期処理を行う場合は戻り値の型が IAsyncAction MainPage::ClickHandler() のように変化するのがポイント。

全体像

MainPage.xaml
<Page
    x:Class="BlankApp1.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:BlankApp1"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <StackPanel>
        <TextBlock x:Name="clipboardText" Text="コピーするとここに表示される"/>
    </StackPanel>
</Page>
pch.h
#pragma once
#include <windows.h>
#include <unknwn.h>
#include <restrictederrorinfo.h>
#include <hstring.h>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.ApplicationModel.Activation.h>
#include <winrt/Windows.UI.Xaml.h>
#include <winrt/Windows.UI.Xaml.Controls.h>
#include <winrt/Windows.UI.Xaml.Controls.Primitives.h>
#include <winrt/Windows.UI.Xaml.Data.h>
#include <winrt/Windows.UI.Xaml.Interop.h>
#include <winrt/Windows.UI.Xaml.Markup.h>
#include <winrt/Windows.UI.Xaml.Navigation.h>
#include <winrt/Windows.ApplicationModel.DataTransfer.h>
MainPage.cpp
#include "pch.h"
#include "MainPage.h"

using namespace winrt;
using namespace Windows::UI::Xaml;
using namespace Windows::ApplicationModel::DataTransfer;
using namespace Windows::Foundation;

namespace winrt::BlankApp1::implementation
{
    MainPage::MainPage()
    {
        InitializeComponent();

        // イベントハンドラの登録
        Clipboard::ContentChanged([this](IInspectable const, IInspectable const) -> IAsyncAction
        {
            DataPackageView content = Clipboard::GetContent();
            // クリップボードに入っているのはテキストか
            if (content.Contains(StandardDataFormats::Text()))
            {
                // clipboardText に表示
                hstring text = co_await content.GetTextAsync();
                clipboardText().Text(text);
            }
        });
    }

    int32_t MainPage::MyProperty()
    {
        throw hresult_not_implemented();
    }

    void MainPage::MyProperty(int32_t /* value */)
    {
        throw hresult_not_implemented();
    }
}

実行

ビルドして実行してみよう。ウィンドウには コピーするとここに表示される と表示されているはずだ。

この状態で、メモ帳などを使ってクリップボードにテキストをコピーすると、うまくいけば表示がコピーした文字列に変わる。

感想

Windows の C++ プログラミングに慣れていると、あまりの簡単さに驚く。プログラムも従来のものより非常に短く簡潔である。
しかし箱庭から出るにはどのようにしたらいいのかが分からない。Microsoft.Data.Sqlite のような Windows 名前空間以外のものはどのように使うのか。地道に COM で操作するのか。

長らく Windows プログラミングの座は C# に奪われていたが、これだけ簡単になれば多少は揺り戻しがあるかもしれない。いうても C++ そのものの分かりづらさ、例えば文法エラーが何を言っているのか分からないという問題があり、主力になることはないと思う、と言い訳した上で、いい時代になったと喜びたい。C++ の簡単なツールにちょっと GUI を付けたいなら C++/WinRT という選択肢ができた。

参照