インターフェース・継承周りの実験①

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

やりたいこと

仕事で読むコードに「interfaceの明示的実装」が出てきたが、 以前理解したはずなのに、またアレ?となった。

もう忘れないようにまとめておきたい。

前提

  • VisualStudio2022
  • .NET6

コードと資料

コード

https://github.com/tera1707/InterfaceJikken

資料

https://github.com/tera1707/Siryou/blob/master/%E3%82%A4%E3%83%B3%E3%82%BF%E3%83%BC%E3%83%95%E3%82%A7%E3%83%BC%E3%82%B9%E3%83%BB%E7%B6%99%E6%89%BF%E5%91%A8%E3%82%8A%E3%81%AE%E5%AE%9F%E9%A8%93.xlsx

実験①

using System.Diagnostics;

namespace InterfaceJikken
{
    internal class Program
    {
        static void Main(string[] args)
        {
            {
                MyClass1 mc1 = new MyClass1();
                MyInterface1 mi1 = new MyClass1();

                //mc1.Method1_1();
                //mc1.Method1_2();
                //((MyInterface1)mc1).Method1_1();
                //((MyInterface1)mc1).Method1_2();
            }

            {
                MyClass2 mc2 = new MyClass2();
                MyInterface2 mi2 = new MyClass2();

                Debug.WriteLine("mc2");
                mc2.Method1_1();
                mc2.Method1_2();

                Debug.WriteLine("mc2 class");
                ((MyClass1)mc2).Method1_1();
                ((MyClass1)mc2).Method1_2();
                ((MyClass2)mc2).Method1_1();//不要なキャスト
                ((MyClass2)mc2).Method1_2();//不要なキャスト

                Debug.WriteLine("mc2 if");
                ((MyInterface1)mc2).Method1_1();
                ((MyInterface1)mc2).Method1_2();
                ((MyInterface2)mc2).Method1_1();
                ((MyInterface2)mc2).Method1_2();

                Debug.WriteLine("mi2");
                mi2.Method1_1();
                mi2.Method1_2();

                Debug.WriteLine("mi2 class");
                ((MyClass1)mi2).Method1_1();
                ((MyClass1)mi2).Method1_2();
                ((MyClass2)mi2).Method1_1();
                ((MyClass2)mi2).Method1_2();

                Debug.WriteLine("mi2 if");
                ((MyInterface1)mi2).Method1_1();//不要なキャスト
                ((MyInterface1)mi2).Method1_2();//不要なキャスト
                ((MyInterface2)mi2).Method1_1();//不要なキャスト
                ((MyInterface2)mi2).Method1_2();//不要なキャスト
            }
        }
    }

    internal interface MyInterface1
    {
        void Method1_1();
        void Method1_2();
    }
    internal interface MyInterface2 : MyInterface1
    {
        void Method2_1();
    }

    internal class MyClass1 : MyInterface1
    {
        public void Method1_1() => Debug.WriteLine(" Method1_1 of MyClass1");
        public void Method1_2()=> Debug.WriteLine(" Method1_2 of MyClass1");
    }

    internal class MyClass2 : MyClass1, MyInterface2
    {
        public new void Method1_1() => Debug.WriteLine(" Method1_1 of MyClass2");
        public void Method1_2() => Debug.WriteLine(" Method1_2 of MyClass2");
        public void Method2_1() => Debug.WriteLine(" Method2_1 of MyClass2");
    }
}

結果

mc2
 Method1_1 of MyClass2
 Method1_2 of MyClass2
mc2 class
 Method1_1 of MyClass1
 Method1_2 of MyClass1
 Method1_1 of MyClass2
 Method1_2 of MyClass2
mc2 if
 Method1_1 of MyClass2
 Method1_2 of MyClass2
 Method1_1 of MyClass2
 Method1_2 of MyClass2
mi2
 Method1_1 of MyClass2
 Method1_2 of MyClass2
mi2 class
 Method1_1 of MyClass1
 Method1_2 of MyClass1
 Method1_1 of MyClass2
 Method1_2 of MyClass2
mi2 if
 Method1_1 of MyClass2
 Method1_2 of MyClass2
 Method1_1 of MyClass2
 Method1_2 of MyClass2

※newを付けてくださいという警告(CS0108)が出ている状態と、newをつけて警告が出なくなった状態は、実行結果は同じ。(当たり前か)

実験② MyClass2の中のMethod1_1の実装を、MyInterface1の明示的な実装にしてみる

コード

using System.Diagnostics;

namespace InterfaceJikken
{
    internal class Program
    {
        static void Main(string[] args)
        {
            {
                MyClass1 mc1 = new MyClass1();
                MyInterface1 mi1 = new MyClass1();

                //mc1.Method1_1();
                //mc1.Method1_2();
                //((MyInterface1)mc1).Method1_1();
                //((MyInterface1)mc1).Method1_2();
            }

            {
                MyClass2 mc2 = new MyClass2();
                MyInterface2 mi2 = new MyClass2();

                Debug.WriteLine("mc2");
                mc2.Method1_1();
                mc2.Method1_2();

                Debug.WriteLine("mc2 class");
                ((MyClass1)mc2).Method1_1();
                ((MyClass1)mc2).Method1_2();
                ((MyClass2)mc2).Method1_1();//不要なキャスト
                ((MyClass2)mc2).Method1_2();//不要なキャスト

                Debug.WriteLine("mc2 if");
                ((MyInterface1)mc2).Method1_1();
                ((MyInterface1)mc2).Method1_2();
                ((MyInterface2)mc2).Method1_1();
                ((MyInterface2)mc2).Method1_2();

                Debug.WriteLine("mi2");
                mi2.Method1_1();
                mi2.Method1_2();

                Debug.WriteLine("mi2 class");
                ((MyClass1)mi2).Method1_1();
                ((MyClass1)mi2).Method1_2();
                ((MyClass2)mi2).Method1_1();
                ((MyClass2)mi2).Method1_2();

                Debug.WriteLine("mi2 if");
                ((MyInterface1)mi2).Method1_1();//不要なキャスト
                ((MyInterface1)mi2).Method1_2();//不要なキャスト
                ((MyInterface2)mi2).Method1_1();//不要なキャスト
                ((MyInterface2)mi2).Method1_2();//不要なキャスト
            }
        }
    }

    internal interface MyInterface1
    {
        void Method1_1();
        void Method1_2();
    }
    internal interface MyInterface2 : MyInterface1
    {
        void Method2_1();
    }

    internal class MyClass1 : MyInterface1
    {
        public void Method1_1() => Debug.WriteLine(" Method1_1 of MyClass1");
        public void Method1_2()=> Debug.WriteLine(" Method1_2 of MyClass1");
    }

    internal class MyClass2 : MyClass1, MyInterface2
    {
        void MyInterface1.Method1_1() => Debug.WriteLine(" Method1_1 of MyClass2"); // ★ココが①と違う
        public void Method1_2() => Debug.WriteLine(" Method1_2 of MyClass2");
        public void Method2_1() => Debug.WriteLine(" Method2_1 of MyClass2");
    }
}

結果

mc2
 Method1_1 of MyClass1 //★ココが①と違う → MyClass2の中で、MyInterface1のMethod1_1の実装をもう一回やってる感じということか
 Method1_2 of MyClass2
mc2 class
 Method1_1 of MyClass1
 Method1_2 of MyClass1
 Method1_1 of MyClass1 //★ココが①と違う
 Method1_2 of MyClass2
mc2 if
 Method1_1 of MyClass2
 Method1_2 of MyClass2
 Method1_1 of MyClass2
 Method1_2 of MyClass2
mi2
 Method1_1 of MyClass2
 Method1_2 of MyClass2
mi2 class
 Method1_1 of MyClass1
 Method1_2 of MyClass1
 Method1_1 of MyClass1 //★ココが①と違う
 Method1_2 of MyClass2
mi2 if
 Method1_1 of MyClass2
 Method1_2 of MyClass2
 Method1_1 of MyClass2
 Method1_2 of MyClass2

実験②では、mc2をMyClass1にキャストしてから、MyClass2の中で明示的にMyInterface1のメソッドとして実装したMyMethod1_1を呼ぼうとすると、不要なキャストとしてMsgがでる。

↓薄い色になって、不要なキャストと言われる

キャストはいらないということは、つまり、

「明示的にMyInterface1のメソッドとしてMyMethod1_1を実装することは、MyClass1に書いたMyMethod1_1の実装を、MyClass2でやり直しているだけ。」

だから、実質、

「MyClass2には、MyMethod1_1の実装はない!」

ということか。

実験③ Method1_1を、MyClass1ではvirtualにし、MyClass2でoverrideする

コード

using System.Diagnostics;

namespace InterfaceJikken
{
    internal class Program
    {
        static void Main(string[] args)
        {
            {
                MyClass1 mc1 = new MyClass1();
                MyInterface1 mi1 = new MyClass1();

                //mc1.Method1_1();
                //mc1.Method1_2();
                //((MyInterface1)mc1).Method1_1();
                //((MyInterface1)mc1).Method1_2();
            }

            {
                MyClass2 mc2 = new MyClass2();
                MyInterface2 mi2 = new MyClass2();

                Debug.WriteLine("mc2");
                mc2.Method1_1();
                mc2.Method1_2();

                Debug.WriteLine("mc2 class");
                ((MyClass1)mc2).Method1_1();
                ((MyClass1)mc2).Method1_2();
                ((MyClass2)mc2).Method1_1();//不要なキャスト
                ((MyClass2)mc2).Method1_2();//不要なキャスト

                Debug.WriteLine("mc2 if");
                ((MyInterface1)mc2).Method1_1();
                ((MyInterface1)mc2).Method1_2();
                ((MyInterface2)mc2).Method1_1();
                ((MyInterface2)mc2).Method1_2();

                Debug.WriteLine("mi2");
                mi2.Method1_1();
                mi2.Method1_2();

                Debug.WriteLine("mi2 class");
                ((MyClass1)mi2).Method1_1();
                ((MyClass1)mi2).Method1_2();
                ((MyClass2)mi2).Method1_1();
                ((MyClass2)mi2).Method1_2();

                Debug.WriteLine("mi2 if");
                ((MyInterface1)mi2).Method1_1();//不要なキャスト
                ((MyInterface1)mi2).Method1_2();//不要なキャスト
                ((MyInterface2)mi2).Method1_1();//不要なキャスト
                ((MyInterface2)mi2).Method1_2();//不要なキャスト
            }
        }
    }

    internal interface MyInterface1
    {
        void Method1_1();
        void Method1_2();
    }
    internal interface MyInterface2 : MyInterface1
    {
        void Method2_1();
    }

    internal class MyClass1 : MyInterface1
    {
        public virtual void Method1_1() => Debug.WriteLine(" Method1_1 of MyClass1");  // ★ココが②と違う
        public void Method1_2()=> Debug.WriteLine(" Method1_2 of MyClass1");
    }

    internal class MyClass2 : MyClass1, MyInterface2
    {
        public override void Method1_1() => Debug.WriteLine(" Method1_1 of MyClass2");  // ★ココが②と違う
        public void Method1_2() => Debug.WriteLine(" Method1_2 of MyClass2");
        public void Method2_1() => Debug.WriteLine(" Method2_1 of MyClass2");
    }
}

結果

mc2
 Method1_1 of MyClass2 // ★ココが②と違う ①とは同じ
 Method1_2 of MyClass2
mc2 class
 Method1_1 of MyClass2 // ★ココが②と違う ①とも違う!
 Method1_2 of MyClass1
 Method1_1 of MyClass2 // ★ココが②と違う ①とは同じ
 Method1_2 of MyClass2
mc2 if
 Method1_1 of MyClass2
 Method1_2 of MyClass2
 Method1_1 of MyClass2
 Method1_2 of MyClass2
mi2
 Method1_1 of MyClass2
 Method1_2 of MyClass2
mi2 class
 Method1_1 of MyClass2 // ★ココが②と違う ①とは同じ
 Method1_2 of MyClass1
 Method1_1 of MyClass2 // ★ココが②と違う ①とは同じ
 Method1_2 of MyClass2
mi2 if
 Method1_1 of MyClass2
 Method1_2 of MyClass2
 Method1_1 of MyClass2
 Method1_2 of MyClass2

親のクラスでvirtualとして実装されたメソッドをoverrideすると、

呼ぶときに親(MyClass1)として呼ばれようが子(MyClass2)として呼ばれようが、overrideしたほうの子のメソッドが呼ばれる様子。

まとめ

