ブログ「サイバー少年」

ブログ「サイバー少年」へようこそ!
小学六年生ごろからプログラミングを趣味にしている高校生のブログです。
勉強したことについての記事などを書いています。フリーソフトも制作、公開しています。
(当ブログについて詳しくは「ブログ概要紹介」を参照)

サイバー少年が作ったフリーソフトは「サイバー少年の作品展示場」へ

PowerShellでWPFとかイベントとか ~ 枠組みをC#で書く ~

この頃、ブログの更新がおろそかでしたが、ネタはあったのです。

今回は大したものではないですが、この前書いた記事「PowerShellで.NETのウィンドウを生成する」の続編的な内容を綴りたいと思います。

そのため、あちらの記事を読んでからこちらを読むこと推奨です。

最近、昔の記事と同じネタを使いまわすばっかりで、やっぱりネタないんだろとか言ってはいけない。



上記リンクの記事では、ウィンドウの生成と表示までをメインに書いてあり、
ボタンを押したら〇〇するみたいなイベントハンドラというかロジックの部分はかなり適当でした。


一応、前にイベントハンドラについて言及したことをまとめましょう。

XAMLでたとえば、Windowの要素としてButtonがあるというときに、PowerShell上でButton.Clickイベントのハンドラを書きたいなというときは、

まず、PowerShellでButtonのインスタンスを取得できるようにするため、XAMLのほうでButton要素に適当なx:Name属性を設定しておきます。

そして、Windowのインスタンスはすでに例のXamlReader.Parseメソッドから取得できているので、
そのWindowのFindNameメソッドにButtonのx:Nameを与えればインスタンスを探して返してくれます。


そして、そのインスタンスのメンバに、PowerShell専用の普段は使えないメソッドですが、Add_(イベント名)というメソッドがあるので、それを呼びます。

引数に { ... } と書くスクリプトブロックを与えれば、たとえばAdd_Clickを呼んだなら、そのあとボタンがクリックされたときにスクリプトブロックが呼ばれるということになります。

ちなみにAdd_(イベント名)じゃなくて、Register-ObjectEventというコマンドレットでもPowerShell的なスタイルでイベントハンドラを登録できるのですが、
これは厳密にいうとイベントハンドラとはちょっと違うPowerShellのジョブというものであるようで、実験してみたところおそらくジョブを扱うスレッドはGUIスレッドと違うという問題で上手いことハンドラが呼ばれませんでした。



このように、
1. XAMLにx:Nameを書いておく
2. FindName
3. Add_(イベント名)
と、三つの工程を踏まなければならないので、イベントを登録するコントロールがひとつならいいですが、たくさんあるともう大変です。

つまりイベントハンドラがたくさんあるような本格的なGUIを、PowerShellから構築することは考えないほうがいいでしょう。




しかし、もしテンプレートというか大体の枠組みは、事前に共通のものを作成しておいて、イベントハンドラもどこに設定するかというような記述は事前にしておいて、

その具体的なイベントハンドラの処理内容や、その他のプロパティ等を個々のケースで設定するという方式を取るということであれば、かなり効果的なのではないでしょうか。


その大体の枠組みはどのように記述するのかというのは、とりあえずカスタマイズ前のウィンドウを生成するスクリプトを外部に書いておいて、
カスタマイズしてほしいコントロールは変数として提供しておくみたいな手法が、まず考えられると思います。


つまり、たとえばXAMLじゃない方法でやりますが、

$window = New-Object System.Windows.Window
$button = New-Object System.Windows.Controls.Button
$window.Content = $button
function AddClickHandler($handler) {
$button.Add_Click($handler) }


みたいなスクリプトを書いておいて、使用者はこれをインクルードした後に、

$button.Content = "メッセージを出す"
AddClickHandler { [System.Windows.MessageBox]::Show('メッセージです') }


みたいな感じでカスタマイズするということです。


このようなものも方法として十分考えられると思うのですが、ケチを付けるとすれば、以下の二点が挙げられると思います。

ひとつは、イベントハンドラがメッセージボックスを出すだけみたいな単純なものならいいんですが、
色々なコントロールのプロパティ等と連携するようなものだと、イベントハンドラが多数になったときに使用者側もスクリプトブロックだらけになりますので、どこで何をいじったのかを把握しづらい、ということですね。


もうひとつはXAMLを使えないということです。

上の例は主にビューをテンプレート化しておいて、ロジックを個々で書くという感じでしたが、
逆にロジックをテンプレート化しておいてビューを個々で書くというケースもあると思います。

そのとき、この前の記事で書いたとおり、コードから複雑なGUIを設計するのは大変ですが、XAMLで書くとわりと楽になります。

