Windowsサービスに、独自のサービスコントロール(SC)を送る

もくじ
https://tera1707.com/entry/2022/02/06/144447

やりたいこと

以前の記事で、サービスを作ったのだが、 SERVICE_CONTROL_PAUSESERVICE_CONTROL_STOPなどの元からあるサービスコトロールではなく、 自分で定義した独自のSCを受け付けたい。

とりあえず、管理者コマンドプロンプトで 'sc control サービス名 SC番号' で独自のSCを送れるようなので、それを受け取れるようにしたい。

やったこと

サービス側のコードとしては、なんのことはない、以前のサービス実験コードのSCを受ける部分に、case文を一個追加するだけ。

以下、そこだけ抜粋

// ServiceControlがSCMから送られてくるたびに呼ばれる
DWORD WINAPI SvcCtrlHandler(DWORD dwControl, DWORD dwEventType, LPVOID lpEventData, LPVOID lpContext)
{
    switch (dwControl)
    {
    case SERVICE_CONTROL_STOP:
        ReportSvcStatus(SERVICE_STOP_PENDING, NO_ERROR, 0);
        // サービス停止シグナルを送る
        SetEvent(ghSvcStopEvent);
        // 停止を報告
        ReportSvcStatus(gSvcStatus.dwCurrentState, NO_ERROR, 0);
        return NO_ERROR;

    case SERVICE_CONTROL_INTERROGATE:
        return NO_ERROR;

    case 200: // ★これがそう
        OutputLogToCChokka(L" 独自SC来ました");
        break;

    default:
        break;
    }

    return NO_ERROR;
}

今回追記したのは case 200: の部分だけ。これで受け取り部分は完成。

SCを送る側は、管理者でコマンドプロンプトを立ち上げて、 'sc control SvcName 200' と打ち込んで実行すれば、200番のSCが飛んでくれる。

ただ、今回、ここで大変苦労した。

独自SCは、128~255 の範囲でないといけないらしい。

なのに、最初 case 666: 、 'sc control SvcName 666' とか、100番など、範囲外の番号を使ってしまっていて、sc controlのコマンドが下記のようにエラーを出して困った。

範囲内の番号を使うことで、うまくSCを投げる/受け取ることができた。

参考

以前調べた、C++でのサービスの作り方

https://qiita.com/tera1707/items/dc24ac78ae76906abb83

SCは、128~255の範囲でしか使えないというのを知ったページ。

https://www.tokovalue.jp/function/ControlService.htm

ほうれん草のごまあえ

完成図

作り方

  • ほうれんそう洗う
  • 2cmくらいにざくざく切る
  • 30秒くらい茹でる
  • ざるにあけて、みずで冷やす
  • 水をしぼって、ボウルにあける
  • めんつゆを入れる(もったいなくないくらい入れて、味ついてたら良し)
  • 砂糖、ほんの少しだけ入れる(こさじ1くらい?)
  • ごま適当にかける

おわり

備考

茹でてから切るのが正しいかも?

タイトルバーで右クリックして出てくるコンテキストメニューから一部項目を消す(Winui3のタイトルバーを右クリックしたときのコンテキストメニューのバグ対応)

もくじ
https://tera1707.com/entry/2022/02/06/144447#WinUI3

やりたいこと

以前、WinUI3のウインドウを最大化できないようにしたくて、下記ページでOverlappedPresenterを使って実現した。 そこでは、最大化ボタンを無効表示にできていた。

https://tera1707.com/entry/2022/04/24/220519

また、その時、タイトルバーを右クリックしたときのコンテキストメニューでも、最大化が無効にできていた。

ただ、今回たまたま見つけた(職場のゴッドハンドが見つけてくれた)のだが、タイトルバーの、めっちゃ端っこを右クリックしてコンテキストメニューを出すと、なぜか最大化が有効になっていて、押すと実際に最大化されてしまう。

上の端スレスレを右クリックすると、下記のようになる...

なぜ?UWPの血を受け継いでアプリがコンテナ?に入って動くようになったときに、そのガワが何か悪さしてるのか? これでは最大化を防いだつもりが防げてないので、何とかしたい。

やり方

WinUI3のウインドウ関係でなんかかゆいときに手がとどかないってなったときは、Win32API直叩きだろう、と最近思ってるので、その辺でやり方探したところ、こちらの記事で、WPFでメニューの追加ができると知った。

なら無効化や削除もできるだろうということで、MSページを参考に試してみた。

サンプルコード

こういう流れでやってみた。

  • ウインドウハンドルを取得
  • OverlappedPresenterで最大化をできないようにする
    • これで、ウインドウの×ボタンの横の最大化ボタンは無効になる
    • タイトルバー(の真ん中あたり)を右クリックして出てくるコンテキストメニューの中の最大化ボタンも無効になる
    • が、タイトルバーの端っこギリギリを右クリックして出てくるコンテキストメニューの中の最大化ボタンは、無効にならない!
  • GetSystemMenu()関数で、システムメニュー(コンテキストメニュー)のハンドルをとる
  • DeleteMenu()関数で、最大化メニューを消す
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using System;
using System.Runtime.InteropServices;

