sh1’s diary

プログラミング、読んだ本、資格試験、ゲームとか私を記録するところ

Buffalo EasyMesh WiFi は1つの回線に複数設定しない(注意点)

家の WiFi 環境を EasyMesh にするときの注意点を知ったのでメモ。(知っている人からすると当たり前のことだと思うのですが)基本的には、EasyMesh は1回線につき1つの EasyMesh のみを設定すること、という記録です。

この記事の内容は 2024 年 2 月現在の状況です。WiFi はまだまだ移り変わるので事情が異なる恐れもあります。

下記の記事の動作テストは、次の機種を利用しました

WIFI

  • WSR-3200AX4S-BK
  • WEX-1800AX4EA

コントローラーの条件

たとえば、1階と2階で別々の EasyMesh を構築したいと思ってもそれはやらないほうがよいです。これは「Baffalo - EasyMesh よくある質問」に答えがあり、コントローラー(easymesh wifi 親機)は1台だけ、とあります。1階と2階に別々のコントローラーを設置することはできないので、よくない設定ということです。

この設定で次のトラブルが発生したため、注意が必要です。

トラブルの実例

EasyMesh のコントローラー(親機)は1台まで、というのは、詳細設定の EasyMesh 機能を「使用する」のチェックマークは1台だけしか(コントローラーに対しては)有効にしてはいけないです。ちなみに、この機能はデフォルトで ON です

仮に EasyMesh を使用しないコントローラー(WiFi 親機)も EasyMesh 機能を「使用する」のままにしていると EasyMesh を使用するコントローラーの調子が悪くなります。具体的には以下の症状が発生しました:

  • EasyMesh のコントローラー(親機) -> エージェント(子機)の接続設定はできるが、数メートルでエージェントの EasyMesh が途切れる
  • しばしばコントローラー(親機)の WiFi 通信に異常が発生する(機器の物理的な再起動で、コントローラーは再度 WiFi の利用ができる状態になってしまう)

また、EasyMesh はコントローラー(親機)1台に対して4台のエージェント(子機)しか接続できない、という物理的な制約があります。

WiFi のコントローラーは、ほとんどの場合1台で満足できるのですが、たとえとして3階の建物のケースを考えてみます。各階に2台ずつ設置しようとします。この場合は、合計6台が必要となってしまうので制約上不可能です。そこで1階だけ別の WiFi を設置することにします。すると、Buffalo のコントローラーはデフォルトで EasyMesh が ON なので、このまま運用すると全体の WiFi ネットワークを狂わせてしまうことになります。

なので、Buffalo の EasyMesh で構築できる網は建物の大きさによって物理的な限界があるようです。また物理的な1回線に対して1つの EasyMesh というのは2重ルーター(多重 NAT)も動作保証外だと「Buffalo チャットサポート」で回答を得ています。

対応策

EasyMesh を使用しないコントローラー(親機)は、詳細設定の EasyMesh 機能を「使用する」のチェックマークを OFF にしましょう。OFF にしたあとは、すべての WiFi を物理的に再起動しましょう。(再起動しないと EasyMesh はおかしいままでした)

また、コントローラー設定が正しく設定できている環境下で、エージェント(子機)の接続設定を再度実施しましょう。すでに接続設定をしてしまっている場合は、リセットをして再設定することをおススメします。(WEX-1800AX4EA は、再設定することで EasyMesh の通信が安定しました)

EasyMesh WiFi を構築する際は、建物に対してコントローラーとエージェントの最大(1+4)5台をどのような位置に設置していくのか、この構想が重要だと思いました。

WiFi は(スマートフォンの影響もあって)技術更新がはやい領域だと思うので、また5年~10年過ぎると全然違う事情になっているのだとは思いますが。

参考

WPF JumpList の使い方について

WPF のアプリを作っているときに JumpList を使おうと思ったときに、システムトレイアイコンのメニューとの使い方の違いを再認識しました。その内容をメモ。

JumpList

JumpList は Windows 7 から追加された windows の機能です。

JumpList とシステムトレイの右クリックメニューは、根本的に用途が異なります。JumpList は基本的にプログラムの起動を促すうごきをします

タスクバーにピン留めするという場合もあって、わかりやすい例だとアプリが実行されていない状態から JumpList を実行するケースもありえます。なので、実行中のアプリに対しての命令をメニューに含めようとすると不都合の生じる恐れがあります。

システムトレイのアプリケーションは、基本的にアプリケーションは(バックグラウンドかもしれませんが)実行状態であり、インスタンスがあります。なので、実行中のアプリに対しての命令をメニューに含めることが可能です。

.NET になった現在だと下記のコードで動きました。

var jumpList = new System.Windows.Shell.JumpList();
var jumpTask = new System.Windows.Shell.JumpTask()
{
    Title = "hello task",
};

jumpList.JumpItems.Add(jumpTask);
jumpList.Apply();

