Getting column index of a clicked control in TGridPanel - delphi

I'm using a TGridPanel to hold some panels. At design time, I've set the grid panel to have 1 row and 5 columns.
I can add a panel to the grid using this code, which works well:
procedure TForm6.AddPanelToGrid(const ACaption: string);
var
pnl: TPanel;
begin
pnl := TPanel.Create(gpOne);
pnl.Caption := ACaption;
pnl.Parent := gpOne;
pnl.Name := 'pnlName' + ACaption;
pnl.OnClick := gpOne.OnClick;
pnl.ParentBackground := false;
pnl.ParentColor := false;
pnl.Color := clLime;
pnl.Font.Size := 14;
gpOne.ControlCollection.AddControl(pnl);
pnl.Height := pnl.Width;
end;
What I want to do is remove a TPanel from the grid when I click on it (which is why I have set the on click handler to that of the grid panel in the above code).
In that click handler I do this, which almost works:
procedure TForm6.gpOneClick(Sender: TObject);
begin
if not (sender is TPanel) then exit;
gpOne.ControlCollection.RemoveControl(Sender as TPanel);
(Sender as TPanel).Free;
gpOne.UpdateControlsColumn( 0 ); <<<-------
gpOne.UpdateControlsRow(0);
gpOne.Refresh();
end;
Using a parameter for UpdateControlColumn() causes the order of the panels in the grid to change - the first and second swap places.
I can get around this by adding the column idex to the panel's tag property, then pass that to UpdateControlColumn(). This then works, but once a panel has been removed the higher tag numbers are no longer valid - the panels have moved column.
So, how can I get the column that a panel is in from within the OnClick handler?
I'm using Delphi 10.1 Berlin - if that makes any difference.
To test this, I started a new project, added a TGridPanel, set it to have 1 row and 5 equally widthed columns. I added 6 TButton controls and created an OnClick handler for each with the following code:
AddPanelToGrid('One'); // changing the string for each button.
Click a few buttons to add some panels, then click the panels to remove them.

TCustomGridPanel has a pair of useful functions, CellIndexToCell() and CellToCellIndex, but they are not public and thus not directly accessible from a TGridPanel.
To make them available declare TGridPanel anew as below:
type
TGridPanel = class(Vcl.ExtCtrls.TGridPanel) // add this
end; // -"-
TForm27 = class(TForm)
Button1: TButton;
gpOne: TGridPanel;
...
end;
Then add rand c variables for row and col, add the call to CellIndexToCell() and use c as argument for UpdateControlsColumn:
procedure TForm27.gpOneClick(Sender: TObject);
var
r, c: integer;
begin
if not (sender is TPanel) then exit;
gpOne.CellIndexToCell(gpOne.ControlCollection.IndexOf(Sender as TPanel), c, r); // add this
gpOne.ControlCollection.RemoveControl(Sender as TPanel);
(Sender as TPanel).Free;
gpOne.UpdateControlsColumn( c ); // <<<-------
gpOne.UpdateControlsRow(0);
gpOne.Refresh();
end;
And follow advise of Remy Lebeau, regarding freeing the panel. ( I just noticed his comment).
If you haven't already, you may also want to take a look at TFlowPanel and its FlowStyle property. TflowPanel reordering after deletion is more predictable if you use more than one row, but depends of course on what you need.

Related

Update corresponding label depending on which combobox fired the event

