Last Updated 2011/09/21
コントロールのインストールのページで、作成したコントロールをツールボックスにコントロールをあらわすビットマップを表示する手順について説明しましたが、そのときのトラブルの解決策について解説します。
表題のトラブルについて調べようと、WEB サイトをウロウロしていたところ、http://www.bobpowell.net/ を見つけました。このサイトは GDI+ 関係の記事がたくさんあるサイトですが、その中に "Windows Forms Chips and Tricks" のページの [Controls]-[The ToolboxBitmap Puzzle] があります。このページでトラブルの解決策を説明しています。ただし、ToolboxBitmapAttribute クラスのGetImageFromResource メソッドにバグがあると書いていますが、どういうバグなのかの説明がありません。また、"Default Namespace" とは何をさすのかの説明がないなど、分かりにくい構成になっています。
そのほかの海外のサイトでも同じトラブルに陥っている人が多くいるようで、たくさんの人が質問を投げかけています。それに対する回答は、bobpowell.net から引用する形で解決策を示しているサイトが多いですね。しかし、いずれもこのトラブルの発生原因について触れていないため、明示された解決策で本当にいいのかの判断ができません。こういう状態はかなり気持ちの悪いものです。そこで、あらためて ToolboxBitmapAttribute クラスのGetImageFromResource メソッドを調査したところ、トラブルの原因が分かりました。
このメソッドは ToolboxBitmapAttribute 属性を付加したクラスの場合、.Net Framework がビットマップを取り出すときに呼び出すもので、そのプロトタイプは以下のとおりです。
public static Image ToolboxBitmapAttribute.GetImageFromResource( Type type, // imageName の名前空間を取得するためのアセンブリの型 string imageName, // リソースとして埋め込んだビットマップまたはアイコンのリソース名(通常はファイル名) bool large // 大きいイメージ(32 x 32)を返すとき true(false のとき、16 x 16 ピクセル) );
このメソッドは以下に示すように、Assembly クラスの GetManifestResourceStream メソッドを呼び出してビットマップのデータを保持するストリームを返します。
Stream stream = type.Module.Assembly.GetManifestResourceStream(type, imageName);
問題は GetManifestResourceStream メソッドの引数として渡す最初の引数にあります。type はリソースの名前空間を取得するために使うものですが、型の名前空間、つまり、コントロールを定義するクラスの名前空間を取得するために指定するものです。この名前空間は本来リソース名を解決するためのものではなくてはなりませんが、それを取得してわけではないのです。では、このメソッドは間違っているのでしょうか。この疑問は以下のコードを実行すれば分かります。
Windows フォームアプリケーションのプロジェクトにビットマップを追加し、その [プロパティ] を開いて、[ビルドアクション] を 「埋め込まれたリソース」に変更したあと、次のコードを実行してください。フォームには button1 と pictureBox1 を配置しているとします。また、ビットマップ名は "AppTest.bmp" とします
private void button1_Click(object sender, EventArgs e) { Image image = ToolboxBitmapAttribute.GetImageFromResource(this.GetType(), "AppTest.bmp", false); if (image != null) pictureBox1.Image = image; }
このコードは正常に動作します。つまり、このケースではメソッドにバグがあるとはいえません。
ここで、以後の説明を容易にするため、UserControl クラスから派生するコントロールを定義するクラスを示します。
namespace MyNamespace { public partial class MyControl : UserControl { .... } }
このプロジェクトの [プロパティ]-[アプリケーション] のページの [アセンブリ名] が "MyAssmblyName"、[既定の名前空間] が " MyDefaultNamespace" とします。また、プロジェクトに追加し、[ビルドアクション] を「埋め込まれたリソース」に変更したビットマップファイル名を "MyBitmapFileName" とします。
さて、こういう状態のとき、.Net Framework SDK に収録されているサンプルコードでは ToolboxBitmapAttribute 属性を以下のように指定するとなっています。
namespace MyNamespace { [ToolboxBitmap(typeof(MyControl), "MyBitmapFileName")] public partial class MyControl : UserControl { .... } }
これでうまくいけば文句なしです。しかし、問題のトラブルに陥ります。トラブルの原因を先に説明したことを念頭において考えて見ましょう。
TolboxBitmapAttribute 属性の最初の引数は MyNamespace を返しますので、リソースを検索するときのリソース名は "MyNamespace MyBitmapFileName" となります。しかし、実際のリソース名は "MyDefaultNamespace.MyBitmapFileName" です。名前が違うのですからリソースの検索に失敗するのは当然ですね。
クラスの名前空間名を以下のように "MyDefaultNamespace" に変更します。
namespace MyDefaultNamespace ← 変更 { [ToolboxBitmap(typeof(MyControl), "MyBitmapFileName")] public partial class MyControl : UserControl { .... } }
こうすると、最初の引数は "MyDefaultNamespace" を返しますから結果的にリソース名は "MyDefaultNamespace.MyBitmapFileName" となって正常に動作します。
この解決策はあくまで結果的にそうなるというだけで、根本的な解決策ではありません。というのは、Microsoft は ラスの名前空間名として、"CompanyName.TechnologyName" の形式にするよう推奨しています。つまり、このルールに違反することになるからです。
以下は私のアイデアではなく、http://www.bobpowell.net/ で紹介されている解決策です。まず、コードを紹介してから説明します。
internal resfinder ← 追加 { } namespace MyNamespace { [ToolboxBitmap(typeof(resfinder), "MyDefaultNamespace.MyBitmapFileName")] ← 変更 public partial class MyControl : UserControl { .... } }
ポイントは、本来の名前空間の外側に空のクラスを定義します。クラス自体はなにもしません。要するに、空の名前空間名(長さ 0 の文字列)を戻すだけの目的で使います。その代わり、ビットマップ名には正しいリソース名、つまり、"MyDefaultNamespace.MyBitmapFileName" を指定します。これで任意の名前空間名を設定することができます。
このトラブルに対して、Microsoft は沈黙しています。バグという認識がないからなのかどうかは分かりませんが、バグフィックスをしない可能性があります。
Microsoft の開発者は自分で(プライベートにという意味) Visual Studio を使ってコントロールを作成するということはないのではないかといつも思います。もし、使っているのならすぐに気が付くはずですから。
私がテストする限り、解決策の 1 も 2 もうまくいきますが、どこかで不都合が発生するのではないかと心配です。そこで、私はビットマップファイルを添付する方法を選びたいと考えています。第三者に配布するコントロールならなおさら安全なほうを選ぶべきでしょう。
−以上−