PropertyGridで既定値をうまいこと扱う方法

Windows FormにはPropertyGridという便利なコントロールがあります。
クラスを渡すとそのプロパティを解析して変更可能なUIを提供する機能があり、かえかえ君の設定画面でも使っています。
このPropertyGridで既定値周りで右往左往したときのメモ。

分かってる人にとっては「こいつ簡単なことに何やってんだ?」的なものかと思いますし、同じようなことを解説したページは他にもあると思います。
自分が分からない状態から分かる状態までの道程をメモしておきたかったので。

何をしたいのか

PropertyGridで既定値を設定できるようにしたい。

設定すること自体は大丈夫だったんですが、既定値を扱う段になってなんかうまいこと行かなくなった。
で、調べた時のメモ。

Windows 10 Pro + VS Community 2015 環境で進めます。

まずは基本的な形を作る

とりあえずフォームにPropertyGridを置きます。

image

ちなみにPropertyGridはツールボックスの「すべてのWindowsフォーム」に含まれてます。

設定対象のクラスを作成します。
今回は単純に色のやつだけ。

class Class1
{
    [Category("配色")]
    [DisplayName("背景色")]
    [Description("ウィンドウの背景色。")]
    public Color BackgroundColor { get; set; }
}

このクラスのインスタンスをFormのLoadイベントで設定します。
BackgroundColor には既に値があるとみなして値を突っ込んでおきます。

Class1 c = new Class1();

private void Form1_Load(object sender, EventArgs e)
{
    this.c.BackgroundColor = Color.White;
    propertyGrid1.SelectedObject = this.c;
}

これを実行するとこんな感じ。想定通り。

image

値が太字になってますが、これは既定値と違うことを意味しています。
既定値が未指定なので、Color.Whiteを指定したことで太字になっています。

既定値を設定したい

既定値はプロパティにDefaultValue属性を設定することで指定できます。
実際にかえかえ君で使ってる例を挙げてみます。

  • [DefaultValue(Keys.Control | Keys.NumPad7)]
  • [DefaultValue(10)]
  • [DefaultValue(true)]

要はプロパティの型に合った値を設定するだけ。
ならColorならこうだろと推測。

[DefaultValue(Color.White)]

設定してみました。

image

が、赤波線。エラーですね。
エラー一覧を見てみる。

image

訳が分からない。

そもそもWhiteって何よって調べると、KnownColor列挙体のやつだと分かった。
だったらKnownColorで設定すればいいんかな?と思って書き換え。

image

エラーが消えた!ビルドも通る!
さっそく実行してみた。

image

太い。

いやまてこれは値を入れるときの問題かもしれない。
そっちもKnownColor.Whiteにすればいいのだろう。

image

赤い。

image

そうか、Colorで渡せばいいのか。

image

エラーは出ないけど、これ結局Color.Whiteを渡してるのと同じ。
振り出しに戻る。

やはりそもそものエラーを解決する必要がある。

image

基本に立ち戻りDefaultValueをMSDNで調べると、気になるコンストラクタが。

image

Typeがどうたらってあるからエラーのメッセージにもつながる気がする。
この解説を見ると、第一引数で型、第二引数でメンバの文字列を指定するとある。
これでやってみることにした。

image

エラーは出ない。ビルドも通る。
値を設定するところも最初の状態に直す。

image

さて実行。

image

細い!

ちゃんと既定値と同じとして認識されてる!
試しに他の値に変えてみる。

image

太い!

無事に目的は達成された模様。
TypeConverterで内部的に変換処理するってことのようです。

これを応用すると複雑なクラスも既定値を設定できます。
良く使いそうなのはFontかなと。

[Category("文字")]
[DisplayName("フォント")]
[Description("テキストのフォント。")]
[DefaultValue(typeof(Font), "MS UI Gothic, 9")]
public Font TextFont { get; set; }

これを同じ値で初期化。

this.c.TextFont = new Font("MS UI Gothic", 9);

表示するとこんな感じ。

image

もちろん展開できます。

image

横のボタンでフォントダイアログも出ます。
プロパティを設定しただけで必要なUIを設定してくれるんで、かなり便利です。
このUIを自作することもできますが、今回の範囲外。

既定値に戻したい

選択中アイテムのリセット自体はPropertyGridのResetSelectedPropertyメソッドで一発。

