13

When I try to disable a Button on a styled VCL from using the follwing line of code

TButton(Sender).enabled:= False;

I get the this result (Button disabled at runtime)

enter image description here

instead of this!! (Button disabled at design time)

enter image description here

It's really confusing to have two or more Buttons with the same color beside each other, one is disabled and the other is enabled

LU RD
  • 34,438
  • 5
  • 88
  • 296
Raul
  • 656
  • 5
  • 17
  • Is that just how that particular style displays, or do you think there is a bug? If the first image is the disabled button, what is the second image? – David Heffernan Mar 06 '12 at 09:13
  • 2
    @DavidHeffernan the first image is disable by code, the second is disabled from the Object Inspector (design time), I think it is a bug! – Raul Mar 06 '12 at 09:24
  • what about `repaint` after disabling button at run-time ? – teran Mar 06 '12 at 10:19
  • @teran I tried to call the repaint procedure but it doesn't make any difference – Raul Mar 06 '12 at 10:23
  • 1
    How sure are you that `Sender` really is the `TButton`. Try `(Sender as TButton).Enabled := False` or even `Button1.Enabled := False;` If `Sender` is not a `TButton` then anything could happen with your code. – David Heffernan Mar 06 '12 at 10:58
  • Thanks Dave, I tried all the above but but sitll having the same result, by the way how can the sender be something else than TButton when getting it passed through the OnClick of a TButton? – Raul Mar 06 '12 at 11:07
  • Ruby Graphite and it happens with any other style. – Raul Mar 06 '12 at 11:33
  • 1
    @Raul If it's the OnClick of the button then the button will be the Sender. But if there are actions involved then the Action will be the Sender. – David Heffernan Mar 06 '12 at 11:34
  • @Raul It's a clear bug. I can reproduce it. I suggest you submit a QC report. I'm trying to come up with a workaround. – David Heffernan Mar 06 '12 at 11:39
  • Logged in QC today. http://qc.embarcadero.com/wc/qcmain.aspx?d=106327 – Warren P Jun 11 '12 at 19:03

2 Answers2

16

The cause of this issue is located in the Paint method of the TButtonStyleHook (in the Vcl.StdCtrls unit) style hook class.

Locate this code in the method

if FPressed then
  Details := StyleServices.GetElementDetails(tbPushButtonPressed)
else if MouseInControl  then //this condition is triggered even if the button is disabled
  Details := StyleServices.GetElementDetails(tbPushButtonHot)
else if Focused then //this condition is triggered even if the button is disabled
  Details := StyleServices.GetElementDetails(tbPushButtonDefaulted)
else if Control.Enabled then
  Details := StyleServices.GetElementDetails(tbPushButtonNormal)
else
  Details := StyleServices.GetElementDetails(tbPushButtonDisabled);

And replace for this code

if FPressed then
  Details := StyleServices.GetElementDetails(tbPushButtonPressed)
else if MouseInControl and Control.Enabled then
  Details := StyleServices.GetElementDetails(tbPushButtonHot)
else if Focused and Control.Enabled then
  Details := StyleServices.GetElementDetails(tbPushButtonDefaulted)
else if Control.Enabled then
  Details := StyleServices.GetElementDetails(tbPushButtonNormal)
else
  Details := StyleServices.GetElementDetails(tbPushButtonDisabled);

Another option is rewrite the Style hook for the TButton :

Check this code based in this article Fixing a VCL Style bug in the TButton component. The advantage of ths code is which you are fixing two issues 103708 and 103962.

Uses
 Winapi.CommCtrl,
 Vcl.Themes,
 Vcl.Styles;

type
  TCustomButtonH=class(TCustomButton);

  //we need this helper to access some strict private fields
  TButtonStyleHookHelper = class Helper for TButtonStyleHook
  protected
   function Pressed : Boolean;
   function DropDown: Boolean;
  end;

  //to avoid writting a lot of extra code we are to use TButtonStyleHook class and override the paint method
  TButtonStyleHookFix = class(TButtonStyleHook)
  protected
    procedure Paint(Canvas: TCanvas); override;
  end;


{ TButtonStyleHookFix }

procedure TButtonStyleHookFix.Paint(Canvas: TCanvas);
var
  LDetails          : TThemedElementDetails;
  DrawRect          : TRect;
  pbuttonImagelist  : BUTTON_IMAGELIST;
  IW, IH, IY        : Integer;
  LTextFormatFlags  : TTextFormatFlags;
  ThemeTextColor    : TColor;
  Buffer            : string;
  BufferLength      : Integer;
  SaveIndex         : Integer;
  X, Y, I           : Integer;
  BCaption          : String;