I have a program with n ComboBoxes and n Labels and I want to update the corresponding Label depending on the selection from the adjacent ComboBox i.e ComboBox2 would update Label2.
I am using the same event handler for every ComboBox and currently checking if Combobox1 or Combobox2 has fired the event handler. Is there a way to use the ItemIndex of the ComboBox passed to the procedure, such as Sender.ItemIndex? This is not currently an option and gives the error 'TObject' does not contain a member named 'ItemIndex'.
procedure TForm2.ComboBoxChange(Sender: TObject);
begin
if Sender = ComboBox1 then
Label1.Caption := ComboBox1.Items.Strings[ComboBox1.ItemIndex]
else
Label2.Caption := ComboBox2.Items.Strings[ComboBox2.ItemIndex];
end;
This code has the desired behavior but is obviously not scale-able.
Every component has a Tag property inherited from TComponent, where the Tag is a pointer-sized integer. As such, you can store each TLabel pointer directly in the corresponding TComboBox.Tag, eg:
procedure TForm2.FormCreate(Sender: TObject);
begin
ComboBox1.Tag := NativeInt(Label1);
ComboBox2.Tag := NativeInt(Label2);
end;
This way, ComboBoxChange() can then directly access the TLabel of the changed TComboBox, eg:
procedure TForm2.ComboBoxChange(Sender: TObject);
var
CB: TComboBox;
begin
CB := TComboBox(Sender);
if CB.Tag <> 0 then
TLabel(CB.Tag).Caption := CB.Items.Strings[CB.ItemIndex];
end;
Option 1
This is the most robust one.
Let your form have private members
private
FControlPairs: TArray<TPair<TComboBox, TLabel>>;
procedure InitControlPairs;
and call InitControlPairs when the form is created (either in its constructor, or in its OnCreate handler):
procedure TForm1.InitControlPairs;
begin
FControlPairs :=
[
TPair<TComboBox, TLabel>.Create(ComboBox1, Label1),
TPair<TComboBox, TLabel>.Create(ComboBox2, Label2),
TPair<TComboBox, TLabel>.Create(ComboBox3, Label3)
]
end;
You need to add the controls to this array manually. That's the downside of this approach. But you only need to do this once, right here. Then everything else can be done automagically.
Now, this is where it gets really nice: Let all your comboboxes share this OnChange handler:
procedure TForm1.ComboBoxChanged(Sender: TObject);
var
i: Integer;
begin
for i := 0 to High(FControlPairs) do
if FControlPairs[i].Key = Sender then
FControlPairs[i].Value.Caption := FControlPairs[i].Key.Text;
end;
Option 2
Forget about any private fields. Now instead make sure that each pair has a unique Tag. So the first combo box and label both have Tag = 1, the second pair has Tag = 2, and so on. Then you can do simply
procedure TForm1.ComboBoxChanged(Sender: TObject);
var
TargetTag: Integer;
CB: TComboBox;
i: Integer;
begin
if Sender is TComboBox then
begin
CB := TComboBox(Sender);
TargetTag := CB.Tag;
for i := 0 to ControlCount - 1 do
if (Controls[i].Tag = TargetTag) and (Controls[i] is TLabel) then
begin
TLabel(Controls[i]).Caption := CB.Text;
Break;
end;
end;
end;
as the shared combo-box event handler. The downside here is that you must be sure that you control the Tag properties of all your controls on the form (at least with the same parent as your labels). Also, they must all have the same parent control.

How can I refer to a control whose name is determined at runtime?