しかし、XAMLで書いてしまうとスクリプトの方との連携が難しくなります。
まあ工夫すれば、前述のようにx:NameとFindNameメソッドで連携を取れるでしょうけど、

問題なのは、スクリプトのほうで既にビューの構造が決まってしまうと思います。

そのため、XAMLを書くといっても、だいぶスクリプトの仕様に縛られた範囲でしか書けず、柔軟性がほぼない感じになります。

それだと、そもそもなぜ同じようなビューをわざわざ個々に書かせるのかという疑問さえ出てきます。


まだ本題に入ってないのに長くなりましたが、ようするに、この方法でも複雑なGUIを扱うのは苦しいものが残っているということです。

話が抽象的すぎて全然伝わってないかもしれませんが…。



ということで本題!


そこで、テンプレートとなるものをWindowクラスのサブクラスとして、C#で書いてしまってはどうでしょうか。

テンプレートをC#で書くということで、こちらは簡単にはいじれなくなりますけどね。


ちなみにPowerShell v5.0からクラス定義の構文が追加されたそうです。
C#じゃなくてこちらで代用できるようになったのかもしれません。


さて、WPFというとビューをXAMLで書いて、ロジックをコードで書くというビューとロジックの分離がやりやすい仕組みになっています。

私がWPFの勉強をするときに逃げた部分なので、偉そうに語るのも変なのですが、今回はバインディングとかも単純なものしかやりません。


つまり、ロジックのほうをテンプレート化してビューを個々で書くという場合は、ロジックをC#で書いてビューを個々にXAMLで書くという方式を取れば、
ロジックとビューを上手いこと分離すればテンプレートはしっかりと、そしてXAMLは柔軟に書くことができるというわけです。

また、逆にビューをテンプレートにするという場合、ビューをC#で書くことになるので、この場合は向いていないと思います。


さて、具体的にはどういうことなのかと言いますと、たとえばウィンドウ内の何らかの要素を何らかのアクションをすれば、メッセージボックスを出すということをテンプレートにしたいとします。

テンプレート内で決めるのはメッセージボックスを出すという動作だけであって、いつそれを行うのかはXAMLのほうで定義してもらうということが可能になるわけです。


まず、Windowクラスの派生クラスとして、メッセージボックスを出すという動作を追加定義したクラスを書きます。

namespace OriginalWindow
public class MessageWindow : Window {
public ShowMessage(object sender, RoutedEventArgs e) {
  MessageBox.Show("メッセージです");
} } }


このShowMessageメソッドがそれです。
これをXAMLからイベントハンドラとして設定することになるので、senderとeが引数に必要です。

まず上のC#のコードをクラスライブラリとしてビルドします。
そしてPowerShellのほうでAdd-Typeを使ってロードします。
具体的には

Add-Type -Path (DLLのファイルパス)

となります。

このようにロードしておくと、XAMLにMessageWindowというオリジナルの要素を使えるようになるのです。


まあそのためには、いきなりMessageWindowとか書き始めても「は?」と言われるので、XAMLのほうに名前空間を定義しないといけません。

それには、

<ex:MessageWindow xmlns:ex="clr-namespace:OriginalWindow;assembly=OriginalWindowLib">

みたいに書きます。

これはex(exである必要はない)というXMLでの名前空間を定義していて、
"clr-namespace:(対応するC#での名前空間);assembly=(クラスがあるアセンブリ名)"
という書式で、その名前空間とC#の上のコードを対応させることができます。

つまり、このXAMLをXamlReaderに渡すと、XamlReaderはex:○○という要素があれば、上のDLLで定義された要素であるというふうに扱ってくれるのです。


そして、以下のようにXAMLを書きます。

<ex:MessageWindow
xmlns:ex="clr-namespace:OriginalWindow;assembly=OriginalWindowLib"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

<Button Content="メッセージを出す" Click="ShowMessage" />

</ex:MessageWindow>


このときx:Classを設定する場合は、x:ClassがMessageWindowを表しているときだけOKでそれ以外は、継承関係のクラスだとしてもエラーになるようです。

どちみちx:Classを設定することの意味があまり存在しません。


さて、このXAMLでは、ClickイベントのハンドラをShowMessageとしていますね。
ShowMessageというと、MessageWindowクラスで追加定義したメソッドです。

ルート要素にあるイベントハンドラは、XAMLから設定することもできるのです。


ここではButtonを要素として、Button.ClickとShowMessageをひも付けたわけですが、ぜんぜん違うビューでもいいわけですね。

Buttonなんか無くして、Window.LoadedイベントとShowMessageをひも付けても、あるいはもっと複雑なGUIを定義して、どこかにひも付けてもいいわけです。

このようにXAMLが本当に好き勝手に書けるのです。


