見通しが良くなるようなイベント処理の方法をご教授願います。
あるデータを持ったグローバルクラスの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ともに参照できません。
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;
ClassAがVCLであれば、TCollectionクラスの派生クラスを定義して、FormA, FormBをプロパティに持つという手もあります。
TListViewのColumnや、TStatusBarのPanelsと同じです
メリットは、デザイン中に設定できること。
デメリットは、TCollection, TCollectionItemの定義が面倒なことです^^;
デメリットの方が大きいですが、こういう手法もあるということで。
# 別のUnitのFormをプロパティにもてるかどうかは試してません。もうしわけない。
> ClassAがVCLであれば、TCollectionクラスの派生クラスを定義して、FormA, FormBをプロパティに持つという手もあります。
うーむ、メソッドポインタのリストを保持することと、オブジェクトのリストを
保持することとは必ずしも一致しないけどね。必ず同じ名前のメソッドを呼び出す
事にするとか、そもそも TFormA や TFormB の宣言が TClassA から見えなければ
呼び出すことも出来ないと思いますけど。
あ、勘違いしていたかもです。
てっきり、TFormAとTFormBには共通の親クラスがあって、そっちにOnChangeイベントがあるのかと。
ClassAはTFormAはわからないが、親クラスはわかるという前提。
TFormA、TFormBの派生元クラスが隠蔽されているのであれば、素直にNotifyEventだけリストしたほうが簡単ですね。
> うーむ、メソッドポインタのリストを保持することと、オブジェクトのリストを
> 保持することとは必ずしも一致しないけどね。
今回の場合、
・1クラスのプロパティ変更で、2つのフォームにOnChangeイベントを発生させる
ということだと理解しました。
メソッドポインタを持つ、というより、直感的に、2オブジェクトのイベントを呼ぶ、というほうがわかりやすいかと思います。
また、メソッドポインタのリストを保持した場合の注意点として、オブジェクトの開放時にメソッドポインタが無効となる点があると思います。
VCLオブジェクトとしてプロパティで連結する場合には、Notificationメソッドで開放通知できる点も、利点だと思います。
にしのさんとお話しするのはおもしろいですね。純粋にこのテーマ自体もおもしろいですし。
C# のデリゲートでは、マルチキャストできるんですけど、Delphi のイベントはできないん
ですよね。それでいろいろ考えたことがありました。
最初にわたしも単にオブジェクトのリストを保持するプロパティーでいいんじゃないか、と
考えたんです。でも、元質問の中の
> またClassAはFormA、FormBともに参照できません。
ことから、リスト中のオブジェクトのクラスを予め想定するような実装はまずいように思い
ました。共通の上位クラスを使うにしても、メソットを呼び出すときには具体的な下位クラス
にキャストしなければなりませんし。したがって、オブジェクトのリストを保持するようでは
予め呼び出すメソッドが全て固定、そして予めオブジェクトがどんなクラスのインスタンスで
あるか分かっている必要があります。TCollection を使う場合でもそうです。それに対して
メソッドそのもののリストでは、暗黙のオブジェクト参照が渡されるため、明示的にその
メソッドが属するオブジェクトを参照する必要がありません。ハンドラの型だけが一致していれ
ばOKです。ところが SizeOf(TNotifyEvent) としてみれば分かりますように、8バイトなので
単純なポインタのリスト TList ではダメです。上では、PNotifyEvent を定義して、New() と
Dispose() で追加削除していますので、ちょっと変わった実装になっています。
> メソッドポインタのリストを保持した場合の注意点として、オブジェクトの開放時にメソッド
> ポインタが無効となる点があると思います。
そうですね。Add() したら、解放時に Delete() する必要があるのは、Delphi の常道だと
思いますけど。二重に登録したり、登録してないのに Delete() しても無害になるように
実装してあります。例として出した TClassA は、本当は純粋なリストオブジェクトとすべき
で、新たにつくられるクラスに has されるようにするのが理想ですけど、そこまではしてい
ません。
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つのクラスにする。」
とした方が後々楽になりそうな気もするのですがどうでしょうか?
以前に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を削除する必要がありません
> sakiyama さん
すばらしい! まさしくこれですね。デザパタの典型例でしたか?
なるほど、登録する方にも共通のクラスのインスタンスを置けば、にしのさんのようにオブジェクトの
リストで管理できますね。わたしは実装の方に気を取られて大局的な視点が欠けていたようです。
勉強になりました。
> このコンポーネントの利点はFreeNotificationを使ってるのでObserverの生死が自動的に管理されます。
これなんですけど、FObservable.FreeNotification(Self); で Observer を Observable に開放通知する
のは分かるんですけど、肝心の TObservable のほうで通知を受けて FObservers のリストを更新する
コードが見当たりません。Notification メソッドを override する必要があるのではないでしょうか?
> これなんですけど、FObservable.FreeNotification(Self); で Observer を > Observable に開放通知する
> のは分かるんですけど、肝心の TObservable のほうで通知を受けて FObservers のリストを更新する
> コードが見当たりません。Notification メソッドを override する必要があるのではないでしょうか?
その辺はVCL任せになってますね
FObserversはTComponentListですので、登録されているコンポーネントのFreeNotificationを捕まえます
すいません。
FreeNotification要りませんでしたね。
以前はTComponentList使ってませんでしたのでその名残でした
> FreeNotification要りませんでしたね。
なるほど、それなら分かりました。ありがとうございました。
ごめんなさい。適当なことを言ってしまいました。
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のどっちがどっちか分からなくなります。
> いつもObservable/Observerのどっちがどっちか分からなくなります。
いや、わたしも2重に勘違いしていたことに気がつきました。ありがとうございます。
sakiyamaさん、ご教授ありがとうございます。
なるほどイベント発生先と受け取り先をそれぞれクラス化し
さらにTComponentListによる管理とは目から鱗です。
ソースを参考にしてさっそく実装してみたいと思います。
ありがとうございました。
はじめまして、ポンと申します。
take様とsakiyama様のやり取りからいろいろ勉強になりましたが、
勉強不足でなんか理解ずらいです。
take様からあのFormAとFormBのソースをもらっていいですか?
>ソースを参考にしてさっそく実装してみたいと思います。
「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.
ツイート | ![]() |