Custom Component OnMouseDown not working; - delphi

I am trying to create a button based on TImage with 2 images governed by OnMouseDown.
The component shows the first image when running but I cannot get the OnMouseDown event to fire. I have looked through Stack Overflow related questions but cannot find an answer (Maybe I am asking the wrong question).
unit ImageBtn;
{$R resources.res resources.rc}
interface
uses WinTypes, WinProcs, Messages, SysUtils, Classes, Controls,
Forms, Graphics, Extctrls, AppEvnts, Buttons;
type
TImageBtn = class(TImage)
private
FCenter : Boolean;
FDoubleBuffered : Boolean;
procedure AutoInitialize;
procedure AutoDestroy;
procedure SetDoubleBuffered(Value : Boolean);
procedure WMSize(var Message: TWMSize); message WM_SIZE;
protected
procedure Click; override;
procedure Loaded; override;
procedure Paint; override;
procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); override;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
published
property OnClick;
property OnDblClick;
property OnDragDrop;
property OnMouseDown;
property OnMouseMove;
property OnMouseUp;
property Center : Boolean read FCenter write FCenter default True;
property DoubleBuffered : Boolean read FDoubleBuffered write SetDoubleBuffered default True;
property Height;
property Proportional default True;
property Stretch default True;
property Width;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('dcs', [TImageBtn]);
end;
procedure TImageBtn.AutoInitialize;
begin
FCenter := True;
FDoubleBuffered := True;
Height := 29;
Proportional := True;
Stretch := True;
Width := 82;
Picture.Bitmap.LoadFromResourceName(HInstance,'BITMAP1');
end;
procedure TImageBtn.AutoDestroy;
begin
end;
procedure TImageBtn.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
inherited;
MouseDown(Button, Shift, X, Y);
Picture.CleanupInstance;
Picture.Bitmap.LoadFromResourceName(HInstance,'BITMAP2');
end;
procedure TImageBtn.SetDoubleBuffered(Value : Boolean);
begin
FDoubleBuffered := Value;
end;
procedure TImageBtn.Click;
begin
inherited Click;
end;
constructor TImageBtn.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
AutoInitialize;
end;
destructor TImageBtn.Destroy;
begin
AutoDestroy;
inherited Destroy;
end;
procedure TImageBtn.Loaded;
begin
inherited Loaded;
end;
procedure TImageBtn.Paint;
begin
inherited Paint;
end;
procedure TImageBtn.WMSize(var Message: TWMSize);
var
W, H: Integer;
begin
inherited;
W := Width;
H := Height;
if (W <> Width) or (H <> Height) then
inherited SetBounds(Left, Top, W, H);
Message.Result := 0;
end;
end.
I have probably made a stupid mistake but I cannot find it. Can anyone help please? Delphi 7 and Windows 11.
Edit
Having read Blurry Sterk's comments, maybe I did not make the problem clear. The OnMouseDown event does not seem to fire but I cannot add a breakpoint as there are no blue points.
Edit2
Please find screenshot of the only lines that have a blue dot against them.
This is obviously related to why my code is not working.
Edit3
Thank you to everyone who replied. I followed the advice by removing the line MouseDown(Button, Shift, X, Y); and it worked. BTW I didn't want to use a TBitBtn because of the frame and alignment. The button needs a shadow. Thank you once again Tom Brunberg and Eirik A.

Related

Delphi installing components in their own package

