Both TButton
and TEdit
are TWinControl
descendents - this means that they are windowed controls. When they are created they are allocated their own HWND
and the operating system posts messages to them directly when they have focus. Disabling their containing form prevents the main form from receiving input messages or from receiving focus but it does not disable any other windowed control if it already has input focus.
If these controls do not have input focus, it is responsibility of the containing form to transfer input focus to them when user input (click, tab key, etc) dictates. If the form is disabled and these controls are not focused then the form will not receive the input messages that would allow it to transfer focus. If focus is transferred to a windowed control, however, then all user input goes directly to that control, even if their parent control's window is disabled - they are in fact their own separate windows.
I'm not sure the behaviour you have observed is a bug - it is perhaps not expected, but it is standard behaviour. There is generally no expectation that disabling one window will also disable others within the same application.
The problem is that there are two separate hierarchies in play. On the VCL level, the Button is a child control and has a parent (the form). On the OS level, however, both are separate windows and the (component level) parent/child relationship is not known to the OS. This would be a similar situation :
procedure TForm1.Button1Click(Sender: TObject);
var
form2 : TForm1;
begin
self.Enabled := false;
form2 := TForm1.Create(self);
try
form2.ShowModal;
finally
form2.Free;
end;
end;
Would you really expect form2
to be disabled when it was shown, simply because its TComponent
owner is Form1
? Surely not. Windowed controls are much the same.
Windows themselves can also have a parent/child relationship, but this is separate from component ownership (VCL parent/child) and does not necessarily behave in the same way. From MSDN:
The system passes a child window's input messages directly to the
child window; the messages are not passed through the parent window.
The only exception is if the child window has been disabled by the
EnableWindow function. In this case, the system passes any input
messages that would have gone to the child window to the parent window
instead. This permits the parent window to examine the input messages
and enable the child window, if necessary.
Emphasis mine - if you disable a child window then its messages will be routed to the parent for an opportunity to inspect and act upon them. The reverse is not true - a disabled parent will not prevent a child from receiving messages.
A rather tedious workaround could be to make your own set of TWinControl
s that behave like this :
TSafeButton = class(TButton)
protected
procedure WndProc(var Msg : TMessage); override;
end;
{...}
procedure TSafeButton.WndProc(var Msg : TMessage);
function ParentForm(AControl : TWinControl) : TWinControl;
begin
if Assigned(AControl) and (AControl is TForm) then
result := AControl
else
if Assigned(AControl.Parent) then
result := ParentForm(AControl.Parent)
else result := nil;
end;
begin
if Assigned(ParentForm(self)) and (not ParentForm(self).Enabled) then
Msg.Result := 0
else
inherited;
end;
This walks up the VCL parent tree until it finds a form - if it does and the form is disabled then it rejects input to the windowed control as well. Messy, and probably could be more selective (maybe some messages should not be ignored...) but it would be the start of something that could work.
Digging further, this does seem to be at odds with the documentation :
Only one window at a time can receive keyboard input; that window is
said to have the keyboard focus. If an application uses the
EnableWindow function to disable a keyboard-focus window, the window
loses the keyboard focus in addition to being disabled. EnableWindow
then sets the keyboard focus to NULL, meaning no window has the focus.
If a child window, or other descendant window, has the keyboard focus,
the descendant window loses the focus when the parent window is
disabled. For more information, see Keyboard Input.
This does not seem to happen, even explicitly setting the button's window to be a child with :
oldParent := WinAPI.Windows.SetParent(Button1.Handle, Form1.Handle);
// here, in fact, oldParent = Form1.Handle, so parent/child HWND
// relationship is correct by default.
A bit more (for repro) - same scenario Edit
tabs focus to button, exit handler enables TTimer. Here the form is disabled, but the button retains focus even though this seems to confirm that Form1's HWND is indeed the parent window of the button and it should lose focus.
procedure TForm1.Timer1Timer(Sender: TObject);
var
h1, h2, h3 : cardinal;
begin
h1 := GetFocus; // h1 = Button1.Handle
h2 := GetParent(h1); // h2 = Form1.Handle
self.Enabled := false;
h3 := GetFocus; // h3 = Button1.Handle
end;
In the case where we move the button into a panel, everything seems to work (mostly) as expected. The panel is disabled and the button loses focus, but focus then moves to the parent form (WinAPI suggests it should be NULL).
procedure TForm1.Timer1Timer(Sender: TObject);
var
h1, h2, h3 : cardinal;
begin
h1 := GetFocus; // h1 = Button1.Handle
h2 := GetParent(h1); // h2 = Panel1.Handle
Panel1.Enabled := false;
h3 := GetFocus; // h3 = Form1.Handle
end;
Part of the problem seems to be here - it looks like the top form itself is taking responsibility for defocusing controls. This works except when the form itself is the one being disabled :
procedure TWinControl.CMEnabledChanged(var Message: TMessage);
begin
if not Enabled and (Parent <> nil) then RemoveFocus(False);
// ^^ False if form itself is being disabled!
if HandleAllocated and not (csDesigning in ComponentState) then
EnableWindow(WindowHandle, Enabled);
end;
procedure TWinControl.RemoveFocus(Removing: Boolean);
var
Form: TCustomForm;
begin
Form := GetParentForm(Self);
if Form <> nil then Form.DefocusControl(Self, Removing);
end
Where
procedure TCustomForm.DefocusControl(Control: TWinControl; Removing: Boolean);
begin
if Removing and Control.ContainsControl(FFocusedControl) then
FFocusedControl := Control.Parent;
if Control.ContainsControl(FActiveControl) then SetActiveControl(nil);
end;
This partially explains the above observed behaviour - focus moves to the parent control and the active control loses focus. It still doesn't explain why the 'EnableWindow` fails to kill focus to the button's child window. This does start to seem like a WinAPI problem...
ActiveControl := nil;
) right before form disabling worked for me, but I would wait for a more sophisticated answer from some of the VCL hackers around. – GladysglagoliticTWinControl.CMEnabledChanged
does attempt to forcibly remove focus from itself if a control gets disabled. Not sure why it shouldn't work if the entire form gets disabled, other than that it was forgotten about. – CoexecutorActiveControl := nil;
It looks clean to me. But you might want to save the ActiveControl so it can be restored when the form is enabled again. – Quirt