BackacheEngineerの技術的な備忘録

技術系でいろいろ書けたらなーと

Vue.js とやらを勉強2

Vue.js で基本の属性をまとめる。

目次

v-html

ほとんど使わないじゃないかなって思ってるやつ。 v-html="---html---" で指定した html が差し込めるってやつ。

サンプル。 ボタン押すと入力した文字がどんどん追加されるってやつ。 こういう、追加作業するときって Web アプリだと多いのだろうか・・・。 そんななさそうなので、「使わない」と勝手に思っている。

  <div id="app">
    <ol v-html = "message"></ol>
    <hr>
    <input type="text" oninput="tempValue(event);">
    <button onclick="add();">add</button>
  </div>

  <script>
    let data = {
      message: '<li>one</li>',
      tempInput: ''
    }

    let app = new Vue({
      el: '#app',
      data: data
    });

    function add(){
      data.message += `<li>${data.tempInput}</li>`
    }
    function tempValue(event){
      data.tempInput = event.target.value;
    }

v-if

html 中に if 文が書き込めるやーつ。 ちゃんと else も書ける。

サンプルはボタン押すと内容が切り替わるよってやつ。 まとめてないけど template 構文を使ってる。

template 構文のおかげで、中身をまるっと変更できる。 後述するコンポーネントとか、実際に Vue.js でアプリ 作ってくってなるとこの template 構文が基本になる(と思っている)。

v-if = "条件" といったふうに書く。 「条件」に bool 値の変数名を書いて、data がその bool 値持ってれば Vue.js がきれいに変数として扱ってくれる。ありがたい。

    <div id="app">
    <template v-if="flag" >
      <p>Display data table.</p>
      <table>
        <tr><th>Name</th><th>mail</th></tr>
        <tr><td>Taro</td><td>taro@yamada</td></tr>
        <tr><td>Hanako</td><td>hanako@flower</td></tr>
        <tr><td>Sachiko</td><td>sachiko@happy</td></tr>
      </table>
    </template>
    <template v-else>
      <p>Display data list</p>
      <ul>
        <li>name: Taro, mail: taro@yamada</li>
        <li>name: Hanako, mail: hanako@flower</li>
        <li>name: Sachiko, mail: sachiko@happy</li>
      </ul>
    </template>
  </div>
  <button onclick="onChangeFlag();">
    change flag
  </button>

  <script>
    let data = {
      flag: true
    }

    let app = new Vue({
      el: '#app',
      data: data
    });

    function onChangeFlag(){
      data.flag = !data.flag;
    }
  </script>

v-for

さっきの v-if と一緒で html に for 文が書ける。 これのおかげでリスト表示とかもらくらくってわけ。

注意すべきなのは、v-if と併用すると 「v-if が後になる」ってことくらい? v-for が優先で動く。

使い方は v-for = " val in values " みたいなかんじ。 values がオブジェクトだとプロパティも抜き出せて、 ( obj, prop ) in values みたいな書き方になる。

index 番号が欲しい時もオブジェクトと同じ書き方で、 ( element, index ) in values になる。 ( javascript の for-in ループとは違うのがちょっと面倒・・?)

サンプルはオブジェクトを対象に、さらに v-if を組み合わせたやつ。

  <div id="app">
      <p>Display data list</p>
      <table>
        <tr>
          <th>name</th>
          <th>mail</th>
          <th>tel</th>
        </tr>
        <!-- v-for が優先で動く。v-if は v-for のあと。 -->
        <tr v-for="( obj, key ) in items" v-if="obj.isPublic">
          <td>{{ key }}</td>
          <td>{{ obj.mail }}</td>
          <td>{{ obj.tel }}</td>
        </tr>
        <tr v-else>
          <td>{{ key }}</td>
          <td>*** 非公開 ***</td>
          <td>*** 非公開 ***</td>
        </tr>
      </table>
    </div>

    <script>
      let data = {
        items: {
          Taro: {mail: 'taro@ssss', tel: '777-777', isPublic: true},
          Machiko: {mail: 'machiko@ieiieieie', tel: '888-888', isPublic: true},
          Administrator: {mail: 'admin@opertor', tel: '999-999', isPublic: false},
          Tester: {mail: 'tester@test', tel: '999-999', isPublic: false}, 
          Takeshi: {mail: 'takeshi@iwatype', tel: '999-999', isPublic: true}
        }
      };

      let app = new Vue({
        el: '#app',
        data: data
      });
    </script>

コンポーネント

すーぱー使う。絶対に使う。 必ず習得すべきもの。

できることは、新しい html のタグを作るようなもの。 C# でいうところのカスタムコントロールとかユーザコントロール。 ユーザコントロールのが近いかな。

このコンポーネントにプロパティ持たせて、そこに値をバインドとかできる。 XAML みたいだと思えば楽かも。(こういう時、自分の得意な言語にあてはめて考えれるのは楽だ)

単一のファイルでコンポーネントを作るときは Vue の関数を呼び出す。 そうでない場合、つまりちゃんとしたアプリだと *.vue っていう拡張子が vue の ファイルに書く感じ。

今回は前者で書く。

Vue.component(conponentName, { conponent が持ついろんなプロパティ })

こんな感じで使う。 conponentName はコンポーネント名で、これがタグになる。文字列で指定する。

第2引数はコンポーネントの本体で、オブジェクトで指定する。

ほんじゃまかサンプル。 「Hello ○○」って書いてくれるだけのコンポーネント。 サンプルは以下の仕様になってる。

  • コンポーネント名は hello(つまり、タグ名が hello)
  • name プロパティを持つ
  • 複数の「Hello ○○」を出すようになってる
  • name を v-bind して指定する

v-bind については後述する。

    <div id="app">
      <hello v-for="name in names" v-bind:name="name"></hello>
    </div>

    <script>
      Vue.component("hello", {
        props: ['name'],
        data: () => {
          return {
            message: "Hello ",
          };
        },
        template: '<p class="hello">{{ message }} {{ name }}</p>',
      });

      let app = new Vue({
        el: "#app",
        data: {
          names: ['taro', 'hanako', 'sachiko', 'takeshi']
        }
      });
    </script>

v-for で複数の name を取得する。names は Vue オブジェクトが持つ data に設定されたプロパティである。

1つ取り出した name が v-bind によってコンポーネントの name 属性にバインドされる。

そして、name 属性が template の {{ name }} 部分に入る仕組み。 name 属性は props: ['name'] で設定されている。(配列なんで複数書けるよ!)

Hello 部分を担う message はコンポーネント内の data プロパティが返している message プロパティのこと。 こいつが {{ message }} に入る。

v-bind

間違いなく超使うやつ。 バインドなんで、XAML でもいっぱい出てきてる。 つまり、特定の値に特定の値を割り当てる感じ。 ただし、v-bind は単方向だから、入力値をプロパティに代入とかはできない。 双方向でバインドしたいときは v-model を使う。

ちょっと前に v-bind で name 属性に変数 name をバインドした。 つまり、こいつは属性に対してバインドできる。

つまり、自分で設定した属性だけでなく html のタグの style とか class にもちゃんとバインドできる。

ほんじゃサンプル。 ボタンを押すと文字色が変わる。 文字色は css 側で class 「red」, 「blue」に対して変えてる単純なもの。

.red {
  color: red;
}

.blue {
  color: blue;
}
  <div id="app">
    <p v-bind:class="classes">{{ message }}</p>
    <button onclick="onChnageColor();">{{ btnText }}</button>
  </div>

  <script>
    let classObj = {
      // プロパティ名がそのままクラス名になる
      red: true,
      blue: false
    };

    let data = {
      message: "This is Message.",
      classes: classObj,
      btnText: "To Blue"
    };

    let app = new Vue({
      el: "#app",
      data: data
    });

    function onChnageColor() {
      classObj.red = !classObj.red;
      classObj.blue = !classObj.blue;
      data.btnText = classObj.red ? "To Blue" : "To Red";
    }
  </script>

classes でクラスを代入してる。プロパティ名がそのままクラス名になる。

そんで、bool 値に従ってクラスが適用される。

サンプルだとプロパティでクラスを代入してるが、下記のようにばちこり書いてもOK。

class="{red: isRed, blue: isBlue}"

v-model

v-model もバインドなんだけど、こいつは input タグの値をプロパティにバインドするやつ。

違った、v-model は双方向バインドのこと。 XAML でもあったやつ。入力した値をプロパティに渡すことができるし、プロパティの値を表示したりもできる。

これ以下の説明は一応そのとおりなんでそのままにしておくが、双方向バインドなんでもっとうまいことできるし、 ちゃんと考えないとたぶんだが沼る。

v-bind と組み合わせれば、input の値をプロパティにバインドして、プロパティにバインドした値を 属性にバインドする。これでリアルタイムに input の値がほかのタグに反映される。

下記がサンプル。 上記で書いてるとおり、input の内容を hello コンポーネントに渡している。

v-model でまとめたとおりの簡単なサンプルなので、これ以上は特に説明なし。

    <div id="app">
      <!-- name 属性に name プロパティを代入してる -->
      <hello v-bind:name="name"></hello>
      <!-- v-model により name プロパティに対して値が代入される。 -->
      <input type="text" v-model="name">
    </div>

    <script>
      // name プロパティを定義し、html 側で name 属性を指定すればここのプロパティとして入る
      Vue.component('hello', {
        props: ['name'],
        template: '<p class="hello">Hello, {{ name }}</p>'
      });

      let app = new Vue({
        el: '#app',
        data: {
          name: 'no-name'
        }
      })
    </script>
v-on

v-on はイベントの発生のこと。 指定した関数を実行してくれる。

下記のように書けば、click 時に func1 が実行される。

v-on:click="func1();"

複数個の関数を実行したいときは

v-on:click="func1();func2();func3()"

みたいに複数個繋げて書く。

コンポーネントに関数をオブジェクトとして持てる。

methods というプロパティを用意し、そこにオブジェクトを書く。 プロパティ名が関数名になり、「function () { ... } 」で関数を設定してやればいい。

