Finding out position of a control inside TGridPanel - delphi

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.

Related

How to get the clicked column on TDBGrid.DblClick(Sender: TObject)?

When using the OnDblClick event of a TDBGrid, how can i know what column was double clicked ?
This is easy with the OnCellClick as it has a TColumn parameter, but not on OnDblClick.
During TDBGrid.OnDblClick the dataset is positioned to the clicked record and the column can be retrieved with the TDBGrid.SelectedIndex property. If you are interested in the underlying dataset field, you can directly access it with TDBGrid.SelectedField.
The OnDblClick event doesn't give you any information about the click, in particular where the click was performed, let alone which grid cell was clicked on. So, you will have to determine that information manually.
Try this:
Get the current mouse position within the grid, by passing Mouse.CursorPos to TDBGrid.ScreenToClient()
Then, use TDBGrid.MouseCoord() to determine the row/column indexes of the cell that is underneath the mouse.
Then, check if the cell row/column corresponds to a data cell, and if so then use the TDBGrid.SelectedIndex property to index into the TDBGrid.Columns property.
This is basically the same thing that TDBGrid does internally when firing the OnCellClick event, only it does this in response to a MouseUp event, which provides the mouse coordinates within the grid, thus skipping the 1st step above.
For example:
type
TDBGridAccess = class(TDBGrid)
end;
procedure TMyForm1.DBGrid1DblClick(Sender: TObject);
var
TitleOffset: Byte;
Pt: TPoint;
Cell: TGridCoord;
Column: TColumn;
begin
TitleOffset := Ord(dgTitles in DBGrid1.Options);
Pt := DBGrid1.ScreenToClient(Mouse.CursorPos);
Cell := DBGrid1.MouseCoord(Pt.X, Pt.Y);
if (Cell.X >= TDBGridAccess(DBGrid1).IndicatorOffset) and (Cell.Y >= TitleOffset) then
begin
Column := DBGrid1.Columns[DBGrid1.SelectedIndex];
// use Column as needed...
end;
end;
UPDATE: based on #UweRaabe's comments, you should be able to just use TDBGrid.SelectedIndex by itself:
procedure TMyForm1.DBGrid1DblClick(Sender: TObject);
var
Index: Integer;
Column: TColumn;
begin
Index := DBGrid1.SelectedIndex;
if (Index <> -1) then
begin
Column := DBGrid1.Columns[Index];
// use Column as needed...
end;
end;

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.

Getting column index of a clicked control in TGridPanel

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.

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

Moving controls in a gridpanel with Delphi

