Delphi Programming / Object Pascal
サイトのトップページへ リンクのページへ ロゴマーク
[掲載 2005年04月10日] [更新 2017年01月09日] Delphi サンプルプログラム集
160_コンポーネントの実行時生成
動作確認等 Windows 7 U64(SP1) + Delphi XE(UP1) Pro
160_CreateComponent.zip [1,609 KB] 2017年01月09日版 (EXE 同梱)



  • 2017年01月09日 (これより前の履歴は省略)
  • コンポーネントをリストとして扱うサンプルを、別ページを新規作成して移動




コンポーネントの実行時作成と継承クラスの利用


コンポーネントを実行時に生成、作成する際の関係サンプルと記事です。

当サイトのサンプルプログラム集の中にも、実行時にコンポーネントを生成する例が多くあります。それらを参考にすれば済むことですが、入門者に近い方の参考となるように、改めて別のページの記事としました。
サンプルでは TButton を扱っていますが、それ以外のコンポーネントでも同様の方法で扱えます。
実行時にコンポーネントを生成することを「動的に生成する」とも言います。





01_ボタンが1つだけの場合と実行時生成の基本


設計時にフォームにボタンを配置すると、Button1 という名前のコンポーネントができます。これは、TButton というクラスの、Button1 という実体 (インスタンス) が作成されたことになります。
実行時にコンポーネント、つまりクラス型のオブジェクトを利用するには、この Button1 に相当するインスタンスの作成をコードで行います。基本的には、TBitmap, TStringList 等のクラス型のオブジェクトの使用方法と同じです。

実行時にコンポーネントを作成して使用するには、
  • (1) そのコンポーネントのクラス型の変数を定義する
  • (2) そのコンポーネントのインスタンスを、Create メソッドで作成する
  • (3) ビジュアルなコンポーネントの場合は、Parent プロパティを設定する
  • (4) 他の必要なプロパティを設定する
  • (5) 必要であれば、イベントのコードも作成して、イベントの設定をする
  • (6) 使い終わったら、メモリの解放やインスタンスを破棄する

という手順になります。
Parent プロパティは、コンポーネントをどのコントロール上に表示するかの設定です。コンポーネントを載せるコントロールを「親コントロール」と言います。つまり、Parent です。この設定をしないと、どのコントロールに表示するか不定となり、表示されません。非ビジュアルコンポーネントでは、Parent の設定は必要ありません。

親コントロールになれるコントロール (コンポーネント) のことをコンテナコントロールといいます。Delphi では、コンテナになれるコントロールと、コンテナななれないコントロールがあります。

次のサンプルは、実行時にボタンを 1 つだけ作成する例です。



[備考]
FireMonkey ではどんなコントロールも親コントロールになれます。



図1
実行開始
図2
[作成] ボタンをクリックすると、ボタンができる
図3
出来たボタンをクリックすると、キャプションが変わる

リスト1
ボタンを 1 つだけ実行時に作成する例
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  private
    { Private 宣言 }
    FButton : TButton;
    procedure ButtonClickEvent(Sender : TObject);
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

{$R *.DFM}

//=============================================================================
//  新規にボタンを作成
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
begin
  //FButtonというボタンのオブジェクトがnilなら作成
  if FButton = nil then begin
    //FButtonというボタンを作成
    FButton := TButton.Create(Self);

    //Parent(配置場所)をSelf,つまりこのForm1とする
    //これがないと配置場所不明で表示されない
    FButton.Parent := Self;

    //配置位置とサイズ
    FButton.Left   := 50;
    FButton.Top    := 50;
    FButton.Width  := 150;
    FButton.Height := 60;

    //キャプション
    FButton.Caption := '新規ボタン';

    //OnClickイベントをButtonClickEventというメソッドに割当てる
    FButton.OnClick := ButtonClickEvent;
  end;
end;

//-----------------------------------------------------------------------------
//  新規に作成したボタンのOnClickイベント
//-----------------------------------------------------------------------------
procedure TForm1.ButtonClickEvent(Sender: TObject);
begin
  //FButton.Caption := 'クリック'; でも同じ
  TButton(Sender).Caption := 'クリック';
end;

end.


コンポーネントをフォームに配置して使用する場合は、オブジェクトインスペクタでイベントを作成することができます。実行時にコンポーネントを作成する場合は、上のコードのように、イベント用のメソッドを作成して、それを割り当てます。

イベント用のメソッドの引数の順番と型は、元のイベントと同じにします。引数の名前は必ずしも同じである必要はありません。オブジェクトインスペクタで、同じコンポーネントの同じイベントを作成して、それを修正して利用すると間違い防止になります。





