通知先が2カ所以上あるような複数イベントの処理

解決


take  2005-03-09 00:37:38  No: 13645

見通しが良くなるようなイベント処理の方法をご教授願います。

あるデータを持ったグローバルクラスのClassAとそれを使用している
FormA、FormBがありClassAのフィールド変数に変化があったときに
OnChangeのような変更を知らせるイベントを持たせています。

そしてFormAが表示されているときは
このOnChangeイベントを受け取ってFormAを再描画しています。
そのイベント代入はフォーム生成時に

 ClassA.OnChange := FormA内で使用する受け取り先

としていて、破棄時に

 ClassA.OnChange := nil;

として受け取らないようにしています。

FormAの代わりにFormBが表示されているときも同様です。

しかし問題なのがFormA、FormBともに表示されるようなときです。
FormA、FormBのどちらにもイベントを送りたいのですが
OnChangeA、OnChangeBとしてそれぞれFormA、FormB用のイベントとする
方法しか思い浮かびません。

もっと良い方法がありましたらご教授願います。

ちなみにFormAはFormBを参照できませんし逆も同様です。
またClassAはFormA、FormBともに参照できません。


jok  2005-03-09 01:47:10  No: 13646

Delphi のイベントはメソッドポインタをひとつだけしか格納できない
プロパティーとして実装されています。ですから、普通の意味でのイベント
ハンドラは複数保持できません。そこで、発想を変えて、複数のメソッド
ポインタを動的に保持することを考えます。下の例では TNotifyEvent 型
のメソッドポインタの動的配列を TList クラスで保持します。メソッド
を直接追加・削除できますので、それがどのクラスに属しているかを明示的
に参照する必要はありません。参考にしてください。

type
PNotifyEvent = ^TNotifyEvent;

TClassA = class(TObject)
private
  EventList:TList;
  function GetEventCount:integer;
protected
public
  constructor Create;
  destructor Destroy;override;
  procedure AddNotifyEvent(ne:TNotifyEvent);
  procedure DeleteNotifyEvent(ne:TNotifyEvent);
  procedure Fire;
  property EventCount:integer read GetEventCount;
end;

  TForm1 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    Memo1: TMemo;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
  private
    { Private 宣言 }
  public
    ClassA:TClassA;
    procedure Method1(Sender: TObject);
    procedure Method2(Sender: TObject);
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

//----------- TClassA ---------------

constructor TClassA.Create;
begin
  EventList := TList.Create;
end;

destructor TClassA.Destroy;
var
  i:integer;
begin
  for i := 0 to EventList.Count-1 do
    Dispose(PNotifyEvent(EventList[i]));
  EventList.Free;
  inherited;
end;

function TClassA.GetEventCount:integer;
begin
  result := EventList.Count;
end;

procedure TClassA.AddNotifyEvent(ne:TNotifyEvent);
var
  p:PNotifyEvent;
  i:integer;
begin
  for i := 0 to EventList.Count-1 do
    if @TNotifyEvent(PNotifyEvent(EventList[i])^) = @ne then exit;
  New(p);
  p^ := ne;
  EventList.Add(p)
end;

procedure TClassA.DeleteNotifyEvent(ne:TNotifyEvent);
var
  i:integer;
begin
  for i := 0 to EventList.Count-1 do
    if @TNotifyEvent(EventList[i]^) = @ne then
    begin
      Dispose(PNotifyEvent(EventList[i]));
      EventList.Delete(i);
      break;
    end;
end;

procedure TClassA.Fire;
var
  i:integer;
begin
  for i := 0 to EventList.Count-1 do
    if Assigned(EventList[i]) then PNotifyEvent(EventList[i])^(self);
end;

//----------- TForm1 ---------------

procedure TForm1.FormCreate(Sender: TObject);
begin
  ClassA := TClassA.Create;
  ClassA.AddNotifyEvent(Method1);
  ClassA.AddNotifyEvent(Method2);
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  ClassA.Free;
end;

procedure TForm1.Method1(Sender: TObject);
begin
  Memo1.Lines.Add('Method1 done');
end;

procedure TForm1.Method2(Sender: TObject);
begin
  Memo1.Lines.Add('Method2 done');
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  ClassA.Fire;
end;

procedure TForm1.Button2Click(Sender: TObject);
begin
  ClassA.DeleteNotifyEvent(Method1);
  ClassA.Fire;
end;


にしの  2005-03-09 05:29:44  No: 13647

ClassAがVCLであれば、TCollectionクラスの派生クラスを定義して、FormA, FormBをプロパティに持つという手もあります。
TListViewのColumnや、TStatusBarのPanelsと同じです