そして、一番気になる、newとvirtual/overrideのやり方の違い(なんで、似たような機能がC#にあるのか?)が、下記ページに書かれていた。

https://atmarkit.itmedia.co.jp/fdotnet/csharp_abc/csharp_abc_004/csharp_abc02.html

こまかいこと

子クラスに親クラスと同じメソッドを定義したときに出てくる下記コメントの「非表示」は、英語だと「Hide」。

非表示というとよくわからないが、Hideだと「親のメソッドを隠す」ということで分かりやすい気がした。

参考

第4回 継承とインターフェイス

わかりやすい!

https://atmarkit.itmedia.co.jp/fdotnet/csharp_abc/csharp_abc_004/csharp_abc02.html

第5回に、インターフェースの話が書かれいてる。

これも、ずっと前からのギモン(抽象クラスとインターフェースがどう違うのか?似てるものが2個あるのはなぜ?)に触れてるようなので、見てみる。

https://atmarkit.itmedia.co.jp/fdotnet/csharp_abc/csharp_abc_004/csharp_abc03.html

C#のPJから使うC++(アンマネージド)のDLLのPJが勝手にコピーされてくれないのでビルド後イベントでコピーする

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

やりたいこと

C#のプロジェクトからC++で作ったDLLを使うときに、ビルドしただけでは自動でコピーされてくれない。 (C#のexeの出力先に、C++のDLLがコピーされてくれないので、ビルドしただけではデバッグできるようにならない)

C#のプロジェクトからC#のDLLを使うときは、使う側のPJの参照設定にC#のDLLのプロジェクトを参照に入れておくだけで、出力先に一緒にコピーされてくれるが、C++のDLLだとそういうわけにはいかない。

そういうときに、ビルド後イベントで、C++のDLLの出力ファイル一式を、使う側のC#のPJの出力先にコピーするようにすると便利だったのでやり方をメモする。

前提

下記のようなソリューション、プロジェクトとその出力物の構成だとする。

図のように、C++DLLをビルドして出来上がったものを、C#の各使う側PJの出力先にコピーしたい。

やったこと

.NET6のPJと.NETFW4.8のPJで試したのだが、微妙に使えるスクリプトが異なったので分けてメモする。

.NET6のC#プロジェクトからC++のDLLを使う場合

下記のスクリプトを、ビルド後イベントに入れた。

xcopy /Y /S /I "$(SolutionDir)$(PlatformName)\$(Configuration)\" "$(ProjectDir)$(OutDir)"

こんな感じ。

.NET6では、下記のスクリプトでも使えた。
(.NET6だと、相対パスが使える??)

xcopy /y /s /d "..\$(PlatformName)\$(Configuration)\" "$(OutDir)"

.NET Framework4.8のC#プロジェクトからC++のDLLを使う場合

下記のスクリプトを、ビルド後イベントに入れた。※.NET6の1個目と同じ。

xcopy /Y /S /I "$(SolutionDir)$(PlatformName)\$(Configuration)\" "$(ProjectDir)$(OutDir)"

※.NET6で使えた相対パスの方は、FW4.8だとうまく動かなかった。.NETFW48だと相対パスが使えない?

参考

ビルド後イベントで使えるマクロの一覧

https://learn.microsoft.com/ja-jp/visualstudio/ide/reference/pre-build-event-post-build-event-command-line-dialog-box?view=vs-2022

Resource.reswでローカライズした文言データの出力先の.priを、MakePri,exe dumpで見る

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

やりたいこと

下記のページのやり方で、WinUI3でローカライズできることが分かった。

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

分かったのだが、ローカライズのためにResource.reswに書いた文言は、どこに出力されているのか?知りたい。

結論

色々調べた限り、下記だった。

パッケージPJでパッケージ&インストールしたアプリの場合

パッケージの中のresources.priに入る。

中身は、ただダブルクリックするだけでは開けない(中身が見れない)が、makepri.exeというツールを使えば見れる。

VisualStudioInstallerで、UWP開発ツール一式を入れると入るらしい。

下記のファイルに対して、

こういうコマンドを、開発者コマンドプロンプトで実行する。

makepri.exe dump /if resources.pri /of aaa.xml

/ifは、見たいpriファイル。

/ofは、出力ファイル。priの中身をxmlとして出すファイル。

上記だと、aaa.xmlに、下記のような感じでソースの値が含まれる。

        <ResourceMapSubtree name="Resources">
            <NamedResource name="SimpleText" uri="ms-resource://40a02821-ea29-4b39-90c3-aec0996185b4/Resources/SimpleText">
                <Candidate qualifiers="Language-JA-JP" type="String">
                    <Value>単純な文字列リソース</Value>
                </Candidate>
                <Candidate qualifiers="Language-EN-US" isDefault="true" type="String">
                    <Value>c</Value>
                </Candidate>
            </NamedResource>
            <ResourceMapSubtree name="MyButton">
                <NamedResource name="Content" uri="ms-resource://40a02821-ea29-4b39-90c3-aec0996185b4/Resources/MyButton/Content">
                    <Candidate qualifiers="Language-JA-JP" type="String">
                        <Value>ボタン名称</Value>
                    </Candidate>
                    <Candidate qualifiers="Language-EN-US" isDefault="true" type="String">
                        <Value>b</Value>
                    </Candidate>
                </NamedResource>
                <NamedResource name="Height" uri="ms-resource://40a02821-ea29-4b39-90c3-aec0996185b4/Resources/MyButton/Height">
                    <Candidate qualifiers="Language-JA-JP" type="String">
                        <Value>50</Value>
                    </Candidate>
                    <Candidate qualifiers="Language-EN-US" isDefault="true" type="String">
                        <Value>150</Value>
                    </Candidate>
                </NamedResource>
                <ResourceMapSubtree name="[using:Microsoft.UI.Xaml.Automation]AutomationProperties">
                    <NamedResource name="Name" uri="ms-resource://40a02821-ea29-4b39-90c3-aec0996185b4/Resources/MyButton/[using:Microsoft.UI.Xaml.Automation]AutomationProperties/Name">
                        <Candidate qualifiers="Language-JA-JP" type="String">
                            <Value>添付プロパティ文言</Value>
                        </Candidate>
                        <Candidate qualifiers="Language-EN-US" isDefault="true" type="String">
                            <Value>tenpu</Value>
                        </Candidate>
                    </NamedResource>
                </ResourceMapSubtree>
            </ResourceMapSubtree>
            <ResourceMapSubtree name="MyStackPanel">
                <NamedResource name="Background" uri="ms-resource://40a02821-ea29-4b39-90c3-aec0996185b4/Resources/MyStackPanel/Background">
                    <Candidate qualifiers="Language-JA-JP" type="String">
                        <Value>Black</Value>
                    </Candidate>
                    <Candidate qualifiers="Language-EN-US" isDefault="true" type="String">
                        <Value>red</Value>
                    </Candidate>
                </NamedResource>
            </ResourceMapSubtree>
            <ResourceMapSubtree name="MyTextBlock">
                <NamedResource name="Text" uri="ms-resource://40a02821-ea29-4b39-90c3-aec0996185b4/Resources/MyTextBlock/Text">
                    <Candidate qualifiers="Language-JA-JP" type="String">
                        <Value>テキストブロック文言</Value>
                    </Candidate>
                    <Candidate qualifiers="Language-EN-US" isDefault="true" type="String">
                        <Value>a</Value>
                    </Candidate>
                </NamedResource>
            </ResourceMapSubtree>
            <ResourceMapSubtree name="PackResource">
                <NamedResource name="Text" uri="ms-resource://40a02821-ea29-4b39-90c3-aec0996185b4/Resources/PackResource/Text">
                    <Candidate qualifiers="Language-JA-JP" type="String">
                        <Value>パックリソース1</Value>
                    </Candidate>
                    <Candidate qualifiers="Language-EN-US" isDefault="true" type="String">
                        <Value>PackResource1</Value>
                    </Candidate>
                </NamedResource>
            </ResourceMapSubtree>

下記のように、WinUI3のexeのプロジェクトと、パッケージのプロジェクトそれぞれにリソースを持たせて、パッケージをビルドすると、上のファイルのように、パッケージのプロジェクトの中のresources.priの中に、全部のPJのリソースが入る。

WinUI3単品で起動できるようにしたアプリの場合

下記の記事で試した、WinUI3単品で起動できるようにする方法でも、同じ方法でローカライズできた。

WinUI3プロジェクトをビルドしたexeを直叩きで起動する

https://tera1707.com/entry/2022/02/27/223359

WinUI3でSelfContained=trueでビルドすると出るエラー対処 その1
https://tera1707.com/entry/2022/03/19/000916

WinUI3でSelfContained=trueでビルドすると出るエラー対処 その2
https://tera1707.com/entry/2022/03/19/004221

WinUI3でSelfContained=trueでビルドすると出るエラー対処 その3
https://tera1707.com/entry/2022/03/19/010307

その場合は、出力先に、<プロジェクト名>.priという名前で、priができる。

上の実験コードのPJだと、下記のようになる。

そのpriをmakepri.exe dumpをすると、同じように、その中にリソースがあることが見れる。
※当然、そのPJの中にある各言語のResources.reswの内容のみが含まれる。パッケージPJの分は含まれない。

参考

makepri.exe

https://learn.microsoft.com/en-us/windows/uwp/app-resources/compile-resources-manually-with-makepri

makepri.exe dump コマンド

https://learn.microsoft.com/en-us/windows/uwp/app-resources/makepri-exe-command-options#dump-command

WinUI3でSelfContained=trueでビルドすると出るエラー対処 その1 https://tera1707.com/entry/2022/03/19/000916

WinUI3でSelfContained=trueでビルドすると出るエラー対処 その2 https://tera1707.com/entry/2022/03/19/004221

WinUI3でSelfContained=trueでビルドすると出るエラー対処 その3 https://tera1707.com/entry/2022/03/19/010307

UAC関連設定とログインユーザー、起動方法により、起動したアプリの権限がどうなるか実験

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

やりたいこと

アプリが管理者として起動しているかどうかを見て何かするような処理を実装したときに、 WindowsUAC(User Account Control。ユーザーアカウント制御)についての理解が足りないためにバグを入れたことがあった。

この際、WindowsUACを有効/無効すると、どういう動きをするのか、起動したアプリに、管理者権限がつくのか、つかないのか、実験/確認したい。

前提

実験には、下記の環境を使用した。

  • Windows10 home
  • 21H2

UACの設定

Windowsで、UAC関連の設定は下記で行う。

UAC 有効/無効

レジストリHKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Systemキーの、 EnableLUAが、

  • 0:無効
  • 1:有効

になる。

ユーザーアカウント制御の設定

windowsの設定画面を開き、検索窓に「UAC」と入れると出てくる「ユーザーアカウント制御設定の変更」を押して出てくる下記画面で設定する。

この設定は、UACの有効無効を切り替えるものではなく、あくまでUACの通知のレベルを切り替えるもの。 スライダを一番下にしたからと言って、UACは無効にならない。有効無効は、上のレジストリでのみ切り替える。

※少なくとも試したWin10 21H2では。

但し、レジストリEnableLUAを0にしてUACを無効にすると、こちらのスライダは、自動で4(通知しない)になる。

UAC関連設定とログインユーザー、起動方法により、起動したアプリの権限がどうなるか

結論、下記のようになった。
(「GetTokenInformationの値」は、あとでそれを取れるサンプルコードを書く)

https://github.com/tera1707/Siryou/blob/master/UAC%E5%AE%9F%E9%A8%93.xlsx

  • UACを無効にしていると、ログイン中に起動させたアプリは、ユーザーの権限と同じ権限を持って、起動する。
    • 管理者ユーザーだと、管理者権限で起動して、管理者特権は「はい」になる
    • 一般ユーザーだと、一般権限で起動して、管理者特権は「いいえ」になる
  • GetTokenInformation関数の戻り値は、
    • UAC無効だと「TokenElevationTypeDefault」固定になる。
    • UAC有効だと、表のようになる。
      • 管理者ユーザーで通常起動すると、一般権限でアプリ起動し「TokenElevationTypeLimited」になる。(本当は管理者権限で起動するけど、UACのせいで「制限」がかかってる、という意味合いか)
      • 一般ユーザーで通常起動すると、「Default」になる。(一般ユーザーで普通に起動したから「普通=Default」だよ、ということか)
      • 管理者ユーザーでも一般ユーザーでも、右クリ>管理者として起動、をすると、「TokenElevationTypeFull」になる。(普通は一般ユーザーで起動するけど、管理者として起動を選んだから昇格してるよ、ということか。)

サンプルコード

「管理者特権」のありなしを取得するコード

下記サイトより。ありがとうございます。

https://dobon.net/vb/dotnet/system/isadmin.html

namespace IsAdministrator
{
    internal class Program
    {
        static void Main(string[] args)
        {
            var isAdmin = IsAdministrator();

            Console.WriteLine($"isAdmin : {isAdmin}");

            Console.ReadLine();
        }

        public static bool IsAdministrator()
        {
            //現在のユーザーを表すWindowsIdentityオブジェクトを取得する
            System.Security.Principal.WindowsIdentity wi = System.Security.Principal.WindowsIdentity.GetCurrent();
            //WindowsPrincipalオブジェクトを作成する
            System.Security.Principal.WindowsPrincipal wp = new System.Security.Principal.WindowsPrincipal(wi);
            //Administratorsグループに属しているか調べる
            return wp.IsInRole(System.Security.Principal.WindowsBuiltInRole.Administrator);
        }
    }
}

これで、「UAC関連設定とログインユーザー、起動方法により、起動したアプリの権限がどうなるか」の項目の表でいうところの「管理者特権」(=タスクマネージャーの詳細画面にある項目)が、「はい」なのか「いいえ」なのか、つまり、アプリが管理者で起動しているのかそうでないのかが取れる。

が、これだと、UACが無効が故の管理者特権ありなのか、UACは有効だけど右クリック>「管理者として起動」による管理者特権ありなのかが区別できない。

UACの有効無効を取得する

同じく下記サイトより。

https://dobon.net/vb/dotnet/system/isadmin.html

using System.Runtime.InteropServices;

namespace IsAdministrator
{
    internal class Program
    {
        static void Main(string[] args)
        {
            var isAdmin = GetTokenElevationType();

            Console.WriteLine($"TokenElevationType : {GetTokenElevationType()}");

            Console.ReadLine();
        }

        [DllImport("advapi32.dll", SetLastError = true)]
        public static extern bool GetTokenInformation(IntPtr TokenHandle, TOKEN_INFORMATION_CLASS TokenInformationClass, IntPtr TokenInformation, uint TokenInformationLength, out uint ReturnLength);

        public enum TOKEN_INFORMATION_CLASS
        {
            TokenUser = 1,
            TokenGroups,
            TokenPrivileges,
            TokenOwner,
            TokenPrimaryGroup,
            TokenDefaultDacl,
            TokenSource,
            TokenType,
            TokenImpersonationLevel,
            TokenStatistics,
            TokenRestrictedSids,
            TokenSessionId,
            TokenGroupsAndPrivileges,
            TokenSessionReference,
            TokenSandBoxInert,
            TokenAuditPolicy,
            TokenOrigin,
            TokenElevationType,
            TokenLinkedToken,
            TokenElevation,
            TokenHasRestrictions,
            TokenAccessInformation,
            TokenVirtualizationAllowed,
            TokenVirtualizationEnabled,
            TokenIntegrityLevel,
            TokenUIAccess,
            TokenMandatoryPolicy,
            TokenLogonSid,
            MaxTokenInfoClass
        }

        public enum TOKEN_ELEVATION_TYPE
        {
            TokenElevationTypeDefault = 1,  // UACが無効になっているか、標準ユーザーです
            TokenElevationTypeFull,         // UACが有効になっており、昇格しています
            TokenElevationTypeLimited       // UACが有効になっており、昇格していません
        }

        /// <summary>
        /// 昇格トークンの種類を取得する
        /// </summary>
        /// <returns>昇格トークンの種類を示すTOKEN_ELEVATION_TYPE。
        /// 取得に失敗した時でもTokenElevationTypeDefaultを返す。</returns>
        public static TOKEN_ELEVATION_TYPE GetTokenElevationType()
        {
            TOKEN_ELEVATION_TYPE returnValue =
                TOKEN_ELEVATION_TYPE.TokenElevationTypeDefault;

            //Windows Vista以上か確認
            if (Environment.OSVersion.Platform != PlatformID.Win32NT ||
                Environment.OSVersion.Version.Major < 6)
            {
                return returnValue;
            }

            TOKEN_ELEVATION_TYPE tet =
                TOKEN_ELEVATION_TYPE.TokenElevationTypeDefault;
            uint returnLength = 0;
            uint tetSize = (uint)Marshal.SizeOf((int)tet);
            IntPtr tetPtr = Marshal.AllocHGlobal((int)tetSize);
            try
            {
                //アクセストークンに関する情報を取得
                if (GetTokenInformation(System.Security.Principal.WindowsIdentity.GetCurrent().Token, TOKEN_INFORMATION_CLASS.TokenElevationType, tetPtr, tetSize, out returnLength))
                {
                    //結果を取得
                    returnValue = (TOKEN_ELEVATION_TYPE)Marshal.ReadInt32(tetPtr);
                }
            }
            finally
            {
                //解放する
                Marshal.FreeHGlobal(tetPtr);
            }

            return returnValue;
        }
    }
}

こちらだと、上の表のとおりにGetTokenElevationType()が値を返すので、その表のとおりに判別できる。

戻り値がTokenElevationTypeDefaultかどうか、でUACが有効かどうかの判別にはならないが、ユーザーが操作したことによる(右クリック>管理者として起動など)、アプリの管理者権限かどうかは判別できそう。

単に、UACが有効かどうかを調べる

こちらは、下記のサイトにあるように、単に上に挙げたレジストリHKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Systemキーの、 EnableLUAが、0か1かで調べられそう。

https://dobon.net/vb/dotnet/system/isuacenabled.html

public static bool IsUacEnabled()
{
    //キーを開く
    var regKey = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System");
    //キーが見つからない時はUACが有効ではない
    if (regKey == null)
        return false;

    var val = (int)regKey.GetValue("EnableLUA", 0);
    //0以外の時はUACが有効
    return val != 0;
}

参考

dobon.net
UACが有効か調べる

https://dobon.net/vb/dotnet/system/isuacenabled.html

現在のユーザーが管理者か調べる
UACが有効で、管理者に昇格しているか調べる

https://dobon.net/vb/dotnet/system/isadmin.html

WindowsのGUIアプリ開発で、ViewModelがやるべきことが何なのか未だ全然わからないので、考えてみる(2023年7月版)その2【作成途中】

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

やりたいこと

なんのためにViewModelを書くのか、ViewModelがなんの役に立つのかが知りたい。

この記事の前半は、こっちに書いた。

https://qiita.com/tera1707/items/1e4c80d26e7e72bfe3cd

以下は、続き。

※実験アプリを作るパート。

前提

今回作るアプリ

  • 差し替え前アプリ
    • WinUI3 パッケージアプリ
    • WindowsAppSDK 1.2.221109.1
  • 差し替え後アプリ

お試しアプリのリポジトリ

https://github.com/tera1707/ViewModelJikken

実験アプリ仕様(概要)

適当に思い付きで、下記のようなアプリを作ってみる。
(実際にそんな操作はできないが、見た目だけそれっぽく作る)

まずはWinUI3で。

  • お部屋を快適にするために、お部屋に関する設定を行う。
  • 設定可能なのは、
    • お部屋の明るさ設定
    • お部屋の温度設定
    • テレビのチャンネル(何見るかorテレビ消すか)
  • お部屋の明るさ設定は、「OFF」「薄暗い」「普通」「めっちゃあかるい」の4つを選択できるが、各々の設定時の実際のライトの明るさとしては、下記とする。
    • OFF → 0(かつ電源OFF)
    • 薄暗い → 30
    • 普通 → 70
    • めっちゃ明るい →100
  • お部屋の温度設定は、OFF(エアコン付けない),10,18,20,25,27度の設定ができるが、
    • 18,20度の時は、冷房運転にして、その温度設定にする。
    • 25,27度の時は、暖房運転にして、その温度設定にする。
    • 10度設定にしようとすると、「マジで寒くなりますけどいいですか?」とMsgを出す
  • テレビのチャンネルは、OFF以外を選択後、30分経過すると、自動でOFFを選択し、テレビを消す
  • 現在値の取得はしない。アプリ起動時、毎回
    • お部屋の明るさを「OFF」にする
    • お部屋の温度設定を「OFF」にする
    • テレビのチャンネルを「OFF」にする
  • アプリ終了時も、
    • お部屋の明るさを「OFF」にする
    • お部屋の温度設定を「OFF」にする
    • テレビのチャンネルを「OFF」にする

制御するデバイスの仕様(ライト/エアコン/テレビ)

  • ライト
    • 点灯と消灯ができる。
    • 0~100で、1刻みに明るさの設定ができる。
  • エアコン
    • 運転と停止ができる。
    • 運転モードには冷房と暖房がある。
    • 温度の設定を、10.0~30.0度まで設定できる。
  • テレビ

実験アプリの仕様(詳細)

中身を、こんなイメージで作る。

View

Viewは、下記のような画面を提供する。

  • 1つのウインドウの中に設定名とその設定の現在値をペアで表示する。
  • 設定名と現在値のペアは、複数表示する。
  • 設定名と現在値のペアがいくつあるかは不定。Modelが言う数だけ表示する。
  • 現在値は、ComboBoxで表示する。
  • ユーザーがComboboxで設定値を操作するたびに、各値の設定を行う。
    (OKボタンを押したときに全項目一括設定ではなく、操作したものを個別に設定したタイミングで設定実施する)

画面イメージ

Serviceクラス(アプリの仕様を司るクラス)

Modelは、下記のような機能(API)を提供する。

  • 存在する設定名と、その設定がもつ設定値のリストを取得するAPI
  • 存在する設定の、設定(値の変更)を行うAPI
  • ※簡単にしたいので、GetのAPIは無し。

ViewModelクラス

ViewModelは、ViewとModelの仲介役をする。

  • アプリ起動時、Viewの初期化のタイミングで、
    • Modelの「存在する設定名と、その設定がもつ設定値のリストを取得するAPI」を呼ぶ。
    • 存在した設定について、Modelの「現在値を取得するAPI」を呼ぶ。
    • 存在した設定名を、全設定分、表示する。
  • Comboboxで設定値を押下時、該当の設定について、Modelが持つ「設定(値の変更)を行うAPI」を呼ぶ。

Modelクラス

仕様を実現するためにデバイス(今回だとライトとかエアコン、テレビ)を操るためのクラス。
※実験アプリなので本当にはそんなデバイスはつけてない。イメージのみ。Debug.WriteLineするだけ。

作成途中メモ

★★★★★★★★★★★
7/18 とりあえずお試しあぷりの大枠はできた。 あと、、、

  • UTコード実装

    • mockのmdelを入れる
    • UT簡単に書く
  • Viewを、WinUI3→WPFに差し替えてみる ★★ここが重要

お試しアプリのリポジトリ

https://github.com/tera1707/ViewModelJikken

メモ

VMにこれを書くべきか?とかを考えるときは、

  • これをVMに書いたときに、Viewだけ挿げ替えたときに移植がすんなりいくか? 全く同じ動きを別のUIにもっていくときに、VMより下(Model)をポコッと外して持っていけるか?

を考えたらいい気がした。(ほぼやったことないけど)

また、

  • 自分が画面仕様だけ知っていて、Modelがなにをしてくれるかを全く知らない状態で、Viewだけを作って、後で別の人が作ったModelと合体させるという開発手順を踏むとして、ViewにとってどういうIn/OUTが欲しいか?

を考えたら、ねっちょりViewModelや、ModelとへばりつくViewにならなくて済む気がした。しらんけど

ContentをもつことができるUserContorlをつくる

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

やりたいこと

<Button>のように、

<Button>
    <Button.Content>
        <TextBlock Text="aaa"/>
    </Button.Content>
</Button>

と書かなくても、

<Button>
    <TextBlock Text="aaa"/>
</Button>

と、直接?値を入れられるようなプロパティを作りたい。

それで、自前のコントロールを作りたい。

やったこと

そういうUserControlを作ろうと思ったが、UserControlは、もともと「Content」のプロパティを持っていて、もともとそういうことができてしまう。

ほなそれを使えばよい、のだが、そういう仕組みがどうなっているかを勉強したかったので、今回UserContolではなくCustomControlでそれをやってみようと思う。

やったこと

[ContentProperty(Name = "プロパティ名")] を付けると、今回やりたいことができるようになる。

下記の実験コードでやってみた。

実験コード

CustomControl部分

MySignal.cs
※以前CustomControlを勉強したときの流用コードなので、名前は気にしない

using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Markup;

namespace CustomControlJikken2
{
    [ContentProperty(Name = "MyContent")] // ★★←これが肝!!!
    public sealed class MySignal : Control
    {
        private Grid _mainGrid;

        public MySignal()
        {
            this.DefaultStyleKey = typeof(MySignal);
        }

        public object MyContent
        {
            get => (object)GetValue(MyContentProperty);
            set { SetValue(MyContentProperty, value); }
        }
        public static readonly DependencyProperty MyContentProperty = DependencyProperty.Register(nameof(MySignal), typeof(object), typeof(MySignal), new PropertyMetadata(null));

        protected override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            _mainGrid = this.GetTemplateChild("MainGrid") as Grid;
            _mainGrid.Children.Add((UIElement)MyContent);
        }
    }
}