PresentationFramework.dll で動いているので初期の頃は dll の参照追加が必要だったり構築方法が変化する中で面倒な印象が残っています。現在は、特になにもしなくても JumpList を追加することができました。

簡単な機能なのでファイルを読み込む機能があり、履歴のような機能を追加するなら検討の余地がありそう。

使い方の例

Visual StudioVisual Studio Code も基本的には、ファイルを開く用途で使用しています。

ブラウザになると、特定のページを開くアクションをします。アプリケーションの起動+ページを開く動作なので上手い応用だと思いました。

一方で Notion のようなアプリケーションだと JumpList に対応していないのか、アプリケーションが不向きなのかはわかりませんが、利用がありません。なので必ずしも対応しなければいけない機能かといえば、そうでもなさそうです。

参考

WPF ToggleButton 注意点の整理

タイトルのとおり、WPF のコントール ToggleButton の使い方についてのメモです。ToggleButton は、通常のボタンに加えて IsChecked プロパティを持つコントールです。

IsChecked プロパティの動作で注意しないと VM 上で困ることがあったのでメモ。

ToggleButton のデザイン

ToggleButton にアイコンを表示したいときは XAML 上で Context に設定するのが手早い。このとき ToggleButton の色が変化する場合 Context に設定したコントールも色が変化しないと困る。

<ToggleButton x:Name="Toggle">
    <ui:SymbolIcon x:Name="Icon"
                    Padding="0" Margin="0"
                    Symbol="Edit32" 
                    Foreground="{Binding ElementName=Toggle, Path=Foreground}"
                    Filled="{Binding ElementName=Toggle, Path=IsChecked}">
    </ui:SymbolIcon>
</ToggleButton>

Foregournd と Filled を変更すると以下のようになって、ちょうどよいかもしれない。

ToggleButton のイベント

Clicked イベントを Command 化してもよいのですが IsChecked が ON/OFF のときで、それぞれ呼び出すイベントを切り替えたほうが便利。EventTrigger を利用した例を示す。

コード上で IsChecked を調べて if 文で2つで分けるのも同じだけど、メソッド名で整理できるだけ有利だと思う。

<ToggleButton>
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Checked">
            <prism:InvokeCommandAction Command="{Binding OnCommand}" />
        </i:EventTrigger>
        <i:EventTrigger EventName="Unchecked">
            <prism:InvokeCommandAction Command="{Binding OffCommand}" />
        </i:EventTrigger>
    </i:Interaction.Triggers>
</ToggleButton>

コード上からの注意点

ToggleButton を ON や OFF に切り替えたいとき、Checked イベントや Unchecked イベントに Binding した各種コマンドを実行するだけだと、ビューのビジュアル上で整合性がとれなくなる。

OnCommand?.Invoke(); // IsChecked が切り替わらない

理由は、Toggle の ON/OFF のカラーが切り替わらないため。コード上から ON/OFF の切り替えを指示したいときは IsChecked プロパティを直接変更する必要があると思います。

<ToggleButton IsChecked="{Binding IsChecked}">
</ToggleButton>
IsChecked = true or false;

個人的に ToggleButton はあまり使わないこともあって ON/OFF 時のビジュアル変化が VM 上の不整合を起こすミスをしてもエラーにならないのは困る。仕様的なものなので、アプリケーションがクラッシュするようなバグにならないので気づくのに遅れる恐れがあると思う。

余談:添付プロパティで対応できるか?

過去の .NET Framework 3.5 ごろの WPF における ToggleButtonIsChecked は Binding に問題があったようです。そのせいか、私も Binding できないような記憶があって、別の方法を考えていました。

仮に添付プロパティで対応できないか検討します。(検討した内容を記録しただけの余談です)

これは問題があって、添付プロパティの値が切り替わったときにしか callback メソッドを実行できない。なので、初期化で苦労することになります。(添付プロパティは初期化メソッドの実行が無い!)

添付プロパティの値を Binding しておき Checked と Unchecked イベントが発生したときに添付プロパティの値を自動的に更新できるようにすればよいと思ったけど、初期値を true, false どちらの値でもよい、と考えるとイベントの割り当て初期化をどうするか、という話になる。

もうひとつ IsAttached を true にするような添付プロパティを用意しておけば、対応できるが冗長すぎる仕様になってしまう。個人的に添付プロパティで IsChecked を操作する意味はあまり無いと思った。

参考

C# TimeProvider の利用について (.NET8)

この記事は「Qiita - C# Advent Calendar 2023」に参加しています。

C# には、時間を表現するクラスに DateTimeDateTimeOffset があります。.NET 8 から TimeProvider クラスが新しく用意されました。

TimeProvider クラスは .NET8 の新機能のひとつ「時間抽象化 (Time abstraction)」として紹介されています。時間の抽象化は、コードテストの課題に新しいメリットがあります。その内容を記録した記事です。

