かずきのBlog@hatena

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

C++ の既存アプリにちょっとずつ Windows 10 風味を付け加えてみる

C++ ネタです。久しく触ってない言語なのと、へましたらメモリリークやらなんやらしやすい言語なので、そういうまずいところ見たら教えてください!

ではやっていきます。

はじめに

先日、以下の記事を書きました。

blog.okazuki.jp

最初に触った言語が C/C++ なので普通の C++ で書けるというのだけで凄くいいなって思ったのでもう少しだけ触ってみました。 多くの人が C++ で Windows アプリを開発している場合には Windows 7 以降(場合によってはそれ以前も???)を対象にしていると思います。

そんな中で Windows 10 対応とかして Windows 7, 8.1 で動かなくなったんじゃぁ元も子もないということになります。 そうならないようにする方法を紹介しようと思います。

スタート地点

MFC アプリケーションで新規作成した、どうしてこうなったというくらい色んなものが入っている(と個人的に思ってる)アプリの雛形をスタート視点としたいと思います。

f:id:okazuki:20181018103843p:plain

とりあえず、ここのヘルプの下の「アプリ名について」というメニューを押したときに UWP として動くときは UWP の機能を呼び出して、そうじゃないときは既存の動作をそのままさせてみたいと思います。

Windows アプリケーション パッケージ プロジェクトでパッケージング

Windows 10 の API は、普通にダイレクトに呼べる奴もあるのですが appx 形式にパッケージングしておかないと呼べない奴もあります。今回はトーストの API を使おうかなと思ってるのですが、それも appx じゃないと動かないものなのでさくっと固めてしまいます。固めるのは簡単で、Windows アプリケーション パッケージ プロジェクトを作って、MFC のアプリを追加するだけです。

f:id:okazuki:20181018104541p:plain

実行した結果は変わり映えしないけど

f:id:okazuki:20181018104508p:plain

スタートメニューを見ると追加されてるのは UWP っぽい雰囲気になってます。右クリックからさくっとクリーンにアンインストールできます。

f:id:okazuki:20181018104641p:plain

Windows 10 の API を呼び出す DLL を作る

ということで、今回は UWP の API を呼び出す DLL を作って UWP のほうにはそれを同梱して、既存のほうには、それを同梱しないという感じで行ってみたいと思います。 C++ のダイナミック リンク ライブラリ (DLL) を作成します。私は UWPFeatures という名前で作りました。 このままビルドすると exe と同じ場所に dll が出力されるようになってるので UWPFeatures プロジェクトのプロパティの出力ディレクトリを以下の値に変更します(すべての構成/すべてのプラットフォームにしてね)

$(SolutionDir)UWPFeatures\$(PlatformTarget)\$(Configuration)\

つぎに、appx に UWPFeatures.dll が含まれるようにします。これはちょっとイレギュラーな方法なのですが以下のようにしてやりました。

  1. 一度ビルドする
  2. Windows アプリケーション パッケージ プロジェクトに既存の項目を追加で UWPFeature.dll をリンクとして追加する

f:id:okazuki:20181018105820p:plain

  1. Windows アプリケーション パッケージ プロジェクトを右クリックしてアンロード
  2. アンロードしたら右クリックして編集
  3. UWP Features.dll のタグを以下のように編集

編集前

<Content Include="..\UWPFeatures\x86\Debug\UWPFeatures.dll">
  <Link>UWPFeatures.dll</Link>
</Content>

編集後

<Content Include="$(SolutionDir)UWPFeatures\$(PlatformTarget)\$(Configuration)\UWPFeatures.dll">
  <Link>UWPFeatures.dll</Link>
</Content>
  1. プロジェクトを右クリックして再読込

これで、プロジェクトの構成に応じて最適な dll が appx に含まれるようになります。正しい手順かは知らないけど、今のところこれしか手が無いように思います。

次に、C++/WinRT を使う用に設定していきます。 UWPFeatures プロジェクトのプロパティで C/C++ -> 言語C++ 言語標準ISO C++17 Standard (/std:c++17) にします。 そして、準拠モードいいえにします。 そして、リンカー -> 入力追加の依存ファイルを編集して windowsapp.lib を追加します。

UWPFeatures プロジェクトの stdafx.h によく使うヘッダーファイルを追加しておきます。

#include "winrt/base.h"
#include "winrt/Windows.Foundation.h"