generic.xaml

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:CustomControlJikken2">

    <Style TargetType="local:MySignal" >
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:MySignal">
                    <Grid x:Name="MainGrid" BorderBrush="Red" BorderThickness="3">
                        <ContentPresenter />
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

CustomControlを使うMainWindow部分

上で作ったCustomControlを使う部分

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

    <local:MySignal>
        <TextBlock Text="AAA" Foreground="White"/>
    </local:MySignal>
</Window>

ContentProperty(Name="プロパティ名") を書いていないと

ContentProperty(Name="プロパティ名") を書いていないと、Mssing Content Property Definition云々のエラーで怒られる。

※このエラーで知ったが、今回やったようなことを「ダイレクトコンテンツ」というらしい。

ContentProperty(Name="プロパティ名")が無い場合は、Contentを書く場合でも下記のように書く必要がある。

<local:MySignal>
    <local:MySignal.MyContent>
        <TextBlock Text="AAA" Foreground="White"/>
    </local:MySignal.MyContent>
</local:MySignal>

参考

[ContentProperty("InnerContent")] で、自前コントロールにContentを持たすことができるようになるとかが書かれてる

https://stackoverflow.com/questions/36446440/how-to-use-a-contentpresenter-inside-a-usercontrol

同じ内容のかずきさんVer ListBoxみたいに、直接なかにいれたいものを並べて書けるようにするやり方が書かれてる!

https://blog.okazuki.jp/entry/20130103/1357202019

めちゃわかりやすい↓

https://www.codeproject.com/Articles/82464/How-to-Embed-Arbitrary-Content-in-a-WPF-Control

VisualStateManager で、xamlで画面変化をつくる

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

やりたいこと

前回、CustomControlの作り方を調べた。