サンプルはボタンを押すと以下の機能が働くもの。

  • ボタンの文字色が変わる
  • カウンターが増える
  • ボタンのメッセージも変える

文字色はクラスを変更し、 css で変えてる。 カウンターとかメッセージは関数の中身でゴリゴリと。

    <div id="app">
      <click></click>
    </div>

    <script>
      Vue.component("click", {
        methods: {
          // この this は component 内のネストのみ
          onChangeColor: function () {
            this.classes.red = !this.classes.red;
            this.classes.blue = !this.classes.blue;
          },
          onAddCounter: function () {
            this.counter++;
          },
          onChangeMessage: function () {
            this.message = this.classes.red
              ? `To Blue counter: ${this.counter}`
              : `To Red counter: ${this.counter}`;
          },
        },
        data: function () {
          return {
            counter: 0,
            classes: {
              red: true,
              blue: false,
            },
            message: `To Blue counter: 0`
          };
        },
        template:
          '<button v-on:click="onChangeColor();onAddCounter();onChangeMessage();" v-bind:class="classes">{{ message }}</button>',
      });

      let app = new Vue({
        el: "#app",
      });
    </script>

v-on も好きな関数を実行できるんで、よく使うだろうなぁ。

次にまとめるの

次はばちこり vue/cli でアプリ作ったやつをまとめたい。 まだ勉強中なんでまたまとまってきたら書く・・・。

Vue.js とやらを勉強

ありがたいことにこのご時世でも次の働き先が見つかった。 そういうことで転職の関係で次に使うやーつを勉強中(XAMLじゃないんかーい)。 ということでこれまでやってきたやつを複数回に分けてまとめる。 仕事もあって input も大量にしなきゃでまとめる時間がない。

目次

Vue.jsって

こいつはいっぱいある javascript とかで Web アプリ作るためのフレームワーク

Nuxt.js っていうルーティングのためのフレームワークと組み合わせるけど、まだ Nuxt.js は勉強してないんであとで。

ルーティングは複数ページ扱う Web アプリでページの遷移とかぐちゃぐちゃできる。 Node.js と Express っていうフレームワークとおんなじ。

JavaScript って

割と普通のスクリプト言語なんだけど、Web の物体を動かせる(らしい)。 実はまだそこの動かすとこまでやれてない。

ただ、HTML + CSS + JavaScript はもうセットっぽい。 そこのフレームワークとして React とか Node.js とか Vue.js とかいろんなのが乱立してる。 戦国時代みたい。

スクリプト言語として勉強した印象は、this とか var / let とか巻き返し(だっけ?)とか 面倒なこともあるけど、まぁ普通のスクリプト言語って感じだった。

単一ページで使う Vue.js

Vue.js の使い方もほんといっぱいあるけれど、まずは単一ファイルで使う基本から。

HTML ファイルを用意して、下記を head タグに入れる。

<script src="https://unpkg.com/vue"></script>

単一ならこれで一応使える。 body 部分は以下のようになる。

<body>
    <!-- message 部分を Vue.js で渡す -->
    <div id="app"> {{ message }} </div>

    <!-- vue.js 部分。 javascript で書く。 -->
    <script>
         let app = new Vue({
             el: "#app",
             data: {
                 message: "Hello World!"
             }
         })
    </script>
</body>

script タグ部分は javascript で書いて、message への値反映とかを vue.js がやってくれる感じ。

el プロパティは id 「app」に対して渡さなきゃいけないっぽい。 ( id に「app2」とか用意して複数個やろうとして失敗はした。)

ほんで、data 部分はオブジェクトで書く。今回みたいに HTML 部分に渡したいなら 同じプロパティで用意すること。

data にはオブジェクトを渡せばいいので、関数もいける。

    <script>
         let app = new Vue({
             el: "#app",
             data:  function() {
                 return {
                     message: "Hello World!"
                 }
             }
         })
    </script>

関数なんでアロー関数も書ける。 かなり入力については寛容なようす。

ボタン押して message 部分書き換え

ボタンを押してさっきの message を切り替える。

<body>
  <div id="app">{{ message }}</div>
  <hr>
  <button onclick="onClick();">click</button>

  <script>
    let data = {
      message: 'Hello World',
    }

    let app = new Vue({
      el: '#app',
      data: data,
    });

    function onClick() {
      data.message = 'You Clicked';
    }
  </script>
</body>

onclick 内に実行させたい関数名を書く。 そんで、script 部分にその関数を書くってわけ。

テキストの内容を反映

さくさく行く。次はテキストの内容をそのまんま反映する。 XAML とかでいうところの TextChange イベント。

<body>
  <div id="app">{{ message }}</div>
  <hr>
  <input type="text" oninput="textInput(event);"/>

  <script>
    let data = {
      message: 'Hello World',
    }

    let app = new Vue({
      el: '#app',
      data: data,
    });

    function textInput(event) {
      data.message = event.target.value;
    }
  </script>
</body>

oninput に実行したい関数を引数付きで書く。 event 引数には input タグが紐づいていて、その値を 「event.target.value」で抜き出してる。

次まとめること

v-bind とか v-html, v-if, v-for など HTML の属性として扱う重要なやつを 単一ファイルのものでまとめる。

それらをまとめ終わったら、vue/cli も使っていった vue アプリについてまとめる。

この属性は基本中の基本で大事だし、v-for は知らないと 繰り返しでタグを用意するのがめちゃめちゃに面倒になる。

Windows 11: WSL2 による Ubuntu 環境構築

タイトルのとおり、Winsows 11 で WSL2 の Ubuntu 環境を構築していく。

最初は前回記事みたいに仮想環境でやってたんだけど、「あれ、WSLで全部いいんじゃね?」ってなってしまって、 結局こっちにした。

目次

はじめに

今回は Windows 11 前提で書く。 WIndows 11 はまだプレビュー段階で、Dev Channel に入らないと Windows のアップデートすらできないはず。

なんで Windows 11 の環境でやるかって、この OS のバージョンから WSL の環境構築がむちゃんこ簡単になるから。

ではやっていく。

WSL の Ubuntu 環境構築

まずは Windows Powershell を開く。

下記コマンドを打つ。

wsl install -d Ubuntu-20.04

おわり!! あとは待つだけ。

いやー、簡単すぎる・・・。 ちなみに、インストールできるディストリビューション( WSL でインストールする OS )は 下記のコマンドで見れる。

wsl --list --online

もひとつ、Ubuntuのインストールが終わるとユーザ名とパスワードを設定してくれと言われるのでちゃんと設定する。

注意点として、このインストールは WSL だとできない。 WSL 2 にアップデートする必要がある。 ということで下記にちょろっとまとめておく。

WSL 2 への更新

GUIインストーラでサクッとできるので紹介。 安心と信頼の Microsoft 公式ドキュメント。

https://docs.microsoft.com/ja-jp/windows/wsl/install-win10#step-4---download-the-linux-kernel-update-package

ここの「最新のパッケージをダウンロードします。」からインストーラをダウンロードして実行するだけ。 とっても簡単。

ただ、上記のドキュメントでも紹介しているとおり、WSL 2 を既定のバージョンにするのを忘れずにやっておく。

下記コマンドを実行する。

wsl --set-default-version 2

NATを使って仮想環境(Ubuntu)構築

目次

NATを使って仮想環境構築

NAT(Network Address Translation)を使って仮想環境を構築したんでまとめる。 仮想環境が欲しくなった理由は、Ruby on Rails を勉強しようとして Unix 環境ほしくなったkら。


なんでWindowsじゃないかって、Rubyでキャリッジリターンあるだけでデプロイ失敗して、「うがあああ」ってなったから。


ちなみに、NATって仕組みを使うとグローバルIPアドレスを節約できる。

自身のPC内に閉じたネットワークを作って、その中に仮想環境を置くイメージ。

通信時はホスト側のIPになって、NATがうまいことゲスト側に渡してくれる(自分なりの理解)。


普通だったら外部の仮想スイッチ作って終わりだが、NATでやらないといけなかった理由がある。

NAT を使う理由

これは環境によると思う。自分はNATを使わんといかんかった・・・と思う(他の方法もあったかも)。

とりあえず理由はたった一つ。


別個の ipv4 アドレスが欲しかった


これだけ。こうなった理由が下の二つ。


これの何がいかんて、


ただの外部スイッチで ipv6 設定だと github に繋げなくて開発できない

HWの都合上、外部スイッチはアダプタを共有しないといけなくて、そうするとゲストとホストのIPがかぶる。だからホストからゲストに通信するってことができない。


ってこと。

だもんで、NATで ipv4 のアドレス作って内部で通信して、かつその ipv4github にも接続できるようにしたろって。

仮想環境でできてほしいこと

以下はできるようにしてく。

仮想環境の構築

やることはざっくり以下の3つ!

  1. Ubuntu のインストール
  2. NAT 導入
  3. xrdp 導入
  4. 外部スイッチ導入

Ubuntu のインストール

Ubuntu をインストールする。ここは割愛。

Hyper-V のクイック作成だかなんだかですぐにできる。

NAT の導入

かなーり苦戦した。

手順は以下のとおり。

  1. 内部スイッチつくる
  2. IPとサブネットマスクを設定する
  3. NATネットワークを作成する
  4. 内部スイッチを仮想環境に割り当てる
  5. 仮想環境でIPアドレスを設定する
内部スイッチつくる

管理者権限で powershell を起動する。(別にHyper-VGUI使ってもよし)

以下のコマンドを実行。

new-vmswitch -name XXX -switchtype Internal

XXXのところは自由。

IPとサブネットマスクを設定する

これにはまず ifIndex (InterfaceIndex)が必要なので、下記コマンドで確認する。

get-netadapter

f:id:BackacheEngineer:20210619160800p:plain
get-netadapterの結果(黄色のとこが ifIndex )

新しく作った内部スイッチの ifIndex を確認すること。11 とか、16 の数字部分だけでよい。

