System.Linq を用いた最大値探索

これに気付いたときものすごく感動したのでメモ.

最小値/最大値探索のコードはいつも for 文を書いてしまうんだけど,
せっかく C# 使ってるんだからもっとスマートな書き方がないもんかと
毎日悶々と過ごしていました.

で,適当に書いてみた結果がこれだよ!

だいぶ周辺コードを省略していますが,
int 型の ID という公開プロパティを持ったクラスのコレクションデータである
RecordData で,ID の最大値を返す MaxID プロパティの get アクセサです.

いつもの自分ならこうしていた.

なにが気に食わないって,初期値付きの自動変数を自分で与えないといけないとこ.
コーディング中に 「えっと,初期値は…」 って一瞬手が止まっちゃう.
最悪な場合は初期値を間違えて正常に動作しなかったり.
これと同じ内容を Linq ならたった 1 行で書けるなんて!
やっぱり Linq はしっかり勉強しておくべきだなぁ.

Popup の StaysOpen プロパティ

が使いこなせない….

Popup を表示させたとき,
Popup 以外をクリックしたら Popup を自動的に閉じるようにしたいとき,
StaysOpen プロパティを使うことで簡単に実装できる…らしいのでトライしてみた.

Popup の StaysOpen プロパティを False にした状態で,
ToggleButton の IsChecked プロパティを Popup の IsOpen プロパティにバインドして,
ToggleButton の操作で Popup の開閉をするようにすると,
とりあえずできるようになった.

どうやら ToggleButton を ON にしたときに,
Popup の GotMouseCapture イベントが発生しているみたい.
Popup がマウスをキャプチャしているおかげで,
Popup 以外の領域をクリックすると,
PreviewMouseDownOutsideCapturedElement イベントが発生して
Popup が閉じる仕組みになっているようだ.

ここまでわかったので,
ToggleButton 以外の要素で Popup の開閉を制御しようと試みた.
しかし Popup がマウスをキャプチャしてくれなくなってしまった.
しかも Popup.CaptureMouse() で強制的にマウスキャプチャしようとしても
戻り値が false となって使えない.

うむ~,なぜなんだ….

ColorPicker を作ってみた

WPF で ColorPicker を作ってみました.

ScreenShot
ScreenShot2

Excel なんかでよく見るような ColorPicker と,
Alpha 値も指定できるカラーパレットを使った ColorPicker の
両方を実装してみました.

少し前に勉強した ItemsControl に関する知識が大活躍.
結構嬉しい&楽しかった.

せっかくなので少し中身の話.
ColorPicker のようなコントロールを作るとき,
「ボタンを押すとドロップダウン形式でコンテンツが表示される」コントロールが必要になります.
ここではこのコントロールを ToggleButton と Popup で実現しています.
これは次のように書くことで意外と簡単にできてしまいます.

実行結果は次のようになります.
DropDown1
Popup の IsOpen プロパティに ToggleButton の IsChecked プロパティをバインドしているため,
ToggleButton を押すと Popup が下に表示されるようになります.
また,Popup の StaysOpen プロパティを False にしているため,
Popup 領域以外をマウスクリックすると Popup が自動的に閉じるようになっています.

基本的にはこの仕組みを使って実装しています.
しかし,このままでは少し困ったことがありました.
たいていのドロップダウンコンテンツを表示させるボタンには,
右端に下三角の矢印のようなものが表示されています.
これを表示させようとして,ToggleButton の Content プロパティに
次のようなコードを記述しました.

11 行目の Path オブジェクトでその三角マークを作っています.
雰囲気を出すために左側に色を固定した四角形を表示させています.
実行結果は次のようになります.
ToggleButtonContent1
一見なんの問題もないように見えますが,
ToggleButton に Width プロパティを指定するととんでもないことになります.
例えば Width=”200″ とすると次のようになります.
ToggleButtonContent2
右端に表示されていたと思っていた下三角マークが実はそうではなかったのです.
ToggleButton の Content は自動的に中央揃えになるようになっているため,
この問題をどうやってもうまく回避できませんでした.

