2008年11月28日金曜日

[VC++ 2008 Express] FormのresXにリソースを追加しても消されてしまう

VC++のフォームアプリケーションで、フォームとは無関係なリソース(例えばイメージ)をそのフォームのresXに追加することはできるのですが、何かの拍子に消えてしまいます(おそらく、フォームデザイナがヘッダーファイルを生成ときのタイミングだと思います)。

VC++のフォームプロジェクトにあるフォームは、ソリューションエクスプローラー内においては、ソースコードは「<フォーム名>.h」でその下の階層に「<フォーム名>.resX」という表示になっています。このresXを開くとリソースの一覧が表示されて、編集可能となっています。そこで、イメージなどのリソースを追加できるのですが、何故か消えてしまうのです。

そこで、フォームとは別なところにリソースを定義して、プログラムで読み取ろうということですが、少なくとも「rcファイル」はmanagedなプロジェクトでは意味がありません。なぜ、Formアプリケーションのプロジェクトに存在しているかはわかりませんのは、おそらくアプリケーションのアイコンの設定するためなのだが、.NETのドキュメントを読むとrcファイルのリソースは利用できませんと書いてあります。

managedなモジュール(Assemblyといったほうが適切か…)にこの手のリソースを組み込む方法はいくつか存在するようですが、新たにresXファイルを追加してそこにリソースを定義するという方法をやってみました。

空のresXファイルを作る

プロジェクトに組み込むresXファイルを用意します。resXといっても所詮XMLファイルなので、ベタな方法として「メモ帳」で作ることにします。ただし、中身がresXの定義に則してないとIDE上で編集できないですし、ビルド時にエラーになるので、最低限の定義だけを書いておきます。

