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

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.

Related

Custom Component OnMouseDown not working;

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.

Access violation assigning autocomplete strings to

I am modifying the edit control with autocomplete from here:
Auto append/complete from text file to an edit box delphi
I want to load autocomplete strings from DB. I declared new properties on autocomplete control descendant:
FACDataSource : TDataSource;
FACFieldName : string;
I call this to load autocomplete strings:
procedure TAutoCompleteEdit.ReadSuggestions;
begin
FAutoCompleteSourceList.Clear;
if (not Assigned(FACDataSource)) or (not Assigned(FACDataSource.DataSet)) or (not ACEnabled) then
exit;
with FACDataSource.DataSet do
begin
if Active and (RecordCount > 0) and (FACFieldName <> '') then
begin
First;
while not EOF do
begin
FAutoCompleteSourceList.Add(FACDataSource.DataSet.FieldByName(FACFieldName).AsString);
Next;
end;
if FAutoCompleteSourceList.Count > 0 then
ACStrings := FAutoCompleteSourceList;
end;
end;
end;
However, I get AccessViolation when assigning FAutoCompleteSourceList to ACStrings. The setter for ACStrings is:
procedure TAutoCompleteEdit.SetACStrings(const Value: TStringList);
begin
if Value <> FACList.FStrings then
FACList.FStrings.Assign(Value);
end;
I get AccessViolation in the line: FACList.FStrings.Assign(Value); (READ of address XXXYYY). Value is defined and not garbage at that point (e.g. in I can view the string list in the debugger). 'FStrings' is an empty stringlist.
It works fine when the control is dropped on the form. But doesn't if I place it within a custom inplace editor shown when user enters a DBGridEH cell.
The inplace editor is like this:
unit UInplaceAutoCompleteEditor;
interface
uses UDBAutoComplete, UMyInplaceEditor, classes, windows, Controls, Buttons, DB;
type TInplaceAutoCompleteEditor = class(TMyInplaceEditor)
private
FEditor : TAutoCompleteEdit;
FButton : TSpeedButton;
FShowButton : boolean;
procedure SetShowButton(value : boolean);
public
constructor Create(AOwner : TComponent); override;
procedure SetFocus; override;
destructor Destroy; override;
protected
procedure EditorKeyDown(Sender : TObject; var Key : Word; Shift : TShiftState);
function GetACDataSource : TDataSource;
procedure SetACDataSource(value : TDataSource);
function GetACFieldName : string;
procedure SetACFieldName(value : string);
procedure SetACEnabled(value : boolean);
function GetACEnabled : boolean;
published
property Editor : TAutoCompleteEdit read FEditor;
property ACDataSource : TDataSource read GetACDataSource write SetACDataSource;
property ACFieldName : string read GetACFieldName write SetACFieldName;
property ACEnabled : boolean read GetACEnabled write SetACEnabled;
property Button : TSpeedButton read FButton;
property ShowButton : boolean read FShowButton write SetShowButton;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('nikolaev', [ TInplaceAutoCompleteEditor ]);
end;
{ TInplaceAutoCompleteEditor }
constructor TInplaceAutoCompleteEditor.Create(AOwner: TComponent);
begin
inherited;
FEditor := TAutoCompleteEdit.Create(self);
FEditor.Parent := self;
FEditor.Align := alClient;
FEditor.Visible := true;
FEditor.WantTabs := true;
FEditor.OnKeyDown := EditorKeyDown;
FButton := TSpeedButton.Create(self);
FButton.Parent := self;
FButton.Align := alRight;
self.FOwnHeight := -1;
self.FOwnWidth := -1;
SetShowButton(false);
end;
destructor TInplaceAutoCompleteEditor.Destroy;
begin
Feditor.Destroy;
FButton.Destroy;
inherited;
end;
procedure TInplaceAutoCompleteEditor.EditorKeyDown(Sender: TObject;
var Key: Word; Shift: TShiftState);
begin
if Key in [ VK_Return, VK_Tab ] then
begin
self.Value := FEditor.Text;
Key := 0;
ConfirmValue;
end;
if Key = VK_Escape then
begin
Key := 0;
CancelValue;
end;
inherited;
end;
function TInplaceAutoCompleteEditor.GetACDataSource: TDataSource;
begin
Result := FEditor.ACDataSource;
end;
function TInplaceAutoCompleteEditor.GetACEnabled: boolean;
begin
Result := FEditor.ACEnabled;
end;
function TInplaceAutoCompleteEditor.GetACFieldName: string;
begin
Result := FEditor.ACFieldName
end;
procedure TInplaceAutoCompleteEditor.SetACDataSource(value: TDataSource);
begin
FEditor.ACDataSource := value;
end;
procedure TInplaceAutoCompleteEditor.SetACEnabled(value: boolean);
begin
FEditor.ACEnabled := value;
end;
procedure TInplaceAutoCompleteEditor.SetACFieldName(value: string);
begin
FEditor.acfieldname := value;
end;
procedure TInplaceAutoCompleteEditor.SetFocus;
begin
inherited;
FEditor.SetFocus;
end;
procedure TInplaceAutoCompleteEditor.SetShowButton(value: boolean);
begin
if value <> FShowButton then
begin
FShowButton := value;
FButton.Visible := value;
end;
end;
end.
This inplace editor inherits from an abstract class like this:
unit UMyInplaceEditor;
interface
uses Windows, classes, types, dbGridEh, ExtCtrls, Controls;
type TMyInplaceEditor = class (TWinControl)
private
FOnValueConfirmed : TNotifyEvent;
FOnCanceled : TNotifyEvent;
FWantTabs : boolean;
procedure AdjustPosition;
protected
FOwnHeight, FOwnWidth : integer;
FValue : Variant;
function GetIsEditing : boolean;
procedure SetIsEditing(value : boolean); virtual;
procedure ConfirmValue;
procedure CancelValue;
procedure SetValue(val : Variant); virtual;
public
property OnValueConfirmed : TNotifyEvent read FOnValueConfirmed write FOnValueConfirmed;
property OnCanceled : TNotifyEvent read FOnCanceled write FOnCanceled;
property Value : Variant read FValue write SetValue;
property IsEditing : boolean read GetIsEditing write SetIsEditing;
procedure SetPosition(parentControl : TWinControl; rect : TRect); virtual;
function ColumnEditable(column : TColumnEH) : boolean; virtual;
constructor Create(AOwner : TComponent); override;
property WantTabs : boolean read FWantTabs write FWantTabs;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('nikolaev', [TMyInplaceEditor]);
end;
constructor TMyInplaceEditor.Create(AOwner : TComponent);
begin
inherited Create(AOwner);
self.AutoSize := false;
self.Visible := false;
self.FOwnHeight := -1;
self.FOwnWidth := -1;
end;
procedure TMyInplaceEditor.AdjustPosition;
var xOffset, yOffset : integer;
begin
xoffset := self.Left + self.Width - self.Parent.Width;
if xOffset > 0 then
self.Left := self.Left - xOffset;
yOffset := self.Top + self.Height - self.Parent.height;
if yOffset > 0 then
self.Top := self.Top - yOffset;
end;
function TMyInplaceEditor.GetIsEditing : boolean;
begin
Result := self.Visible;
end;
procedure TMyInplaceEditor.SetIsEditing(value: Boolean);
begin
self.Visible := value;
self.BringToFront;
{if Visible then
self.SetFocus;}
end;
procedure TMyInplaceEditor.SetPosition(parentControl : TWinControl; rect: TRect);
begin
self.Parent := parentControl;
self.Top := rect.Top;//parentControl.Top;
self.Left := rect.Left;//parentControl.left;
if self.FOwnWidth = -1 then
self.Width := rect.Right - rect.Left
else
self.Width := self.FOwnWidth;
if self.FOwnHeight = -1 then
self.Height := rect.Bottom - rect.Top
else
self.Height := self.FOwnHeight;
AdjustPosition;
end;
function TMyInplaceEditor.ColumnEditable(column : TColumnEH) : boolean;
begin
Result := true;
end;
procedure TMyInplaceEditor.ConfirmValue;
begin
if Assigned(FOnValueConfirmed) then
FOnValueConfirmed(self);
end;
procedure TMyInplaceEditor.CancelValue;
begin
if Assigned(FOnCanceled) then
FOnCanceled(self);
end;
procedure TMyInplaceEditor.SetValue(val : Variant);
begin
FValue := val;
end;
end.
The InplaceEditor is used in a descendant from DBGridEH. I override ShowEditor and HideEditor to show / hide my editor in certain cases.
Again, the autocomplete control only throws exception when embedded in the inplaceeditor control.
What causes access violation?
The problem is that the code you are using mis-handles interface reference counting. Here are the relevant extracts:
type
TEnumString = class(TInterfacedObject, IEnumString)
....
Note that this class is derived from TInterfacedObject and so it manages its lifetime using reference counting.
Then the code goes on like this:
type
TAutoCompleteEdit = class(TEdit)
private
FACList: TEnumString;
....
So we are going to hold a reference to the object rather than the interface. That looks dubious already.
Then we do this:
constructor TAutoCompleteEdit.Create(AOwner: TComponent);
begin
inherited;
FACList := TEnumString.Create;
....
end;
destructor TAutoCompleteEdit.Destroy;
begin
FACList := nil;
inherited;
end;
There's nothing here to keep the object alive. At other points in the code we take a reference to the IEnumString interface. But then as soon as that reference is released, the object thinks that there are no references left. And so it is deleted. Then, later on, the code refers to FACList which now points at an object that has been destroyed.
A simple way to fix this would be to make sure that the TAutoCompleteEdit control always holds a reference to the interface:
type
TAutoCompleteEdit = class(TEdit)
private
FACList: TEnumString;
FEnumString: IEnumString;
....
constructor TAutoCompleteEdit.Create(AOwner: TComponent);
begin
inherited;
FACList := TEnumString.Create;
FEnumString := FACList;
....
end;
And with this change you can then remove the destructor for TAutoCompleteEdit since the object behind FEnumString will get destroyed by the reference counting mechanism.
Another way to fix this would be to change TEnumString to disable automatic reference counting. That would look like this:
type
TEnumString = class(TObject, IInterface, IEnumString)
private
function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
....
end;
function TEnumString.QueryInterface(const IID: TGUID; out Obj): HResult;
begin
if GetInterface(IID, Obj) then
Result := 0
else
Result := E_NOINTERFACE;
end;
function TEnumString._AddRef: Integer;
begin
Result := -1;
end;
function TEnumString._Release: Integer;
begin
Result := -1;
end;
And then you'd need the TAutoCompleteEdit destructor to look like this:
destructor TAutoCompleteEdit.Destroy;
begin
FACList.Free;
inherited;
end;
And a final option would be to avoid holding a TEnumString at all and instead only hold an IEnumString reference. Let the reference counting manage lifetime as in the first solution. But then you'd need to implement another interface that allowed the TAutoCompleteEdit to obtain the TStrings object.

