Flexible method of detecting focused control change
Asked Answered
H

3

7

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.

Heurlin answered 25/6, 2012 at 11:40 Comment(2)
If you said more than one TFocusObserver might exists at one time I guess, did you mean that all of them should have get notified about those focus changes or only one of them ?Normand
What is the base class of TFocusObserver?Creative
C
9

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?

Creative answered 25/6, 2012 at 13:14 Comment(2)
Thanks, this solution has two drawbacks: 1 - I must place this component on a form so it could capture change of focus. 2- It will detect the focus change only on the form on which it sits. However, if nothing better comes up I'll accept this as an answer.Heurlin
I appreciate your time and the second approach works for me. Thanks.Heurlin
N
0

You could remember the value of Screen.OnActiveControlChange just before your component replaces it.

FOnActiveControlChange := Screen.OnActiveControlChange;
Screen.OnActiveControlChange = MyOnActiveControlChange;

Then in xxx.MyOnActiveControlChange

begin
  // what you wanted to do here
  ...

  if Assigned( FOnActiveControlChange) then begin

    // Forward to previous subscriber.
    FOnActiveControlChange( Sender, ...);
end;

But this only works if you're in control of the application, if someone else uses your component and he/she has other components that also use OnActiveControlChange things might go wrong.

Notify answered 25/6, 2012 at 12:3 Comment(2)
Thanks Marck, as I stated in my question - someone else might change assignment for TScreen.OnActiveControlChange. So this won't work...Heurlin
Well, another option is to listen for CM_FOCUSCHANGED messages.Notify
L
0

I know this is a very old post but here it is :

If using a third party library is an option, you could always use Spring4d

I implemented something similar in the past. It goes in the lines of :

uses

Spring;

type

TActiveControl = class(TObject)
private
   FEvent : Event<TNotifyEvent>;
   class var FInstance : TActiveControl;
   function GetOnActiveControlChanged : IEvent<TNotifyEvent>;
   procedure DoOnActiveControlChanged(Sender : TObject);
public
   class property Instance : TActiveControl read FActiveControl;
   property OnActiveControlChanged : IEvent<TNotifyEvent> get GetOnActiveControlChanged;
   constructor Create;
   destructor Destroy; override;
end;

TSubscriber = class(TObject)
private
   procedure DoOnActiveControlChanged(Sender : TObject);
public
   constructor Create;
   destructor Destroy; override;
end;

...

function TActiveControl.GetOnActiveControlChanged : IEvent<TNotifyEvent>
begin
  Result := FEvent;
end;

constructor TActiveControl.Create
begin
  Screen.OnActiveControlChanged := DoOnActiveControlChanged;
end;

destructor TActiveControl.Destroy;
begin
   OnActiveControlChanged.Clear;
   FOnActiveControlChanged := nil;
   inherited;
end;

procedure TActiveControl.DoOnActiveControlChanged(Sender : TObject);
begin
  if OnActiveControlChanged.CanInvoke
     OnActiveControlChanged.Invoke(Sender);
end;

procedure TSubscriber .DoOnActiveControlChanged(Sender : TObject);
begin
   // OnActiveControl has been triggered
end;


constructor TSubscriber.Create;
begin
   TActiveControl.Instance.OnActiveControlChanged.Add(DoOnActiveControlChanged); 
end;


destructor TSubscriber .Destroy


initialization

   TActiveControl.FInstance := TActiveControl.Create;

finalization

   FreeAndNil(TActiveControl.FInstance);

In this case I use a simple version of the singleton pattern for TActiveControl. You could also create an interface for it (IActiveControl) and inject it when necessary using IOC. This is a further step though.

This way, all your calls to Screen.OnActiveControlChanged should instead use TActiveControl.Instance.OnActiveControlChanged. Spring4d is awesome. There's lots of stuff in it, including multicast, IOC container, collections, etc. etc.

Lucie answered 3/8, 2018 at 22:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.