補足:TimeProvider は、主にテストのためのものです。一例ですが Issues に書いてあった内容は、以下のとおり。

The abstraction does one thing and one thing only: it makes it possible to test the flow of time. That's it.

DateTime.Now との違い

TimeProvider クラスは、現在時間を取得することができます。従来の DateTime クラスを使って現在時間を取得した場合と比較します。

public void Run(string[] args)
{
    var now = TimeProvider.System.GetLocalNow();
    var utcNow = TimeProvider.System.GetUtcNow();

    Console.WriteLine(now);
    Console.WriteLine(utcNow);

    var now2 = DateTime.Now;
    var utcNow2 = DateTimeOffset.UtcNow;

    Console.WriteLine(now2);
    Console.WriteLine(utcNow2);
}
2023/12/08 15:53:10 +09:00
2023/12/08 6:53:10 +00:00
2023/12/08 15:53:10
2023/12/08 6:53:10 +00:00

これだけだと違いはなさそう。テストコードから DateTime.Now を呼び出すケースと TimeProvider.System.GetLocalNow() を呼び出すケースだと、どのような違いが出るのか、そこがこの先の読み解きポイントになります。

ちなみに DateTimeProvider から取得した変数 now utcNow はどちらも DateTimeOffset 型です。なので GetLocalNow() でも +09:00 が出力されていますね。

補足:DateTime 型は Kind プロパティで UTC か Local かをチェックできます。

実装例

(現地時間の)正午(12時)かどうかをチェックするサービスクラスを実装してみます。(これはビジネス等の用途にあわせる部分なので、どんなものでも)

時間のチェックを目的としたクラスの中で、時間を用意して結果を返却するようにします。

もちろん TimeProvider を用意する箇所はどこかで必要だけれど、大体のシステムなら TimeProvider.System だけでもいい。これは仕様策定の際にもコメントがあったようです:

At the end of the day, we expect almost no one will use anything other than TimeProvider.System in production usage. Unlike many abstractions then, this one is special: it exists purely for testability.

public class TimeService
{
    private readonly TimeProvider _TimeProvider;

    public TimeService(TimeProvider timeProvider) => _TimeProvider = timeProvider;

    public bool IsNoon()
    {
        var now = _TimeProvider.GetLocalNow();

        return now.Hour == 12;
    }
}

DateTime を利用しても同じロジックは可能です。これはテストの際にどうなるでしょうか。

   public bool IsNoon()
    {
        var now = DateTime.Now;

        return now.Hour == 12;
    }

テストに利用する

今回の一番重要な部分です。

public class Tests
{
    public class NoonTimeProvider : TimeProvider
    {
        private readonly TimeSpan JST = new TimeSpan(9, 0, 0);

        public override DateTimeOffset GetUtcNow()
        {
            return new DateTimeOffset(2023, 12, 1, 3, 0, 0, JST);
        }
    }

    [SetUp]
    public void Setup()
    {
    }

    [Test]
    public void Test()
    {
        var testTimeProvider = new NoonTimeProvider();

        var testService = new TimeService(testTimeProvider);
        var isNoon = testService.IsNoon();

        Assert.IsTrue(isNoon);
    }
}

GetUtcNow を override することで、テスト用の時間を用意しています。テストのために抽象化するクラスの部分は、すこし嵩張るかもしれません。

もしも TimeService クラスで時間を取得する際に DateTime.Now を利用していたらどうでしょうか。上のコードの例では TimeProvider クラスを継承して GetUtcNow メソッドを override しましたが、DateTime クラスは seal されているので継承が難しく、また Now プロパティを override するというのも難しく、仮にどうにかできたとしてもモヤモヤする実装になりそう。

基本的には DateTimeDateTimeOffset は、(本来)ある時間を表現したデータ型のようです。DateTimeDateTimeOffset から新たに現在時間を問い合わせる Now といった機能は便利ですが、拡張しづらい。

テストコードを書く際に難があったので、今回の TimeProvider が出てきました。なお、注意点として TimeProvider クラスの GetLocalNow は override できません。なので、ここまでのテストコードは(次で説明をする)都合の悪い部分が残っています。もう一歩拡張します。

継続的インテグレーションの考慮

IsNoon メソッドは、メソッドの中で GetLocalNow を利用しているため、UTC 現地時間の補正が加/減算されます。(日本だと +9:00 のようなこと)なので、(ここまでのコードだと、テストコード実行者が複数いて、複数の国に拠点がある場合)テストの実行環境によって成功したり失敗したりする恐れがあります。

これに対応したコードは次のようになります。

public class Tests
{
    public class NoonTimeProvider : TimeProvider
    {
        public override DateTimeOffset GetUtcNow()
        {
            return new DateTimeOffset(2023, 12, 1, 3, 0, 0, TimeSpan.Zero);
        }

        public override TimeZoneInfo LocalTimeZone => 
            TimeZoneInfo.FindSystemTimeZoneById("Tokyo Standard Time");
    }

