6

I am working at a huge, legacy source code where several SetFocus is called at many places, but sometimes, the check if the control is visible or enabled is missing.

Due to limited time, and the huge amount of source code, I decided that I want to ignore these errors, since the focus is (in our case) not a critical feature. A raised Exception will result in a complete failure, while a missing focus is just an optical issue.

My current plan is following:

  1. I create an unit with a class helper like this:

    type TWinControlEx = class helper for TWinControl procedure SetFocusSafe; end;

    procedure TWinControlEx.SetFocusSafe; begin if CanFocus then SetFocus; end;

  2. I include the unit to every unit which uses ".SetFocus" (I will use the global code search)

  3. I replace every .SetFocus with .SetFocusSafe

There is a problem though: If possible, I want to avoid that coworkers accidently use .SetFocus , or forget to include the classhelper unit.

Which other options do I have?

The best case would be if there is a technique/hack to make SetFocus not raising an exception. (Without recompiling the VCL)

Daniel Marschall
  • 3,739
  • 2
  • 28
  • 67
  • 4
    It would be far easier to fix the code properly than what you are suggesting. – David Heffernan Dec 07 '16 at 12:29
  • 3
    Agreed that the best solution is just to fix it. If you're calling `SetFocus` on a control that cannot be focused then your code has somehow gotten out of sync with the expected state of the application. This points to broader problems where you are executing code in an inappropriate context. If the state of the application is not what you expect then it suggests that something else has gone wrong previously that may cascade into even more problems going forward. – J... Dec 07 '16 at 12:36

3 Answers3

7

Just patch the TWinControl.SetFocus method:

unit SetFocusFix;

interface

implementation

uses
  Controls,
  Forms,
  SysUtils,
  Windows;

type
  TWinControlHack = class(TWinControl)
  public
    procedure SetFocus; override;
  end;

procedure TWinControlHack.SetFocus;
var
  Parent: TCustomForm;
begin
  if not CanFocus then Exit;

  Parent := GetParentForm(Self);
  if Parent <> nil then
    Parent.FocusControl(Self)
  else if ParentWindow <> 0 then
    Windows.SetFocus(Handle)
  else
    ValidParentForm(Self);
end;

procedure RedirectFunction(OrgProc, NewProc: Pointer);
type
  TJmpBuffer = packed record
    Jmp: Byte;
    Offset: Integer;
  end;
var
  n: UINT_PTR;
  JmpBuffer: TJmpBuffer;
begin
  JmpBuffer.Jmp := $E9;
  JmpBuffer.Offset := PByte(NewProc) - (PByte(OrgProc) + 5);
  if not WriteProcessMemory(GetCurrentProcess, OrgProc, @JmpBuffer, SizeOf(JmpBuffer), n) then
    RaiseLastOSError;
end;

initialization
  RedirectFunction(@TWinControl.SetFocus, @TWinControlHack.SetFocus);

end.
Stefan Glienke
  • 20,860
  • 2
  • 48
  • 102
  • Unfortunately this hack sometimes doesn't work. It means that TWinControlHack.SetFocus is called but application raises exception EInvalidOperation "Cannot focus a disabled or invisible window." on line Parent.FocusControl(Self). I don't know how to make this happen on request. I noticed this while analysing bugreport file from customer. As workaround I placed whole method into try catch block. I meet this error occasionaly few years. I thought that your solution will be final. – truthseeker Aug 22 '22 at 09:47
  • CanFocus is **BROKEN** (or incomplete in Delphi). Please check this article on how to [fix it](https://gabrielmoraru.com/setfocus-is-broken-in-delphi/) – Gabriel Mar 29 '23 at 14:13
4

Alternatively

  TWinControlEx = class helper for TWinControl
    procedure SetFocus; reintroduce;
  end;

with...

procedure TWinControlEx.SetFocus;
var
  Parent: TCustomForm;
begin
  if not CanFocus then Exit;
  Parent := GetParentForm(Self);
  if Parent <> nil then
    Parent.FocusControl(Self)
  else if ParentWindow <> 0 then
    Winapi.Windows.SetFocus(Handle)
  else
    ValidParentForm(Self);
end;
J...
  • 30,968
  • 6
  • 66
  • 143
3

My answer below does not answer DIRECTLY your question but it is still relevant because you rely on CanFocus. CanFocus returns a lie. You should not rely on it. The documentation is also wrong. More exactly, CanFocus can return True even if the control is not focusable. In this case an exception will be raised.

So, use this instead:

function CanFocus(Control: TWinControl): Boolean;   
begin
 Result:= Control.CanFocus AND Control.Enabled AND Control.Visible;
 if Result
 AND NOT Control.InheritsFrom(TForm)
 then
   { Recursive call:
     This control might be hosted by a panel which could be also invisible/disabled.
     So, we need to check all the parents down the road, until we encounter the parent Form.
     Also see: GetParentForm }
   Result:= CanFocus(Control.Parent); { Parent of a control could be nil, but in this case Control.CanFocus will deal with that.}
end;


procedure SetFocus(Control: TWinControl);
begin
 if CanFocus(Control)
 then Control.SetFocus;
end;

PS: Under Lazarus CanFocus works properly.

Update 2023

Please see this new article about CanFocus and how to fix it. You can find there even a tool that will replace all Control.SetFocus methods with the fixed function SetFocus().


Justification:

J provided a nice answer, but I don't like class helpers because if you have more than one class helper for the same class, the only one will be used. The process is almost "by dice": the order of the units in the "uses" clause determine which helper will apply. I don't like this amount of randomness in a programming language.

Gabriel
  • 20,797
  • 27
  • 159
  • 293