begin

  if StyleServices.Available then
  begin
    BCaption := Text;

    if Pressed then
      LDetails := StyleServices.GetElementDetails(tbPushButtonPressed)
    else
    if MouseInControl and Control.Enabled then
      LDetails := StyleServices.GetElementDetails(tbPushButtonHot)
    else
    if Focused and Control.Enabled  then
      LDetails := StyleServices.GetElementDetails(tbPushButtonDefaulted)
    else
    if Control.Enabled then
      LDetails := StyleServices.GetElementDetails(tbPushButtonNormal)
    else
      LDetails := StyleServices.GetElementDetails(tbPushButtonDisabled);

    DrawRect := Control.ClientRect;
    StyleServices.DrawElement(Canvas.Handle, LDetails, DrawRect);

    if Button_GetImageList(handle, pbuttonImagelist) and (pbuttonImagelist.himl <> 0) and ImageList_GetIconSize(pbuttonImagelist.himl, IW, IH) then
    begin
      if (GetWindowLong(Handle, GWL_STYLE) and BS_COMMANDLINK) = BS_COMMANDLINK then
        IY := DrawRect.Top + 15
      else
        IY := DrawRect.Top + (DrawRect.Height - IH) div 2;

      //here the image is drawn properly according to the ImageAlignment value
      case TCustomButton(Control).ImageAlignment of
        iaLeft  :
                  begin
                    ImageList_Draw(pbuttonImagelist.himl, 0, Canvas.Handle, DrawRect.Left + 3, IY, ILD_NORMAL);
                    Inc(DrawRect.Left, IW + 3);
                  end;
        iaRight :
                  begin
                    ImageList_Draw(pbuttonImagelist.himl, 0, Canvas.Handle, DrawRect.Right - IW -3, IY, ILD_NORMAL);
                    Dec(DrawRect.Right, IW - 3);
                  end;

        iaCenter:
                  begin
                   ImageList_Draw(pbuttonImagelist.himl, 0, Canvas.Handle, (DrawRect.Right - IW) div 2, IY, ILD_NORMAL);
                  end;


        iaTop   :
                  begin
                   ImageList_Draw(pbuttonImagelist.himl, 0, Canvas.Handle, (DrawRect.Right - IW) div 2, 3, ILD_NORMAL);
                  end;


        iaBottom:
                  begin
                   ImageList_Draw(pbuttonImagelist.himl, 0, Canvas.Handle, (DrawRect.Right - IW) div 2, (DrawRect.Height - IH) - 3, ILD_NORMAL);
                  end;

      end;


    end;

    if (GetWindowLong(Handle, GWL_STYLE) and BS_COMMANDLINK) = BS_COMMANDLINK then
    begin
      if pbuttonImagelist.himl = 0 then
        Inc(DrawRect.Left, 35);

      Inc(DrawRect.Top, 15);
      Inc(DrawRect.Left, 5);
      Canvas.Font := TCustomButtonH(Control).Font;
      Canvas.Font.Style := [];
      Canvas.Font.Size := 12;
      LTextFormatFlags := TTextFormatFlags(DT_LEFT);
      if StyleServices.GetElementColor(LDetails, ecTextColor, ThemeTextColor) then
         Canvas.Font.Color := ThemeTextColor;
      StyleServices.DrawText(Canvas.Handle, LDetails, BCaption, DrawRect, LTextFormatFlags, Canvas.Font.Color);
      SetLength(Buffer, Button_GetNoteLength(Handle) + 1);
      if Length(Buffer) <> 0 then
      begin
        BufferLength := Length(Buffer);
        if Button_GetNote(Handle, PChar(Buffer), BufferLength) then
        begin
          LTextFormatFlags := TTextFormatFlags(DT_LEFT or DT_WORDBREAK);
          Inc(DrawRect.Top, Canvas.TextHeight('Wq') + 2);
          Canvas.Font.Size := 8;
          StyleServices.DrawText(Canvas.Handle, LDetails, Buffer, DrawRect,
            LTextFormatFlags, Canvas.Font.Color);
        end;
      end;

      if pbuttonImagelist.himl = 0 then
      begin
        if Pressed then
          LDetails := StyleServices.GetElementDetails(tbCommandLinkGlyphPressed)
        else if MouseInControl then
          LDetails := StyleServices.GetElementDetails(tbCommandLinkGlyphHot)
        else if Control.Enabled then
          LDetails := StyleServices.GetElementDetails(tbCommandLinkGlyphNormal)
        else
          LDetails := StyleServices.GetElementDetails(tbCommandLinkGlyphDisabled);
        DrawRect.Right := 35;
        DrawRect.Left := 3;
        DrawRect.Top := 10;
        DrawRect.Bottom := DrawRect.Top + 32;
        StyleServices.DrawElement(Canvas.Handle, LDetails, DrawRect);
      end;

    end
    else
    if (GetWindowLong(Handle, GWL_STYLE) and BS_SPLITBUTTON) = BS_SPLITBUTTON then
    begin
      Dec(DrawRect.Right, 15);
      DrawControlText(Canvas, LDetails, Text, DrawRect, DT_VCENTER or DT_CENTER);
      if DropDown then
      begin
        LDetails := StyleServices.GetElementDetails(tbPushButtonPressed);
        SaveIndex := SaveDC(Canvas.Handle);
        try
          IntersectClipRect(Canvas.Handle, Control.Width - 15, 0,
            Control.Width, Control.Height);
          DrawRect := Rect(Control.Width - 30, 0, Control.Width, Control.Height);
          StyleServices.DrawElement(Canvas.Handle, LDetails, DrawRect);
        finally
          RestoreDC(Canvas.Handle, SaveIndex);
        end;
      end;

      with Canvas do
      begin
        Pen.Color := StyleServices.GetSystemColor(clBtnShadow);
        MoveTo(Control.Width - 15, 3);
        LineTo(Control.Width - 15, Control.Height - 3);
        if Control.Enabled then
          Pen.Color := StyleServices.GetSystemColor(clBtnHighLight)
        else
          Pen.Color := Font.Color;
        MoveTo(Control.Width - 14, 3);
        LineTo(Control.Width - 14, Control.Height - 3);
        Pen.Color := Font.Color;
        X := Control.Width - 8;
        Y := Control.Height div 2 + 1;
        for i := 3 downto 0 do
        begin
          MoveTo(X - I, Y - I);
          LineTo(X + I + 1, Y - I);
        end;
      end;

    end
    else
    begin
      //finally the text is aligned and drawn depending of the value of the ImageAlignment property
      case TCustomButton(Control).ImageAlignment of
        iaLeft,
        iaRight,
        iaCenter : DrawControlText(Canvas, LDetails, BCaption, DrawRect, DT_VCENTER or DT_CENTER);
        iaBottom : DrawControlText(Canvas, LDetails, BCaption, DrawRect, DT_TOP or DT_CENTER);
        iaTop    : DrawControlText(Canvas, LDetails, BCaption, DrawRect, DT_BOTTOM or DT_CENTER);
      end;
    end;
  end;