Keep values from the beforepost event to the afterpost event

I am writing this question for Delphi 2007, but I'm pretty sure that this is a common problem in all kind of languages.
So, I have a project where I need to keep informations about the old and new value of certain fields (which are given in the BeforePost event of the dataset I'm working with) and use them in the AfterPost event.
For now, I have been using global variables, but there is already so many of them in the project that this is becoming a real issue when it comes to managing documentation and/or comments.
Basically, I am asking if there is a better way (in Delphi 2007 or in general) to keep the informations from the BeforePost event of a Dataset and get them back in the AfterPost event.
first create a new Custom Data Source
TDataRecord = array of record
FieldName: string;
FieldValue: Variant;
end;
TMyDataSource = class(TDataSource)
private
LastValues: TDataRecord;
procedure MyDataSourceBeforePost(DataSet: TDataSet);
procedure SetDataSet(const Value: TDataSet);
function GetDataSet: TDataSet;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
function GetLastValue(FieldName: string): Variant;
property MyDataSet: TDataSet read GetDataSet write SetDataSet;
end;
{ TMyDataSource }
constructor TMyDataSource.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
end;
destructor TMyDataSource.Destroy;
begin
SetLength(LastValues, 0);
inherited Destroy;
end;
function TMyDataSource.GetDataSet: TDataSet;
begin
Result := DataSet;
end;
procedure TMyDataSource.SetDataSet(const Value: TDataSet);
begin
DataSet := Value;
DataSet.BeforePost := MyDataSourceBeforePost;
end;
procedure TMyDataSource.MyDataSourceBeforePost(DataSet: TDataSet);
var
i: integer;
begin
SetLength(LastValues, DataSet.FieldCount);
for i:=0 to DataSet.FieldCount-1 do
begin
LastValues[i].FieldName := DataSet.Fields.Fields[i].FieldName;
LastValues[i].FieldValue := DataSet.Fields.Fields[i].OldValue;
end;
end;
function TMyDataSource.GetLastValue(FieldName: string): Variant;
var
i: integer;
begin
Result := Null;
for i:=0 to Length(LastValues)-1 do
if SameText(FieldName, LastValues[i].FieldName) then
begin
Result := LastValues[i].FieldValue;
break;
end;
end;
and after override application Data Source
TForm1 = class(TForm)
private
MyDataSource: TMyDataSource;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
ADOQuery1.Active := true;
MyDataSource := TMyDataSource.Create(Self);
MyDataSource.MyDataSet := ADOQuery1;
DBGrid1.DataSource := MyDataSource;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
MyDataSource.Free;
end;
procedure TForm1.ADOQuery1AfterPost(DataSet: TDataSet);
var
AValue: Variant;
begin
AValue := MyDataSource.GetLastValue('cname');
if not VarIsNull(AValue) then;
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 :)

