†Sibylのお部屋†

メニュー項目の動的生成

作成開始日 2022.03.02
最終更新日 2022.03.03

メニュー項目はフォームデザイン時に決まっている場合が多いが、実行時に追加・変更されることもある。例えば「最近開いたファイル」のように、使用中に動的に変化する項目がこれに当る。また、動的に生成される項目は、配列として処理することが望ましい。「最近開いたファイル」も頭に「1,2,3…」とナンバリングされている事が多い。これは、ショートカットキーの役割もするが、各項目の移動や交換の際に、項目がナンバリングされていると便利だからでもある。

メニュー項目の動的生成

そこで、ここでは、10個の配列型のメニュー項目を想定し、それらのメニュー項目を動的に作成して、メニューに追加する方法について説明する。まず、メニュー項目の配列の生成の基本は次のようになる。
Var
  miMyItem : Array[1..10] of TMenuItem;

Begin
  //オブジェクトの生成
  For n:=1 to 10 Do
  Begin
    miMyItem[n]:=TMenuItem.Create(Self);
  End;

  //項目名の設定;設定文字列は任意
  miMyItem[1].caption:='e:\data\abc.txt';
  miMyItem[2].caption:='d:\bin\myrexx.cmd';
  miMyItem[3].caption:='f:\img\myimg.jpg';
  …
キモは、メニュー項目はオブジェクト(つまりポインタ)なので、変数型を宣言しただけでは使用できず、Createでインスタンスを生成する必要がある点。オブジェクトの作成を一般化すると、次のようになる;
Var
  Object: TClass;

Begin
  Object:=TClass.Create(Self);
次に、生成したメニュー項目の名称を設定している。設定内容は処理によって様々だが、ここでは例としてファイルのフルパス名を指定しておいた。尤も、メニュー項目を動的に生成する場合は、設定ファイルやキー入力で内容を指定する可能性が高く、ここで示しているように、直接ソースに設定を書く事はないと思うが(フォームデザイン段階で確定出来るので、動的にする生成する意味がない)。

メニューへの登録

ともあれ、以上でメニュー項目のインスタンスは生成されたのだが、このままでは、どこにも属していない=つまり表示できない。これを、たとえば「PopupMenu1」(フォームデザイン時に追加)というポップアップメニューに登録するには;
  For n:=1 to 10 Do
  Begin
    PopupMenu1.items.Add(miMyItem[n]);	//itemsがキモ
  End;
これで、PopupMenu1.Popupを実行すれば、生成された項目が表示されるようになる。キモはTPopupMenu型の「items」プロパティで、これがメニューツリーのルートに相当する。つまり、この作業は、メニューのルートに項目を追加していることになる。もし、サブメニューに追加したければ、親となるメニュー項目にAddする。例えば、MainMenu1にMenuItem3という項目が存在し、MenuItem3のサブメニューに生成した項目を追加する場合には;
  For n:=1 to 10 Do
  Begin
    MainMenu1.MenuItem3.Add(miMyItem[n]);
  End;

イベントハンドラの作成

さて、こうしてメニュー項目を生成し、登録はできても、選択(クリック)した時の処理が指定できないのでは意味がない。つまり、各項目に「OnClick」イベントハンドラを作成する必要があるわけだ。デザイン段階で作成される項目は、オブジェクト・インスペクタで「OnClick」イベントハンドラを作成することになるが、ここで扱っている動的生成では、この方法は使えない。

加えて、動的生成は数も内容も事前に確定していないのが一般的なので、事前に各項目毎にイベントハンドラを定義しておくことは難しい。そうではなく、動的生成した同一グループの項目に関しては、単一のイベントハンドラを起動して、ハンドラ内で分岐処理をする方が賢明。たとえば、「MyMenuOnClick」というプロシージャを定義しておいて;

  Private
    Procedure MyMenuOnClick(Sender: TObject);

  ……

Begin
  //OnClickイベントハンドラの登録
  For n:=1 to 10 Do
  Begin
    miMyItem[n].OnClick:=MyMenuOnClick;
  End;
のような要領で10個の項目全てに同一のOnClickイベントハンドラを指定しておく。イベントハンドラが単純なプロパティのような形式で設定できるのがミソ。

さて、問題はMyMenuOnClickの中身;処理内容はともかく、どうやってメニュー項目を識別して処理を分岐させるか?基本的には、送り元の項目である「Sender」をチェックすれば判別可能。

Procedure TForm1.MyOnClick(Sender: TObject);
Begin
  If Sender=miMyItem[1] then ....
  If Sender=miMyItem[2] then ....
  If Sender=miMyItem[3] then ....
まあ、この形式が基本なんだが、これは余りにカッコ悪い。それに、メニュー項目で処理に使用するプロパティはたいてい「Caption」だろう。前述の「最近開いたファイル」も、項目名にファイルのパスが含まれているので、「Caption」情報だけ取得できれば、後の情報はほぼ不要である。ならば、Senderから直接Captionを取得するとか、SenderをTMenuItem型オブジェクトとして扱うとかはできないだろうか?
Procedure TForm1.MyOnClick(Sender: TObject);
Var
  mi: TMenuItem;
  st: string;

Begin
  st:=Sender.caption;	//これは無理…
  mi:=Sender;		//これも無理…
これができれば便利なのだが、残念ながら、このままではこれは不可能。そりゃそうだ、TObjectがTMenuItemのプロパティを全て持っているわけではない。しかし、クラス型のキャストを行えば、代入が可能になる。
Procedure TForm1.MyOnClick(Sender: TObject);
Var
  mi: TMenuItem;
  st: string;

Begin
  mi:=Sender as TMenuItem;	//クラス型キャスト
  st:=mi.caption;		//呼び出し元の項目の項目名を取得
これによって、何番目の項目かを考える事なく、選択した項目の項目名が取得できる。また、これはオブジェクトはポインタなので、参照だけではなく代入も反映される。つまり;
  mi.caption:='abc...';
のような処理を行えば、呼び出し元のmiMyItem[n]のCaptionを変更することができる。


【Sibylのお部屋目次】 【ホーム】