This question already has an answer here:
Accessing protected event of TWinControl
(1 answer)
Closed 8 years ago.
I'm working on delphi components.
I've been trying to access a customized component's designated parent control's onClick event. By designate, users can designate the component's parent control by using object inspector as a property. parent controls can be any of control components on the same form. However, because all parent controls I've made are subclasses of TControl and onClick event of TControl is protected, I can not access parent control's onclick event. practically, a customized component is like a sub-component positioned right next to a parent control, so whenever, a user clicks a customized component, I wanted parent control's click event will occur, if click event exists.
when I run this code, typecasting exception occurs.
procedure TSubCom.SetParentControl(const Value : TControl);
var
parentContLeft : Integer; //parent control's left + width
parentContTop : Integer; //parent control's top
begin
FParentControl := Value;
parentContLeft := FParentControl.Left + FParentControl.Width;
parentContTop := FParentControl.Top;
Left := parentContLeft - (Width div 2);
Top := parentContTop - (Height div 2);
Repaint;
end;
//TSubCom's onClick event is linked with its parent control's onClick event
procedure TSubCom.Click;
var
Parent: wrapClass;
begin
inherited;
if(FParentControl <> nil) then
begin
ShowMessage(FPArentControl.Name);
Parent := FParentControl as wrapClass;
ShowMessage('1');
if Assigned(Parent.OnClick) then
begin
Parent.OnClick(Self);
end;
// FParentControl as FParentControl.ClassType;
// if(FParentControl.OnClick <> nil) then
// FParentControl.OnClick;
end;
end;
Declare a class for accessing protected members,
typecast the Parent to this class, and do not use the OnClick event, instead use Click.
type
TControlAccess = class(TControl);
procedure TSubCom.Click;
begin
inherited Click;
if ParentControl <> nil then
TControlAccess(ParentControl).Click;
end;
Related
I'm referring to question
Passing the variable to another Form
Is there also a way to handover data - for example from a settings form to the application's main form without using a global variable?
Since you are talking about a "settings form", I assume that the form is shown modally. Then it is actually almost trivial.
As an example, create a new VCL application with a label and a button:
Then create a settings form used to set the font of the main label in the middle. It can look like this, with two TLabel controls, two TEdit controls, two TCheckBox controls, and two TButton controls.
Don't forget to make sure the tab order is correct, that each control has a unique access key (use the FocusControl property of the label to make the connection to the appropriate edit box), that the OK button has Default = True and ModalResult = mrOk, and that the Cancel button has Cancel = True and ModalResult = mrCancel.
(As a bonus, set NumbersOnly = True on the size edit box.)
Now, to pass information between the forms, it is as simple as this:
procedure TfrmMain.btnSettingsClick(Sender: TObject);
var
dlg: TfrmSettings;
begin
dlg := TfrmSettings.Create(Self);
try
// Populate dialog
dlg.eFont.Text := lblCaption.Font.Name;
dlg.eSize.Text := lblCaption.Font.Size.ToString;
dlg.cbBold.Checked := fsBold in lblCaption.Font.Style;
dlg.cbItalic.Checked := fsItalic in lblCaption.Font.Style;
if dlg.ShowModal = mrOk then
begin
// Apply settings from dialog
lblCaption.Font.Name := dlg.eFont.Text;
lblCaption.Font.Size := StrToInt(dlg.eSize.Text);
if dlg.cbBold.Checked then
lblCaption.Font.Style := lblCaption.Font.Style + [fsBold]
else
lblCaption.Font.Style := lblCaption.Font.Style - [fsBold];
if dlg.cbItalic.Checked then
lblCaption.Font.Style := lblCaption.Font.Style + [fsItalic]
else
lblCaption.Font.Style := lblCaption.Font.Style - [fsItalic];
end;
finally
dlg.Free;
end;
end;
The settings form has several possibilities to handover data to application mainform without using global variable. I'll assume that the setting form has bee created by the mainform like this:
SettingForm := TSettingForm.Create(Self);
SettingForm.ShowModal;
When the setting form is done (closed), ShowModal returns and mainform can access any filed (variable) or property of the setting form, before destroying it:
ShowMessage(SettingForm.SomeVariable.ToString);
SettingForm.Free;
Another way to do is to use an event.
type
TSettingFormValueAvailableEvent = procedure (Sender : TObject; Value : Integer) of object;
// Create the form and assign an event handler then show the form
SettingForm := TSettingForm.Create(Self);
SettingForm.OnValueAvailable := SettingFormValueAvailable;
SettingForm.ShowModal;
// The event handler in main form
procedure TForm1.SettingFormValueAvailable(Sender: TObject; Value : Integer);
begin
ShowMessage(Value.ToString);
end;
// The event declaration in TFormSetting
private
FOnValueAvailable : TSettingFormValueAvailableEvent ;
public
property OnValueAvailable : TSettingFormValueAvailableEvent read FOnValueAvailable write FOnValueAvailable;
// The use of the event in the form setting
procedure TFormSetting.Button1.Click(Sender : TObject);
begin
if Assigned(FOnValueAvailable) then
FOnValueAvailable(Self, 1234); // Pass value 1234
end;
Using an event is a little bit more code but it is "real time". The main form can react immediately when something happens while SettingForm is being displayed.
Using Delphi Tokyo and FireMonkey:
I have a lot of different frames on a form and would like to set some form-level variables as the focus on the form changes in and out of the different frames.
Ex. I have a Insert button on the form and want to enable it if the frame the user is in allows inserts and then again disable it upon leaving the frame's focus.
There are OnEnter and OnExit events on the frame, but they never execute.
Obviously there are edits etc. on the frames.
type
TForm1 = class(TForm)
Label1: TLabel;
procedure FormFocusChanged(Sender: TObject);
private
FFocusedFrame: TFrame;
public
{ Public declarations }
end;
...
procedure TForm1.FormFocusChanged(Sender: TObject);
var
LParent: TFmxObject;
begin
if Focused <> nil then
begin
LParent := Focused.GetObject.Parent;
while (LParent <> nil) and not (LParent is TFrame) do
LParent := LParent.Parent;
if (LParent <> nil) and (FFocusedFrame <> LParent) then
begin
FFocusedFrame := TFrame(LParent);
Label1.Text := FFocusedFrame.Name;
end;
end;
end;
end.
No need to hook up OnEnter and OnExit for every control
The frames can not receive focus, and therefore they do not fire OnEnter() or OnExit() events.
After you have placed a frame on the form, you can create two common event handlers for all edit controls (or other input controls on the frame)
procedure TForm14.Frame112EditExit(Sender: TObject);
begin
Button1.Enabled := False;
end;
procedure TForm14.Frame112EditEnter(Sender: TObject);
begin
Button1.Enabled := True;
end;
and link the OnEnter() and OnExit() events of all those edit controls to these two event handlers.
I was unsure whether the events are fired in correct order when moving from one edit control to anotherone, but a short test (on Windows) shows that OnExit() of the control we leave is fired before OnEnter() of the control we enter, as expected.
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 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.