メリットは、デザイン中に設定できること。
デメリットは、TCollection, TCollectionItemの定義が面倒なことです^^;
デメリットの方が大きいですが、こういう手法もあるということで。

# 別のUnitのFormをプロパティにもてるかどうかは試してません。もうしわけない。


jok  2005-03-09 08:25:04  No: 13648

> ClassAがVCLであれば、TCollectionクラスの派生クラスを定義して、FormA, FormBをプロパティに持つという手もあります。

うーむ、メソッドポインタのリストを保持することと、オブジェクトのリストを
保持することとは必ずしも一致しないけどね。必ず同じ名前のメソッドを呼び出す
事にするとか、そもそも TFormA や TFormB の宣言が TClassA から見えなければ
呼び出すことも出来ないと思いますけど。


にしの  2005-03-09 09:51:27  No: 13649

あ、勘違いしていたかもです。
てっきり、TFormAとTFormBには共通の親クラスがあって、そっちにOnChangeイベントがあるのかと。
ClassAはTFormAはわからないが、親クラスはわかるという前提。

TFormA、TFormBの派生元クラスが隠蔽されているのであれば、素直にNotifyEventだけリストしたほうが簡単ですね。

> うーむ、メソッドポインタのリストを保持することと、オブジェクトのリストを
> 保持することとは必ずしも一致しないけどね。

今回の場合、
・1クラスのプロパティ変更で、2つのフォームにOnChangeイベントを発生させる
ということだと理解しました。

メソッドポインタを持つ、というより、直感的に、2オブジェクトのイベントを呼ぶ、というほうがわかりやすいかと思います。

また、メソッドポインタのリストを保持した場合の注意点として、オブジェクトの開放時にメソッドポインタが無効となる点があると思います。
VCLオブジェクトとしてプロパティで連結する場合には、Notificationメソッドで開放通知できる点も、利点だと思います。


jok  2005-03-09 11:56:55  No: 13650

にしのさんとお話しするのはおもしろいですね。純粋にこのテーマ自体もおもしろいですし。
C# のデリゲートでは、マルチキャストできるんですけど、Delphi のイベントはできないん
ですよね。それでいろいろ考えたことがありました。

最初にわたしも単にオブジェクトのリストを保持するプロパティーでいいんじゃないか、と
考えたんです。でも、元質問の中の

> またClassAはFormA、FormBともに参照できません。

ことから、リスト中のオブジェクトのクラスを予め想定するような実装はまずいように思い
ました。共通の上位クラスを使うにしても、メソットを呼び出すときには具体的な下位クラス
にキャストしなければなりませんし。したがって、オブジェクトのリストを保持するようでは
予め呼び出すメソッドが全て固定、そして予めオブジェクトがどんなクラスのインスタンスで
あるか分かっている必要があります。TCollection を使う場合でもそうです。それに対して
メソッドそのもののリストでは、暗黙のオブジェクト参照が渡されるため、明示的にその
メソッドが属するオブジェクトを参照する必要がありません。ハンドラの型だけが一致していれ
ばOKです。ところが SizeOf(TNotifyEvent) としてみれば分かりますように、8バイトなので
単純なポインタのリスト TList ではダメです。上では、PNotifyEvent を定義して、New() と
Dispose() で追加削除していますので、ちょっと変わった実装になっています。

> メソッドポインタのリストを保持した場合の注意点として、オブジェクトの開放時にメソッド
> ポインタが無効となる点があると思います。

そうですね。Add() したら、解放時に Delete() する必要があるのは、Delphi の常道だと
思いますけど。二重に登録したり、登録してないのに Delete() しても無害になるように
実装してあります。例として出した TClassA は、本当は純粋なリストオブジェクトとすべき
で、新たにつくられるクラスに has されるようにするのが理想ですけど、そこまではしてい
ません。


take  2005-03-09 18:10:04  No: 13651

jokさん、にしのさん、ご教授ありがとうございます。

色々なプログラムでデータの構造化を考えたとき、どうしても複数通知処理で
悩んでしまうのです。

これが同じユニット内で上下関係があるクラスの場合は上位に渡したり、
さらに下位に投げたりとできるのですが、上下関係が無いクラスの場合に
どのようにすればよいのかと困っているところです。

参照に関してですが、ここでいうClassAはグローバルデータですので
interface部にて

var
  GDClassA : TClassA;
  
と定義してあり、またusesは自身が使用するクラス以外は記述しない
ことで独立性を高めています。

またFormA,FormBはグローバルデータを参照するに過ぎないので

implementation部にusesとして参照宣言しているといったイメージです。

