How to modernize the look of the TJvCaptionPanel buttons? - delphi

I use a TJvCaptionPanel in Delphi 10.4 to show a panel with a caption and buttons:
(TJvCaptionPanel is part of the OpenSource JEDI Visual Component Library available from GetIt)
This is the object code of the JvCaptionPanel1 object instance, so you can paste it in the Form Designer:
object JvCaptionPanel1: TJvCaptionPanel
Left = 560
Top = 79
Width = 210
Height = 306
Align = alRight
Buttons = [capClose, capHelp]
Caption = 'My Test Caption'
CaptionPosition = dpTop
CaptionFont.Charset = DEFAULT_CHARSET
CaptionFont.Color = clWhite
CaptionFont.Height = -13
CaptionFont.Name = 'Tahoma'
CaptionFont.Style = [fsBold]
FlatButtons = True
Icon.Data = {
0000010001001010000001002000680400001600000028000000100000002000
0000010020000000000040040000000000000000000000000000000000000000
00000000000000000000777777A4777777E07777773177777763777777887777
7788777777617777772D777777DF777777A80000000000000000000000000000
00000000000000000000777777C5787878FE7F7F7FFDA9A9A9FDC0C0C0FDBFBF
BFFDA8A8A8FD7F7F7FFD787878FE777777C90000000000000000000000000000
0000000000007676760E777777CBABABABFDF4F4F4FDFDFDFDFDFDFDFDFDFDFD
FDFDFDFDFDFDF4F4F4FDA9A9A9FD777777C77777770C00000000000000000000
000000000000777777A8B0B0B0FDFCFCFCFDFDFDFDFDFDFDFDFDFDFDFDFDFDFD
FDFDFDFDFDFDEADCCEFCF2EAE2FCAEAEAEFD777777A300000000000000000000
000076767635898989FDF9F9F9FDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFD
FDFDDDC4ACFCCDA782FCFCFCFCFCF8F8F8FD888888FD77777730000000000000
00007777778EBEBEBEFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFCFCFCFCD5B5
96FCBE8D5CFCF9F6F3FCFDFDFDFDFDFDFDFDBCBCBCFD77777789000000000000
0000777777BDDBDBDBFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDD7B99BFCB883
4DFCF3EBE3FCFDFDFDFDFDFDFDFDFDFDFDFDD9D9D9FD777777B7000000000000
0000777777C6E0E0E0FDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDC59A6FFCDEC7
AFFCFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDDEDEDEFD777777C1000000000000
0000777777AACFCFCFFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDCAA37BFCE4D0
BDFCFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDCCCCCCFD777777A4000000000000
000077777766A4A4A4FDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDD1AF8DFCE8D8
C8FCFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDA2A2A2FD77777761000000000000
00007372710C797979E7DFDFDFFDFDFDFDFDFDFDFDFDFDFDFDFDDBC2A8FCF0E7
DEFCFDFDFDFDFDFDFDFDFDFDFDFDDDDDDDFD787878E378767509000000000000
0000A47547088376694C848484FBE5E5E5FCFDFDFDFCFDFDFDFCF1E8E0FCFBFA
F9FCFDFDFDFCFDFDFDFCE4E4E4FC838383FA80766C47A2764A08000000000000
0000B1773C9FA7764445827568557B7B7BF1B6B6B6FCE8E8E8FCFCFCFCFCFCFC
FCFCE7E7E7FCB5B5B5FC7B7B7BF07E756B51A476483DB1773CA8000000000000
0000B1773CB7B1773CF5AB77425F8F765C1D7B76718F777676DD7A7A7AFC7A7A
7AFC767676DC7A76728D8C755F1BA8764457B1773DF2B1773CCA000000000000
0000B1763B3CB1773CF5B2783DFEB1773DC2A9774357000000005D5D5D065D5D
5D0600000000A8774552B0773DBCB2783DFEB2783DFCB1773C56000000000000
000000000000B1773B2FB1773CBAB1773CE6B1773C6F00000000000000000000
000000000000B1773C68B1773CEFB1773CD1B1763B470000000000000000E007
0000E0070000C0030000C0030000800100008001000080010000800100008001
0000800100008001000080010000800100008001000082410000C3C30000}
OutlookLook = False
Resizable = False
TabOrder = 2
OnButtonClick = JvCaptionPanel1ButtonClick
end
Although JvCaptionPanel1.FlatButtons = True, as you can see from the above screenshot, the buttons have an old-fashioned "stone-age" "Atari" pixelized look:
This contrasts with the otherwise modern look of my application.
Is it possible to "modernize" the look of the buttons to make them appear more "modern"? How could this be done?