その後、下記コマンドで ifIndex のネットワークアダプタに対してIPとサブネットマスクを設定する。 今回は例なのでとりあえず ifIndex は 11 にする。

new-netipaddress -IPAddress 192.168.1.254 -PrefixLength 24 -InterfaceIndex 11

IPアドレスはここでは適当。サブネットマスクは「255.255.255.0」になる 24 。 これで、IPが 254個作れるのかな?(ネットワーク弱者)

個人用だったらぶっちゃけこんないらない。

NATネットワークを作成する

下記のコマンドでNATネットワークを作る。

new-netnat -Name YYY -InternalIPInterfaceAddressPrefix 192.168.1.0/24

YYY のとこは自由。

InternalIPInterfaceAddressPrefix はちゃんと前述した IP アドレスを含むようにする。

※ このNATネットワークは1個までしか作れない

内部スイッチを仮想環境に割り当てる

こっからhyper-vで。

これまでの設定をした内部スイッチを仮想環境のネットワークアダプタに割り当てる。

ここの詳細手順は省略。

仮想環境でIPアドレスを設定する

仮想環境を起動し、仮想環境内で IP アドレスを設定して固定する。

サブネットマスクは 「255.255.255.0」なんで、 「192.168.1.0」から「192.168.1.253」までかな?(ゲートウェイあるんで減るはず?)

例えば、以下のように設定する。

項目 内容
IPアドレス 192.168.1.100
サブネットマスク 255.255.255.0
デフォルトゲートウェイ 192.168.1.254
DNS 8.8.8.8

IPアドレスDNSは割と自由に決めてよし。

サブネットマスクデフォルトゲートウェイは「作った内部スイッチ次第」なんで注意。

確認

以上で NAT の導入は終わり。 ゲストからホストに繋げるし、ホストは外部につなげるはず。

念のため NAT が機能しているかどうかを下記コマンドで確認する。

get-netnatsession

これで何か表示されたらOK。

何も表示されないならなんかおかしい。 再起動かけるとかいろいろする。サブネットマスクとかはちゃんと確認する。

xrdp の導入

これは Windows から Ubuntuリモートデスクトップ接続するために必要。 なんか通信方式が違うらしく、初期状態ではリモートデスクトップではできない。

Ubuntuからターミナルを開き、下記を実行する。

sudo apt install xrdp

これで、インストールだけは完了

firewall の設定変更

ufw でポートの「3389」を許可する。

下記コマンドを入力する。

sudo ufw allow from any to any port 3389 proto tcp

確認する。下記コマンドを入力する。

sudo ufw status

すると以下の結果が返ってくるはず。

f:id:BackacheEngineer:20210620091501p:plain
ufw status


Status: inactive

とか表示されるときは、ufw がそもそも動いてないので、 下記コマンドで起動する。

sudo ufw enable

xrdp.ini の編集

NAT構成だからかなんか知らないが、なんか xrdp の起動にすら失敗するので xrdp.ini を編集する。

エディタはなんでもよいが、デフォの gedit 使う。

sudo gedit /etc/xrdp/xrdp.ini

ファイルが開けたら、「port」の設定してるとこ探して編集する。

port=tcp://[ipv4 address]:3389

ipv4 address」には設定した ipv4 のアドレスを入れる。これ入れないとなんか xrdp 起動しても接続できなかった。

これで起動できるはず。下記コマンドで xrdp を起動する。

sudo systemctr start xrdp

画面の共有設定の変更

Ubuntu の設定から「Sharing」を開き、画面だけ共有するよう設定する。

共有を Active にするために、画面右上にある toggle ボタンで編集可能なよう切り替える(わかりにくいわ!!)。

f:id:BackacheEngineer:20210620092822p:plain
sharing画面

切り替えたら「Screen Sharing」を下記のように設定する。 いろいろアクセスするにはパスワードを要求するようにしている。

f:id:BackacheEngineer:20210620093042p:plain
画面共有の設定

確認

これでリモートデスクトップで接続するための設定が完了する。

Ubuntuからログアウトし、ホスト(Windows)からリモートデスクトップで「 Ubuntu に設定した ipv4 の IP アドレス」に接続する。

ログアウトは xrdp において必須らしい。

引用

xrdp の設定でお世話になった先駆者様。

minokamo.tokyo

最後に高速接続のために

最後に、ipv4 だけだと死ぬほど遅かったので ipv6 の仮想スイッチを作り、仮想環境に割り当てる。

これでネットワークを十分に高速にしたうえで、ipv4 の環境が完成する(はず)。

ただ、おそらく ipv4 が優先されるので、ipv6 を優先してほしい場合は以下の手順で ipv6 で接続するようにする。

  1. 指定ドメインipv6 確認
  2. /etc/hosts の編集
指定ドメインipv6 確認

下記コマンドでサクッと確認できる。 なお、ipv6 がなければ ipv4 でやるしかない・・・。

host [ドメイン]

指定はドメインでいいはず。google.com とか。

/etc/hosts の編集

調べた ip を登録する。下記コマンドでエディタ開いて編集する。

sudo gedit /etc/hosts

ここで、「ip\tドメイン」とか「ip\sドメイン」の書式で追加するだけ。

以上で、全部終わり!!

C#:自作属性を使ってコマンドラインオプション処理を1クラスに

目次

C#の属性を使った処理をまとめる。(転職活動が忙しすぎて更新できんかった・・・) 最初は一般的なことを書くが、後半は 属性を自作して処理を追加するといったことまでまとめる。



属性でできること

属性にできることはいっぱいある。一例を書く。

  • フィールド、プロパティに対してライブラリ側で管理したい処理を任せられる
  • 単体試験で様々なパラメータの調整を任せられる
  • コード内で管理が完結する(はず)


3つ目が、とってもいいことだと思う。 コード内で完結してくれれば無駄に管理物が増えないからだ。 また、コードはGitやSVNなど構成管理をするので、勝手に何がどう変わったかをチェックできる。



属性を使う

割と使うことなるのは下記あたりか。

  • Serializable
  • Obsolete
  • STAThread

最後のは Form アプリケーションやってる人だったらよく見るやつ。

結局ここら辺の属性ってのは、「フィールド・プロパティ・メソッドがなんたるか」、というのを 定義してくれている。

シリアライズ対象だったり、古いやつだったり、シングルスレッドで動かしたりといった具合だ。



自作属性を作る

