Search code examples
delphifocuscomponentsdelphi-2009

Flexible method of detecting focused control change


I need to write a component which will register in self other components and will detect if one of the registered components receive focus.

For example for my component TFocusObserver I am registering three objects.

FocusObserver.Register(MyMemo);
FocusObserver.Register(MyButton);
FocusObserver.Register(MyEdit);

And now if one of this components receives focus then FocusObserver is firing up some notification event.

I was looking how to detect a focus change and have found that TScreen.OnActiveControlChange is exactly what I need. So my component could hook up to this event. The problem is that more than one TFocusObserver might exists or later in a future somoene else might want to use OnActiveControlChange.

This is the time in which I would benefit from multicast event - it would solve my problem right away.

I was thinking how to solve this and I have currently two ideas:

  1. Extending somehow TScreen so it would provide one more event for me.
  2. Introduce an intermediate object which will hook up to OnActiveControlChange and expose one multicast event for other objects.

After a brief look at the sources I have no clear idea how to solve it by using first idea and the second idea has the drawback that someone can simply assign another method to OnActiveControlChange and everything fill fall apart.

Will be grateful for some suggestions.


Solution

  • If your focusObserver class can be a descendant of TWinControl, than you can do this:

    TFocusObserver = class( TWinControl )
    
      procedure CMFocusChanged(var Message: TCMFocusChanged); message CM_FOCUSCHANGED;
    end;
    

    and

    procedure TFocusObserver.CMFocusChanged(var Message: TCMFocusChanged);
    var
      LControl: TWinControl;
    
    begin
          LControl := TWinControl(Message.Sender);
    
          if LControl <> nil then
          begin
            form1.Caption := lControl.Name;
          end;
    end;
    

    Here the main idea is to watch CM_FOCUSCHANGED.

    Second approach:

    When registering the Control you replace it's WindowProc. Here's a little code snippet:

    TRegisteredComp = class
      private
        fControl: TControl;
        fowndproc: TWndMethod;
        procedure HookWndProc(var Message: TMessage);
      public
        constructor Create( c: TControl );
        destructor Destroy; override;
      end;
    
      TFocusObserver = class
      private
        l: TList;
       public
        constructor Create;
        destructor Destroy; override;
        procedure reg( c: TControl );
    
      end;
    

    and under implementation:

    constructor TFocusObserver.Create;
    begin
      l := TList.Create;
    end;
    
    destructor TFocusObserver.Destroy;
    var i: integer;
    begin
      for i := 0 to l.Count - 1 do
        TRegisteredComp(l[i]).Free;
      l.Free;
      inherited;
    end;
    
    procedure TFocusObserver.reg( c: TControl );
    var
      rc: TRegisteredComp;
    begin
      rc := TRegisteredComp.Create( c );
      l.Add( rc );
    end;
    
    constructor TRegisteredComp.Create(c: TControl);
    begin
      fControl := c;
      fowndproc := c.WindowProc;
      c.WindowProc := HookWndProc;
    end;
    
    destructor TRegisteredComp.Destroy;
    begin
      fControl.WindowProc := fowndproc;
      inherited;
    end;
    
    procedure TRegisteredComp.HookWndProc(var Message: TMessage);
    begin
      if ( Message.Msg = CM_FOCUSCHANGED ) and
        ( TControl(Message.LParam) = fControl ) then
        form1.ListBox1.Items.Add( 'focused: ' + fControl.Name );
    
      fowndproc( Message );
    end;
    

    than just register the control you want to watch, example:

    procedure TForm1.FormCreate(Sender: TObject);
    var
      i: Integer;
    begin
      fo := TFocusObserver.Create;
      for i := 0 to ControlCount - 1 do
        fo.reg( Controls[i] );
    end;
    

    How does it sound?