In a previous question here I asked about drag n drop within the gridpanel.
Drag N Drop controls in a GridPanel
The question I have next is that I am having weird behavior whenever I try to move controls diagonally when they are near other controls. Controls that not suppose to move are shifting cells. Up and down, sideways it is fine. But diagonal moves, when the moved cell contents are on the same row/column with other cells which hold controls will cause unexpected shifts. I have tried beginupdate/endupdate the shifts still happen. There is a LOCK function for the gridpanel but lock anything. It happens when the drop is on an empty cell, and even cells that already have contents.
here is the test project (Delphi 2010 w/o exe)
http://www.mediafire.com/?xmrgm7ydhygfw2r
type
TForm1 = class(TForm)
GridPanel1: TGridPanel;
btn1: TButton;
btn3: TButton;
btn2: TButton;
lbl1: TLabel;
procedure FormCreate(Sender: TObject);
private
{ Private declarations }
procedure GridPanelDragDrop(Sender, Source: TObject; X, Y: Integer);
procedure btnDragOver(Sender, Source: TObject; X, Y: Integer;
State: TDragState; var Accept: Boolean);
procedure btnDragDrop(Sender, Source: TObject; X, Y: Integer);
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure SetColumnWidths(aGridPanel: TGridPanel);
var
i,pct: Integer;
begin
aGridPanel.ColumnCollection.BeginUpdate;
pct:=Round(aGridPanel.ColumnCollection.Count/100);
for i := 0 to aGridPanel.ColumnCollection.Count - 1 do begin
aGridPanel.ColumnCollection[i].SizeStyle := ssPercent;
aGridPanel.ColumnCollection[i].Value := pct;
end;
aGridPanel.ColumnCollection.EndUpdate;
end;
procedure SetRowWidths(aGridPanel: TGridPanel);
var
i,pct: Integer;
begin
aGridPanel.RowCollection.BeginUpdate;
pct:=Round(aGridPanel.RowCollection.Count/100);
for i := 0 to aGridPanel.RowCollection.Count - 1 do begin
aGridPanel.RowCollection[i].SizeStyle := ssPercent;
aGridPanel.RowCollection[i].Value := pct;
end;
aGridPanel.RowCollection.EndUpdate;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
btn1.OnDragOver := btnDragOver;
btn2.OnDragOver := btnDragOver;
btn3.OnDragOver := btnDragOver;
GridPanel1.OnDragOver := btnDragOver;
GridPanel1.OnDragDrop := GridPanelDragDrop;
btn1.OnDragDrop := btnDragDrop;
btn2.OnDragDrop := btnDragDrop;
btn3.OnDragDrop := btnDragDrop;
SetColumnWidths(GridPanel1);
SetRowWidths(GridPanel1);
end;
procedure TForm1.btnDragOver(Sender, Source: TObject; X, Y: Integer;
State: TDragState; var Accept: Boolean);
begin
Accept := (Source is TButton);
end;
procedure TForm1.btnDragDrop(Sender, Source: TObject; X, Y: Integer);
var
src_x,src_y, dest_x, dest_y: Integer;
btnNameSrc,btnNameDest: string;
src_ctrlindex,dest_ctrlindex:integer;
begin
if Source IS tBUTTON then
begin
//GridPanel1.ColumnCollection.BeginUpdate;
btnNameSrc := (Source as TButton).Name;
btnNameDest := (Sender as TButton).Name;
src_ctrlindex := GridPanel1.ControlCollection.IndexOf(Source as tbutton);
src_x := GridPanel1.ControlCollection.Items[src_ctrlindex].Column;
src_y := GridPanel1.ControlCollection.Items[src_ctrlindex].Row;
dest_ctrlindex := GridPanel1.ControlCollection.IndexOf(Sender as tbutton);
dest_x := GridPanel1.ControlCollection.Items[dest_ctrlindex].Column;
dest_y := GridPanel1.ControlCollection.Items[dest_ctrlindex].Row;
GridPanel1.ControlCollection[src_ctrlindex].Column := dest_x;
GridPanel1.ControlCollection[src_ctrlindex].Row := dest_y;
//GridPanel1.ColumnCollection.EndUpdate;
lbl1.Caption := Format('"%s" from cell %d:%d to Cell %s=%d:%d', [btnNameSrc,src_x,src_y,btnNameDest,dest_x,dest_y]);
end;
end;
procedure TForm1.GridPanelDragDrop(Sender, Source: TObject; X, Y: Integer);
var
DropPoint: TPoint;
CellRect: TRect;
i_col, i_row, src_x,src_y, dest_x, dest_y: Integer;
btnNameSrc,btnNameDest: string;
src_ctrlindex:integer;
begin
if Source is tbutton then
begin
btnNameSrc := (Source as TButton).Name;
btnNameDest := '';
src_ctrlindex := GridPanel1.ControlCollection.IndexOf(Source as tbutton);
src_x := GridPanel1.ControlCollection.Items[src_ctrlindex].Column;
src_y := GridPanel1.ControlCollection.Items[src_ctrlindex].Row;
DropPoint := Point(X, Y);
for i_col := 0 to GridPanel1.ColumnCollection.Count-1 do
for i_row := 0 to GridPanel1.RowCollection.Count-1 do
begin
CellRect := GridPanel1.CellRect[i_col, i_row];
if PtInRect(CellRect, DropPoint) then
begin
// Button was dropped over Cell[i_col, i_row]
dest_x := i_col;
dest_y := i_row;
Break;
end;
end;
lbl1.Caption := Format('"%s" from cell %d:%d to Cell %s=%d:%d', [btnNameSrc,src_x,src_y,btnNameDest,dest_x,dest_y]);
GridPanel1.ControlCollection[src_ctrlindex].Column := dest_x;
GridPanel1.ControlCollection[src_ctrlindex].Row := dest_y;
end;
end;
This is not about dragging, when an item's both column and row are changing the change occurs in two steps. With your code, first the column, then the row. If in the column change, f.i., there happens to be already an other control, this other control is pushed aside, even if its cell is not the ultimate location of the target cell of the moving control.
Begin/EndUpdate will not work, the control collection never checks the update count. What can you do is to use a protected hack to access the control item's InternalSetLocation method. This method has a 'MoveExisting' parameter which you can pass 'False'.
type
THackControlItem = class(TControlItem);
procedure TForm1.GridPanelDragDrop(Sender, Source: TObject; X, Y: Integer);
var
[...]
begin
if Source is tbutton then
begin
[...]
lbl1.Caption := Format('"%s" from cell %d:%d to Cell %s=%d:%d', [btnNameSrc,src_x,src_y,btnNameDest,dest_x,dest_y]);
THackControlItem(GridPanel1.ControlCollection[src_ctrlindex]).
InternalSetLocation(dest_x, dest_y, False, False);
// GridPanel1.ControlCollection[src_ctrlindex].Column := dest_x;
// GridPanel1.ControlCollection[src_ctrlindex].Row := dest_y;
end;
end;
You might need to test if the target cell is empty or not before calling 'InternalSetLocation' depending on what you expect to be the correct control movement.
I use a quite different way to do the Job... Create a whole unit just to add a method to ExtCtrls.TControlCollection without touching unit ExtCtrls (first hack) and make such method use InternalSetLocation (second hack). I also explain both hacks on this post.
Then i only need to add such unit to implementation uses section (before gridpanel declaration) and call the method i created... very simple to use.
Here is how i do it, step by step:
I include such unit i maded for such job to the project (add file)
I add to my TForm interface uses section such unit (or where i need it)
I use my method AddControlAtCell instead of ExtCtrls.TControlCollection.AddControl
Here is the unit i had created for such job, save it as unitTGridPanel_WithAddControlAtCell:
unit unitTGridPanel_WithAddControlAtCell;
interface
uses
Controls
,ExtCtrls
;
type TGridPanel=class(ExtCtrls.TGridPanel)
private
public
procedure AddControlAtCell(AControl:TControl;AColumn:Integer;ARow:Integer); // Add Control on specifed cell, if there already exists a Control it will be deleted
end;
implementation
uses
SysUtils
;
type
THackControlItem=class(TControlItem); // To get internal access to InternalSetLocation procedure
procedure TGridPanel.AddControlAtCell(AControl:TControl;AColumn:Integer;ARow:Integer);
var
TheControlItem:TControlItem; // To let it be added in a specified cell, since ExtCtrls.TControlCollection.AddControl contains multiply BUGs
begin // Add Control on specifed cell, if there already exists a Control it will be deleted
if (-1<AColumn)and(AColumn<ColumnCollection.Count) // Cell with valid Column
and // Cell inside valid range
(-1<ARow)and(ARow<RowCollection.Count) // Cell with valid Row
then begin // Valid cell, must check if there is already a control
if (Nil<>ControlCollection.ControlItems[AColumn,ARow]) // Check if there are any controls
and // A control is already on the cell
(Nil<>ControlCollection.ControlItems[AColumn,ARow].Control) // Check if cell has a control
then begin // There is already a control, must be deleted
ControlCollection.Delete(ControlCollection.IndexOf(ControlCollection.ControlItems[AColumn,ARow].Control)); // Delete the control
end;
TheControlItem:=ControlCollection.Add; // Create the TControlItem
TheControlItem.Control:=TControl(AControl); // Put the Control in the specified cell without altering any other cell
THackControlItem(ControlCollection.Items[ControlCollection.IndexOf(AControl)]).InternalSetLocation(AColumn,ARow,False,False); // Put the ControlItem in the cell without altering any other cell
end
else begin // Cell is out of range
raise Exception.CreateFmt('Cell [%d,%d] out of range on ''%s''.',[AColumn,ARow,Name]);
end;
end;
end.
I hope the comments are enough clear, please read them to understand why and how i do it.
Then, when i need to add a control to the gridpanel at a specified cell i do the next simple call:
TheGridPanel.AddControlAtCell(TheControl,ACloumn,ARow); // Add it at desired cell without affecting other cells
A very, very basic example of adding a runtime newly created TCheckBox at a specific cell could be like this:
// AColumn is of Type Integer
// ARow is of Type Integer
// ACheckBox is of Type TCheckBox
// TheGridPanel is of Type TGridPanel
ACheckBox:=TCheckBox.Create(TheGridPanel); // Create the Control to be added (a CheckBox)
ACheckBox.Visible:=False; // Set it to not visible, for now (optimization on speed, e tc)
ACheckBox.Color:=TheGridPanel.Color; // Just to use same background as on the gridpanel
ACheckBox.Parent:=TheGridPanel; // Set the parent of the control as the gridpanel (mandatory)
TheGridPanel.AddControlAtCell(ElCheckBox,ACloumn,ARow); // Add it at desired cell without affecting other cells
ElCheckBox.Visible:=True; // Now it is added, make it visible
ElCheckBox.Enabled:=True; // And of course, ensure it is enabled if needed
Please Note that i use this two Hacks:
type THackControlItem let me access the method InternalSetLocation.
type TGridPanel=class(ExtCtrls.TGridPanel) let me add a method to ExtCtrls.TGridPanel without even touching (neither needing source of ExtCtrls)
Important: Also note that i mention it requieres to add the unit to the uses of the interface of each form where you want to use the method AddControlAtCell; that is for normal people, advanced people could also create another unit, etc... the 'concept' is to have the unit on the uses before the declaration of the GridPanel where you wnat to use it... example: if GridPanel is putted at design time on a form... it must go on implementation uses of such form unit.
Hope this helps some one else.
The solution below works without any kind of hacking.
My code is in C++ Builder but i think it is simply to understand for Delphi users because it rely only on VCL functions.
PS: note that I drag TPanels instead of TButtons (a very minor change).
void TfrmVCL::ButtonDragDrop(TObject *Sender, TObject *Source, int X, int Y)
{
TRect CurCellRect;
TRect DestCellRect;
int Col;
int Row;
int destCol; int destRow;
int srcIndex; int destIndex;
TPanel *SrcBtn;
TPanel *DestBtn;
SrcBtn = dynamic_cast<TPanel *>(Source);
if (SrcBtn)
{
int ColCount = GridPnl->ColumnCollection->Count ;
int RowCount = GridPnl->RowCollection->Count ;
// SOURCE
srcIndex = GridPnl->ControlCollection->IndexOf( SrcBtn );
// DESTINATION
// we get coordinates of the button I drag onto
DestBtn= dynamic_cast<TPanel *>(Sender);
if (!DestBtn) return;
destIndex = GridPnl->ControlCollection->IndexOf( DestBtn );
destCol = GridPnl->ControlCollection->Items[ destIndex ]->Column; // the column for the dragged button
destRow = GridPnl->ControlCollection->Items[ destIndex ]->Row;
DestCellRect = GridPnl->CellRect[ destCol ][ destRow ];
// Check all cells
for ( Col = 0 ; Col < ColCount ; Col++ )
{
for ( Row = 0 ; Row < RowCount ; Row++ )
{
// Get the bounding rect for this cell
CurCellRect = GridPnl->CellRect[ Col ][ Row ];
if (IntersectRect_ForReal(DestCellRect, CurCellRect))
{
GridPnl->ControlCollection->Items[srcIndex]->SetLocation(Col, Row, false);
return;
}
}
}
}
}

Resources