再入防止のためにイベントハンドラを別メソッドでラップするには?

解決


mikichan  2005-04-24 08:29:29  No: 14451

キーを連続して叩くなどでのイベントハンドラへの再入を防止するために
通常は、
procedure TForm1.StringGrid1SelectCell(Sender: TObject; ACol,
  ARow: Integer; var CanSelect: Boolean);
begin
  StringGrid1.OnSelectCell:=nil;
  try
     ...//処理
  finally
    StringGrid1.OnSelectCell:=StringGrid1SelectCell;
  end;
end;
などと書くと思うのですが、いちいちすべてのイベントハンドラに上記のような
コードを入れるのは手間なので、フォームのCreate時に全てのイベントハンドラに対して
上記処理でラップしたいのですが、うまくいきません。

//やりたいこと
procedure TForm1.FormCreate(Sender: TObject);
begin
  for i:=0 to ComponentCount - 1 do begin
    WrapEventHandlersToAvoidReEntrant(Components[i]);
  end;
end;

procedure TForm1.WrapEventHandlersToAvoidReEntrant(c: TComponet);
begin
//設定されているすべてのイベントハンドラを再入防止の処理でラップする
//(ここは本当はOn〜という名前のプロパティの型と、それがnilかどうかを動的に調べたい)
  c.OnClick:=GetWrappedEventHandler(c.OnClick);
end;

//以下は多分イベントの型ごとに必要?
function TForm1.GetWrappedEventHandler(e: TNotifyEvent):TNotifyEvent;
begin
  Result:=...;
end;
function TForm1.GetWrappedEventHandler(e: TKeyEvent):TKeyEvent;
begin
  Result:=...;
end;

あるMLの過去ログで調べたのですが、PropInfo やTMethod などを使えばいいのかなと
思いつつ、いろいろ試行錯誤して結局できませんでした。
どなたかアドバイスありましたらよろしくお願いします。。。


りおりお  2005-04-24 10:04:39  No: 14452

イベントハンドラの中で明示的に 

Application.ProcessMessages;

などのメッセージ処理をしない限り再入はありえないと思います。
キー入力は一つののハンドラが終わるまでキューで待っています。
ですから再入はないです。連続して実行されることはありますけど。
その場合はイベントハンドラで nil 指定して、出るとき再設定して
も無意味ですけど。


mikichan  2005-04-24 20:40:20  No: 14453

Application.ProcessMessagesはしていませんが、イベントハンドラから
時間のかかる処理を呼ぶなどした場合やはり再入が起こって
しまいます。。。
具体的にはStringGrid内でRetuneキーを押したら、
StringGrid.Col:=NextColというように選択セルを移動します。
そのときSelectCellイベントが起きますが、その中でDBアクセスします。
Returnキーを素早く連続で押すと再入が起こります。
以下はそのときの様子です。

KeyDown start (1回目のReturnキー押下。Col:=Col+1する)
SelectCell start
KeyDown start (2回目のReturnキー押下。素早く押すと発生)
SelectCell start
SelectCell end
KeyDown end
SelectCell end
KeyDown end


りおりお  2005-04-24 21:33:55  No: 14454

なるほど、きっと DB では、アプリが固まらないようにメッセージ処理をしている
んですね。だから再入がおこると。

うーん、でも、全てのハンドラに nil と再設定するのはオススメできません。
再入が疑われるところだけにするのがオススメです。一括でラップする方法は
思い浮かびません。


りおりお  2005-04-24 21:43:30  No: 14455

大抵のコンポのイベントにある OnKeyDown の引数には var Key: Word;
があります。 

if Flag then Key := 0;

のように Key に 0 を代入するとキー入力はなかったことにできます。
また、 TStringGrid の OnSelectCell イベントには、var CanSelect: Boolean
がありますから、これを false にするとフォーカスは移りません。

ですから、Flag:Boolean のような変数を宣言しておけば、適切に再入防止が
できると思います。


mikichan  2005-04-25 02:28:51  No: 14456

フラグを使う等の方法も使っていましたが、少し違う方法で
やってみたいと思いまして。
それに再入防止目的だけでなく、「イベントハンドラをラップ」できれば
他の事にも応用できると思ってどうしたら実現できるのかなと考えていたのです。
(Delphiのリフレクション操作に関するいい情報が見つからなくて、ここに投稿した次第です)
引き続きアドバイス募集中です。よろしくお願い致します。。


Fusa  2005-04-25 07:10:25  No: 14457

procedure OnEventNil
begin
  Button1.OnClick := nil;
  Button2.OnClick := nil;
  Button3.OnClick := nil;
  Button4.OnClick := nil;
end;

procedure OnEventNormal
begin
  Button1.OnClick := Button1OnClick;
  Button2.OnClick := Button2OnClick;
  Button3.OnClick := Button3OnClick;
  Button4.OnClick := Button4OnClick;
end;

こんなものを用意して
何かのイベントに応じて
呼び出してみたり

プロパティで状態変化を実装して
その際に、OnEventNilやOnEventNormalを呼び出したり
してもいいんじゃないでしょうか。

命名がセンスないのは見過ごしください。


mikichan  2005-04-26 10:10:07  No: 14458

うーん、一括してnilにしたりセットしたりする必要性はあまりないです。
それにそれらを呼び出したりプロパティの状態をセットしたりするコードを
あちこちに書くのがいやなので1回で済ませたいと思っているのです。
フォームCreate時に
  for i:=0 to ComponentCount - 1 do begin
    WrapEventHandlersToAvoidReEntrant(Components[i]);
  end;