    [SetUp]
    public void Setup()
    {
    }

    [Test]
    public void Test()
    {
        var testTimeProvider = new NoonTimeProvider();

        var testService = new TimeService(testTimeProvider);
        var isNoon = testService.IsNoon();

        Assert.IsTrue(isNoon);
    }
}

TimeProviderタイムゾーンを変更し、テストコード中は固定することができます。FindSystemTimeZoneByIdタイムゾーンの ID を指定する必要があります。

上の例だと、テストコードを東京の UTC 時間に設定しています。これでテスト実行環境差を抽象化することができました。「時間の抽象化」という TimeProvider クラスの機能を活用できた、という話なんだと思います。

ここまでの話から DateTimeDateTimeOffset で「時間の抽象化」をやろうとすると、よりテスト用のモックが肥大化することが予想できると思います。(ローカル時間だけならいいのかもしれない)

今後は(時間に関しては).NET8 の TimeProvider クラスを活用したテスト用のモックを作るようにしたほうが良いケースが多そうです。

時間の扱いは細かい話が多く、例えばサマータイムの扱いには注意が必要です。OS でサマータイムを処理している場合もありますが、企業による取り組みのような機械的に処理されないケースもあるし、人の都合にあわせて改定されることもあります。(こうした際も TimeProvider クラスは、有効に機能するはず)

ITimer インターフェース

ITimer インターフェースは次のとおり。

public interface ITimer : IDisposable, IAsyncDisposable
{
    bool Change(TimeSpan dueTime, TimeSpan period);
}

以下のようなサンプルコードの提示が「.NET8 の新機能」にあるのですが、どういうことを説明しているのかよくわかりませんでした。

// Create a timer using a time provider.
ITimer timer = timeProvider.CreateTimer(
    callBack, state, delay, Timeout.InfiniteTimeSpan);

// Measure a period using the system time provider.
long providerTimestamp1 = TimeProvider.System.GetTimestamp();
long providerTimestamp2 = TimeProvider.System.GetTimestamp();

var period = GetElapsedTime(providerTimestamp1, providerTimestamp2);

ITimer を利用している例は、現在のネット上だと FakeTimeProvider を利用したものが見つかります。NUnit の場合はちょっとひと手間が必要になるように思います。

サンプルは以下のとおり。

CreateTimer は dueTime の後に period の間隔で state を引数としたコールバックを発生させる。一度だけ発生させる場合は Timeout.InfiniteTimeSpan(周期的なコールバック無効)を設定して dueTime を実行時間にする。

テストの内容が1時間とか2時間といった長時間の経過を見ないとわからない場合、時間を抽象化することで時間を進めてテストを実装します。現実に時間の経過を待つのがめんどくさい、現実的ではない。そういった際に FakeTimeProviderAdvance というメソッドで時間を進めることができます。

timeProvider.Advance(TimeSpan.FromHours(1));

イメージとしてはこんな感じだと思います。

[Test]
public void TestITimer()
{
    var testTimeProvider = new FakeTimeProvider();
    var result = false;

    var itimer = testTimeProvider.CreateTimer(
        _ =>
        {
            // state は null なので _ にしている
            // 時間経過後にすること
            result = true;
        }, 
     state: null, 
     dueTime: TimeSpan.FromSeconds(1000),
     period: Timeout.InfiniteTimeSpan);

    testTimeProvider.Advance(TimeSpan.FromSeconds(1000));

    Assert.True(result);
}

参考

2023 年 会社勤めの買ってよかったモノ、まとめ

12月の風物詩。毎年やっているけど、買って損していないのかチェックみたいな気もする。

例年実施していた購入物の写真は後日追加できれば

QOL 関係

小型冷蔵庫

ふるさと納税で購入。作業部屋の飲み物やお菓子を冷蔵庫に入れたい。整理整頓にもなるしこのほうがいいか、ってなりました。(残りは今年もタオルを買って備品を更新)

キーボード

キーボードが1つ壊れてしまったのと、予備キーボード「バッファロー “SAVIOR” BSKBC02BK」も10年以上が経過して物理的に劣化していたのがわかったので、まとめて更新しました。

ここ数年は Stingray のフラットなキーボードが馴染む感じだったので、予備のキーボードも KEY PALETTO に。丈夫そうだしね。

文具

デスクにペン立てとブックエンドを追加。シンプルなものでも良かったんだけど、アクセントに。結構かわいい。

レバーレスコントローラー作成

つぎの記事で、レバーレスコントローラーを作って遊んでいました。

これは結構たのしくて、来年もすこし継続しそう。正直、ゲームもそこそこに DIY を遊んでいる状態です。このあたりから、3Dプリンターの使い方もわかりはじめて、家電の小さな部品が破損した際に、壊れたパーツを(ジェネリック部品として)プリントしたりできました。

