View Issue Details

IDProjectCategoryView StatusLast Update
0034763LazarusLCLpublic2019-01-27 14:39
ReporterAntonio RamírezAssigned ToMichl 
PrioritynormalSeverityminorReproducibilityalways
Status resolvedResolutionfixed 
PlatformWindowsOS7OS Version7
Product Version2.0RC3Product Build 
Target Version2.0Fixed in Version2.1 (SVN) 
Summary0034763: Regression: not every TGrouBox child follows parent's "Enabled" property.
DescriptionAdding some controls to a TGroupBox in 2.0RC3 and setting the TGroupBox Enabled property to False, when the program starts they still look as if they were enabled; they behave correctly: they don't detect mouse clicks and such; but they appear as if they were enabled. This used to work correctly under 1.8.4 and doesn't affect every control, two that come to mind: buttons and checkboxes.
(A quick program is attached to test this).
Steps To ReproduceAdd a TGroupBox to a form.
Add some controls to the TGroupBox
Set the TGroupBox's Enabled property to False
Compile and run the program
The controls appear as if they were enabled.
Appearance behaves correctly when toggling the group's Enabled property at run time.
TagsNo tags attached.
Fixed in Revision60224
LazTarget2.0
WidgetsetWin32/Win64
Attached Files
  • bugreport.zip (64,855 bytes)
  • win32-enable-190104.patch (7,678 bytes)
    Index: lcl/interfaces/win32/win32callback.inc
    ===================================================================
    --- lcl/interfaces/win32/win32callback.inc	(revision 59978)
    +++ lcl/interfaces/win32/win32callback.inc	(working copy)
    @@ -287,26 +287,6 @@
         TWin32WSWinControl.SetText(ComboBox, ComboBox.Items[Index]);
     end;
     
    -procedure EnableChildWindows(WinControl: TWinControl; Enable: boolean);
    -var
    -  i: integer;
    -  ChildControl: TWinControl;
    -begin
    -  for i := 0 to WinControl.ControlCount-1 do
    -  begin
    -    if WinControl.Controls[i] is TWinControl then
    -    begin
    -      ChildControl := TWinControl(WinControl.Controls[i]);
    -
    -      if not Enable or ChildControl.Enabled then
    -      begin
    -        EnableWindow(ChildControl.Handle, Enable and ChildControl.Enabled);
    -        EnableChildWindows(ChildControl, Enable and ChildControl.Enabled);  // Recursive call
    -      end;
    -    end;
    -  end;
    -end;
    -
     // A helper class for WindowProc to make it easier to split code into smaller pieces.
     // The original function was about 2400 lines.
     
    @@ -1471,8 +1451,7 @@
     
     procedure TWindowProcHelper.DoMsgEnable;
     begin
    -  if WParam <> 0 Then
    -    LMessage.Msg := LM_SETEDITABLE;
    +  LMessage.Msg := LM_ENABLE;        // ~bk
       if Window = Win32WidgetSet.AppHandle then
         if WParam = 0 then
         begin
    @@ -1484,13 +1463,8 @@
           Screen.EnableForms(DisabledForms);
         end;
     
    -  // disable child windows of for example groupboxes, but not of forms
    -  if Assigned(lWinControl) and not (lWinControl is TCustomForm) then
    -    EnableChildWindows(lWinControl, WParam<>0);
    -
    -  // ugly hack to give bitbtns a nice look
    -  // When no theming active, the internal image needs to be
    -  // recreated when the enabled state is changed
    +  // When themes are not enabled, it is necessary to redraw the BitMap associated
    +  // with the TCustomBitBtn so Windows will reflect the new UI appearence.
       if not ThemeServices.ThemesEnabled and (lWinControl is TCustomBitBtn) then
         DrawBitBtnImage(TCustomBitBtn(lWinControl), TCustomBitBtn(lWinControl).Caption);
     end;
    Index: lcl/interfaces/win32/win32winapi.inc
    ===================================================================
    --- lcl/interfaces/win32/win32winapi.inc	(revision 59978)
    +++ lcl/interfaces/win32/win32winapi.inc	(working copy)
    @@ -1212,23 +1212,111 @@
     end;
     
     {------------------------------------------------------------------------------
    -  Method:  EnableWindow
    -  Params: HWnd    - handle to window
    -          BEnable -  whether to enable the window
    -  Returns: If the window was previously disabled
    +  Method:  EnableWindow                       Response to bug #0033923
    +  Params:  HWnd    - handle to window
    +           BEnable -  whether to enable the window
    +  Returns: If the window was previously disabled.
     
       Enables or disables mouse and keyboard input to the specified window or
    -  control.
    +  control and updates the graphical appearance of the control
    +
    + ------------------------------------------------------------------------------
    +  Note June 2018: ~bk
    +  In normal lazarus behavior, Enable is called by
    +    1� - TWinControl.InitializeWnd (wcfInitializing in TWinControl.FWinControlFlags)
    +    2� - When property TWinControl.Enabled changes : TWinControl.CMENABLEDCHANGED
    +
    +  To satisfy W10 new behaviour, if the aHWND parameter effectively changes the
    +  Enabled state of its window, except if it is a TCusomForm descendant,
    +  all its direct children will be asked to update their UI state if needed and
    +  recusively propagate down the Controls children tree.
      ------------------------------------------------------------------------------}
    +function GetWinControl(aHWnd : HWND):TWinControlAccess; inline;
    +begin
    +  Result := TWinControlAccess(GetWin32WindowInfo(aHWnd)^.WinControl);
    +end;
    +
     function TWin32WidgetSet.EnableWindow(HWnd: HWND; BEnable: Boolean): Boolean;
    +  { Retrieve TWinControl via its Handle}
    +
    +  { Unconditionally set the HWnd Enabled for all children of aWinControl and
    +    invalidate TControl's }
    +  procedure EnableHWndChildren(aWinControl: TWinControl; aRequestedEnable : boolean);
    +  var
    +    i: integer;
    +    ChildControl: TControl;
    +    ChildWinControl: TWinControl absolute ChildControl;
    +    lRequestedEnable : boolean;
    +    lHWNDEnabledOld : boolean;   // State prior to Windows.EnableWindow
    +    lHWNDEnabledNew : boolean;
    +  begin
    +    for i := 0 to aWinControl.ControlCount-1 do begin
    +      ChildControl := aWinControl.Controls[i];
    +      if ChildControl is TWinControl then begin
    +        if not ChildWinControl.HandleAllocated then  // Do nothing more
    +          continue;
    +        lRequestedEnable := ChildWinControl.Enabled and aRequestedEnable;
    +        lHWNDEnabledOld := IsWindowEnabled(ChildWinControl.Handle);
    +        Windows.EnableWindow(ChildWinControl.Handle, lRequestedEnable);
    +        lHWNDEnabledNew := IsWindowEnabled(ChildWinControl.Handle);
    +        { Test if propagating to children make sense }
    +        if (lHWNDEnabledOld=lHWNDEnabledNew) or   // Did not change, children OK
    +          (ChildWinControl.ControlCount < 1) or   // No children
    +           (lHWNDEnabledNew<>lRequestedEnable)    // Not the requested enabled
    +        then
    +          Continue;
    +        { Propagate to children }
    +        EnableHWndChildren(ChildWinControl, lRequestedEnable);
    +      end
    +      else // Seems necessary. TControl having no handle we have nothing
    +           // to determine previous UI enabled state.
    +        ChildControl.Invalidate;
    +    end;
    +  end;
    +
    +  { Retrieve TWinControl parent HWND enabled state }
    +  function GetParentHWNDEnabled(aWinControl : TWinControl):boolean;
    +  var
    +    lParentHWND : HWND;
    +    lParentControl : TWinControl;
    +  begin
    +    lParentControl := aWinControl.Parent;
    +    { If not assigned or directly under TCustomForm -> true }
    +    Result := not assigned(lParentControl) or lParentControl.InheritsFrom(TCustomForm);
    +    if not Result then begin { Otherwise extract parent control HWND.Enabled }
    +      lParentHWND := GetParent(HWND);
    +      result := lParentHWND=0;
    +      if not Result then
    +        Result := IsWindowEnabled(lParentHWND);
    +    end;
    +  end;
    +
     var
    -  OldEnable: Boolean;
    +  lWinControl : TWinControlAccess;   // The control that is represented by HWnd
    +  lIsForm : boolean;                 // TCustom form : takes its own Enabled state
    +  lRequestedEnable : boolean;        // New requested state.
     begin
    -  OldEnable := IsWindowEnabled(HWnd);
    -  if OldEnable <> BEnable then
    -    Result := Boolean(Windows.EnableWindow(HWnd, BEnable))
    +  lWinControl := GetWinControl(HWnd);
    +  if not Assigned(lWinControl) then        // Raise an error instead ?
    +    Exit;
    +  lIsForm := lWinControl.InheritsFrom(TCustomForm);
    +  if lIsForm then
    +    lRequestedEnable := BEnable             // ignore IsWindowEnabled(lParentHWND)
       else
    -    Result := not OldEnable;
    +    lRequestedEnable := BEnable and GetParentHWNDEnabled(lWinControl);
    +
    +  Result := Boolean(Windows.EnableWindow(HWnd,lRequestedEnable));
    +  { Initializing the HWND enable state when it gets InitializeWnd'd. -> children
    +    do not yet have a handle yet }
    +  if (wcfInitializing in lWinControl.FWinControlFlags)
    +     or lIsForm then                 // Do not propagate EnableWindow fo forms
    +    exit;
    +  // Result contains WS_DISABLED style. True means disabled
    +  if (Result<>lRequestedEnable)           // No change (Note inverted EnableWindow result)
    +     or (lWinControl.ControlCount<1) then // or no controls , exit.
    +    Exit;
    +  { Force actualization of Children }
    +  EnableHWndChildren(lWinControl, lRequestedEnable);
     end;
     
     {------------------------------------------------------------------------------
    

Relationships

related to 0034018 closedMichl LCL: Regression from r58448 & r58497 
related to 0033923 closedMichl Behavior of EnableWindow since last MS Upgrades 
related to 0034807 new TCheckBox.Action.Checked may be incorrectly updated 

Activities

Antonio Ramírez

2018-12-26 10:43

reporter  

bugreport.zip (64,855 bytes)

Michl

2018-12-26 14:41

developer   ~0112889

It comes with revision 58764. I don't know, why it was changed, so assigning to Luiz.

Antonio Ramírez

2019-01-09 08:29

reporter   ~0113281

It gets fixed reverting revision 58912.

Michl

2019-01-09 10:26

developer   ~0113283

> It gets fixed reverting revision 58912.

Yes, I tried to fix the enable issue in Delphi way. There WS_DISABLED is set in CreateParams, in Lazarus not.
It almost was working, but not on GTK2, at least Linux Mint 17.3 KDE GTK2. So I reverted it.

I'll test the other widgetsets to check if its just a Win32 issue.

Antonio Ramírez

2019-01-10 19:08

reporter   ~0113309

Thanks.

Michl

2019-01-26 00:31

developer  

win32-enable-190104.patch (7,678 bytes)
Index: lcl/interfaces/win32/win32callback.inc
===================================================================
--- lcl/interfaces/win32/win32callback.inc	(revision 59978)
+++ lcl/interfaces/win32/win32callback.inc	(working copy)
@@ -287,26 +287,6 @@
     TWin32WSWinControl.SetText(ComboBox, ComboBox.Items[Index]);
 end;
 
-procedure EnableChildWindows(WinControl: TWinControl; Enable: boolean);
-var
-  i: integer;
-  ChildControl: TWinControl;
-begin
-  for i := 0 to WinControl.ControlCount-1 do
-  begin
-    if WinControl.Controls[i] is TWinControl then
-    begin
-      ChildControl := TWinControl(WinControl.Controls[i]);
-
-      if not Enable or ChildControl.Enabled then
-      begin
-        EnableWindow(ChildControl.Handle, Enable and ChildControl.Enabled);
-        EnableChildWindows(ChildControl, Enable and ChildControl.Enabled);  // Recursive call
-      end;
-    end;
-  end;
-end;
-
 // A helper class for WindowProc to make it easier to split code into smaller pieces.
 // The original function was about 2400 lines.
 
@@ -1471,8 +1451,7 @@
 
 procedure TWindowProcHelper.DoMsgEnable;
 begin
-  if WParam <> 0 Then
-    LMessage.Msg := LM_SETEDITABLE;
+  LMessage.Msg := LM_ENABLE;        // ~bk
   if Window = Win32WidgetSet.AppHandle then
     if WParam = 0 then
     begin
@@ -1484,13 +1463,8 @@
       Screen.EnableForms(DisabledForms);
     end;
 
-  // disable child windows of for example groupboxes, but not of forms
-  if Assigned(lWinControl) and not (lWinControl is TCustomForm) then
-    EnableChildWindows(lWinControl, WParam<>0);
-
-  // ugly hack to give bitbtns a nice look
-  // When no theming active, the internal image needs to be
-  // recreated when the enabled state is changed
+  // When themes are not enabled, it is necessary to redraw the BitMap associated
+  // with the TCustomBitBtn so Windows will reflect the new UI appearence.
   if not ThemeServices.ThemesEnabled and (lWinControl is TCustomBitBtn) then
     DrawBitBtnImage(TCustomBitBtn(lWinControl), TCustomBitBtn(lWinControl).Caption);
 end;
Index: lcl/interfaces/win32/win32winapi.inc
===================================================================
--- lcl/interfaces/win32/win32winapi.inc	(revision 59978)
+++ lcl/interfaces/win32/win32winapi.inc	(working copy)
@@ -1212,23 +1212,111 @@
 end;
 
 {------------------------------------------------------------------------------
-  Method:  EnableWindow
-  Params: HWnd    - handle to window
-          BEnable -  whether to enable the window
-  Returns: If the window was previously disabled
+  Method:  EnableWindow                       Response to bug #0033923
+  Params:  HWnd    - handle to window
+           BEnable -  whether to enable the window
+  Returns: If the window was previously disabled.
 
   Enables or disables mouse and keyboard input to the specified window or
-  control.
+  control and updates the graphical appearance of the control
+
+ ------------------------------------------------------------------------------
+  Note June 2018: ~bk
+  In normal lazarus behavior, Enable is called by
+    1� - TWinControl.InitializeWnd (wcfInitializing in TWinControl.FWinControlFlags)
+    2� - When property TWinControl.Enabled changes : TWinControl.CMENABLEDCHANGED
+
+  To satisfy W10 new behaviour, if the aHWND parameter effectively changes the
+  Enabled state of its window, except if it is a TCusomForm descendant,
+  all its direct children will be asked to update their UI state if needed and
+  recusively propagate down the Controls children tree.
  ------------------------------------------------------------------------------}
+function GetWinControl(aHWnd : HWND):TWinControlAccess; inline;
+begin
+  Result := TWinControlAccess(GetWin32WindowInfo(aHWnd)^.WinControl);
+end;
+
 function TWin32WidgetSet.EnableWindow(HWnd: HWND; BEnable: Boolean): Boolean;
+  { Retrieve TWinControl via its Handle}
+
+  { Unconditionally set the HWnd Enabled for all children of aWinControl and
+    invalidate TControl's }
+  procedure EnableHWndChildren(aWinControl: TWinControl; aRequestedEnable : boolean);
+  var
+    i: integer;
+    ChildControl: TControl;
+    ChildWinControl: TWinControl absolute ChildControl;
+    lRequestedEnable : boolean;
+    lHWNDEnabledOld : boolean;   // State prior to Windows.EnableWindow
+    lHWNDEnabledNew : boolean;
+  begin
+    for i := 0 to aWinControl.ControlCount-1 do begin
+      ChildControl := aWinControl.Controls[i];
+      if ChildControl is TWinControl then begin
+        if not ChildWinControl.HandleAllocated then  // Do nothing more
+          continue;
+        lRequestedEnable := ChildWinControl.Enabled and aRequestedEnable;
+        lHWNDEnabledOld := IsWindowEnabled(ChildWinControl.Handle);
+        Windows.EnableWindow(ChildWinControl.Handle, lRequestedEnable);
+        lHWNDEnabledNew := IsWindowEnabled(ChildWinControl.Handle);
+        { Test if propagating to children make sense }
+        if (lHWNDEnabledOld=lHWNDEnabledNew) or   // Did not change, children OK
+          (ChildWinControl.ControlCount < 1) or   // No children
+           (lHWNDEnabledNew<>lRequestedEnable)    // Not the requested enabled
+        then
+          Continue;
+        { Propagate to children }
+        EnableHWndChildren(ChildWinControl, lRequestedEnable);
+      end
+      else // Seems necessary. TControl having no handle we have nothing
+           // to determine previous UI enabled state.
+        ChildControl.Invalidate;
+    end;
+  end;
+
+  { Retrieve TWinControl parent HWND enabled state }
+  function GetParentHWNDEnabled(aWinControl : TWinControl):boolean;
+  var
+    lParentHWND : HWND;
+    lParentControl : TWinControl;
+  begin
+    lParentControl := aWinControl.Parent;
+    { If not assigned or directly under TCustomForm -> true }
+    Result := not assigned(lParentControl) or lParentControl.InheritsFrom(TCustomForm);
+    if not Result then begin { Otherwise extract parent control HWND.Enabled }
+      lParentHWND := GetParent(HWND);
+      result := lParentHWND=0;
+      if not Result then
+        Result := IsWindowEnabled(lParentHWND);
+    end;
+  end;
+
 var
-  OldEnable: Boolean;
+  lWinControl : TWinControlAccess;   // The control that is represented by HWnd
+  lIsForm : boolean;                 // TCustom form : takes its own Enabled state
+  lRequestedEnable : boolean;        // New requested state.
 begin
-  OldEnable := IsWindowEnabled(HWnd);
-  if OldEnable <> BEnable then
-    Result := Boolean(Windows.EnableWindow(HWnd, BEnable))
+  lWinControl := GetWinControl(HWnd);
+  if not Assigned(lWinControl) then        // Raise an error instead ?
+    Exit;
+  lIsForm := lWinControl.InheritsFrom(TCustomForm);
+  if lIsForm then
+    lRequestedEnable := BEnable             // ignore IsWindowEnabled(lParentHWND)
   else
-    Result := not OldEnable;
+    lRequestedEnable := BEnable and GetParentHWNDEnabled(lWinControl);
+
+  Result := Boolean(Windows.EnableWindow(HWnd,lRequestedEnable));
+  { Initializing the HWND enable state when it gets InitializeWnd'd. -> children
+    do not yet have a handle yet }
+  if (wcfInitializing in lWinControl.FWinControlFlags)
+     or lIsForm then                 // Do not propagate EnableWindow fo forms
+    exit;
+  // Result contains WS_DISABLED style. True means disabled
+  if (Result<>lRequestedEnable)           // No change (Note inverted EnableWindow result)
+     or (lWinControl.ControlCount<1) then // or no controls , exit.
+    Exit;
+  { Force actualization of Children }
+  EnableHWndChildren(lWinControl, lRequestedEnable);
 end;
 
 {------------------------------------------------------------------------------

Michl

2019-01-26 00:39

developer   ~0113626

I applied the patch from BrunoK in trunk slightly changed and without the comments. Thanks for it!

Please test and close if ok.

Antonio Ramírez

2019-01-27 07:41

reporter   ~0113652

Seems to be fixed, thanks a lot :)

BrunoK

2019-01-27 13:19

reporter   ~0113661

@Michl Thanks for having taken in consideration my revisions.

Just some little points in changing the names of some variables, considering the removal of comments.

From a maintenance point of view :

lHWNDEnabledOld and lHWNDEnabledNew : the objective is to convey that work is done on the native windows HWND enabled state and NOT on TWinControl.Enabled.

lIsForm : there is a IsForm property in TCustomForm. Using lIsForm shows that we are NOT dealing with TCustomForm.IsForm.

In general, the declarations like :
function TWin32WidgetSet.EnableWindow(HWnd: HWND; BEnable: Boolean): Boolean;
 // HWnd: HWND variable name = type
confuse the debugger (and maybe code tools).
I had to temporarily refactor the declaration to function TWin32WidgetSet.EnableWindow(aHWnd: HWND; BEnable: Boolean): Boolean;
 when I designed my revisions to be able to debug.

That's not much of a problem as my computer is quite fast and rebuilding lazarus not really time consuming. (Except when things go wrong...)

Michl

2019-01-27 14:39

developer   ~0113667

> lHWNDEnabledOld and lHWNDEnabledNew : the objective is to convey that work is
> done on the native windows HWND enabled state and NOT on TWinControl.Enabled.

On WigGetSet level it is clear, mostly we deal with handle. So no need for extra long hard readable names.

> lIsForm : there is a IsForm property in TCustomForm. Using lIsForm shows that we
> are NOT dealing with TCustomForm.IsForm.

That is a vild point. But don't use with... block, so there can't be such a misunderstanding.

Of course identfier HWnd: HWND is odd. Thats a old known issue and there are more of them. I don't change it and a lot of formatting things if not needed, as then the SVN log got broken.
Just as note: to much documentation is bad, as we are on Pascal and code should be readable without comments. Comments breake this reading.

Issue History

Date Modified Username Field Change
2018-12-26 10:43 Antonio Ramírez New Issue
2018-12-26 10:43 Antonio Ramírez File Added: bugreport.zip
2018-12-26 14:40 Michl Assigned To => Michl
2018-12-26 14:40 Michl Status new => assigned
2018-12-26 14:40 Michl Assigned To Michl => Luiz Americo
2018-12-26 14:41 Michl Note Added: 0112889
2018-12-26 14:42 Michl Relationship added related to 0034018
2018-12-26 14:43 Michl Relationship added related to 0033923
2019-01-09 08:29 Antonio Ramírez Note Added: 0113281
2019-01-09 10:12 Michl Assigned To Luiz Americo => Michl
2019-01-09 10:26 Michl Note Added: 0113283
2019-01-10 19:08 Antonio Ramírez Note Added: 0113309
2019-01-26 00:30 Michl Relationship added related to 0034807
2019-01-26 00:31 Michl File Added: win32-enable-190104.patch
2019-01-26 00:39 Michl Fixed in Revision => 60224
2019-01-26 00:39 Michl LazTarget => 2.0
2019-01-26 00:39 Michl Note Added: 0113626
2019-01-26 00:39 Michl Status assigned => resolved
2019-01-26 00:39 Michl Fixed in Version => 2.1 (SVN)
2019-01-26 00:39 Michl Resolution open => fixed
2019-01-26 00:39 Michl Target Version => 2.0
2019-01-27 07:41 Antonio Ramírez Note Added: 0113652
2019-01-27 13:19 BrunoK Note Added: 0113661
2019-01-27 14:39 Michl Note Added: 0113667