Disabling the form still allow childs controls to receive input
Asked Answered
P

1

12

I'm having a lot of headache in the last days with delphi, what im trying to do is a lot simple, block the interface at somepoint and enable after some other point.

But as simply as it sound i couldn't figure out why somethings are allowed by design, so to clarify:

1) create a project

2) in the form put a edit and a button, tab order of the edit must be first

3) configure the OnExit event of the edit and write:

Enabled := False; 

4) configure the OnClick event of the button and write:

ShowMessage('this is right?');

basically this is it, now compile, the focus it will be at the edit, press tab and the form will be disabled as we demanded, so accordingly to the tab order the next control to gain focus is the button (but we disabled the form), now press space and the message should come up.

so the question is: is this right? whats the logical explanation to this behaviour?

thx in advance.

Prut answered 9/2, 2015 at 11:42 Comment(19)
No. That is not correct.Gladysglagolitic
@Gladysglagolitic Your should elaborate on that point.Legerdemain
@Carcigenicate, that behavior is not correct. I've tested the scenario and could reproduce it in Delphi XE3. If you disable the form, you should not be able to click the button (nor anyhow interact with the form). It is a bug. The OP just asked is this right ? and I said no, that is not correct.Gladysglagolitic
@Gladysglagolitic i see, well this question is a simply version of what i was trying to do in this question, #28373400, since i couldn't get any help from there i tried to do a simple version to demostrate that was not my approach that had problems in the first place.Prut
The form becomes fully disabled after it gets inactive (in your case when the message is shown). But you can narrow the problem down e.g. with a single edit box dropped on a form; if you disable the form from a timer tick event, the only focused edit box on a form remains enabled. I doubt this is by design.Gladysglagolitic
@Gladysglagolitic so i guess there's not fix to this, should i put a ShowMessage('user: this is a fix for a bug, be patience') in every action that i need to be certain that the form is disablePrut
Defocusing active control (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.Gladysglagolitic
This behaviour made sense to me, hence my now-deleted answer, but testing in XE, I see that TWinControl.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.Coexecutor
Looks to me like a bug (?) or inconsistency in Windows itself. I could pretty much reproduce the same behavior is Windows XP system dialogs (using SPY++ and a short test program). e.g. Keyboard TAB/SPACE working while mouse events are ignored...Counterblow
I like ActiveControl := 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
@CraigYoung yeah it works quite well, i might end doing thatPrut
@Counterblow yes, looks like a bug in Windows API. Look at the end part of the edited answer from J below and the comments there.Lafleur
@Counterblow Yes I see now. Havent got acces to my development machine before and cofused with a different scenario. I'm therefore deleting my comment.Latona
@Prut Why would you want to disable whole form? Do you know that by doing this you prevent it from processing any incoming messages? I mean if you disable the main form you can't even close the application properly becouse main form isn't processing the required messages. You can resize the form either. And I'm wondering if such form would even redraw itself after being covered by another window on older vindows versions like WinXPLatona
@Latona im not trying to disable the main form, im working on mdi enviroment atm and i need to block this mdi childs when they are doing some backgroud tasks so the user can keep working to other mdi child while the busy one finish. of course the example doest mention anything about mdi but the problem is the same.Prut
@Prut Wouldn't it be bette if you disable certain controls (buttons, edit boxes, etc) instead of their parent window. Yes disabling parent window seems easier but by doing so you don't give your user a chance to see that the controls that are on that window are to be blocked. Their apearence doesen't change in any way. What you do with this is only prevent your window to process incoming messages. And that also includes forwarding messages to its clients. Now why this fails in your example also puzles me.Latona
@Latona thx for the tips, yes im aware that is not good for the end user to only disable the form without notice about anything, to solve that we have a owned form that notify the user about this background tasks, again the example does not describe my end solution i did this the simplest way so that anyone could reproduce. about your solution i just simply don't think is viable to block every control manually in a application with dozens, sometimes thousands of forms.Prut
@Prut It all depends on the overal design. Do you perhaps use Actions. They can be quite powerfull for quickly enabling or disabling certain controlls. For instnace you can actually make that enabling or disabling action enables or disables multiple buttons while none of them actually executes any code related to that action. How to do that? Asign some action to any of your existing buttons. This wil change button caption and its OnClick method. So now all you need is to change back the caption and OnClick method. ...Latona
... This way the button will stil act as it did before. The only difference is that you can now enable or disable it by enabling or disabling the action the button is tied to. And yes you can tie multiple button to same action this way and thus get ability to enable or disable all of them. ANd this can be even used across multiple forms at once.Latona
E
11

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 TWinControls 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...

Economic answered 9/2, 2015 at 13:6 Comment(9)
If you disable e.g. a panel, all its children are disabled (and the focused one loses its focus), so why would a form should be an exception ?Gladysglagolitic
@Gladysglagolitic i expect that too, still think that this shouldn't be allowed or if really is by design there should be a way to configure this bahaviour, anyway, so back to the my originally question, #28373400, there's really no way to block this unwanted messages from the form and his TWinControl childs components?Prut
Actually, AFAIK the OS is aware of the parent/child relationship of these windows. According to MSDN, "the descendant window loses the focus when the parent window is disabled". The OnExit is probably being triggered before the button is actually focused (which makes sense).Lafleur
Your first code example is about ownership, not parent/child relationship. I don't think this is the issue here.Counterblow
@Lafleur Child windows (HWND parent/child) are not disabled when their parent windows are. I'll add more information.Economic
@Economic I know they are not disabled. But it's supposed to lose focus, which is not happening. I just tested, and it has nothing to do with when OnExit is triggered. Seems to be a Windows bug to me. I've put a timer in the form with 500ms to delay the disabling of the form (to ensure button is already focused) and it still doesn't lose focus. I've even tried directly using the EnableWindow function (mentioned in the linked documentation), instead of Enabled := False;, but nothing.Lafleur
@Economic I didn't follow. What does the timer handle have to do with it?Lafleur
@Lafleur Sorry - I follow what you're saying now - it does seem wrong, I've added the relevant section on the topic.Economic
thx for the effor @Economic , it really helped to undestand what was happeningPrut

© 2022 - 2024 — McMap. All rights reserved.