PC 関係、スマートフォン関係

外付けハードディスクと内蔵 SSD を買ったのは、今年はローカルに AI を導入して遊んでみる機会があったからです。AI 関係はすごい進歩があった一方で、23年に入ってからは、(個人的な感覚として)22年から加速度的な進化を遂げるほどでもなく、触ってみると(学習の)器用な部分と不器用な部分があり万能感はまだ無いよね、って思いました。

ただ、AI を使える/使えないの差はあると思うようになって、その違いは触ったことがあるかないかの有無ではなくて、AI の癖を知っていることや、プロンプトエンジニアリングのような部分になると思いました。結局、使う人の技術が求められるモノだと思っています。

Anker のバッテリーは野球観戦のために更新したものですね。

運動

今年はあんまり買い足していないことがわかりました。

去年購入した「IRONMIND キャプテンズ・オブ・クラッシュ ハンドグリッパー」は、暇なときは潰して遊んでいるのですが(54kg)柔らかくなってしまったのか、力が付いたのかどっちなのだろう。

結ばない靴ひもは想像以上に便利でよかった。本気でランニングする人には向かないんだろうけど、ちょっとしたスニーカー、運動靴くらいならこれでいいと思う。通常の靴紐より馴染むと思った。

今年は WBC にあわせて、去年の終わりから野球知識をアップデートしてました。WBC はもちろん最高だったし、甲子園まで応援に行ったりもして阪神を応援していたところ、こっちでも最高の形となり満足度の高すぎるイベントでした。(優勝記念品も購入した)いっぱいの人で、甲子園すごかった。

本は、他に古本でちょこちょこ仕入れたのと会社での購入もあって、自分で買った新品は少ない。

プログラミングに関して、今年は Blazor と WinUI3 の学習のさわりを始めることができた。Blazor はちょっと継続するか微妙なところだが、WinUI3 は今もモチベーションがあるので楽しんでいきたい。

総括

今年も成長を感じることができず、だらだらと時間を過ごした印象があります。ある意味で、プライベートの趣味(ゲーム、アニメ、DIY 等)が充実していたので幸福ではありました。こうした空いた時間が多い人生を大切にしたい一方で、どこか物足りなさも否めません。(悩ましい)

2022年は「投資」をすこし真面目に(真剣と言うには烏滸がましい)やりはじめて、すこし金銭感覚を覚えだした年だったと思います。

2023年は継続して投資を続けています。そこまで追加した金額は大きくなくて、配当目的で NISA に補填した程度でした。(サイバーエージェントが下がったので ABEMA 年間パス用など安価なのを少し)来年は NISA もアップデートされるので、このあたりは引き続き「投資」を怠りなく続けていきたい。リターンの考え方については、継続して勉強を進めています。

今年は、去年よりも自分がどの程度「リスクのあるポートフォリオである」ということの理解の解像度を上げて、利益を得ることができたと思います。

2023年12月1日現在、2023年1月ごろと比べると米株、日本株共に(今は)良くなっているので、約200万円くらいの利益になっています。うまく行き過ぎたと思うので、実際はあとでもう少し下がるんじゃないかなぁ、と思っています。

ビジネス面では、今年は2022年とは違って、あまり忙しくありませんでした。予想したとおりではありました。

ここのブログ記事は自己投資みたいなものなので、忙しくないなら記事を増やせばいいのですが、そこは上手くいきませんでした。(投資の勉強であったり SF 小説やライトノベルをたくさん読んでいました)Unity の記事はすこしだけですが、書くことができたのはよかったです。

ただ Unity は(株価を見てもわかるとおり)評判を落としつつあります。この先、生き残ることができるのか疑問を感じつつあります。C#バージョンアップの追従も悪く、根本的に鈍重になっている現状で、評価が下がる中での料金改定はさすがに不味かった。

来年は(今年よりは)ビジネス面で忙しくなりそう。日常を変えてみたいと思う一方で、今の現状に満足しているところもあり、ないものねだりなのか。

とにかく、来年も来年で満足できる2024年にできるように、取り組んでいこう。運動 or 投資 or WinUI3 あたりかな?

年末年始で友達ともよく相談することにしよう。

履歴

C# なるべく高精度な Timer を使う

Windows において、高精度なタイマーを実装するのは 2000 年以前から難しい(困難)ことで知られています。

ゲーム開発においては 60/30 fps でゲームを動かすので、そのためによくタイマーを利用しますが、諸々の理由でなるべく高精度なタイマーであるほうが望ましいケースもある。(格闘ゲーム音楽ゲームは理由をイメージしやすい)

そんなわけでなるべく高精度なタイマーをどのようにして利用するのか記録した記事です。

C# の Timer 事情

  • System.Timers.Timer
  • System.Thereading.Timer
  • System.Windows.Forms.Timer
  • System.Windows.Threading.DispatcherTimer

