How to make Delphi TButton control stay pressed?
Asked Answered
G

2

6

I've seen How to make a Delphi TSpeedButton stay pressed ..., but I want it to be TButton because of the way it supports drawing glyph (I mean Images, ImageIndex, HotImageIndex, ...). I know that I can draw it all by code, but I thought there must be some trick that makes it stay down.

Gurtner answered 25/10, 2017 at 14:5 Comment(9)
Nope, a TButton does not have a "down" stateEstremadura
@DavidHeffernan, I know, actually I'm looking for a message or something to simulate this behavior by code.Gurtner
How? The underlying Win32 control has no such state.Estremadura
@DavidHeffernan I wouldn't ask a question if I knew how! I suspected that it was possible after reading Rob's comment here: Do we have Button down property as BooleanGurtner
Delphi's TButton wraps the Win32 BUTTON control which does not have a down/pushed state.Estremadura
It's not a matter of how to do it. It's a matter of if it's possible, which David told you three times, it is not. You will need to roll out your own code to mimic this, as it's not natively supported by the Windows API. Surely you don't expect us to write you a custom control, do you? TSpeedButton <> TButton.Silberman
SendMessage(Button.Handle, BM_SETSTATE, BST_PUSHED, LPARAM(True)); (link). I'm not posting an answer because I'm almost sure that you are not sure about what you want: the API draws a button pressed as highlighted because a button is only pressed when it is the active control and you most probably don't want a lot of active looking buttons all around.Anglo
@SertacAkyuz, But how can you actually toggle the button states by clicking on it?Shakespeare
@Kobik - I don't want to toggle it, I was trying to explain toggling it will not lead to desired behavior. ... Deriving a new control as in your answer, although it doesn't satisfy the actual question, is what should be done.Anglo
S
10

You can use a TCheckbox or a TRadioButton to have an appearance of a Button with the BS_PUSHLIKE style.

Makes a button (such as a check box, three-state check box, or radio button) look and act like a push button. The button looks raised when it isn't pushed or checked, and sunken when it is pushed or checked.

Both TCheckBox and TRadioButton are actually sub-classed from the standard Windows BUTTON control. (This will give a toggle button behavior similar to .net CheckBox with Appearance set to Button - see: Do we have Button down property as Boolean).

type
  TButtonCheckBox = class(StdCtrls.TCheckBox)
  protected
    procedure CreateParams(var Params: TCreateParams); override;
  end;

procedure TButtonCheckBox.CreateParams(var Params: TCreateParams);
begin
  inherited CreateParams(Params);
  Params.Style := Params.Style or BS_PUSHLIKE;
end;

Set the Checked property to make it pressed or not.

To set an image list use Button_SetImageList macro (which sends a BCM_SETIMAGELIST message to the button control) e.g.:

uses CommCtrl;
...
procedure TButtonCheckBox.SetImages(const Value: TCustomImageList);    
var
  LButtonImageList: TButtonImageList;
begin
  LButtonImageList.himl := Value.Handle;
  LButtonImageList.uAlign := BUTTON_IMAGELIST_ALIGN_LEFT;
  LButtonImageList.margin := Rect(4, 0, 0, 0);
  Button_SetImageList(Handle, LButtonImageList);
  Invalidate;
end;

Note: To use this macro, you must provide a manifest specifying Comclt32.dll version 6.0

Each TButton uses it's own internal image list (FInternalImageList) that holds 5 images for each button state (ImageIndex, HotImageIndex, ...). So when you assign an ImageIndex or HotImageIndex etc, it rebuilds that internal image list, and uses that. If only one image is present, it is used for all states. If needed, see source TCustomButton.UpdateImages to learn how it's done, and apply the same logic for your TButtonCheckBox.


Actually the inverse method could be easily applied directly to a TButton by turning it into a "check box" using BS_PUSHLIKE + BS_CHECKBOX styles, and omitting the BS_PUSHBUTTON style completely. I borrowed a bit of code from TCheckBox and used an interposer class for demo:

type
  TButton = class(StdCtrls.TButton)
  private
    FChecked: Boolean;
    FPushLike: Boolean;
    procedure SetPushLike(Value: Boolean);
    procedure Toggle;
    procedure CNCommand(var Message: TWMCommand); message CN_COMMAND;
  protected
    procedure SetButtonStyle(ADefault: Boolean); override;
    procedure CreateParams(var Params: TCreateParams); override;
    procedure CreateWnd; override;

    function GetChecked: Boolean; override;
    procedure SetChecked(Value: Boolean); override;
  published
    property Checked;
    property PushLike: Boolean read FPushLike write SetPushLike;
  end;

implementation

procedure TButton.SetButtonStyle(ADefault: Boolean);
begin
  if not FPushLike then inherited;
  { Else, do nothing - avoid setting style to BS_PUSHBUTTON }
end;

procedure TButton.CreateParams(var Params: TCreateParams);
begin
  inherited CreateParams(Params);
  if FPushLike then
  begin
    Params.Style := Params.Style or BS_PUSHLIKE  or BS_CHECKBOX;
    Params.WindowClass.style := Params.WindowClass.style and not (CS_HREDRAW or CS_VREDRAW);
  end;
