SetFocus is broken in Delphi. Has been since v1 and there are no signs that it will be fixed soon. What does it mean, broken? Do I really have to explain it? We all know. Try to set focus on a visual control that is disabled or is invisible or simply is not focusable, and the program will crash. Of course, we can try to call CanFocus before we actually set focus, that that function is flawed also.
And because recently I worked on a legacy project where SetFocus was really abused (in 4200 places) I create a safe alternative for Delphi’s SetFocus and a tool to do the batch replace.
The safe alternative looks like this:
function CanFocus(Control: TWinControl): Boolean;
begin
Result:= Control.CanFocus AND Control.Enabled AND Control.Visible;
if Result
AND NOT Control.InheritsFrom(TForm)
then Result:= CanFocus(Control.Parent); { 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. We stop when we encounter the parent Form.
Also see: GetParentForm }
end;
procedure SetFocus(Control: TWinControl);
begin
if CanFocus(Control)
then Control.SetFocus;
end;
As we can see, the work is actually done in CanFocus. Here we check for all possible causes of “accidents”. One important thing to know is that we should not try to focus a control if it is hosted by a disabled control. Therefore, we need to do a recursive check on all parents of our control, down the tree until we hit the parent form. Here we stop.
The program that is doing the batch replace can be found here: https://github.com/GabrielOnDelphi/Delphi_Fix_SetFocus
It looks up for all lines of code that contain something like
MyControl.SetFocus;
If a line is found, it extracts the name of the control and put it inside the new SetFocus function:
SetFocus(MyControl);
Update: I updated the program to automatically add the name of the unit where the new SetFocus is implemented to the “uses” clause. With this change, we can truly do a full batch replace and compile the program, like nothing happen.
The code
The code in the main form is like this (simplified code):
procedure TfrmMain.btnSearchClick(Sender: TObject);
var SearchResult: TSearchResult;
begin
FileList:= ListFilesOf(edtPath.Text, edtFilter.Text, True, True);
try
for TextFile in FileList do
begin
SearchResult:= FindSetFocusInFile(TextFile, bReplace);
if SearchResult.Found
then
begin
lbxResults.AddItem(SearchResult.FileName + ' Found at: '+ SearchResult.PositionsAsString, SearchResult);
Inc(FoundFilesCount);
end
else
begin
lbxResults.Items.Add(TextFile);
FreeAndNil(SearchResult);
end;
end;
Caption:= 'Searched '+ IntToStr(FileList.Count)+ ' files. Found: '+ IntToStr(Replaces)+ ' times in '+ IntToStr(FoundFilesCount)+ ' files.';
finally
FreeAndNil(FileList);
end;
end;
The heavy listing is done by FindSetFocusInFile in cFindInFile.pas. It is looking for the SetFocus keyword and method it with the SetFocus() function.
ListFilesOf is a function from ccIO.pas that returns all PAS files in the specified folder and SearchResult is an object that stores how many files contained the SetFocus methods and on which line.
Note: I provided an already compiled EXE file. However, if you want to compile the program yourself, you need the Delphi LightSaber library.