WPF
Win32API

Win32でのWPFコンテンツのホスト(MaterialDesignToolkitの利用)

ごく小さい検討用アプリを書く場合に、私はWin32(C/C++)を利用しているのだけど、
見栄えが古臭いのは気になってしまうところ。。。

ということで、Win32アプリをベースにして、ボタンくらいはかっこよく見せようとがんばってみました。
開発環境はVisualStudio2017を使っています。
いつもは、自分用に用意しているテンプレートプロジェクトをベースにコードを書き足して作業しているので、
一からプロジェクトを作成する場合とはちょっと手順/設定が違うかもしれませんがご容赦ください。

コード置き場:
https://github.com/tanakah-sw/MyWPFTest

1: WPFのプロジェクト作成

VS2017のメニューから、C# WPFアプリを選択してプロジェクトを作成します。
普段、C#なんて使わないので、ちょっとドキドキしますねw
手順が間違っていたらご指摘ください。

MaterialDesignToolkitの導入

https://github.com/ButchersBoy/MaterialDesignInXamlToolkit
これをみたのが、そもそもの動機です。
NuGetから以下のパッケージを探してプロジェクトに追加します。
nuget.png

クラスライブラリ/COMの設定

出力の種類をクラスライブラリに設定し、さらにCOM参照可能に設定します。COM相互運用機能の登録にもチェックをつけます。
COMにするのは、WPF/C#のメンバー関数をWin32/C++から呼び出せるようにするためです。
vs1.png
v4.png
v3.png

2:C#/Xamlコーディング

画面デザイン

xamlの先頭に以下のコードを書き足して、MaterialDesignToolkitを使用するようにします。

<UserControl
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"

    x:Class="WPFPage.TestPanel"
    TextElement.Foreground="{DynamicResource MaterialDesignBody}"
    TextElement.FontWeight="Regular"
    TextElement.FontSize="11"
    TextOptions.TextFormattingMode="Ideal"
    TextOptions.TextRenderingMode="Auto"
    Background="{DynamicResource MaterialDesignPaper}"
    FontFamily="{DynamicResource MaterialDesignFont}" d:DesignHeight="320" Width="640">

するとMaterialDesignToolkitの部品が追加され、コモンWPFコントロールのうちのいくつかが置き換わります。
ボタンとか、チェックボックスとか。
v5.png

部品を追加したら、適当にプロパティを設定して完成させます。

<CheckBox Content="チェックテスト" HorizontalAlignment="Left" Height="40" Margin="26,89,0,0" VerticalAlignment="Top" Width="150" FontFamily="Meiryo UI" FontSize="16" Click="ClickCheck" IsChecked="{Binding CheckTest, Mode=OneWay}"/>
<Button Content="ボタンテスト" HorizontalAlignment="Left" Height="40" Margin="40,35,0,0" VerticalAlignment="Top" Width="105" FontFamily="Meiryo UI" FontSize="16" FontWeight="Normal" IsEnabled="{Binding CheckTest}" Click="ClickButton"/>

余談ですが、VisualStudioのエディタが非常に使いづらくてイラつきますね。画面デザイン側は思い通りの位置に部品配置できず、テキスト側は勝手にカーソルが動いたり、ちょっとマウスクリック位置がずれただけで横スクロールしたり。。。苦痛な作業でした。

画面デザインの対応C#コード

今回の設計では、WPF->Win32の手段はPostMessage()で、Win32->WPFの手段はCOM呼び出しで、を想定しています。
PostMessage()にATOMを載せて文字列も送れるようにしてみました。
もっとスマートなやり方があるんでしょうが、C#とか使ったことないんでごめんなさい><

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interop;
using System.Runtime.InteropServices;


namespace WPFPage
{

    public partial class TestPanel : UserControl
    {
        [DllImport("user32.dll")]
        public static extern int PostMessage(int hWnd, int msg, int wParam, IntPtr lParam);
        [DllImport("kernel32.dll")]
        public static extern int AddAtom(char[] lpstring);

        public static ViewModel lvm=null;

        public TestPanel(bool bcheck)
        {
            InitializeComponent();

            InteropTest.lbt = this;
            lvm = new ViewModel();
            DataContext = lvm;

            lvm.CheckTest = bcheck;
        }


        private void ClickButton(object sender, RoutedEventArgs e)
        {
            HwndSource source = (HwndSource)HwndSource.FromVisual(this);
            Button clicked = (Button)sender;
            lvm.Message = "ClickButton";
            PostMessage(source.Handle.ToInt32(), 0x0111, 0, (IntPtr)AddAtom(lvm.Message.ToCharArray()));
        }

        private void ClickCheck(object sender, RoutedEventArgs e)
        {
            HwndSource source = (HwndSource)HwndSource.FromVisual(this);
            if (lvm.CheckTest == false)
            {
                lvm.CheckTest = true;
                lvm.Message = "Checked";
            }
            else
            {
                lvm.CheckTest = false;
                lvm.Message = "Unchecked";
            }
            PostMessage(source.Handle.ToInt32(), 0x0111, 0, (IntPtr)AddAtom(lvm.Message.ToCharArray()));
        }

        public void UnCheck()
        {
            lvm.CheckTest = false;
        }
    }