namespace MaximizeTestWinui3
{
    public sealed partial class MainWindow : Window
    {
        public MainWindow()
        {
            this.InitializeComponent();
            
            // WinUI3のウインドウのハンドルを取得
            var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
            Microsoft.UI.WindowId windowId = Microsoft.UI.Win32Interop.GetWindowIdFromWindow(hWnd);
            Microsoft.UI.Windowing.AppWindow appWindow = Microsoft.UI.Windowing.AppWindow.GetFromWindowId(windowId);

            // 標準タイプのウインドウ(OverlappedPresenter)に対して、最大化の無効設定を行う
            var op = OverlappedPresenter.Create();
            op.IsMaximizable = false;
            op.IsResizable = false;
            appWindow.SetPresenter(op);

            // Win32APIで、コンテキストメニューの最大化を削除する
            IntPtr systemMenuHandle = GetSystemMenu(hWnd, false);
            DeleteMenu(systemMenuHandle, (uint)4, (uint)(MF_BYPOSITION));
        }

        // Win32 API関連
        private readonly Int32 MF_BYPOSITION = 0x400;
        [DllImport("user32.dll")]
        private static extern IntPtr GetSystemMenu(IntPtr hWnd, bool bRevert);
        [DllImport("user32.dll")]
        static extern bool DeleteMenu(IntPtr hMenu, uint uPosition, uint uFlags);
    }
}

結果

上記コードを動かすと、下図のようになる。

コード中の、DeleteMenu(systemMenuHandle, (uint)4, (uint)(MF_BYPOSITION));の部分の「4」は、最大化の項目が、コンテキストメニュー内の上から0番始まりで数えて4番目だから。

注意点・メモなど

EnableMenuItem()関数で無効化ができなかった

MSのドキュメントによると、EnableMenuItem()という、メニュー項目の有効無効ができるAPIがある様子。

だが、試したところ、EnableMenuItem()では、メニューの無効化はできなかった。 WinUI3では、この関数はうまく動いてくれないのかもしれない。

DeleteMenu()関数でメニューの項目を削除することはできたので、仕方なくそっちで代用することにした。

サイズ変更も同じ現象が起きる

上のコードと結果の画面を見ると、コードでは最大化禁止のほかに、サイズ変更も禁止にしているのに、

端っこを右クリックしたときのコンテキストメニューでは、「サイズ変更」が押せてしまう。

どうもWinUI3に不具合があるっぽい。

「端っこ」は、Resizableなときにマウスポインタが「↕」の形に変化するあたりっぽい

色々試したところ、どうも掲題の通りっぽい。

Resizable = true でサイズ変更可能な時は、マウスポインタが矢印型になって右クリックできない場所が、今回、Resizable = false にして、サイズ変更を禁止したために右クリックできてしまって、そこがなんか変、ということっぽい。何かそこだけ特殊なことをしているのか。。

WPFでためすと、上端で右クリックしてもContextMenu出ない

WPFだと、上の端っこで右クリックしても、そもそもContextMenuが出てこなかった。

やっぱり、WinUI3のガワには、何かがいる?

追記・自分でInsertMenuしたメニュー項目は、EnableMenuItemで有効無効表示切替えれるっぽい

どうも、上でEnableMenuItemで有効無効を切り替えで出来なかったのは、元からある項目だったからっぽい。(最大化とか、最小化とか。)

試しに自分でInsertMenuして追加したメニュー項目だと、下図のようにEnableMenuItemで有効無効切り替えできた。

試したコード

using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using System.Runtime.InteropServices;
using System;

namespace MaximizeTestWinui3
{
    public sealed partial class MainWindow : Window
    {
        public MainWindow()
        {
            this.InitializeComponent();
            
            // WinUI3のウインドウのハンドルを取得
            var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
            Microsoft.UI.WindowId windowId = Microsoft.UI.Win32Interop.GetWindowIdFromWindow(hWnd);
            Microsoft.UI.Windowing.AppWindow appWindow = Microsoft.UI.Windowing.AppWindow.GetFromWindowId(windowId);

            // 標準タイプのウインドウ(OverlappedPresenter)に対して、最大化の無効設定を行う
            var op = OverlappedPresenter.Create();
            op.IsMaximizable = false;
            op.IsResizable = false;
            appWindow.SetPresenter(op);

            IntPtr systemMenuHandle = GetSystemMenu(hWnd, false);

            for (int i = 0; i < 6; i++)
            {
                InsertMenu(systemMenuHandle, 0, (int)MF_BYPOSITION, ITEMONEID, "Item " + i);
            }

            for (int i = 0; i < 12; i++)
            {
                EnableMenuItem(systemMenuHandle, (uint)i, MF_BYPOSITION | ((i % 2 == 0) ? MF_GRAYED : 0));//1個飛ばしで無効にしてみる
            }
        }

        uint MF_BYCOMMAND = 0x00000000;
        uint MF_BYPOSITION = 0x00000400;
        uint MF_ENABLED = 0x00000000;
        uint MF_DISABLED = 0x00000002;
        uint MF_GRAYED = 0x00000001;
        uint MF_STRING = 0x00000000;

