Visual Studio .NET プロパティ ブラウザによるコンポーネントの本格的な RAD 化
サンプルをダウンロードする - codesamples.exe
Microsoft Corporation
2001 年 9 月
要約: この記事では、.NET のプロパティ ブラウザとその新機能の使い方について説明します。
MSDN Code Center で関連するサンプルをダウンロードする。
目次
はじめに
何ができるのか?
基本事項: 属性によるブラウジングのカスタマイズ
展開可能なプロパティと文字列変換: TypeConverter とプロパティ ブラウザ
カスタム型の編集と表示
代替プロパティ ビューの提供
実際の使用方法
結論
はじめに
初期の Windows® の時代に Microsoft® Visual Basic® がリリースされて以来、プロパティ ブラウザは本格的なラピッド アプリケーション開発 (RAD) を可能にする重要な役割を果たしてきました。Microsoft Visual Studio® .NET では、この伝統に則り、プロパティ ブラウザにこれまでにはなかったさまざまな機能が追加されています。Visual Studio .NET 環境に参加するコンポーネントやその他のオブジェクトを作成するときには、ぜひこれらの新機能を活用して、ユーザーにとって便利なコンポーネントを作成してください。
何ができるのか?
これまでのバージョンのプロパティ ブラウザは、基本的には COM の型情報を処理し、そこに含まれているプロパティを表示していました。COM コンポーネントのパブリック API は、通常はインターフェイス定義言語 (IDL) に定義されており、プロパティ ブラウザから見えないようにする nonbrowsable や、プロパティをデータ バインディング可能にする bindable などの固定された属性のセットが付加されていました。その他の標準の値リストやカテゴリ化されたプロパティなどのブラウジング機能を実現するためには、コンポーネント作成者が IPerPropertyBrowsing や ICategorizeProperties などの COM インターフェイスをインプリメントする必要がありました。.NET Framework Windows フォーム クラスを使って書かれている.NET Framework と Visual Studio .NET のプロパティ ブラウザは、これらの機能をより単純で統一された形で提供し、多数の新しいエキサイティングな機能を備えています。
もちろん、Visual Studio .NET のプロパティ ブラウザは、これまでのバージョンと同じ機能をサポートしています。たとえば、ITypeInfo の型情報を調べる方法や、上記の拡張ブラウジング インターフェイスをサポートする方法を知っています。しかし、魅力的な新機能の大部分については、それらを使用するためには.NET Framework 上でマネージド コードを使ってコンポーネントを作成する必要があります。以下に、これらの素晴らしい新機能とその内容についての簡単なリストを示します。
-
メタデータ属性
プロパティの属性は、プロパティ ブラウザの動作の多くの部分を決定します。これにより、コンポーネント作成者は、どのプロパティが可視状態になるのか、それらがどのように分類されるのか、マルチセクションに含めることができるのか、そして他のプロパティの値に影響を与えるのかどうかを簡単に制御することができます。これらの属性は簡単に理解し、利用することができます。 -
階層サポート
プロパティをサブプロパティに分割して、論理的に整理することができます。 -
値のグラフィカルな表現
プロパティの値のテキストに加えて、選ばれた色やフォントのサンプルなどを、小さなグラフィックスとして描画することができます。 -
カスタム型編集
コンポーネントは、日付型の日付ピッカーや、色のための拡張カラー ピッカーなど、型の編集のためのカスタム ユーザー インターフェイスを提供することができます。プロパティ ブラウザの側がサポートする型を決めていた時代は終わりました。このタスクは、カスタム型については、コンポーネントの側に任せられるようになったのです。フレームワーク自身は、すべての基本的な組み込み型を編集するための機能を用意しています。 -
拡張可能なビュー
これは「プロパティ タブ」とも呼ばれるもので、.NET コンポーネントはデザイン サーフェス上に表示される標準のプロパティおよびイベント ビュー以外のビューを追加することができます。 -
再利用可能なコンポーネント
Visual Studio .NET プロパティ ブラウザは、主に System.Windows.Forms.PropertyGrid コントロールから作られていますが、これをアプリケーション内で再利用することができます。
明らかに、.NET プロパティ ブラウザについては、学ばなくてはならないことがたくさんあります。この記事では手始めとして、個々の素晴らしい新機能を活用する方法を説明します。
基本事項: 属性によるブラウジングのカスタマイズ
ブラウジングを制御するための基本的なメカニズムは、IDL で定義される COM コンポーネントのものと似ており、メタデータ属性を追加することによって行われます。ブラウジングを制御するための最も基本的な属性は BrowsableAttribute です。デフォルトでは、プロパティ ブラウザはオブジェクト上に定義されているすべてのパブリックな読み取り可能プロパティを表示し、それらを "Misc" と呼ばれる「その他」のカテゴリに追加します。次に、単純なコンポーネントの例を示します。
public class SimpleComponent : System.ComponentModel.Component
{
private string data = "(none)";
private bool dataValid = false;
public string Data {
get {
return data;
}
set {
if (value != data) {
dataValid = true;
data = value;
}
}
}
public bool IsDataValid {
get {
// データに対して何らかのチェックを実行する
//
return dataValid;
}
}
}
このコンポーネントは、プロパティ ブラウザでは次のように表示されます。
図 1. プロパティ ブラウザで単純なコンポーネントを表示した様子
この例では、SimpleComponent は Data と IsDataValid という 2 つのプロパティを持っています。IsDataValid は読み取り専用で、開発者はその値をデザイン時に知る必要がないので、これをプロパティ ブラウザ内に表示することにはほとんど意味がありません。BrowsableAttribute 属性を追加すれば、このプロパティを簡単に非表示にすることができます。
[Browsable(false)]
public bool IsDataValid {
get {
// データに対して何らかのチェックを実行する
//
return dataValid;
}
}
C# コンパイラは属性クラス名に "Attribute" という単語を自動的に追加するので、省略してかまいません。ただし、"[BrowsableAttribute(false)]" と入力することも可能です。プロパティまたはクラスで明示的に指定されていない属性については、プロパティはその属性のデフォルト値を持つものと仮定されます。この例では、BrowsableAttribute のデフォルト値は true です。これはまた、Visual Basic .NET で書かれたコードにも当てはまります。唯一の真の違いは、Visual Basic .NET が属性の前後に、上に示した大括弧ではなく、角括弧 ('<' と '>') を使用することです。
また、図 1 の値 "abc" が太字になっていることに注意してください。太字の値は、値がデフォルト値から変更されており、デザイナ内でフォームまたはコントロールのためにコードが生成されるときには、その値が永続化されることを示しています。デフォルト値に設定されたままのプロパティの値を永続化する理由はありません。スタートアップ時にコンポーネントの初期化に要する時間が長くなり、ファイルに生成されるコードの量が増えるだけだからです。では、この SimpleComponent がプロパティ ブラウザに対して「デフォルト」値が何であるかを知らせ、値がコードに永続化されないようにするためにはどうすればいいのでしょうか? この目的には、DefaultValueAttribute が使用できます。これはコンストラクタの引数としてオブジェクトを取るので、任意の型の値を渡すことができます。プロパティ ブラウザが値を表示するときには、現在の値を DefaultValueAttribute の値と比較し、値が異なる場合にはテキストを太字で表示します。この例では、" (none) " 以外の値である場合に、値が太字で表示されます。
[DefaultValue("(none)")]
public string Data {
// . . .
}
コンポーネントにメソッドを追加することで、プロパティが単純なストック値よりも複雑なロジックを必要とするかどうかを決定することができます。メソッドの名前は "ShouldSerialize" にプロパティ名を続けたもので、このメソッドは Boolean を返さなくてはなりません。この例では、メソッドは ShouldSerializeData という名前になります。SimpleComponent に下記のメソッドを追加すると、Data プロパティに DefaultValueAttribute を追加するのと同じ効果が得られますが、こちらの方法では決定をオンデマンドで行うことができます。
private bool ShouldSerializeData()
{
return Data != "(none)";
}
一般にユーザーは、カテゴリに分類されたプロパティの方がはるかにナビゲートしやすいと感じます。CategoryAttribute は、その名前が示唆するように、このタスクを担当しています。これにカテゴリ名文字列を指定することで、プロパティ ブラウザはプロパティをカテゴリ名ごとにグループ化するようになります。カテゴリ名には任意の名前を使用することができます。
[DefaultValue("(none)"), Category("Sample")]
public string Data {
// . . .
}
コンポーネントの開発者が頻繁に行うタスクの 1 つに、カテゴリ文字列のローカライゼーションがあります。System.ComponentModel.CategoryAttribute クラスを見ると、GetLocalizedString メソッドがこの目的に使用できることがわかります。ローカライズされたカテゴリ文字列を作成するには、独自のカテゴリ属性を定義しなくてはなりません。この例では、コンポーネントのマニフェスト リソースの中で、キーに基づいてカテゴリ名を探します。属性をプロパティに適用するときには、実際のカテゴリ名を指定する代わりに、このキーが使用されます。この属性でカテゴリ文字列が初めて検索されると、GetLocalizedString オーバーライドが呼び出され、そのキー値を受け取ります。その後、返された値がプロパティ ブラウザにカテゴリ名として表示されます。
internal class LocCategoryAttribute : CategoryAttribute {
public LocCategoryAttribute(string categoryKey) :
base(categoryKey){
}
protected override string GetLocalizedString(string key) {
// 現在のロケールのリソース セットを取得する
//
ResourceManager resourceManager =
new ResourceManager();
string categoryName = null;
// このキーをカテゴリ名のリソースとして
// 持っているカルチャーが見つかるまで探索し、
// 不変カルチャーに到達した場合には終了する
//
for (CultureInfo culture =
CultureInfo.CurrentCulture;
categoryName == null &&
culture != CultureInfo.InvariantCulture;
culture = culture.Parent) {
categoryName = (string)
resourceManager.GetObject(key, culture);
}
return categoryName;
}
}
この属性を使用するには、キーを含んだリソースを定義し、この属性をプロパティに追加します。
[LocCategory("SampleKey")]
public string Data {
// . . .
}
デザイン サーフェス上で複数のコンポーネントを選択したとき、プロパティ ブラウザはプロパティ名と型に基づいて、それらのコンポーネントのプロパティの共通部分、または「マージ」した結果を表示します。その後、プロパティ値が変更されると、選択されたすべてのコンポーネントが、選択されたプロパティの値として、その値を受け取ります。プロパティによっては、他のプロパティとのマージ部分に含めてもほとんど意味がないものがあります。一般には、コンポーネントの名前のような一意性が必要な値を持つプロパティがこれに該当します。複数のコンポーネントが選択されているときに値を設定すると、すべてのプロパティが同じ値に変更されるので、このようなプロパティはマージに含められないようにしておくと便利です。MergablePropertyAttribute はこの問題を解決します。プロパティに、値を false にしてこの属性を追加しておくだけで、そのプロパティはデザイン サーフェス上で複数のコンポーネントが選択されるときには非表示となります。
一部のプロパティは他のプロパティの値に影響を与えることがあります。たとえば、データ バウンド コンポーネントでは、DataSource プロパティをクリアすると、そのプロパティがバインドされているデータ行を指定する DataMember プロパティが暗黙のうちにクリアされます。このようなケースは RefreshPropertiesAttribute によって処理されます。そのデフォルト値は "None" ですが、別の属性値が指定されると、プロパティ ブラウザはこの属性を持っている値が変更されたときに、自動的にリフレッシュを行います。この種の値としては、プロパティ ブラウザにすべてのプロパティの値を検索して再ペイントするよう指定する Repaint と、オブジェクトそのものに対してそのプロパティを検索することを意味する All の 2 つがあります。変更によってプロパティの追加または削除が引き起こされるときには All を使用します。ただし、これは高度な使用シナリオであり、単なる再描画よりも時間がかかることに注意してください。ほとんどのケースでは RefreshProperteis.Repaint が適しています。この属性は、値が変更されたときに変化を引き起こすプロパティに設定するのであり、変更の影響を受けるプロパティに設定するのではないことを覚えておいてください。
最後に、DefaultPropertyAttribute と DefaultEventAttribute は、プロパティ ブラウザがクラスの中で、初期状態としてどのプロパティまたはイベントを強調表示するかを指定するクラス レベルの属性です。選択対象が異なるコンポーネントに変更されると、プロパティ ブラウザは前のコンポーネントで選択されていたのと同じ名前および型のプロパティを選択しようと試みます。そのようなプロパティが選択できない場合には、DefaultPropertyAttribute で指定されたプロパティが選択されます。プロパティ ブラウザのイベント ビューでは、DefaultEventAttribute の値が使用されます。また、これはデザイナ上でコンポーネントをダブルクリックしたときにハンドラが生成されるイベントでもあります。
展開可能なプロパティと文字列変換: TypeConverter とプロパティ ブラウザ
Visual Studio .NET プロパティ ブラウザの最も優れた機能の 1 つは、ネストされたプロパティを表示する機能です。これにより、カテゴリよりもさらに細かい、論理的なグループ分けが可能となります。ネストされたプロパティは、カテゴリ分類モードとアルファベット順のモードでも使用できます。これにより、たとえば Left プロパティと Top プロパティの両方を表示する代わりに、X と Y に展開可能な Location プロパティを用意して、プロパティ リストをコンパクトに抑えることができます。
図 2. ネストされたプロパティ
では、プロパティが展開可能かどうかは何によって決定されるのでしょうか? その答えはプロパティそのものではなく、プロパティの型にあります。.NET Framework では、個々の型に TypeConverter が関連付けられています。Boolean や文字列などの型の TypeConverter は、プロパティ ブラウザの中で型が展開されないようにしています。Boolean 型のサブプロパティを用意してもほとんど意味はないでしょう!
TypeConverter は、実際には.NET Framework の中でいくつかの機能を持っていますが、特にプロパティ ブラウザの中で重要な役割を果たしています。その名前が示唆するように、TypeConverter は値を 1 つの型から別の型に動的に変換するための標準的な手段を提供します。たとえば、プロパティ ブラウザが直接扱えるのは文字列だけなので、これらの文字列値をプロパティの型に変換したり、その逆を行う作業は TypeConverter に任されています。また、TypeConverter は型の展開が可能かどうかを制御し、複合的な型をプロパティ ブラウザでシームレスに扱えるようにしています。
たとえば、次のような Person という名前の型があったとします。
[TypeConverter(typeof(PersonConverter))]
public class Person {
private string firstName = "";
private string lastName = "";
private int age = 0;
public int Age {
get {
return age;
}
set {
age = value;
}
}
public string FirstName {
get {
return firstName;
}
set {
this.firstName = value;
}
}
public string LastName {
get {
return lastName;
}
set {
this.lastName = value;
}
}
}
このクラスには TypeConverterAttribute が適用され、この型で使用される TypeConverter が指定されています。TypeConverterAttribute が指定されていなければ、デフォルトの TypeConverter が選択されますが、これにはあまり機能はありません。PersonConverter の例では、TypeConverter の GetPropertiesSupported および GetProperties メソッドをオーバーライドして、型を展開可能にしています。
internal class PersonConverter : TypeConverter {
public override PropertyDescriptorCollection
GetProperties(ITypeDescriptorContext context,
object value,
Attribute[] filter){
return TypeDescriptor.GetProperties(value, filter);
}
public override bool GetPropertiesSupported(
ITypeDescriptorContext context) {
return true;
}
}
この操作は頻繁に使用されるので、.NET Framework にはこのコードとまったく同じ機能を持つ ExpandableObjectConverter という名前の TypeConverter からの派生形が含まれています。ExpandableObjectConverter から TypeConverter を派生させれば、簡単に型を展開可能にすることができます。次に、Person 型を文字列との間で変換できるように TypeConverter を変更します。
internal class PersonConverter : ExpandableObjectConverter {
public override bool CanConvertFrom(
ITypeDescriptorContext context, Type t) {
if (t == typeof(string)) {
return true;
}
return base.CanConvertFrom(context, t);
}
public override object ConvertFrom(
ITypeDescriptorContext context,
CultureInfo info,
object value) {
if (value is string) {
try {
string s = (string) value;
// "Last, First (Age) " というフォーマットを解析する
//
int comma = s.IndexOf(',');
if (comma != -1) {
// カンマが現れたので、
// ラスト ネームを取得する
string last = s.Substring(0, comma);
int paren = s.LastIndexOf('(');
if (paren != -1 &&
s.LastIndexOf(')') == s.Length - 1) {
// ファースト ネームを取得する
string first =
s.Substring(comma + 1,
paren - comma - 1);
// 年齢を取得する
int age = Int32.Parse(
s.Substring(paren + 1,
s.Length - paren - 2));
Person p = new Person();
p.Age = age;
p.LastName = last.Trim();
.FirstName = first.Trim();
return p;
}
}
}
catch {}
// ここに到達した場合には、文字列が
// 解析できなかったことを報告する
//
throw new ArgumentException(
"Can not convert '" + (string)value +
"' to type Person");
}
return base.ConvertFrom(context, info, value);
}
public override object ConvertTo(
ITypeDescriptorContext context,
CultureInfo culture,
object value,
Type destType) {
if (destType == typeof(string) && value is Person) {
Person p = (Person)value;
// 単に "Last, First (Age) " の形の文字列を作成する
return p.LastName + ", " +
p.FirstName +
" (" + p.Age.ToString() + ")";
}
return base.ConvertTo(context, culture, value, destType);
}
}
これで、十分な機能を持つ TypeConverter が完成しました。これは展開可能であり、ユーザーはこの型をサブプロパティとして、または 1 つの文字列として操作することができます。
図 3. 展開可能な TypeConverter
プロパティを上の図のように使用するには、UserControl を作成し、これを次のプロパティ コードに貼り付けます。
private Person p = new Person();
public Person Person {
get {
return p;
}
set {
this.p = value;
}
}
カスタム型の編集と表示
プロパティ ブラウザの中でのプロパティ編集には 3 つの方法があります。第 1 に、プロパティをその場で文字列として編集し、(必要ならば) TypeConverter に値と文字列との間での変換を行わせることができます。第 2 に、ドロップダウン矢印によって、プロパティの下に編集用 UI を表示させることができます。最後に、省略記号ボタンによって、ファイル ダイアログやフォント ピッカーなどの他のカスタム UI を表示させることができます。文字列編集についてはすでに説明したので、まずドロップダウンの例について解説しましょう。
.NET Framework にはドロップダウン編集の例がいくつか含まれています。コントロールの AccessibleRole、Dock、および Color プロパティは、ドロップダウン エディタによって実現可能な処理の実例となっています。
図 4. ドロップダウン エディタ
TypeConverter は単純なドロップダウンの仕事も行います。TypeConverter のドキュメンテーションを見ると、これを行うための仮想メソッドとして GetStandardValuesSupported()、GetStandardValues()、および GetStandardValuesExclusive() の 3 つがあることがわかります。これらのメソッドにより、プロパティに対して定義済みの値のリストを提供することができます。実際、(一番左のグラフィックの例のように) プロパティ ブラウザがドロップダウンに Enum 値を表示できるのは、TypeConverter のおかげです。プロパティ ブラウザは Enum 型を扱うためのコードは持っておらず、TypeConverter の機能を使用しています。
たとえば、"FamilyMember" という名前のコンポーネントが、ユーザーが人物間の関係を選択するための "Relation" という名前のプロパティを持っていたとします。選択を容易にするために、このプロパティには母、父、娘、姉妹などの一般的な値を含んだドロップダウンが用意されています。また、関係にはさまざまな可能性が考えられるので、ユーザーがカスタム文字列を入力できるようにしておくのが望ましいでしょう。
public class FamilyMember : Component {
private string relation = "Unknown";
[TypeConverter(typeof(RelationConverter)),
Category("Details")]
public string Relation {
get { return relation;}
set { this.relation = value;}
}
}
internal class RelationConverter : StringConverter {
private static StandardValuesCollection defaultRelations =
new StandardValuesCollection(
new string[]{"Mother", "Father", "Sister",
"Brother", "Daughter", "Son",
"Aunt", "Uncle", "Cousin"});
public override bool GetStandardValuesSupported(
ITypeDescriptorContext context) {
return true;
}
public override bool GetStandardValuesExclusive(
ITypeDescriptorContext context) {
// ここで false を返すと、プロパティは
// ドロップダウンと、手動で入力できる値を
// 持つという意味になる
return false;
}
public override StandardValuesCollection GetStandardValues(
ITypeDescriptorContext context) {
return defaultRelations;
}
}
では、これよりも高度にカスタマイズされたユーザー インターフェイスは、どうすれば実現できるのでしょうか? この目的には、UITypeEditor という名前のクラスを使用することができます。UITypeEditor は、プロパティを表示するときと、ユーザーがドロップダウンまたはポップアップ エディタのボタンをクリックしたときにプロパティ ブラウザから呼び出されるいくつかのメソッドを含んでいます。
Image、Color、または Font.Name などのいくつかのプロパティ型は、値が表示されるスペースのすぐ左に、その値の小さなグラフィック表現を描画します。これは UITypeEditor の PaintValue メソッドをインプリメントすることによって行われます。エディタを定義しているプロパティのプロパティ値を描画するとき、プロパティ ブラウザはそのエディタを長方形と Graphics オブジェクトによって表現します。例として、プロパティ ブラウザの中に表現を描画することができる Grade という型を考えてみましょう。Grade クラスは次のような内容を持っています。
[Editor(typeof(GradeEditor), typeof(System.Drawing.Design.UITypeEditor))]
[TypeConverter(typeof(GradeConverter))]
public struct Grade
{
private int grade;
public Grade(int grade)
{
this.grade = grade;
}
public int Value
{
get
{
return grade;
}
}
}
プロパティ ブラウザにグレードを入力すると、その値に応じて異なるビットマップが表示されます。
図 5. プロパティ ブラウザへのグレードの入力
これを実現するためのコードは単純です。上の Grade 型の EditorAttribute が GradeEditor クラスを参照していることに注意してください。
public class GradeEditor : UITypeEditor
{
public override bool GetPaintValueSupported(
ITypeDescriptorContext context)
{
// プロパティ ブラウザに対し、
// カスタム描画を行うことを通知する
return true;
}
public override void PaintValue(PaintValueEventArgs pe)
{
// 値に基づいて正しいビットマップを選択する
string bmpName = null;
Grade g = (Grade)pe.Value;
if (g.Value > 80)
{
bmpName = "best.bmp";
}
else if (g.Value > 60)
{
bmpName = "ok.bmp";
}
else
{
bmpName = "bad.bmp";
}
// ビットマップを、用意されたサーフェス上に描画する
Bitmap b = new Bitmap(typeof(GradeEditor), bmpName);
pe.Graphics.DrawImage(b, pe.Bounds);
b.Dispose();
}
}
前に述べたように、UITypeEditor はプロパティ値に対してポップアップまたはドロップダウン エディタも提供することができます。この記事で後に示すサンプルは、このプロセスの例を示しています。詳細については、UITypeEditor.GetEditStyle および UITypeEditor.EditValue メソッドと、IWindowsFormsEditorService インターフェイスの項を参照してください。
代替プロパティ ビューの提供
Visual C#? .NET で Windows フォーム アプリケーションを作成していると、プロパティ ブラウザのツールバーの上に、稲妻のようなボタンが追加されていることに気づくでしょう。このボタンを押すと、プロパティの代わりにイベント ハンドラを編集できるプロパティ ブラウザ ビューに移動することができます。これは実際には、開発者による拡張が可能なメカニズムとなっています。
プロパティ ブラウザ内のビューは「プロパティ タブ」と呼ばれており、ビューの追加に関連する中心的なクラスは System.Windows.Forms.Design.PropertyTab という名前を持っています。プロパティ タブは特定のコンポーネントやデザイナ ドキュメントに関連付けることができ、またつねに利用可能なように静的に関連付けることができます。コンポーネントまたはドキュメントに関連するタブは、クラスの PropertyTabAttribute を使って指定されます。この属性は、作成するタブのタイプを指定し、また PropertyTabAttribute の PropertyTabScope パラメータを通して、プロパティ ブラウザ上での可視性をどのように管理するかを指定します。スコープ付きのコンポーネントを持つタブは、PropertyTabAttribute を含んでいるコンポーネントが可視状態のときにのみ可視状態となります。ドキュメント スコープのタブは、現在のデザイナ ドキュメント上のすべてのデザイナ オブジェクトで可視状態を保ちます。プロパティ タブのデフォルト スコープは PropertyTabScope.Component です。
サンプルの PropertyTab については、付属の "FunkyButton" プロジェクトを参照してください。FunkyButton は、ボタンの形状を長方形以外のコントロールに変更できる PropertyTab を公開している UserControl です。
図 6. FunkyButton
プロパティ ブラウザは、選択されたオブジェクトのプロパティを、現在選択されているプロパティ タブから取得します。つまり、プロパティ タブには、表示されているプロパティのセットを操作する機会が与えられています。Events タブは、プロパティのような形でイベントを返すことによって、このような操作を行います。この例では、プロパティ タブはコントロールの頂点を表すプロパティを作成します。
.NET Framework の中のプロパティは、PropertyDescriptor という名前のクラスによってカプセル化されています。PropertyDescriptor そのものは抽象基本クラスで、フレームワーク内の特殊な派生クラスが、オブジェクトが公開している通常のプロパティへのアクセスを提供します。ただし、プロパティ ブラウザはプロパティに対して直接作用するのではなく、これらの PropertyDescriptor オブジェクトに作用するので、プログラマは特殊なタスクを実行する独自の PropertyDescriptor を作成することができます。この例では、コントロールの頂点の数を表すものを 1 つ、そして個々の頂点を表すものを 1 つ作成します。ここでも、どのオブジェクトの実際のプロパティにも対応していないエントリをプロパティ ブラウザに追加していることに注意してください。
プロパティ ブラウザは、プロパティの PropertyTab を要求するときには GetProperties メソッドを呼び出します。このサンプルの PropertyTab では、このメソッドは次のような内容を持ちます。
public override PropertyDescriptorCollection
GetProperties(ITypeDescriptorContext context, object component,
Attribute[] attrs)
{
// プロパティのリスト
//
ArrayList propList = new ArrayList();
// 頂点の数のプロパティを追加する
//
propList.Add(new NumPointsPropertyDescriptor(this));
// 各頂点のプロパティ記述子を追加する
//
for (int i = 0; i < ((FunkyButton)component).Points.Count; i++)
{
propList.Add(new VertexPropertyDescriptor(this,i));
}
// PropertyDescriptor のコレクションを返す
PropertyDescriptor[] props =
(PropertyDescriptor[])
propList.ToArray(typeof(PropertyDescriptor));
return new PropertyDescriptorCollection(props);
}
GetProperties は、単にわれわれが返そうとするプロパティ記述子のコレクションを返します。PropertyDescriptor そのものはきわめて単純です。作成方法については、コード サンプルを参照してください。
FunkyButton サンプルは、ドロップダウン エディタのインプリメンテーションも示しています。個々の Point の頂点について、PropertyTab は Point 値を入力するときに使われる標準的な "X,Y" 方式を、FunkyButton の形状の表現を表示するエディタに置き換え、頂点をグラフィカルに配置できるようにします。これにより、ボタンの形状の設定がはるかに簡単になります。
図 7. 頂点のグラフィカルな配置
プロパティを提供しているのはカスタムの PropertyTab なので、このプロパティのエディタをオーバーライドするのも簡単です。このためには、PropertyDescriptor の GetEditor メソッドをオーバーライドし、カスタム エディタのインスタンスを返します。
public override object GetEditor(Type editorBaseType)
{
// UITypeEditor を探す
//
if (editorBaseType == typeof(System.Drawing.Design.UITypeEditor))
{
// エディタを作成し、返す
//
if (editor == null)
{
editor = new PointUIEditor(owner.target);
}
return editor;
}
return base.GetEditor(editorBaseType);
}
エディタのデザインも同じように簡単です。エディタは単なる UserControl なので、他のタイプの WindowsForms オブジェクトと同じようにデザインすることができます。
図 8. エディタのデザイン
最後に、ユーザーがプロパティ ブラウザの下向き矢印をクリックしたとき、エディタは自分のユーザー インターフェイスを作成し、ドロップダウンとして表示する機会を与えられます。これは PointUIEditor の UITypeEditor.EditValue のオーバーライドによって処理されます。
public override object EditValue(
ITypeDescriptorContext context,
IServiceProvider sp, object value)
{
// エディタ サービスを取得する
IWindowsFormsEditorService edSvc =
(IWindowsFormsEditorService)
sp.GetService(typeof(IWindowsFormsEditorService));
// UI を作成する
if (ui == null)
{
ui = new PointEditorControl();
}
// UI を、この頂点の設定を使って初期化する
ui.SelectedPoint = (Point)value;
ui.EditorService = edSvc;
ui.Target = (FunkyButton)context.Instance;
// エディタ サービスに対し、コントロールをドロップダウンとして
// 表示するように指示する
edSvc.DropDownControl(ui);
// 更新された値を返す
return ui.SelectedPoint;
}
実際の使用方法
最後に、Visual Studio .NET のプロパティ ブラウザのコアの部分を、読者のアプリケーション内で使用できるようにしました。この System.Windows.Forms.PropertyGrid という名前のコントロールを Visual Studio .NET のツールボックスに追加するには、[Customize Toolbox] ダイアログ ボックスの [.NET Framework Components] タブで [PropertyGrid] を選択します。
PropertyGrid は通常の Windows フォーム コントロールと同じように動作します。レイアウト内でアンカー設定やドッキングを行うことができ、その色を変更することもできます。次に、PropertyGrid に固有の興味深いプロパティを示します。
-
SelectedObject
PropertyGrid がプロパティを表示するオブジェクトを決定します。 -
ToolbarVisible
PropertyGrid の上部のツールバーを表示または非表示にします。 -
HelpVisible
PropertyGrid の下部のヘルプ テキストを表示または非表示にします。 -
PropertySort
PropertyGrid の並べ替えのタイプを決定します (カテゴリ別、アルファベット順など)。
いずれのプロパティも、デザイン時に通常と同じように設定することができます。ただし実行時には、PropertyGrid を使って、それがブラウズしているオブジェクトを操作できるようになります。以下に、ボタンをブラウズしている Form 上の PropertyGrid の例を示します。この例では、PropertyGrid のツールバーとヘルプ情報は非表示になっています。上で述べたように、これは PropertyGrid 型そのもののプロパティを使って制御することができます。
図 9. ツールバーとヘルプ情報が非表示となっている PropertyGrid
結論
.NET Framework と Visual Studio .NET では、プロパティ ブラウザの新バージョンに多数の新機能が追加されています。プロパティ ブラウザは開発者の RAD エクスペリエンスの中核にあるので、プロパティ ブラウザを Visual Basic でポピュラーにした使いやすさを保ちながら、さらに高度な柔軟性が実現されています。アプリケーション内で PropertyGrid を使用することにより、ユーザー インターフェイス デザインを単純化し、ユーザー インターフェイスのレイアウトに費やす時間を減らして、より多くの時間を優れたアプリケーションの開発に費やせるようになります。