jokさんのサンプルであるTListで管理するという発想は思いつきませんでした
参考になります。
これをあと少し拡張したいので、引き続きご教授願えますか?

というのは、一番最初のサンプルでClassAに対してFormA,FormBさらに
OnChangeイベントと言いましたが、実際にはClassAのように複数通知先を持ちたい
クラスClassBも存在します。

それだけなら今TPersistentからClassAを作っているのでTPersistentを継承し
TPersistentExなどとしてこのクラスに
procedure AddNotifyEvent(ne:TNotifyEvent);
を実装しておけば、ClassA、ClassBともにTPersistentExから継承することで
各クラスにソースを書く必要が無くなりますよね?

しかし今度はOnChangeイベントの他にたとえば初期化を通知するイベント
OnRefreshを作ろうとしたときややっこしくなりそうです。

そのときの方法として
「複数のイベント通知を管理するTListをさらに用意しイベントを2次元管理する。」
という方法が楽かと思ったのですが
「イベントを通知するという処理を1つのクラスにする。」
とした方が後々楽になりそうな気もするのですがどうでしょうか?


sakiyama  2005-03-09 20:18:36  No: 13652

以前にhttp://www.borland.co.jp/cppbuilder/papers/vctocb/chap45.html
をdelphiに移植したTObserver/TObservableコンポーネントを作ったんですが使えませんか
ちょっと長いですが以下Observer.pas
--------------
unit Observer;

interface

uses
  SysUtils, Classes, Contnrs;

type
  TUpdateEvent = procedure(Sender, Arg: TObject) of object;
  TObserver = class;

  TObservable = class(TComponent)
  private
    FObservers: TComponentList;
    FChanged: Boolean;
    function GetObservers(Index: Integer): TObserver;
  protected
    procedure AddObserver(Value: TObserver);
    procedure RemoveObserver(Value: TObserver);
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    procedure Notify(Arg : TObject);
    procedure Changed(State: Boolean = True);
    function HasChanged: Boolean;
    property Observers[Index: Integer]: TObserver read GetObservers;
  end;

  TObserver = class(TComponent)
  private
    { Private 宣言 }
    FObservable: TObservable;
    FOnUpdate: TUpdateEvent;
    procedure SetObservable(const Value: TObservable);
  protected
    { Protected 宣言 }
    procedure DoUpdate(Arg : TObject);
  public
    { Public 宣言 }
  published
    { Published 宣言 }
    property Observable: TObservable read FObservable write SetObservable;
    property OnUpdate:TUpdateEvent read FOnUpdate write FOnUpdate;
  end;

  procedure Register;

implementation

(** コンポーネント登録手続き **)
procedure Register;
begin
  RegisterComponents('Samples', [TObserver, TObservable]);
end;

{ TObservable }
procedure TObservable.AddObserver(Value: TObserver);
begin
  if FObservers.IndexOf(Value) < 0 then
    FObservers.Add(Value);
end;

procedure TObservable.Changed(State: Boolean = True);
begin
  FChanged := State;
end;

constructor TObservable.Create(AOwner: TComponent);
begin
  inherited;
  FObservers := TComponentList.Create(False);
  FChanged := False;
end;

destructor TObservable.Destroy;
begin
  FObservers.Free;
  inherited;
end;

function TObservable.GetObservers(Index: Integer): TObserver;
begin
  Result := FObservers[Index] as TObserver;
end;

function TObservable.HasChanged: Boolean;
begin
  Result := FChanged;
end;

procedure TObservable.Notify(Arg : TObject);
var i: Integer;
begin
  if FChanged then begin
    for i := 0 to FObservers.Count -1 do
      (FObservers[i] as TObserver).DoUpdate(Arg);
    FChanged := False;
  end;
end;

procedure TObservable.RemoveObserver(Value: TObserver);
begin
  FObservers.Extract(Value);
end;

{ TObserver }
procedure TObserver.DoUpdate(Arg : TObject);
begin
  if Assigned(FOnUpdate) then
    FOnUpdate(Self, Arg);
end;

procedure TObserver.SetObservable(const Value: TObservable);
begin
  if Assigned(FObservable) then
    FObservable.RemoveObserver(Self);

  FObservable := Value;
  if Assigned(FObservable) then begin
    FObservable.AddObserver(Self);
    FObservable.FreeNotification(Self);
  end;
end;

end.
----ここまで

ClassAのメンバにTObservableをもって、Form1,2にTObserverを置きます
Form1.ObserverのTObservableにClassAのObservableを登録して
Updateイベントに通知されたら行う処理を書きます
ClassAが変更されたら
  Observable1.Changed;
  Observable1.Notify(Self);