        private const Int32 ITEMONEID = 1000;

        [DllImport("user32.dll")]
        private static extern IntPtr GetSystemMenu(IntPtr hWnd, bool bRevert);
        [DllImport("user32.dll")]
        private static extern bool InsertMenu(IntPtr hMenu, Int32 wPosition, Int32 wFlags, Int32 wIDNewItem, string lpNewItem);

        [DllImport("user32.dll")]
        static extern bool EnableMenuItem(IntPtr hMenu, uint uIDEnableItem, uint uEnable);
    }
}

追記:WM_INITMENUPOPUP が来たときにEnableMenuItem()で無効化できる

上で、EnableMenuItem()では、最大化のメニューを無効にできなかったと書いたが、Winuiのissueに上記を投げたところ、やり方あることを教えてもらった。

  • ウインドウプロシージャでWM_INITMENUPOPUP をひっかけて、
  • その中でEnableMenuItem()で、指定のitemを無効にする

ができるらしい。ので、下記でためした。

結果、うまくいった。
下記で、Workaroundとしてはつかえるかもしれない。(WAコード長いが....)

using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using System;
using System.Runtime.InteropServices;

namespace MaximizeTestWinui3
{
    public sealed partial class MainWindow : Window
    {
        private NativeMethods.WinProc newWndProc = null;
        private IntPtr oldWndProc = IntPtr.Zero;

        public MainWindow()
        {
            this.InitializeComponent();

            var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
            newWndProc = new NativeMethods.WinProc(NewWindowProc);
            oldWndProc = NativeMethods.SetWindowLongPtr64(hwnd, PInvoke.User32.WindowLongIndexFlags.GWL_WNDPROC, newWndProc);

            var windowId = Microsoft.UI.Win32Interop.GetWindowIdFromWindow(hwnd);
            var appWindow = Microsoft.UI.Windowing.AppWindow.GetFromWindowId(windowId);
            ((OverlappedPresenter)appWindow.Presenter).IsMaximizable = false;
            ((OverlappedPresenter)appWindow.Presenter).IsMinimizable = true;
            ((OverlappedPresenter)appWindow.Presenter).IsResizable = false;
        }

        private IntPtr NewWindowProc(IntPtr hWnd, PInvoke.User32.WindowMessage Msg, IntPtr wParam, IntPtr lParam)
        {
            switch (Msg)
            {
                case PInvoke.User32.WindowMessage.WM_INITMENUPOPUP:
                {
                    NativeMethods.EnableMenuItem(wParam, NativeMethods.SC_MAXIMIZE, NativeMethods.MF_BYCOMMAND | NativeMethods.MF_DISABLED | NativeMethods.MF_GRAYED); 
                    return IntPtr.Zero;
                }

            }
            return NativeMethods.CallWindowProc(oldWndProc, hWnd, Msg, wParam, lParam);
        }
    }

    public class NativeMethods
    {
        public delegate IntPtr WinProc(IntPtr hWnd, PInvoke.User32.WindowMessage Msg, IntPtr wParam, IntPtr lParam);

        [DllImport("user32.dll", EntryPoint = "SetWindowLongPtr")]
        public static extern IntPtr SetWindowLongPtr64(IntPtr hWnd, PInvoke.User32.WindowLongIndexFlags nIndex, WinProc dwNewLong);
        
        [DllImport("user32.dll")]
        public static extern IntPtr CallWindowProc(IntPtr lpPrevWndFunc, IntPtr hWnd, PInvoke.User32.WindowMessage Msg, IntPtr wParam, IntPtr lParam);

        [DllImport("User32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
        public static extern bool EnableMenuItem(IntPtr hMenu, uint uIDEnableItem, uint uEnable);

        public const int MF_BYCOMMAND = 0x00000000;
        public const int MF_BYPOSITION = 0x00000400;
        public const int MF_ENABLED = 0x00000000;
        public const int MF_GRAYED = 0x00000001;
        public const int MF_DISABLED = 0x00000002;

        public const int SC_SIZE = 0xF000;
        public const int SC_MOVE = 0xF010;
        public const int SC_MINIMIZE = 0xF020;
        public const int SC_MAXIMIZE = 0xF030;
        public const int SC_NEXTWINDOW = 0xF040;
        public const int SC_PREVWINDOW = 0xF050;
        public const int SC_CLOSE = 0xF060;
        public const int SC_RESTORE = 0xF120;
    }
}

備考

初めてmiscrosoftのWinuiのリポジトリにissueを出した。 どうなるものか。。

https://github.com/microsoft/microsoft-ui-xaml/issues/8201

⇒上のWAを教えてもらった。投げてみるもんですね。

参考

MS公式 メニュー関連のAPI

https://learn.microsoft.com/ja-jp/windows/win32/menurc/menu-functions

WPFでのメニューへの項目追加

https://qiita.com/todu/items/a3d053f514a4fe36bf29

その他参考

http://wisdom.sakura.ne.jp/system/winapi/win32/win81.html

Windowsサービスで発生するエラーのメモ

目次
https://tera1707.com/entry/2022/02/06/144447#WindowsService

やりたいこと

以前、C++のサービスの作り方を調べて、サンプルを作った。

サービスの処理の中で、Windowsのルール、お作法に従えていないときなどには、Windowsのイベントビューアにいろいろエラーログが残る様子。

上記のサンプルで勉強する中で、見たエラーログを、下記にメモする。

もくじ