必要な部分だけ、プロジェクト内にあるresXからコピペするのですが、まず、プロジェクトにある(エラーになっていない)resXファイルをXMLエディタで開きます(コンテキストメニューから「ファイルを開くアプリケーションの選択」というのを選ぶ)。そのresXファイルから、最初の「<?xml version="1.0" encoding="utf-8"?>」から、「</resheader>」の部分まで(たぶん、「<resheader name="writer">」の対になっているのが最後のなので、そこまでをコピー、ペーストします。そのあとの行に「</root>」でrootを閉じればOKです。
ファイルは、拡張子をresXにしてUTF-8で保存します。拡張子よりも前の部分(いわゆるbasename)はプログラムで読み込むときに指定するので、わかり易いものにしておいたほうがよいでしょう。

プロジェクトに追加する

作った空のresXファイルをプロジェクトのコンテキストメニューで「追加→既存の項目」で追加すると、ソリューションエクスプローラー上でプロジェクトの下の「リソースファイル」というツリーの中に現れるはずです。そうしたら、そのresXファイルを開くと、resX用のリソース定義用のビュー(マネージリソースエディタ)が表示されます。
そこで、好きなリソースを追加してください。

リソースの読み込み

ManagedなC++のコードの中でリソースを読み込む方法ですが、まずResourceManagerでresXに定義されたリソースを読み込みます。resXファイル名のbasenameを「リソース名」ということにすると(「hoge.resX」なら「hoge」)。

//アセンブリを取得する
System::Reflection::Assembly^ assembly = System::Reflection::Assembly::GetExecutingAssembly();
//リソースを読み込む
System::Resources::ResourceManager^ resources = 
 gcnew System::Resources::ResourceManager("<アセンブリ名>.<リソース名>", assembly);

ここでの「アセンブリ名」といっているのは(この言い方が正しいかは不明)、普通はプロジェクト名になっているのでプロジェクト名にします。

あとは、ResourceManagerなどのドキュメント、サンプルを見ると使い方がわかりますが、例として、イメージを読み込む場合は以下のような感じになります。

//リソースからイメージを読み込む
System::Drawing::Bitmap^ image = (System::Drawing::Bitmap^)(resources->GetObject(L"<リソースオブジェクト名>"));

ここでの「リソースオブジェクト名」は、resXの定義で命名したものを指定します。マネージリソースエディタでイメージファイルを読み込むとファイル名のbasenameになりますが、エディタ上で変更できます。

この場合、リソースファイルを手動で追加しましたが、いわゆる国際化(カルチャー毎の設定)も対応しているはずなので、ファイル名にカルチャー名を付加したresXファイルを用意しておけば、ResourceManagerで適切に処理されるはずです。

2008年11月26日水曜日

[.NET]バインドされたDataGridViewに追加した項目を選択状態にする

先ほどの記事でDataGridViewとBindingSourceおよびDataTableの関係について書いたが、それを調べるための目的は、DataGridViewにバインドされたDataTableにプログラムで行を追加したときにその項目を選択させるためだったので、その方法を書いてみることにする。

実際にこのような動作をするプログラムを書かないと、状況がつかめないかもしれないが、DataGridViewは、バインドされたDataTableに新しく行を追加しても、選択位置は一番最初のままである。そのため、新しく追加された項目は、スクロールアウトされていると表示されない。しかも、DataGridViewは選択されている項目を表示させようとするため、さらに行が追加されると、選択されている行がスクロールアウトされていても選択行を表示させるためスクロールする。

すなわち、新しい行を表示させるためにはDataGridViewの選択行を変更しなければならない。ここで前提としては、DataGridViewの選択モードは、FullRowSelectになっているものとする。CellSelectでもうまくいくと思われるが試してはいない。

以下のコードはC++で書くことにするが、VC++2008 Express EditionでDataTableやDataSetをデータデザイナを使って定義する方法は、こちらの記事を参照されたい。

まず、データデザイナで定義をするとヘッダファイルにDataSetのサブクラスが作られて、その中にDataTableのサブクラスを定義するようなコードを生成するが、ここでは、"MyDataSet"や"MyDataTable"のように"My"+クラス名というものを定義したとする。

あと、DataTableの定義の前提として、主キーでAutoIncrementされる列(Column)を定義し、カラム名は"ID"とする。あと、定義されたDataSet(すなわち"MyDataSet")は、Formのメンバー変数として、「MyDataSet^ myDataSet;」と定義され、コンストラクタのどこかで、「myDataSet = gcnew MyDataSet();」のようにインスタンスを生成しており、また、フォームデザイナでは、BindingSourceを"myBindingSource"と定義したとする。

とりあえず、コードは以下の通り

//新しい行を生成する
MyDataSet::MyDataTableRow^ newRow = myDataSet->MyDataTable->NewMyDataTableRow();

//ここでnewRowのメンバー(カラム)にデータを代入する
//newRow-><カラム名> = …;
//カラム"ID"は追加時に設定されるので何もしなくてよい

//行をDataTableに追加する
myDataSet->MyDataTable->Rows->Add(newRow);
myDataSet->MyDataTable->AcceptChanges();

//追加した行のBindingSourceの位置を取得する
int position = myBindingSource->Find(L"ID", newRow->ID);

//BindingSourceの現在の位置を変更する
myBindingSource->Position = position;

ここでの最大のポイントは、BindingSourceの現在の位置はDataGridViewの選択行に一致するのだが、項目がソートされているとDataTableの行(Row)のインデックスとは一致しない。そのため、新しく追加した項目がBindingSourceでどの位置にあるかを調べてから設定するということである。

あと、このコードでの"newRow"はAdd();で"MyDataTable"に追加すると、"ID"にAutoIncrementされた値が代入される。そのため、この値を使って"myBindingSource"のFind();を呼び出せば、位置がわかるということになる。

[.NET]DataGridViewのデータについて

前記事でVC++ 2008 ExpressでDataGridViewにDataSetをバインドする方法について書いたが、その後DataGridViewを操作しようとしたところ、多少はまってしまった。ドキュメントを読んでわかったことだが、DataGridViewと表示・編集しようとしているデータとの関係について書いてみることにする。

DataGridViewに表示されるデータがどこにあるのか、という意味で3つのパターンがある。

普通のモード
普通という言い方が正しいかどうかはわからないが、単にDataGridViewのインスタンスを生成した場合の状態で、対象となるデータはDataGridViewの中に存在する。具体的には、DataGridView.RowsプロパティであるDataGridViewRowCollectionの中に行のデータが格納されている。
バインディングモード
これは、前記事で書いた、DataSetなどに存在するデータをDataGridViewに表示・編集する方法で、具体的には、DataGridView.DataSourceプロパティにデータの実体があるオブジェクトを設定する。VC++やVC#のIDEでデータデザイナとフォームデザイナを使った場合には、これらの設定はIDEによってコード化されるので、特にコードを書く必要はない。
仮想モード
このモードについては、MSDNなどのドキュメントにいろいろ書かれているので、詳細はそちらへ譲るが、基本的にはDataGridViewが表示・編集のためにデータの参照・設定が必要になった場合、イベントが発生する。そのイベントの処理でデータを受け渡しなどをする。この場合、データの実体はプログラマー任せとなる。

実際には、編集などではもっと複雑なデータのやりとりをするようだが、特殊なことをしない限り、あまり意識する必要はないもよう。この手のことは、DataGridViewのドキュメントの中に「共有モード」という記述があるので、それを参照されたい。

ここのでポイントは、バインディングや仮想モードになっているときには、DataGridView.Rowsプロパティにデータが存在しないということである。要するにこのプロパティを使ってデータを操作しているドキュメント上のサンプルなどは、普通のモードで動いているDataGridViewでの話であって、バインディングなどの場合には通用しない。

ところで、バインディングの場合、DataSourceは、基本的にはBindingSourceのインスタンスを設定する。このBindingSourceに実際のデータをBindingSource.DataSourceに設定する。このことは、ドキュメントに詳しく書かれているので詳細は割愛するが、BindingSource.DataSourceには、DataSetなどの複雑な構造のデータや、単純なオブジェクトのリスト(System.Collections.Generic.Listなど)を設定することができる。

DataGridViewで編集などをする場合には、行やセルの選択をいう操作が必要になるのだが、BindingSource.DataSourceに設定されるデータは、列挙可能(IListが実装されている)なものでしかないので、選択された位置という情報は持っていない。すなわち、BindingSourceが選択や項目の並び替え(ソート)を担っているということになる。

プログラムで項目を選択したい場合、普通のモードのDataGridViewときには、Rowsプロパティから行に対するデータを取得して、セルや行(Row)に対して選択を設定する。ところが、バインディングモードの場合、DataGridView.Rowsプロパティを使うのではなく、DataSourceプロパティに設定されたBindingSourceを操作する。具体的にはPositionプロパティやMoveFirstメソッドなどで現在の位置を変更する。

DataGridView.DataSourceにBindingSourceを設定している場合、プログラムでBindingSourceの現在の位置を変更すると、DataGridViewの表示上の選択位置が変更され、マウス操作などでDataGridViewの選択位置が変更されるとBindingSourceの現在の位置が変更される、という仕組みになっている。