If I were you, I'd create a custom control:
unit PanelCaption;
interface
uses
Windows, Messages, SysUtils, Types, UITypes, Classes, Graphics, Controls,
StdCtrls, Forms;
type
TPanelCaption = class(TCustomControl)
private
FTextColor: TColor;
FCloseBtnHot: Boolean;
FCloseBtnDown: Boolean;
FCloseBtnClicked: TNotifyEvent;
procedure SetTextColor(const Value: TColor);
function CloseBtnRect: TRect;
procedure DoCloseBtnClicked;
protected
procedure Paint; override;
procedure Resize; override;
procedure MouseMove(Shift: TShiftState; X: Integer; Y: Integer); override;
procedure CMTextChanged(var Message: TMessage); message CM_TEXTCHANGED;
procedure CMMouseLeave(var Message: TMessage); message CM_MOUSELEAVE;
procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X: Integer;
Y: Integer); override;
procedure MouseUp(Button: TMouseButton; Shift: TShiftState; X: Integer;
Y: Integer); override;
public
constructor Create(AOwner: TComponent); override;
published
property Align;
property Anchors;
property BiDiMode;
property Caption;
property Color;
property TextColor: TColor read FTextColor write SetTextColor;
property Constraints;
property Ctl3D;
property DockSite;
property DoubleBuffered;
property DragCursor;
property DragKind;
property DragMode;
property Enabled;
property Font;
property Padding;
property ParentBackground;
property ParentBiDiMode;
property ParentColor;
property ParentCtl3D;
property ParentDoubleBuffered;
property ParentFont default True;
property ParentShowHint;
property PopupMenu;
property ShowHint;
property TabOrder;
property TabStop;
property Touch;
property Visible;
property StyleElements;
property OnAlignInsertBefore;
property OnAlignPosition;
property OnClick;
property OnCloseBtnClick: TNotifyEvent read FCloseBtnClicked write FCloseBtnClicked;
property OnContextPopup;
property OnDblClick;
property OnDragDrop;
property OnDockDrop;
property OnDockOver;
property OnDragOver;
property OnEndDock;
property OnEndDrag;
property OnEnter;
property OnExit;
property OnGesture;
property OnGetSiteInfo;
property OnMouseActivate;
property OnMouseDown;
property OnMouseEnter;
property OnMouseLeave;
property OnMouseMove;
property OnMouseUp;
property OnStartDock;
property OnStartDrag;
property OnUnDock;
end;
procedure Register;
implementation
uses
Math;
function Scale(X: Integer): Integer;
begin
Result := MulDiv(X, Screen.PixelsPerInch, 96);
end;
{ TPanelCaption }
function TPanelCaption.CloseBtnRect: TRect;
begin
Result := Rect(ClientWidth - ClientHeight, 0, ClientWidth, ClientHeight);
end;
procedure TPanelCaption.CMMouseLeave(var Message: TMessage);
begin
if FCloseBtnHot or FCloseBtnDown then
begin
FCloseBtnHot := False;
FCloseBtnDown := False;
InvalidateRect(Handle, CloseBtnRect, False);
end;
end;
procedure TPanelCaption.CMTextChanged(var Message: TMessage);
begin
inherited;
Invalidate;
end;
constructor TPanelCaption.Create(AOwner: TComponent);
begin
inherited;
Color := clActiveCaption;
FTextColor := clCaptionText;
end;
procedure TPanelCaption.DoCloseBtnClicked;
begin
if Assigned(FCloseBtnClicked) then
FCloseBtnClicked(Self);
end;
procedure TPanelCaption.MouseDown(Button: TMouseButton; Shift: TShiftState; X,
Y: Integer);
begin
inherited;
if Button = mbLeft then
begin
FCloseBtnDown := CloseBtnRect.Contains(Point(X, Y));
if FCloseBtnDown then
InvalidateRect(Handle, CloseBtnRect, False);
end;
end;
procedure TPanelCaption.MouseMove(Shift: TShiftState; X, Y: Integer);
var
LCloseButtonHot: Boolean;
begin
LCloseButtonHot := CloseBtnRect.Contains(Point(X, Y));
if LCloseButtonHot <> FCloseBtnHot then
begin
FCloseBtnHot := LCloseButtonHot;
InvalidateRect(Handle, CloseBtnRect, False);
end;
end;
procedure TPanelCaption.MouseUp(Button: TMouseButton; Shift: TShiftState; X,
Y: Integer);
begin
inherited;
if FCloseBtnDown then
begin
FCloseBtnDown := False;
InvalidateRect(Handle, CloseBtnRect, False);
if CloseBtnRect.Contains(Point(X, Y)) then
DoCloseBtnClicked;
end;
end;
procedure GetActualTextHeight(DC: HDC; out H: Integer);
var
m: TTextMetric;
begin
if GetTextMetrics(DC, m) then
H := m.tmHeight - m.tmDescent - m.tmExternalLeading - m.tmInternalLeading
else
H := Scale(20);
end;
procedure TPanelCaption.Paint;
var
R: TRect;
S: string;
XHeight: Integer;
SizeReduction: Integer;
begin
inherited;
Canvas.Brush.Color := Color;
Canvas.Font.Assign(Font);
Canvas.Font.Color := FTextColor;
R := ClientRect;
Dec(R.Right, ClientHeight);
Canvas.FillRect(R);
S := #32 + Caption;
Canvas.TextRect(R, S, [tfSingleLine, tfLeft, tfVerticalCenter, tfEndEllipsis]);
R := CloseBtnRect;
Canvas.Brush.Color := IfThen(FCloseBtnHot, IfThen(FCloseBtnDown, clBlack, clWhite), Color);
Canvas.FillRect(R);
GetActualTextHeight(Canvas.Handle, XHeight);
SizeReduction := R.Height - XHeight;
if SizeReduction > 0 then
R.Inflate(-SizeReduction div 2, -SizeReduction div 2);
Canvas.Pen.Color := IfThen(FCloseBtnHot, IfThen(FCloseBtnDown, clWhite, clBlack), Font.Color);
Canvas.Pen.Width := Scale(2);
Canvas.MoveTo(R.Left, R.Top);
Canvas.LineTo(R.Right, R.Bottom);
Canvas.MoveTo(R.Right, R.Top);
Canvas.LineTo(R.Left, R.Bottom);
end;
procedure TPanelCaption.Resize;
begin
inherited;
Invalidate;
end;
procedure TPanelCaption.SetTextColor(const Value: TColor);
begin
if FTextColor <> Value then
begin
FTextColor := Value;
Invalidate;
end;
end;
procedure Register;
begin
RegisterComponents('Rejbrand 2020', [TPanelCaption]);
end;
end.
Here I chose to implement the close button manually in code. It wouldn't be any more difficult to use a TSpeedButton control instead. In fact, it would be simpler, but then you wouldn't get full control over its appearance and behaviour.

Related

Is there a TShape control based on a TWinControl

Is there an equivalent control to TShape which will act as a container? I.e. derived from a WinControl
Not in the native VCL, no. The closest would be a TPanel with a custom-painted background. Otherwise, you will have to write your own (or find a third-party one) that derives from TCustomControl or TWinControl directly.
unit WinShape_U;
interface
uses
System.SysUtils, System.Classes, Vcl.Controls,
Winapi.Windows, Vcl.Graphics, Vcl.ExtCtrls, Winapi.Messages;
type
TWinShape = class(TWinControl)
private
{ Private declarations }
FPen: TPen;
FBrush: TBrush;
FShape: TShapeType;
FCanvas: TCanvas;
procedure SetBrush(Value: TBrush);
procedure SetPen(Value: TPen);
procedure SetShape(Value: TShapeType);
protected
{ Protected declarations }
property Canvas: TCanvas read FCanvas;
procedure ChangeScale(M, D: Integer; isDpiChange: Boolean); override;
procedure Paint;
procedure CreateParams(var Params: TCreateParams); override;
procedure WMEraseBkgnd(var Msg: TWMEraseBkgnd); message WM_ERASEBKGND;
procedure WMPaint(var Message: TWMPaint); message WM_PAINT;
public
{ Public declarations }
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
published
{ Published declarations }
procedure StyleChanged(Sender: TObject);
property Align;
property Anchors;
property Brush: TBrush read FBrush write SetBrush;
property DragCursor;
property DragKind;
property DragMode;
property Enabled;
property Constraints;
property ParentShowHint;
property Pen: TPen read FPen write SetPen;
property Shape: TShapeType read FShape write SetShape default stRectangle;
property ShowHint;
property Touch;
property Visible;
property OnContextPopup;
property OnDragDrop;
property OnDragOver;
property OnEndDock;
property OnEndDrag;
property OnMouseActivate;
property OnMouseDown;
property OnMouseEnter;
property OnMouseLeave;
property OnMouseMove;
property OnMouseUp;
property OnGesture;
property OnStartDock;
property OnStartDrag;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('Additional', [TWinShape]);
end;
{ TShape }
procedure TWinShape.ChangeScale(M, D: Integer; isDpiChange: Boolean);
begin
FPen.Width := MulDiv(FPen.Width, M, D);
inherited;
end;
constructor TWinShape.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FCanvas := TControlCanvas.Create;
TControlCanvas(FCanvas).Control := Self;
ControlStyle := ControlStyle + [csReplicatable];
Width := 65;
Height := 65;
FPen := TPen.Create;
FPen.OnChange := StyleChanged;
FBrush := TBrush.Create;
FBrush.OnChange := StyleChanged;
end;
procedure TWinShape.CreateParams(var Params: TCreateParams);
begin
inherited CreateParams(Params);
Params.ExStyle := Params.ExStyle or WS_EX_TRANSPARENT;
end;
destructor TWinShape.Destroy;
begin
FPen.Free;
FBrush.Free;
FCanvas.Free;
inherited Destroy;
end;
procedure TWinShape.Paint;
var
X, Y, W, H, S: Integer;
begin
with Canvas do
begin
Pen := FPen;
Brush := FBrush;
X := Pen.Width div 2;
Y := X;
W := Width - Pen.Width + 1;
H := Height - Pen.Width + 1;
if Pen.Width = 0 then
begin
Dec(W);
Dec(H);
end;
if W < H then S := W else S := H;
if FShape in [stSquare, stRoundSquare, stCircle] then
begin
Inc(X, (W - S) div 2);
Inc(Y, (H - S) div 2);
W := S;
H := S;
end;
case FShape of
stRectangle, stSquare:
Rectangle(X, Y, X + W, Y + H);
stRoundRect, stRoundSquare:
RoundRect(X, Y, X + W, Y + H, S div 4, S div 4);
stCircle, stEllipse:
Ellipse(X, Y, X + W, Y + H);
end;
end;
end;
procedure TWinShape.SetBrush(Value: TBrush);
begin
FBrush.Assign(Value);
end;
procedure TWinShape.SetPen(Value: TPen);
begin
FPen.Assign(Value);
end;
procedure TWinShape.SetShape(Value: TShapeType);
begin
if FShape <> Value then
begin
FShape := Value;
Invalidate;
end;
end;
procedure TWinShape.StyleChanged(Sender: TObject);
begin
Invalidate;
end;
procedure TWinShape.WMEraseBkgnd(var Msg: TWMEraseBkgnd);
begin
Msg.Result := 1;
end;
procedure TWinShape.WMPaint(var Message: TWMPaint);
begin
if not (csDestroying in ComponentState) then
begin
Canvas.Lock;
try
Paint;
finally
Canvas.Unlock;
end;
end;
end;
end.