I need help on how to implement class that can be shown in object Inspector

i have
...
TDispPitch = class
private
iLineSize: Integer;
iLineColor: TColor;
bDisplayAccent: Boolean;
bVisible: Boolean;
published
property LineSize : Integer read iLineSize write iLineSize;
...etc
end;
...
and i wanted this feature shown in Object Insepector to edit the settings.
i try adding
property DispPitch: TDispPitch read FDispPitch write FDispPitch. like
the DispPitch can be shown but i cannot see its properties. like LineSize, LineColor etc.
You must derive your class from TPersistent, or a descendant, in order to make it available in the Object Inspector:
TDispPitch = class(TPersistent)
private
...
published
property ...
...
end;
From Delphi Documentation:
TPersistent is the ancestor for all
objects that have assignment and
streaming capabilities.
The class needs to derive from TPersistent, and should implement the Assign() (or AssignTo()) method, as well as expose an OnChange event so the containing class can react to changes, eg:
type
TDispPitch = class(TPersistent)
private
iLineSize: Integer;
iLineColor: TColor;
bDisplayAccent: Boolean;
bVisible: Boolean;
FOnChange: TNotifyEvent;
procedure Changed;
procedure SetLineSize(Value : Integer);
procedure SetLineColor(Value: TColor);
procedure SetDisplayAccent(Value: Boolean);
procedure SetVisible(Value: Boolean);
public
procedure Assign(Source: TPersistent); override;
property OnChange: TNotifyEvent read FOnChange write FOnChange;
published
property LineSize : Integer read iLineSize write SetLineSize;
property LineColor: TColor read iLineColor write SetLineColor;
property DisplayAccent: Boolean read bDisplayAccent write SetDisplayAccent;
property Visible: Boolean read bVisible write SetVisible;
end;
procedure TDispPitch.Assign(Source: TPersistent);
var
LSource: TDispPitch;
begin
if Source is TDispPitch then
begin
LSource := TDispPitch(Source);
iLineSize := LSource.LineSize;
iLineColor := LSource.LineColor;
bDisplayAccent := LSource.DisplayAccent;
bVisible := LSource.Visible;
Changed;
end else
inherited;
end;
procedure TDispPitch.Changed;
begin
if FOnChange <> nil then FOnChange(Self);
end;
procedure TDispPitch.SetLineSize(Value : Integer);
begin
if iLineSize <> Value then
begin
iLineSize := Value;
Changed;
end;
end;
procedure TDispPitch.SetLineColor(Value: TColor);
begin
if iLineColor <> Value then
begin
iLineColor := Value;
Changed;
end;
end;
procedure TDispPitch.SetDisplayAccent(Value: Boolean);
begin
if bDisplayAccent <> Value then
begin
bDisplayAccent := Value;
Changed;
end;
end;
procedure TDispPitch.SetVisible(Value: Boolean);
begin
if bVisible <> Value then
begin
bVisible := Value;
Changed;
end;
end;
Then you use it like this:
type
TSomeOtherClass = class(...)
private
FDispPitch: TDispPitch;
procedure DispPitchChanged(Sender: TObject);
procedure SetDispPitch(Value: TDispPitch);
public
constructor Create; override;
destructor Destroy; override;
published
property DispPitch: TDispPitch read FDispPitch write SetDispPitch;
end;
constructor TSomeOtherClass.Create;
begin
inherited;
FDispPitch := TDispPitch.Create;
end;
destructor TSomeOtherClass.Destroy;
begin
FDispPitch.Free;
inherited;
end;
procedure TSomeOtherClass.DispPitchChanged(Sender: TObject);
begin
... use new FDispPitch values as needed...
end;
procedure TSomeOtherClass.SetDispPitch(Value: TDispPitch);
begin
FDispPitch.Assign(Value);
end;

Resources