では UWPFeatures に適当にヘッダーファイルを追加しましょう。私は Notification.h という名前で追加しました。そして以下のように編集します。

#pragma once

#include "stdafx.h"

extern "C" __declspec(dllexport) void ShowNotification(const wchar_t* text);

続けて実装も。Notification.cpp を追加して以下のようにします。

#include "stdafx.h"
#include "Notification.h"

#include "winrt/Windows.UI.Notifications.h"
#include "winrt/Windows.Data.Xml.Dom.h"

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::UI::Notifications;


extern "C" void ShowNotification(const wchar_t* text)
{
    // 今から 2 時間有効なトースト通知を出す
    auto notificationManager = ToastNotificationManager::GetDefault();
    auto toastXml = ToastNotificationManager::GetTemplateContent(ToastTemplateType::ToastText01);
    auto textNode = toastXml.GetElementsByTagName(L"text").Item(0);
    textNode.AppendChild(toastXml.CreateTextNode(text));
    auto toast = ToastNotification(toastXml);
    toast.ExpirationTime(winrt::clock::now() + std::chrono::hours() * 2);
    notificationManager.CreateToastNotifier().Show(toast);
}

ビルドしてビルドエラーがないことを確認しましょう。

次に MFC アプリのほうに行きます。プロジェクト名App.cpp というファイルの中に CWinRTMFCAppApp::OnAppAbout というメソッドがあります。(あれ、今思ったけど C++ だとメンバー関数っていうんでしたっけという記憶がよみがえってきた)

これを以下のように書き換えます。

void CWinRTMFCAppApp::OnAppAbout()
{
    // DLL の有無で処理切り分け
    auto hmodule = LoadLibrary(L"UWPFeatures.dll");
    if (hmodule == NULL)
    {
        CAboutDlg aboutDlg;
        aboutDlg.DoModal();
        return;
    }

    // トーストを出す関数を取得
    auto showNotification = (void (*)(const wchar_t*))GetProcAddress(hmodule, "ShowNotification");
    if (showNotification == NULL)
    {
        // エラーがあったらエラーの情報を出す
        LPVOID lpMsgBuf;
        FormatMessage(
            FORMAT_MESSAGE_ALLOCATE_BUFFER |
            FORMAT_MESSAGE_FROM_SYSTEM |
            FORMAT_MESSAGE_IGNORE_INSERTS,
            NULL,
            GetLastError(),
            MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
            (LPTSTR)&lpMsgBuf,
            0,
            NULL
        );
        MessageBox(NULL, (LPCTSTR)lpMsgBuf, L"Error", MB_OK | MB_ICONINFORMATION);
        LocalFree(lpMsgBuf);
        return;
    }

    showNotification(L"This message is from MFC app");
    FreeLibrary(hmodule);
}

では実行してみましょう!MFC アプリをスタートアッププロジェクトにしてメニューを選択してみます。

f:id:okazuki:20181018111923p:plain

今まで通りですね!次は Windows アプリケーション パッケージ プロジェクトをスタートアッププロジェクトにして実行してみます。

f:id:okazuki:20181018112053p:plain

通知が出ましたね!

まとめ

こんな感じで作れば既存の処理のほうには、あまり手を入れずに UWP 機能の詰まった dll の LoadLibrary が成功したときだけ Windows 10 っぽくふるまうということも出来るかなと思います。Windows アプリケーション パッケージ プロジェクトで作るとインストール・アンインストールを繰り返してもレジストリを汚さないというメリットがあるので個人的には好きです。

ストアからも配れるし、ストアから配らない場合にもちゃんとした証明書で署名する or 信頼されたルート証明機関あたりに証明書突っ込めば(企業内だとポリシーとかで)インストール出来ます。

ソースコードは以下のリポジトリにあげています。

github.com

多分、C++/WinRT の VISX 入れて Windows 10 SDK も入れてないと動かないと思います。ヘッダーとかが SDK に入ってるみたいなので。

  • C++/WinRT の VSIX はここから。

marketplace.visualstudio.com

  • Windows 10 の SDK は Visual Studio Installer から
  • C++/WinRT のドキュメントはこちら。例によって機械翻訳なので英語で見た方がいいかも。日本語のほうは誰かプルリク投げた方がいいくらいの出来栄えに見える…。

docs.microsoft.com