その中で、VisualStateManagerというのを使って、画面の状態遷移(たとえば信号を緑から赤に切り替えたりとか)ができるというのを知った。

前回のCustomControlを、そのVisualStateManagerを使ったものに書き換えながら、勉強してみる。

前提

WInUI3 WindowsAppSDK 1.2.221109.1 VisualStudio2022

やったこと

結論、下記のコードで実現できた。

画面側のxamlコード。

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:CustomControlJikken2">

    <Style TargetType="local:MySignal" >
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:MySignal">
                    <Grid x:Name="MainGrid">
                        
                        <!-- アニメーション -->
                        <VisualStateManager.VisualStateGroups>
                            <VisualStateGroup>

                                <!-- Transition -->
                                <VisualStateGroup.Transitions>
                                    <VisualTransition To="SignalGreen" GeneratedDuration="0:0:0.2"/>
                                </VisualStateGroup.Transitions>

                                <!-- 緑/赤/OFF -->
                                <VisualState x:Name="SignalOff">
                                    <Storyboard>
                                        <ColorAnimation Storyboard.TargetName="PART_SignalGreen"  Storyboard.TargetProperty="(Grid.Background).Color" To="Gray" />
                                        <ColorAnimation Storyboard.TargetName="PART_SignalRed"  Storyboard.TargetProperty="(Grid.Background).Color" To="Gray" />
                                    </Storyboard>
                                </VisualState>
                                <VisualState x:Name="SignalGreen">
                                    <Storyboard>
                                        <ColorAnimation Storyboard.TargetName="PART_SignalGreen"  Storyboard.TargetProperty="(Grid.Background).Color" To="Green" />
                                        <ColorAnimation Storyboard.TargetName="PART_SignalRed"  Storyboard.TargetProperty="(Grid.Background).Color" To="Gray" />
                                    </Storyboard>
                                </VisualState>
                                <VisualState x:Name="SignalRed">
                                    <Storyboard>
                                        <ColorAnimation Storyboard.TargetName="PART_SignalGreen"  Storyboard.TargetProperty="(Grid.Background).Color" To="Gray" />
                                        <ColorAnimation Storyboard.TargetName="PART_SignalRed"  Storyboard.TargetProperty="(Grid.Background).Color" To="Red" />
                                    </Storyboard>
                                </VisualState>
                            </VisualStateGroup>
                        </VisualStateManager.VisualStateGroups>
                        
                        <!-- 信号の見た目 -->
                        <Grid Background="Gray" >
                            <Grid.RowDefinitions>
                                <RowDefinition />
                                <RowDefinition />
                                <RowDefinition /> 
                            </Grid.RowDefinitions>

                            <Grid x:Name="PART_SignalGreen" Grid.Row="0" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="5"
                                BorderBrush="{TemplateBinding BorderBrush}" Background="Green">
                                <TextBlock Text="Go" HorizontalAlignment="Center" VerticalAlignment="Center"/>
                            </Grid>

                            <Grid x:Name="PART_SignalRed" Grid.Row="1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="5"
                                BorderBrush="{TemplateBinding BorderBrush}" Background="Red">
                                <TextBlock Text="stop" HorizontalAlignment="Center" VerticalAlignment="Center"/>
                            </Grid>

                            <Button x:Name="PART_StartButton" Grid.Row="2" Content="SignalStart" HorizontalAlignment="Center" VerticalAlignment="Center">

                            </Button>
                        </Grid>
                    </Grid>                    
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

それに対するC#コード。

using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System;
using System.Linq;

namespace CustomControlJikken2
{
    public sealed class MySignal : Control
    {
        /// <summary>
        /// 信号切り替わり間隔(簡単にするために、赤→青、青→赤 どちらも同じ時間で切り替わるようにした)
        /// </summary>
        public TimeSpan IntervalTime
        {
            get => (TimeSpan)GetValue(IntervalTimeProperty);
            set { SetValue(IntervalTimeProperty, value); }
        }
        public static readonly DependencyProperty IntervalTimeProperty
            = DependencyProperty.Register(nameof(IntervalTime), typeof(TimeSpan), typeof(MySignal), new PropertyMetadata(TimeSpan.FromSeconds(1)));

        private Button startButton;

        private DispatcherTimer signalIntervalTimer = new DispatcherTimer();

        public MySignal()
        {
            this.DefaultStyleKey = typeof(MySignal);
            signalIntervalTimer.Tick += SignalIntervalTimer_Tick;
        }

        private void SignalIntervalTimer_Tick(object sender, object e)
        {
            var vsm = VisualStateManager.GetVisualStateGroups(this.GetTemplateChild("MainGrid") as Grid).First();
            var currentStatus = vsm.CurrentState;

            if (currentStatus.Name == "SignalRed")
            {
                // 青信号にする
                VisualStateManager.GoToState(this, "SignalGreen", true);
            }
            else
            {
                // 赤信号にする
                VisualStateManager.GoToState(this, "SignalRed", false);
            }
        }

        // 自分でoverrideを追加
        protected override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            if (startButton is not null)
            {
                startButton.Click -= StartButton_Click;
            }

            startButton = this.GetTemplateChild("PART_StartButton") as Button;

            if (startButton is not null)
            {
                startButton.Click += StartButton_Click;
            }

            VisualStateManager.GoToState(this, "SignalOff", false);
        }

        private void StartButton_Click(object sender, RoutedEventArgs e)
        {
            var vsm = VisualStateManager.GetVisualStateGroups(this.GetTemplateChild("MainGrid") as Grid).First();
            var currentStatus = vsm.CurrentState;

            if (currentStatus.Name == "SignalOff")
            {
                signalIntervalTimer.Interval = IntervalTime;
                signalIntervalTimer.Start();
                VisualStateManager.GoToState(this, "SignalGreen", false);
            }
            else
            {
                signalIntervalTimer.Stop();
                VisualStateManager.GoToState(this, "SignalOff", false);
            }
        }
    }
}

VisualStateManagerを使う流れ

ControlTemplateの一番外側の要素に<VisualStateManager.VisualStateGroups>を入れる

下記のようにする。

    <Style TargetType="local:MySignal" >
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:MySignal">
                    <Grid x:Name="MainGrid">
                        
                        <!-- アニメーション -->
                        <VisualStateManager.VisualStateGroups>  // ★コレ!
                            <VisualStateGroup>

                                <!-- Transition -->
                                <VisualStateGroup.Transitions>
                                    <VisualTransition To="SignalGreen" GeneratedDuration="0:0:0.2"/>
                                </VisualStateGroup.Transitions>

                                <!-- 緑/赤/OFF -->
                                <VisualState x:Name="SignalOff">
                                    <Storyboard>
                                        <ColorAnimation Storyboard.TargetName="PART_SignalGreen"  Storyboard.TargetProperty="(Grid.Background).Color" To="Gray" />
                                        <ColorAnimation Storyboard.TargetName="PART_SignalRed"  Storyboard.TargetProperty="(Grid.Background).Color" To="Gray" />
                                    </Storyboard>
                                </VisualState>
          ・
          ・
          ・
          ・

<VisualStateManager.VisualStateGroups>の中に<VisualStateGroup>を入れ、名前を付ける

まず <VisualStateManager.VisualStateGroups>の中に<VisualStateGroup>を入れる。

<VisualStateGroup>は名前を付けられている場合が多いようだが、名前なしでも動く。 今回は、シンプルな信号機としての動きを付けたいだけなので、とりあえず名前なしにした。

※一般的なControlの中の一般的な状態(PointOverとか)が入ってるVisualStateGroupには、 <VisualStateGroup x:Name="CommonStates">という名前が付けられてることが多い。というアレ。

<VisualStateGroup>の中に、<VisualState x:Name="状態名">を入れる

下記の部分。

今回は信号機なので、

  • 緑信号
  • 赤信号
  • 信号OFF

の③状態を作った。

<!-- 緑/赤/OFF -->
<VisualState x:Name="SignalOff">
    <Storyboard>
        <ColorAnimation Storyboard.TargetName="PART_SignalGreen"  Storyboard.TargetProperty="(Grid.Background).Color" To="Gray" />
        <ColorAnimation Storyboard.TargetName="PART_SignalRed"  Storyboard.TargetProperty="(Grid.Background).Color" To="Gray" />
    </Storyboard>
</VisualState>
<VisualState x:Name="SignalGreen">
    <Storyboard>
        <ColorAnimation Storyboard.TargetName="PART_SignalGreen"  Storyboard.TargetProperty="(Grid.Background).Color" To="Green" />
        <ColorAnimation Storyboard.TargetName="PART_SignalRed"  Storyboard.TargetProperty="(Grid.Background).Color" To="Gray" />
    </Storyboard>
</VisualState>
<VisualState x:Name="SignalRed">
    <Storyboard>
        <ColorAnimation Storyboard.TargetName="PART_SignalGreen"  Storyboard.TargetProperty="(Grid.Background).Color" To="Gray" />
        <ColorAnimation Storyboard.TargetName="PART_SignalRed"  Storyboard.TargetProperty="(Grid.Background).Color" To="Red" />
    </Storyboard>
</VisualState>

コードの方にVisualStateManager.GoToState(this, "状態名", false);を書く

前回のCustomCotnrol作成の記事のほうで、カスタムコントロールC#コードの中で、具体的にGridの色を指定していた部分を、VisualStateManager.GoToState(this, "状態名", false);に変える。

おわり

以上で、VisualStateManager.GoToState(this, "SignalGreen", false);としたら、xaml<VisualState x:Name="SignalGreen">のところで指定した対象と色に変わってくれる。

SignalRedやSignalOffの状態(VisualState)も同じ。

ハマったポイント

TargetPropertyの指定の仕方

Storyboard.TargetProperty="(Grid.Background).Color" To="Gray" の部分を、最初

Storyboard.TargetProperty="Background" To="Gray" にしていて、

ずっとこのエラーになってた。

だいぶ昔に書いた、WPFのこの記事のことを思い出して、(Grid.Background).Colorを試して、うまくいった。

※ただ、なんでこれでうまくいくのかが、正直もうひとつしっくりこない。。 (Grid.Column)のように、かっこを付けるのは、それが「添付プロパティ」のとき、という話があるが、Backgroundは添付プロパティではないような気がする。。。

上の以前の記事をかいたときの自分は、これ理解したのだったか。。。?

→とりあえず、Storyboard.TargetProperty を設定するときは、「"(Grid.Background).Color"」のように、"(出所.プロパティ)"と、正式?な書き方をすれば間違いない、と思っておく。。。

あらかじめ対象のGridのBackGroundを指定しておかないとエラーになる

これは、上と同じ以前のWPFのこの記事で書いたほぼそのままなのだが、あらかじめ対象のGridのBackGroundを指定しておかないとエラーになった。

具体的には、

ここのBackgroundになにか入れておかないと、

こういうエラーが出た。理由は、上の以前の記事に書いた通りと思う。

メモ

VisualStateGroup を調べてると「CommonStates」というのがやたら出てきて、それが「一般的なControlの状態」とか言われるが、どういうこと?

存在してる、ほとんどのControlが、CommonStatesという名前のVisualStateGroupを持っているらしい。

じゃあ自分で作ったCustomControlに、そういう名前のVisualStateGroup を作って、PointOverとかの名前を持ったVisualStateを作ったら、その通りの動き(マウスオーバー時の動き)をしてくれるのか?

→そうじゃないっぽい。

あくまで「VisualStateGroup 」というのは名前で、複数コントロールで「CommonStates」という名前のVisualStateGroup が存在してるのは、いうなればたまたま。

CommonStatesの中のVisualStateに、NormalとかPointerOverとかの、複数コントロールで共通したStateがあるのもたまたま。 一般的な各コントロールに、同じような状態があるから同じ名前を付けただけ。

⇒たまたま、ほとんどのコントロールに、そういう同じVisualStateGroup、VisualStateがあるから、ほとんどのコントロールで同じ書き方をすれば、各状態に対して制御を行うことができちゃう、という感じ。 (一般的なコントロール(例えばButton)のデフォルトのTemplateをみると、ほとんどのCotnorlに、確かにそういうVisualStateがある。)