I have developed two unit files (.pas) and i have 2 different components
the first one is MyPCButton and second is MyPanel
MyPCButton.pas uses MyPanel.pas
The Problem is when i try to put them in seperate packages, when i install the MyPCButton component which is using MyPanel.pas, the package installs both of them in the same package, if i install the MyPanel.pas first then MyPCButton package refuses the install and says "couldn't create output file for MyPanel.bpl in the package output directory"
I have placed MyPanel in interface section and i have placed it in implementation section but i still get the same error,
What i wanna do is install them sepeerately in their own packages
MyPCButton.pas :
unit MyPCButton;
interface
uses
Winapi.Windows,Winapi.Messages,System.SysUtils, System.Classes, Vcl.Controls, Vcl.ExtCtrls,
Vcl.Imaging.PngImage,Vcl.Graphics,Types,cxGraphics;
type
TMyPCButton=class (TCustomControl)
private
FCaption:TCaption;
FIcon:TPngImage;
FIconIndex:integer;
FIconList:TcxImageList;
FIconWidth:integer;
FIconLeftMargin,FIconRightMargin:integer;
FCloseIconList:TcxImageList;
FCloseIconWidth:integer;
FCloseIconLeftMargin,FCloseIconRightMargin:integer;
FFont:TFont;
FColorDefault,FColorDefaultFont:TColor;
FColorHover,FColorHoverFont:TColor;
FColorActive,FColorActiveFont:TColor;
FCaptionWidth:integer;
FMaximumCaptionWidth:integer;
FState:Byte;
FCurCloseIconState:integer;
FActive: Boolean;
FBuffer: TBitmap;
R3:TRect;
FOnClick,FOnCloseClick: TNotifyEvent;
FOnActivate,FOnDeactivate:TNotifyEvent;
FFocused:Boolean;
FGroupNo: integer;
procedure SetIconIndex(const Value:Integer);
procedure SetIconList(const Value:TcxImageList);
procedure SetCaption(const Value: TCaption);
procedure SetCloseIconList(const Value: TcxImageList);
procedure SetAutoSize;
procedure WndProc(var Message: TMessage); override;
procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X: Integer; Y: Integer); override;
procedure MouseMove(Shift: TShiftState; X: Integer; Y: Integer); override;
procedure SetActive(const Value: Boolean);
procedure SwapBuffers;
procedure CheckGroupNo;
protected
procedure Paint; override;
property Canvas;
procedure DoEnter; override;
procedure DoExit; override;
procedure KeyDown(Sender: TObject; var Key: Word;Shift: TShiftState);
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
function GetTotalWidth:integer;
function GetLeftSpace:integer;
function GetRightSpace:integer;
procedure SetPositionInPanel;
procedure SetActiveAfterClose;
published
property Caption:TCaption read FCaption write SetCaption;
property IconIndex:integer Read FIconIndex write SetIconIndex;
property IconList:TcxImageList Read FIconList write SetIconList;
property CloseIconList:TcxImageList Read FCloseIconList write SetCloseIconList;
property Active:Boolean read FActive write SetActive;
property OnClick: TNotifyEvent read FOnClick write FOnClick;
property OnCloseClick: TNotifyEvent read FOnCloseClick write FOnCloseClick;
property OnActivate: TNotifyEvent read FOnActivate write FOnActivate;
property OnDeActivate: TNotifyEvent read FOnDeactivate write FOnDeactivate;
property GroupNo:integer read FGroupNo write FGroupNo;
property TabStop;
property Align;
end;
procedure Register;
implementation
uses Math,pvalues,pfunctions,MyPanel;
function IsIntInInterval(x, xmin, xmax: integer): boolean; inline;
begin
IsIntInInterval := (xmin <= x) and (x <= xmax);
end;
function PointInRect(const Point: TPoint; const Rect: TRect): boolean; inline;
begin
PointInRect := IsIntInInterval(Point.X, Rect.Left, Rect.Right) and
IsIntInInterval(Point.Y, Rect.Top, Rect.Bottom);
end;
procedure Register;
begin
RegisterComponents('MyComponents', [TMyPCButton]);
end;
procedure TMyPCButton.CheckGroupNo;
var
i:integer;
begin
for i:=0 to Parent.ControlCount-1 do
begin
if (Parent.Controls[i] is TMyPCButton) then
begin
if ((Parent.Controls[i] as TMyPCButton).Active) and
((Parent.Controls[i] as TMyPCButton).Name<>Self.Name) and
((Parent.Controls[i] as TMyPCButton).GroupNo=Self.GroupNo)
then
(Parent.Controls[i] as TMyPCButton).Active:=False;
end;
end;
end;
constructor TMyPCButton.Create(AOwner: TComponent);
begin
inherited;
if _V_RegValuesInitated=false then
_P_RegValuesInitate;
FFocused:=false;
FBuffer := TBitmap.Create;
Height:=30;
Width:=50;
FFont:=TFont.Create;
FFont.Assign(R_BtnTB.VFont);
FBuffer.Canvas.Font.Assign(FFont);
FState:=0;
FCurCloseIconState:=-1;
FIconIndex:=-1;
FIconWidth:=16;
FIconLeftMargin:=5;
FIconRightMargin:=5;
FCloseIconWidth:=17;
FCloseIconLeftMargin:=16;
FCloseIconRightMargin:=6;
FCaptionWidth:=0;
FMaximumCaptionWidth:=160;
OnKeyDown:=KeyDown;
end;
destructor TMyPCButton.Destroy;
begin
inherited;
FreeAndNil(FIcon);
FreeAndNil(FBuffer);
end;
procedure TMyPCButton.DoEnter;
begin
inherited;
if FActive=false then
FState:=1;
FFocused:=true;
paint;
end;
procedure TMyPCButton.DoExit;
begin
inherited;
if FActive=false then
FState:=0
else
FState:=2;
FFocused:=false;
paint;
end;
function TMyPCButton.GetLeftSpace: integer;
begin
Result:=Parent.Left;
end;
function TMyPCButton.GetRightSpace: integer;
begin
Result:=GetTotalWidth-Parent.Width;
end;
function TMyPCButton.GetTotalWidth: integer;
begin
Result:=Self.Left+Self.Width;
end;
procedure TMyPCButton.SetPositionInPanel;
var
TotalWidth,LeftSpace,RightSpace:integer;
begin
if (Owner is TMyPanel) then
begin
if (Owner as TMyPanel).Parent is TMyPanel then
begin
LeftSpace:=GetLeftSpace;
if LeftSpace<0 then
LeftSpace:=LeftSpace*-1;
RightSpace:=GetRightSpace;
TotalWidth:=GetTotalWidth;
if (TotalWidth-LeftSpace)<Self.Width then
Parent.Left:=Parent.Left+(((TotalWidth-LeftSpace)-Self.Width)*-1)
else
if TotalWidth-LeftSpace>(Parent).Parent.Width then
begin
Parent.Left:=Parent.Left-(TotalWidth-LeftSpace-(Parent).Parent.Width);
end;
end;
end;
end;
procedure TMyPCButton.SetActiveAfterClose;
var
VControlCount,VPosition:integer;
begin
if (Parent is TMyPanel) then
begin
VControlCount:=Parent.ControlCount;
if VControlCount>1 then
begin
for VPosition:=0 to VControlCount-1 do
begin
if (Parent.Controls[VPosition] as TMyPCButton).Name=Self.Name then
break;
end;
if VPosition+1=Parent.ControlCount then
(Parent.Controls[VPosition-1] as TMyPCButton).Active:=true
else
(Parent.Controls[VPosition+1] as TMyPCButton).Active:=true;
end;
end;
end;
procedure TMyPCButton.KeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
begin
if FActive=false then
SetActive(True);
if (Key=13) and (Assigned(FOnClick)) then FOnClick(Self);
paint;
end;
procedure TMyPCButton.MouseDown(Button: TMouseButton; Shift: TShiftState; X,
Y: Integer);
var
SelfWidth:integer;
begin
inherited;
if (FActive=false) and PointInRect(point(X,Y),R3)=false then
begin
FState:=2;
SetActive(True);
paint;
end;
if PointInRect(point(X,Y),R3) then
begin
if (Assigned(FOnCloseClick)) then FOnCloseClick(Self);
SelfWidth:=Self.Width;
Width:=0;
Parent.Width:=Parent.Width-SelfWidth;
if FActive then
SetActiveAfterClose;
Self.Destroy;
end
else
begin
if (Assigned(FOnClick)) then FOnClick(Self);
end;
end;
procedure TMyPCButton.MouseMove(Shift: TShiftState; X, Y: Integer);
begin
inherited;
if PointInRect(point(X,Y),R3)=false then
begin
if FState-1<>FCurCloseIconState then
begin
FCurCloseIconState:=FState-1;
paint;
end;
end
else
begin
if FState+1<>FCurCloseIconState then
begin
FCurCloseIconState:=FState+1;
paint;
end;
end;
end;
procedure TMyPCButton.Paint;
var
R2,R4:TRect;
ColorBackground,ColorFont:TColor;
begin
inherited;
if FBuffer.Canvas.Font.Name<>R_BtnTB.VFont.Name then
begin
FBuffer.Canvas.Font.Name:=R_BtnTB.VFont.Name;
end;
if FBuffer.Canvas.Font.Size<>R_BtnTB.VFont.Size then
begin
FBuffer.Canvas.Font.Size:=R_BtnTB.VFont.Size;
end;
if FBuffer.Canvas.Font.Quality<>R_BtnTB.VFont.Quality then
begin
FBuffer.Canvas.Font.Quality:=R_BtnTB.VFont.Quality;
end;
FBuffer.SetSize(Width,Height);
if FState=0 then
begin
ColorBackground:=R_BtnTB.DefaultColor;
ColorFont:=R_BtnTB.DefaultFontColor
end;
if FState=1 then
begin
ColorBackground:=R_BtnTB.HoverColor;
ColorFont:=R_BtnTB.HoverFontColor
end;
if FState=2 then
begin
ColorBackground:=R_BtnTB.ActiveColor;
ColorFont:=R_BtnTB.ActiveFontColor
end;
FBuffer.Canvas.Brush.Color:=ColorBackground;
FBuffer.Canvas.Font.Color:=ColorFont;
FBuffer.Canvas.FillRect(ClientRect);
if ((Assigned(FIconList)) and (FIconIndex>-1)) then
begin
FIconList.Draw(FBuffer.Canvas,FIconLeftMargin,(ClientHeight div 2)-(FIconList.Height div 2),FIconIndex);
end;
R2.Top:=(ClientHeight div 2)-(FBuffer.Canvas.TextHeight(FCaption) div 2);
R2.Height:=ClientHeight;
R2.Left:=FIconLeftMargin+FIconWidth+FIconRightMargin;
R2.Width:=FCaptionWidth;
DrawText(FBuffer.Canvas.Handle, PChar(FCaption), -1, R2,DT_LEFT);
if Assigned(FCloseIconList) then
begin
R3.Top:=0;
R3.Left:=ClientWidth-FCloseIconWidth;
R3.Height:=ClientHeight;
R3.Width:=FCloseIconWidth;
FCloseIconList.Draw(FBuffer.Canvas,R3.Left+(FCloseIconList.Width div 2),(R3.Height div 2)-(FCloseIconList.Height div 2),FCurCloseIconState);
end;
if FFocused then
begin
R4.Top:=1;
R4.Left:=1;
R4.Width:=ClientWidth-2;
R4.Height:=ClientHeight-2;
DrawFocusRect(FBuffer.Canvas.Handle,R4);
end;
SwapBuffers;
end;
procedure TMyPCButton.SetActive(const Value: Boolean);
var
MyPoint:TPoint;
begin
if FActive<>Value then
begin
FActive := Value;
if FActive then
begin
CheckGroupNo;
FState:=2;
SetPositionInPanel;
if Assigned(FOnActivate) then FOnActivate(Self);
end
else
begin
MyPoint := ScreenToClient(Mouse.CursorPos);
if PtInRect(ClientRect, MyPoint) then
FState:=1
else
FState:=0;
if Assigned(FOnDeactivate) then FOnDeactivate(Self);
end;
paint;
end;
end;
procedure TMyPCButton.SetAutoSize;
begin
FCaptionWidth:=FBuffer.Canvas.TextWidth(FCaption);
if FCaptionWidth>160 then
FCaptionWidth:=FMaximumCaptionWidth;
Width:=FIconLeftMargin+FIconWidth+FIconRightMargin+FCaptionWidth+FCloseIconLeftMargin+FCloseIconWidth-12+FCloseIconRightMargin;
end;
procedure TMyPCButton.SetCaption(const Value: TCaption);
begin
inherited;
FCaption := Value;
SetAutoSize;
paint;
end;
procedure TMyPCButton.SetCloseIconList(const Value: TcxImageList);
begin
FCloseIconList := Value;
paint;
end;
procedure TMyPCButton.SetIconIndex(const Value: Integer);
begin
FIconIndex:=Value;
paint;
end;
procedure TMyPCButton.SetIconList(const Value: TcxImageList);
begin
FIconList:=Value;
paint;
end;
procedure TMyPCButton.WndProc(var Message: TMessage);
begin
inherited;
case Message.Msg of
CM_MOUSEENTER:
begin
if FActive=False then
begin
FState:=1;
Paint;
end;
end;
CM_MOUSELEAVE:
begin
if FActive=False then
begin
FState:=0;
FCurCloseIconState:=-1;
Paint;
end
else
begin
FState:=2;
FCurCloseIconState:=-1;
Paint;
end;
end;
WM_ERASEBKGND:
Message.Result := 1;
end;
end;
procedure TMyPCButton.SwapBuffers;
begin
BitBlt(Canvas.Handle, 0, 0, Width, Height, FBuffer.Canvas.Handle, 0, 0, SRCCOPY);
end;
end.
MyPanel.pas
unit MyPanel;
interface
uses
System.SysUtils, System.Classes, Vcl.Controls, Vcl.ExtCtrls;
type
TMyPanel = class(TPanel)
private
{ Private declarations }
protected
{ Protected declarations }
public
{ Public declarations }
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
published
{ Published declarations }
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('MyComponents', [TMyPanel]);
end;
constructor TMyPanel.Create(AOwner: TComponent);
begin
inherited;
end;
destructor TMyPanel.Destroy;
begin
inherited;
end;
end.
There is nothing particularly special about what you are trying to do but your implementation and the way you describe installing pas files into packages suggests that you are perhaps not clear on how to go about it.
First of all, it has long been considered bad practice to combine components and IDE registration in the same package.
You should implement your components in a Runtime-only Package (or packages). You then have a Design-Time Package which includes the corresponding Runtime package in it's requires list.
The Design-Time Package also contains (typically) a single unit which uses the units containing your components and implements the Register function.
You then install the Design-Time Package into the IDE, which will load the Runtime Packages as needed to register the components.
Within that overall approach, you still have a number of options for how to organise your packages.
You could have separate runtime packages for each control and separate designtime packages to install each control separately, but it sounds like you will then have a lot of dependencies between your packages which can quickly become problematic to unravel and create dependencies in the order in which you build and install your packages.
In your case since your two components have this dependency on each other, it sounds like it would make most sense to keep those components in one single runtime package and have a single designtime package to install all the components from that runtime package.
If you really wanted to you could still have separate design-time packages to install each control individually from that single runtime package, but I really don't see any advantage in doing that in your case (unless there are further complications or consideration which are not apparent from your question).
it was my mistake
i had changed the default dcp output directory and apparently i had to add the new path in library path, so now i am able to put/install the .pas files in their own packages

