このページでは、アプリケーションの1ユニットでしかなかったクラスを、コンポーネントに変換する手順を説明します。
おしながき.
フォームファイルへの保存.
フォームファイルへのプロパティ値の保存.
フォームファイルに保存されたプロパティ値は、次回フォームを読み込んだときに、値が復元されます。つまりユーザがフォームに配置し、オブジェクトインスペクタで設定した値を、フォームを開き直したときに再現できるようになります。
フォームファイルはテキストファイルなので、一度覗いてみるとどのようにプロパティ値が保存されるのかわかります。
パブリッシュプロパティ値の保存の指定.
パブリッシュプロパティの場合、以下のいずれかに該当すると、プロパティ値がフォームファイル(*.dfm)に保存されます。
- storedキーワードで指定した値がtrue。
- storedキーワードで指定したメソッドがtrueを返した。
- オブジェクトインスペクタでプログラマが設定した値が、初期値と異なる。
- 初期値が未定(ない、またはnodefaultが指定された)。
ただし、C++Builderのヘルプには説明が無いのですが、プロパティが書き込み可能である場合のみ保存されるようであり、読み出し専用のプロパティは保存されないようです。
さらに注意が必要なのは、後で述べる継承されたフォームに配置されたコンポーネントでは、初期値とstoredキーワードは無視されるようです。つまり継承元のフォーム内の該当コンポーネントの対応するプロパティーの値と不一致の場合のみ、フォームファイルにプロパティー値が保存されるようです。storedキーワードが無視されるのは予想外で、このような仕様でいいのか、ちょっとC++Builderを疑っています。
storedキーワードによるプロパティ値の保存の指定.
storedキーワードを使用すれば、プロパティ値をフォームファイルに保存するか否かを直接指定できます。以下がその記述例です。プロパティのアクセスメソッドを記述する中カッコの中に、storedキーワードに続けて以下の値のいずれかを記述します。
__property bool Icon = {read=fIcon, write=setIcon, stored=true};
- true:常にプロパティ値を保存する。
- false:常にプロパティ値を保存しない。
- メソッド名:プロパティ値を保存するか否かを戻り値で返すメソッドを指定する。
文字列型(AnsiString型)のプロパティなどの単純型でない型の場合、プロパティの定義で初期値を指定できないので、プロパティ値を保存するか否かを返すメソッドを指定します。このメソッドで、設定されている値が初期値と異なる場合にtrueを返せば、初期値と異なる場合にのみ値をフォームファイルに保存するようにできます。
初期値の指定.
プロパティの初期値の指定は、プロパティの定義で行います。プロパティのアクセスメソッドを記述する中カッコの中に、defaultキーワードに続けて初期値を記述します。
__property bool Icon = {read=fIcon, write=setIcon, default=true};
初期値を指定しない場合は、nodefaultキーワードを記述します。ただし上位コンポーネントに対して新しく追加したプロパティの初期値は、defaultキーワードを使用して初期値を指定しなければ未定として扱われるので、初期値にnodefaultを記述しても特に意味はありません。つまりnodefaultキーワードが意味を持つのは、上位コンポーネントから継承したプロパティの初期値を取り消したい場合のみです。
__property bool Icon = {read=fIcon, write=setIcon, nodefault};
ただし、C++Builderのヘルプにも記述されているように、ここで記述した初期値によってプロパティ値が初期化されるわけではありません。プロパティ値の初期化はコンストラクタなどで別途明示的に行う必要があります。
サブコンポーネントの保存・復元.
プロパティの型が他のコンポーネントであるものはサブコンポーネント呼ばれます。プログラマが独自に造ったサブコンポーネントはC++Builderがフォームファイルへの保存方法を知らないので、フォームファイルへの保存手順やフォームファイルからの読み出し手順を明示的に指示しないと、フォームファイルへの保存・復元が出来ません。
サブコンポーネントの条件.
サブコンポーネントをフォームファイルに保存するためにはいくつか条件があります。C++Builderのヘルプによると、以下のいずれかを満たしている必要があります。
- サブコンポーネントがTPersistentの下位オブジェクトであり、サブコンポーネントを生成したコンポーネントがサブコンポーネントのオーナーである。さらにサブコンポーネントのGetOwnerメソッドをオーバーライドして、オーナーであるコンポーネントを返すようにしている。
- サブコンポーネントがTComponent(これはTPersistentの下位クラス)の下位オブジェクトであり、サブコンポーネントを生成したコンポーネントがサブコンポーネントのオーナーである。さらにサブコンポーネントのOwnerプロパティを、サブコンポーネントを生成したコンポーネントに設定している。
- サブコンポーネントがTComponentの下位オブジェクトである。さらにサブコンポーネントであることを示すためにSetSubComponentメソッドを呼び出している。
実際に試してみたところ、最初の条件でしかフォームファイルへの保存が出来ませんでした。他の条件については私の認識が怪しのかもしれないですし、したがって上記の条件の解釈も間違っているかもしれません。
保存・復帰に使用するメソッドの選択.
保存・復帰に使用するメソッドにはTWriterProc/TReaderProcを使用する方法と、TStreamProcを使用する方法の2種類があります。どちらを使うかは、サブコンポーネントの性質・特徴によって選択します。
- TWriterProc/TReaderProc型を使用する場合は、単純型と同じようにテキストでフォームファイルに値を保存する。実際にフォームファイルに格納されている様子は、Classes::TStrings*型のプロパティが参考になる。格納する値の数や順番はプログラマが決定する。DefinePropertyメソッドをオーバーライドする必要がある。
- TStreamProc型を使用する場合は、16進数表現のバイナリで値を保存する。実際にフォームファイルに格納されている様子は、Graphics::TBitmap*型のプロパティが参考になる。格納する値の意味はプログラマが決定する。DefineBinaryPropertyメソッドをオーバーライドする必要がある。
大抵の場合はTWriterProc/TReaderProc型で十分だと思います。私もTWriterProc/TReaderProc型しか使ったことがないので、TStreamProc型についてはこれ以上説明しません。以下の説明はすべてTWriterProc/TReaderProc型を使用した場合です。
保存メソッドの作成.
サブコンポーネントを直接保存するメソッドを作成します。これは保存したいサブコンポーネントのメソッドの1つとして実装します。
このメソッドは、引数によって渡されるTWriter型のメソッドを使用して、サブコンポーネントを保存します。TWriterクラスが用意しているメソッドの代表的なものの簡単な説明と、注意事項を記載します。
メソッド名 | 説明 | 注意事項 |
WriteBoolean | bool型の値を書き込む。 | - |
WriteChar | char型の値を書き込む。 | - |
WriteInteger | 整数型の値を書き込む。 | - |
WriteSingle | 浮動小数点(float)型の値を書き込む。 | - |
WriteFloat | 浮動小数点(Extended)型の値を書き込む。 | - |
WriteDate | TDateTime型の値を書き込む。 | - |
WriteString | 文字列(AnsiString)型の値を書き込む。 | - |
WriteIdent | 識別子を書き込む。 | 識別子は文字列として書き込まれる。列挙型・ポインタなどの値を識別子に変換するのは明示的に行う必要がある。 |
WriteVariant | Variant型の値を書き込む。 | ヘルプには「型識別子を書き込んでから値を書き込む」とあるが、実際には型識別子にあたるものは書き込まれず、値のみが書き込まれる。単純にWriteVariantをコールするだけと、復元時に整数型はchar/short/longの区別がつかなくなるなどの点に注意が必要。識別子が書き込まれないのはヘルプが間違っているのか、それとも明示的にプログラマが書き込むコードを記述しろという意味なのか、判断材料がありませんでした。 |
WriteListBegin | リストの開始マーカを書き込む。 | これらは必ずペアで使用する必要がある。 |
WriteListEnd | リストの終端マーカを書き込む。 |
これらの使用例を示します。
void __fastcall TSampleSubSComponent::saveData(TWriter *writer) { int i; // expListはTList型です。 writer->WriteListBegin(); for (i = 0; i < expList->Count; i++) { writer->WriteIdent(((TExplainItem *)expList->Items[i])->Control->Name); // コンポーネント名 writer->WriteString(((TExplainItem *)expList->Items[i])->Message); // メッセージ } writer->WriteListEnd(); }
復元メソッドの作成.
サブコンポーネントを復元するメソッドを作成します。これも復元したいサブコンポーネントのメソッドの1つとして実装します。
このメソッドは、引数によって渡されるTReader型のメソッドを使用して、サブコンポーネントを復元します。TReaderクラスが用意しているメソッドの代表的なものの簡単な説明と、注意事項を記載します。
メソッド名 | 説明 | 注意事項 |
ReadBoolean | bool型の値を読み出す。 | - |
ReadChar | char型の値を読み出す。 | - |
ReadInteger | 整数型の値を読み出す。 | - |
ReadSingle | 浮動小数点(float)型の値を読み出す。 | - |
ReadFloat | 浮動小数点(Extended)型の値を読み出す。 | - |
ReadDate | TDateTime型の値を読み出す。 | - |
ReadString | 文字列(AnsiString)型の値を読み出す。 | - |
ReadIdent | 識別子を読み出す。 | 識別子は文字列である。列挙型の場合、値への変換は明示的に行う必要がある。識別子からポインタを取得する場合、読み込み後の初期化で明示的に行う必要がある。 |
ReadVariant | Variant型の値を読み出す。 | 単純にReadVariantをコールするだけだと、読み出した値の型は、整数型では値を表現できる最小の型になるので、保存時の型とは異なる可能性がある。これが問題になる場合は型を別に保存するか、整数型の場合は型を変換するなどの処置が必要。これは前記と同じですね。 |
ReadListBegin | リストの開始マーカを読み出す。 | これらは必ずセットで使用する必要がある。 |
ReadListEnd | リストの終端マーカを読み出す。 | |
EndOfList | リストの終端マーカの存在をチェックする。 |
これらの例を示します。
この例ではわかりやすくするために省略していますが、実際にはTReader::Read〜メソッドは例外を発生する可能性があるので、例外を処理した方が良いと思われます。
void __fastcall TSampleSubComponent::loadData(TReader *reader) { AnsiString name, mess; Clear(); // まずはコンポーネントを初期化 reader->ReadListBegin(); while (!reader->EndOfList()) { name = reader->ReadIdent(); mess = reader->ReadString(); Append(name, mess); // 読み出したコンポーネント名とメッセージを記憶 } reader->ReadListEnd(); }
DefinePropertiesメソッドのオーバーライド.
保存メソッドと復元メソッドを作成したら、それらをC++Builderがコールできるように、その存在を通知しなければ意味がありません。それにはDefinePropertiesメソッドをオーバーライドし、TFiler::DefinePropertyメソッドで、保存メソッドと復元メソッドを通知します。
DefinePropertiesメソッドの例を示します。
void __fastcall TSampleSubComponent::DefineProperties(Classes::TFiler* Filer) { bool writeprop; int i; // 親クラスの同メソッド呼び出し TPersistent::DefineProperties(Filer); // 書き込む必要性のチェック writeprop = false; if (Filer->Ancestor) { // 継承した値である場合 // 値が同じであるか否かをチェック if (expList->Count != ((TSampleSubComponent *)Filer->Ancestor)->expList->Count) writeprop = true; else for (i = 0; i < expList->Count; i++) { if ((EXPITEM(i)->Message != ANCESITEM(i)->Message) || (EXPITEM(i)->CtlName != ANCESITEM(i)->CtlName)) { writeprop = true; break; } } } else { // 継承した値でない場合 writeprop = (expList->Count > 0); } Filer->DefineProperty("Data", loadData, saveData, writeprop); }
TFiler::DefinePropertyメソッドをコールする前に、親クラスのDefinePropertiesメソッドをコールしておく必要があります。これは上位クラスに含まれるサブコンポーネントを処理するために必要です。
TFiler::DefinePropertyメソッドの最初の引数はプロパティ名ですが、このプロパティ名はフォームファイルに記録するためだけに使用されるものであり、サブコンポーネントに存在しないものでかまいません。TBitBtn::Glyphプロパティなどが参考になると思います。TBitBtn::GlyphプロパティはフォームファイルにはGlyph.Dataとして保存されていますが、Graphics::TBitmapを調べてみるとDataなどというプロパティは存在しません。フォームファイルを覗いてみてください。
その後の2つの引数は保存メソッドと復元メソッドです。
最後の引数はプロパティの値を保存する必要があるか否かを指定するものであり、trueの場合に保存メソッドがコールされて保存されます。この値の指定方法については次節で説明します。
プロパティー値の保存の判断.
フォームファイルへはプロパティー値を常に書き込むのではなく、次の条件に該当するときのみ書き込みます。サンプルソースは前記のものを参照してください。
- コンポーネントが配置されているのが継承されたフォームではなく、プロパティー値が初期値と異なる場合。
- コンポーネントが継承されたフォームに配置されていて、プロパティー値が継承元フォーム内のインスタンスと異なる場合。
ここで、「継承されたフォーム」というものが何なのか理解しにくいと思います。実際には、言葉どおり、あるフォームから継承して作ったフォームだけでなく、フレームに配置された場合も該当しますので、注意が必要です。
これら継承されたフォームを考慮しないなら、継承されたフォーム内に配置されているコンポーネントのプロパティ値は、継承元と同じであってもフォームファイルに保存されます。当然、プログラムの実行やフォームデザイナでフォームファイルを開いた場合には保存されたプロパティー値を読み込みます。このとき、TComboBoxのItemsプロパティとItemIndexプロパティのように、一方のプロパティ値の読み込みが、他方の初期化をまねく関係にあると、「ItemIndexプロパティがフォームファイルに保存・復帰されない」という不具合の原因になります。
読み込み後の初期化.
TReader::ReadIdentメソッドで読み出したコンポーネント名から該当コンポーネントのポインタを得たい場合などには、コンポーネント名を検索する必要があります。しかし、フォームファイルから読み出したコンポーネントの順番は不定なので、TReader::ReadIdentメソッドをコールしている復元メソッドがコンポーネント名を読み出した時点では、該当コンポーネントがまだ生成されていない可能性があります。つまり復元メソッドが読み出したコンポーネント名から該当コンポーネントのポインタを検索するタイミングは、すべてのコンポーネントの読み出しが終わった後でなければなりません。
この問題を解決するために、Loadedメソッドをオーバーライドし、Loadedメソッドの中でこれらの処理を行うようにします。Loadedメソッドがコールされるタイミングは、フォームファイルの読み出しが終わった後です。
Loadedメソッドの例を示します。この例では、コンポーネントの親コントロール内に含まれるコンポーネントから、コンポーネント名の一致するコンポーネントを探してそのポインタを取得します。コンポーネント名はItemsに複数記憶しているものとします。
void __fastcall TSampleComponent::Loaded(void) { TComponent *rootctrl; AnsiString ctrlname; int i, j; // 親クラスの同メソッド呼び出し TBevel::Loaded(); // 親コントロール(フォーム/フレーム/ダイアログ)を取得 rootctrl = Owner; // コンポーネントのポインタを取得 for (j = 0; j < Items->Count; j++) { ctrlname = Items->CtlNames[j]; // 親コントロール内のコンポーネントから一致するものを探す for (i = 0; i < rootctrl->ComponentCount; i++) { if (rootctrl->Components[i]->Name == ctrlname) { Items->Controls[j] = (TControl *)rootctrl->Components[i]; // 発見 break; } } } }
コンポーネントの登録.
コンポーネントパレットへの登録.
作成するコンポーネントをコンポーネントパレットに登録したい場合、RegisterComponents関数をコールする必要があります。プロジェクトをコンポーネントの新規作成ダイアログからパレットページ名を指定して作ったのであれば、すでにテンプレートに作成されているはずです。
RegisterComponents関数へ渡す引数は順に、登録するパレットページ名、登録するコンポーネントのクラスIDのリストへのポインタ、クラスIDのリストの最後のインデックスです。詳細はヘルプを参照してください。
PagePanelの例だと、以下のようになります。
TComponentClass classes[1] = {__classid(TPagePanel)}; RegisterComponents("Ysl", classes, 0);
RegisterComponents関数は、関数名が「Components」と複数形になっていることからもわかるように、複数のコンポーネントクラスを一括して登録することも出来ます。その場合はテンプレートを修正して、コンポーネントの新規作成ダイアログで指定した以外のコンポーネントクラスを追加します。
例として、FontNameBoxには4つのコンポーネントが含まれていますので、次のように、クラスIDのリストの定義を追加し、クラスIDのリストの最後のインデックスを修正しています。
TComponentClass classes[4] = { __classid(TFontNameComboBox), __classid(TFontNameListBox), __classid(TCharsetComboBox), __classid(TCharsetListBox) }; RegisterComponents("Ysl", classes, 3);
コンポーネントのアイコンの作成.
コンポーネントパレットで各コンポーネントを識別するためのアイコンを、コンポーネント毎に指定することが出来ます。特に指定しなければデフォルトのアイコンがコンポーネントパレットに表示されますが、どのコンポーネントでも同じになってしまうので、できればアイコンを作った方がコンポーネントパレットで識別しやすくなります。
まずはアイコンの作成から。
アイコンはC++Builderに付属のイメージエディタを使用して作ります。アイコンは拡張子.dcrのDelphiコンポーネントリソースファイル内に格納するので、まずはDelphiコンポーネントリソースファイルを作ります。余談ですが、C++Builderのヘルプには.dcrのファイルが、Delphiコンポーネントリソースファイルとか、ダイナミックリソースファイルとか、コンポーネントリソースファイルとか、色々な名前で登場します。
ファイル名はコンポーネント本体のソースファイルの拡張子を.dcrに換えたものです。アイコンは大きさ24×24のビットマップで、コンポーネントのクラス名と同じ名称をつけます。イメージエディタは名称を大文字にしてしまいますが、アイコン名はケースインセンシティブなので、そのままでOkです。
複数のコンポーネントを含む場合、Delphiコンポーネントリソースファイル内に、コンポーネントの数だけクラス名のビットマップを作成します。FontNameBoxが参考になると思います。
あとはコンポーネントのインストール時にC++Builderが勝手にアイコンを探してコンポーネントパレットに表示してくれます。
サブコンポーネントの登録.
フォームファイル(*.dfm)に記録したいクラスで、フォームの宣言で参照されるフォームクラスやコンポーネントクラスでないものは、プログラマが明示的にクラスを登録する必要があります。わかりにくい説明ですが、コンポーネント内に含まれるためだけに定義されるクラスであるサブコンポーネントなどが該当します。PagePanelのTPageSheetがその例です。
このようなサブコンポーネントは登録することではじめて、そのインスタンスをフォームファイルに記録・読み出しできるようになります。
サブコンポーネントの登録はRegisterClass関数を使って、RegisterComponents関数と同じコンポーネントパレットへの登録のRegister関数の内部で行います。RegisterClass関数へ渡す引数は登録するコンポーネントのクラスIDのみです。
PagePanelのTPageSheetの例を示します。
RegisterClass(__classid(TPageSheet));
コンポーネントのインストール.
ここまでで、作成したコンポーネントをC++Builderにインストールするために必要なものはそろいます。次は実際にC+Builderにインストールし、フォームの設計時にコンポーネントパレットから選択してフォームに配置したり、オブジェクトインスペクタやオブジェクトツリーで編集できるようにします。
インストールするパッケージの選択.
インストールしたコンポーネントは、特殊なDLLであるパッケージに保存されます。まずは保存するパッケージを選択します。
「コンポーネント−コンポーネントのインストール」を選択すると、次のダイアログが表示されます。
すでにあるパッケージに追加するか、新しいパッケージを作ってインストールするかを、タブで選択します。ユニットファイル名はコンポーネント本体のソースファイルを指定します。検索パスはそのままで大丈夫です。パッケージファイル名は追加する既存のパッケージファイル、または新規作成するパッケージファイル名を指定します。新規パッケージを選択した場合は、パッケージの説明も指定することが出来ます。パッケージの説明は後からでも変更できます。
デフォルトで既存のパッケージdclusr.bpkへの追加が選択されていますが、このパッケージはC++Builderユーザがコンポーネントを追加するために用意されているようです。
パッケージを選択すると次のウィンドウが開きます。
パッケージを開く.
一度C++Builderを閉じた後は、「コンポーネント-パッケージのインストール」で表示される次のダイアログで、インストールしたパッケージを選択して編集ボタンを押せば、上記のパッケージウィンドウが開きます。
コンパイルとインストール.
上記パッケージウィンドウのContains以下に、次に述べるファイルを追加します。
- コンポーネント本体のソースファイル(*.cpp)。
- Delphiコンポーネントリソースファイル(*.dcr)。
Delphiコンポーネントリソースファイル(*.dcr)をドラッグ&ドロップでパッケージウィンドウに追加しようとすると、「すでに追加されています」と怒られて追加できない場合、いったんコンポーネント本体のソースファイル(*.cpp)を削除し、追加ボタンで再度コンポーネント本体のソースファイル(*.cpp)を追加してみてください。これでうまく追加できる場合があります。
追加したらパッケージウィンドウ上部のボタンバーにある「コンパイル」を押してコンパイルします。エラーがなければ「インストール」ボタンが有効になるので、インストールボタンを押します。「情報」ダイアログが出て「〜がインストールされました。」というメッセージが表示されれば、無事C++BuilderのIDEにコンポーネントがインストールされているはずです。RegisterComponents関数で指定したコンポーネントパレットを確認してみてください。
Copyright 2005-2016, yosshie.