end;

{ TButtonStyleHookHelper }

function TButtonStyleHookHelper.DropDown: Boolean;
begin
  Result:=Self.FDropDown;
end;

function TButtonStyleHookHelper.Pressed: Boolean;
begin
  Result:=Self.FPressed;
end;


initialization
 TStyleManager.Engine.RegisterStyleHook(TButton, TButtonStyleHookFix);
RRUZ
  • 134,889
  • 20
  • 356
  • 483
  • 1
    +1 well done. I knew our resident VCL styles guru would get stuck into this one. I can't believe how poor the VCL code is here. – David Heffernan Mar 06 '12 at 16:25
  • when writing a fix it is simply much cleaner to handle the not Control.Enabled condition first. It means you only have to test that property once. – David Heffernan Mar 06 '12 at 16:39
  • 2
    Needs more than "+1" Embarcadero should pay both of you for the extra efforts :) – Raul Mar 06 '12 at 16:43
11

It's clearly a bug in the VCL. The problem appears to be that modifying the Enabled property of a button from an event handler attached to that button does not change the visual appearance of the button. The button's behaviour is changed (you cannot click it if you set Enabled to False this way), but the visuals do not indicate it.

I submitted QC#103962 and no doubt a future update will fix the problem. In the meantime I offer the following workaround:

procedure TMyForm.Button1Click(Sender: TObject);
begin
  Button1.Enabled := False;
  Button1.Perform(CM_RECREATEWND, 0, 0);
end;

This will force the button's window handle to be recreated and this seems to be enough to get the visuals sorted. There are probably alternative ways to work around this but this was all I have found so far.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490