02_生成したコンポーネントやクラスの解放と破棄 -  Free と nil


一般に、生成したコンポーネント、オブジェクトは、使い終わったら Free メソッド等で解放します。そうしないと、メモリリークが発生する場合があります。
また、nil を代入、あるいは、FreeAndNil 関数を実行しておけば、そのインスタンスがなくなり (オブジェクトの実体がなくなり)、誤動作を避けるのに有効です。
Free メソッドでは、インスタンスがなくなるわけではありません。メモリの解放です。その領域を自由に使用可能にしたという意味です。つまり、文字通り「解放」です。

次のコードは、このことを確認するためのものです。実行すると、100 という値が表示されます。
オブジェクトが使用していたメモリ領域を解放しても、そのオブジェクトが使用していたデータ (バイトデータ) はメモリ上に残されています。そのため、そのデータが取得できるわけです。


図4
Free してもインスタンスは参照可能

リスト2
Free でメモリを解放してもそのオブジェクトのインスタンス (実体) はなくならない
procedure TForm1.Button1Click(Sender: TObject);
var
  LButton : TButton;
begin
  LButton := TButton.Create(Self);
  try
    LButton.Parent := Self;
    LButton.Width  := 100;
    LButton.Height := 25;
  finally
    LButton.Free;
  end;
  ShowMessage(IntToStr(LButton.Width));
end;


一方、次のコードは、実行すると以下のようなメモリアクセス違反の例外が発生します。
このメモリアクセス違反の例外エラーは、実体 (インスタンス) のないオブジェクトを参照したり、使おうとすると発生する、よく見かけるエラーです。

モジュール 'Project1.exe' のアドレス 004BD4D6 でアドレス 00000048 に対する読み取り違反がおきました。


図5
読み取り違反

リスト3
インスタンス (実体) がないオブジェクトにはアクセスできない
procedure TForm1.Button1Click(Sender: TObject);
var
  LButton : TButton;
begin
  LButton := TButton.Create(Self);
  try
    LButton.Parent := Self;
    LButton.Width  := 100;
    LButton.Height := 25;
  finally
    FreeAndNil(LButton);
  end;
  ShowMessage(IntToStr(LButton.Width));
end;


グローバル変数として定義したオブジェクトは nil に初期化されます。一方、ローカル変数として定義したオブジェクトは nil には初期化されず、不定値となります。
ローカルなオブジェクトの場合、インスタンス生成前に nil を代入しておくことがあります。そうすることにより、インスタンスの生成有無を Assigned 関数で、あるいは nil 値かどうかで判定することができます。

ローカルな変数とは、メソッドやイベント内で定義した変数のことです。それ以外は全てグローバルな変数ということになります。グローバルな変数は、ユニット内、あるいは別のユニット内に定義されている変数類もあります。

ローカルに生成したクラスは、その関数やイベント等のプロシージャ内、つまりローカルな処理が終了するとそのインスタンスはなくなります。ただし、そのオブジェクトが生成したメモリが解放されるわけではありません。
メモリを解放しないとメモリリークが発生します。下に示すコードのように nil の代入ではメモメモリは解放されません。



図6
メモリを解放しないとメモリリークが発生する
  • メモリマネージャ FastMM4 による検出結果

リスト4
ローカルなオブジェクトの Free と nil
//=============================================================================
//  ローカルなオブジェクトの異性はFreeする
//  ローカルな処理が終了するとそのインスタンスはなくなる
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LStringList : TStringList;
begin
  LStringList := TStringList.Create;
  try
    //処理コード
  finally
    //またはFreeAndNil(LStringList);
    LStringList.Free;
  end;
end;

//=============================================================================
//  インスタンスをnilにしてもオブジェクスのメモリが残る例
//  このコードはメモリリークが発生する
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
var
  LStringList : TStringList;
begin
  LStringList := TStringList.Create;
  try
    //処理コード
  finally
    LStringList := nil;
  end;
end;


オブジェクトを Create メソッドで生成する際、引数にオーナー (オブジェクトの所有者) を指定できる場合は、明示的に指定しておけば、その所有者が破棄される時に、自動的に解放、破棄されます。
多くの場合、コンポーネントを配置するフォーム自身を意味する Self が使われているようです。
Create メソッドでオーナーを指定した場合であっても、Free や FreeAndNil 関数を使用して解放、破棄しても構いません。

FreeAndNil は、内部で次のコードを実行しています (Delphi XE の場合)。



リスト5
FreeAndNil 関数の処理内容
procedure FreeAndNil(var Obj);
var
  Temp: TObject;
begin
  Temp := TObject(Obj);
  Pointer(Obj) := nil;
  Temp.Free;