というわけで,
ToggleButton の Template プロパティを設定してみました.
ToggleButton のコード全体は次のようになります.

実行結果は次のようになります.
ToggleButtonContent3
無事下三角マークが右端にきました.
しかし,今度は Button としての外観が失われてしまいました.
とほほ・・・.
仕方がないので Button っぽい外観を自前実装して,
冒頭の Screenshot にあるようなボタンができあがりました.
これが意外と大変で,ちょっと時間を費やしてしまった.
本当に他に方法がなかったのだろうか・・・.

ItemsControl で折れ線グラフのプロットは無理があり過ぎる

以前の記事で ItemsControl を利用してデータ点をプロットする方法をちょろっと紹介しましたが、
よくよく調べてみると、やっぱりデータ点が多いときは描画に時間がかかってしまいます。
そうは言ってもよくわかんないので、
簡単なサンプルを作ってみました。

まずはデータ点のクラスを次のように定義します。

折れ線グラフは 1 本だけとは限らないので、
複数本のグラフを保持できるようにしました。
GraphData クラスが複数本のグラフを保持しています。
その下層の GraphPoitns クラスが 1 本分のグラフの点を保持しています。
最下層の GraphPoint クラスがあるグラフのある点の情報を保持しています。

このクラスを用いて ViewModel でグラフを用意します。

View が表示されると同時に描画されてしまうと
グラフを表示するためにかかる時間が実感できないので、
TestCommand が実行されたら描画されるようにしておきます。

31 行目の繰り返し回数でグラフの本数が決まります。
34 行目の繰り返し回数で 1 つのグラフに対するデータ点数が決まります。
上記のコードでは 1000 点のグラフが 2 つできることになります。
グラフの形状は 36 行目で記述している通り、右肩下がりの直線グラフになっています。

蛇足ですが、ViewModelBase クラスは INotifyPropertyChanged を実装しており、
プロパティ変更ヘルパとして SetProperty<T>() メソッドが定義されています。

さて、ここからが本題です。
このグラフを表示するための View を ItemsControl を用いて次のように記述します。

最初にあるボタンは TestCommand を実行するためのものです。
つまりこのボタンを押すとグラフが描画されます。

ItemsControl は、自分の中に ItemsControl を入れるという、
少々複雑なことになっています。
上位の ItemsControl で複数本のグラフを描画するようにし、
入れ子になっている ItemsControl で 1 つのグラフに対するデータ点すべてを描画させています。

上記の ViewModel ではデータ点は 2000 個ですが、
これくらいだと大して表示に時間がかかるようには感じません。
しかし、ViewModel.cs の 34 行目の繰り返し回数を 10000 回にすると、
つまり、データ点を 20000 個にすると、
途端に表示に時間がかかってしまいます。

グラフを 1 回だけ描画するなら多少我慢できますが、
軸の移動/拡大処理等ダイナミックなグラフ表示を考えているので、
これはちょっと面白くありません。

やっぱり DrawingVisual に手を出すしかないのかな。
ItemsControl を使う方法は XAML をフル活用している感じがして
結構気に入っていたんだけど。

サンプルコード:ItemsControlSample.zip

LineGraph という名のカスタムコントロールを作ってみる その 2

今回はグラフの目盛線の描画方法を紹介しますが,
その前に基本となるクラスを説明しないことには,
途中で意味不明なコードが出てきますので,
まずはその基本クラスを簡単に紹介します.

紹介する基本クラスは NotifyPropertyChangedDataClass 抽象クラスです.
INotifyPropertyChanged インターフェースを実装するためのもので,
プロパティ値変更を容易に実装できるように工夫しています.

ポイントは SetProperty<T>() メソッドで,
このメソッドを使ってプロパティ値の変更をする/しないの判断を含めて処理させます.
実際の使用例を交えてグラフの軸設定に関するパラメータをまとめた
AxisStyle クラスを次のように定義します.