で、一般的なContorlの中身(c#?コード)の中で、今回やったのと同じように、VisualStateを遷移させるような処理をしてるのだと思う。(.NETのgithubまでは見てないが...)

CommonStatesは大体のコントロールに含まれるという割に、それ以上のことが調べても全然でてこないのは、 たまたま同じという感じだから、MSの公式に、「CommonStatesにはこういうStateがある」的公式説明がないのだと推測。。。
(そういう公式ドキュメントがどうしても見つけられなかった。。。)

で、いざVisualStateGroup やVisualState を使って実装をしたい(例えばマウスオーバー時にBackgroundの色を変えたいとか)となったときには、 対象のコントロールに、実際にどういうVisualStateGroup があるのか、VisualStateGroup にCommonStatesがあるのか、とかを確認しようと思うと、そのコントロールのデフォルトのテンプレートをみるのがよさそう。

デフォルトのテンプレートのありか(generic.xamlのありか)は、下記を参照。

https://tera1707.com/entry/2022/10/02/235818

※たぶん、各コントロールのcsコードの中で、VisualState のx:Nameで名前指定した状態に、遷移を行っているのだと思う。(VisualStateGroup の名前は使われてなさそう?CommonStatesを、テンプレートの中でCommonStatesAAAAとか変な名前にしても、普通に動くので。VisualState を変な名前にすると動かなくなる)

要するに、一般的なコントロールの見た目をカスタムしたいときは、 デフォルトのテンプレートに書かれてるVisualState を使って、 動き(Backgroudnの色とか)を指定してやればOK。

自前のCustomControlを作ったときに、見た目の状態船員をしたいときは、今回のような実装をすればOK。

メモ2

VisualStateManager.GoToState(this, "SignalOff", false);の3つ目の引数をtrueにすると、

xaml中の<VisualStateGroup.Transitions>が有効になる。

今回のコードだと、下記の部分が有効になり、

<VisualStateGroup.Transitions>
    <VisualTransition To="SignalGreen" GeneratedDuration="0:0:0.2"/>
</VisualStateGroup.Transitions>

SignalGreenの状態に遷移するときだけ、0.2秒で状態遷移が行われるようになる。

falseにしてると、そうはならない。(ほかの状態と同じように、デフォルト?の動作をする)

参考

前回、CustomCOntrolを作った記事

https://tera1707.com/entry/2023/06/16/135543

WPFでアニメーションをしたときの記事

https://qiita.com/tera1707/items/ee6d72bb68a8e2131082#%E5%88%A5%E3%81%AE%E4%BE%8B

ms公式 VisualStateManager Class

https://learn.microsoft.com/en-us/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.visualstatemanager?view=windows-app-sdk-1.3

CommonStatesが持ってるVisualState
公式情報を見つけたいのだが、どうしても見つからない。
それ以外の情報についても、下記ページわかりやすい。

https://atmarkit.itmedia.co.jp/ait/articles/0907/13/news093_3.html

.NetFramework4.8情報なので古いが、なんか手掛かりになりそう?↓
コードからこの辺をやるときも参考になりそう

https://learn.microsoft.com/ja-jp/dotnet/desktop/wpf/graphics-multimedia/storyboards-overview?view=netframeworkdesktop-4.8

カスタムコントロール(CustomControl)を作る

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

やりたいこと

以前WPFでUserControlを作ったが、CustomControlというものもあるらしい。

どうも、UserCotnrolは、作ったときに定義した見た目(Template)をそのまま使うのだが、CustomControlは、それを使うときに、使う側がTemplateを定義して、見た目を好きにできるらしい。

それって、いつも使ってる、元からあるコントロールに対してTemplateを指定して見た目をカスタムするということが、自分で作ったコントロールでもできるということか。

私のお仕事上、いろんなコントロールを組み合わせて独自のコントロールを作るということは頻繁にあるが、それで作ったコントロールに対して、使う側で好きなテンプレートを指定したいということがなかったので、CustomControlを使う機会がなかった。
(コントロールのライブラリを作って、他に配る、とかするときに使うんだろなと思っていた)

が、とりあえず知ってて損はないと思ったのでいっぺん作ってみる。

前提

WinUI3 1.2.221109.1 VisualStudio2022

実験用プロジェクト作成と、カスタムコントロールの追加

まずは、VisualStudioでひな型をつくるところから。

追加 > 新しい項目、で、カスタムコントロール(WinUI3)を選ぶ。

Themesフォルダの中に、Generic.xamlが追加される。

また、指定した名前のcsファイルが追加される。

カスタムコントロール追加直後の初期状態

WinUI3のテンプレート作った直後

追加直後

追加直後のGeneric.xaml
→初期状態のコードは別にいらんので、後で消しちゃう。

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:CustomControlJikken2">

    <Style TargetType="local:MySignal" >
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:MySignal">
                    <Border
                        Background="{TemplateBinding Background}"
                        BorderBrush="{TemplateBinding BorderBrush}"
                        BorderThickness="{TemplateBinding BorderThickness}">
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

追加直後のcsファイル(今回は「MySignal.cs」)
あとでここに必要なものを追記していく。

// Copyright (c) Microsoft Corporation and Contributors.
// Licensed under the MIT License.

using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Documents;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;

// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.

namespace CustomControlJikken2
{
    public sealed class MySignal : Control
    {
        public MySignal()
        {
            this.DefaultStyleKey = typeof(MySignal);
        }
    }
}

csのthis.DefaultStyleKey = typeof(MySignal);は消してはいけない。
このコードで、デフォルトのテンプレートを取ってきてる。

これで、カスタムコントロールの追加はOK。次は、実験的に、中身を変えてみる。

カスタムコントロールの中身を自分の好きなようにする

実験アプリの仕様

今回は、実験的に、信号機のようなカスタムコントロールを作ってみた。仕様はこんなかんじ。

  • 動き仕様
    • ボタンを押すと、赤信号、青信号が一定間隔で交互に点灯する。
    • 青信号、赤信号の点灯時間は、どちらも同じ。
    • 点灯時間は、指定できる。
  • 中身仕様
    • クラス名は「MySignal」。
    • 青信号用のGrid「PART_SignalGreen」と、
    • 赤信号用のGrid「PART_SignalRed」を持っている。
    • 信号点灯開始のためのボタン「PART_StartButton」を持っている。
    • 点灯時間を設定するためのプロパティ「IntervalTime」が生えている。

実験コード

コードは、こんな感じ。
https://github.com/tera1707/CustomControlJikken

generic.xaml

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:CustomControlJikken2">

    <Style TargetType="local:MySignal" >
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:MySignal">
                    <Grid Background="Gray" >
                        <Grid.RowDefinitions>
                            <RowDefinition />
                            <RowDefinition />
                            <RowDefinition />
                        </Grid.RowDefinitions>

                        <Grid x:Name="PART_SignalGreen" Grid.Row="0" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="5"
                                BorderBrush="{TemplateBinding BorderBrush}" Background="Green"
                                >
                            <TextBlock Text="Go" HorizontalAlignment="Center" VerticalAlignment="Center"/>
                        </Grid>

                        <Grid x:Name="PART_SignalRed" Grid.Row="1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="5"
                                BorderBrush="{TemplateBinding BorderBrush}" Background="Red"
                                >
                            <TextBlock Text="stop" HorizontalAlignment="Center" VerticalAlignment="Center"/>
                        </Grid>

                        <Button x:Name="PART_StartButton" Grid.Row="2" Content="SignalStart" HorizontalAlignment="Center" VerticalAlignment="Center"/>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using System;
using Windows.UI;

namespace CustomControlJikken2
{
    // usercontrolは、画面(xaml)とそのコードビハインド(cs)ががっちり紐づいてるが、
    // CustomControlは、画面とコードががっちりは紐づいてない感じ?(最低限必要なものだけを、cs上で定義してる感じ。まただから、デフォルトstyleの中で名前を付けたコントロールがあっても、直接見えなくて、GetTemplateChildで探さないといけないっぽい。)
    // がっちり紐づいてないから、CustomControlを使う側で、styleとtemplateを指定できるのか。

    // やるべきこと
    // xamlで、主要な部品に名前をつける
    // コードで、その主要な部品に対応する変数を定義する
    // コードで、ApplyTemplateの中で、GetTemplateChild()でその名前を付けた部品を探して変数に入れる
    // DependencyPropertyを作成して、このコントロールを使う側から設定、取得できるプロパティを生やす(UserControlと同じ)
    // あとは、その部品たちを使ってやりたいことをやる

    public sealed class MySignal : Control
    {
        /// <summary>
        /// 信号切り替わり間隔(簡単にするために、赤→青、青→赤 どちらも同じ時間で切り替わるようにした)
        /// </summary>
        public TimeSpan IntervalTime
        {
            get => (TimeSpan)GetValue(IntervalTimeProperty);
            set { SetValue(IntervalTimeProperty, value); }
        }
        public static readonly DependencyProperty IntervalTimeProperty
            = DependencyProperty.Register(nameof(IntervalTime), typeof(TimeSpan), typeof(MySignal), new PropertyMetadata(TimeSpan.FromSeconds(5)));

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

        // 本コントロール中に必須で存在させたい部品(=generic.xamlに名前付きで作成したコントロール)
        private Grid signalGreen;
        private Grid signalRed;
        private Button startButton;

        private DispatcherTimer signalIntervalTimer = new DispatcherTimer();

        // null:信号停止中
        // false:赤信号表示中 true:青信号表示中
        private bool? IsGoing = null;
        private static readonly SolidColorBrush GreenBrush = new(Color.FromArgb(0xff, 0, 0xff, 0));
        private static readonly SolidColorBrush RedBrush = new(Color.FromArgb(0xff, 0xff, 0, 0));
        private static readonly SolidColorBrush GrayBrush = new (Color.FromArgb(0xff, 0xaa, 0xaa, 0xaa));

        public MySignal()
        {
            this.DefaultStyleKey = typeof(MySignal);
            signalIntervalTimer.Tick += SignalIntervalTimer_Tick;
        }

        private void SignalIntervalTimer_Tick(object sender, object e)
        {
            if (IsGoing == false)
            {
                // 青信号にする
                signalGreen.Background = GreenBrush;
                signalRed.Background = GrayBrush;
                IsGoing = true;
            }
            else
            {
                // 赤信号にする
                signalGreen.Background = GrayBrush;
                signalRed.Background = RedBrush;
                IsGoing = false;
            }
        }

        // 自分でoverrideを追加
        // (クラス名のところでAlt+Enterを押して出たメニューで「上書きを生成」を選び、出てきた一覧からOnApplyTemplateを選ぶ、でもいい)
        protected override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            if (startButton is not null)
                startButton.Click -= StartButton_Click;

            signalGreen = this.GetTemplateChild("PART_SignalGreen") as Grid;
            signalRed = this.GetTemplateChild("PART_SignalRed") as Grid;
            startButton = this.GetTemplateChild("PART_StartButton") as Button;

            if (startButton is not null)
                startButton.Click += StartButton_Click;
        }

        private void StartButton_Click(object sender, RoutedEventArgs e)
        {
            if (IsGoing == null)
            {
                signalIntervalTimer.Interval = IntervalTime;
                signalIntervalTimer.Start();
                IsGoing = false;
            }
            else
            {
                signalIntervalTimer.Stop();
                IsGoing = null;
            }

            // 一旦、全消灯にする
            signalGreen.Background = GrayBrush;
            signalRed.Background = GrayBrush;
        }
    }
}

Custom Controlを作るうえで実装すること

generic.xamlにデフォルトテンプレートを書く

CustomControlは、UserControlと違って、CustomControlを使うときに、外からStyle/Templateを指定することができる。

つまり、機能はそのままで、使う側が好きな見た目にすることができるのが特徴。

だがまず作るうえでは、デフォルトのstyle(使う側がstyleを指定しなかったときに使うstyle)を作る。

今回だと、上の実験コードのgeneric.xamlがそれ。

デフォルトテンプレートの中の主要部品に名前を付ける

CustomControlは、使うときに外からTemplateを指定することができるが、その外からのTemplateの中にも、主要(必須)な部品は備わっている必要がある。

上の実験コードだと、「PART_*****」という名前の付いた部品がそれ。

CustomControlを作るうえでは、主要(必須)な部品には「PART_」というのを頭につけるのが慣例の様子。

コードに、主要な部品に対応する変数を定義し、関連付ける

デフォルトのテンプレートは見た目を司るが、コードの方が動きを司る。

そのコードとテンプレート、動きと見た目を関連付けるために、コード側に、テンプレート中の主要部品に対応する変数を定義し、PART_***と名付けた名前を使って紐づける。

上の実験コードだと、MySignal.cs中の、

private Grid signalGreen;
private Grid signalRed;
private Button startButton;

が変数で、

signalGreen = this.GetTemplateChild("PART_SignalGreen") as Grid;
signalRed = this.GetTemplateChild("PART_SignalRed") as Grid;
startButton = this.GetTemplateChild("PART_StartButton") as Button;

が、名前で紐づけている部分。

デフォルトテンプレート中のコントロールのイベントを関連付ける

デフォルトテンプレート中に配置したコントロールのイベント(「Click」など)を拾って何かする必要があれば、そのイベントに、カスタムコントロールのクラスのメソッドを紐づける。

上の実験コードで言うと、

protected override void OnApplyTemplate()
{
    base.OnApplyTemplate();

    if (startButton is not null)
        startButton.Click -= StartButton_Click;  //★ココで古いイベントを解除し、

    signalGreen = this.GetTemplateChild("PART_SignalGreen") as Grid;
    signalRed = this.GetTemplateChild("PART_SignalRed") as Grid;
    startButton = this.GetTemplateChild("PART_StartButton") as Button; //★ココでイベントを取りたいボタンのコントロールを取得して、

    if (startButton is not null)
        startButton.Click += StartButton_Click; //★ココでイベントとクラス内のメソッドを紐づける
}

の★を付けた3か所の部分。

古いイベントを解除するのは、styleを適用されるたびにOnApplyTemplate()が行われて、イベントが残りっぱなしだとリークしてしまうからと思われる。(styleを途中で付け替えることはあまりないかもしれないが。)

必要なプロパティを生やす

カスタムコントロールに必要なプロパティを作成する。

ここのやり方は、UserControlと同じ。→(UserControlの作り方)https://qiita.com/tera1707/items/13a9ee14c2936c068a45

実験コードだと、

