WinUI3

WinUI 3 の多言語化

WinUI 3 アプリを多言語化する方法についてです。

詳細は Zenn をどうぞ

WinUI 3 アプリの msixbundle を作成する

TemplateStudioWizardWinUI 3 アプリで msixbundle 形式のパッケージを作成する方法です。

詳細は Zenn をどうぞ

WinUI 3 で気に掛ける issue メモ

WinUI 3(Windows App SDK)公式 issue のうち、気になる issue のメモ(随時更新)。

microsoft-ui-xamlWindowsAppSDK に分かれているのが使いづらいが……。

未解決


解決済


.NET 8 で WinUI 3 アプリをビルドする

DotNet8WinUI 3 アプリのターゲットフレームワークを .NET 8 にするとビルド時に NETSDK1083 エラーが発生しますので、その解決方法です。

詳細は Zenn をどうぞ

カスタムコントロールで自由に描画する(C# / WinUI 3)

WinUI 3 でカスタムコントロールの描画をする方法を整理しました。

詳細は Zenn をどうぞ

テーマの実際の色等をコードで取得する(C# / WinUI 3)

テーマで指定されている色が実際何色なんだ、というのをコードで取得する方法を整理しました。

詳細は Zenn をどうぞ

WPF vs WinUI 3 機能対比表

WPF から WinUI 3 に移行する際に迷ったところを一覧にしました。

詳細は Zenn をどうぞ

SetWindowSubclass によるウィンドウプロシージャーのカスタム(C# / WinUI 3)


はじめに

GitHub標準のイベント等では処理できないウィンドウメッセージ(WM_*)を処理したい場合、自前のウィンドウプロシージャー(WndProc)を作る必要があります。

C# では、WinForms(フォーム)や WPF なら、ウィンドウプロシージャーをカスタムする仕組みが用意されている(HwndSource)ので簡単なのですが、WinUI 3(Windows App SDK)には残念ながら今のところそのような仕組みはないようです。

結局ネイティブ Windows API を使うしかないようですので、その方法をまとめました。Windows API なので WinUI 3 専用ではありませんが、出番としては WinUI 3 で開発するときくらいかと思います。

はやいとこ WinUI 3 にも簡単な仕組みが用意されることを祈っています……。

サンプルプログラムのソースコードは GitHub に上げてあります。

使用する API

ウィンドウプロシージャーをカスタムするのを「サブクラス化」と呼んでいるようです(C# でいう派生クラスではありません)が、マイクロソフトのドキュメントによれば、サブクラス化は新旧 2 種類あります。

旧バージョンでは SetWindowLong() or SetWindowLongPtr() と CallWindowProc() を使います。欠点は先のドキュメントにいろいろ書いてありますが、個人的にはそれよりも、元のウィンドウプロシージャーを自前で管理する分、手間が増えるのかなと思います。

新バージョンでは SetWindowSubclass() と DefSubclassProc() を使用します。元のウィンドウプロシージャーは気にしなくて済みます。

ここでは新バージョンを使います。

というか、新バージョンの情報が少なくて苦労したというのもあってここにまとめたというのもあります。

やりかた

ウィンドウメッセージを扱うので、NuGet で PInvoke.User32 を入れておきます。

カスタムウィンドウプロシージャーの型(SUBCLASSPROC コールバック関数)をデリゲートとして宣言しておきます。型の詳細はこちらに書いてあります。

internal delegate IntPtr SubclassProc(IntPtr hWnd, User32.WindowMessage msg, IntPtr wPalam, IntPtr lParam, IntPtr idSubclass, IntPtr refData);

カスタムウィンドウプロシージャーを作成します。

自分が処理したいメッセージの部分だけ書いて、あとは元のウィンドウプロシージャーに丸投げします。

private IntPtr CustomSubclassProc(IntPtr hWnd, User32.WindowMessage msg, IntPtr wPalam, IntPtr lParam, IntPtr idSubclass, IntPtr refData)
{
    switch (msg)
    {
        case User32.WindowMessage.WM_HogeHoge:
            (処理)
            return IntPtr.Zero;
        default:
            // 関心のあるメッセージ以外は次のウィンドウプロシージャーにお任せ
            return DefSubclassProc(hWnd, msg, wPalam, lParam);
    }
}

カスタムウィンドウプロシージャーを登録します。

_subclassProc = new SubclassProc(CustomSubclassProc);
SetWindowSubclass(hWnd, _subclassProc, IntPtr.Zero, IntPtr.Zero);

注意点の 1 つめは、SetWindowSubclass() に渡すのはカスタムウィンドウプロシージャー本体を「インスタンス化」したもの、ということです。インスタンス化していない CustomSubclassProc も SetWindowSubclass() に渡せてしまいますが罠です。

注意点の 2 つめは、そのインスタンスをずっと(カスタムウィンドウプロシージャーが不要となるまで)持ち続ける、ということです。

Crashこれらを守らないと、特に 64 ビットバイナリ(x64)において、一見うまく動くようですが、メッセージ処理をこなしているうちにアプリが強制終了します。私はこれでハマりました。

強制終了時は集約エラーハンドラーにすら引っかかりませんでした。デバッガーでは以下の箇所で落ちていました。

global::Microsoft.UI.Xaml.Application.Start((p) => {
    var context = new global::Microsoft.UI.Dispatching.DispatcherQueueSynchronizationContext(global::Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread());
    global::System.Threading.SynchronizationContext.SetSynchronizationContext(context);
    new App();
});

サンプルプログラム

Sampleサンプルプログラム(GitHub に上げてあります)では、ウィンドウのタイトルバーにヘルプボタンを表示しています。

このコンテキストヘルプは WinUI 3 標準ではハンドルできないため、カスタムウィンドウプロシージャーで処理しています。

ヘルプボタンをクリックする度に、ウィンドウのサイズが変化します(アプリとしてはありえない動きですが……)。

ちなみに、SetWindowSubclass() にインスタンス化していない CustomSubclassProc を直接渡すようにコードを変更すると、うちの環境で x64 バイナリを実行すると、チェックボックス連打で 1 分もしないうちにアプリが落ちました。

【追記】
Zenn をはじめてみました。試しにこの記事を Zenn に再掲してみました。

WinUI 3 で実際にアプリを作った感想

MainWindow静的 HTML によるヘルプ作成をアシストするツール「ちょちょいとファイル合併 2」(ソースコード)を WinUI 3(Windows App SDK)で作成した。

実際にアプリを作ると WinUI 3 の良いところ悪いところを実感できたので、そのまとめ。

なお、開発は Template Studio for WinUI を用いて行っているので、素の WinUI 3 より少し高機能な環境を使用している。

良いところ(メリット)

【デザインはわりと好き】

settingsボタンやチェックボックスなどのデザイン(Fluent Design)はわりと好き。

少なくとも、素の WinForms や WPF よりは洗練されている感がある。

マテリアルデザインはデフォルト(?)の紫をはじめとしたどぎつい色が多いので、それよりも落ち着いていて好き。ただ、パステル調のマテリアルデザインはきれいなので、そちらのほうが好き。

あと、ウィンドウを半透明+ぼかしにするアクリル効果は使いどころが難しいと思う。

【アクセスキーが表示されるのが良い】

AccessKeyコントロールにアクセスキー(Alt+x)が設定されている場合、Alt キーを押して離すとアクセスキーが表示されるのは分かりやすい。

普通のボタンはラベルにアクセスキーを表示しておけるが、アイコンボタンはアクセスキーを表示できないので、アクセスキー表示機能があることでユーザーはアクセスキーを認識できる。

Alt キーを「離した」時に表示されるので、既に知っているアクセスキーを使うときはわざわざ表示されず、うざくないのもポイント。

【RelativePanel】

WPF にあった DockPanel はなくなり、代わりに RelativePanel が登場。ちょっと使い方が難しいが、その分、高機能になっている。

RelativePanel で一番気に入ったのは、DockPanel の LastChildFill をより良く実現できること。

MainWindowDockPanel.LastChildFill の場合、残りのスペースを埋めるコントロール(右の図ではメイクファイル入力欄)を XAML 上で最後に記述することになり、結果的に、タブオーダーが見た目と異なってしまう(参照ボタンの後に入力欄が来る)。

RelativePanel なら、残りのスペースを埋めるコントロールを好きな順番で書くことができる(RelativePanel.RightOf / LeftOf を両方指定すると伸びる)ので、タブオーダーが見た目通りになる。

【静的バインディング】

XAML でのバインディングに静的バインディングの {x:Bind} が使える。

WPF は動的バインディングの {Binding} だったので、実行してみないとエラーが分からず、この点が個人的にとても嫌いだった。WinUI 3 で静的バインディングになり、コンパイル時に解決するのでミスが減る。パフォーマンスも向上するようだ。

ただ、デフォルトでバインディングの Mode が OneTime になっているので、WPF に慣れていると「値反映されない?」ってことになりがちなのは注意。

なお、{Binding} も使える。

【マルチリンガル】

恐らく Template Studio のおかげかと思うが、ローカライズの仕組みが最初から使える状態になっているのは良いと思う。

Ver 1.83 現在ではちょちょいとファイル合併 2 は多言語対応していないが、実験用に多言語対応してみてもいいかもしれない。

悪いところ(デメリット)

正直、悪いところが多すぎるし、1 つ 1 つのダメージがでかすぎる。なんとなくの印象だけど、すぐには改善されない気がする……。

※自分が気付いてないだけかもしれないので、もしご存じのかたはコメントいただけると幸いです。

【ウィンドウの機能が弱すぎる】

なんと、ウィンドウに Left / Top プロパティーがない。Template Studio では素の Window よりも強いサードパーティーの WindowEx を使っているが、それでもない。ウインドウを動かすには、コードビハインドで AppWindow.Move() を使う。MVVM どこ行った……。

ウィンドウサイズをコンテンツに合わせて調整する SizeToContent プロパティーもない。これを自力で実現するために無駄にコードを書く必要がある。自分は何をやっているのか……と虚しくなる。

なお、メインウィンドウに限っては XAML で Width / Height プロパティーを指定しても効かないが、これは Template Studio が何かしているからのようだ。コードビハインドで指定すると効くし、子ウィンドウなら XAML でも効く。

【ウィンドウをモーダルにできない】

子ウィンドウをモーダルウィンドウ(閉じるまで親ウィンドウを操作できないウィンドウ)にできない。モードレスになるので、環境設定ウィンドウで環境設定している時にメインウィンドウをクリックすると、メインウィンドウの操作ができてしまう。

確かに Windows 以外の OS に目を向ければ Haiku OS の流儀はすべてモードレスで、OS 全体でその流儀が浸透しているなら、モードレスはモードレスで使い勝手が良いし、個人的な好みで言えばモードレスかもしれない。しかし、Windows の流儀はモーダルなので、自分のアプリだけモードレスというのは違和感しかない。

ちょちょいとファイル合併 2 では擬似的にモーダルになるようにまたしても無駄にコードを書くこととなり、むなしさが増した。

PaintWindows 11 付属のペイントは WinUI 3 でありながら本物のモーダルウィンドウを実現しているっぽくて、どうやっているのか気になる。

【メッセージボックスが見切れる】

ContentDialogWinUI 3 のメッセージボックスは ContentDialog だが、メインウィンドウの範囲内でしか表示できないので、メインウィンドウよりも大きな内容を表示しようとすると見切れてしまう。

MessageDialog仕方ないので MessageDialog を使っているが、デザインが WinUI 3 とは異なるので違和感が出てしまう。

【ポップアップメニューがきつきつ】

Kitsu本質的には ContentDialog と同じ問題なのではと思うが、ポップアップメニュー(Flyout)も無理矢理メインウィンドウの範囲内で表示しようとするので、アイテム数が多いとスクロールバー表示になってしまい、非常に使いづらい。

Item3対処方法としては、スクロールしなくてもメインウィンドウ内に納まるアイテム数にするしかない。

ボタンとの位置関係によっては、納まるアイテム数にしていても、初回だけはスクロールバー表示になってしまい、これはバグだと思う。

ドキュメントによれば、FlyoutBase.ShouldConstrainToRootBounds プロパティを false にすればウィンドウ外に表示できるようなのだが、なぜか効かない。Popup.ShouldConstrainToRootBounds プロパティのドキュメントには「WinUI 3 では false はサポートされない」と書いてあるので、そういうことなのかもしれない。

【パッケージを作るの面倒】

公開用のパッケージを作る際、msix が作成され、msixbundle が作成されない。

手動で msixbundle を作成するのが面倒くさい。

【ロードマップが更新されない】

Windows App SDK のロードマップはこのへんこのへんに書かれているが、1.1 あたりで止まっている。

既に 1.2 が出ているが、この先どうなるのかの見通しが分からない。

【バグっぽい】

集約エラーハンドラー(Application.UnhandledException)がうまく動かなくてずっと悩んでいたが Windows App SDK 1.2 になったら動いたので、ただのバグだったようだ。

ポップアップメニューの初回位置もしかり。

【情報が少ない】

ノウハウしかり、ロードマップしかり、WinUI 3 に関する情報が非常に少ない。

クラスの検索をすると WinUI 3 ではなく UWP が引っかかりがち。

Uwp例えば、現時点では「TextBlock WinUI3」で検索すると UWP の TextBlock(Windows.UI.Xaml.Controls 名前空間)がトップに出てきて、WinUI 3 の TextBlock(Microsoft.UI.Xaml.Controls 名前空間)にはたどり着けない。

総評

良いところに目を向けて活用していきたいと思うものの、少なくとも現状では、WinUI 3 での開発は茨の道だと思う。

機能比較表

WPF と WinUI 3 の機能比較表を作成

更新履歴

  • 2022/11/23 初版。
  • 2022/11/26 バインディングについて追記。
  • 2022/11/26 ポップアップメニューについて追記。
  • 2023/05/21 機能比較表について記載。

WinUI 3 立ち位置メモ

アーキテクチャー的に? 構造的に? 階層的に? WinUI 3 がどのへんにいるのかがよく分からないので、調べてみた。

※情報をご存じの方はご教示いただけると幸いです!

今のところの想像はこんな感じ。

階層図2

以下元ネタ。

月別アーカイブ
記事検索
最新コメント
  • ライブドアブログ