SetProperty() メソッドはプロパティ値に変更がある場合に true を返します.
幅 Width プロパティは最小値/最大値が変更されると同時に
変更通知をおこなわなければならないため,
最小値/最大値プロパティの set アクセサの中で変更通知をおこなうようにしています.

それぞれの set アクセサで,
value に対する条件を書いていますが,
実際には IDataErrorInfo インターフェースを実装して
入力値検証の仕組みも同時に実装させます.
ここでは省略します.

それでは
改めて LineGraph カスタムコントロールを次のように定義します.

上記のように Control クラスから派生させ,
静的なコンストラクタで LineGraph の既定スタイルを読み込むようにしておきます.

LineGraph に軸設定に関するパラメータを
依存関係プロパティとして定義します.
軸は横軸/縦軸/第 2 主軸の 3 種類あるので,
3 つ定義します.

宣言時に初期化をおこなっても構いませんが,
完成後の可読性を鑑みてコンストラクタ内にて初期化をおこないます.
プロパティの型は先ほど定義した AxisStyle です.
このように依存関係プロパティを定義してから,
LineGraph の既定 Style を Generic.xaml で次のように定義します.

ラベルや目盛など他のコントロールも同時に配置していますが,
今回のメインは GraphGridPath という名前を付けた Path オブジェクトです.
LineGraph.cs の静的なコンストラクタ内で
DefaultStyleKeyProperty.OverrideMetadata() メソッドをコールしているため,
Generic.xaml で定義した名前を FindName() メソッドで探し出すことができます.
FindName() での検索先が更新されるタイミングは OnApplyTemplate() イベントハンドラが最速らしいので,
このメソッドを次のように override します.

なんだかコードがいっぱいになってしまいました.
ついでに定義したコントロールもすべて読み込むようにしています.
ポイントは次の 2 点のみ.

  • IsApplyTemplate プロパティでテンプレート適用済確認できるようにした
  • MainContainerGrid のサイズ変更イベントハンドラでグラフ要素を更新するようにした

IsApplyTemplate というプライベートプロパティを定義し,
この OnApplyTemplate() を通ったかどうか確認できるようにしておきます.
これは,
依存関係プロパティのプロパティ変更のタイミングが,
OnApplyTemplate() が呼び出されるタイミングより速いため,
プロパティ変更のタイミングで XAML で定義したコントロールに対する操作を
おこなうときに NullReferenceException が発生しないようにするためのものです.
というわけで IsApplyTemplate プロパティは後で出てきます.

すべてを包括する MainContainerGrid にサイズ変更があった場合,
必ずグラフ要素を更新しなければならないため,
SizeChanged イベントハンドラを override しています.

ここまできてようやくグリッド線の描画にたどり着きました.
長かった….

BuildGraphGrid() メソッド内でグリッド線の描画に関するコードを記述します.

GeometryGroup を新規に生成して,
それぞれの軸に対するグリッド線を Children に追加しています.

ここに出てくる XAxisPositionFromValue() メソッドなどは,
描画すべき位置をコントロールのサイズと軸設定から算出してくれるヘルパで,
次のように定義しています.

以上.これでグリッド線が描画されます.
試しに見てみるには,
WPF アプリケーションプロジェクトのほうで
実際に View に配置してみます.

MainView.xaml はこんな感じ.

対する MainViewModel は次のようになります.

ちなみに ViewModelBase は INotifyPropertyChanged を実装した
NotifyPropertyChangedDataClass 抽象クラスと同じ内容です.
したがってここでも SetProperty() メソッドを使っています.

というわけで実行結果.
GridOnly_2

このままでは縦軸と第 2 主軸のグリッド線の見分けがつかないので
少し工夫しないといけませんが,
とりあえず基礎は完成.
なんか周辺要素が多くて予想以上に長くなった….