I am using a popup menu in Delphi. I want to use it in a "radio group" fashion where if the user selects an item it is checked and the other items are not checked. I tried using the AutoCheck property, but this allows multiple items to be checked. Is there a way to set the popup menu so that only one item can be checked?
To treat the popup (or any other) menu items like radio group items, set the 'RadioItem' property to true for each item you want to have in the radio group.
Instead of showing a checkmark, it will show a bullet by the selected item, but it will work the way you want, and the visual cue will actually match a windows standard.
Zartog is right, but if you want to keep the checkbox, assign this event to every item in the popup menu.
Note that this code is a little hairy looking because it does not depend on knowing the name of your popup menu (hence, looking it up with "GetParentComponent").
procedure TForm2.OnPopupItemClick(Sender: TObject);
var
i : integer;
begin
with (Sender as TMenuItem) do begin
//if they just checked something...
if Checked then begin
//go through the list and *un* check everything *else*
for i := 0 to (GetParentComponent as TPopupMenu).Items.Count - 1 do begin
if i <> MenuIndex then begin //don't uncheck the one they just clicked!
(GetParentComponent as TPopupMenu).Items[i].Checked := False;
end; //if not the one they just clicked
end; //for each item in the popup
end; //if we checked something
end; //with
end;
You can assign the event at runtime to every popup box on your form like this (if you want to do that):
procedure TForm2.FormCreate(Sender: TObject);
var
i,j: integer;
begin
inherited;
//look for any popup menus, and assign our custom checkbox handler to them
if Sender is TForm then begin
with (Sender as TForm) do begin
for i := 0 to ComponentCount - 1 do begin
if (Components[i] is TPopupMenu) then begin
for j := 0 to (Components[i] as TPopupMenu).Items.Count - 1 do begin
(Components[i] as TPopupMenu).Items[j].OnClick := OnPopupItemClick;
end; //for every item in the popup list we found
end; //if we found a popup list
end; //for every component on the form
end; //with the form
end; //if we are looking at a form
end;
In response to a comment below this answer: If you want to require at least one item to be checked, then use this instead of the first code block. You may want to set a default checked item in the oncreate event.
procedure TForm2.OnPopupItemClick(Sender: TObject);
var
i : integer;
begin
with (Sender as TMenuItem) do begin
//go through the list and make sure *only* the clicked item is checked
for i := 0 to (GetParentComponent as TPopupMenu).Items.Count - 1 do begin
(GetParentComponent as TPopupMenu).Items[i].Checked := (i = MenuIndex);
end; //for each item in the popup
end; //with
end;
To enlarge on Zartog's post: Popup menus in Delphi (from at least D6) have a GroupIndex property which allow you to have multiple sets of radio items within a menu. Set GroupIndex to 1 for the first group, 2 for a second etc.
So:
Set AutoCheck = True
Set RadioItem = True
Set GroupIndex if you need more than one group of radio items
Related
I want to list some values (ID-s in this case) of the selected rows of a TDBGrid in a TEdit control.
I've tried AfterScroll event, to catch the event after(!) a selection, but it doesn't work if I use the mouse.
If I click on a row with mouse, it doesn't appear in the TDBGrid.SelectedRows collection, only after the next click/selection.
If I do the selection with keyboard, everything works fine.
Do you have any idea, how to solve this?
Simplified code of my solution:
procedure TForm1.ClientDataSet1AfterScroll(DataSet: TDataSet);
begin
edtIDs.Text := string.Join(',', GetSelectedIDs().ToArray) ;
end;
function TForm1.GetSelectedIDs() : TList<string>;
var
i: Integer;
ds: TDataSet;
bmOrig: TBookmark;
begin
FSelectedIDs.Clear();
ds := DBGrid1.DataSource.DataSet;
bmOrig := ds.GetBookmark();
ds.AfterScroll := nil; //switch off AfterScroll event
try
if DBGrid1.SelectedRows.Count > 0 then begin
for i := 0 to DBGrid1.SelectedRows.Count - 1 do begin
ds.GotoBookmark(DBGrid1.SelectedRows.Items[i]);
FSelectedIDs.Add(ds.FindField('ID').AsString);
end;
ds.GotoBookmark(bmOrig);
end;
finally
ds.AfterScroll := ClientDataSet1AfterScroll; //switch on AfterScroll event
ds.FreeBookmark(bmOrig);
end;
Result := FSelectedIDs;
end;
Replace the OnAfterScroll event of the data source by the OnColEnter event of the TDBGrid.
Form the help of TDBGrid.OnColEnter:
Occurs when focus moves to a new cell in the grid.
Write an OnColEnter event handler to take specific action when a new cell has just been selected.
Focus moves to a cell when
The user navigates to the cell using the keyboard. For example, when the user uses the Tab key, or the Home key.
The user clicks the mouse button down in the cell.
The SelectedField or SelectedIndex property is set.
Read the SelectedField or SelectedIndex property to determine which cell was just entered.
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.
I'm using this code to assign values:
combobox1.Text:=form1.listview1.Selected.Caption;
But i'm getting this error: Cannot assign a TListItems to a TComboBox
You can't add a ListView.Items to a ComboBox.Items (as the compiler has told you, one is a TListItems collection and the other is a descendant of TStrings, and they're not type compatible). You can add the caption of a selected ListItem to the ComboBox.Items.
You need to add it to the ComboBox.Items:
ComboBox1.Items.Add(ListView1.Selected.Caption);
If you want to add all selected items, you need to use a loop:
var
Item: TListItem;
begin
Item := ListView1.Selected;
while Item <> nil do
begin
ComboBox1.Items.Add(Item.Caption);
Item := ListView1.GetNextItem(Item, sdAll, [isSelected]);
end;
If you just want to add all items from the ListView to the ComboBox (which seems pretty pointless, as they're already displayed in the ListView):
var
i: Integer;
begin
for i := 0 to ListView1.Items.Count - 1 do
ComboBox1.Items.Add(ListView1.Items[i].Caption);
end;
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
When the user clicks 'x' on a Pinned Form OnClose is called.
When the user clicks 'x' on an Unpinned Form OnHide is called
When the user clicks 'UnPin' on a Pinned Form OnHide is called.
I'm trying to synchronise the visible forms with a menu system but I don't know how to determine the difference in the OnHide event between when the user clicks 'x' and when the user clicks 'UnPin'. I want to intercept the 'x' and call Close instead.
Each child is a descendant of TManagerPanel which in turn is a descendant of TForm with the border style set to bsSizeToolWin, Drag Kind set to dkDock and Drag Mode is dmAutomatic.
type
TPanelManager = class(TForm)
...
private
...
Panels: TManagerPanelList;
Settings: TSettings; //User Settings
...
end;
...
function TPanelManager.InitChild(ChildClass: TManagerPanelClass): TManagerPanel;
var
Child: TManagerPanel;
begin
Child := ChildClass.Create(Self);
Child.Connection := MSConnection1;
Child.Settings := Settings;
Child.Styles := Styles;
...
Child.OnPanelClosed := PanelClosed;
Child.OnPercentChanged := PercentChanged;
...
Child.OnPanelHide := PanelHide;
Child.Font := Font;
Child.Initialise;
Child.ManualDock(DockTarget);
Panels.AddPanel(Child);
Result := Child;
end;
procedure TPanelManager.PanelClosed(Sender: TObject; var Action: TCloseAction);
var
MenuItem: TMenuItem;
Child: TManagerPanel;
begin
if Sender is TManagerPanel then
begin
Child := TManagerPanel(Sender);
Action := caFree;
MenuItem := MenuItemFromChild(Child);
MenuItem.Checked := False;
Settings[RemoveAmpersand(MenuItem.Caption)] := MenuItem.Checked;
Panels.Remove(Child);
end;
end;
EDIT:
What I mean by a "Pinned" Form: A docked form with the pin set such that it always visible.
What I mean by a "UnPinned" Form: A docked form with the pin released such that a tab appears in a dock tab set and the form appears when the tab is selected.
Delphi Version is 2007
it seems that pinning and unpinning a docked form changes it's parent between a TTabDockPanel and the TPanel I'm docking it to.
Adding an OnHide method to the Demo Dock Form...
procedure TfrmDock.FormHide(Sender: TObject);
begin
if Assigned(Self.Parent) then
ShowMessage(Self.Parent.ClassName)
else
ShowMessage('No Parent');
end;
I can now distinguish between "Floating", "Docked,Pinned" and "Docked, Unpinned" when the form gets hidden.
EDIT
I've found a better way of doing this
procedure TfrmDock.FormHide(Sender: TObject);
begin
if Assigned(Parent) then
begin
if Not (csDocking in ControlState) then //This was the original test above
begin
if Parent is TTabDockPanel then // This is now a safety check
begin
if TTabDockPanel(Parent).AnimateSpeed = 1 then //Additional Test
//form is closing
else
//form is hiding (Unpinned focused changed)
end;
end
else
//form is being unpinned.
end;
end;
In DockCaptionMouseUp the Animation Speed is set to 1 so that the panel appears to close (Hides really fast). The same happens for "Unpinning" but control state changes.