  • イベント7009
  • イベント7000
  • イベント7034
  • イベント7031
  • イベント7011
  • イベント7046

■イベント7009 「SvcName サービスの接続を待機中にタイムアウト (30000 ミリ秒) になりました。」

サービスのexeが起動してから、StartServiceCtrlDispatcher()を呼ぶまでに、デフォルトで30秒以上かかったときに、このログが出る。

StartServiceCtrlDispatcher()を読んだ後に30秒Sleepしてみても、7009は発生しなかった。 (StartServiceCtrlDispatcherで登録したハンドラの中の先頭でSleepさせてみて実験)

image.png

※このエラー発生時、エラー発生時点でサービスのexe(プロセス)は終了していた。

※デフォルトの30秒というのは、下記レジストリで変えられる。

  • キー
    • HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control
    • ServicesPipeTimeout

参考:https://www.yrl.com/fwp_support/faq/a1hrbt0000000n9x.html

※試しにServicesPipeTimeoutを5秒(5000)にしたときの7009エラーは下記の通り。
 レジストリの値を変えた後は、再起動必要。

image.png

インサイドWindows 第6版のP348 4.2 サービス の中に、

StartServiceCtrlDispatcher関数は、SCMと名前付きパイプを使ってやり取りするための接続を確立した後、名前付きパイプを通じてSCMからコマンドが届くのを待ちます

とある。この関数が「接続を確立する」のが、指定秒数より遅れると、7009になるものだと思われる。

★追記

上で試した

  • main関数が開始してからStartServiceCtrlDispatcher()を呼ぶ前に、30秒以上かかってしまった場合

に加えて、

  • main関数が開始してからStartServiceCtrlDispatcher()を呼ぶ前に、returnしてしまった場合

にも、このイベントが発生する様子。

イベントのDiscriptionから、「待たされたときに発生する」イメージを持ってしまうが、 「StartServiceCtrlDispatcher()にたどり着かなかった」ときにも発生する。地味に注意。

■イベント7000 「SvcName サービスを、次のエラーが原因で開始できませんでした: そのサービスは指定時間内に開始要求または制御要求に応答しませんでした。」

今回試した限りでは、上のエラーイベント7009と同時に、ペアで発生していた。

■イベント7034 「SvcName サービスは予期せぬ原因により終了しました。このサービスの強制終了は 1 回目です。」

サービスが予期せぬエラーで終わったときに出る。

今回試したときは、サービスのプロセスをtaskkillコマンドで意図して強制終了したときに出た。
(メッセージに「強制終了は・・・」とある。もしかしたら、taskkillでなくて本当に予期せぬエラーの場合は、違うコードなのかも?未検証。)

■イベント7031 「SvcName サービスは予期せぬ原因により終了しました。このサービスの終了は 1 回目です。次の修正操作が 60000 ミリ秒以内に実行されます: サービスの再開。」

下図のように、サービスの設定で、エラー時にサービスを再起動するように設定していると、 サービスが予期せぬエラーが終わったときにこのイベントが出た。 (再起動するように設定してないと、7034になるっぽい)

「最初のエラー」だけに再起動の設定をして、「次のエラー」以降を「何もしない」にしていても、「エラーカウントのリセット」が0日後になっていると、1回エラーが起きても回数が即リセットされるようで、何度taskkillしても7031で「1回目」になり、毎回再起動された。

「エラーカウントのリセット」を「1日後に行う」にすると、カウントがされるようになり、2回目にtaskkillしたときに下図ようになり、以降再起動はされなかった。

■イベント7011 「SvcName サービスからのトランザクション応答を待機中にタイムアウト (5000 ミリ秒) になりました。」

サービスがサービスコトロールを受け付けてから一定時間(デフォルト30秒)以内に処理を返さない場合(具体的にはサービスコトロールを処理する関数を抜けない場合)に発生。

デフォルトの30秒は、イベント7009のタイムアウト値の設定のレジストリの値ServicesPipeTimeoutの値になる。(ので、今回実験したときに5000になってたので5秒でエラーイベント発生した。)

発生時、コマンドプロンプトでSCを送っていた側には下図のようなエラーが出た。

また実験的に、下記のようなSCを受ける関数を作って、200番のSCを送って試した際、