end;





03_名前なしでコンポーネントを生成


最初のサンプルでは、ボタンに FButton という名前を付けています。もし、作成したボタンの名前を使用する必要がなければ、生成部分のコードは以下のようにも書けます。
この場合、FButton 変数の定義は必要ありません。

ただし、このコードでは Button1 をクリックする度に新しいボタンが生成されます。新しいボタンを生成しないようにするためには、生成したイベントやメソッド内での解放、破棄の処理が必要です。解放、破棄の処理をすると、生成したボタンを他から利用することができません。

このような、名前なしでコンポーネントを生成するコードは、主に、生成したコンポーネントを、生成したイベントやメソッド内だけで一時的に使用する場合に使われています。
例えばファイルを開く TOpenDialog の生成、あるいはダイアログの生成等が考えられます。TButton を単独で生成して使うような場面はほとんど考えられません。



リスト6
ボタンを 1 つだけ実行時に作成する例。コンポーネントの名前を使用する必要がない場合
procedure TForm1.Button1Click(Sender: TObject);
begin
   //ボタンを作成
   with TButton.Create(Self) do begin
     Parent := Self;

     Left   := 50;
     Top    := 50;
     Width  := 150;
     Height := 60;

     Caption := '新規ボタン';

     OnClick := ButtonClickEvent;
   end;
end;


[備考]
上のようにコンポーネントを生成する場合は、Create メソッドの引数を nil にしないようにします。Create メソッドでオーナを指定しておけば、そのオーナの解放処理の時に、生成したコンポーネントも自動的に解放処理されます。たとえ名前なしでコンポーネントを生成しても、オーナを持っていれば、Delphi は、生成したコンポーネントを内部で管理します。





04_派生 ( 継承 ) クラスのコンポーネントの生成


生成するボタンのクラスを定義して実装する方法もあります。この方法は Delphi のクラスという性質をうまく利用した方法で柔軟性があります。
次のサンプルでは、生成した FButton のイベント用のメソッドを割り当てていませんが、ボタンをクリックすると、ダイアログが表示されます。

コードの最初の方、interface 部で、TplAddButton という名前のクラスを定義しています。このクラスは、TButton から作成しています。この時、TplAddButton は、TButton の派生クラス、あるいは、TButton を継承したクラスと言います。継承クラスは、元のクラスの機能を全て持っています。したがって、継承元のクラスの protected 部のメンバが使用可能となります。
また、実行時に生成するコンポーネントにいろいろな機能を追加することもできます。

この TplAddButton に次のような Click というメソッドを実装しています。

    procedure Click; override;

TplAddButton のインスタンスである FButton をクリックすると、このメソッドが自動的に呼び出されて実行される仕組みになっています。


リスト7
継承クラスを使用して、コンポーネントを実行時に生成
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls;

type
  TplAddButton = class(TButton)
  protected
    procedure Click; override;
  end;

  TForm1 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  private
    { Private 宣言 }
    //TplAddButtonから派生したボタン
    FButton : TplAddButton;
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

{$R *.DFM}

//=============================================================================
//  新規にTplAddButtonクラスのボタンを作成
//
//  FButtonはTplAddButtonコンポネントのインスタンス(実体)
//  TplAddButtonはTButtonの継承コンポーネント(派生クラス)
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
begin
  if FButton = nil then begin
    FButton := TplAddButton.Create(Self);

    FButton.Parent  := Self;
    FButton.Top     := 50;
    FButton.Left    := 50;
    FButton.Width   := 150;
    FButton.Height  := 60;

    FButton.Caption := '追加ボタン';
  end;
end;

{ TplAddButton }

//-----------------------------------------------------------------------------
//  TplAddButtonクラスのClickメソッド
//-----------------------------------------------------------------------------
procedure TplAddButton.Click;
begin
  //継承元のOnClickメソッドを実行
  inherited;

  MessageBox(Handle, '追加ボタンがクリックされました', '情報', MB_ICONINFORMATION);
end;

end.


[備考 1]
上のコードの Click メソッドは、[メッセージ処理メソッドの作成] の説明のように、Clぐらいまでをタイプし、リストから Click を選択し、[Ctrl] + [Shift] + [C} で実装できます。
コーディングの間違いを防止するためにも、この方法をお勧めします。


[備考 2]
オブジェクトのインスタンスを「Button1 というオブジェクト」と言うように、単に「オブジェクト」と言う場合もあります。また、「Button1 というコンポーネント」と言う場合もあります。
それが、実際にインスタンスが生成されているオブジェクトを意味するかどうかは、文章の流れや、実際のコードで判断することになります。