と書くと登録されているObserverに変更通知が行きます

このコンポーネントの利点はFreeNotificationを使ってるのでObserverの生死が自動的に管理されます。
なのでForm1の破棄時にObservableからObserverを削除する必要がありません


jok  2005-03-09 21:52:58  No: 13653

> sakiyama さん

すばらしい! まさしくこれですね。デザパタの典型例でしたか?

なるほど、登録する方にも共通のクラスのインスタンスを置けば、にしのさんのようにオブジェクトの
リストで管理できますね。わたしは実装の方に気を取られて大局的な視点が欠けていたようです。
勉強になりました。

> このコンポーネントの利点はFreeNotificationを使ってるのでObserverの生死が自動的に管理されます。

これなんですけど、FObservable.FreeNotification(Self); で Observer を Observable に開放通知する
のは分かるんですけど、肝心の TObservable のほうで通知を受けて FObservers のリストを更新する
コードが見当たりません。Notification メソッドを override する必要があるのではないでしょうか?


sakiyama  2005-03-09 22:09:26  No: 13654

> これなんですけど、FObservable.FreeNotification(Self); で Observer を > Observable に開放通知する
> のは分かるんですけど、肝心の TObservable のほうで通知を受けて FObservers のリストを更新する
> コードが見当たりません。Notification メソッドを override する必要があるのではないでしょうか?

その辺はVCL任せになってますね
FObserversはTComponentListですので、登録されているコンポーネントのFreeNotificationを捕まえます


sakiyama  2005-03-09 22:16:52  No: 13655

すいません。
FreeNotification要りませんでしたね。
以前はTComponentList使ってませんでしたのでその名残でした


jok  2005-03-09 22:30:52  No: 13656

> FreeNotification要りませんでしたね。

なるほど、それなら分かりました。ありがとうございました。


sakiyama  2005-03-09 22:37:18  No: 13657

ごめんなさい。適当なことを言ってしまいました。
jokさんのおっしゃる通りNotificationが必要ですね。
ObservableとObserverの解放がごっちゃになっていました。
procedure TObserver.Notification(AComponent: TComponent;
  Operation: TOperation);
begin
  inherited;
  if (Operation = opRemove) and (AComponent = FObservable) then
    SetObservable(nil);
end;
が無いとObserverがいつまでも死んだObservable抱えちゃいますね。
FreeNotificationも必要です。
BorlandのModel/Observerにはちゃんとあるんですが、勘違いして消したようです。

いつもObservable/Observerのどっちがどっちか分からなくなります。


jok  2005-03-09 22:45:51  No: 13658

> いつもObservable/Observerのどっちがどっちか分からなくなります。

いや、わたしも2重に勘違いしていたことに気がつきました。ありがとうございます。


take  2005-03-10 01:17:45  No: 13659

sakiyamaさん、ご教授ありがとうございます。

なるほどイベント発生先と受け取り先をそれぞれクラス化し
さらにTComponentListによる管理とは目から鱗です。

ソースを参考にしてさっそく実装してみたいと思います。
ありがとうございました。


ポン  2005-04-01 01:54:08  No: 13660

はじめまして、ポンと申します。
take様とsakiyama様のやり取りからいろいろ勉強になりましたが、
勉強不足でなんか理解ずらいです。
take様からあのFormAとFormBのソースをもらっていいですか?


take  2005-04-03 22:40:36  No: 13661

>ソースを参考にしてさっそく実装してみたいと思います。

「Observers」という単語がちょっと難しかったのでイベント発生側をSend
受け取り側をReceiveとした他、Change関係のフラグを使用しないようにしました。
他はほとんどsakiyama様のソースそのままですが・・・
ちなみに実際に本番でも使用していますが特に問題なく動いています。

複数通知先イベント発生/受け取りユニット MultiEvent.pasと
サンプルフォーム Unit1.pasを載せておきます。

TForm1.OnUpdate1やTForm1.OnUpdate2の中に処理やブレイクポイントを付けて
お試しください。

unit MultiEvent;

interface

uses
  SysUtils, Classes, Contnrs;

