Search code examples
delphifocusdelphi-xeribbontaction

Delphi XE - TRibbon actions always send focus to MainForm


When I place a TRibbon control on a form that is not the MainForm of the application, that TRibbon's actions (i.e. Cut, Paste) will always return focus to the MainForm after the action is executed.

This occurs even if the TForm that holds the TRibbon is not a child of the MainForm.

I am using Windows 7 64-bit, Embarcadero RAD Studio XE Version 15.0.3953.35171.

Am I using the TRibbon control incorrectly, or is this an issue with the TRibbon?


Solution

  • This is evidently by design. Sample code snippet from 'ribbonactnctrls.pas':

    procedure TRibbonBaseButtonControl.Click;
    begin
      inherited;
      SetFocus(Application.MainForm.Handle);
    end;
    

    As you see there are no conditions checked that would help us avoid the call. There's the same code also in menu item selection and key press handlers.


    I would probably modify the source commenting the focus calls, and try to see if there're any side effects.

    As an alternative you can restore the focus back to your form after it is switched to the main form. Suppose 'ActionList1' is the TActionList that contains standard actions on the not main form:

    type
      TForm2 = class(TForm)
        ..
        procedure ActionList1Execute(Action: TBasicAction; var Handled: Boolean);
      private
       ..
    
    procedure TForm2.ActionList1Execute(Action: TBasicAction; var Handled: Boolean);
    begin
      PostMessage(Handle, WM_SETFOCUS, WPARAM(True), 0);
    end;
    

    This will however cause the main form to flash briefly every time an action is executed. If you don't want that, you can change the design so that the main form knows when it is getting an unwanted focus, and fake that it's not focused.

    In unit1:

    const
      UM_CANCELIGNOREFOCUS = WM_USER + 7;
    
    type
      TForm1 = class(TForm)
        ..
      private
        FIgnoreFocus: Boolean;
        procedure UMCancelIgnoreFocus(var Msg: TMessage); message UM_CANCELIGNOREFOCUS;
        procedure WMNCActivate(var Msg: TWMNCActivate); message WM_NCACTIVATE;
      public
        property IgnoreFocus: Boolean write FIgnoreFocus;
      end;
    
    ...
    uses Unit2;
    
    procedure TForm1.WMNCActivate(var Msg: TWMNCActivate);
    begin
      Msg.Result := 0;
      if not (Msg.Active and FIgnoreFocus) then
        inherited;
    end;
    
    procedure TForm1.UMCancelIgnoreFocus(var Msg: TMessage);
    begin
      FIgnoreFocus := False;
      TForm(Msg.WParam).SetFocus;
    end;
    

    in unit2:

    uses
      unit1;
    
    procedure TForm2.ActionList1Execute(Action: TBasicAction; var Handled: Boolean);
    begin
      Form1.IgnoreFocus := True;
      PostMessage(Form1.Handle, UM_CANCELIGNOREFOCUS, NativeInt(Self), 0);
    end;
    


    However, this is not enough if you don't have 'MainFormOnTaskBar' set in project source, since then the main form will not only gain focus but will be brought to front. In this case both forms could respond to the unwanted focus change/activation by freezing their z-orders. The code would then become for unit1:

    const
      UM_CANCELIGNOREFOCUS = WM_USER + 7;
    
    type
      TForm1 = class(TForm)
        ..
      private
        FIgnoreFocus: Boolean;
        procedure UMCancelIgnoreFocus(var Msg: TMessage); message UM_CANCELIGNOREFOCUS;
        procedure WMNCActivate(var Msg: TWMNCActivate); message WM_NCACTIVATE;
        procedure WMWindowPosChanging(var Msg: TWMWindowPosChanging);
            message WM_WINDOWPOSCHANGING;
      public
        property IgnoreFocus: Boolean read FIgnoreFocus write FIgnoreFocus;
      end;
    
    var
      Form1: TForm1;
    
    implementation
    
    {$R *.dfm}
    
    uses Unit2;
    
    procedure TForm1.WMNCActivate(var Msg: TWMNCActivate);
    begin
      Msg.Result := 0;
      if not (Msg.Active and FIgnoreFocus) then
        inherited;
    end;
    
    procedure TForm1.WMWindowPosChanging(var Msg: TWMWindowPosChanging);
    begin
      inherited;
      if FIgnoreFocus then
        Msg.WindowPos.flags := Msg.WindowPos.flags or SWP_NOZORDER;
    end;
    
    procedure TForm1.UMCancelIgnoreFocus(var Msg: TMessage);
    begin
      FIgnoreFocus := False;
      TForm(Msg.WParam).SetFocus;
    end;
    

    and for unit2:

    type
      TForm2 = class(TForm)
        ..
        procedure ActionList1Execute(Action: TBasicAction; var Handled: Boolean);
      private
        procedure WMWindowPosChanging(var Msg: TWMWindowPosChanging);
            message WM_WINDOWPOSCHANGING;
      public
      end;
    
    var
      Form2: TForm2;
    
    implementation
    
    uses
      unit1;
    
    {$R *.dfm}
    
    procedure TForm2.ActionList1Execute(Action: TBasicAction; var Handled: Boolean);
    begin
      Form1.IgnoreFocus := True;
      PostMessage(Form1.Handle, UM_CANCELIGNOREFOCUS, NativeInt(Self), 0);
    end;
    
    procedure TForm2.WMWindowPosChanging(var Msg: TWMWindowPosChanging);
    begin
      inherited;
      if Form1.IgnoreFocus then
        Msg.WindowPos.flags := Msg.WindowPos.flags or SWP_NOZORDER;
    end;