public TimeSpan IntervalTime
{
    get => (TimeSpan)GetValue(IntervalTimeProperty);
    set { SetValue(IntervalTimeProperty, value); }
}
public static readonly DependencyProperty IntervalTimeProperty
    = DependencyProperty.Register(nameof(IntervalTime), typeof(TimeSpan), typeof(MySignal), new PropertyMetadata(TimeSpan.FromSeconds(5)));

の部分。

これで、TimeSpan型の、初期値が5秒のプロパティが作成された。

あとは、用意したものを使ってやりたいことをコードに書く

今まで用意したデフォルトテンプレート、主要部品(PART_****)、イベント、プロパティを使って、やりたいことをコードに書けばOK。

今回の実験コードだと、ボタンを押したらタイマーをスタートして、タイマが満了するたびに青信号と赤信号を切り替えたい、そのタイマーが何秒で満了するかはプロパティの値を見て行いたい、ということなので、それを書いたのが上の実験コードとなる。

CustmoControlを使う側からテンプレートを入れてみる

ここまででCustomControlを作ってみたが、使うときに使う側が好きなテンプレートを指定できるのがCustomControlの特徴だったので、実際に使う側でテンプレートを指定して使ってみる。

MainWindow.xaml

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

    <GridView>
        <!-- styleを指定しないと、Generic.xamlに書いた標準styleで動く -->
        <local:MySignal Width="200" Height="250" IntervalTime="0:0:1" />

        <!-- Templateを指定すると、そのTemplateで動いてくれる -->
        <local:MySignal Width="200" Height="250" IntervalTime="0:0:3" >
            <local:MySignal.Style>
                <Style TargetType="local:MySignal">
                    <Setter Property="Template">
                        <Setter.Value>
                            <!-- 丸い信号 -->
                            <ControlTemplate>
                                <Grid Background="Gray">
                                    <Grid.RowDefinitions>
                                        <RowDefinition />
                                        <RowDefinition />
                                        <RowDefinition />
                                    </Grid.RowDefinitions>

                                    <Button x:Name="PART_StartButton"  Grid.Row="0" Content="Oshite Kudasai"/>
                                    
                                    <Grid x:Name="PART_SignalGreen" Grid.Row="1">
                                        <Ellipse Fill="ForestGreen"/>
                                    </Grid>
                                    
                                    <Grid x:Name="PART_SignalRed" Grid.Row="2">
                                        <Ellipse Fill="Pink"/>
                                    </Grid>
                                </Grid>
                            </ControlTemplate>
                        </Setter.Value>
                    </Setter>
                </Style>
            </local:MySignal.Style>
        </local:MySignal>
    </GridView>
    
</Window>

上に書いたのが、使う側でテンプレートをしてしないパターン。 下に書いたのが、使う側でテンプレートを指定するパターン。

動かしてみたのがこちら。

テンプレートを指定したほうの見た目がなんかちょっと思ったのと違うが、動きはしてくれてるのでよしとする。

終わり

とりあえず、動くCustomControlができた。

これだけでも何となくは動いているが、

見た目を制御するうえで、VisualStateMangaerというのを使うと、 xamlのほうで、動きを表現できたりするらしい。

次回それをやってみる。

メモ

ソリューション直下の「Theme」フォルダの中の「generic.xaml」が特別で、カスタムコントロールはここからデフォルトのテンプレート(x:keyがなくて、ターゲットタイプが作成したカスタムコントロールのクラス名のTemplate)を探しに行くとのこと。

→それはそうなのだが、なんとなく、自前で作ったResourceDictionary.xamlの中にデフォルトテンプレートを書いててもうまく動く気がする。(まだ試してない2023/6/15)

参考

MS公式

https://learn.microsoft.com/en-us/windows/apps/winui/winui3/xaml-templated-controls-csharp-winui-3

MS公式 Custom XAML Controls(WPF

https://learn.microsoft.com/en-us/archive/msdn-magazine/2019/may/xaml-custom-xaml-controls

かずきさんブログ カスタムコントロール

https://blog.okazuki.jp/entry/2014/09/08/221209

ユーザーコントロールの作り方(カスタムコントロールではなく、ゆーーざーコントロール

https://qiita.com/tera1707/items/13a9ee14c2936c068a45

カスタムコントロールは、デフォルトスタイルがgeneric.xamlに書かれてしまう。csと分離されるのでなんかいやだなと思っていたら、下記のような強引な?やり方で対策できるらしい。こんなのありか?でもやってみる

http://neareal.net/index.php?Programming%2F.NetFramework%2FWPF%2FCuston%20and%20UserControl%2FTechniqueOfAddingCustomControl

起動時にNavigationViewItemを選択しておく/コードから選択を解除する

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

やりたいこと

なにもしないと、NavigationViewの表示直後は、どの項目(NavigationViewItem)も選んでいない状態で表示される。

こんな感じ。

しかし私の場合は、まずどれかの画面(Page)を開いた状態でアプリ起動してほしいことがおおい。

こんな感じ。

なので、Pageを表示した状態で起動させる方法を調べたメモ。

サンプルコード

下記のようなコードを書いて、実験した。

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

    <Grid>
        <NavigationView x:Name="navi" PaneDisplayMode="Top" IsSettingsVisible="False"
                        SelectionChanged="nvSample_SelectionChanged" Loaded="nvSample_Loaded">
            <NavigationView.MenuItems>
                <NavigationViewItem x:Name="vi1" Tag="MainPage" Icon="Emoji2" Content="メインページ"  />
                <NavigationViewItem x:Name="vi2" Tag="SubPage"  Icon="Save" Content="サブページ" />
            </NavigationView.MenuItems>

            <Frame x:Name="contentFrame"/>

        </NavigationView>

        <Button Content="選択解除" Click="Button_Click"/>
    </Grid>
</Window>
namespace WinUI3PackageProjectTemplate
{
    /// <summary>
    /// An empty window that can be used on its own or navigated to within a Frame.
    /// </summary>
    public sealed partial class MainWindow : Window
    {
        public MainWindow()
        {
            this.InitializeComponent();
        }

        private void nvSample_SelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs args)
        {
            if (args.SelectedItemContainer != null)
            {
                if ((string)args.SelectedItemContainer?.Tag == "MainPage")
                {
                    contentFrame.Navigate(typeof(MainPage), null, new SlideNavigationTransitionInfo() { Effect = SlideNavigationTransitionEffect.FromRight });
                }
                else if ((string)args.SelectedItemContainer?.Tag == "SubPage")
                {
                    contentFrame.Navigate(typeof(SubPage), null, new SlideNavigationTransitionInfo() { Effect = SlideNavigationTransitionEffect.FromLeft });
                }
            }
        }

        private void nvSample_Loaded(object sender, RoutedEventArgs e)
        {
            // ①Navigateメソッドを使う
            // 青線:出ない 画面:出る
            contentFrame.Navigate(typeof(MainPage), null);

            // ②NavigationViewItemのIsSelectedをtrueにする(NavigationViewItemに名前つけるパターン)
            // 青線:出る 画面:出る
            vi1.IsSelected = true;

            // ③NavigationViewItemのIsSelectedをtrueにする(NavigationViewから探すパターン)
            // 青線:出る 画面:出る
            navi.MenuItems.OfType<NavigationViewItem>().ToList().First().IsSelected = true;

            // ④NavigationViewのSelectedItemプロパティにNavigtionViewItemをセットする
            // 青線:出る 画面:出る
            navi.SelectedItem = vi1;//但し、画面は表示されるが、NavigationViewItemの下に選択されていることを示す青線がでない
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            navi.MenuItems.OfType<NavigationViewItem>().ToList().ForEach(x => x.IsSelected = false);
            navi.SelectedItem = null;
        }
    }
}

起動時にNavigationViewItemを選択しておく方法

調べた限り、下記の4種類のやり方があった。

private void nvSample_Loaded(object sender, RoutedEventArgs e)
{
    // ①Navigateメソッドを使う
    // 青線:出ない 画面:出る
    contentFrame.Navigate(typeof(MainPage), null);

    // ②NavigationViewItemのIsSelectedをtrueにする(NavigationViewItemに名前つけるパターン)
    // 青線:出る 画面:出る
    vi1.IsSelected = true;

    // ③NavigationViewItemのIsSelectedをtrueにする(NavigationViewから探すパターン)
    // 青線:出る 画面:出る
    navi.MenuItems.OfType<NavigationViewItem>().ToList().First().IsSelected = true;

    // ④NavigationViewのSelectedItemプロパティにNavigtionViewItemをセットする
    // 青線:出る 画面:出る
    navi.SelectedItem = vi1;//但し、画面は表示されるが、NavigationViewItemの下に選択されていることを示す青線がでない
}

青線がでる、というのは、下図のような感じで、選択されていることを示す線が出ているということ。

上のコードの②③④だと、その線がでる。①だとでない。

これは、①は、NavigationViewItemのどれかを「選んだ」のではなく、単にFrameの部分の表示を切り替えただけなので、 選択表示にならないものと思われる。(当たり前か?)

なので、nvSample_SelectionChanged()を通らない。

②③④は、NavigationViewItemのどれかを「選んだ」ことになるので、選択表示がでるとおもわれる。

nvSample_SelectionChanged()も通る。

これを、NavigationViewのLoaded時に行ってやれば、どれかのItemを選んだ状態で起動することができる。

どれをやればいいのかといわれると、

①は見た目、Itemを選択しているように見えなくて困るので、

上記②③④のうちのどれか1つを行えばOKと思うが、②④はNavigationViewItemに名前を付けないといけないので、名前を付けなくて済む③がよいかも。

※注意

この処理を、Windowのコンストラクタでやるのはやめた方がよさそう。

この後書く、選択の解除がうまく動かないケースがあった。 (Loaded以降のタイミングでやった方がよさそう)

コードから選択を解除する

どうも、NavigationViewは、一度Itemを選ぶと、選択されている項目がない状態にもどることはできない仕様っぽい。

しかし、どうしてもコードから全解除したい。

ということを言っている、別の方が挙げているmicrosoft-ui-xamlのgithubのissueがあったので、それを参考にやってみた。

下記のようにすれば、全解除ができた。

private void Button_Click(object sender, RoutedEventArgs e)
{
    navi.MenuItems.OfType<NavigationViewItem>().ToList().ForEach(x => x.IsSelected = false);
    navi.SelectedItem = null;
}

やっていることは、

  • すべてのNavigationViewItemの、IsSelected を false にする。
  • NavigationViewの、SelectedItem を null にする

ということ。

これで解除できる。が、issueをみるに、「たまたまそうやれば全解除できるけど、たまたまですよ」感を感じるので、将来もずっとこれで行けるかどうかはわからないかも?

参考

github issue
NavigationViewは、一度Itemを選ぶと、選択されている項目がない状態にもどることはできない仕様とある。

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

NavigationView系メモ記事

https://tera1707.com/entry/2022/02/27/213659

https://tera1707.com/entry/2022/03/09/232350

generic.xamlにあるリソースの値を上書きしたときに、いうことを聞くヤツと聞かないヤツがいる(StaticResourceとThemeResourceの違い)

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

やりたいこと

WinUI3アプリで、デフォルトのコントロールの色の定義を上書きすることで、簡単に見た目をカスタムしたいということをした。

そのために、generic.xamlに書かれている色や数字を、自分のコードの中にも書くことで上書き(同じリソース(同じx:keyをもつリソース)を、別の値で定義する)とした。

ただ、今回、単に値を上書きしただけではいうことを聞いてくれないヤツがいた。

具体的には、RadioButtonの角の丸さを、自前のテンプレートを作らずにいじろうと思い、 デフォルトのテンプレートの中を見て、下図のコレを変えてみた。

で、下記のようなコードを書いてみたが、思った見た目にならない。

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

    <Grid>
        <Grid.Resources>
            <CornerRadius x:Key="ControlCornerRadius">20</CornerRadius>
        </Grid.Resources>

        <StackPanel>
            <RadioButton Content="AAAAAA"  Background="Orange"/>←★コレ★
        </StackPanel>
    </Grid>
</Window>

見た目。 もっと角が丸くなってほしいのに、まるくならない。

なぜなのか?調べる。

結論

generic.xamlの該当のリソースが使われるときに、

Value="{StaticResource ControlCornerRadius}"

という感じで、StaticResourceとして使われているため。

これが、もし

Value="{ThemeResource ControlCornerRadius}"

のように、ThemeResourceであれば、そのリソースを上書きしてやれば、その値が反映される。

どうしてそのような違いが生まれるか、は、はっきりした理由は分からないのだが、StaticResourceとThemereResourceの、リソースを見に行くタイミングの違い、っぽい。

実験

下記2つのコントロール、同じControlCornerRadiusという値を使っている。

generic.xamlより

ただし、それぞれのstyle(テンプレート)の中で、

  • ComboBox
    • その値を、ThemeResourceとして使っている
  • RadioButton
    • その値を、StaticResourceとして使っている

という違いがある。

ComboBox

    <Style x:Key="DefaultComboBoxStyle" TargetType="ComboBox">
        <Setter Property="Padding" Value="{ThemeResource ComboBoxPadding}" />
        <Setter Property="MaxDropDownHeight" Value="504" />
        <Setter Property="Foreground" Value="{ThemeResource ComboBoxForeground}" />
        <Setter Property="Background" Value="{ThemeResource ComboBoxBackground}" />
        <Setter Property="BorderBrush" Value="{ThemeResource ComboBoxBorderBrush}" />
        <Setter Property="BorderThickness" Value="{ThemeResource ComboBoxBorderThemeThickness}" />
        <Setter Property="TabNavigation" Value="Once" />
        <Setter Property="TextBoxStyle" Value="{StaticResource ComboBoxTextBoxStyle}" />
        <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled" />
        <Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto" />
        <Setter Property="ScrollViewer.HorizontalScrollMode" Value="Disabled" />
        <Setter Property="ScrollViewer.VerticalScrollMode" Value="Auto" />
        <Setter Property="ScrollViewer.IsVerticalRailEnabled" Value="True" />
        <Setter Property="ScrollViewer.IsDeferredScrollingEnabled" Value="False" />
        <Setter Property="ScrollViewer.BringIntoViewOnFocusChange" Value="True" />
        <Setter Property="HorizontalContentAlignment" Value="Stretch" />
        <Setter Property="HorizontalAlignment" Value="Left" />
        <Setter Property="VerticalAlignment" Value="Top" />
        <Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}" />
        <Setter Property="FontSize" Value="{ThemeResource ControlContentThemeFontSize}" />
        <Setter Property="UseSystemFocusVisuals" Value="{ThemeResource IsApplicationFocusVisualKindReveal}" />
        <Setter Property="primitives:ComboBoxHelper.KeepInteriorCornersSquare" Value="true" />
        <Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}" />←★コレ★
  ・
  ・
  ・