//リセットして更新
propertyGrid1.ResetSelectedProperty();
propertyGrid1.Refresh();

これを新たに追加するボタンに仕込んでみます。
まずはボタンを追加。

image

先のコードをClickイベントに仕込んだら早速実行。
まずは値を既定値から変えてみる。

image

ボタンを押してみる。

image

既定値になりました。

子要素のあるFontも同様にやってみる。

image

子要素だけを変更しても全体が変更扱いになる模様。
どうなるか試しにNameとSizeを変更。

image

ボタン押下。

image

全体が既定値に戻りました。
この動作はDefaultValueでFont全体を設定しているので当たり前ですね。

既定値から変更されているか判断したい

リセットが分かると次の課題は「選択中アイテムがリセット可能な状態なのか」の判断。
単純に考えれば既定値と比較すればいいんじゃないの?って感じですが、やり方があったのでまとめ。

リセット可能かどうかの状態はPropertyDescriptorのCanResetValueメソッドで取れます。
これで既定値に戻すボタンを制御しようと思うので、PropertyGridのアイテム選択状態が変更されたときに発生するSelectedGridItemChangedイベントと、値が変更されたときに発生するにPropertyValueChangedイベントに下記の処理を仕込みます。

//選択中のアイテムがプロパティか
if (propertyGrid1.SelectedGridItem.GridItemType == GridItemType.Property)
{
    //選択中のアイテムがキャンセル可能か
    button1.Enabled = propertyGrid1.SelectedGridItem.PropertyDescriptor.CanResetValue(propertyGrid1.SelectedObject);
}
else
{
    button1.Enabled = false;
}

実行してみます。

image

既定値と同じなのでボタンが利用不可になりました。

背景色を変更してみます。

image

ボタンが利用可能になりました。

次に子要素のあるFontでSizeを変更してみます。

image

ボタンが利用不可のままです。
選択を親要素まで持っていくと利用可能になります。

image

この動きは子要素個別でリセットできないのが影響しているのでしょうか。
ユーザーとしてはまるごとリセットであっても反応してくれた方が良いですよね。
Visual Studioのプロパティも子要素でも反応しますし(リセットはまるごと)。

実現するには親要素でCanResetValueすればいいはず。
親要素はParentプロパティで取れるのですが、1階層で済むのか疑問。
そこで再帰的にParentを辿る方法で取る。
それがこのコード。

//GirdItemのPropertyなParentを再帰的に辿る匿名関数の定義
Converter<GridItem, GridItem> f = null;
f = delegate (GridItem gi) { return gi.Parent == null || gi.Parent.GridItemType != GridItemType.Property ? gi : f(gi.Parent); };

親要素のGridItemType.Property をチェックしてるのは、親要素Propertyの上にはCategoryやrootがあるので、そこまで上がってしまわないため、
これで取れるので、先のコードを直してみる。

//選択中のアイテムがプロパティか
if (propertyGrid1.SelectedGridItem.GridItemType == GridItemType.Property)
{
    //GirdItemのPropertyなParentを再帰的に辿る匿名関数の定義
    Converter<GridItem, GridItem> f = null;
    f = delegate (GridItem gi) { return gi.Parent == null || gi.Parent.GridItemType != GridItemType.Property ? gi : f(gi.Parent); };
    //選択中のアイテムがキャンセル可能か
    button1.Enabled = f(propertyGrid1.SelectedGridItem).PropertyDescriptor.CanResetValue(propertyGrid1.SelectedObject);
}
else
{
    button1.Enabled = false;
}

これで実行。

image

ボタンが利用可能になった!
もちろんこのままボタンを押せばリセットされます。

image

この対処法が最適解なのかは分かりません。
PropertyGridが対応するプロパティは様々あるため、この方法では例外が出てしまうのがあるかもしれません。

最後に

オプション画面の構築は地味に面倒な作業で、それを楽にできないかと、かえかえ君のオプション画面でPropertyGridを採用してみました。
やはり結構便利でUIのことをあまり考えず実装できたのですが、やはりそれなりに面倒なことはあるんだなと。
とは言え知ってしまえばコードは小さいので、今後のツール開発ではPropertyGridでのオプション画面で十分だなと思った次第です。


コメントをどうぞ

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です