このように4つもタイマーが存在している。各タイマーの違いは下記サイトに詳しいです。

ざっくりと下2つは GUI 用途で精度が(比較して)低い。上2つは、ほとんど差が無いようです。精度的には 16ms くらいまで、という説明になっていますが、これは疑問もあってバラつきが報告されています。

ここでは OS の負荷によって sleep の待機時間が揺らぐため、安定したいなら timeSetEvent はどうか、というコメントがつきました。

このような感じで高精度なタイマーを使いたいなら、System.Timers.Timer or System.Thereading.Timer を利用するというのが基本になって、もうすこし深く掘るなら timeSetEventtimeGetTime といった Windows マルチメディアのタイマーを利用する形になると思います。

今回は、どうせなので Windows マルチメディアのタイマーを利用してみました。

補足:自分で sleep を利用したタイマーを作る場合

C#Sleep() は reference を参照すると以下のようになっています。具体的な実装は見えません。

 /*=========================================================================
  ** Suspends the current thread for timeout milliseconds. If timeout == 0,
  ** forces the thread to give up the remainer of its timeslice.  If timeout
  ** == Timeout.Infinite, no timeout will occur.
  **
  ** Exceptions: ArgumentException if timeout < 0.
  ** ThreadInterruptedException if the thread is interrupted while sleeping.
=========================================================================*/
[System.Security.SecurityCritical]  // auto-generated
[ResourceExposure(ResourceScope.None)]
[MethodImplAttribute(MethodImplOptions.InternalCall)]
private static extern void SleepInternal(int millisecondsTimeout);

[System.Security.SecuritySafeCritical]  // auto-generated
public static void Sleep(int millisecondsTimeout)
{
    SleepInternal(millisecondsTimeout);
    // Ensure we don't return to app code when the pause is underway
    if(AppDomainPauseManager.IsPaused)
        AppDomainPauseManager.ResumeEvent.WaitOneWithoutFAS();
}

stack overflow を参考にすると以下など。

結局 Sleep() も数 ms の動作を保証し続けることは難しい。「C#マイクロ秒レベルの高精度Timer」の内容になると思います。

Windows マルチメディアのタイマー実装の注意

具体的な話になるが timeBeginPeriod を設定したほうがよい。下記のように timeBeginPeriod の値を固定値にしているサンプルコードもあるが、PC のスペックにあわせて timeGetDevCaps で情報を取得したほうが丁寧だと思います。

古い情報だと timeBeginPeriod 自体、本当に動作に影響を与えているのか、といった情報もありますが、これはさすがにもう十分に反証と訂正がされているといっていいでしょう。

補足すると、timeBeginPeriod は windows10 2004 以前は、OS 全体に影響を及ぼす仕様だったようです。(下記 Microsoft Learn に詳細あり)

上述の(固定値)記事は 2007 年のものなので、取り扱いには下記のとおり注意が必要だった。コピペ実装は怖いよねという話。

なので、Windows 10 ver 2004 以降での利用とするほうが OS の挙動としては親切です。TargetFramework を指定しておきましょう。

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net7.0-windows10.0.19041.0</TargetFramework>
    <UseWPF>true</UseWPF>
  </PropertyGroup>
</Project>

or

[SupportedOSPlatform("windows10.0.19041.0")]

私の今の考えは Windows98-2000 世代だと、timeBeginPeriod を必要するようなゲームは、フルスクリーンで遊ぶものだったのでギリギリ仕様として想定/運用できたように思います。それでも、注意の言及は無かったわけでもない。凝り性の人は、やっぱり調べていますし、Windows 10 まで修正が無かったというのは(私の意見からすると)さすがに遅い対応だとも思います。 > コンストラクタで timeBeginPeriod し、デストラクタで timeEndPeriod するようなプログラムを組んではいけないのである。(日記より)

実装するときの P/Invoke

下記のとおり api を利用することができます。

#region P/Invoke

[StructLayout(LayoutKind.Sequential)]
public struct TIMECAPS
{
    public UInt32 wPeriodMin;
    public UInt32 wPeriodMax;
};

private const int TIMERR_NOERROR = 0x00;
private const int TIMERR_BASE = 0x60;
private const int TIMERR_NOCANDO = TIMERR_BASE + 0x01;
private const int TIMERR_STRUCT = TIMERR_BASE + 0x21;
private const int TIME_ONESHOT = 0x00; // イベントを1度だけ実行
private const int TIME_PERIODIC = 0x01; // イベントを繰り返し実行

private delegate void LPTIMECALLBACK(uint uTimerID, uint uMsg, ref uint dwUser, uint dw1, uint dw2);

[DllImport("winmm.dll", SetLastError = true, EntryPoint = "timeGetDevCaps")]
static extern uint __TimeGetDevCaps(ref TIMECAPS timeCaps, uint sizeTimeCaps);