How do I create an alpha blended panel?

I'm trying to display a truly alpha blended TPanel in Delphi XE2. I've found quite a few attempts online, but none of them work correctly.
What I'm trying to achieve is a 'semi modal' form. A form that is displayed over the top of other controls with a faded background in a similar manner to that seen in web browsers.
I've got it working in a basic form, but it suffers from the following problems:
A large amount of flicker when resizing the panel.
If a control is moved over the top of the panel it leaves a trail.
Here's my efforts thus far (based on some code I found here).
unit SemiModalFormU;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, ExtCtrls;
type
ISemiModalResultHandler = interface
['{0CC5A5D0-1545-4257-A936-AD777E0DAFCF}']
procedure SemiModalFormClosed(Form: TForm);
end;
TTransparentPanel = class(TCustomPanel)
private
FBackground: TBitmap;
FBlendColor: TColor;
FBlendAlpha: Byte;
procedure ColorBlend(const ACanvas: TCanvas; const ARect: TRect; const ABlendColor: TColor; const ABlendValue: Byte);
procedure SetBlendAlpha(const Value: Byte);
procedure SetBlendColor(const Value: TColor);
protected
procedure CaptureBackground;
procedure Paint; override;
procedure WMEraseBkGnd(var msg: TWMEraseBkGnd); message WM_ERASEBKGND;
procedure WMMove(var Message: TMessage); message WM_MOVE;
procedure WMParentNotify(var Message: TWMParentNotify); message WM_PARENTNOTIFY;
public
constructor Create(aOwner: TComponent); override;
destructor Destroy; override;
procedure ClearBackground;
procedure SetBounds(ALeft, ATop, AWidth, AHeight: Integer); override;
published
property BlendColor: TColor read FBlendColor write SetBlendColor;
property BlendAlpha: Byte read FBlendAlpha write SetBlendAlpha;
property Align;
property Alignment;
property Anchors;
end;
TSemiModalForm = class(TComponent)
strict private
FFormParent: TWinControl;
FBlendColor: TColor;
FBlendAlpha: Byte;
FSemiModalResultHandler: ISemiModalResultHandler;
FForm: TForm;
FTransparentPanel: TTransparentPanel;
FOldFormOnClose: TCloseEvent;
private
procedure OnTransparentPanelResize(Sender: TObject);
procedure RepositionForm;
procedure SetFormParent(const Value: TWinControl);
procedure OnFormClose(Sender: TObject; var Action: TCloseAction);
protected
procedure Notification(AComponent: TComponent; Operation: TOperation); override;
public
procedure ShowSemiModalForm(AForm: TForm; SemiModalResultHandler: ISemiModalResultHandler); virtual;
property ModalPanel: TTransparentPanel read FTransparentPanel;
published
constructor Create(AOwner: TComponent); override;
property BlendColor: TColor read FBlendColor write FBlendColor;
property BlendAlpha: Byte read FBlendAlpha write FBlendAlpha;
property FormParent: TWinControl read FFormParent write SetFormParent;
end;
implementation
procedure TTransparentPanel.CaptureBackground;
var
canvas: TCanvas;
dc: HDC;
sourcerect: TRect;
begin
FBackground := TBitmap.Create;
with Fbackground do
begin
width := clientwidth;
height := clientheight;
end;
sourcerect.TopLeft := ClientToScreen(clientrect.TopLeft);
sourcerect.BottomRight := ClientToScreen(clientrect.BottomRight);
dc := CreateDC('DISPLAY', nil, nil, nil);
try
canvas := TCanvas.Create;
try
canvas.handle := dc;
Fbackground.Canvas.CopyRect(clientrect, canvas, sourcerect);
finally
canvas.handle := 0;
canvas.free;
end;
finally
DeleteDC(dc);
end;
end;
constructor TTransparentPanel.Create(aOwner: TComponent);
begin
inherited;
ControlStyle := controlStyle - [csSetCaption];
FBlendColor := clWhite;
FBlendAlpha := 200;
end;
destructor TTransparentPanel.Destroy;
begin
FreeAndNil(FBackground);
inherited;
end;
procedure TTransparentPanel.Paint;
begin
if csDesigning in ComponentState then
inherited
end;
procedure TTransparentPanel.SetBounds(ALeft, ATop, AWidth, AHeight: Integer);
begin
if (Visible) and
(HandleAllocated) and
(not (csDesigning in ComponentState)) then
begin
FreeAndNil(Fbackground);
Hide;
inherited;
Parent.Update;
Show;
end
else
inherited;
end;
procedure TTransparentPanel.WMEraseBkGnd(var msg: TWMEraseBkGnd);
var
ACanvas: TCanvas;
begin
if csDesigning in ComponentState then
inherited
else
begin
if not Assigned(FBackground) then
Capturebackground;
ACanvas := TCanvas.create;
try
ACanvas.handle := msg.DC;
ACanvas.draw(0, 0, FBackground);
ColorBlend(ACanvas, Rect(0, 0, Width, Height), FBlendColor, FBlendAlpha);
finally
FreeAndNil(ACanvas);
end;
msg.result := 1;
end;
end;
procedure TTransparentPanel.WMMove(var Message: TMessage);
begin
CaptureBackground;
end;
procedure TTransparentPanel.WMParentNotify(var Message: TWMParentNotify);
begin
CaptureBackground;
end;
procedure TTransparentPanel.ClearBackground;
begin
FreeAndNil(FBackground);
end;
procedure TTransparentPanel.ColorBlend(const ACanvas: TCanvas; const ARect: TRect;
const ABlendColor: TColor; const ABlendValue: Byte);
var
BMP: TBitmap;
begin
BMP := TBitmap.Create;
try
BMP.Canvas.Brush.Color := ABlendColor;
BMP.Width := ARect.Right - ARect.Left;
BMP.Height := ARect.Bottom - ARect.Top;
BMP.Canvas.FillRect(Rect(0,0,BMP.Width, BMP.Height));
ACanvas.Draw(ARect.Left, ARect.Top, BMP, ABlendValue);
finally
FreeAndNil(BMP);
end;
end;
procedure TTransparentPanel.SetBlendAlpha(const Value: Byte);
begin
FBlendAlpha := Value;
Paint;
end;
procedure TTransparentPanel.SetBlendColor(const Value: TColor);
begin
FBlendColor := Value;
Paint;
end;
{ TSemiModalForm }
constructor TSemiModalForm.Create(AOwner: TComponent);
begin
inherited;
FBlendColor := clWhite;
FBlendAlpha := 150;
FTransparentPanel := TTransparentPanel.Create(Self);
end;
procedure TSemiModalForm.SetFormParent(const Value: TWinControl);
begin
FFormParent := Value;
end;
procedure TSemiModalForm.ShowSemiModalForm(AForm: TForm;
SemiModalResultHandler: ISemiModalResultHandler);
begin
if FForm = nil then
begin
FForm := AForm;
FSemiModalResultHandler := SemiModalResultHandler;
FTransparentPanel.Align := alClient;
FTransparentPanel.BringToFront;
FTransparentPanel.Parent := FFormParent;
FTransparentPanel.BlendColor := FBlendColor;
FTransparentPanel.BlendAlpha := FBlendAlpha;
FTransparentPanel.OnResize := OnTransparentPanelResize;
AForm.Parent := FTransparentPanel;
FOldFormOnClose := AForm.OnClose;
AForm.OnClose := OnFormClose;
RepositionForm;
AForm.Show;
FTransparentPanel.ClearBackground;
FTransparentPanel.Visible := TRUE;
end;
end;
procedure TSemiModalForm.OnFormClose(Sender: TObject; var Action: TCloseAction);
begin
FForm.OnClose := FOldFormOnClose;
try
FForm.Visible := FALSE;
FSemiModalResultHandler.SemiModalFormClosed(FForm);
finally
FForm.Parent := nil;
FForm := nil;
FTransparentPanel.Visible := FALSE;
end;
end;
procedure TSemiModalForm.Notification(AComponent: TComponent;
Operation: TOperation);
begin
inherited Notification(AComponent, Operation);
if (Operation = opRemove) then
begin
if AComponent = FFormParent then
SetFormParent(nil);
end;
end;
procedure TSemiModalForm.OnTransparentPanelResize(Sender: TObject);
begin
RepositionForm;
end;
procedure TSemiModalForm.RepositionForm;
begin
FForm.Left := (FTransparentPanel.Width div 2) - (FForm.Width div 2);
FForm.Top := (FTransparentPanel.Height div 2) - (FForm.Height div 2);
end;
end.
Can anybody help me with the problems or point me to an alpha blend panel that already exists?
Thanks for all your suggestions. I've taken the input and created a new component that does exactly what I need. Here's what it looks like:
The comment that pointed me in the right direction was the one by NGLN that I upvoted. If you post it as the answer I'll accept it.
I tried to add the component code to this answer, but StackOverflow wouldn't format it correctly. However, you can download the source and a full demo application here.
The component provides the following functionality:
The semi modal form is a child of the main form. This means that it
can be tabbed to just like the other controls.
The overlay area is drawn correctly with no artefacts.
The controls under the overlay area are automatically disabled.
The semi modal form/overlay can be shown/hidden if required e.g.
switching tabs.
A SemiModalResult is passed back in an event.
There are still a number of small issues that I would like to iron out. If anybody knows how to fix them, please let me know.
When the parent form is moved or resized it needs to call the
ParentFormMoved procedure. This allows the component to
resize/reposition the overlay form. Is there any way to hook into
the parent form and detect when it is moved?
If you mimimise the main form, then restore it, the overlay form appears immediately, then the main form is animated back to it's previous position. Is there a way to detect when the main form has finished animating?
The rounded corners of the semi modal window are not too pretty. I'm
not sure there's much that can be done about this as it's down to the
rectangular region.
Your code does not show the form modally, and I wonder why you would not. But then, maybe I do not understand the term semi modal.
In any case, I think the idea to create a half-transparent form on which to show the actual dialog will do just fine:
function ShowObviousModal(AForm: TForm; AParent: TWinControl = nil): Integer;
var
Layer: TForm;
begin
if AParent = nil then
AParent := Application.MainForm;
Layer := TForm.Create(nil);
try
Layer.AlphaBlend := True;
Layer.AlphaBlendValue := 128;
Layer.BorderStyle := bsNone;
Layer.Color := clWhite;
with AParent, ClientOrigin do
SetWindowPos(Layer.Handle, HWND_TOP, X, Y, ClientWidth, ClientHeight,
SWP_SHOWWINDOW);
Result := AForm.ShowModal;
finally
Layer.Free;
end;
end;
Usage:
procedure TForm1.Button1Click(Sender: TObject);
begin
FDialog := TForm2.Create(Self);
try
if ShowObviousModal(FDialog) = mrOk then
Caption := 'OK';
finally
FDialog.Free;
end;
end;