type
  TMultiEventUpdateEvent = procedure(Sender, Arg: TObject) of object;
  TMultiEventReceive = class;

  TMultiEventSend = class(TComponent)
  private
    FObservers: TComponentList;
    function GetObservers(Index: Integer): TMultiEventReceive;
  protected
    property Observers[Index: Integer]: TMultiEventReceive read GetObservers;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;

    procedure Add(Value: TMultiEventReceive);
    procedure Delete(Value: TMultiEventReceive);

    procedure Notify(Arg : TObject);
  end;

  TMultiEventReceive = class(TComponent)
  private
    { Private 宣言 }
    FObservable: TMultiEventSend;
    FOnUpdate: TMultiEventUpdateEvent;
    procedure SetObservable(const Value: TMultiEventSend);
  protected
    { Protected 宣言 }
    procedure DoUpdate(Arg : TObject);
  public
    { Public 宣言 }
    property Observable: TMultiEventSend read FObservable write SetObservable;
  published
    { Published 宣言 }
    procedure Notification(AComponent: TComponent; Operation: TOperation); override;
    property OnUpdate:TMultiEventUpdateEvent read FOnUpdate write FOnUpdate;
  end;

implementation

{ TMultiEventSend }

procedure TMultiEventSend.Add(Value: TMultiEventReceive);
begin
  if FObservers.IndexOf(Value) < 0 then
    FObservers.Add(Value);
end;

constructor TMultiEventSend.Create(AOwner: TComponent);
begin
  inherited;
  FObservers := TComponentList.Create(False);
end;

destructor TMultiEventSend.Destroy;
begin
  FObservers.Free;
  inherited;
end;

function TMultiEventSend.GetObservers(Index: Integer): TMultiEventReceive;
begin
  Result := FObservers[Index] as TMultiEventReceive;
end;

procedure TMultiEventSend.Notify(Arg : TObject);
var i: Integer;
begin
  for i := 0 to FObservers.Count -1 do begin
    (FObservers[i] as TMultiEventReceive).DoUpdate(Arg);
  end;
end;

procedure TMultiEventSend.Delete(Value: TMultiEventReceive);
begin
  FObservers.Extract(Value);
end;

{ TMultiEventReceive }

procedure TMultiEventReceive.DoUpdate(Arg : TObject);
begin
  if Assigned(FOnUpdate) then
    FOnUpdate(Self, Arg);
end;

procedure TMultiEventReceive.Notification(AComponent: TComponent;
  Operation: TOperation);
begin
  inherited;
  if (Operation = opRemove) and (AComponent = FObservable) then
    SetObservable(nil);
end;

procedure TMultiEventReceive.SetObservable(const Value: TMultiEventSend);
begin
  if Assigned(FObservable) then begin
    FObservable.Delete(Self);
  end;
  FObservable := Value;
  if Assigned(FObservable) then begin
    FObservable.Add(Self);
    FObservable.FreeNotification(Self);
  end;
end;

end.

------------------------------------------------------------------------------------

unit Unit1;

interface

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

type
  TTestData = class(TPersistent)
  private
    { Private 宣言 }
    FEvents: TMultiEventSend;
    FValue: Integer;
    procedure SetValue(const Value: Integer);
  public
    { Public 宣言 }
    constructor Create;
    destructor Destroy; override;

    property Events : TMultiEventSend read FEvents write FEvents;
    property Value : Integer read FValue write SetValue;
  end;

type
  TForm1 = class(TForm)
    Button1: TButton;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure Button1Click(Sender: TObject);
  private
    { Private 宣言 }
    FTest : TTestData;
    FEvents1: TMultiEventReceive;
    FEvents2: TMultiEventReceive;
    procedure OnUpdate1(Sender, Arg: TObject);
    procedure OnUpdate2(Sender, Arg: TObject);
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

{$R *.DFM}

{ TTestData }

constructor TTestData.Create;
begin
  FEvents := TMultiEventSend.Create(nil);
end;

destructor TTestData.Destroy;
begin
  FEvents.Free;
  inherited;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  FTest := TTestData.Create;
  FEvents1 := TMultiEventReceive.Create(Self);
  FEvents1.OnUpdate := OnUpdate1;
  FEvents2 := TMultiEventReceive.Create(Self);
  FEvents2.OnUpdate := OnUpdate2;
  FTest.Events.Add(FEvents1);
  FTest.Events.Add(FEvents2);
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  FTest.Events.Delete(FEvents2);
  FTest.Events.Delete(FEvents1);
  FEvents2.Free;
  FEvents1.Free;
  FTest.Free;
end;

procedure TForm1.OnUpdate1(Sender, Arg: TObject);
var
  s : string;
begin
  s := '';
end;

procedure TForm1.OnUpdate2(Sender, Arg: TObject);
var
  s : string;
begin
  s := '';
end;

procedure TTestData.SetValue(const Value: Integer);
begin
  FValue := Value;
  FEvents.Notify(Self);
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  FTest.Value := 0;
end;

end.


※返信する前に利用規約をご確認ください。

※Google reCAPTCHA認証からCloudflare Turnstile認証へ変更しました。






  このエントリーをはてなブックマークに追加