Friday, December 26, 2008

Update of StickyButton

In my previous entry I gave code to create a TButton descendant that emulates a TSpeedButton, but can accept 32 bit images from TImageLists (as TButtons do in Delphi 2009, but in contrast to TSpeedButtons). I mentioned that the new component does not automatically change state according to its Action's Checked property (if it is indeed associated with an Action). It turns out that this can be very easily corrected. Just add the following two procedures:

function TdhStickyButton.GetChecked: Boolean;
begin
Result := Down;
end;

procedure TdhStickyButton.SetChecked(Value: Boolean);
begin
Down := Value;
end;

The declaration section now becomes:

type
TdhStickyButton = class(TButton)
private
FDDown: Boolean;
FJustKilledWhenDown: Boolean;
procedure WMKillFocus(var Message: TWMKillFocus); message WM_KILLFOCUS;
procedure CNCommand(var Message: TWMCommand); message CN_COMMAND;
procedure SetDownState(Value: Boolean);
protected
function GetChecked: Boolean; override;
procedure SetChecked(Value: Boolean); override;
public
constructor Create(AOwner: TComponent); override;
published
property Down: Boolean read FDDown write SetDownState default false;
end;

Thursday, December 25, 2008

Delphi 2009

Installed Delphi 2009 a week ago and have been trying to update my code. The big change is Unicode support. All strings are now Unicode strings by default so there are a lot of instances that need reassigning to AnsiString. That was not too difficult.

My next attempt was to use 32 bit images in an ImageList. Not as easy as expected. I tried to load PNG images but the transparency did not come out very well. There was a faint grey border around the images, as if the semi-transparent pixels were grey instead of semi-transparent (fully transparent pixels were OK). After many hours of experimenting I found out that 32 bit bitmaps work perfectly. So now I use Inkscape to draw the icons, export to a PNG file, then use Pixelformer to convert from PNG to 32 bit BMP (using A8R8G8B8 format), then load the BMPs into the TImageList (which is set to 32 bits) and assign clNone as the transparent color. Pixelformer is a nice painting program that handles transparency very well and is perfect for this job.

The alpha-channel images can be used on TButtons as well. Unfortunately, they do not work on TSpeedButtons. So I thought of changing all TSpeedButtons to TButtons, but TButtons do not have a Down state. I searched the web and found out I could use the BN_SETSTATE message to make a button seem depressed. The problem is, the button does not stay depressed: as soon as you move the focus away, the button pops up again. This strange phenomenon was partly explained by Dennis Martin in this web page. It seems that a Click event is fired when the button looses focus. Dennis has a solution, but I wanted to create a component to emulate a TSpeedButton. I came up with a different way. Here is the code (feel free to use and share):

//TdhStickyButton, stays down when Down is true.
//intercepts Kill_Focus message to set a flag that shows
// that it is down and was just killed (lost focus).
//intercepts WM_Command, which receives a Click message,
// and only sends it along if it was not just killed in a down state.
// also, resets temporary flag and sets the Down state appropriately
// (otherwise it reverts automatically).
type
TdhStickyButton = class(TButton)
private
FDDown: Boolean;
FJustKilledWhenDown: Boolean;
procedure WMKillFocus(var Message: TWMKillFocus); message WM_KILLFOCUS;
procedure CNCommand(var Message: TWMCommand); message CN_COMMAND;
procedure SetDownState(Value: Boolean);
protected
public
constructor Create(AOwner: TComponent); override;
published
property Down: Boolean read FDDown write SetDownState default false;
end;

procedure Register;

implementation

procedure Register;
begin
RegisterComponents('Additional', [TdhStickyButton]);
end;

constructor TdhStickyButton.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FDDown := false;
end;

procedure TdhStickyButton.WMKillFocus(var Message: TWMKillFocus);
begin
if FDDown then FJustKilledWhenDown := true else FJustKilledWhenDown := false;
inherited;
end;

procedure TdhStickyButton.CNCommand(var Message: TWMCommand);
begin
if (Message.NotifyCode = BN_CLICKED) and not(FJustKilledWhenDown) then Click;
SendMessage(self.Handle, BM_SETSTATE, Longint(FDDown), 0);
FJustKilledWhenDown := false;
end;

procedure TdhStickyButton.SetDownState(Value: Boolean);
begin
FDDown := Value;
SendMessage(self.Handle, BM_SETSTATE, Longint(FDDown), 0);
end;

A StickyButton is used like a regular button. If you want to show it down, just set its 'Down' property to true. The button will not toggle between down and up automatically, you have to do it with code. Also, if you attach an Action to the button, it will not toggle together with the Action's 'checked' property, as SpeedButtons do; again, you have to do it with code.