end;

procedure TButton.CreateWnd;
begin
  inherited CreateWnd;
  if FPushLike then
    SendMessage(Handle, BM_SETCHECK, Integer(FChecked), 0);
end;

procedure TButton.CNCommand(var Message: TWMCommand);
begin
  if FPushLike and (Message.NotifyCode = BN_CLICKED) then
    Toggle
  else
    inherited;
end;

procedure TButton.Toggle;
begin
  Checked := not FChecked;
end;

function TButton.GetChecked: Boolean;
begin
  Result := FChecked;
end;

procedure TButton.SetChecked(Value: Boolean);
begin
  if FChecked <> Value then
  begin
    FChecked := Value;
    if FPushLike then
    begin
      if HandleAllocated then
        SendMessage(Handle, BM_SETCHECK, Integer(Checked), 0);
      if not ClicksDisabled then Click;
    end;
  end;
end;

procedure TButton.SetPushLike(Value: Boolean);
begin
  if Value <> FPushLike then
  begin
    FPushLike := Value;
    RecreateWnd;
  end;
end;

Now if you set PushLike property to True, you can use the Checked property to toggle the button state.

Shakespeare answered 26/10, 2017 at 9:45 Comment(1)
That's exactly what I needed and works great in 10.1 Berlin. I'll check it in D2010 by tomorrow. Thanks a lot for the detailed answer.Gurtner
G
3

This is just a modification to kobik's detailed answer. I added GroupIndex property to make a group of buttons work together (let only one of them stay down at a time when GroupIndex <> 0). Such facility was not even asked in the question, but I thought people who come here in the future may need it soon after, just like I did. I also removed PushLike property and assumed it to be True by default, since I named it the TToggleButton after all.

uses
  Winapi.Windows, Vcl.StdCtrls, Winapi.Messages, Vcl.Controls, Vcl.ActnList;

type
  TToggleButton = class(TButton)
  private
    FChecked: Boolean;
    FGroupIndex: Integer;
    procedure Toggle;
    procedure CNCommand(var Message: TWMCommand); message CN_COMMAND;
    procedure SetGroupIndex(const Value: Integer);
    procedure TurnSiblingsOff;
  protected
    procedure SetButtonStyle(ADefault: Boolean); override;
    procedure CreateParams(var Params: TCreateParams); override;
    procedure CreateWnd; override;

    function GetChecked: Boolean; override;
    procedure SetChecked(Value: Boolean); override;
  published
    property Checked;
    property GroupIndex: Integer read FGroupIndex write SetGroupIndex;
  end;

implementation

 { TToggleButton}

procedure TToggleButton.SetButtonStyle(ADefault: Boolean);
begin
  { do nothing - avoid setting style to BS_PUSHBUTTON }
end;

procedure TToggleButton.CreateParams(var Params: TCreateParams);
begin
  inherited CreateParams(Params);
  Params.Style := Params.Style or BS_PUSHLIKE  or BS_CHECKBOX;
  Params.WindowClass.style := Params.WindowClass.style and not (CS_HREDRAW or CS_VREDRAW);
end;

procedure TToggleButton.CreateWnd;
begin
  inherited CreateWnd;
  SendMessage(Handle, BM_SETCHECK, Integer(FChecked), 0);
end;

procedure TToggleButton.CNCommand(var Message: TWMCommand);
begin
  if Message.NotifyCode = BN_CLICKED then
    Toggle
  else
    inherited;
end;

procedure TToggleButton.Toggle;
begin
  Checked := not FChecked;
end;

function TToggleButton.GetChecked: Boolean;
begin
  Result := FChecked;
end;

procedure TToggleButton.SetChecked(Value: Boolean);
begin
  if FChecked <> Value then
  begin
    FChecked := Value;
    if HandleAllocated then
      SendMessage(Handle, BM_SETCHECK, Integer(Checked), 0);
    if Value then
      TurnSiblingsOff;
    if not ClicksDisabled then Click;
  end;
end;

procedure TToggleButton.SetGroupIndex(const Value: Integer);
begin
  FGroupIndex := Value;
  if Checked then
    TurnSiblingsOff;
end;

procedure TToggleButton.TurnSiblingsOff;
var
  I: Integer;
  Sibling: TControl;
begin
  if (Parent <> nil) and (GroupIndex <> 0) then
    with Parent do
      for I := 0 to ControlCount - 1 do
      begin
        Sibling := Controls[I];
        if (Sibling <> Self) and (Sibling is TToggleButton) then
          with TToggleButton(Sibling) do
            if GroupIndex = Self.GroupIndex then
            begin
              if Assigned(Action) and
                 (Action is TCustomAction) and
                 TCustomAction(Action).AutoCheck then
                TCustomAction(Action).Checked := False;
              SetChecked(False);
            end;
      end;
end;

The TurnSiblingsOff method is borrowed from TRadioButton.

Gurtner answered 27/10, 2017 at 8:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.