かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

MFC に XAML Islands で UWP のコントロールを追加してみよう

MFC に UWP のコントロール置けるってさ。

やってみよう

MFC アプリを新規作成します。今回は XAMLIslandsMFCApp という名前で SDI アプリケーションでいってみたいと思います。残りはデフォの設定で作成!実行するとこんな感じです。 懐かしい。(Borland C++ Builder を昔はメインに使ってたけど、少しだけ MFC もかじってた)

f:id:okazuki:20191101152935p:plain

パッケージングしよう

とりあえず msix にパッケージングします。しなくてもできるのですが、なんか自分のところだとうまくいかなかったので今回は妥協という感じで。 Windows アプリケーション パッケージ プロジェクトをさくっと追加してアプリケーションノードに MFC アプリのプロジェクトを追加します。

現時点では Windows 10 1903 でしかサポートされてないので、プロジェクトを作るときは Target version と Minimum version できちんと 1903 を選びましょう。

f:id:okazuki:20191101153301p:plain

ソリューションエクスプローラーは以下のような感じになります。

f:id:okazuki:20191101153403p:plain

次に MFC アプリに C++/WinRT の NuGet パッケージを追加します。

  • Microsoft.Windows.CppWinRT

そして XAML Islands 用のパッケージも追加します。

  • Microsoft.Toolkit.Win32.UI.SDK (現時点では rc1 なのでプレビューパッケージにチェックを入れてインストールしてください)

pch.h に XAML Islands 関連や UWP 関連のヘッダーファイルの include を追加します。pragmaundef は、今のところこれを追加しないとコンパイルエラーになるバグの回避用です。将来的にはいらなくなるでしょう。ここか、各ファイルに使う機能の入ったヘッダーファイルの include を追加します。

// pch.h: This is a precompiled header file.
// Files listed below are compiled only once, improving build performance for future builds.
// This also affects IntelliSense performance, including code completion and many code browsing features.
// However, files listed here are ALL re-compiled if any one of them is updated between builds.
// Do not add files here that you will be updating frequently as this negates the performance advantage.

#ifndef PCH_H
#define PCH_H

// add headers that you want to pre-compile here
#include "framework.h"

#pragma push_macro("TRY")
#undef GetCurrentTime
#undef TRY

#include <winrt/base.h>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Foundation.Collections.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.Hosting.h>
#include <winrt/Windows.UI.Popups.h>
#include <windows.ui.xaml.hosting.desktopwindowxamlsource.h>

#pragma pop_macro("TRY")
#pragma pop_macro("GetCurrentTime") 


#endif //PCH_H

XAMLIslandsMFCApp.h を開いて CXAMLIslandsMFCAppApp (プロジェクトの名前付けミスった…!) クラスの private なメンバー変数に winrt::Windows::UI::Xaml::Hosting::WindowsXamlManager 型の変数を追加します。

class CXAMLIslandsMFCAppApp : public CWinAppEx
{
public:
    CXAMLIslandsMFCAppApp() noexcept;

private:
    winrt::Windows::UI::Xaml::Hosting::WindowsXamlManager _windowsXamlManager{ nullptr }; // add

// Overrides
public:
    virtual BOOL InitInstance();
    virtual int ExitInstance();

// Implementation
    UINT  m_nAppLook;
    BOOL  m_bHiColorIcons;

    virtual void PreLoadState();
    virtual void LoadCustomState();
    virtual void SaveCustomState();

    afx_msg void OnAppAbout();
    DECLARE_MESSAGE_MAP()
};

そして CXAMLIslandsMFCAppApp クラスの InitInstance メソッドの CWinAppEx::InitInstance メソッドの呼び出しの前あたりに XAML Islands の初期化処理を書きます。

BOOL CXAMLIslandsMFCAppApp::InitInstance()
{
    // InitCommonControlsEx() is required on Windows XP if an application
    // manifest specifies use of ComCtl32.dll version 6 or later to enable
    // visual styles.  Otherwise, any window creation will fail.
    INITCOMMONCONTROLSEX InitCtrls;
    InitCtrls.dwSize = sizeof(InitCtrls);
    // Set this to include all the common control classes you want to use
    // in your application.
    InitCtrls.dwICC = ICC_WIN95_CLASSES;
    InitCommonControlsEx(&InitCtrls);

    winrt::init_apartment(winrt::apartment_type::single_threaded); // add
    _windowsXamlManager = winrt::Windows::UI::Xaml::Hosting::WindowsXamlManager::InitializeForCurrentThread(); // add

    CWinAppEx::InitInstance();

        ... 省略 ...
}

これで下準備が出来ました。コントロールを追加していきます。

CXAMLIslandsMFCAppView クラスに XAML Islands をホストするウィンドウを管理してくれる winrt::Windows::UI::Xaml::Hosting::DesktopWindowXamlSource クラスのメンバー変数を作ります。