[DllImport("winmm.dll", SetLastError = true, EntryPoint = "timeSetEvent")]
private static extern uint __TimeSetEvent(uint uDelay, uint uResolution, LPTIMECALLBACK lpTimeProc, ref uint dwUser, uint fuEvent);

[DllImport("winmm.dll", SetLastError = true, EntryPoint = "timeKillEvent")]
private static extern uint __TimeKillEvent(uint uTimerId);

/// <summary>
/// システム時刻をミリ秒単位で取得
/// </summary>
/// <returns>システム時刻をミリ秒単位</returns>
[DllImport("winmm.dll", EntryPoint = "timeGetTime")]
private static extern uint __GetTime();

/// <param name="uMilliseconds">アプリケーションまたはデバイス ドライバーの最小タイマー解像度 (ms)</param>
/// <returns>TIMERR_NOERROR を返却</returns>
[DllImport("winmm.dll", EntryPoint = "timeBeginPeriod")]
private static extern uint __BeginPeriod(uint uMilliseconds);

/// <summary>
/// 以前に設定した最小タイマー解像度をクリア
/// </summary>
/// <param name="uMilliseconds">前回の呼び出しで指定された最小タイマーのクリア</param>
/// <returns>TIMERR_NOERROR を返却</returns>
[DllImport("winmm.dll", EntryPoint = "timeEndPeriod")]
private static extern uint __EndPeriod(uint uMilliseconds);

#endregion

具体的な C# のクラスにしたケースは、下記のサンプルを参考にしてみてください。

注意点を挙げると、timeSetEvent を使用する際は LPTIMECALLBACK コールバック関数を(クラス内などで)インスタンスを保持しておかないと、GC が働いて ExecutionEngineException の例外が発生してしまう。

ダメなコードを例示するとこんな感じ。LPTIMECALLBACK の部分を匿名関数のようにして解決していると、タイマーで待機している間にメモリー上から失われてしまいます。(GC が解決をするタイミング次第で、匿名関数部が動いたり動かなかったりするはず)

uint dwUser = 0;

_TimerID = __TimeSetEvent(interval, resolution, (uint uTimerID, uint uMsg, ref uint dw, uint dw1, uint dw2) =>
{
    // なにかの処理
}, ref dwUser, TIME_PERIODIC);

なので、下の例示のように _TimeCallback のような変数にして、クラス変数などにしてコールバック関数が動作する間はインスタンスを持ち続ける必要があります。使い終わったらインスタンスを手放すといった対応は、C言語のポインターに近いと思います。

uint dwUser = 0;

_TimeCallback = (uint uTimerID, uint uMsg, ref uint dw, uint dw1, uint dw2) =>
{
    // なにかの処理
};

_TimerID = __TimeSetEvent(interval, resolution, _TimeCallback, ref dwUser, TIME_PERIODIC);

太古の情報

Windows マルチメディアタイマーは、2000 年以前から存在するようで、ネット上の古い日記にも言及がありました。

あとの日記で こちらもおススメされていました。現在もメンテナンスがされていて「ゲームの VSYNC 問題とその対策」になっています。

なので、timeGetTime() といったマルチメディアタイマーは(現在も歴史的にも)高精度なほうだと評価されているようです。

パフォーマンスカウンターも同様に評価されており、環境によってどちらがよいのか甲乙は調べないとわからない。使い分けになる。

やねうらおさんは、YaneuraoGameSDK を公開しており、FpsTimerGameTimer といったクラスを含んでいます。こちらも参考になるかもしれません。(2007 年の SDK なのでもう古いのと、SDL を利用しているので Sleep() のように見えない部分もある)

サンプル

GitHub に今回のタイマーをクラス化したサンプルを掲載しています。

  • MultimediaTimer
  • ElapsedEventArgs

基本的には System.Timers.Timer と同じような使い方にしています。

HeritageLibrary.Windows

参考

WinUI3 App.xaml (App.xaml.cs) をカスタマイズする

 

App.xaml は全体で使うリソースを宣言するファイルで、App.xaml.cs は(主に)アプリケーションのエントリーポイント。

WPF でも WinUI 3 でもあるこのファイルですが、微妙に内容は異なっていて、Prism のようなライブラリでは App.xaml をカスタマイズすることがあります。

App.xaml をカスタマイズするメリットを挙げると、(Prism ライブラリは)App.xaml を書き換えることで DI をサポートする、MVVM に対応するといった機能を提供するようになります。

また、WinUI 3 は、App クラスに(WPF でいう)OnExit に対応するメソッドが無かったりするので、個人的にカスタマイズするメリットは(他プロジェクトと比較して)高くなってしまったように思いました。WPF で利用していたライブラリ自体がまだ対応してなかったりするためです。

独自に App.xaml および App.xaml.cs をカスタマイズしようとすると、どうやって記述するのだろう、といった内容をメモする記事です。

以下は、WinUI 3 のプロジェクトで実験していますが、WPF でも基本的な部分(考え方)は同じです。実験的なコードなので、改良する点は多いと思います。