この3行で全部済む!というのが理想なんですけど。。。


ここにも  2005-04-26 12:29:40  No: 14459

回答に礼をしない人がいる・・・


mikichan  2005-04-26 21:15:42  No: 14460

すみません、解決時点でまとめてお礼すればよいかなと思っていました。
(りおりおさん、Fusaさん、回答ありがとうございます)


test  2005-04-26 23:46:16  No: 14461

TButton の OnClick だけですが、イベントハンドラのラップということで

procedure WrapEventHandlersToAvoidReEntrant(AOwner: TComponent);
var
  I: Integer;
  C: TComponent;
  B: TButton;
begin
  for I := 0 to AOwner.ComponentCount - 1 do
  begin
    C := AOwner.Components[I];
    if (C is TButton) then
    begin
      B := C as TButton;
      if (not Assigned(B.Action)) then
      begin
        B.Action := TBasicAction.Create(C);
        B.Action.OnExecute := B.OnClick;
      end;
      B.OnClick := Form1.WrapEventHandler;
    end;
    WrapEventHandlersToAvoidReEntrant(C);
  end;
end;

procedure TForm1.WrapEventHandler(Sender: TObject);
begin
  if (Sender is TButton) then
  begin
    if (Tag <> 0) then
      Exit;
    Tag := 1;
    try
      TButton(Sender).Action.Execute;
    finally
      Tag := 0;
    end;
  end;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  WrapEventHandlersToAvoidReEntrant(Self);
end;

procedure TForm1.Button1Click(Sender: TObject);
  procedure Delay(MS: DWORD);
  var
    T: DWORD;
  begin
    T := GetTickCount;
    while (GetTickCount - T < MS) do
    begin
      Application.ProcessMessages;
      Sleep(1);
    end;
  end;
begin
  Memo1.Lines.Add('Start');
  Delay(1000);
  Memo1.Lines.Add('End');
end;


mikichan  2005-04-27 04:11:20  No: 14462

TBasicActionですか、使ったことがないので勉強します。
(上記サンプルでラップできてるのかどうかちょっとよく分かりませんでした。。。MemoにStart...(しばらく待った後)...Endとは出たのですが。)


木星人  2005-04-27 22:39:20  No: 14463

面白そうなので作ってみました。  長いけど勘弁。

// オリジナルのイベントを保持するクラス
type TEventWrapper = class
private
  _OriginalEvent : TMethod;

    //とりあえず今回はSelectCellだけ。
  procedure OnSelectCell(Sender: TObject; ACol, ARow: Longint;
        var CanSelect: Boolean);
public
  constructor Create(orig : TMethod);
  class function WrapSelectCell(c : TComponent) : TEventWrapper;
end;

{ TEventWrapper }

constructor TEventWrapper.Create(orig: TMethod);
begin
  _OriginalEvent := orig;
end;

  //ラップしたいイベントの数だけ同じようなメソッドを作る
procedure TEventWrapper.OnSelectCell(Sender: TObject; ACol,
  ARow: Integer; var CanSelect: Boolean);
var
  m : TSelectCellEvent;
begin
  m := (Sender as TStringGrid).OnSelectCell;
  try
    (Sender as TStringGrid).OnSelectCell := nil;
    if Assigned(TSelectCellEvent(_OriginalEvent)) then
      TSelectCellEvent(_OriginalEvent)(Sender, ACol, ARow, CanSelect);
  finally
    (Sender as TStringGrid).OnSelectCell := m;
  end;
end;

class function TEventWrapper.WrapSelectCell(c: TComponent): TEventWrapper;
var
  pi : PPropInfo;
  m : TSelectCellEvent;
begin
  Result := nil;
  pi := GetPropInfo(c.ClassInfo, 'OnSelectCell');
  if Assigned(pi) then
  begin
    Result := TEventWrapper.Create(GetMethodProp(c, pi));
    m := Result.OnSelectCell;
    SetMethodProp(c, pi, TMethod(m));
  end;
end;


木星人  2005-04-27 22:39:57  No: 14464

(* つづき *)
{ TForm1 }

procedure TForm1.FormCreate(Sender: TObject);
var
  i : Integer;
  w : TEventWrapper;
begin
  _Wrappers := TObjectList.Create(True);
  for i:=0 to ComponentCount-1 do
  begin
      //これもラップするイベントの数だけ必要
    w := TEventWrapper.WrapSelectCell(Components[i]);
    if w <> nil then
      _Wrappers.Add(w);
  end;
end;

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

procedure TForm1.StringGrid1KeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
begin
  Memo1.Lines.Add('KeyDown Start');
  StringGrid1.Col := StringGrid1.Col + 1;
  Memo1.Lines.Add('KeyDown End');
end;

procedure TForm1.StringGrid1SelectCell(Sender: TObject; ACol,
  ARow: Integer; var CanSelect: Boolean);
    // testさんの関数を使わせていただきます
  procedure Delay(MS: DWORD);
  var
    T: DWORD;
  begin
    T := GetTickCount;
    while (GetTickCount - T < MS) do
    begin
      Application.ProcessMessages;
      Sleep(1);
    end;
  end;
begin
  Memo1.Lines.Add('SelectCell Start');
  Delay(1000);
  Memo1.Lines.Add('SelectCell End');
end;

ラップした場合としていない場合のMemo1の内容を比べれば
再入が防止されているのがわかります。


mikichan  2005-04-29 03:34:15  No: 14465

もう少し勉強してみます。
とりあえず解決にします。
書き込みくださったみなさん、どうもありがとうございました。


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

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






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