As a kind of self-study exercise, I've made a form which contains six panels in a 2x3 rectangle and I want them to switch between visible and invisible one after another. I'm trying to do so by using a for loop of some kind. I could of course write something like:
Panel1.Visible := true;
Panel1.Visible := false;
Panel2.Visible := true;
Panel2.Visible := false;
Panel3.Visible := true;
etc. etc.
But this takes quite a lot of typing and is pretty inefficient when I decide I want it to wait for 100ms between each step. For example, I'd then have to edit all the six steps to wait. This is doable for six steps, but maybe another time I want to do it a hundred times! So I'm thinking there must also be a way to use a for loop for this, where a variable varies from 1 to 6 and is used in the object identifier. So it would something like this:
for variable := 1 to 6 do begin
Panel + variable.Visible := true;
Panel + variable.Visible := false;
end;
Now, this obviously doesn't work, but I hope somebody here can tell me if this is in fact possible and if yes, how. Maybe I can use a string as the identifier? My explanation is probably pretty bad because I don't know all the technical terms but I hope the code explains something.
You can loop through the panel's Owner's Components array.
var
i: Integer;
TmpPanel: TPanel;
begin
{ This example loops through all of the components on the form, and toggles the
Visible property of each panel to the value that is opposite of what it has (IOW,
if it's True it's switched to False, if it's False it's switched to True). }
for i := 0 to ComponentCount - 1 do
if Components[i] is TPanel then
begin
TmpPanel := TPanel(Components[i]);
TmpPanel.Visible := not TmpPanel.Visible; // Toggles between true and false
end;
end;
You can also use the FindComponent method, if you want a very specific type of component by name. For instance, if you have the 6 panels, and their names are Panel1, Panel2, and so forth:
var
i: Integer;
TmpPanel: TPanel;
begin
for i := 1 to 6 do
begin
TmpPanel := FindComponent('Panel' + IntToStr(i)) as TPanel;
if TmpPanel <> nil then // We found it
TmpPanel.Visible := not TmpPanel.Visible;
end;
end;
This is a situation where you want to create the controls dynamically at runtime rather than at designtime. Trying to grapple with 6 different variables is just going to be a world of pain. And when you need the grid to be 3x4 rather than 2x3, you'll regret that decision even more.
So, start with a completely blank form. And add, in the code, a two dimensional array of panels:
private
FPanels: array of array of TPanel;
Then, in the form's constructor, or an OnCreate event handler, you can initialise the array by calling a function like this:
procedure TMyForm.InitialisePanels(RowCount, ColCount: Integer);
var
Row, Col: Integer;
aLeft, aTop, aWidth, aHeight: Integer;
Panel: TPanel;
begin
SetLength(FPanels, RowCount, ColCount);
aTop := 0;
for Row := 0 to RowCount-1 do begin
aLeft := 0;
aHeight := (ClientHeight-aTop) div (RowCount-Row);
for Col := 0 to ColCount-1 do begin
Panel := TPanel.Create(Self);
FPanels[Row, Col] := Panel;
Panel.Parent := Self;
aWidth := (ClientWidth-aLeft) div (ColCount-Col);
Panel.SetBounds(aLeft, aTop, aWidth, aHeight);
inc(aLeft, aWidth);
end;
inc(aTop, aHeight);
end;
end;
And now you can refer to your panels using cartesian coordinates rather than a flat one dimensional array. Of course, you can easily enough declare a flat one dimensional array as well if you want.
The key idea is that when you are creating large numbers of control in a structured layout, you are best abandoning the designer and using code (loops and arrays).
Use FindComponent method of TComponent:
for variable := 1 to 6 do begin
pnl := FindComponent('Panel' + IntToStr(variable));
if pnl is TPanel then
begin
TPanel(pnl).Visible := true;
TPanel(pnl).Visible := false;
end;
end;
As others have answered, FindComponent is the way to go.
But if you just want to modify generic properties for the component, such as visible, position etc, it's not necessary to compare to the type.
This will work just as fine:
for i := 1 to 16 do
begin
(FindComponent( 'P' + inttostr(i) ) as TControl).Visible := false;
end;
(NOTE: this is for Delphi 6/ 7, modern versions probably do this in other ways)
Actually my answer
If you use a name convention to name your component like
"Mycomponent" + inttostr(global_int)
you can use it to find it very easily :
function getMyComponent(id:integer) : TComponent;
begin
result := {Owner.}FindConponent('MyComponent'+inttostr(id));
end;
You also can make your generated components to interact each other by using (sender as TComponent).name to know which other component are related to him.
Exemple
Following is an example of what you can do with this :
Imagine a pagecontrol where tabs are an interface you want to have multiple time
(for ex, to describe columns in a file with 1 tab = 1 col, and you want to dynamically add tabs).
For our example, we are naming button and edit this way :
Button : "C_(column_number)_btn"
Edit : "C_(column_number)_edi"
You can actually refer directly to the edit with a buttonclick, linked at runtime by calling findcomponent :
procedure TForm1.ColBtnClick(Sender:TObject);
var nr : string; Edit : TEdit;
begin
// Name of the TButton. C(col)_btn
nr := (Sender as TButton).Name;
// Name of the TEdit C_(column)_edi
nr := copy(nr,1,length(nr)-3)+'edi';
// Get the edit component.
edit := (Form1.Findcomponent(nr) as TEdit);
//play with it
Edit.Enabled := Not Edit.Enabled ;
showmessage(Edit.Text);
Edit.hint := 'this hint have been set by clicking on the button';
//...
end;
Of course, you link this procedure to every generated buttons.
If anyone wants to practice with it, you may want to know how to generate the tabsheet and components, here you go :
procedure Form1.addCol(idcol:integer, owner : TComponent); // Form1 is a great owner imo
var
pan : TPanel; // Will be align client with the new tabsheet
c: TComponent; //used to create components on the pannel
tab : TTabSheet;
begin
try
pan := TPanel.create(owner);
pan.name := format('Panel_%d',[idcol]);
pan.caption := '';
// dynamically create that button
c := TButton.create(Owner);
with c as TButton do
begin
Name := format('C%d_btn',[idcol]);
Parent := pan;
//Top := foo;
//Left := bar;
caption := 'press me';
OnClick := Form1.ColBtnClick; // <<<<<<< link procedure to event
end;
//create a Tedit the same way
c := TEdit.create(Owner);
with c as TEdit do
Name := format('C%d_edi',[idcol]);
Parent := pan;
// other properties
// create the tabsheet and put the panel in
finally
tab := TTabSheet.Create(Parent);
tab.caption := 'Column %d';
tab.PageControl := Pagecontrol1;
pan.Parent := tab;
pan.Align := alClient;
end;
end;
Generating names to get the component is actually a very good way to have a clean code.
Scrolling through parent - child components in order to find the one you want is actually inefficient and becomes hell if there is many component (in my example, if there is 3, 10 or unknown number of TEdit looping child (brother) components will be ugly.
Maybe this example is useless but It may helps someone, someday.

Why don't child controls of a TStringGrid work properly?

I am placing checkboxes (TCheckBox) in a string grid (TStringGrid) in the first column. The checkboxes show fine, positioned correctly, and respond to mouse by glowing when hovering over them. When I click them, however, they do not toggle. They react to the click, and highlight, but finally, the actual Checked property does not change. What makes it more puzzling is I don't have any code changing these values once they're there, nor do I even have an OnClick event assigned to these checkboxes. Also, I'm defaulting these checkboxes to be unchecked, but when displayed, they are checked.
The checkboxes are created along with each record which is added to the list, and is referenced inside a record pointer which is assigned to the object in the cell where the checkbox is to be placed.
String grid hack for cell highlighting:
type
THackStringGrid = class(TStringGrid); //used later...
Record containing checkbox:
PImageLink = ^TImageLink;
TImageLink = record
...other stuff...
Checkbox: TCheckbox;
ShowCheckbox: Bool;
end;
Creation/Destruction of checkbox:
function NewImageLink(const AFilename: String): PImageLink;
begin
Result:= New(PImageLink);
...other stuff...
Result.Checkbox:= TCheckbox.Create(nil);
Result.Checkbox.Caption:= '';
end;
procedure DestroyImageLink(AImageLink: PImageLink);
begin
AImageLink.Checkbox.Free;
Dispose(AImageLink);
end;
Adding rows to grid:
//...after clearing grid...
//L = TStringList of original filenames
if L.Count > 0 then
lstFiles.RowCount:= L.Count + 1
else
lstFiles.RowCount:= 2; //in case there are no records
for X := 0 to L.Count - 1 do begin
S:= L[X];
Link:= NewImageLink(S); //also creates checkbox
Link.Checkbox.Parent:= lstFiles;
Link.Checkbox.Visible:= Link.ShowCheckbox;
Link.Checkbox.Checked:= False;
Link.Checkbox.BringToFront;
lstFiles.Objects[0,X+1]:= Pointer(Link);
lstFiles.Cells[1, X+1]:= S;
end;
Grid's OnDrawCell Event Handler:
procedure TfrmMain.lstFilesDrawCell(Sender: TObject; ACol, ARow: Integer;
Rect: TRect; State: TGridDrawState);
var
Link: PImageLink;
CR: TRect;
begin
if (ARow > 0) and (ACol = 0) then begin
Link:= PImageLink(lstFiles.Objects[0,ARow]); //Get record pointer
CR:= lstFiles.CellRect(0, ARow); //Get cell rect
Link.Checkbox.Width:= Link.Checkbox.Height;
Link.Checkbox.Left:= CR.Left + (CR.Width div 2) - (Link.Checkbox.Width div 2);
Link.Checkbox.Top:= CR.Top;
if not Link.Checkbox.Visible then begin
lstFiles.Canvas.Brush.Color:= lstFiles.Color;
lstFiles.Canvas.Brush.Style:= bsSolid;
lstFiles.Canvas.Pen.Style:= psClear;
lstFiles.Canvas.FillRect(CR);
if lstFiles.Row = ARow then
THackStringGrid(lstFiles).DrawCellHighlight(CR, State, ACol, ARow);
end;
end;
end;
Here's how it looks when clicking...
What could be causing this? It's definitely not changing the Checked property anywhere in my code. There's some strange behavior coming from the checkboxes themselves when placed in a grid.
EDIT
I did a brief test, I placed a regular TCheckBox on the form. Check/unchecks fine. Then, in my form's OnShow event, I changed the Checkbox's Parent to this grid. This time, I get the same behavior, not toggling when clicked. Therefore, it seems that a TCheckBox doesn't react properly when it has another control as its parent. How to overcome this?
TStringGrid's WMCommand handler doesn't allow children controls to handle messages (except for InplaceEdit).
So you can use e.g. an interposed class (based on code by Peter Below) or draw controls by hands, as some people have adviced. Here is the code of the interposed class:
uses
Grids;
type
TStringGrid = class(Grids.TStringGrid)
private
procedure WMCommand(var AMessage: TWMCommand); message WM_COMMAND;
end;
implementation
procedure TStringGrid.WMCommand(var AMessage: TWMCommand);
begin
if EditorMode and (AMessage.Ctl = InplaceEditor.Handle) then
inherited
else
if AMessage.Ctl <> 0 then
begin
AMessage.Result := SendMessage(AMessage.Ctl, CN_COMMAND,
TMessage(AMessage).WParam, TMessage(AMessage).LParam);
end;
end;
In Delphi7 at least I do this:
You need to draw a checkbox on the cell, and keep it in sync with an array of boolean (here fChecked[]) that indicates the state of the checkbox in each row. Then, in the DrawCell part of the TStringGrid:
var
cbstate: integer;
begin
...
if fChecked[Arow] then cbState:=DFCS_CHECKED else cbState:=DFCS_BUTTONCHECK;
DrawFrameControl(StringGrid.canvas.handle, Rect, DFC_BUTTON, cbState);
...
end;
To get the checkbox to respond to the space-bar, use the KeyDown event, and force a repaint:
if (Key = VK_SPACE) And (col=ColWithCheckBox) then begin
fChecked[row]:=not fChecked[row];
StringGrid.Invalidate;
key:=0;
end;
A similar approach is needed for the OnClick method.
Can u use VirtualTreeView in toReportMode (TListView emulating) mode instead of grid ?
Can u use TDBGrid over some in-memory table like NexusDB or TClientDataSet ?
Ugly approach would be presenting checkbox like a letter with a custom font - like WinDings or http://fortawesome.github.com/Font-Awesome
This latter is most easy to implement, yet most ugly to see and most inflexible to maintain - business logic gets intermixed into VCL event handlers

Finding out position of a control inside TGridPanel

How I can find out the position (row and column index) of controls inside TGridPanel? I'd like to use common OnClick event for number of buttons and need to know the X,Y position of the button.
I'm using Delphi 2007.
Unfortunately, because of the magic of TGridPanel, it is a little more complicated than just getting the Top and Left properties...
This should do it for any Control, adapt it to your needs:
procedure GetRowColumn(const AControl: TControl; var ARow, AColumn: Integer);
var
I: Integer;
begin
if AControl.Parent is TGridPanel then
begin
I := TGridPanel(AControl.Parent).ControlCollection.IndexOf(AControl);
if I > -1 then
begin
ARow := TGridPanel(AControl.Parent).ControlCollection[I].Row;
AColumn := TGridPanel(AControl.Parent).ControlCollection[I].Column;
end;
end;
end;
procedure TForm1.ButtonClick(Sender: TObject);
var
Row, Column : Integer;
begin
GetRowColumn(Sender as TControl, Row, Column);
// do something with Row and Column
ShowMessage( Format('row=%d - col=%d',[Row, Column]));
end;
You can use Sender cast as a tButton and then ask it for its top and left for example:
Procedure TForm1.OnClick(Sender:tObject);
var
X,Y : Integer;
begin
if Sender is TButton then
begin
X := TButton(Sender).Top;
Y := TButton(Sender).Left;
// do something with X & Y
end;
end;
Or if your just wanting to know what button was pressed, you can also use the TAG property to insert a number into each button, and then retrieve the tag value in your onclick event. Just remember to first set the Tag property to something. You can do this in the form designer if your just dropping buttons into the grid panel or in the routine your using to create and insert your buttons.
Procedure TForm1.OnClick(Sender:tObject);
var
iButton : integer;
begin
if Sender is TComponent then
begin
iButton := TComponent(Sender).Tag;
// do something with iButton
end;
end;
You can also use the tag property to store more than just an integer, since a pointer currently uses the same memory size as the integer you can cast a pointer to an integer and insert that value into the tag property. Just be aware that any pointer you place in this field is still treated as an integer. You are responsible for the memory it points to, it will not be managed by the component.

What is the best way in Delphi to show customized Message Dialogs?

I am using Delphi, and I want to show custom text in the buttons of a MessageDlg, as described here. What is the best way to do that?
Answering my own question.... I wrote the below unit which works well for me.
Delphi provides CreateMessageDialog() to give you a dialog template, which you can modify before displaying. I used that to create a function I called MessageDlgCustom, which takes the same parameters as a standard MessageDlg, but adds one more for replacement button titles.
It correctly handles custom fonts and automatically adjusts buttons to be wide enough for their message. If the buttons overflow the dialog, then that gets adjusted too.
After using that unit, the below sample works:
case MessageDlgCustom('Save your changes?',mtConfirmation,
[mbYes,mbNo,mbCancel],
['&Yes, I would like to save them with this absurdly long button',
'&No, I do not care about my stupid changes',
'&Arg! What are you talking about? Do not close the form!'],
nil) //nil = no custom font
of
mrYes:
begin
SaveChanges;
CloseTheForm;
end; //mrYes (save & close)
mrNo:
begin
CloseForm;
end; //mrNo (close w/o saving)
mrCancel:
begin
//do nothing
end; //mrCancel (neither save nor close)
end; //case
If someone else knows a better way, please share it.
unit CustomDialog;
interface
uses
Dialogs, Forms, Graphics, StdCtrls;
function MessageDlgCustom(const Msg: string; DlgType: TMsgDlgType;
Buttons: TMsgDlgButtons; ToCaptions: array of string;
customFont: TFont) : integer;
procedure ModifyDialog(var frm: TForm; ToCaptions : array of string;
customFont : TFont = nil);
implementation
uses
Windows, SysUtils;
function GetTextWidth(s: string; fnt: TFont; HWND: THandle): integer;
var
canvas: TCanvas;
begin
canvas := TCanvas.Create;
try
canvas.Handle := GetWindowDC(HWND);
canvas.Font := fnt;
Result := canvas.TextWidth(s);
finally
ReleaseDC(HWND,canvas.Handle);
FreeAndNil(canvas);
end; //try-finally
end;
function MessageDlgCustom(const Msg: string;
DlgType: TMsgDlgType; Buttons: TMsgDlgButtons; ToCaptions: array of string;
customFont: TFont): integer;
var
dialog : TForm;
begin
try
dialog := CreateMessageDialog(Msg, DlgType, Buttons);
dialog.Position := poScreenCenter;
ModifyDialog(dialog,ToCaptions,customFont);
Result := dialog.ShowModal;
finally
dialog.Release;
end; //try-finally
end;
procedure ModifyDialog(var frm: TForm; ToCaptions: array of string;
customFont: TFont);
const
c_BtnMargin = 10; //margin of button around caption text
var
i,oldButtonWidth,newButtonWidth,btnCnt : integer;
begin
oldButtonWidth := 0;
newButtonWidth := 0;
btnCnt := 0;
for i := 0 to frm.ComponentCount - 1 do begin
//if they asked for a custom font, assign it here
if customFont <> nil then begin
if frm.Components[i] is TLabel then begin
TLabel(frm.Components[i]).Font := customFont;
end;
if frm.Components[i] is TButton then begin
TButton(frm.Components[i]).Font := customFont;
end;
end;
if frm.Components[i] is TButton then begin
//check buttons for a match with a "from" (default) string
//if found, replace with a "to" (custom) string
Inc(btnCnt);
//record the button width *before* we changed the caption
oldButtonWidth := oldButtonWidth + TButton(frm.Components[i]).Width;
//if a custom caption has been provided use that instead,
//or just leave the default caption if the custom caption is empty
if ToCaptions[btnCnt - 1]<>'' then
TButton(frm.Components[i]).Caption := ToCaptions[btnCnt - 1];
//auto-size the button for the new caption
TButton(frm.Components[i]).Width :=
GetTextWidth(TButton(frm.Components[i]).Caption,
TButton(frm.Components[i]).Font,frm.Handle) + c_BtnMargin;
//the first button can stay where it is.
//all other buttons need to slide over to the right of the one b4.
if (1 < btnCnt) and (0 < i) then begin
TButton(frm.Components[i]).Left :=
TButton(frm.Components[i-1]).Left +
TButton(frm.Components[i-1]).Width + c_BtnMargin;
end;
//record the button width *after* changing the caption
newButtonWidth := newButtonWidth + TButton(frm.Components[i]).Width;
end; //if TButton
end; //for i
//whatever we changed the buttons by, widen / shrink the form accordingly
frm.Width := Round(frm.Width + (newButtonWidth - oldButtonWidth) +
(c_BtnMargin * btnCnt));
end;
end.
As an alternative you can use the Open Source SynTaskDialog unit. SynTaskDialog uses the Windows TaskDialog API natively on newer Windows versions and emulates it on older versions. You even can use it with FireMonkey.
For an example of a customizable MessageDlg function have a look at this answer.
You may have a look at the TDam component available on GitHub (https://github.com/digao-dalpiaz/Dam).
This component allows you to create customized Message Dialogs with pre-defined buttons, using formatted text (HTML Text), and allowing to customize a lot of aspects of dialogs.
Besides that, you can manage all your app dialogs into a "container", which stores all dialogs as objects (TDamMsg).
TDam Message Example
TDamMsg properties allows to customize message dialog, like:
Button1 - button 1 caption
Button2 - button 2 caption
Button3 - button 3 caption
Buttons: TDamMsgButtons = Defines the buttons in the message dialog:
dbOK: Defines one button OK
dbYesNo: Defines two buttons Yes/No
dbOne: Defines one button by Button1 defined caption
dbTwo: Defines two buttons by Button1 and Button2 defined captions
dbThree: Defines three buttons by Button1, Button2 and Button3 defined captions
Also, make sure that your 3rd party controls also
call your custom message dlg and not standard
MessageDlg function. That is if they're actually
using it. It is possible that 3rd party controls
do not use the Delphi messagedlg and call the
MessageBox API directly. If that's case, you might
end up with inconsistencies in showing message
boxes.

Resources