RadioButton

    <Style x:Key="DefaultRadioButtonStyle" TargetType="RadioButton">
        <Setter Property="Background" Value="{ThemeResource RadioButtonBackground}" />
        <Setter Property="Foreground" Value="{ThemeResource RadioButtonForeground}" />
        <Setter Property="BorderBrush" Value="{ThemeResource RadioButtonBorderBrush}" />
        <Setter Property="Padding" Value="8,6,0,0" />
        <Setter Property="HorizontalAlignment" Value="Left" />
        <Setter Property="VerticalAlignment" Value="Center" />
        <Setter Property="HorizontalContentAlignment" Value="Left" />
        <Setter Property="VerticalContentAlignment" Value="Top" />
        <Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}" />
        <Setter Property="FontSize" Value="{ThemeResource ControlContentThemeFontSize}" />
        <Setter Property="MinWidth" Value="120" />
        <Setter Property="UseSystemFocusVisuals" Value="{StaticResource UseSystemFocusVisuals}" />
        <Setter Property="FocusVisualMargin" Value="-7,-3,-7,-3" />
        <Setter Property="CornerRadius" Value="{StaticResource ControlCornerRadius}" />←★コレ★
  ・
  ・
  ・

で、自分の画面のxamlの中に、ControlCornerRadiusのリソースを入れて奴やる。

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

    <Grid>
        <Grid.Resources>
            <CornerRadius x:Key="ControlCornerRadius">20</CornerRadius>
        </Grid.Resources>

        <StackPanel>
            <RadioButton Content="AAAAAA"  Background="Orange"/>
            <ComboBox SelectedIndex="0" Background="Red">
                <x:String>AAAA</x:String>
                <x:String>BBBB</x:String>
            </ComboBox>
        </StackPanel>
    </Grid>
</Window>

これを動かすと、

という感じで、StaticResourceとして値を使っているRadioButtonの角は丸くならず、
ThemeResourceとして値を使っているComboBoxの方だけ丸くなる。

generic.xamlにあるリソースの値で、値を上書できるのは、ThemeResourceとして使われているもの、と思った方がよさそう。

思ったこと

ThemeResourceだったら、自前で同じリソースを定義したら上書きできるのだと分かったが、 上で試したControlCornerRadiusのような、複数のコントロールで使われている値を安易に上書することはやめた方がよさそう。

思ってもないコントロールの角が丸くなった、とかに、きっとなる。

デフォルトのコントロールで使われるリソース値を上書きする場合は、MSの公式が言うように、特定のコントロールの名前がKey名についているような、「そのコントロール向けのリソース値」のみにした方がよいと思う。

例えばこういうリソース。

総合して、どういうときにリソースの上書きを行って、どういうときにgeneric.xamlのstyleをマルっとコピーすればいいのか?

個人的な考えだが、

generic.xamlにあるデフォルトのstyle、テンプレートの中で、Themeresourceとして使われているリソースの値を上書きするだけで、望みの見た目にできるコントロールは、リソースの上書きだけすればよい。

そうでないもの、つまり上記だけではすまない、デフォルトの見た目をそもそも変えたい(線を追加したい、〇〇を配置したい、変えたい値がstaticresourceや直値で書かれてる、など)ような場合は、generic.xamlにあるデフォルトのstyleを自前のコードにコピーしてきて、それをカスタムすればよい。

と思う。

参考

MS公式
このあたりに大事なことが書かれてるっぽいが、頭が追い付かない。。。

https://learn.microsoft.com/ja-jp/windows/apps/design/style/xaml-resource-dictionary#lookup-behavior-for-xaml-resource-references

リソース キーをオーバーライドして Expander コントロールをカスタマイズできません
→ズバリ知りたいことではないが、StaticresourceだとUIがリソースのxamlを検索する機会がないが、Themeresourceだとある、みたいなことが書いてる。 あと、ControlTemplateのコピーを作って、そこをカスタムすることで解決できたとも書いてる。やっぱりその解決方法でよいのだと思う。

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

AzurePipelineのパイプラインを作成し、中身を書いてみる

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

やりたいこと

前回の記事で、AzurePipeline練習のための、AzureDevopsの登録、リポジトリの作成までやった。

今回は、Pileineを実際に作成し、中身の処理を書いて、ビルドするところくらいまでやりたい。

前提

  • 以下は、2023年5月時点で試したことのメモ。

やったこと

リポジトリにアプリのソリューションをpushする

前回作ったリポジトリに、アプリのコードをpushして、そいつのビルドをパイプラインで行ってみる。

今回は、WinUI3でテンプレートから精製したばかりの中身のないアプリをpushしてみた。 下記のような構造。

パイプラインの作成

「Pipeline」を押す。

最初、こういう画面になる。

「Create Pipeline」を押す。

Gitの種類を聞かれるので、

「Azure Repos Git」を選ぶ。

※今回は、AzureDevOps上のGitでやってみるので、それを選んだ。  「GitHub」を選べば、Githubで作ってるリポジトリでも、pipelineを組める様子。

リポジトリの一覧が出てくるので、

さっき作ったリポジトリ名を選ぶ。

※ここで、さきほど「GitHub」を選んでると、自分のGithubプロジェクトのアドレスを選ぶ画面になり、  選んだGirhubプロジェクトが持ってるリポジトリの一覧が出てくる。それを選べば、GithubリポジトリでPipeleineできるっぽい。(未検証)

pipelineの種類の選択が出てくるので、

.NET Desktopを選ぶ。(今回、WIinUI3アプリだが、それっぽい選択肢がなかったのでとりあえず)

これで、デフォルトのデスクトップアプリ向けのyamlが生成される。

yaml書く

pipelineをつくると、デフォルトのyamlリポジトリの先頭フォルダにできてた。

下記のような内容。

# .NET Desktop
# Build and run tests for .NET Desktop or Windows classic desktop solutions.
# Add steps that publish symbols, save build artifacts, and more:
# https://docs.microsoft.com/azure/devops/pipelines/apps/windows/dot-net

trigger:
- main

pool:
  vmImage: 'windows-latest'

variables:
  solution: '**/*.sln'
  buildPlatform: 'Any CPU'
  buildConfiguration: 'Release'

steps:
- task: NuGetToolInstaller@1

- task: NuGetCommand@2
  inputs:
    restoreSolution: '$(solution)'

- task: VSBuild@1
  inputs:
    solution: '$(solution)'
    platform: '$(buildPlatform)'
    configuration: '$(buildConfiguration)'

- task: VSTest@2
  inputs:
    platform: '$(buildPlatform)'
    configuration: '$(buildConfiguration)'

とりあえず、デフォyamlに書かれてる内容を理解してみる。

また少し+αになるような内容も調べてみる。(デバッグのための出力方法とか)

trigger:

trigger:
- main

上記のように書くと、「main」というブランチがpushされたら、pipelineが走る、という意味になる。

trigger:
- main
- develop/*

と書くと、

  • 「main」ブランチ
  • 「develop/」が先頭につくブランチ(develop/bugfixなど)

がpushされたら、パイプラインが走る、という意味になる。

trigger:
  branches:
    include:
    - main
    - releases/*
    exclude:
    - releases/old*

みたいに、複雑な指定もできる。(こういうブランチ名は走らせるけど、こういうブランチ名は走らせない、などを指定できる)

詳しくはこちら参照
https://learn.microsoft.com/en-us/azure/devops/pipelines/repos/azure-repos-git?view=azure-devops&tabs=yaml#ci-triggers

pool:

pipeleine実行に使う仮想マシンを指定する

一覧はこれ

https://learn.microsoft.com/en-us/azure/devops/pipelines/agents/hosted?view=azure-devops&tabs=yaml#software

自分はWindowsだけ使えれば今はいいので、windows-latestでよさそう

No hosted parallelism has been purchased or granted. エラー

パイプラインをrunすると、

No hosted parallelism has been purchased or granted. 

というエラーがでて、パイプラインが止まってしまった。

これは、今のAzureパイプラインを利用するには、無料利用枠の登録というのが必要らしく、それを行っていないために出ている様子。

参考

https://rainbow-engine.com/azure-no-hosted-parallelism/

申請ページ

https://aka.ms/azpipelines-parallelism-request

フォームの記入の仕方

https://qiita.com/Hachiyou-Renge/items/2083f2ce9e8b38558805

無料で使えなくなった理由

https://learn.microsoft.com/ja-jp/azure/devops/release-notes/2021/sprint-184-update#azure-pipelines-1

https://devblogs.microsoft.com/devops/change-in-azure-pipelines-grant-for-private-projects/

23/04/月ころに、上記のページで、自分の作成したorganization「https://dev.azure.com/XXXXXX/」に対して、申請をした。2,3日後にメールがくるはず。

→次の日にメール来た。日本人からすると、怪しい海外メールのような見た目のメールが来た。

pipelineをrunしてみると、下記のような感じで、runできるようになった。

variables:

変数を定義する。
https://learn.microsoft.com/en-us/azure/devops/pipelines/yaml-schema/variables?view=azure-pipelines

variables:
  solution: '**/PipelineTest.sln'
  buildPlatform: 'x64'
  buildConfiguration: 'Release'

上記では、「solution」という名前の「**/PipelineTest.sln」という文字列を定義している。他のも同じ。
使うときはこうする。

      - script: |
            echo $(solution)111111 # outputs my value
      - script: 
            echo ${{variables.solution}} # outputs my value

※ちなみに、**は「再帰的なワイルドカード」で、この場合だと、すべてのサブディレクトリ内のすべての .sln ファイルを検索する。
 *は「単一フォルダーのワイルドカード」。

また上記ページの例にあるように、

変数は、

  • パイプライン全体レベル
  • stageレベル
  • jobレベル

のように、適する階層に定義できる。

steps:

こちらのぺージにあるように、
https://learn.microsoft.com/en-us/azure/devops/pipelines/get-started/key-pipelines-concepts?view=azure-devops

  • Pipeline
    • stage
      • job
        • task
        • task
      • job
        • task
    • stage
      • job
        • task
      • job
        • task

のような構造にできる。

- task: NuGetToolInstaller@1

nugetに必要なお作法っぽい。nugetでリストアとかするならとりあえず配置。

- task: NuGetCommand@2

リストアしたければとりあえず下記を書いとく。(下記はslnを渡してるが、csprojでも行けるっぽい)

- task: NuGetCommand@2
  inputs:
    restoreSolution: '$(solution)'

- task: VSBuild@1

「slnをビルドするとき」は、VSBuildでよいらしい。
https://learn.microsoft.com/en-us/azure/devops/pipelines/tasks/reference/msbuild-v1?view=azure-pipelines

「csprojをビルドするとき」は、MSBuildがよいらしい。
https://learn.microsoft.com/ja-jp/azure/devops/pipelines/tasks/reference/vsbuild-v1?view=azure-pipelines

- task: VSBuild@1
  inputs:
    solution: '$(solution)'
    platform: '$(buildPlatform)'
    configuration: '$(buildConfiguration)'

デバッグ用変数出力

      steps:
      - script: |
            echo ${{variables.solution}} # outputs my value

もしくは

      - task: CmdLine@2
        displayName: echo2
        inputs:
          script: 'echo AAAAAAAAAAAAAAAA'

アーティファクトへの出力

ビルドした成果物を、azurepipelineのVMの中から取り出したい場合は、 「アーティファクト」という領域に成果物をコピーしてやらないといけない。

でないと、VMは毎回起動時にリセットされて、さらピンな環境に戻ってしまうので、毎回きれいに忘れてしまう。

アーティファクトに配置するには、下記のコマンドを使う。

      - task: PublishPipelineArtifact@1
        inputs:
          targetPath: '$(Build.SourcesDirectory)\'
          artifact: 'MyArtifact'
          publishLocation: 'pipeline'

※ただし、これだとソースコード全部をartifactに含めてしまう。しかるべきフォルダだけをtargetPathに指定するように直す必要あり。

非パッケージアプリだと、これでartifactにフォルダごと置く、が最終出力でよさそう。