作り方をまとめる。作るうえで例がないとわかりにくいので、 今回はコマンドラインオプション処理を任せる属性を作る。 ( python ならライブラリあるじゃんww というのはNG。C#が使いたいんだよ!)

コマンドラインオプションでやってほしいのは以下のとおりにする。 全力で作りこむならもっと機能が必要になる。

  • 必須オプションを設定できる
  • オプション名を自由に設定できる
  • 必須オプションの有無を確認する
  • コマンドラインオプションで指定された値はフィールドやプロパティに格納する
  • コマンドラインオプションのヘルプを作れる


先にどんなものを作るのか結果を載せておく。下記のように、フィールドやプロパティに対して 属性を指定することで、コマンドラインオプションと入力された値の格納先を指定する。 そして、コマンドラインオプションのチェックだとかなんかはぜーんぶ1クラスに任せてしまうというもの。

コード。

[CommandLineTargetClass]
class Program
{
    [CommandLineAttr("i", true, "help: input file")]
    public static string InputFile { get; set; }

    [CommandLineAttr("o", false, "help: output file")]
    public static string OutputFile { get; set; } = @"C:\temp\outputFile.txt";

    [CommandLineAttr("test", false, "help: test option")]
    static string privateTest { get; } = "Test!!";

    [CommandLineAttr("mode", false, "help: mode")]
    static int _mode = 0;

    [CommandLineAttr("debug", false, "help: debug mode start")]
    static bool _debug = false;

    static void Main()
    {
        var parser = new CommandLineParser(typeof(Program));

        if (parser.IsHelp)
        {
            Console.WriteLine(parser.HelpMessage);
            return;
        }

        if (parser.IsError)
        {
            Console.WriteLine(parser.ErrorMessage);
            return;
        }

        // main process start ...
        Console.WriteLine($"default: none\t\t\t\t\tvalue: {InputFile}");
        Console.WriteLine($"default: C:\\temp\\outputFile.txt\t\tvalue: {OutputFile}");
        Console.WriteLine($"default: Test!!\t\t\t\tvalue: {privateTest}");
        Console.WriteLine($"default: 0\t\t\t\t\tvalue: {_mode}");
        Console.WriteLine($"default: false\t\t\t\tvalue: {_debug}");
    }
}

使ってみるとこんな感じ

f:id:BackacheEngineer:20210605163545g:plain
コマンドラインオプションのデモ



環境

今回試す環境は下記のとおり。やっとこさ .NET 5 でやるけどもうすぐ .NET 6 くるとかいう絶望。



クラスを作る

まず作るのはカスタム属性クラスである。Attribute クラスを継承したクラスである。

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public sealed class CommandLineAttr : Attribute
{

}


完了だ。 実はもうこれだけで自作属性はできた。もう使える。


次に行く前にいくつか説明を行う。

項目 説明
AttributeUsage 属性を付ける対象を選べる。今回はコマンドラインオプションなので、フィールドとプロパティにした。

AllowMultiple はこの属性を複数つけれるようにするかどうか。今回は1つだけにしたいんで false 。

Inherited は属性を継承させるか。これは「この属性を付けているプロパティなどに対して」という意味。
sealed 継承不可にする便利なやつ
Attribute 継承元クラス。これを継承するだけでなんと自作属性完成だ。


さっき作ったのが「プロパティやフィールドをコマンドラインオプションとして認識するための属性」である。


あともう一個、「そのクラスがコマンドラインオプションを持つクラスかどうか確認するための属性」を作る。 とはいっても作り方はすでに説明したとおり簡単。

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public sealed class CommandLineTargetClass: Attribute
{

}



コンストラクタを考える

作ったクラスのコンストラクタを考える。 こいつが重要なのだ。なぜなら、コンストラクタの引数は属性に渡すパラメータそのものだからである。

今回は3つ必要になる。

  • string引数:オプション文字列を指定できるように
  • bool引数:必須かどうかを指定できるように
  • string引数:オプションのヘルプメッセージを指定できるように


まずはコマンドラインオプション側から。

/// <summary>
/// deal Command line option Attribute
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public sealed class CommandLineAttr : Attribute
{
    static bool? _inherited;

    /// <summary>
    /// CLOption Name
    /// </summary>
    public string CLOptionName { get; }
    /// <summary>
    /// Is Option Required
    /// </summary>
    public bool Required { get; }
    /// <summary>
    /// Help Message
    /// </summary>
    public string HelpMessage { get; }
    /// <summary>
    /// Is Inherited Property of AttributeUsage
    /// </summary>
    public static bool Inherited
    {
        get
        {
            if (_inherited != null) return _inherited.Value;
            var attr = typeof(CommandLineAttr).GetCustomAttributes(typeof(AttributeUsageAttribute), true)[0] as AttributeUsageAttribute;
            _inherited = attr.Inherited;
            return _inherited.Value;
        }
    }

    /// <summary>
    /// Constructor
    /// </summary>
    /// <param name="clOptionName">Command line option name</param>
    /// <param name="required"> Is option required </param>
    /// <param name="helpMessage">Help message</param>
    public CommandLineAttr(string clOptionName, bool required, string helpMessage = null)
    {
        CLOptionName = clOptionName;
        Required = required;
        HelpMessage = helpMessage;
    }
}



続いてコマンドラインターゲットクラス。

/// <summary>
/// this attribute is used by developers to check Command Line Attributes for properties or fields in Class..
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public sealed class CommandLineTargetClass : Attribute
{
    static bool? _inherited;

    /// <summary>
    /// Is Inherited Property of AttributeUsage
    /// </summary>
    public static bool Inherited
    {
        get
        {
            if (_inherited != null) return _inherited.Value;
            var attr = typeof(CommandLineAttr).GetCustomAttributes(typeof(AttributeUsageAttribute), true)[0] as AttributeUsageAttribute;
            _inherited = attr.Inherited;
            return _inherited.Value;
        }
    }
}


Inheritedプロパティはあとあと使うので作った。まぁ、正直いらないような気もしてる。



機能を追加する

残りの機能を作っていこう。あとは以下の3機能だ。

  • 必須オプションを設定できる
  • オプション名を自由に設定できる
  • 必須オプションの有無を確認する
  • コマンドラインオプションで指定された値はフィールドやプロパティに格納する
  • コマンドラインオプションのヘルプを作れる

さて、さっき作ったのはただの属性だ。 今回作る機能は Parser クラスを作って実行されることにする。

public sealed class CommandLineParser
{
}

このクラスにどんどん機能を盛り込んでいく。まず、「必須オプションの有無を確認する機能」から。



必須オプションの有無を確認する機能

この機能にまず必要なのは以下の2点。

  1. 入力されたオプション引数を取得する
  2. クラスに設定された「CommandLineAttr」属性を取得する
  3. Requiredプロパティを確認しながら、引数に必須オプションがあるかどうか確認する


1はだいぶ簡単に取得できる。 下記で終わりだ。

Args = Environment.GetCommandLineArgs()[1..]; // First arg is EXE file name.

.NET 5 なので配列の範囲指定を使用している。たしかC# 8.0 以降の機能なんで、 .NET Framework 4.8 の場合は適宜変更が必要(フレームワークによってデフォのC#バージョンが決まるため)。

問題は2と3である。 これはまず細分化する。

  • クラスから、フィールド・プロパティを全部取得する
  • 取得したフィールド・プロパティから、自作した CommandLineAttr 属性のインスタンスを取得する
  • 取得した CommandLineAttr インスタンスの「Required」プロパティを確認する
  • 1で取得した引数と比較し、Requiredプロパティが true の引数があるか確認する

である。


まずは全部のフィールド・プロパティを取得するものから。 とはいっても、できるだけ最小限にしたいので「CommandLineAttr」属性が 引っ付いてるもののみにする。

IEnumerable<MemberInfo> getAllCommandLineMember()
{
    var memlist = new List<MemberInfo>();
    memlist.AddRange(_targetType.GetRuntimeFields());
    memlist.AddRange(_targetType.GetRuntimeProperties());

    foreach (var member in memlist)
    {
        if (member.GetCustomAttribute(typeof(CommandLineAttr), CommandLineAttr.Inherited) == null) continue;
        yield return member;
    }
}

「_target」は「CommandLineTargetClass」属性がついているクラスのTypeインスタンスである。 「GetRuntimeFields」と「GetRuntimeProperties」でフィールドとプロパティを全部取得する。

private だの public だの static だのは関係なくすべて取得できる。

そして、次の foreach で「CommandLineAttr」属性を持つものだけを返す。 「GetCustomAttribute」メソッドで「CommandLineAttr」属性が抜き出せるかどうかチェックしている。 抜き出せない場合は null が返ってくる。抜き出せたら、「CommandLineAttr」属性を持っているということ。


次に「CommandLineAttr」属性のインスタンスを取得する。 さっきのメソッド使って foreach し、「GetCustomAttribute」メソッドで取得していく。

IEnumerable<CommandLineAttr> getCLAttrs()
{
    foreach (var mem in getAllCommandLineMember())
    {
        yield return mem.GetCustomAttribute(typeof(CommandLineAttr), CommandLineAttr.Inherited) as CommandLineAttr;
    }
}

さっきと似たような「GetCustomAttribute」の書き方をしてわざわざ別メソッドにしてるのは、 「getAllCommandLineMember」メソッドは「コマンドラインオプションで指定された値をフィールドやプロパティに代入する」際にも 使いたいからである。


最後に、Required プロパティを確認しながらチェックしていく。

const string c_Prefix = "-";
readonly string _pleaseHelp = $"\n\nPlease reference help that you specify \" {c_Prefix}h or {c_Prefix}help\"";
List<CommandLineAttr> _commandLineAttrList;

// 「CommandLineAttr」属性インスタンスを全部取得するプロパティ
List<CommandLineAttr> CommandLineAttrList
{
    get
    {
        if (_commandLineAttrList != null) return _commandLineAttrList;
        _commandLineAttrList = getCLAttrs().ToList();
        return _commandLineAttrList;
    }
}

// コマンドラインをチェックするメソッド
bool chkCommadLine()
{
    // check required option 
    if (chkRequired(CommandLineAttrList) == false) return false;

    return true;
}

// 必須オプションが指定されているかチェックするメソッド
bool chkRequired(IEnumerable<CommandLineAttr> allCommandLineAttrs)
{
    foreach (var optName in allCommandLineAttrs.Where(x => x.Required).Select(x => $"{c_Prefix}{x.CLOptionName}"))
    {
        if (Args.Contains(optName) == false)
        {
            ErrorMessage = $"Error: Option {optName} is not specified.{_pleaseHelp}";
            return false;
        }
    }
    return true;
}

最後の「chkRequired」メソッドが今回の目的のメソッドである。 foreach でループを回す際に、「Required」プロパティが true の「CommandLineAttr」属性に絞り込み、 さらにそれをユーザが指定したコマンドライン引数と同じ形になるよう Select してやる。

Selectしたやつが Args というコマンドライン引数に「存在しない」なら、 必須オプションが指定されていないということである。 その時はエラーメッセージを設定し、false を返す。 すべての必須オプションが存在すれば true を返す。



コマンドラインオプションで指定された値をフィールドやプロパティに格納する

次に、各フィールドやプロパティにコマンドライン引数で指定された値を格納する機能を作る。 まずは細分化、もとい簡単なアルゴリズムを。

  1. 「CommandLineAttr」属性が付いたフィールド・プロパティを取得する
  2. 「CommandLineAttr」属性から必要なオプション名を作成する
  3. 2のオプション名がコマンドライン引数に存在するかチェックする
  4. 存在する場合、その時指定されている値をキャストし、フィールド・プロパティに格納する
  5. 存在しない場合、何もしない


1は既にメソッドとして作った。 2は「GetCustomAttribute」メソッドから「CommandLineAttr」属性のインスタンスを取得し、作る。

なので、ここまでは簡単。

次からがちょっと厄介だが、今回は4を実現するために辞書を作ることにした。

Keyには入力されたコマンドライン引数のオプション(ex. -i , -o など)、

Valueにはその時指定された内容(ex. -i input.txt の input.txt 側)を格納する。

これで、TryGetValue メソッドを使っていろいろできるようになる。

以下がそのコードである。

// プロパティ・フィールドに値を格納するメソッド
void setValueToPropAndField()
{
    foreach (var mem in getAllCommandLineMember())
    {
        var option = $"{c_Prefix}{(mem.GetCustomAttribute(typeof(CommandLineAttr)) as CommandLineAttr).CLOptionName}";

        Type memType;
        if (mem is PropertyInfo pi)
        {
            memType = pi.PropertyType;
            if (tryGetOptionValue(memType, option, out var newVal) == false) continue;
            pi.SetValue(null, newVal);
        }
        else if (mem is FieldInfo fi)
        {
            memType = fi.FieldType;
            if (tryGetOptionValue(memType, option, out var newVal) == false) continue;
            fi.SetValue(null, newVal);
        }
    }
}

Dictionary<string, string> _optValueDic;
// 入力されたコマンドライン引数をキー、その時指定された内容をValueに持つ辞書のプロパティ
Dictionary<string, string> OptValueDic
{
    get
    {
        if (_optValueDic != null) return _optValueDic;
        _optValueDic = new Dictionary<string, string>();
        var opts = CommandLineAttrList.Select(x => $"{c_Prefix}{x.CLOptionName}").ToList();
        var beforeOpt = string.Empty;
        foreach (var arg in Args)
        {
            if (opts.Contains(arg))
            {
                // if options are duplicated, Exceptin occur.
                _optValueDic.Add(arg, string.Empty);
                beforeOpt = arg;
            }
            else
            {
                _optValueDic[beforeOpt] = arg;
            }
        }
        return _optValueDic;
    }
}

// 格納前に値をキャストするメソッド
bool tryGetOptionValue(Type memberType, string option, out object val)
{
    val = null;
    // implement other type after days ...
    if (memberType == typeof(int))
    {
        if (OptValueDic.TryGetValue(option, out var value) == false) return false;
        val = int.Parse(value);
    }
    else if (memberType == typeof(double))
    {
        if (OptValueDic.TryGetValue(option, out var value) == false) return false;
        val = double.Parse(value);
    }
    else if (memberType == typeof(string))
    {
        if (OptValueDic.TryGetValue(option, out var value) == false) return false;
        val = value;
    }
    else if (memberType == typeof(bool)) val = OptValueDic.TryGetValue(option, out _);
    else return false;

    return true;
}


今回、辞書を作る過程で「重複したオプションがあると例外が発生する」ようになっている。 もしここでそんなことが起きてほしくない場合は、別途メソッドを追加する必要がある。

また、値をキャストする「tryGetOptionValue」メソッドはキャスト対応が間に合っていない。 別に List 型だってやろうと思えばできるのである。しかし、今回は最低限として、

  • int
  • double
  • string
  • bool

とした。

memType はフィールド・プロパティのタイプのことである。このタイプに沿ってキャストする。 見てみるとわかるが、int や double は変換に失敗すると例外がでる。


今までの2機能のまとめ

今までの2機能を CommandLineParser クラスのコンストラクタで呼び出す。 これにより、 CommandLineParser クラスのインスタンスを作った時点で 「必須オプションのチェック」、「フィールド・プロパティに対する引数値の代入」が 完了することになる。

public CommandLineParser(Type targetType)
{
    _targetType = targetType;
    if (_targetType == null) throw new ArgumentNullException();
    if (isDefineCommandTargetAttr() == false) throw new Exception($"Type[{_targetType.Name}] does not have {nameof(CommandLineTargetClass)}.");

    Args = Environment.GetCommandLineArgs()[1..]; // First arg is EXE file name.

    IsError = chkCommadLine() == false;

    // implement checking args method after days ...

    setValueToPropAndField();
}

// CommandLineTargetClass 属性が存在するかどうか
bool isDefineCommandTargetAttr()
{
    return _targetType?.GetCustomAttributes(typeof(CommandLineTargetClass), CommandLineTargetClass.Inherited).Length != 0;
}


コンストラクタの引数は「CommandLineTargetClass」属性を付与したクラスの Type インスタンスである。 「CommandLineTargetClass」属性がついてるかどうかのチェックメソッドも追加した。



ヘルプメッセージを出力する機能

最後の機能はヘルプメッセージを出力する機能である。

「-h」とか指定すると表示されるやつ。

「CommandLineAttr」属性の引数にヘルプメッセージ用の string 型がいるのは、 ヘルプメッセージのフォーマットを固定にして、そのオプションの説明を指定できるようにしたかったから。

そしてこれをプロパティで取得できるようにすることで、デバッグ時にコピペできるようにもする。

出力する情報は以下のとおり。

  • ツール名や会社名、説明などアセンブリに登録された情報
  • 「usage : ...」ってやつ
  • 必須オプションだけは usage の時に出力する
  • 必須じゃないオプションは「[ options ... ]」でひとまとめにする
  • 各オプションのヘルプメッセージ
  • ヘルプ表示するにはどのオプションを指定するか
// ヘルプメッセージのプロパティ
public string HelpMessage
{
    get
    {
        var sb = new StringBuilder();

        sb.AppendLine(getAssemblyInfo());

        sb.Append($"usage: {getAssemblyName()} ");
        var allAttrs = CommandLineAttrList;
        var reqOpts = allAttrs.Where(x => x.Required).Select(x => $"{c_Prefix}{x.CLOptionName}").ToList();
        if (reqOpts.Count != 0)
        {
            foreach (var reqOpt in reqOpts)
            {
                sb.Append($"{reqOpt} {{ param }} ");
            }
        }
        sb.AppendLine($"[ options ... ]");

        sb.AppendLine();

        foreach (var attr in allAttrs.OrderBy(x => x.CLOptionName.Length))
        {
            sb.AppendLine($"{c_Prefix}{attr.CLOptionName}\t: {attr.HelpMessage}");
        }

        sb.AppendLine();

        sb.AppendLine("Display this message.");
        sb.AppendLine($"{c_Prefix}h");
        sb.AppendLine($"{c_Prefix}help");

        return sb.ToString();
    }
}

// アセンブリに設定された「ツール名」「バージョン」「会社名」「著作権」「ツールの説明」を抜き出して文字列にするメソッド
string getAssemblyInfo()
{
    var assembly = Assembly.GetAssembly(_targetType);
    if (assembly == null) return string.Empty;

    var sb = new StringBuilder();
    var fvi = FileVersionInfo.GetVersionInfo(assembly.Location);

    // FileDescription is Title.
    sb.AppendLine($"{fvi.FileDescription} {fvi.FileVersion} {fvi.CompanyName} {fvi.LegalCopyright}");
    sb.AppendLine(fvi.Comments);

    return sb.ToString();
}

// アセンブリに設定されたアプリケーションの名前を取得するメソッド
string getAssemblyName()
{
    var assembly = Assembly.GetAssembly(_targetType);
    if (assembly == null) return string.Empty;
    return assembly.GetName().Name;
}


メッセージは StringBuilder を使って作る。

いろんなツールでこの機能が使えないと不便なので、 アセンブリから会社名とかを抜き出し、 StringBuilder に格納する。 これにより、適宜アセンブリを設定するだけでヘルプメッセージが変わってくれるようになる。 これでツール説明部分が完了である。

次に、これまでに作った「CommandLineAttrList」プロパティを使って「CommandLineAttr」属性を全部抜き出す。 そこから、必須属性だけを抜き出し、それをメッセージ用の StringBuilder に追加してやる。 あとはサクッと「[ options ... ]」を追加してやって usage 部分は完了。

次に、各オプションのヘルプメッセージ部分。 「CommandLineAttrList」プロパティで全部抜き出してるんで、それで foreach を回し、ヘルプメッセージ参照するだけである。

今回は長さでソートしてみた。本当はアルファベット順とか、必須のやつは先頭とか別のソートのがいいのかもしれない。 必須のオプションとそうでないオプションをグループ分けして表示するのもよさそうである。 ここはすごい改良の余地が残る。

最後にヘルプメッセージ表示のためのオプションを足してヘルプメッセージは完了である。



ヘルプメッセージかどうかの判定

とっても簡単だが、指定オプションがヘルプかどうかのプロパティも載せておく。

public bool IsHelp
{
    get
    {
        if (Args == null || Args.Any() == false) return false;
        return Args.Any(x => x == $"{c_Prefix}h" || x == $"{c_Prefix}help");
    }
}



CommandLineParser の全体コード

全体コードも載せる。git にも上げてるけど、ブログはブログで完結しててほしいんで。

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Text;

namespace CLOptionLib
{
    /// <summary>
    /// command line parser
    /// </summary>
    public sealed class CommandLineParser
    {
        #region private member
        Type _targetType;
        const string c_Prefix = "-";
        readonly string _pleaseHelp = $"\n\nPlease reference help that you specify \" {c_Prefix}h or {c_Prefix}help\"";
        Dictionary<string, string> _optValueDic;
        List<CommandLineAttr> _commandLineAttrList;
        #endregion

        #region property
        /// <summary>
        /// Arguments
        /// </summary>
        public IEnumerable<string> Args { get; }
        /// <summary>
        /// Did Command Line Error occur.
        /// </summary>
        public bool IsError { get; private set; }
        /// <summary>
        /// error message
        /// </summary>
        public string ErrorMessage { get; private set; }
        /// <summary>
        /// exist help command
        /// </summary>
        public bool IsHelp
        {
            get
            {
                if (Args == null || Args.Any() == false) return false;
                return Args.Any(x => x == $"{c_Prefix}h" || x == $"{c_Prefix}help");
            }
        }
        /// <summary>
        /// help message
        /// </summary>
        public string HelpMessage
        {
            get
            {
                var sb = new StringBuilder();

                sb.AppendLine(getAssemblyInfo());

                sb.Append($"usage: {getAssemblyName()} ");
                var allAttrs = CommandLineAttrList;
                var reqOpts = allAttrs.Where(x => x.Required).Select(x => $"{c_Prefix}{x.CLOptionName}").ToList();
                if (reqOpts.Count != 0)
                {
                    foreach (var reqOpt in reqOpts)
                    {
                        sb.Append($"{reqOpt} {{ param }} ");
                    }
                }
                sb.AppendLine($"[ options ... ]");

                sb.AppendLine();

                foreach (var attr in allAttrs.OrderBy(x => x.CLOptionName.Length))
                {
                    sb.AppendLine($"{c_Prefix}{attr.CLOptionName}\t: {attr.HelpMessage}");
                }

                sb.AppendLine();

                sb.AppendLine("Display this message.");
                sb.AppendLine($"{c_Prefix}h");
                sb.AppendLine($"{c_Prefix}help");

                return sb.ToString();
            }
        }
        /// <summary>
        /// command line attr of target type
        /// </summary>
        List<CommandLineAttr> CommandLineAttrList
        {
            get
            {
                if (_commandLineAttrList != null) return _commandLineAttrList;
                _commandLineAttrList = getCLAttrs().ToList();
                return _commandLineAttrList;
            }
        }
        /// <summary>
        /// Key: option
        /// <para>Value: argument value</para>
        /// </summary>
        Dictionary<string, string> OptValueDic
        {
            get
            {
                if (_optValueDic != null) return _optValueDic;
                _optValueDic = new Dictionary<string, string>();
                var opts = CommandLineAttrList.Select(x => $"{c_Prefix}{x.CLOptionName}").ToList();
                var beforeOpt = string.Empty;
                foreach (var arg in Args)
                {
                    if (opts.Contains(arg))
                    {
                        // if options are duplicated, Exception occur.
                        _optValueDic.Add(arg, string.Empty);
                        beforeOpt = arg;
                    }
                    else
                    {
                        _optValueDic[beforeOpt] = arg;
                    }
                }
                return _optValueDic;
            }
        }
        #endregion

        #region Get assembly name
        /// <summary>
        /// Get assembly name
        /// </summary>
        /// <returns>assembly name of exe</returns>
        string getAssemblyName()
        {
            var assembly = Assembly.GetAssembly(_targetType);
            if (assembly == null) return string.Empty;
            return assembly.GetName().Name;
        }
        #endregion

        #region Get assembly info
        /// <summary>
        /// Get assembly info.
        /// <para>Tool Title</para>
        /// <para>Tool Version</para>
        /// <para>CompanyName</para>
        /// <para>LecalCopyright</para>
        /// <para>Comments(tool description)</para>
        /// </summary>
        /// <returns>string : {title version company legalCopyright \n comments}</returns>
        string getAssemblyInfo()
        {
            var assembly = Assembly.GetAssembly(_targetType);
            if (assembly == null) return string.Empty;

            var sb = new StringBuilder();
            var fvi = FileVersionInfo.GetVersionInfo(assembly.Location);

            // FileDescription is Title.
            sb.AppendLine($"{fvi.FileDescription} {fvi.FileVersion} {fvi.CompanyName} {fvi.LegalCopyright}");
            sb.AppendLine(fvi.Comments);

            return sb.ToString();
        }
        #endregion

        #region ctor
        /// <summary>
        /// ctor
        /// </summary>
        /// <param name="targetType">Type with specified CommandLineTargetClass Attribute</param>
        /// <exception cref="ArgumentNullException"/>
        /// <exception cref="Exception"/>
        public CommandLineParser(Type targetType)
        {
            _targetType = targetType;
            if (_targetType == null) throw new ArgumentNullException();
            if (isDefineCommandTargetAttr() == false) throw new Exception($"Type[{_targetType.Name}] does not have {nameof(CommandLineTargetClass)}.");

            Args = Environment.GetCommandLineArgs()[1..]; // First arg is EXE file name.

            IsError = chkCommadLine() == false;

            // implement checking args method after days ...

            setValueToPropAndField();
        }
        #endregion

        #region Check having CommandLineTargetClass Attribute
        /// <summary>
        /// check having CommandLineTargetClass Attribute.
        /// </summary>
        /// <returns>true: has CommandLineTargetClass, false: does not have CommandLineTargetClass</returns>
        bool isDefineCommandTargetAttr()
        {
            return _targetType?.GetCustomAttributes(typeof(CommandLineTargetClass), CommandLineTargetClass.Inherited).Length != 0;
        }
        #endregion

        #region Check Command Line Options
        /// <summary>
        /// Check Command Line Options.
        /// </summary>
        /// <returns>true: OK, false: NG</returns>
        bool chkCommadLine()
        {
            // check required option 
            if (chkRequired(CommandLineAttrList) == false) return false;

            return true;
        }
        #endregion

        #region Get Command Line Attrs
        /// <summary>
        /// Get Command Line Attrs of MemberInfo
        /// </summary>
        /// <returns>Command Line Attr Objects</returns>
        IEnumerable<CommandLineAttr> getCLAttrs()
        {
            foreach (var mem in getAllCommandLineMember())
            {
                yield return mem.GetCustomAttribute(typeof(CommandLineAttr), CommandLineAttr.Inherited) as CommandLineAttr;
            }
        }
        #endregion

        #region Get Command Line Member
        /// <summary>
        /// Get Command Line Member.
        /// </summary>
        /// <returns></returns>
        IEnumerable<MemberInfo> getAllCommandLineMember()
        {
            var memlist = new List<MemberInfo>();
            memlist.AddRange(_targetType.GetRuntimeFields());
            memlist.AddRange(_targetType.GetRuntimeProperties());

            foreach (var member in memlist)
            {
                if (member.GetCustomAttribute(typeof(CommandLineAttr), CommandLineAttr.Inherited) == null) continue;
                yield return member;
            }
        }
        #endregion

        #region check required option
        /// <summary>
        /// Check required option
        /// </summary>
        /// <param name="allCommandLineAttrs">All Command Line Attrs</param>
        /// <returns>true: OK, false: NG</returns>
        bool chkRequired(IEnumerable<CommandLineAttr> allCommandLineAttrs)
        {
            foreach (var optName in allCommandLineAttrs.Where(x => x.Required).Select(x => $"{c_Prefix}{x.CLOptionName}"))
            {
                if (Args.Contains(optName) == false)
                {
                    ErrorMessage = $"Error: Option {optName} is not specified.{_pleaseHelp}";
                    return false;
                }
            }
            return true;
        }
        #endregion

        #region set value to property and field
        /// <summary>
        /// set value to property and field
        /// </summary>
        void setValueToPropAndField()
        {
            foreach (var mem in getAllCommandLineMember())
            {
                var option = $"{c_Prefix}{(mem.GetCustomAttribute(typeof(CommandLineAttr)) as CommandLineAttr).CLOptionName}";

                Type memType;
                if (mem is PropertyInfo pi)
                {
                    memType = pi.PropertyType;
                    if (tryGetOptionValue(memType, option, out var newVal) == false) continue;
                    pi.SetValue(null, newVal);
                }
                else if (mem is FieldInfo fi)
                {
                    memType = fi.FieldType;
                    if (tryGetOptionValue(memType, option, out var newVal) == false) continue;
                    fi.SetValue(null, newVal);
                }
            }
        }
        #endregion

        #region option value after cast type
        /// <summary>
        /// option value after cast type.
        /// </summary>
        /// <param name="memberType">member(property or field) type</param>
        /// <param name="option">option name(Key of OptValueDic)</param>
        /// <param name="val">out value</param>
        /// <returns>true: success getting, false: fail getting</returns>
        bool tryGetOptionValue(Type memberType, string option, out object val)
        {
            val = null;
            // implement other type after days ...
            if (memberType == typeof(int))
            {
                if (OptValueDic.TryGetValue(option, out var value) == false) return false;
                val = int.Parse(value);
            }
            else if (memberType == typeof(double))
            {
                if (OptValueDic.TryGetValue(option, out var value) == false) return false;
                val = double.Parse(value);
            }
            else if (memberType == typeof(string))
            {
                if (OptValueDic.TryGetValue(option, out var value) == false) return false;
                val = value;
            }
            else if (memberType == typeof(bool)) val = OptValueDic.TryGetValue(option, out _);
            else return false;

            return true;
        }
        #endregion
    }
}



まとめ

属性でできることや、自作属性の作り方、また自作属性を使ったコマンドラインオプションをまとめた。 属性を使ってできることはもっともっとあるだろうし、理解しておくとすごいできることが広がる(と思うのでまとめて考えを整理)。


ただ注意したいのは、今回作った CommandLineParser クラスは肝心要の「引数で指定した値のチェック」や 「重複オプションのチェック」などはしていない。それらは適宜追加が必要である。 ただ、これも列挙型を作り「このコマンドラインオプションに来る値の種類」を指定できるようにすると、 「引数で指定した値のチェック」もできるようになる。

例えば、ファイルの存在有無とか。

また、最近では「runコマンドに対するヘルプ」といった単位でヘルプを参照できる。 「docker run -h」などで「run のヘルプ」が見れるといった形である。 こういったモダンな感じにするのも忘れてはいけないと思う。


以上で属性のまとめ。 結構長くなってしまった。 自分も使っていこうかなぁと考えながら作ったので、このコマンドラインオプションを認識するコードはgitから見れるという。

ダウンロードもしやすいようにリポジトリ作った。これでブランチも作りやすいね。

https://github.com/akadamario/CommandLineParser

XAML: カスタムコントロール(今風なToggleButton)

いよいよもってカスタムコントロールである。ControlTemplate の知識等の集大成に近い。 なお、ほかにもいろいろな知識がいるので、結局こいつは難しい部類の印象。 そして、これをまとめるからには「ResourceDictionary」もまとめないといけない。 実のところ、外観だけなら「ResourceDictionary」のが適していると思う。

カスタムコントロールを使う場面は「独自のバインディングターゲットを作りたい」、これに尽きると思う。 このバインディングターゲットを作りたいというのは、「元のプロパティが依存関係プロパティじゃないから、カスタムコントロールで仕方なく依存関係プロパティにしなくちゃいけない」も含まれる。

独自バインディングターゲットはいらない、ってなれば、まぁResourceDictionaryでいいかなぁと個人的には思う。

目次

カスタムコントロール

カスタムコントロールというのは、言ってしまえば自作コントロールである。 ユーザコントロールとちょっと似ているけれど、明確に違うところがある。

機能 バインディングターゲット作成 Name属性付与 内部コード実装 外観の実装
カスタムコントロール
ユーザコントロール ×
ResourceDictionary × ×

しれっと ResourceDictionary いるけどこれも入れておいた方がよさそうだったので入れた。 他にも差異がありそうな項目がいそうだが、とりあえずこれだけで。

見てのとおり、1点明確な違いはName属性が付与できるかできないかである。 Name属性については過去にまとめた。

backacheengineer.hatenablog.com

したがって、Name属性をつけられるという点においてはカスタムコントロールはライブラリ向きということになる。 そのツール独自の画面でいい、となるとまぁユーザコントロールでよいだろう。 バインディングターゲットが必要ないのであれば、ユーザコントロールである必要もなく、ResourceDictionary を使えばよい。

デザインを作るうえでの参考

デザインを作るのは困難だと思う。参考になるデザインは他のツールか、 Windowsの設定とかのデザインを見ればなんとなーくふわっと「こんなの作りたい」というのは出てくる。 けれども、ControlTemplate でそれを実現しようとなると普通に難しいという・・・。

そういう時には generic.xaml というファイルが下記フォルダにいるはずなので参考になる(はず)。 途中の番号部分はたぶんwin10のビルド番号かなんかなので、人によって違う。

C:\Program Files (x86)\Windows Kits\10\DesignTime\CommonConfiguration\Neutral\UAP\10.0.19041.0\Generic

こいつには VisualStateManager とか ツールのテーマカラーとかがなんと定義されているのだ。 おーすごい。ただ量もえげつない。でも参考になる(はず)。

今風な ToggleButton を作る

ということでカスタムコントロールで今風な ToggleButton を作ってみる。 普通の ToggleButton だと押してある状態がON、そうでないときOFFみたいなボタンなんだけれども、 今風だとなんかバーになってると思う。それを作る。

既に作ったのでまずは結果から。まだまだ改良の余地ありなやつだが、とりあえずできたんで・・・。

f:id:BackacheEngineer:20210507173552g:plain
今風なToggleButton

必要な機能

必要な機能は以下のとおり。実際に使う場合も含めて考えてる。

  1. ON状態かOFF状態か判別可能なテキストを表示する。
  2. このテキストは設定で非表示にできる。
  3. ON・OFFの文字列は設定で変更できる。(ON、OFFが常にツールとして最良とは限らないため)
  4. デフォはテキスト表示、ON/OFF の文字列とする。
  5. トグルボタンの外観は今風なバー形式。
  6. ON時は青、OFF時は赤の背景を使用する。
  7. バー部分をクリックすると、ちゃんとバーが動いているように見せる。

1 は TextBlock をそのまま使う。

テキストの切り替えは Trriger でサクッと。

2 はソースとなる変数を作って設定する。ターゲットは「Visibility」というenum。 これなら表示状態、非表示状態、非表示かつ描画領域非確保状態を扱える。

3 はソースとなる変数を作って設定する。ターゲットはもちろん Text である。

4 は内部コード内で初期化する。

5 は Slider を使って実装していく。

6 は Slider の外観を変更して対応する。

7 はどうしようか悩んだ結果、透明な ToggleButton をバーにかぶせることにした(おい)。 これでクリック動作は確保できるので、あとは VisualState を実装してやって、自分で動かす。

XAMLC#コード

まずはXAMLから。 まとめていて気が付いたけど、これサイズが変更できないのでは・・・。とりあえずこのままで。 本当に改善の余地が多いなぁ。

コメントが一切ないのは、カスタムコントロールに対してコピペする可能性を考慮した。 日本語含むカスタムコントロールの「Generic.xaml」はビルドに失敗するんで。 (1文足せば日本語も使える。単純に「UTF-8」しかできないのが問題で、なんか「shift-jis」もいけるように1文だけ足せばいいはず)

<ControlTemplate x:Key="sliderTemplate" TargetType="Slider">
    <Grid Width="70" Height="40">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>

        <Rectangle Name="checkedRect"
                   Grid.Column="2"
                   Fill="Red">
        </Rectangle>

        <Thumb Name="barThumb"
                 Grid.Column="1"
                 Width="24">
        </Thumb>

        <Rectangle Name="uncheckedRect"
                   Grid.Column="0"
                   Width="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Slider}, Path=Value}"
                   Fill="Blue"/>
    </Grid>
</ControlTemplate>

<Style TargetType="{x:Type local:ModernToggle}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:ModernToggle}">
                <Grid HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
                      VerticalAlignment="{TemplateBinding VerticalAlignment}"
                      Margin="{TemplateBinding Margin}">

                    <VisualStateManager.VisualStateGroups>
                        <VisualStateGroup x:Name="CheckedState">
                            <VisualState x:Name="UnChecked">
                                <Storyboard>
                                    <DoubleAnimation Storyboard.TargetName="slider"
                                                     Storyboard.TargetProperty="Value"
                                                     To="0" Duration="0:0:0.2"/>
                                </Storyboard>
                            </VisualState>
                            <VisualState x:Name="Checked">
                                <Storyboard>
                                    <DoubleAnimation Storyboard.TargetName="slider"
                                                     Storyboard.TargetProperty="Value"
                                                     To="46" Duration="0:0:0.2"/>
                                </Storyboard>
                            </VisualState>
                        </VisualStateGroup>
                    </VisualStateManager.VisualStateGroups>
                    
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="Auto" />
                        <ColumnDefinition Width="Auto" />
                    </Grid.ColumnDefinitions>

                    <TextBlock Grid.Column="0"
                               Name="stateText"
                               Text="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},Path=UnCheckedStateText}"
                               FontSize="{TemplateBinding FontSize}"
                               Margin="24"
                               Visibility="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},Path=StateTextVisibility}"
                               HorizontalAlignment="Center"
                               VerticalAlignment="Center"/>
                    
                    <Slider Grid.Column="1"
                            Name="slider"
                            Template="{StaticResource sliderTemplate}"
                            Value="0"
                            Maximum="46"/>

                    <ToggleButton Name="toggle"
                                  IsChecked="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=IsChecked}"
                                  Grid.Column="1"
                                  Opacity="0"/>
                </Grid>

                <ControlTemplate.Triggers>
                    <DataTrigger Binding="{Binding ElementName=toggle, Path=IsChecked}" Value="True">
                        <Setter TargetName="stateText" Property="Text" Value="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=CheckedStateText}"/>
                    </DataTrigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

続いてC#コード。 ここで注意すべきなのが、「バインディングソースは依存関係プロパティじゃなくていいこと」。 別に依存関係プロパティで定義してもいいんだけど、無駄なコードは避けておきたい。 継承元が ContentControl なので、IsChecked を実装している。 実は ToggleButton を継承すると IsChecked は独自実装する必要ないんだけど、VisualState の操作がうまくできなかった。 力不足だ。この継承をうまいこと使えれば、たぶんだが VisualState をもっと楽できる。

public class ModernToggle : ContentControl
{
    public event EventHandler CheckedChanged;

    /// <summary>
    /// text of checked state 
    /// </summary>
    public string CheckedStateText { get; set; } = "ON";
    /// <summary>
    /// text of unchecked state
    /// </summary>
    public string UnCheckedStateText { get; set; } = "OFF";
    /// <summary>
    /// visibility statetext
    /// </summary>
    public Visibility StateTextVisibility { get; set; } = Visibility.Visible;

    /// <summary>
    /// dependencyproperty IsChecked
    /// </summary>
    public static DependencyProperty IsCheckedProperty { get; private set; }
    /// <summary>
    /// true: Checked, false: Unchecked
    /// </summary>
    public bool IsChecked
    {
        get => (bool)GetValue(IsCheckedProperty);
        set => SetValue(IsCheckedProperty, value);
    }

    /// <summary>
    /// static constructor
    /// </summary>
    static ModernToggle()
    {
        IsCheckedProperty = DependencyProperty.Register(nameof(IsChecked), typeof(bool), typeof(ModernToggle), new PropertyMetadata(false, OnCheckedChanged));
    }

    /// <summary>
    /// event of CheckedChanged for DependencyProperty
    /// </summary>
    static void OnCheckedChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
    {
        (obj as ModernToggle).OnCheckedChanged(EventArgs.Empty);
    }

    /// <summary>
    /// event of CheckedChanged 
    /// </summary>
    protected virtual void OnCheckedChanged(EventArgs args)
    {
        VisualStateManager.GoToState(this, IsChecked == true ? "Checked" : "UnChecked", true);
        CheckedChanged?.Invoke(this, args);
    }
}

まとめ

  • カスタムコントロールは難しい
  • カスタムコントロールはName属性がつけれるのでライブラリ向け
  • 実は参考になる xaml がいる

こんなもんだろうか。 ControlTemplate は本当に難しい。 今回の実装も、正直微妙だなと思っている。 うまいこと継承とか使いこなしたいところ。

あと、もっと今風にするなら丸くしたいなと。 Thumb コントロールの外観を丸くして、Rectangle の角を丸くすればもっと今風になるなって。 デザインは奥が深い。プログラマもデザインする時代となると、もっと本読まないといけないかもね。

XAML: Templateについて その2

やっとこさ ControlTemplate が終わったんでまとめる。

目次

ControlTemplate でできること

まずは簡単なおさらいからで、ControlTemplate でできることは「外観の再定義」である。 DataTemplate でも外観の変更はできるのだけど、ControlTemplate はバインディング等も改造できるので、こちらの方ができることが多い。 DataTemplate は、バインディングする際に親定義から探してバインドするといったことができない。 つまり、TemplateBinding や RelativeSource の Mode=TemplatedParent が使えない。

基本的な ControlTemplate

ControlTemplate は Template プロパティに対して定義していく。

まずは 簡単に ControlTemplate を適用したものを載せる。

<Button Content="Click me!"
        Click="Button_Click"
        HorizontalAlignment="Center"
        VerticalAlignment="Center"
        Grid.Row="0"
        Grid.Column="1"
        BorderBrush="Red"
        BorderThickness="2">
    <Button.Template>
        <ControlTemplate>
            <Border>
                <TextBlock Text="temporary"/>
            </Border>
        </ControlTemplate>
    </Button.Template>
</Button>

上記の xaml コードは以下のような結果になる。

f:id:BackacheEngineer:20210506180346p:plain
簡単なControlTemplateを適用したボタン

見てわかるとおり、ただの TextBlock にしか見えない。たとえ Button 側に BorderBrush や BorderThickness を設定しても、うまいこと ControlTemplate に反映してくれるわけではないということである。しかも表示されているテキストは Button 側に設定したものではない。これではボタンとして見てわからないので、当然改造しなければならない。 かといって、ControlTemplate 内に定義している Border 等に決め決めで設定しても、その色やテキストしか使えず、使い物にならない。

これを解決するのが何度か出てきている TemplateBinding である。

TemplateBinding

TemplateBinding は前章で説明した内容を解決するものである。これは、「ControlTemplate を適用するコントロールのプロパティをバインディングソースにする」ということである。

つまり、TemplateBinding を使用すると下記のことを実現できる。

  • Button に設定した色を ControlTemplate に反映する
  • Button に設定した線の太さを ControlTemplate に反映する
  • Button に設定したテキストを ControlTemplate に反映する

もちろん、上記以外にもいろいろ実現できる。HorizontalAlignment も VerticalAlignment もバインディングソースにできる。 Button 側に設定した内容を ControlTemplate に引き継げるのである。

xaml コードを見ていく。

<Button Content="Click me!"
        Click="Button_Click"
        HorizontalAlignment="Center"
        VerticalAlignment="Center"
        Grid.Row="0"
        Grid.Column="2"
        BorderBrush="Red"
        BorderThickness="2">
    <Button.Template>
        <ControlTemplate>
            <Border BorderBrush="{TemplateBinding BorderBrush}"
                    BorderThickness="{TemplateBinding BorderThickness}">
                <TextBlock Text="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=Content}"/>
            </Border>
        </ControlTemplate>
    </Button.Template>
</Button>

そして、適用した結果の画像が下記である。

f:id:BackacheEngineer:20210506181651p:plain
TemplateBinding を適用した結果

見てのとおり、枠線の色と太さ、それとテキストが見事に反映されている。 ここで、Text へのバインディングが TemplateBinding でないことに気が付く。 これは、Buttun の Content プロパティを TwoWay でバインディングしなければいけないのが理由である。 TemplateBinding は OneWay のバインディングを前提としており、TwoWay のバインディングができないためだ。 そして、TwoWay でのバインディングかつ TemplateBinding と同じ効果をもたらすのが RelativeSource による Binding というわけである。 正確に書くと、「Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Mode=TwoWay, Path=Content」といったように、Mode の選択が可能なのである。

ControlTemplate にバインドするプロパティが TwoWay でバインドしないといけないのかを見極める必要がある。 なお、OneWay の場合は RelativeSource でもよい。「Binding RelativeSource={RelativeSource Mode=TemplatedParet}, Mode=OneWay, Path=....」の省略形が「TemplateBinding」なのである。 (なお、ぶっちゃけ TwoWay が必要なやつで TemplateBinding するとエラーになったりするのでそのタイミングで RelativeSource の書き方を試せばいい・・・)

しかし、ここで気づくかもしれないが、実は外観はできても「ボタンを押したときの動作が見えない」のだ。 これは VisualState という状態遷移を考慮していないためである。この状態遷移を考慮するうえで必要なのが VisualStateManager だ。

VisualStateManager

VisualStateManager は外観状態を管理するものである(そのまんま)。注意したいのが、この VisualState はコントロールごとにあること。 今回の例に挙げ続けている Button コントロールは2グループの VisualStateGroup からなる。

  • CommonStates:Normal, PointerOver, Pressed, Disabled
  • FocusStates:Focused, Unfocused, PointerFocused

すべての状態を厳密に決める必要はない。特に動作がなくていい状態は空の指定ができるためだ。 そして、これまでの xaml コードはボタンの Template を直接いじくってきたが、ここにきてリソースにおけるように Style として定義していく。 Style としてリソースに定義しておけば、すべてのボタンに対して同じ外観、VisualState を反映できる。

以下が Style の xaml コードだ。一気に長くなるが、下記の実装になる。

  • 枠線の色は 赤色
  • 枠線の太さは 2
  • フォントサイズは 24
  • ボタンの外観と状態を再定義
  • 外観は Border と ContentPresenter
  • 押したときの状態はボタンの色が LighitBlue になる。
  • 操作不可状態にしたらボタンが灰色になって操作不可になる。
  • ボタンにフォーカスがあるとき、テキストが点線で囲まれる。

xaml コードを見ればわかるが、VisualState はプロパティの値を上書きしているだけである。なお、この上書きに対してはアニメーションを用いることが可能である。今回はアニメーションを使わず、瞬時にプロパティを上書きしていく。

<Style x:Key="btnStyle3" TargetType="Button">
    <Setter Property="BorderBrush" Value="Red"/>
    <Setter Property="BorderThickness" Value="2"/>
    <Setter Property="FontSize" Value="24"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="Button">
                <Border Name="border"
                        Background="{TemplateBinding Background}"
                        BorderBrush="{TemplateBinding BorderBrush}"
                        BorderThickness="{TemplateBinding BorderThickness}"
                        CornerRadius="12">
                    <!--#region 状態定義 -->
                    <!-- 各状態の定義がないと遷移できない。 -->
                    <!-- また、この定義はすべて書いておかないといけない。なお、不要なものはタグを空にしておけばよい。 -->
                    <!-- さらには、定義場所も注意がいる。下手すると全く反映されない。 -->
                    <VisualStateManager.VisualStateGroups>
                        <VisualStateGroup x:Name="CommonStates">
                            <VisualState x:Name="Normal"/>
                            <VisualState x:Name="PointerOver"/>
                            <!-- ボタン押下時の色変更 -->
                            <VisualState x:Name="Pressed">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames Storyboard.TargetName="border"
                                                                   Storyboard.TargetProperty="Background">
                                        <DiscreteObjectKeyFrame KeyTime="0:0:0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <SolidColorBrush Color="LightBlue"/>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </VisualState>
                            <!-- 操作不可時、灰色の Rectangle で覆う -->
                            <VisualState x:Name="Disabled">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames Storyboard.TargetName="disabledRect"
                                                                   Storyboard.TargetProperty="Visibility">
                                        <!-- DiscreteObjectKeyFrame : 特定の値に直接変更するやつ -->
                                        <!-- キーフレームは、アニメーションが開始されてから特定の時間が経過したときのターゲットプロパティの値を示す -->
                                        <DiscreteObjectKeyFrame KeyTime="0:0:0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </VisualState>
                        </VisualStateGroup>

                        <VisualStateGroup x:Name="FocusedStates">
                            <VisualState x:Name="Unfocused"/>
                            <!-- フォーカスした際、瞬時に点線の透明度を 1 にするアニメーション -->
                            <VisualState x:Name="Focused">
                                <Storyboard>
                                    <DoubleAnimation Storyboard.TargetName="focusRectangle"
                                                     Storyboard.TargetProperty="Opacity"
                                                     To="1" Duration="0"/>
                                </Storyboard>
                            </VisualState>
                            <VisualState x:Name="PointerFocused"/>
                        </VisualStateGroup>
                    </VisualStateManager.VisualStateGroups>
                    <!--#endregion-->

                    <Grid>
                        <!-- 内容の本体 -->
                        <ContentPresenter Name="contentpresenter"
                                          Content="{TemplateBinding Content}"
                                          ContentTemplate="{TemplateBinding ContentTemplate}"
                                          Margin="{TemplateBinding Padding}"
                                          HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
                                          VerticalAlignment="{TemplateBinding VerticalAlignment}"/>
                        <!-- フォーカス時の点線用Rectangle -->
                        <Rectangle Name="focusRectangle"
                                   Opacity="0"
                                   Stroke="{TemplateBinding Foreground}"
                                   StrokeThickness="1"
                                   StrokeDashArray="2 2"
                                   Margin="4"
                                   RadiusX="12"
                                   RadiusY="12"/>
                        <!-- 操作不可時の覆い隠す用Rectangle -->
                        <Rectangle Name="disabledRect"
                                   Fill="Black"
                                   Visibility="Collapsed"
                                   Opacity="0.5"
                                   RadiusX="12"
                                   RadiusY="12"/>
                    </Grid>
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>

このスタイルを使って簡単なデモをする。

デモ内容

  • 左側のボタンを押すと真ん中のボタンが操作不可になる。
  • 右側のボタンを押すと真ん中のボタンが操作可になる。
  • 真ん中のボタンを押すと Debug.WriteLine で「Click」と出てくる。
  • 上記動作はイベントで書く。

まずは結果から。

f:id:BackacheEngineer:20210506193348g:plain
ボタンのVisualStateManagerのデモ

ちゃんとデモ内容どおりに外観が変化していることがわかる。 VisualSteteManager で状態を再定義してやれば、馴染みの動作になるというわけである。 なお、真ん中のボタンを押したときの画像は割愛する。

このデモに使った xaml コードと C# コードも載せておく。

xaml コード(スタイル部分は省略している。)

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition Width="1*" />
        <ColumnDefinition Width="1*" />
    </Grid.ColumnDefinitions>

    <Button Content="Disable right btn"
            Click="Button_Click_1"
            Grid.Column="0"
            Padding="12"
            HorizontalAlignment="Center"
            VerticalAlignment="Center"
            Style="{StaticResource btnStyle3}"/>

    <Button Name="operateBtn"
            Content="btn"
            Click="Button_Click"
            Grid.Column="1"
            Padding="12"
            HorizontalAlignment="Center"
            VerticalAlignment="Center"
            Style="{StaticResource btnStyle3}"/>

    <Button Content="Enable left btn"
            Click="Button_Click_2"
            Grid.Column="2"
            Padding="12"
            HorizontalAlignment="Center"
            VerticalAlignment="Center"
            Style="{StaticResource btnStyle3}"/>
</Grid>

C# コード

public partial class ButtonTemplateDemo : Window
{
    public ButtonTemplateDemo()
    {
        InitializeComponent();
    }

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        System.Diagnostics.Debug.WriteLine("Click!");
    }

    private void Button_Click_1(object sender, RoutedEventArgs e)
    {
        operateBtn.IsEnabled = false;
    }

    private void Button_Click_2(object sender, RoutedEventArgs e)
    {
        operateBtn.IsEnabled = true;
    }
}

まとめ

  • ControlTemplate は外観を再定義できる。
  • ControlTemplate を適用するコントロールのプロパティをバインドしたいとき、TemplateBinding か Binding の RelativeSource を用いる。
  • 上記の使い分けは バインド方向が OneWay か TwoWay かで決める。
  • 動作中の外観は VisualStateManager で再定義できる。
  • 動作中の外観にアニメーションが使える。

次はこの ControlTemplate の知識を使って「カスタムコントロール」を作っていく。 この知識があって初めて、カスタムコントロールが自在に作れるようになるのだ。