How best to create a TPanel with a close 'cross' button in the top right?

There are several third-pary controls (such as the Raize Components) which have a close 'cross' button 'option' (eg the page control). My requirement is simpler, I'd like to plonk a cross 'button' aligned top right on to a TPanel and access its clicked event. Is there either a simple way of doint this without creating a TPanel descendent, or is there a paid or free library component that I can use?
I wrote a control for you.
unit CloseButton;
interface
uses
Windows, Messages, SysUtils, Classes, Controls, UxTheme;
type
TCloseButton = class(TCustomControl)
private
FMouseInside: boolean;
function MouseButtonDown: boolean;
protected
procedure Paint; override;
procedure MouseMove(Shift: TShiftState; X: Integer; Y: Integer); override;
procedure WndProc(var Message: TMessage); override;
procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X: Integer;
Y: Integer); override;
procedure MouseUp(Button: TMouseButton; Shift: TShiftState; X: Integer;
Y: Integer); override;
public
constructor Create(AOwner: TComponent); override;
published
property Align;
property Anchors;
property Enabled;
property OnClick;
property OnMouseUp;
property OnMouseDown;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('Rejbrand 2009', [TCloseButton]);
end;
{ TCloseButton }
constructor TCloseButton.Create(AOwner: TComponent);
begin
inherited;
Width := 32;
Height := 32;
end;
function TCloseButton.MouseButtonDown: boolean;
begin
MouseButtonDown := GetKeyState(VK_LBUTTON) and $8000 <> 0;
end;
procedure TCloseButton.MouseDown(Button: TMouseButton; Shift: TShiftState; X,
Y: Integer);
begin
inherited;
Invalidate;
end;
procedure TCloseButton.MouseMove(Shift: TShiftState; X, Y: Integer);
begin
inherited;
if not FMouseInside then
begin
FMouseInside := true;
Invalidate;
end;
end;
procedure TCloseButton.MouseUp(Button: TMouseButton; Shift: TShiftState; X,
Y: Integer);
begin
inherited;
Invalidate;
end;
procedure TCloseButton.Paint;
function GetAeroState: cardinal;
begin
result := CBS_NORMAL;
if not Enabled then
result := CBS_DISABLED
else
if FMouseInside then
if MouseButtonDown then
result := CBS_PUSHED
else
result := CBS_HOT;
end;
function GetClassicState: cardinal;
begin
result := 0;
if not Enabled then
result := DFCS_INACTIVE
else
if FMouseInside then
if MouseButtonDown then
result := DFCS_PUSHED
else
result := DFCS_HOT;
end;
var
h: HTHEME;
begin
inherited;
if UseThemes then
begin
h := OpenThemeData(Handle, 'WINDOW');
if h <> 0 then
try
DrawThemeBackground(h,
Canvas.Handle,
WP_CLOSEBUTTON,
GetAeroState,
ClientRect,
nil);
finally
CloseThemeData(h);
end;
end
else
DrawFrameControl(Canvas.Handle,
ClientRect,
DFC_CAPTION,
DFCS_CAPTIONCLOSE or GetClassicState)
end;
procedure TCloseButton.WndProc(var Message: TMessage);
begin
inherited;
case Message.Msg of
WM_MOUSELEAVE:
begin
FMouseInside := false;
Invalidate;
end;
CM_ENABLEDCHANGED:
Invalidate;
end;
end;
end.
Sample (with and without themes enabled):
Just put this in a TPanel at the top-right corner and set Anchors to top and right.
I'm sure you can find a ton of such a components available for free from Torry's or any other similar site... however, if you only need such a feature on a single panel, then drop an button onto panel, anchor it to top-right corner and youre done. If you also want to have "caption area" on that panel, then it might be bit more work...
BTW if you have JVCL installed then you already have such a component installed - it is called TjvCaptionPanel or similar.
And if you (or anyone else) want a finished TClosePanel (with the added optional functionality to propagate the Enabled property down through the contained controls), I have written one for you:
unit ClosePanel;
interface
USES Windows, Messages, SysUtils, Classes, Controls, ExtCtrls, UxTheme, CloseButton;
TYPE
TPosition = (posCustom,posTopLeft,posTopCenter,posTopRight,posMiddleRight,posBottomRight,posbottomCenter,posBottomLeft,posMiddleLeft,posCenter);
TEnableState = RECORD
CTRL : TControl;
State : BOOLEAN
END;
TClosePanel = CLASS(TCustomPanel)
CONSTRUCTOR Create(AOwner : TComponent); OVERRIDE;
PRIVATE
FCloseBtn : TCloseButton;
FPosition : TPosition;
States : ARRAY OF TEnableState;
FAutoEnable : BOOLEAN;
PROTECTED
PROCEDURE SetEnabled(Value : BOOLEAN); OVERRIDE;
PROCEDURE SetParent(Parent : TWinControl); OVERRIDE;
PROCEDURE SetPosition(Value : TPosition); VIRTUAL;
PROCEDURE MoveCloseButton; VIRTUAL;
PROCEDURE WMWindowPosChanged(VAR Message : TWMWindowPosChanged); MESSAGE WM_WINDOWPOSCHANGED;
FUNCTION GetOnClose: TNotifyEvent; VIRTUAL;
PROCEDURE SetOnClose(Value : TNotifyEvent); VIRTUAL;
PUBLIC
PROPERTY DockManager;
PUBLISHED
PROPERTY Align;
PROPERTY Alignment;
PROPERTY Anchors;
PROPERTY AutoSize;
PROPERTY AutoEnable : BOOLEAN read FAutoEnable write FAutoEnable default TRUE;
PROPERTY BevelEdges;
PROPERTY BevelInner;
PROPERTY BevelKind;
PROPERTY BevelOuter;
PROPERTY BevelWidth;
PROPERTY BiDiMode;
PROPERTY BorderWidth;
PROPERTY BorderStyle;
PROPERTY Caption;
PROPERTY CloseBtn : TCloseButton read FCloseBtn write FCloseBtn;
PROPERTY Color;
PROPERTY Constraints;
PROPERTY Ctl3D;
PROPERTY UseDockManager default True;
PROPERTY DockSite;
PROPERTY DragCursor;
PROPERTY DragKind;
PROPERTY DragMode;
PROPERTY Enabled;
PROPERTY FullRepaint;
PROPERTY Font;
PROPERTY Locked;
PROPERTY Padding;
PROPERTY ParentBiDiMode;
PROPERTY ParentBackground;
PROPERTY ParentColor;
PROPERTY ParentCtl3D;
PROPERTY ParentFont;
PROPERTY ParentShowHint;
PROPERTY PopupMenu;
PROPERTY Position : TPosition read FPosition write SetPosition default posTopRight;
PROPERTY ShowHint;
PROPERTY TabOrder;
PROPERTY TabStop;
PROPERTY VerticalAlignment;
PROPERTY Visible;
PROPERTY OnAlignInsertBefore;
PROPERTY OnAlignPosition;
PROPERTY OnCanResize;
PROPERTY OnClick;
PROPERTY OnClose : TNotifyEvent read GetOnClose write SetOnClose;
PROPERTY OnConstrainedResize;
PROPERTY OnContextPopup;
PROPERTY OnDockDrop;
PROPERTY OnDockOver;
PROPERTY OnDblClick;
PROPERTY OnDragDrop;
PROPERTY OnDragOver;
PROPERTY OnEndDock;
PROPERTY OnEndDrag;
PROPERTY OnEnter;
PROPERTY OnExit;
PROPERTY OnGetSiteInfo;
PROPERTY OnMouseActivate;
PROPERTY OnMouseDown;
PROPERTY OnMouseEnter;
PROPERTY OnMouseLeave;
PROPERTY OnMouseMove;
PROPERTY OnMouseUp;
PROPERTY OnResize;
PROPERTY OnStartDock;
PROPERTY OnStartDrag;
PROPERTY OnUnDock;
END;
PROCEDURE Register;
IMPLEMENTATION
PROCEDURE Register;
BEGIN
RegisterComponents('HeartWare', [TClosePanel]);
END;
TYPE
TMyCloseBtn = CLASS(TCloseButton)
CONSTRUCTOR Create(AOwner : TComponent); OVERRIDE;
PROTECTED
PROCEDURE WMWindowPosChanged(VAR Message : TWMWindowPosChanged); MESSAGE WM_WINDOWPOSCHANGED;
PRIVATE
SaveW : INTEGER;
SaveH : INTEGER;
SaveX : INTEGER;
SaveY : INTEGER;
END;
{ TClosePanel }
CONSTRUCTOR TClosePanel.Create(AOwner : TComponent);
BEGIN
INHERITED Create(AOwner);
FAutoEnable:=TRUE;
FCloseBtn:=TMyCloseBtn.Create(Self);
FCloseBtn.Name:='CloseButton';
FCloseBtn.Tag:=1
END;
FUNCTION TClosePanel.GetOnClose : TNotifyEvent;
BEGIN
Result:=CloseBtn.OnClick
END;
PROCEDURE TClosePanel.MoveCloseButton;
PROCEDURE SetPos(ModeX,ModeY : INTEGER);
PROCEDURE SetLeft(Value : INTEGER);
BEGIN
IF FCloseBtn.Left<>Value THEN FCloseBtn.Left:=Value
END;
PROCEDURE SetTop(Value : INTEGER);
BEGIN
IF FCloseBtn.Top<>Value THEN FCloseBtn.Top:=Value
END;
BEGIN
CASE ModeX OF
-1 : SetLeft(0);
0 : SetLeft((ClientWidth-FCloseBtn.Width) DIV 2);
1 : SetLeft(ClientWidth-FCloseBtn.Width)
END;
CASE ModeY OF
-1 : SetTop(0);
0 : SetTop((ClientHeight-FCloseBtn.Height) DIV 2);
1 : SetTop(ClientHeight-FCloseBtn.Height)
END
END;
BEGIN
CASE FPosition OF
posTopLeft : SetPos(-1,-1);
posTopCenter : SetPos(0,-1);
posTopRight : SetPos(1,-1);
posMiddleRight : SetPos(1,0);
posBottomRight : SetPos(1,1);
posbottomCenter : SetPos(0,1);
posBottomLeft : SetPos(-1,1);
posMiddleLeft : SetPos(-1,0);
posCenter : SetPos(0,0)
END
END;
PROCEDURE TClosePanel.SetEnabled(Value : BOOLEAN);
PROCEDURE Enable;
VAR
REC : TEnableState;
BEGIN
FOR REC IN States DO REC.CTRL.Enabled:=REC.State;
SetLength(States,0)
END;
PROCEDURE Disable;
VAR
I : Cardinal;
CMP : TComponent;
REC : TEnableState;
BEGIN
SetLength(States,0);
FOR I:=1 TO ComponentCount DO BEGIN
CMP:=Components[PRED(I)];
IF CMP IS TControl THEN BEGIN
REC.CTRL:=CMP AS TControl;
REC.State:=REC.CTRL.Enabled;
REC.CTRL.Enabled:=FALSE;
SetLength(States,SUCC(LENGTH(States)));
States[HIGH(States)]:=REC
END
END
END;
BEGIN
IF AutoEnable THEN
IF Value THEN Enable ELSE Disable;
FCloseBtn.Enabled:=Value;
INHERITED SetEnabled(Value)
END;
PROCEDURE TClosePanel.SetOnClose(Value : TNotifyEvent);
BEGIN
FCloseBtn.OnClick:=Value
END;
PROCEDURE TClosePanel.SetParent(Parent : TWinControl);
BEGIN
INHERITED SetParent(Parent);
IF FCloseBtn.Tag=1 THEN BEGIN
Position:=posTopRight; FCloseBtn.Tag:=0; Caption:=''
END
END;
PROCEDURE TClosePanel.SetPosition(Value : TPosition);
BEGIN
FPosition:=Value;
MoveCloseButton
END;
PROCEDURE TClosePanel.WMWindowPosChanged(VAR MESSAGE : TWMWindowPosChanged);
BEGIN
INHERITED;
MoveCloseButton
END;
{ TMyCloseBtn }
CONSTRUCTOR TMyCloseBtn.Create(AOwner : TComponent);
BEGIN
INHERITED Create(AOwner);
Width:=16; Height:=16; Parent:=AOwner AS TWinControl
END;
PROCEDURE TMyCloseBtn.WMWindowPosChanged(VAR Message : TWMWindowPosChanged);
BEGIN
INHERITED;
IF (Parent IS TClosePanel) AND (TClosePanel(Parent).Position<>posCustom) THEN
WITH Message.WindowPos^ DO IF (cx<>SaveW) OR (cy<>SaveH) OR (x<>SaveX) OR (y<>SaveY) THEN BEGIN
SaveW:=cx; SaveH:=cy; SaveX:=x; SaveY:=y;
TClosePanel(Parent).MoveCloseButton
END;
WITH Message.WindowPos^ DO BEGIN
SaveW:=cx; SaveH:=cy; SaveX:=x; SaveY:=y
END
END;
END.
You can set the position of the Close Button (which I have defaulted to 16x16 pixels instead of the 32x32 of Andreas' default) using the TClosePanel.Position property. If you set this to any other value than posCustom, then it'll auto-move around the panel whenever the panel (or the button) changes size. If you set it to posCustom, you'll have to control the placement yourself with the exposed CloseBtn property. You may then need to alter Andreas' file to expose the Anchors, Visible, Top, Left, Width and Height properties. Alter the PUBLISHED section in his code to the following:
published
property Anchors;
property Enabled;
property Height;
property Left;
property Top;
property Visible;
property Width;
property OnClick;
property OnMouseUp;
property OnMouseDown;
end;

Slider (on/off) style component similar to iPad

Anyone knows of a On/off Delphi component similar to the slider control found on the iPad?
TAdvSmoothSlider (http://www.tmssoftware.com/site/advsmoothslider.asp) is the only one I found so far, but I would prefer not to have to purchase/install the entire TMS Smooth Controls Pack.
Anyone knows of a similar component? I already have On/Off images in png format so a component based on png images would also work.
Thanks in advance for any replies/ideas.
Could you do with something like this?
unit OnOffSwitch;
interface
uses
Classes, Controls, Windows, Messages, Graphics, Themes;
type
TOnOffSwitch = class(TCustomControl)
private
FMouseHover: Boolean;
FOff: Boolean;
FSliderRect: TRect;
procedure SetMouseHover(Value: Boolean);
procedure SetOff(Value: Boolean);
procedure UpdateSliderRect;
procedure WMWindowPosChanged(var Message: TWMWindowPosChanged);
message WM_WINDOWPOSCHANGED;
procedure CMEnabledChanged(var Message: TMessage);
message CM_ENABLEDCHANGED;
procedure CMFocusChanged(var Message: TCMFocusChanged);
message CM_FOCUSCHANGED;
procedure CMMouseLeave(var Message: TMessage); message CM_MOUSELEAVE;
protected
procedure KeyUp(var Key: Word; Shift: TShiftState); override;
procedure MouseDown(Button: TMouseButton; Shift: TShiftState;
X, Y: Integer); override;
procedure MouseMove(Shift: TShiftState; X, Y: Integer); override;
procedure Paint; override;
public
constructor Create(AOwner: TComponent); override;
published
property Anchors;
property Constraints;
property DragCursor;
property DragKind;
property DragMode;
property Enabled;
property Font;
property Off: Boolean read FOff write SetOff default True;
property OnClick;
property OnContextPopup;
property OnDblClick;
property OnDragDrop;
property OnDragOver;
property OnEndDock;
property OnEndDrag;
property OnEnter;
property OnExit;
property OnKeyDown;
property OnKeyPress;
property OnKeyUp;
property OnMouseDown;
property OnMouseMove;
property OnMouseUp;
property OnStartDock;
property OnStartDrag;
property ParentFont default False;
property ParentShowHint;
property PopupMenu;
property ShowHint;
property TabOrder;
property TabStop default True;
property Visible;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('Samples', [TOnOffSwitch]);
end;
{ TOnOffSwitch }
resourcestring
SOff = 'OFF';
SOn = 'ON';
procedure TOnOffSwitch.CMEnabledChanged(var Message: TMessage);
begin
Invalidate;
inherited;
end;
procedure TOnOffSwitch.CMFocusChanged(var Message: TCMFocusChanged);
begin
Invalidate;
inherited;
end;
procedure TOnOffSwitch.CMMouseLeave(var Message: TMessage);
begin
SetMouseHover(False);
inherited;
end;
constructor TOnOffSwitch.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
ControlStyle := [csCaptureMouse, csClickEvents, csDoubleClicks, csOpaque];
FOff := True;
Caption := SOff;
Width := 75;
Height := 25;
TabStop := True;
Font.Name := 'Tahoma';
Font.Style := [fsBold];
end;
procedure TOnOffSwitch.KeyUp(var Key: Word; Shift: TShiftState);
begin
if Key = VK_SPACE then
SetOff(not FOff);
inherited KeyUp(Key, Shift);
end;
procedure TOnOffSwitch.MouseDown(Button: TMouseButton; Shift: TShiftState;
X, Y: Integer);
begin
if (Shift = [ssLeft]) and PtInRect(FSliderRect, Point(X, Y)) then
SetOff(not FOff);
inherited MouseDown(Button, Shift, X, Y);
end;
procedure TOnOffSwitch.MouseMove(Shift: TShiftState; X, Y: Integer);
begin
if GetCaptureControl = nil then
SetMouseHover(PtInRect(FSliderRect, Point(X, Y)));
inherited MouseMove(Shift, X, Y);
end;
procedure TOnOffSwitch.Paint;
var
Button: TThemedButton;
PaintRect: TRect;
Details: TThemedElementDetails;
begin
if ThemeServices.ThemesAvailable then
begin
if not Enabled then
Button := tbPushButtonDisabled
else if not FOff then
Button := tbPushButtonPressed
else
Button := tbPushButtonNormal;
PaintRect := ClientRect;
Details := ThemeServices.GetElementDetails(Button);
ThemeServices.DrawElement(Canvas.Handle, Details, PaintRect);
if FOff then
Inc(PaintRect.Left, Round(2 / 5 * Width))
else
Dec(PaintRect.Right, Round(2 / 5 * Width));
Canvas.Brush.Style := bsClear;
Canvas.Font := Self.Font;
if not Enabled then
Canvas.Font.Color := $00A0A0A0
else
Canvas.Font.Color := $00555555;
DrawText(Canvas.Handle, PChar(Caption), -1, PaintRect, DT_CENTER or
DT_VCENTER or DT_SINGLELINE);
if Enabled and not FOff then
begin
OffsetRect(PaintRect, 0, 1);
Canvas.Font.Color := clWhite;
DrawText(Canvas.Handle, PChar(Caption), -1, PaintRect, DT_CENTER or
DT_VCENTER or DT_SINGLELINE);
end;
if not Enabled then
Button := tbPushButtonDisabled
else if Focused then
Button := tbPushButtonDefaulted
else if FMouseHover then
Button := tbPushButtonHot
else
Button := tbPushButtonNormal;
PaintRect := FSliderRect;
Details := ThemeServices.GetElementDetails(Button);
ThemeServices.DrawElement(Canvas.Handle, Details, PaintRect);
if Focused then
begin
PaintRect := ThemeServices.ContentRect(Canvas.Handle, Details, PaintRect);
SetTextColor(Canvas.Handle, clWhite);
DrawFocusRect(Canvas.Handle, PaintRect);
end;
end;
end;
procedure TOnOffSwitch.SetMouseHover(Value: Boolean);
begin
if FMouseHover <> Value then
begin
FMouseHover := Value;
Invalidate;
end;
end;
procedure TOnOffSwitch.SetOff(Value: Boolean);
begin
if FOff <> Value then
begin
FOff := Value;
if FOff then
Caption := SOff
else
Caption := SOn;
UpdateSliderRect;
Invalidate;
end;
end;
procedure TOnOffSwitch.UpdateSliderRect;
begin
if FOff then
SetRect(FSliderRect, 0, 0, Round(2 / 5 * Width), Height)
else
SetRect(FSliderRect, Round(3 / 5 * Width), 0, Width, Height);
end;
procedure TOnOffSwitch.WMWindowPosChanged(var Message: TWMWindowPosChanged);
begin
inherited;
UpdateSliderRect;
Font.Size := Round(Height div 3) + 1;
end;
end.
AFAIK, TMS Smooth Pack is available to registered Delphi users for free, so you should be able to get it and use its TAdvSmoothSlider in your apps for free too.

How to display an "X' in a checked checkbox instead of a checkmark?

The CheckBox component displays a checkmark when checked.
I would like to display an 'X' instead.
You could do something like this:
unit CheckboxEx;
interface
uses
SysUtils, Windows, Messages, Graphics, Classes, Controls, UxTheme;
type
TCrossType = (ctChar, ctGDI);
TCheckboxEx = class(TCustomControl)
private type
THoverState = (hsNormal = 1, hsHover = 2, hsPushed = 3);
private const
DEFAULT_PADDING = 3;
DEFAULT_CHECK_CHAR = '✘';
CHECK_LINE_PADDING = 4;
private
{ Private declarations }
FCaption: TCaption;
FChecked: boolean;
FPadding: integer;
FCheckWidth, FCheckHeight: integer;
FCheckRect, FTextRect: TRect;
theme: HTHEME;
FHoverState: THoverState;
FCheckFont: TFont;
FCheckChar: Char;
FMouseHover: boolean;
FCrossType: TCrossType;
procedure SetCaption(const Caption: TCaption);
procedure SetChecked(Checked: boolean);
procedure SetPadding(Padding: integer);
procedure UpdateMetrics;
procedure CheckFontChange(Sender: TObject);
procedure SetCheckChar(const CheckChar: char);
procedure DetermineState;
procedure SetCrossType(CrossType: TCrossType);
protected
procedure Paint; override;
procedure MouseMove(Shift: TShiftState; X: Integer; Y: Integer); override;
procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X: Integer;
Y: Integer); override;
procedure MouseUp(Button: TMouseButton; Shift: TShiftState; X: Integer;
Y: Integer); override;
procedure WndProc(var Message: TMessage); override;
procedure Click; override;
procedure KeyDown(var Key: Word; Shift: TShiftState); override;
procedure KeyUp(var Key: Word; Shift: TShiftState); override;
{ Protected declarations }
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
{ Public declarations }
published
{ Published declarations }
property ParentColor;
property ParentFont;
property Color;
property Visible;
property Enabled;
property TabStop default true;
property TabOrder;
property OnDblClick;
property OnEnter;
property OnExit;
property OnKeyUp;
property OnKeyPress;
property OnKeyDown;
property OnMouseActivate;
property OnMouseLeave;
property OnMouseEnter;
property OnMouseMove;
property OnMouseUp;
property OnMouseDown;
property OnClick;
property Font;
property CheckFont: TFont read FCheckFont write FCheckFont;
property Caption: TCaption read FCaption write SetCaption;
property Checked: boolean read FChecked write SetChecked default false;
property Padding: integer read FPadding write SetPadding default DEFAULT_PADDING;
property CheckChar: Char read FCheckChar write SetCheckChar default DEFAULT_CHECK_CHAR;
property CrossType: TCrossType read FCrossType write SetCrossType default ctGDI;
end;
procedure Register;
implementation
uses Math;
procedure Register;
begin
RegisterComponents('Rejbrand 2009', [TCheckboxEx]);
end;
var
Hit: boolean;
function _EnumFontsProcBool(var LogFont: TLogFont; var TextMetric: TTextMetric;
FontType: Integer; Data: Pointer): Integer; stdcall;
begin
hit := SameStr(LogFont.lfFaceName, Pstring(Data)^);
result := IfThen(hit, 0, 1);
end;
function FontInstalled(const FontName: TFontName): boolean;
var
LF: TLogFont;
fn: string;
begin
hit := false;
FillChar(LF, sizeOf(LF), 0);
LF.lfCharSet := DEFAULT_CHARSET;
fn := FontName;
EnumFontFamiliesEx(GetDC(0), LF, #_EnumFontsProcBool, cardinal(#fn), 0);
result := hit;
end;
function IsKeyDown(const VK: integer): boolean;
begin
IsKeyDown := GetKeyState(VK) and $8000 <> 0;
end;
{ TCheckboxEx }
procedure TCheckboxEx.CheckFontChange(Sender: TObject);
begin
Invalidate;
end;
procedure TCheckboxEx.Click;
begin
inherited;
if Enabled then
begin
SetChecked(not FChecked);
SetFocus;
end;
end;
constructor TCheckboxEx.Create(AOwner: TComponent);
begin
inherited;
TabStop := true;
FMouseHover := false;
FChecked := false;
FPadding := DEFAULT_PADDING;
FCheckChar := DEFAULT_CHECK_CHAR;
FCrossType := ctGDI;
theme := 0;
FHoverState := hsNormal;
FCheckFont := TFont.Create;
FCheckFont.Assign(Font);
if FontInstalled('Arial Unicode MS') then
FCheckFont.Name := 'Arial Unicode MS';
FCheckFont.OnChange := CheckFontChange;
end;
destructor TCheckboxEx.Destroy;
begin
FCheckFont.Free;
if theme <> 0 then
CloseThemeData(theme);
inherited;
end;
procedure TCheckboxEx.DetermineState;
var
OldState: THoverState;
begin
inherited;
OldState := FHoverState;
FHoverState := hsNormal;
if FMouseHover then
FHoverState := hsHover;
if (csLButtonDown in ControlState) or (IsKeyDown(VK_SPACE) and Focused) then
FHoverState := hsPushed;
if (FHoverState <> OldState) and UseThemes then
Invalidate;
end;
procedure TCheckboxEx.KeyDown(var Key: Word; Shift: TShiftState);
begin
inherited;
if Key = VK_SPACE then
DetermineState;
end;
procedure TCheckboxEx.KeyUp(var Key: Word; Shift: TShiftState);
begin
inherited;
if Key = VK_SPACE then
begin
Click;
DetermineState;
end;
end;
procedure TCheckboxEx.MouseDown(Button: TMouseButton; Shift: TShiftState; X,
Y: Integer);
begin
inherited;
DetermineState;
end;
procedure TCheckboxEx.MouseMove(Shift: TShiftState; X, Y: Integer);
begin
inherited;
FMouseHover := true;
DetermineState;
end;
procedure TCheckboxEx.MouseUp(Button: TMouseButton; Shift: TShiftState; X,
Y: Integer);
begin
inherited;
DetermineState;
end;
procedure TCheckboxEx.Paint;
var
ext: TSize;
frect: TRect;
begin
inherited;
Canvas.Brush.Color := Self.Color;
Canvas.FillRect(ClientRect);
if UseThemes then
begin
if theme = 0 then
begin
theme := OpenThemeData(Handle, 'BUTTON');
UpdateMetrics;
end;
if Enabled then
DrawThemeBackground(theme,
Canvas.Handle,
BP_CHECKBOX,
ord(FHoverState),
FCheckRect,
nil)
else
DrawThemeBackground(theme,
Canvas.Handle,
BP_CHECKBOX,
CBS_UNCHECKEDDISABLED,
FCheckRect,
nil);
end
else
if Enabled then
DrawFrameControl(Canvas.Handle,
FCheckRect,
DFC_BUTTON,
DFCS_BUTTONCHECK)
else
DrawFrameControl(Canvas.Handle,
FCheckRect,
DFC_BUTTON,
DFCS_BUTTONCHECK or DFCS_INACTIVE);
Canvas.TextFlags := TRANSPARENT;
Canvas.Brush.Style := bsClear;
Canvas.Font.Assign(Font);
DrawText(Canvas.Handle,
PChar(FCaption),
length(FCaption),
FTextRect,
DT_SINGLELINE or DT_VCENTER or DT_LEFT);
if Focused then
begin
ext := Canvas.TextExtent(FCaption);
frect := Rect(FTextRect.Left,
(ClientHeight - ext.cy) div 2,
FTextRect.Left + ext.cx,
(ClientHeight + ext.cy) div 2);
Canvas.DrawFocusRect(frect);
end;
if FChecked then
case FCrossType of
ctChar:
begin
Canvas.Font.Assign(FCheckFont);
DrawText(Canvas.Handle,
CheckChar,
1,
FCheckRect,
DT_SINGLELINE or DT_VCENTER or DT_CENTER);
end;
ctGDI:
begin
Canvas.Pen.Width := 2;
Canvas.Pen.Color := clBlack;
Canvas.Pen.Mode := pmCopy;
Canvas.MoveTo(FCheckRect.Left + CHECK_LINE_PADDING, FCheckRect.Top + CHECK_LINE_PADDING);
Canvas.LineTo(FCheckRect.Right - CHECK_LINE_PADDING, FCheckRect.Bottom - CHECK_LINE_PADDING);
Canvas.MoveTo(FCheckRect.Right - CHECK_LINE_PADDING, FCheckRect.Top + CHECK_LINE_PADDING);
Canvas.LineTo(FCheckRect.Left + CHECK_LINE_PADDING, FCheckRect.Bottom - CHECK_LINE_PADDING);
end;
end;
end;
procedure TCheckboxEx.SetCaption(const Caption: TCaption);
begin
if not SameStr(FCaption, Caption) then
begin
FCaption := Caption;
Invalidate;
end;
end;
procedure TCheckboxEx.SetCheckChar(const CheckChar: char);
begin
if FCheckChar <> CheckChar then
begin
FCheckChar := CheckChar;
if FChecked then Invalidate;
end;
end;
procedure TCheckboxEx.SetChecked(Checked: boolean);
begin
if FChecked <> Checked then
begin
FChecked := Checked;
Invalidate;
end;
end;
procedure TCheckboxEx.SetCrossType(CrossType: TCrossType);
begin
if FCrossType <> CrossType then
begin
FCrossType := CrossType;
if FChecked then Invalidate;
end;
end;
procedure TCheckboxEx.SetPadding(Padding: integer);
begin
if FPadding <> Padding then
begin
FPadding := Padding;
UpdateMetrics;
Invalidate;
end;
end;
procedure TCheckboxEx.UpdateMetrics;
var
size: TSize;
begin
FCheckWidth := GetSystemMetrics(SM_CXMENUCHECK);
FCheckHeight := GetSystemMetrics(SM_CYMENUCHECK);
if UseThemes then
begin
UxTheme.GetThemePartSize(theme, Canvas.Handle, BP_CHECKBOX, CBS_UNCHECKEDNORMAL, nil, TS_DRAW, size);
FCheckWidth := size.cx;
FCheckHeight := size.cy;
end;
FCheckRect := Rect(0,
(ClientHeight - FCheckHeight) div 2,
FCheckWidth,
(ClientHeight + FCheckHeight) div 2);
FTextRect := Rect(FCheckWidth + FPadding,
0,
ClientWidth,
ClientHeight);
end;
procedure TCheckboxEx.WndProc(var Message: TMessage);
begin
inherited;
case Message.Msg of
CM_MOUSELEAVE:
begin
FMouseHover := false;
DetermineState;
end;
WM_SIZE:
begin
UpdateMetrics;
Invalidate;
end;
WM_SETFOCUS, WM_KILLFOCUS:
Invalidate;
end;
end;
end.
Now (with CrossType set to ctChar) you can use any Unicode character as the checkmark, the default choice being ✘ (U+2718: HEAVY BALLOT X). The images below illustrate that the control works both with and without visual themes:
The following image illustrates that you can choose any character as your checkmark:
This character is ✿ (U+273F: BLACK FLORETTE).
If you set CrossType to ctGDI instead of ctChar, the control will draw a cross manually and not a character:
I didn't use double-buffering this time, because there is no noticable flickering with themes enabled. Without themes, however, there is flickering. To remedy this, simply use a FBuffer: TBitmap and draw on FBuffer.Canvas instead of Self.Canvas and then BitBlt at the end of Paint, as I do in my other controls here at SO.
You'll have to write a custom control and paint it yourself.
If this is a real check box then it's a bad idea to avoid the system's default drawing. However, if you want to do something like a voting form then I could see why you might opt to do this.
I would go the opposite way, anyway, select all items by default and let the user remove the ones who should be left out from the list.
Having checkbutton a serious limitation in designs, who want to stay in VCL, can use BitBtn as a check, using "Kind" property to paint the Cancel or Ok images when user click on it. Also delete after every condition change, the "Caption" property, because the BitBtn must have a square layout to simulate a check. Use also a tLabel at left or right hand as you wish.
if lAutoMode = False then
begin
lAutoMode := True;
BitBtn1.Kind := bkOK;
BitBtn1.Caption := '';
end
else
begin
lAutoMode := False;
BitBtn1.Kind := bkAbort;
BitBtn1.Caption := '';
end;
When create the Form, set the initial state for the BitBtn.

How to suppress standard RadioButton check behavior in Delphi?

I realize this one is a bit strange, so I'll explain. For a simple internet radio player I need a control to specify rating (1-5 "stars"). I have no experience or talent for graphical design, so all my attempts at drawing bitmaps look ridiculous/awful, take your pick. I couldn't find a 3rd party control with that functionality and look that fits standard VCL controls. So...
It occurred to me that I could achieve an OK look and consistency with Windows UI by using standard radiobuttons without captions, like this:
I had a vague (and incorrect) recollection of a GroupIndex property; assigning a different value to each radiobutton would let multiple radiobuttons be checked at the same time. Alas, TRadioButton does not have a GroupIndex property, so that's that.
Is it possible to completely override the natural radiobutton behavior, so that more than one button can show up as checked at the same time? Or,
Can I acquire all the bitmaps Windows uses for radiobuttons (I assume they're bitmaps) from the system and draw them directly, including theming support? In this case I would still like to retain all the effects of a radiobutton, including the mouse hover "glow", etc, so that means getting all the "native" bitmaps and drawing them as necessary, perhaps on a TPaintBox.
For maximum convenience, you could write a small control that draws native, themed, radio boxes:
unit StarRatingControl;
interface
uses
SysUtils, Windows, Messages, Graphics, Classes, Controls, UxTheme;
type
TStarRatingControl = class(TCustomControl)
private const
DEFAULT_SPACING = 4;
DEFAULT_NUM_STARS = 5;
FALLBACK_BUTTON_SIZE: TSize = (cx: 16; cy: 16);
private
{ Private declarations }
FRating: integer;
FBuffer: TBitmap;
FSpacing: integer;
FNumStars: integer;
FButtonStates: array of integer;
FButtonPos: array of TRect;
FButtonSize: TSize;
FDown: boolean;
PrevButtonIndex: integer;
PrevState: integer;
FOnChange: TNotifyEvent;
procedure SetRating(const Rating: integer);
procedure SetSpacing(const Spacing: integer);
procedure SetNumStars(const NumStars: integer);
procedure SwapBuffers;
procedure SetState(const ButtonIndex: integer; const State: integer);
protected
{ Protected declarations }
procedure WndProc(var Message: TMessage); override;
procedure Paint; override;
procedure MouseMove(Shift: TShiftState; X: Integer; Y: Integer); override;
procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X: Integer;
Y: Integer); override;
procedure MouseUp(Button: TMouseButton; Shift: TShiftState; X: Integer;
Y: Integer); override;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
{ Public declarations }
published
{ Published declarations }
property OnChange: TNotifyEvent read FOnChange write FOnChange;
property Rating: integer read FRating write SetRating default 3;
property Spacing: integer read FSpacing write SetSpacing default DEFAULT_SPACING;
property NumStars: integer read FNumStars write SetNumStars default DEFAULT_NUM_STARS;
property OnDblClick;
property OnKeyUp;
property OnKeyPress;
property OnKeyDown;
property OnMouseWheelDown;
property OnMouseWheelUp;
property OnMouseWheel;
property OnMouseEnter;
property OnMouseLeave;
property OnMouseActivate;
property OnMouseMove;
property OnMouseUp;
property OnMouseDown;
property OnClick;
property Align;
property Anchors;
property Color;
end;
procedure Register;
implementation
uses Math;
function IsIntInInterval(x, xmin, xmax: integer): boolean; inline;
begin
IsIntInInterval := (xmin <= x) and (x <= xmax);
end;
function PointInRect(const X, Y: integer; const Rect: TRect): boolean; inline;
begin
PointInRect := IsIntInInterval(X, Rect.Left, Rect.Right) and
IsIntInInterval(Y, Rect.Top, Rect.Bottom);
end;
procedure Register;
begin
RegisterComponents('Rejbrand 2009', [TStarRatingControl]);
end;
{ TStarRatingControl }
constructor TStarRatingControl.Create(AOwner: TComponent);
var
i: Integer;
begin
inherited;
FBuffer := TBitmap.Create;
FRating := 3;
FSpacing := DEFAULT_SPACING;
FNumStars := DEFAULT_NUM_STARS;
SetLength(FButtonStates, FNumStars);
SetLength(FButtonPos, FNumStars);
for i := 0 to high(FButtonStates) do
FButtonStates[i] := RBS_NORMAL;
FDown := false;
PrevButtonIndex := -1;
PrevState := -1;
end;
destructor TStarRatingControl.Destroy;
begin
FBuffer.Free;
inherited;
end;
procedure TStarRatingControl.MouseDown(Button: TMouseButton; Shift: TShiftState;
X, Y: Integer);
var
i: integer;
begin
inherited;
FDown := true;
for i := 0 to FNumStars - 1 do
if PointInRect(X, Y, FButtonPos[i]) then
begin
SetState(i, RBS_PUSHED);
Exit;
end;
end;
procedure TStarRatingControl.MouseMove(Shift: TShiftState; X, Y: Integer);
var
i: Integer;
begin
inherited;
if FDown then Exit;
for i := 0 to FNumStars - 1 do
if PointInRect(X, Y, FButtonPos[i]) then
begin
SetState(i, RBS_HOT);
Exit;
end;
SetState(-1, -1);
end;
procedure TStarRatingControl.MouseUp(Button: TMouseButton; Shift: TShiftState;
X, Y: Integer);
var
i: Integer;
begin
inherited;
for i := 0 to FNumStars - 1 do
if PointInRect(X, Y, FButtonPos[i]) and (i = PrevButtonIndex) and (FRating <> i + 1) then
begin
SetRating(i + 1);
if Assigned(FOnChange) then
FOnChange(Self);
end;
FDown := false;
MouseMove(Shift, X, Y);
end;
procedure TStarRatingControl.Paint;
var
t: HTHEME;
i: Integer;
begin
inherited;
FBuffer.Canvas.Brush.Color := Color;
FBuffer.Canvas.FillRect(ClientRect);
FButtonSize := FALLBACK_BUTTON_SIZE;
if UseThemes then
begin
t := OpenThemeData(Handle, 'BUTTON');
if t <> 0 then
try
GetThemePartSize(t, FBuffer.Canvas.Handle, BP_RADIOBUTTON, RBS_NORMAL, nil, TS_DRAW, FButtonSize);
for i := 0 to FNumStars - 1 do
with FButtonPos[i] do
begin
Left := i * (Spacing + FButtonSize.cx);
Top := (Self.Height - FButtonSize.cy) div 2;
Right := Left + FButtonSize.cx;
Bottom := Top + FButtonSize.cy;
end;
for i := 0 to FNumStars - 1 do
DrawThemeBackground(t,
FBuffer.Canvas.Handle,
BP_RADIOBUTTON,
IfThen(FRating > i, RBS_CHECKEDNORMAL) + FButtonStates[i],
FButtonPos[i],
nil);
finally
CloseThemeData(t);
end;
end
else
begin
for i := 0 to FNumStars - 1 do
with FButtonPos[i] do
begin
Left := i * (Spacing + FButtonSize.cx);
Top := (Self.Height - FButtonSize.cy) div 2;
Right := Left + FButtonSize.cx;
Bottom := Top + FButtonSize.cy;
end;
for i := 0 to FNumStars - 1 do
DrawFrameControl(FBuffer.Canvas.Handle,
FButtonPos[i],
DFC_BUTTON,
DFCS_BUTTONRADIO or IfThen(FRating > i, DFCS_CHECKED));
end;
SwapBuffers;
end;
procedure TStarRatingControl.SetNumStars(const NumStars: integer);
var
i: integer;
begin
if FNumStars <> NumStars then
begin
FNumStars := NumStars;
SetLength(FButtonStates, FNumStars);
SetLength(FButtonPos, FNumStars);
for i := 0 to high(FButtonStates) do
FButtonStates[i] := RBS_NORMAL;
Paint;
end;
end;
procedure TStarRatingControl.SetRating(const Rating: integer);
begin
if FRating <> Rating then
begin
FRating := Rating;
Paint;
end;
end;
procedure TStarRatingControl.SetSpacing(const Spacing: integer);
begin
if FSpacing <> Spacing then
begin
FSpacing := Spacing;
Paint;
end;
end;
procedure TStarRatingControl.SetState(const ButtonIndex, State: integer);
var
i: Integer;
begin
for i := 0 to FNumStars - 1 do
if i = ButtonIndex then
FButtonStates[i] := State
else
FButtonStates[i] := RBS_NORMAL;
if (PrevButtonIndex <> ButtonIndex) or (PrevState <> State) then
Paint;
PrevButtonIndex := ButtonIndex;
PrevState := State;
end;
procedure TStarRatingControl.SwapBuffers;
begin
BitBlt(Canvas.Handle,
0,
0,
Width,
Height,
FBuffer.Canvas.Handle,
0,
0,
SRCCOPY);
end;
procedure TStarRatingControl.WndProc(var Message: TMessage);
begin
inherited;
case Message.Msg of
WM_SIZE:
begin
FBuffer.SetSize(Width, Height);
Paint;
end;
end;
end;
end.
Just adjust the properties NumStars, Rating, and Spacing, and have fun!
Of course, you could also write a component that uses custom bitmaps instead of the native Windows radio buttons.
Making radio buttons that look like radio buttons but behave differently would confuse the user. Also, you would end up needing half-check marks when you decide to display existing ratings. So something like a progress bar (maybe custom-colored or custom-drawn) to display, how "complete" user satisfaction is could be a better option.
I agree with Eugene and Craig that something like stars would be better, but, to answer the question posed:
The unthemed radio button images are available by calling LoadBitmap with OBM_CHECKBOXES. You can assign that directly to a TBitmap's Handle property, and then divide the width by 4 and the height by 3 to get the subbitmap measurements. Use TCanvas.BrushCopy to do the drawing.
To draw the themed images you need to use Delphi's Themes.pas. Specifically call ThemeServices.GetElementDetails with tbRadioButtonUncheckedNormal or tbRadioButtonCheckedNormal and pass the result to ThemeServices.DrawElement along with the client rect.
Here's a simple override that makes a TCheckBox draw as a checked radio button so you can see how it works:
TCheckBox = class(StdCtrls.TCheckBox)
constructor Create(AOwner: TComponent); override;
procedure PaintWindow(DC: HDC); override;
end;
constructor TCheckBox.Create(AOwner: TComponent);
begin
inherited;
ControlState := ControlState + [csCustomPaint];
end;
procedure TCheckBox.PaintWindow(DC: HDC);
begin
ThemeServices.DrawElement(DC,
ThemeServices.GetElementDetails(tbRadioButtonCheckedNormal), ClientRect);
end;
You could place each radiobutton on a separate (tiny) panel, and that would make a substitute for the missing GroupIndex property.
Maybe not the nicest method, still relatively cheap, it seems to me.
Good inspiration gave you Andreas Rejbrand (+1). I'll provide you just some small piece of code of what you are probably looking for. It's form with two overlapped images with one common event - OnMouseDown. It contains just some mad formula - unfortunately with constants, which I've made some time ago. But sorry I'm not mathematician, so please be patient with me and let's take this also as the inspiration :)

Resources