ファイル構成(App.xaml

  • App.xaml
  • AppEx.cs (AppExBase.cs を継承)
  • AppExBase.cs

AppEx AppExBase の2つのクラスを新たに用意して、AppEx クラスは AppExBase クラスを継承します。

App.xaml は以下のように修正しました。

<?xml version="1.0" encoding="utf-8"?>
<local:AppEx
    x:Class="Sample.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:Sample">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
                <!-- Other merged dictionaries here -->
            </ResourceDictionary.MergedDictionaries>
            <!-- Other app resources here -->
        </ResourceDictionary>
    </Application.Resources>
</local:AppEx>

ルートのタグを local:AppEx に変更しただけです。

local:AppExx:Class で指定している App.xaml.cs が、コードビハインドで対応しています。(結びつきます)

AppExBase クラスの実装

using Microsoft.UI.Xaml;

namespace Sample;

public abstract class AppExBase : Application
{
    public AppExBase()
    {
    }

    protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
    {
        OnInitialize(args);
    }

    protected abstract Window CreateShell();
    protected abstract void OnInitialize(Microsoft.UI.Xaml.LaunchActivatedEventArgs args);
    protected abstract void OnExit(WindowEventArgs args);
}

AppExBase は Application クラスを継承しているので、一般的な App.xaml.cs とクラスの継承関係、OnLaunched の実行(WPF では StartUp)といった処理の構成が、同じ形で整えています。

本来であれば、OnLaunched でウィンドウを表示するのですが、具体的な実装は抽象メソッドに任せておきます。

AppEx の実装

using Microsoft.UI.Xaml;
using System;

namespace Sample;

public abstract class AppEx : AppExBase
{
    private bool _ContentLoaded;

    public AppEx()
    {
        InitializeComponents();
    }

    private void InitializeComponents()
    {
        if (_ContentLoaded)
        {
            return;
        }

        _ContentLoaded = true;

        var resourceLocator = new System.Uri("ms-appx:///App.xaml", System.UriKind.Absolute);

        Microsoft.UI.Xaml.Application.LoadComponent(this, resourceLocator);
    }

    protected override void OnInitialize(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
    {
        var window = CreateShell();

        if (window is null)
        {
            throw new NullReferenceException(nameof(window));
        }

        window.Closed += (sender, wargs) =>
        {
            OnExit(wargs);
        };

        window.Activate();
    }
}

AppEx は、カスタマイズしたい(主に初期化関係の)具体的な実装を施しています。CreateShellPrism のウィンドウの取得方法を真似して、App.cs に具体的なウィンドウ取得の実装を任せます。

WinUI 3 は、App クラスに(WPF 等における)OnExit に対応するメソッドが無いのですが、このように AppEx を介することで OnExit メソッドを(簡易的に)作成することができます。一度このコードを用意しておけば、簡易ライブラリとして利用することができるはずです。

InitializeComponents メソッドは、WPF や WinUI 3 のデフォルトで実装される InitializeComponent メソッドと同じことをしています。WPF だと以下のようなコードが App.g.i.cs などの自動生成させるコードファイルに用意されます。(一度確認してみるとよいと思います)

/// <summary>
/// InitializeComponent (MainWindow で自動生成されている例)
/// </summary>
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "7.0.12.0")]
public void InitializeComponent() {
    if (_contentLoaded) {
        return;
    }
    _contentLoaded = true;
    System.Uri resourceLocater = new System.Uri("/Sample;V1.0.0.0;component/views/mainwindow.xaml", System.UriKind.Relative);
    
    #line 1 "..\..\..\..\Views\MainWindow.xaml"
    System.Windows.Application.LoadComponent(this, resourceLocater);
    
    #line default
    #line hidden
}

App.xaml.cs の実装

通常でも(ほとんどの場合)コーディングすることになる App.xaml.cs は、App をカスタマイズしてきたゴールになるコードブロックです。

以下のコード部分に、カスタマイズすることで得られたメリットがないとダメ。または、ここにメリットがあるように作っています。

using Microsoft.UI.Xaml;

namespace Sample;

public partial class App
{
    private Window _Window;

    protected override Window CreateShell()
    {
        _Window = new MainWindow();

        return _Window;
    }

    protected override void OnExit(WindowEventArgs args)
    {
    }
}

こんな感じで App ファイルの中身をスッキリさせることができたり、OnExit メソッドを用意したりできた。

Prism に似せたいのなら、次のようなメソッドを override できるようにすればいいと思う。

  • RegisterTypes
  • ConfigureModuleCatalog

プロジェクトに全体 DI であったり、MVVM の仕組みを用意するのは便利なので App の工夫は、(同様のライブラリを利用できないなどの理由があるなら)メリットがあるように思いました。

Prism の App のコードや PrismApplication のコードを参考にしました。

参考