  • Sleepしてる最中にエラーイベントが出て、sc controlコマンドは上図のようなエラーになる。
  • が、35秒経過後、サービス側はSleepを抜けて、何事もなかったかのように続きが実行される。状態もRUNNNINGのまま。

という動きになった。

DWORD WINAPI SvcCtrlHandler(DWORD dwControl, DWORD dwEventType, LPVOID lpEventData, LPVOID lpContext)
{
    switch (dwControl)
    {
    case 200:
        Sleep(35000);
        break;
 ・・・以下省略

インサイドWindowsには、「SCの処理は30秒以内に終わらせないとエラーになる」と書いているのに、サービスのexeは何のエラーも出さないじゃないか、と思っていたら、  exeが例外出すとかではなく、「エラーのイベントがイベントログに記録される」ということだった。

■イベント7046

7011が10回連続で発生すると、7046になることがあるらしい。
が、今自分の実験用サービスで、7011を10回起こしても7046は起きなかった。実験用に、無理やりSleepさせてたり、タイムアウトを5秒にしていることで起きなくなっているのかもしれない。 要検証。

DIでServiceCollectionに同じInterfaceのクラスを複数登録する

もくじ
https://tera1707.com/entry/2022/02/06/144447

やりたいこと

以前の記事で、IocServiceCollection を使って、DI(Dependency Injection)をやったが、そのときコンストラクタで受けるクラスは、それぞれ別のインターフェースを実装するクラスだった。

同じインターフェースを実装したクラスを複数受け取りたいときはどうしたらいいか?調べてみる。

まずこまったこと

下記のツールを作ってるときにおきたこと。

https://github.com/tera1707/FileVerUpTool

下記のコンストラクタのように、同じインターフェースを実装したクラスを3つ受け取りたかった。

public VersionReadWrite(ISearchSpecifiedExtFile ssef, IProjMetaDataHandler mdh1, IProjMetaDataHandler mdh2, IProjMetaDataHandler mdh3)
{
    _finder = ssef;
    _sdkcsproj = mdh1;
    _dfwcsproj = mdh2;
    _cppproj = mdh3;
}

そのために、services.AddSingletonを、下記のように3回読んで、3つ分登録しようとした。

services.AddSingleton<IProjMetaDataHandler, SdkTypeCsprojHandler>();
services.AddSingleton<IProjMetaDataHandler, DotnetFrameworkProjHandler>();
services.AddSingleton<IProjMetaDataHandler, CppProjHandler>();

そうすると、コンストラクタで受け取るIProjMetaDataHandlerを実装したクラスが、3つとも、3個目に登録したCppProjHandlerになってしまった。
おそらく、前の二つは、最後のCppProjHandlerに後勝ちで上書きされたものと思われる。

なので、今回は下記のようにした。

登録する個所

services.AddSingleton<IProjMetaDataHandler[]>(x => new IProjMetaDataHandler[] { new SdkTypeCsprojHandler(), new DotnetFrameworkProjHandler(), new CppProjHandler() });

使うコンストラク

public VersionReadWrite(ISearchSpecifiedExtFile ssef, IProjMetaDataHandler[] handlers)
{
    _finder = ssef;
    _sdkcsproj = handlers[0];
    _dfwcsproj = handlers[1];
    _cppproj = handlers[2];
}

これで、うまく、同じインターフェースを実装したクラスを3つ分受け取ることができた。

[WinUI3] WinUI3で表を使うために、CommunityToolkit.WinUI.UI.Controls の DataGridを試す

WinUI3のDataGridで表を作りたい。

DataGridはWindows Community Toolkitに含まれている。(WinAppSDKの元にはない)

https://github.com/CommunityToolkit/WindowsCommunityToolkit

Windows Community Toolkitの7.1.2が、WinAppSDKの1.0をサポートしているらしい。

https://devblogs.microsoft.com/ifdef-windows/windows-community-toolkit-for-project-reunion-0-5/

昔は、名前空間が「Microsoft.Toolkit.Uwp.*」だったのが、

WinUI3対応のために「CommunityToolkit.WinUI.*」になった。

???

CommunityToolkitは、「.NET Community Toolkit」に移行しようとしている??

2021/12/1時点で、CommunityToolkitの7.1.2がでて、これが最後になる??

最後のリリース?

https://github.com/CommunityToolkit/WindowsCommunityToolkit/releases/tag/winui-7.1.2

今後はこっち?

https://github.com/CommunityToolkit/dotnet

でも、CommunityToolkit Sample Appでは、UWP向けだが7.1.3も出てるとある。よくわからん、、、

下記ページが、WinUI3のDataGridの使い方としてはよさそう。

https://devlog.grapecity.co.jp/winui-3-datagrid-1/

やってみたら何となく動きはしたが、上のp-時にあるようなサンプルコードに、MSの公式のどこでたどり着けるのか??

<Window
    x:Class="DataGridTest.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:DataGridTest"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:controls="using:CommunityToolkit.WinUI.UI.Controls"
    mc:Ignorable="d">

    <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
        <controls:DataGrid x:Name="WinUI3DataGrid" />
    </StackPanel>
</Window>
using Microsoft.UI.Xaml;
using System.Collections.Generic;

namespace DataGridTest
{
    public sealed partial class MainWindow : Window
    {
        List<MyData> DataList { get; set; } = new List<MyData>();

        public MainWindow()
        {
            this.InitializeComponent();

            DataList.Add(new MyData("1", "AAA"));
            DataList.Add(new MyData("2", "BBBB"));
            DataList.Add(new MyData("3", "CCCCC"));

            WinUI3DataGrid.ItemsSource = DataList;
        }
    }

    public record MyData(string Name, string Value);

}

結果

上で見た「CommunityToolkit/WindowsCommunityToolkit」のリポジトリの、 mainではなく「rel/winui7.1.2」ブランチに、WinUI3用っぽいコード一式があった。

https://github.com/CommunityToolkit/WindowsCommunityToolkit/tree/rel/winui/7.1.2

最終的に

「CommunityToolkit.WinUI.UI.Controls」を使うことにした。

今回はテストツールのために使ったので凝った見た目にしたりはしてないが、普通に表として使う分には十分だった。

https://github.com/CommunityToolkit/WindowsCommunityToolkit
の「rel/winui7.1.2」ブランチに該当。

dotnetコマンドで.gitignoreを作成する

もくじ
https://tera1707.com/entry/2022/02/06/144447

やりたいこと

新しくgithubリポジトリを作ったときに、.gitignoreがないので、コミットするときにいらないファイル(binフォルダやobjフォルダの中身など)がたくさん入ってしまうので、gitignoreを配置して不要なファイルがコミットの候補にあがらないようにしたい。

やりかた

dotnetコマンドを使う。

対象のソリューション(.sln)があるフォルダで、powershellを開く。
(エクスプローラでそのフォルダを開き、エクスプローラのパスが表示されてる窓に「powershell」と入れてエンターを押す)

そこで、dotnet new gitignore を実行する。

そうすると、dotnetで使うような標準的なgitignoreが作成される。その状態でそのプロジェクトをコミットしようとすると、不要なファイルが候補にあがらなくなってる。

参考

Visual StudioでGitリポジトリにコミットするときにオブジェクトファイルやコンパイル結果のファイルも含まれてしまう (gitignore ファイルの作成) (Visual Studioの使い方 Tips)

https://www.ipentec.com/document/visual-studio-create-gitignore-file

XElementを使ってxmlファイルを読み込む

もくじ
https://tera1707.com/entry/2022/02/06/144447

やりたいこと

xmlファイルを読み込みたい。
具体的には、VisualStudioのC#(.net6)のプロジェクトファイル(.csproj)を読み込んで、中身の情報を見たい。

csprojのサンプルはこんな感じ。

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <AssemblyVersion>1.0.1234.5</AssemblyVersion>
    <Version>1.2.5000.0</Version>
    <Authors>MySakuseiSha</Authors>
    <Company>MyCompany</Company>
    <Product>MySeihinMei</Product>
    <Description>MySetsumei</Description>
    <Copyright>MyCopyRight</Copyright>
  </PropertyGroup>

</Project>

やりかた

System.Xml.Linq名前空間XElement を使う。

XElement xml = XElement.Load(x);

// 全部取る
var infos = xml.Elements();

//---------------------------------

// 全部取る
foreach (var info in infos)
{
    Debug.WriteLine(" " + info.Name + ", " + info.Value);

    foreach (var element in info.Elements())
    {
        Debug.WriteLine("  " + element.Name + ", " + element.Value);
    }
}

このコードで、上に挙げたcsprojnサンプルを読んだときの結果の一部

 PropertyGroup, Exenet6.0enableenable1.0.1234.51.2.5000.0MySakuseiShaMyCompanyMySeihinMeiMySetsumeiMyCopyRight
  OutputType, Exe
  TargetFramework, net6.0
  ImplicitUsings, enable
  Nullable, enable
  AssemblyVersion, 1.0.1234.5
  Version, 1.2.5000.0
  Authors, MySakuseiSha
  Company, MyCompany
  Product, MySeihinMei
  Description, MySetsumei
  Copyright, MyCopyRight

値が取れてる。

参考

MS公式 XElement クラス

https://learn.microsoft.com/ja-jp/dotnet/api/system.xml.linq.xelement?view=net-7.0

【保存版】C#xmlを読み書きする最もシンプルな方法はこれ!

https://www.sejuku.net/blog/86867

指定フォルダの中にある.特定の拡張子のファイルをリストUPする

もくじ
https://tera1707.com/entry/2022/02/06/144447

やりたいこと

Grepみたいな機能を作るために、指定した拡張子のファイルを、指定のフォルダの中から、子フォルダの中も含めて検索するような機能をつくりたい。

やりかた

DirectoryInfoクラスのGetFiles()GetDirectories()を使う。

サンプルコード

using Microsoft.UI.Xaml;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;

namespace FileVerUpTool
{
    public sealed partial class MainWindow : Window
    {
        string targetExt = "*.csproj";
        string targetDir = @"C:\Users\masa\source\repos\FlyoutableToggleButtonJikken";
        List<string> foundList = new();

        public MainWindow() => this.InitializeComponent();

        void SearchFiles(string target)
        {
            var parentDir = new DirectoryInfo(target);
            var files = parentDir.GetFiles(targetExt);

            // その階層にある対象ファイルをリストに入れる
            files.ToList().ForEach(x=> foundList.Add(x.FullName));

            // その階層にあるフォルダを探し、
            var childDirs = parentDir.GetDirectories();
            // フォルダの中を探しに行く(再帰的に)
            foreach (var dir in childDirs)
            {
                SearchFiles(dir.FullName);
            }
        }

        private void myButton_Click(object sender, RoutedEventArgs e)
        {
            // csprojを探す
            SearchFiles(targetDir);

            // 見つかった奴を表示する
            foundList.ForEach(x => Debug.WriteLine(x));
        }
    }
}

参考

ある文字列を含むすべてのファイルを検索する

https://dobon.net/vb/dotnet/file/searchtextinfiles.html

charmapでフォントファイル(.ttf)の中身を見る

もくじ
https://tera1707.com/entry/2022/02/06/144447

やりたいこと

フォントファイル(.ttf)の中にどんなフォントがあるのかと、その文字コードを知りたい。

やりかた

charmap.exeを使う。

C:\Windows\System32の中にあるcharmap.exeを実行すると、下記のような画面になる。

画面上部のコンボボックスに、PCにインストールされているフォントの一覧があるので、そこから見たいフォントを選択する。

自作でフォントファイル(.ttf)を作ったときには、そのフォントをインストールして、charmap.exeを起動して、そのフォントを選択すると、その.ttfの中にあるフォントと、それぞれの文字コードを見ることができる。

画面下部の、U0021;というところが文字コード

#やりかた(別解)

記事を下記ながら、MS公式ではないが、charmapのUWP版というのを見つけた。

apps.microsoft.com

これでも同じことができるっぽい。

もとのcharmap.exeは、windowsに標準でインストールされてるので手軽に使えそうだが、わかりやすく見たいのであれば、UWP版を使ってもよさそう。
(また、charmapっぽいstoreアプリは、検索すればいくつか出てきた。似たものは複数ありそう。)

クラスのprivateなフィールド、プロパティの値を外から変えて、UnitTestする(.NET6)

もくじ
https://tera1707.com/entry/2022/02/06/144447#UnitTest

やりたいこと

クラスの中で持っているprivateなフィールドやプロパティの値を外からいじって、そのクラスをユニットテストしたい。

以前、.NET6でprivateやinternalなメソッドを呼ぶ方法を調べて、今回やりたいようなことは できるんだろうなと思っていたが実際にやってはいなかったので、ほんとにできるか試したい。

前提

.NET6

やりかた

以前と同じように、リフレクションを使う。

サンプルコード

namespace GetFieldTest
{
    internal class Program
    {
        static void Main(string[] args)
        {
            var mc1 = new MyClass();
            mc1.DisplayField();

            var mc2 = new MyClass();

            // まず対象クラスのtypeを取得
            var type = typeof(MyClass);

            // そのtypeでGetFieldする
            var field = type.GetField("_fieldString", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
            var prop = type.GetProperty("_propertyString", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);

            // クラスのインスタンスを指定して、取得したFieldに対してSetValueする
            field.SetValue(mc2, "Irekonda Field Mojiretsu");
            prop.SetValue(mc2, "Irekonda Property Mojiretsu");

            // 値をセットしたので、値を書き換えたクラスでメソッドを読んでみる
            mc2.DisplayField();
        }
    }

    // テスト対象のクラス
    internal class MyClass
    {
        private string _fieldString = "Motomotono Field Mojiretsu";
        private string _propertyString { get; set; } = "Motomotono Property Mojiretsu";

        public void DisplayField()
        {
            Console.WriteLine(_fieldString);
        }
        public void DisplayProperty()
        {
            Console.WriteLine(_propertyString);
        }
    }
}

実行結果

2回目によんだときは、フィールドとプロパティの値が書き換えられている。

注意

Propertyのほうは、`setまたはinitがないと、SetValueのところでArgumentExceptionという例外がでる。

参考

クラス自体がinternalなときのUnitTest(.NET6版)

https://tera1707.com/entry/2022/01/29/173244

private、internalなメソッドのUnitTest(.NET6版)

https://tera1707.com/entry/2022/01/29/202451

WinUI3アプリをローカライズする(ResourceDictionary使用)

WInUI3関連記事
https://tera1707.com/entry/2022/02/06/144447#WinUI3

やりたいこと

WinUI3アプリをローカライズしたい。

以前の記事で、MSのドキュメントをもとに、.reswを使ってローカライズをした。

WinUI3を手探りで使う中でローカライズをしたいときに、MS公式にあるやり方で間違いないだろうということでそのやり方でやってみたが、使いづらい面も個人的にあった。
(x:Uidを使うところ。こちらでやったWPFでのローカライズのように、単なる文字列リソースとして扱えないのが、便利でもあるが面倒だなと感じた。)

そういう時に、下記の記事で、WPFではリソースディクショナリを使ってもローカライズできるということを知った。

nishy-software.com

同じやり方がWinUI3でもできるんじゃないか?と思ったので、やってみる。

結果

WinUI3でもできた。
が、それがMSの想定するやり方なのか?は、ぱっとドキュメントを見つけられなかった。

とりあえず、やったことをメモっておく。

前提

  • WinUI3
  • WindowsAppSDK 1.2.221109.1
  • .NET6
  • VisualStudio2022 17.4.3

やり方

はじめに

以前調べた、reswを使ったローカライズと、共通する手順も多いので、そちらも参照。

手順

このテンプレートを使って、WinUI3プロジェクト作成する。

パッケージプロジェクトに含まれているPackage.appxmanifestのコードを開いて、使う言語の設定を行う。

<Resources>
    <Resource Language="x-generate"/>
</Resources>

となっている部分を、

  <Resources>
    <Resource Language="ja-JP"/>
    <Resource Language="en-US"/>
  </Resources>

と直す。

下記のような感じで、プロジェクトの下に、en-USja-JPフォルダを作成する。

作ったフォルダの中に、リソースディクショナリをそれぞれ追加する。
フォルダを右クリック⇒追加⇒新しい項目、で、下図のリソースディクショナリを選んで追加する。

今回は「StringDictionary.xaml」という名前にした。

追加したリソースディクショナリを、画面のxamlから見えるようにするために、App.xamlに、リソースディクショナリを登録するコードを入れる。

App.xamlに、下記の「★ココ!★」の部分を追記する。

<Application
    x:Class="WinUI3ResTest.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:WinUI3ResTest">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
                <!-- Other merged dictionaries here -->
                <ResourceDictionary Source="StringDictionary.xaml"/> <!-- ★ココ!★ -->
            </ResourceDictionary.MergedDictionaries>
            <!-- Other app resources here -->
        </ResourceDictionary>
    </Application.Resources>
</Application>

リソースディクショナリに、ローカライズしたい文言を書く。

今回は、下記のようにした。

en-USフォルダの中のStringDictionary.xaml

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <x:String x:Key="TestString1">aiueo</x:String>
    <x:String x:Key="TestString2">kakikukeko</x:String>
</ResourceDictionary>

ja-JPフォルダの中のStringDictionary.xaml

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <x:String x:Key="TestString1">あいうえお</x:String>
    <x:String x:Key="TestString2">かきくけこ</x:String>
</ResourceDictionary>

※地味にハマったポイント

VisualStudioからリソースを追加した直後は、私の環境だと、StringDictionary.xamlSJISになっていて、これを使って画面表示させると、文字が化けて表示されてしまった。
それでは困るので、今回は手動(ほかのエディタを使って)UTF-8にしてやると、文字化けせずに表示された。

作ったリソースディクショナリを使う画面を作る。
今回は試しに下記のようにした。

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

    <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
        <Button x:Name="myButton" Click="myButton_Click" Content="{StaticResource TestString1}"></Button>
    </StackPanel>
</Window>

ボタンの中の文字が、リソースディクショナリに書いた文言になる想定。

日本語、英語で動かすと、下図のようになった。

英語

日本語

コードからリソースを取りたいとき

別記事で試したが、下記で取れる。
(今の言語に対応したリソースを取ってくれる)

            var rc = (string)Application.Current.Resources["TestString1"];
            Debug.WriteLine(rc);

余談

ローカライズの簡易テストのときに、WPFのときにコードに仕込んで言語を切り替えていた

CultureInfo.CurrentUICulture = new CultureInfo("ja-JP", false);

が、WinUI3のパッケージ版では効かなかった。
(実際にWindouwの言語設定を変えないと、言語が切り替わってくれなかった)

なにか違うのかも...

参考

以前調べた、reswを使ったローカライズ記事(WinUI3)

https://tera1707.com/entry/2022/03/24/224855

以前調べた、Propertiesを使ったローカライズ記事(WPF/.net Framework)

https://qiita.com/tera1707/items/fb6570f3894a607f9dce

以前調べた、ResourceDictionaryを使ったローカライズ記事(WPF)

https://tera1707.com/entry/2023/01/11/233227

UIの現在の表示言語設定をコードから変更する

もくじ
https://tera1707.com/entry/2022/02/06/144447

やりたいこと

以前の記事で、多言語対応のやり方を調べたが、 多言語対応の動作確認をするときに、実際にWindowsの言語の設定を変えてテストいようとすると、一度サインアウトしないといけなかったりしてとてもめんどくさい。

画面上の見た目がどうなるか程度にテストするときに、なにか簡易的なやり方がないか?

やりかた

CultureInfo.CurrentUICultureに、設定したい言語のカルチャーをセットする。

サンプルコード

とりあえず、アプリ起動直後に、CultureInfo.CurrentUICultureに、設定したい言語のカルチャーをセットする。

CultureInfo.CurrentCultureではなくCultureInfo.CurrentUICulture

using System.Globalization;
using System.Windows;

namespace CultureTest
{
    public partial class App : Application
    {
        protected override void OnStartup(StartupEventArgs e)
        {
            CultureInfo.CurrentUICulture = new CultureInfo("ja-JP", false);
            base.OnStartup(e);
        }
    }
}

これで、以前の記事でやっていた方法でのローカライズでは、簡易的に言語ごとのリソースが切り替わってくれた。

参考

Properties(resx)を使ったローカライズ

https://qiita.com/tera1707/items/fb6570f3894a607f9dce

ResourceDictionaryを使ったローカライズ

https://tera1707.com/entry/2023/01/11/233227