Why doesn't my custom component update when I change properties?

I have created a component, TGridPaintBox, based on TPaintBox. It is basically a paintbox with added "grid functionality". It's not a data grid. More like a chess board component.
In the object explorer I can set certain properties. Most importantly I can set the grid dimensions (how many cells across/down), but also options relating to drawing. Whether the cells should be square, the color of odd/even cells etc.
My first version of this component had properties directly on the class, and when I changed a property, the designtime drawing was updated immediately. As the component grew, I wanted to organize my properties a little better, and introduced some "options properties", like drawing options, behaviour options etc. After introducing this, the designtime drawing no longer updates like before. After changing a property, I have to click on the component for it to update. Can anyone tell me why this happens?
Here's a stripped down version of the code. I hope it will explain the behaviour:
(PS: This is my first component, even though I've been using Delphi since 1997, so if anyone can spot anything stupid in the way I've done it, please feel free to tell me)
unit GridPaintBox;
interface
type
TGridDrawOption = (gdoSquareCells,gdoCenterCells,gdoDrawCellEdges,gdoDrawFocus);
TGridDrawOptions = set of TGridDrawOption;
TGridOptions = class(TPersistent)
private
FCellsX : integer;
FCellsY : integer;
FDrawOptions : TGridDrawOptions;
public
constructor Create(aGridPaintBox : TGridPaintBox);
procedure Assign(Source : TPersistent); override;
published
property CellsX : integer read FCellsX write FCellsX;
property CellsY : integer read FCellsY write FCellsY;
property DrawOptions : TGridDrawOptions read FDrawOptions write FDrawOptions;
end;
TGridPaintBox = class(TPaintBox)
private
FGridOptions : TGridOptions;
FFocusedX,
FFocusedY : integer;
FOnFocusChanged: TNotifyEvent;
procedure CalculateSizeAndPosition;
procedure DrawCell(X,Y : integer);
procedure DrawCells;
procedure SetGridOptions(const Value: TGridOptions);
protected
procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); override;
public
constructor Create(aOwner : TComponent); override;
destructor Destroy; override;
procedure Paint; override;
procedure SetFocus(X,Y : integer);
published
property OnFocusChanged : TNotifyEvent read FOnFocusChanged write FOnFocusChanged;
property Options : TGridOptions read FGridOptions write SetGridOptions;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('Samples', [TGridPaintBox]);
end;
procedure TGridPaintBox.CalculateSizeAndPosition;
begin
<...>
end;
constructor TGridPaintBox.Create(aOwner: TComponent);
begin
inherited;
FGridOptions := TGridOptions.Create(self);
end;
procedure TGridPaintBox.DrawCell(X, Y: integer);
begin
<...>
end;
procedure TGridPaintBox.DrawCells;
var
X,Y : integer;
begin
CalculateSizeAndPosition;
for Y := 0 to FGridOptions.CellsY-1 do
for X := 0 to FGridOptions.CellsX-1 do
DrawCell(X,Y);
end;
procedure TGridPaintBox.Paint;
begin
Canvas.Font := Font;
Canvas.Brush.Color := Color;
Canvas.Brush.Style := bsSolid;
Canvas.FillRect(Rect(0,0,Width,Height));
DrawCells;
if Assigned(OnPaint) then
OnPaint(Self);
end;
procedure TGridPaintBox.SetGridOptions(const Value: TGridOptions);
begin
FGridOptions.Assign(Value);
invalidate;
end;
procedure TGridPaintBox.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
SetFocus(PixelToCellX(X),PixelToCellY(Y));
inherited;
end;
procedure TGridPaintBox.SetFocus(X, Y: integer);
begin
if (FocusedX=X) and (FocusedY=Y) then
exit;
FFocusedX := X;
FFocusedY := Y;
if assigned(OnFocusChanged) then
OnFocusChanged(self);
invalidate;
end;
constructor TGridOptions.Create(aGridPaintBox : TGridPaintBox);
begin
FCellsX := 20;
FCellsY := 8;
FDrawOptions := [gdoSquareCells,gdoCenterCells,gdoDrawCellEdges];
end;
procedure TGridOptions.Assign(Source : TPersistent);
begin
if Source is TGridOptions then
begin
FCellsX := TGridOptions(Source).CellsX;
FCellsY := TGridOptions(Source).CellsY;
FDrawOptions := TGridOptions(Source).DrawOptions;
end
else
inherited;
end;
end.
It happens because you don't have a setter for the options set which would invalidate your control which belongs to. The click in the form designer invokes the control to invalidate though, but you should handle this by your own in such options setter. So I would store the options owner for better access to the direct owner class instance and in the options setter force this owner, the control to redraw:
type
TGridPaintBox = class;
TGridDrawOption = (gdoSquareCells, gdoCenterCells, gdoDrawCellEdges, gdoDrawFocus);
TGridDrawOptions = set of TGridDrawOption;
TGridOptions = class(TPersistent)
private
FOwner: TGridPaintBox;
FCellsX: Integer;
FCellsY: Integer;
FDrawOptions: TGridDrawOptions;
procedure SetCellsX(AValue: Integer);
procedure SetCellsY(AValue: Integer);
procedure SetDrawOptions(const AValue: TGridDrawOptions);
public
constructor Create(AOwner: TGridPaintBox);
procedure Assign(ASource: TPersistent); override;
published
property CellsX: Integer read FCellsX write SetCellsX;
property CellsY: Integer read FCellsY write SetCellsY;
property DrawOptions: TGridDrawOptions read FDrawOptions write SetDrawOptions;
end;
implementation
constructor TGridOptions.Create(AOwner: TGridPaintBox);
begin
FOwner := AOwner;
FCellsX := 20;
FCellsY := 8;
FDrawOptions := [gdoSquareCells, gdoCenterCells, gdoDrawCellEdges];
end;
procedure TGridOptions.SetCellsX(AValue: Integer);
begin
if FCellsX <> AValue then
begin
FCellsX := AValue;
FOwner.Invalidate;
end;
end;
procedure TGridOptions.SetCellsY(AValue: Integer);
begin
if FCellsY <> AValue then
begin
FCellsY := AValue;
FOwner.Invalidate;
end;
end;
procedure TGridOptions.SetDrawOptions(const AValue: TGridDrawOptions);
begin
if FDrawOptions <> AValue then
begin
FDrawOptions := AValue;
FOwner.Invalidate;
end;
end;
Further notes:
If you don't explicitly need to have paint box' published properties and events like for instance Color, Font or OnPaint event, derive your control from TGraphicControl instead of from TPaintBox. You can choose what properties and events will you publish by your own.

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;

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