private:
    winrt::Windows::UI::Xaml::Hosting::DesktopWindowXamlSource _xamlIsland{ nullptr };

クラスウィザードを使って CXAMLIslandsMFCAppView クラスに WM_CREATEWM_CLOSEWM_SIZE のメッセージハンドラーを追加します。 OnCreate メソッドで DesktopWindowXamlSource のインスタンスを作ってコントロールを置いていきます。

int CXAMLIslandsMFCAppView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
    if (CView::OnCreate(lpCreateStruct) == -1)
        return -1;

    // create a DesktopWindowXamlSource instance
    _xamlIsland = winrt::Windows::UI::Xaml::Hosting::DesktopWindowXamlSource{};
    auto interop = _xamlIsland.as<IDesktopWindowXamlSourceNative>();
        // attach to current view
    interop->AttachToWindow(GetSafeHwnd());

    // create uwp controls
    winrt::Windows::UI::Xaml::Controls::TextBox textBox;
    winrt::Windows::UI::Xaml::Controls::Button button;
    button.Content(winrt::box_value(winrt::hstring(L"Click!!")));

    winrt::Windows::UI::Xaml::Controls::StackPanel panel;
    panel.Children().Append(textBox);
    panel.Children().Append(button);
    // set the uwp control instance to the DesktopWindowXamlSource instance
    _xamlIsland.Content(panel);

    return 0;
}

DesktopWindowXamlSource がウィンドウで、この上に UWP のコントロールが置けます。そして、このウィンドウに UWP のコントロールを置く感じです。

OnClose では後しますをします。

void CXAMLIslandsMFCAppView::OnClose()
{
    _xamlIsland.Close();
    _xamlIsland = nullptr;

    CView::OnClose();
}

OnSize ではViewのサイズに応じて DesktopWindowXamlSource のサイズを調整します。

void CXAMLIslandsMFCAppView::OnSize(UINT nType, int cx, int cy)
{
    CView::OnSize(nType, cx, cy);

    // fit to this view
    auto interop = _xamlIsland.as<IDesktopWindowXamlSourceNative>();
    HWND islandHwnd = NULL;
    winrt::check_hresult(interop->get_WindowHandle(&islandHwnd));
    RECT viewRect{};
    GetWindowRect(&viewRect);
    ::SetWindowPos(islandHwnd, NULL, 0, 0,  viewRect.right - viewRect.left, viewRect.bottom - viewRect.top, SWP_SHOWWINDOW);
    auto p = _xamlIsland.Content().as<winrt::Windows::UI::Xaml::Controls::StackPanel>();
    p.UpdateLayout();
}

実行してみましょう。UWP のコントロールが表示されます。

f:id:okazuki:20191101162901p:plain

イベントハンドラーも追加してみましょう。

イベント購読解除用の winrt::event_revoker とイベントハンドラー用のメンバー関数を CXAMLIslandsMFCAppView に追加します。

private:
    winrt::Windows::UI::Xaml::Controls::Button::Click_revoker _clickRevoker;
    winrt::Windows::Foundation::IAsyncAction OnButtonClick(winrt::Windows::Foundation::IInspectable const& sender, winrt::Windows::UI::Xaml::RoutedEventArgs const& e);

OnCreate でイベントの登録処理を追加して

// ボタンを作った直後くらいにこの処理を入れる
_clickRevoker = button.Click(winrt::auto_revoke, { this, &CXAMLIslandsMFCAppView::OnButtonClick });

OnClose には購読解除処理も入れておきます。

void CXAMLIslandsMFCAppView::OnClose()
{
    _clickRevoker.revoke();
    _xamlIsland.Close();
    _xamlIsland = nullptr;

    CView::OnClose();
}

そして、イベントハンドラーに適当に処理を追加します。今回は入力した内容をそのままメッセージボックスに出します。

winrt::Windows::Foundation::IAsyncAction CXAMLIslandsMFCAppView::OnButtonClick(winrt::Windows::Foundation::IInspectable const& sender, winrt::Windows::UI::Xaml::RoutedEventArgs const& e)
{
    auto input = _xamlIsland
        .Content()
        .as<winrt::Windows::UI::Xaml::Controls::StackPanel>()
        .Children()
        .GetAt(0)
        .as<winrt::Windows::UI::Xaml::Controls::TextBox>()
        .Text();
    winrt::Windows::UI::Popups::MessageDialog dialog{ input };
    co_await dialog.ShowAsync();
}

実行すると、こんな感じになります。

f:id:okazuki:20191101165618p:plain

まとめ

ここまでで見た目は出来た雰囲気です。 でもフォーカス制御やメッセージをディスパッチしてあげるなどの処理がまだ必要です…。つらたん…。詳細はここにあるのですがまだよくわからんのですよ。

github.com