C#でテンプレートとして定義するのは、ロジックすなわち機能、メッセージボックスを出すという機能だけであるといえます。

先ほども言いましたが、ビューがどんな感じになるのかは、テンプレートには書かないようにさせられるという特徴があるということです。



では、最後に(最後にと言っても長くなりそうですが)、もっと複雑な例を書いてみます。

バインディングも使います。


まず機能だけの観点から話しますが、なんらかのアクションで数値を1ずつ増やしていってそれをなんらかのもので表示するというカウンターを作りましょう。

名前空間やアセンブリは先ほどと同じ場所に書くとします。


さて、カウンターのカウント数は、XAMLのほうでバインディングしないといけないので、まずカウント数などのバインディングするデータを集めたクラスを定義して、これをDataContextに設定することにします。


public class CountData {
  public int Count { get; private set; }
  public CountData(int value) {
    this.Count = value;
} }


このクラスはこれだけです。

ひとつのインスタンスは同じカウント数しか保持できない不変オブジェクトにしておきます。

Countを直接いじるようにすると、その変更を通知する必要があるみたいで面倒だからです。
DataContextごと新しくしてしまえば変更がバインディングしているところにも反映されます。


そしてカウンター機能を持つウィンドウを表すクラスを定義します。

public class CounterWindow : Window {
  public int IncrementSize { get; set; }
  public CounterWindow() {
    this.IncrementSize = 1; // 1が既定値
    this.DataContext = new CountData(0);
  }
 
  public void Increment(object sender, RoutedEventArgs e) {
    CountData data = (CountData)this.DataContext;
    this.DataContext = new CountData(data.Count + this.IncrementSize);
  }
}

IncrementSizeはとりあえずは実質1です。
そしてIncrementメソッドでインクリメントして、それはバインディング先にも反映されることになります。


ではXAMLを書きましょう。

単純にボタンにカウント数が表示されていて、そのボタンを押してカウントを増やすというビューにします。

<ex:CounterWindow
xmlns:ex="clr-namespace:OriginalWindow;assembly=OriginalWindowLib"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

<Button Content="{Binding Count}" Click="Increment" />

</ex:CounterWindow>


これだけで、ButtonのContentがCountと結びつき、ClickイベントとIncrementメソッドが結びつくというのですから、とても楽です。


しかも、ビューとロジックが分離しているとはいっても、そもそもビューとロジックを分離させたかったのではなく、テンプレートと個々の定義を分離させたかったのであって、

個々の定義、つまりXAMLのほうから多少はテンプレート、すなわちC#のコードに対して注文を付けたいことがあると思います。


たとえばどういう注文かというと、一回のインクリメントで1じゃなくて2増やすようにしてくれというようなことです。

そのためにIncrementSizeプロパティを定義しておいたのです。


XAMLでContentだのMarginだのというものを設定していますが、これらは要素のプロパティであるわけですよね。

つまり、CounterWindowのプロパティであるIncrementSizeもXAMLから設定できるはずです。

ようするに

<CounterWindow IncrementSize="2"...

なんて書けば2増やすように変更できてしまうということです。


このようにプロパティを架け橋にしてXAMLからC#のコードへ情報を渡すことができます。

ただ、XamlReaderでは、クラスのコンストラクタが呼び出されてからこのプロパティが設定されるようですので、

初期化時に、この注文を確認したいということであれば、Loadedイベントなどを使います。


XAMLに書くということじゃなくC#のコード内でLoadedイベントに登録するということです。
別にXAMLに書いてもいいとは思いますがコードのほうがいいんじゃないでしょうか。


というわけで、今回もまた激しくわかりづらいクソ記事を書いてしまったことを後悔しておりますが、

ロジックをC#でテンプレートにして、PowerShellではXAMLを書いて連携することにより、
PowerShell上でイベントハンドラの登録に苦労することなく、自由度の高いビューを作成できるという記事でした。


使い捨てのGUIツールを作っていて、ロジックにおいてそれぞれの共通点が多いということであれば、このように一度C#でテンプレート化してしまうというのもお勧めします。


ビューの共通点が多いということであれば、本題の前に書いた外部スクリプトにまとめる方法を使うのもいいでしょう。

もちろん、この二つの方法を組み合わせることもできますからね。


いろいろと工夫できる点が見つかっていくものですよね。

tag: Windows PowerShell C# イベント WPF XAML テンプレート スクリプト

コメント

コメントの投稿

トラックバック

トラックバック URL
http://cyberboy6.blog.fc2.com/tb.php/445-ff75ef73
この記事にトラックバックする(FC2ブログユーザー)

当ブログをご利用(閲覧等)になる場合は必ず「当ブログの利用規定」をお守りください。