成果物をartifactに置くときには、

  • ソリューションのslnをVSBuildでビルドして、各プロジェクト(csprojなど)の下にできた成果物(bin)の下を、PublishPipelineArtifact@1でartifactに上げる
  • ソリューションのslnをVSBuildでビルドせず、プロジェクトのcsprojを個別にMSBuildでビルドする。その際、共通の出力先フォルダを指定しておき、できたものをPublishPipelineArtifact@1で一括でartifactに上げる

などのやり方が考えられる。(やったことあるのは後者の方。でも前者のほうが、パイプラインのスクリプトの量は減りそう。ソリューションの設定が少し面倒かもだが)

パッケージプロジェクトでパッケージを作る方法

パッケージアプリだと、VSでやってた「発行」のようなことをしないといけないかも?

パッケージを作るときのやり方参考。
https://qiita.com/okazuki/items/ef5f3357e2835be8fbdd

上記ではMSBuildを使ってるが、pipelineのタスク「VSBuild」で、wapprojを含むslnをビルドすれば、それだけでwapprojの下の「AppxPackage」フォルダに、msixができてそう。

       - task: VSBuild@1
         inputs:
           solution: '$(solution)'
           platform: '$(buildPlatform)'
           configuration: '$(buildConfiguration)'

MSBuildを使うときは、

.wapprojに対して、MSBuildでUapAppxPackageBuildModeをStoreUploadにしておけば、storeに挙げるためのパッケージができるっぽい。 →UapAppxPackageBuildModeを指定しなくても、下記でAppxPackageはできた。

       - task: MSBuild@1
         inputs:
           solution: '**/*.wapproj'
           msbuildVersion: '17.0'
           msbuildArchitecture: 'x64'
           platform: 'x64'
           configuration: 'Release'

※storeuploadだと、.msixuploadのファイルができる。パッケージのフォルダの中身自体は、全く同じだった。

.msixuploadのファイルがが必要であれば、UapAppxPackageBuildModeStoreUploadにしておけばよさそう。

パイプラインキャッシュでビルド時間を短縮する

今回は、20個以上のcsproj、vcsprojを含むソリューションをパイプラインでビルド時、nugetパッケージの解決にとにかく時間がかかるということがあったので、下記のページにあるように、nugetパッケージの解決にかかる時間を短縮するためにキャッシュを使った。

https://learn.microsoft.com/ja-jp/azure/devops/pipelines/artifacts/caching-nuget?view=azure-devops

が、キャッシュ機能は、それ以外にも、次回ビルド時にも保存しておきたいものをおいておける様子。(未検証)

https://learn.microsoft.com/ja-jp/azure/devops/pipelines/release/caching?view=azure-devops

下記はサンプルのコードの、キャッシュ部分を抜き出したもの。

  • Nugetツールインストールをして、
  • キャッシュを行い、
  • リストアをする
    • ただし、キャッシュがすでにリストアされてないときだけ

という流れで行う。

variables:
  NUGET_PACKAGES: $(Pipeline.Workspace)/.nuget/packages

steps:
- task: NuGetToolInstaller@1
  displayName: 'NuGet tool installer'

- task: Cache@2
  displayName: 'NuGet Cache'
  inputs:
    key: 'nuget | "$(Agent.OS)" | **/packages.lock.json,!**/bin/**,!**/obj/**'
    path: '$(NUGET_PACKAGES)'
    cacheHitVar: 'CACHE_RESTORED'

- task: NuGetCommand@2
  displayName: 'NuGet restore'
  condition: ne(variables.CACHE_RESTORED, true)
  inputs:
    command: 'restore'
    restoreSolution: '$(solution)'

CACHE_RESTOREDは、キャッシュがヒットしたとき(キャッシュを使ったとき)に、Cache@2タスクがtrueにしてくれるフラグ。

参考情報

AzurePipeline情報源まとめ

https://tera1707.com/entry/2023/05/22/222306

AzurePipelineの情報源まとめ

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

yaml情報源

Azure DevOps ドキュメントtop

https://learn.microsoft.com/ja-jp/azure/devops/?view=azure-devops

.NET core以降のyamlの基本っぽい
pipelineの基本。全体の流れが説明されてる
https://learn.microsoft.com/en-us/azure/devops/pipelines/get-started/key-pipelines-concepts?view=azure-devops

https://learn.microsoft.com/en-us/azure/devops/pipelines/ecosystems/dotnet-core?view=azure-devops&tabs=dotnetfive

triggerの書き方

https://learn.microsoft.com/en-us/azure/devops/pipelines/repos/azure-repos-git?view=azure-devops&tabs=yaml#ci-triggers

変数の設定の仕方

https://learn.microsoft.com/en-us/azure/devops/pipelines/process/variables?view=azure-devops&tabs=yaml%2Cbatch#set-variables-in-pipeline

poolの書き方

https://learn.microsoft.com/en-us/azure/devops/pipelines/agents/pools-queues?view=azure-devops&tabs=yaml%2Cbrowser#designate-a-pool-in-your-pipeline

pool:で指定するvmImageの名前

https://learn.microsoft.com/en-us/azure/devops/pipelines/agents/hosted?view=azure-devops&tabs=yaml#software

元から定義されている変数
Build.ArtifactStagingDirectory とか Build.SourcesDirectory とか。

https://learn.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops&tabs=yaml

変数の作り方
variables:

https://learn.microsoft.com/en-us/azure/devops/pipelines/yaml-schema/variables?view=azure-pipelines

式の書き方
(eq()とかor()とか、startsWith()とか。また、always()とか、failed()とか。)

https://learn.microsoft.com/en-us/azure/devops/pipelines/process/expressions?source=recommendations&view=azure-devops

タスクの一覧

https://learn.microsoft.com/en-us/azure/devops/pipelines/tasks/reference/?view=azure-pipelines

条件の書き方
(condition)

https://learn.microsoft.com/en-us/azure/devops/pipelines/process/conditions?view=azure-devops&tabs=yaml%2Cstages

アーティファクトのpublish

https://learn.microsoft.com/en-us/azure/devops/pipelines/artifacts/pipeline-artifacts?view=azure-devops&tabs=yaml

https://learn.microsoft.com/en-us/azure/devops/pipelines/tasks/reference/publish-pipeline-artifact-v1?view=azure-pipelines

https://learn.microsoft.com/en-us/azure/devops/pipelines/tasks/reference/copy-files-v2?view=azure-pipelines&tabs=yaml

パイプラインキャッシュ

https://learn.microsoft.com/ja-jp/azure/devops/pipelines/release/caching?view=azure-devops

nugetパッケージをキャッシュする

https://learn.microsoft.com/ja-jp/azure/devops/pipelines/artifacts/caching-nuget?view=azure-devops

Azureパイプラインの練習環境を作る

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

やりたいこと

Azureパイプラインを使って、ビルドや成果物の作成、フォルダ構成の作成を自動で行えるようにしたい。

で、それにはAzureのパイプラインというのを使うと便利で、そのパイプラインにやりたいことを書くには、.yml(ヤムルと読むらしい)に、stageやらjobやらtaskなどというものを書けばよいらしい。

さっぱりわからないので、練習したい。練習したいが環境がないので、まずは練習できる環境を作りたい。

前提

  • 以下は、2023年5月時点で試したことのメモ。
  • 事前に、マイクロソフトアカウントを取得しておく。
  • Azureに登録した後にこのメモを書いているので、DevOpsへの登録作業あたり、画面写真がなかったり、少々不正確だったりするかもしれない。

やったこと

Azure DevOpsに登録

まずは、Azure DevOpsに登録をする。「無料で始める」を押す。

https://azure.microsoft.com/ja-jp/products/devops/

※この辺で、プランの選択等をしたはずなのだが、メモをしてなかったので手順やら画面が不明。 とりあえず、

  • あらかじめ登録済みのMSアカウントをAzure DevOpsに紐づけるようなことをした
  • 無料のプランを選択した(たしかF3?とかそういう名前のもの)

というようなことをしたはず。(あれ、Azure App Serviceの話と混ざってるかも?プラン選択とかなかったかも?)



organizationの作成

一通り登録が終わると、下記画面になるのでContinueを押す。

DevOpsの組織(Organization)の作成をする。
組織=Organizationで、Azure DevOpsの一番大きな単位で、言葉のまんま「開発を行う組織/グループ」に必要な機能がまるっと入ったものっぽい。
(その下に、プロジェクト があって、プロジェクトの下にリポジトリやらWikiやら、Pipelineがあるイメージ)

Projectの作成

プロジェクトの作成を行う。
プロジェクトは「一つの開発」のイメージ。たぶん、プロジェクトの名前は、開発しようとしているアプリやらシステムの名前になるのだと思う。

名前を入力して、CreateProjectを押す。
※無償プランだと、Privateだけしか選べない様子。

※初回は上のような画面になるが、2回目からは下図「NewProject」ボタンを押してプロジェクト作成をする。

Repositoryの作成

リポジトリの作成を行う。
下図では、

  • tera1707 というorganizationに、
    • PipelineRenshu というプロジェクトを作り、

まずはプロジェクト画面を開き、その中の「Repos」を押す。
押すと、画面上部にOrganization や Project の階層構造的な表示がでるので、その一番右の項目(これがリポジトリ名)を押し、その中の「New Repository」を押す。

そうすると、Cteate Repository の画面がでる。
ここに、好きなリポジトリ名を入れる。
必要なら、.gitignoreを選ぶ。いらなければNoneのままにする。
(私はVisualStudioをいつも使うので、「VisualStudio」をいつも選んでる)

リポジトリを作ると、下図のような画面になる。図では、仮で「aaa」というリポジトリを作った。
クローンをするために、画面右の「Clone」を押す。

リポジトリをCloneする

Clone画面が出てくる。
まずは、「Generate Gir Credentials」を押し、パスワードを作成する。

また、とりあえず今回はHTTPSでいいので、HTTPSを選んで、その右側に出ているアドレスをコピーする。

あとは、gitコマンド、もしくは好きなgitクライアントアプリにそのアドレスを貼り付けて、Cloneする。
cloneの際に、Azure DevOpsのIDと先ほど作成したパスワードを聞かれるので、入力すればCloneできる。

おわりに

とりあえず、pipeline関連の作業をするまでの前準備をメモした。 次から、pipelineの作成等のメモを挙げる。

参考

Azure DevOps ドキュメントtop

https://learn.microsoft.com/ja-jp/azure/devops/?view=azure-devops

deps.jsonファイルは何者か?消していいか?

やりたいこと

.net6環境で、自分のアプリ(exe)やライブラリ(dll)をビルドすると、 xxxxx.deps.jsonというファイルができてくる。

これは何のファイルなのか?もしいらないファイルなら消したいが、消してよいのか?を調べてみた。

結論

ライブラリ(dll)ビルドで出来るライブラリ名.deps.jsonは、消してOK。

アプリ(exe)ビルドで出来るアプリ名.deps.jsonは消してはいけない。消すとexeが動かなくなる。

そもそも「アプリ名.deps.json」とは何か?

  • .netのアプリが動くときに、アプリホスト(ビルドしてできるアプリ名.exe)がどういう環境で動くか、 どういう依存ファイルを使うか、を知るためのファイル。

  • .net6アプリをビルドすると、下記のようなものができる。

    • アプリ名.exe:
      アプリホスト。apphost.exeというファイルをコピーしたものらしい。
    • アプリ名.dll:
      実はアプリ本体。アプリ名.exeがこいつを実行してる感じ。
    • アプリ名.deps.json
      アプリホストがどういう環境で動くか、どういう依存ファイルを使うかが書かれてる。
  • exeをビルドしても、dllをビルドしても、deps.jsonは作成される。

  • ただし、exeから参照しているdllのdeps.jsonは、exeをビルドしたときにexeの出力先にはコピーされない。
    なので、「ライブラリ名.deps.json」は、ライブラリのdllを使うexeからは使われないので、消してもOK。
    (dllは、そいつを読み込んだexeが動いてる環境で動くから、dll側のdeps.jsonはいらないということか)

  • exeビルドで生成されるdeps.jsonを削除すると、exeが起動しなくなる。
    その場合、イベントログに、エラーが残ってる。
    なので、exeと一緒に生成されるdeps.jsonは、消してはいけない。

参考

公式情報。
ふわっとそうなんだとわかるが、詳しくは消したり書き換えたりいろいろ実験しないといるのかいらないのか結局わからなかった...

https://github.com/dotnet/sdk/blob/main/documentation/specs/runtime-configuration-file.md

CPU負荷の計測方法まとめ

負荷計測系記事もくじ
https://tera1707.com/entry/2022/02/06/144447#Profile

CPU負荷の計測方法まとめ

CPU負荷の計測方法をいくつか調べた。

どの方法をどういうときに使えばよいか、だいたい見えてきたので、表にしてまとめておく。

本番環境でデータを取りたい場合は、④のパターンがよさそう。
(データを採取するWPRは、データ採取するPCになにもインストールしなくてOKで、作業用のPCにだけ、WindowsADKを入れればよい)

もっと細かく、メソッドレベルの情報が見たい、などの場合は、①②で短時間だけデータを取って、作業用PCでVisualStudioを使って解析、がよさそう。

表はこちら:

Siryou/CPU負荷状況のデータの採取と閲覧の仕方4選.xlsx at master · tera1707/Siryou · GitHub