    [ClassInterface(ClassInterfaceType.AutoDual)]
    public class InteropTest
    {
        public static TestPanel lbt;

        public void Uncheck()
        {
            lbt.UnCheck();
        }
    }
}

ViewModel

WPFのウリ?のデータバインディングというんでしょうか?見よう見まねでそのあたりを実装してみました。

using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace WPFPage
{
    public class ViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        private string message = "";
        public string Message
        {
            get
            {
                return this.message;
            }
            set
            {
                if (value != this.message)
                {
                    this.message = value;
                    NotifyPropertyChanged();
                }
            }
        }

        private bool checkTest = false;
        public bool CheckTest
        {
            get
            {
                return this.checkTest;
            }
            set
            {
                if (value != this.checkTest)
                {
                    this.checkTest = value;
                    NotifyPropertyChanged();
                }
            }
        }
    }
}

3: Win32のプロジェクトの追加

先ほど作成したWPFのソリューションにWin32のプロジェクトを追加します。
プロパティ設定のポイントは、共通言語ランタイムサポートの設定として/clrを指定することです。
なんかこの時点でまた少しやる気がそがれますね。C++11 STLのスレッドとかPPLとかと容易には共存できないってことですので。

Win32側の"おまじない"

Win32 C++のアプリをベースにするのでWinMain()が必須なのですが、その先頭にCOMアパートメント指定を入れるのがCLRでの流儀のようです。

[System::STAThreadAttribute]
int __stdcall WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int)

COMの設定

WPF側に書いたWPFPage.InteropTestを呼び出せるようにします。
COMとか詳しくないんで、こんなんでいいのか不安ですが。。。

  HRESULT hresult;
  CLSID clsid;
  hresult=CLSIDFromProgID(L"WPFPage.InteropTest", &clsid);
  hresult=CoCreateInstance(clsid, NULL, CLSCTX_INPROC_SERVER, IID_IUnknown, (void **)&pUnkWpf);
  hresult=pUnkWpf->QueryInterface(IID_IDispatch, (void **)&pDisWpf);

Win32側からのイベントで、COM関数を実行します。
めんどくさかったので引数なし^^;;;

  LPOLESTR funcname=L"UnCheck";
  DISPID dispid=0;
  hresult=pDisWpf->GetIDsOfNames(IID_NULL, &funcname, 1, LOCALE_SYSTEM_DEFAULT, &dispid);
  if(FAILED(hresult)) return false;

  DISPPARAMS params;
  memset(&params, 0, sizeof(DISPPARAMS));
  params.cArgs=0;
  params.cNamedArgs=0;
  params.rgdispidNamedArgs=NULL;
  params.rgvarg = NULL;

  VARIANT vRet;
  VariantInit(&vRet);

  hresult=pDisWpf->Invoke(dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_METHOD, &params, &vRet, NULL, NULL);

WPFページの貼り付け

Win32のリソースエディタで用意しておいたSTATICウィンドウの上に、WPFページを貼り付けます。
また、WPFがPostMessage()したメッセージをフックして、情報を取り出します。
(いつも私はWin32プロジェクトをANSI版に設定しているので、ATOMから取り出した文字列をコード変換しています)

namespace ManagedCode
{
  using namespace System;
  using namespace System::Windows;
  using namespace System::Windows::Interop;

  HWND p_hwnd;

  IntPtr ChildHwndSourceHook(IntPtr, int msg, IntPtr wparam, IntPtr lparam, bool% handled)
  {
    if(msg==WM_COMMAND){
      ATOM a=(ATOM)((int)lparam);
      wchar_t wbuf[256];
      GetAtomNameW(a, wbuf, 256);
      DeleteAtom(a);

      char buf[256];
      WideCharToMultiByte(CP_ACP, 0, (LPCWSTR)wbuf, -1, buf, 256, NULL, NULL);

      SetDlgItemText(p_hwnd, IDC_STATIC_MESSAGE, buf);
    }
    return IntPtr(0);
  }

  HWND GetHwnd(HWND parent, int x, int y, int width, int height)
  {
    p_hwnd=parent;
    HwndSourceParameters params;
    params.PositionX=x;
    params.PositionY=y;
    params.Width=width;
    params.Height=height;
    params.ParentWindow=IntPtr(parent);
    params.WindowStyle=WS_VISIBLE| WS_CHILD;

    HwndSource^ source=gcnew HwndSource(params);
    source->AddHook(gcnew HwndSourceHook(ChildHwndSourceHook));

    UIElement^ page=gcnew WPFPage::TestPanel(true);
    source->RootVisual=page;

    return (HWND)source->Handle.ToPointer();
  }
}

表示の調整

WPFのページを透明にすることはできないようです。仕方ないので、Win32側のSTATIC/DIALOGの背景色を合わせます。

  case WM_CTLCOLORSTATIC:
  case WM_CTLCOLORDLG:
    {
      HDC  hdc_ctrl=(HDC)wparam;
      HWND hwnd_ctrl=(HWND)lparam;
      return (LRESULT)hbrush;
    }
    break;

4: 完成

C#で作成したCOMライブラリを、管理者権限でRegAsmを使って登録します。

  c:\Windows\Microsoft.NET\Framework\v4.0.30319\RegAsm.exe WPFpage.dll

これで、一応は目標達成、かな。
v7.png