Below you can see the code for a component that have inside a TPersistent class that allow me to assign some TCustomButtons (TButton or TBitBtn).
I place my component on a modal form and I assign the 2 buttons (OK and Cancel).
Normally when I press any of this buttons, my form should getting closed.
My question is why the form is not getting closed?
type
TMyComp = class;
TButtons = class;
TMyComp = class(TComponent)
private
FButtons: TButtons;
procedure SetButtons(Value: TButtons);
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
published
property Buttons: TButtons read FButtons write SetButtons;
end;
TButtons = class(TPersistent)
private
FOwner: TMyComp;
FBtnOk: TCustomButton;
FBtnCancel: TCustomButton;
procedure SetCustomButton(Index: Integer; Value: TCustomButton);
procedure BtnOkOnClick(Sender: TObject);
procedure BtnCancelOnClick(Sender: TObject);
protected
public
constructor Create(AOwner: TMyComp); virtual;
procedure Assign(Source: TPersistent); override;
published
property BtnOk: TCustomButton index 0 read FBtnOk write SetCustomButton;
property BtnCancel: TCustomButton index 1 read FBtnCancel write SetCustomButton;
end;
implementation
constructor TMyComp.Create(AOwner: TComponent);
begin
inherited;
FButtons:= TButtons.Create(Self);
end;
destructor TMyComp.Destroy;
begin
FButtons.Free;
inherited;
end;
//------- TButtons ---------
constructor TButtons.Create(AOwner: TMyComp);
begin
inherited Create;
FOwner:= AOwner;
end;
procedure TButtons.Assign(Source: TPersistent);
begin
if Source is TButtons then
begin
FBtnOk:= TButtons(Source).BtnOk;
FBtnCancel:= TButtons(Source).BtnCancel;
end
else
inherited Assign(Source);
end;
procedure TButtons.SetCustomButton(Index: Integer; Value: TCustomButton);
begin
case Index of
0: if FBtnOk <> Value then
begin
FBtnOk:= Value;
if Assigned(FBtnOk) then
begin
//TBitBtn
if (FBtnOk is TBitBtn) then
(FBtnOk as TBitBtn).OnClick:= BtnOkOnClick;
//TButton
if (FBtnOk is TButton) then
(FBtnOk as TButton).OnClick:= BtnOkOnClick;
end;
end;
1: if FBtnCancel <> Value then
begin
FBtnCancel:= Value;
if Assigned(FBtnCancel) then
begin
//TBitBtn
if (FBtnCancel is TBitBtn) then
(FBtnCancel as TBitBtn).OnClick:= BtnCancelOnClick;
//TButton
if (FBtnCancel is TButton) then
(FBtnCancel as TButton).OnClick:= BtnCancelOnClick;
end;
end;
end;
if Assigned(Value) then Value.FreeNotification(FOwner);
end;
procedure TButtons.BtnCancelOnClick(Sender: TObject);
begin
showmessage('Cancel pressed!');
if Sender is TButton then
(Sender as TButton).ModalResult:= mrCancel;
if Sender is TBitBtn then
(Sender as TBitBtn).ModalResult:= mrCancel;
end;
procedure TButtons.BtnOkOnClick(Sender: TObject);
begin
//do some input validations here...
showmessage('Ok pressed!');
if Sender is TButton then
(Sender as TButton).ModalResult:= mrOk;
if Sender is TBitBtn then
(Sender as TBitBtn).ModalResult:= mrOk;
end;
Other answers/comments have explained why the code is not working - you are setting the button's ModalResult too late, so it is not propagating to the Form's ModalResult when you are expecting it to be.
I want to suggest an alternative implementation that also incorporates a solution, and addresses some other things your code is lacking:
type
TButtons = class;
TMyComp = class(TComponent)
private
FButtons: TButtons;
procedure SetButtons(Value: TButtons);
protected
procedure Notification(AComponent: TComponent; Operation: TOperation); override;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
published
property Buttons: TButtons read FButtons write SetButtons;
end;
TButtons = class(TPersistent)
private
FOwner: TMyComp;
FButtons: array[0..1] of TCustomButton;
FClickEvents: array[0..1] of TNotifyEvent;
function GetCustomButton(Index: Integer): TCustomButton;
procedure SetCustomButton(Index: Integer; Value: TCustomButton);
procedure BtnOkOnClick(Sender: TObject);
procedure BtnCancelOnClick(Sender: TObject);
public
constructor Create(AOwner: TMyComp);
destructor Destroy; override;
procedure Assign(Source: TPersistent); override;
published
property BtnOk: TCustomButton index 0 read GetCustomButton write SetCustomButton;
property BtnCancel: TCustomButton index 1 read GetCustomButton write SetCustomButton;
end;
implementation
//------- TMyComp ---------
constructor TMyComp.Create(AOwner: TComponent);
begin
inherited;
FButtons := TButtons.Create(Self);
end;
destructor TMyComp.Destroy;
begin
FButtons.Free;
inherited;
end;
procedure TMyComp.Notification(AComponent: TComponent; Operation: TOperation);
var
i: Index;
begin
inherited;
if Operation = opRemove then
begin
for i := Low(FButtons.FButtons) to High(FButtons.FButtons) do
begin
if AComponent = FButtons.FButtons[i] then
begin
FButtons.FButtons[i] := nil;
FButtons.FClickEvents[i] := nil;
Exit;
end;
end;
end;
end;
//------- TButtons ---------
constructor TButtons.Create(AOwner: TMyComp);
begin
inherited Create;
FOwner := AOwner;
end;
constructor TButtons.Destroy;
begin
Assign(nil);
inherited;
end;
procedure TButtons.Assign(Source: TPersistent);
var
i: Integer;
begin
if Source = nil then
begin
for i to Low(FButtons) to High(FButtons) do
SetCustomButton(i, nil);
end
else if Source is TButtons then
begin
for i to Low(FButtons) to High(FButtons) do
SetCustomButton(i, TButtons(Source).FButtons[i]);
end
else
inherited Assign(Source);
end;
function TButtons.GetCustomButton(Index: Integer): TCustomButton;
begin
Result := FButtons[Index];
end;
type
TCustomButtonAccess = class(TCustomButton)
end;
procedure TButtons.SetCustomButton(Index: Integer; Value: TCustomButton);
begin
if FButtons[Index] <> Value then
begin
if Assigned(FButtons[Index]) then
begin
TCustomButtonAccess(Value).OnClick := FClickEvents[Index];
FClickEvents[Index] := nil;
FButtons[Index].RemoveFreeNotification(FOwner);
end;
FButtons[Index] := Value;
if Assigned(Value) then
begin
Value.FreeNotification(FOwner);
FClickEvents[Index] := TCustomButtonAccess(Value).OnClick;
case Index of
0: TCustomButtonAccess(Value).OnClick := BtnOkOnClick;
1: TCustomButtonAccess(Value).OnClick := BtnCancelOnClick;
end;
end;
end;
end;
procedure TButtons.BtnOkOnClick(Sender: TObject);
var
Form: TCustomForm;
begin
//do some input validations here...
ShowMessage('Ok pressed!');
Form := GetParentForm(TControl(Sender));
if Form <> nil then
Form.ModalResult := mrOk;
// optional
if Assigned(FClickEvents[0]) then
FClickEvents[0](Sender);
end;
procedure TButtons.BtnCancelOnClick(Sender: TObject);
var
Form: TCustomForm;
begin
ShowMessage('Cancel pressed!');
Form := GetParentForm(TControl(Sender));
if Form <> nil then
Form.ModalResult := mrCancel;
// optional
if Assigned(FClickEvents[1]) then
FClickEvents[1](Sender);
end;
A simpler reproduction case can be like this:
Create an application with two forms, remove second form from the auto-created forms list. Add a button on each form with the following click handlers.
In unit1:
procedure TForm1.Button1Click(Sender: TObject);
var
F: TForm;
begin
F := TForm2.Create(nil);
try
F.ShowModal;
finally
F.Free;
end;
end;
In unit2:
procedure TForm2.Button1Click(Sender: TObject);
begin
Button1.ModalResult := mrOk;
end;
Run application, press button to launch the modal form. Press button on second form, the form does not close.
The reason the form is not closed is, the button's click handler is too late to set the modal result of the form.
You can see why it is too late from the code in TCustomButton.Click; in 'Vcl.StdCtrls.pas'. Comments are by me.
procedure TCustomButton.Click;
var
Form: TCustomForm;
begin
Form := GetParentForm(Self);
if Form <> nil then Form.ModalResult := ModalResult; // this is where modal result is checked
inherited Click; // this is where your click handler is run
end;
You'd notice a second click closes the form. That's because the button's modal result is already set after the first click.
In your event handler you're setting ModalResult of the button. A modal form closes when that form's ModalResult is set.
The purpose of a button's ModalResult is that the framework will set the parent form's ModalResult to the same value.
Setting the button component's ModalResult property is an easy way to make clicking the button close a modal form. When a button is clicked, the ModalResult property of its parent form is set to the same value as the button's ModalResult property.
For example, if a dialog box has OK and Cancel buttons, their ModalResult properties could be set at design time to mrOk and mrCancel, respectively. At run time, clicking the OK button then changes the dialog's ModalResult property to mrOk, and clicking the Cancel button changes the dialog's ModalResult property to mrCancel. Unless further processing is required, no OnClick event handlers are required for the buttons.
So, basically what you're doing wrong is that you failed to predefine the ModalResult of the button. So:
At the time the button is clicked, its ModalResult is still mrNone.
And therefore doesn't change the form's ModalResult.
(I suspect that if you click the button a second time it will behave as you expect.)
To resolve your problem, you have 2 options:
Preferably set the ModalResult of the button sooner. There's no reason for a "Cancel" button to ever have a ModalResult <> mrCancel. That's why it's usually streamed in the DFM.
If you absolutely must use the event handler; set the form's ModalResult.
Related
Using some answers in StackOverflow I've created a searcheable TComboBox in Delphi. It works fine when you add it directly to a Form, but breaks as soon as you add it to a TPanel and I can't seem to figure out why.
Directly on the form:
After typing t:
Inside a panel:
After typing t:
Here is the component's code:
unit uSmartCombo;
interface
uses
Vcl.StdCtrls, Classes, Winapi.Messages, Controls;
type
TSmartComboBox = class(TComboBox)
private
FStoredItems: TStringList;
procedure FilterItems;
procedure CNCommand(var AMessage: TWMCommand); message CN_COMMAND;
procedure RedefineCombo;
procedure SetStoredItems(const Value: TStringList);
procedure StoredItemsChange(Sender: TObject);
protected
procedure KeyPress(var Key: Char); override;
procedure CloseUp; override;
procedure Loaded; override;
procedure DoExit; override;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
property StoredItems: TStringList read FStoredItems write SetStoredItems;
end;
procedure Register;
implementation
uses
SysUtils, Winapi.Windows, Vcl.Forms;
procedure Register;
begin
RegisterComponents('Standard', [TSmartComboBox]);
end;
constructor TSmartComboBox.Create(AOwner: TComponent);
begin
inherited;
FStoredItems := TStringList.Create;
FStoredItems.OnChange := StoredItemsChange;
end;
destructor TSmartComboBox.Destroy;
begin
FStoredItems.Free;
inherited;
end;
procedure TSmartComboBox.DoExit;
begin
inherited;
RedefineCombo;
end;
procedure TSmartComboBox.Loaded;
var LParent: TWinControl;
LPoint: TPoint;
begin
inherited;
if Items.Count > 0 then
FStoredItems.Assign(Items);
AutoComplete := False;
Style := csDropDownList;
// The ComboBox doesn't behave properly if the parent is not the form.
// Workaround to pull it from any parenting
//if not (Parent is TForm) then
//begin
// LParent := Parent;
// while (not (LParent is TForm)) and Assigned(LParent) do
// LParent := LParent.Parent;
// LPoint := ClientToParent(Point(0,0), LParent);
// Parent := LParent;
// Left := LPoint.X;
// Top := LPoint.Y;
// BringToFront;
//end;
end;
procedure TSmartComboBox.RedefineCombo;
var S: String;
begin
if Style = csDropDown then
begin
if ItemIndex <> -1 then
S := Items[ItemIndex];
Style := csDropDownList;
Items.Assign(FStoredItems);
if S <> '' then
ItemIndex := Items.IndexOf(S);
end;
end;
procedure TSmartComboBox.SetStoredItems(const Value: TStringList);
begin
if Assigned(FStoredItems) then
FStoredItems.Assign(Value)
else
FStoredItems := Value;
end;
procedure TSmartComboBox.StoredItemsChange(Sender: TObject);
begin
if Assigned(FStoredItems) then
begin
RedefineCombo;
Items.Assign(FStoredItems);
end;
end;
procedure TSmartComboBox.KeyPress(var Key: Char);
begin
if CharInSet(Key, ['a'..'z']) and not (Style = csDropDown) then
begin
DroppedDown := False;
Style := csDropDown;
end;
inherited;
if not (Ord(Key) in [13,27]) then
DroppedDown := True;
end;
procedure TSmartComboBox.CloseUp;
begin
if Style = csDropDown then
RedefineCombo;
inherited;
end;
procedure TSmartComboBox.CNCommand(var AMessage: TWMCommand);
begin
inherited;
if (AMessage.Ctl = Handle) and (AMessage.NotifyCode = CBN_EDITUPDATE) then
FilterItems;
end;
procedure TSmartComboBox.FilterItems;
var I: Integer;
Selection: TSelection;
begin
SendMessage(Handle, CB_GETEDITSEL, WPARAM(#Selection.StartPos), LPARAM(#Selection.EndPos));
Items.BeginUpdate;
Try
if Text <> '' then
begin
Items.Clear;
for I := 0 to FStoredItems.Count - 1 do
if (Pos(Uppercase(Text), Uppercase(FStoredItems[I])) > 0) then
Items.Add(FStoredItems[I]);
end
else
Items.Assign(FStoredItems);
Finally
Items.EndUpdate;
End;
SendMessage(Handle, CB_SETEDITSEL, 0, MakeLParam(Selection.StartPos, Selection.EndPos));
end;
end.
Any help in how I can proceed to figure out why this is happening would be greatly appreciated!
Edit 1:
After doing some extra debugging, I've noticed the messages being sent to the ComboBox differ from the ones inside the panel.
A CBN_EDITUPDATE is never sent, like #Sherlock70 mentioned in the comments, which makes the FilterItems procedure never trigger.
I've also noticed the form behaves strangely after using the ComboBox inside the panel, sometimes freezing and even not responding, like it gets stuck in a loop.
This unpredictable behavior has made me move away from this approach, and I'm probably going to take an alternate route to create a "searchable ComboBox".
Going to leave the question open if someone wants to figure it out and maybe even use the component.
I hope this will help someone in future even after 7 months of the question. Setting the style of a Combobox will destroy the window handle of that Combobox and create a new one. This means windows will free your control's Window Handle and create a new one.
You are setting your Combobx style while searching and this is wrong. Try removing Style := from your code and test it again you will get the same results for Combobox on a form and Combobox on a panel or other TWinControl. As you can see in the following code, setting Style will call RecreateWnd.
procedure TCustomComboBox.SetStyle(Value: TComboBoxStyle);
begin
if FStyle <> Value then
begin
FStyle := Value;
if Value = csSimple then
ControlStyle := ControlStyle - [csFixedHeight] else
ControlStyle := ControlStyle + [csFixedHeight];
RecreateWnd;
end;
end;
RecreateWnd will call DestroyHandle()
procedure TWinControl.CMRecreateWnd(var Message: TMessage);
var
WasFocused: Boolean;
begin
WasFocused := Focused;
DestroyHandle;
UpdateControlState;
if WasFocused and (FHandle <> 0) then Windows.SetFocus(FHandle);
end;
Then DestroyHandle will call DestroyWnd() which will call DestroyWindowHandle().
I have a simple form that only contains a TTouchKeyboard. The forms BorderStyle is set to bsToolWindow. To avoid the form getting focus when clicking the touch keyboard I handle the WM_MOUSEACTIVATE message with this implementation:
procedure TKeyboardForm.WMMouseActivate(var Message: TWMMouseActivate);
begin
Message.Result := MA_NOACTIVATE;
end;
The BorderStyle setting allows the form to be dragged with the title bar, but in that case the form still gets the focus. Is there a way to avoid this?
Update: I tried adding WS_EX_NOACTIVATE to ExStyle in CreateParams, but unfortunately that doesn't hinder the form to receive focus when dragged.
procedure TKeyboardForm.CreateParams(var Params: TCreateParams);
begin
inherited;
Params.ExStyle := Params.ExStyle or WS_EX_NOACTIVATE;
end;
Because I was not very pleased with the approach that requires me to manually update the focused form variable in the keyboard form, I searched for a more transparent solution and came up with this approach.
Update: The previous approach had some issues with VCL styles. In addition not all of the message handlers were really necessary, though others turned out to be helpful, too. This version works well with VCL styles avoiding any flicker as far as possible:
type
TKeyboardForm = class(TForm)
TouchKeyboard1: TTouchKeyboard;
private
FLastFocusedForm: TCustomForm;
procedure SetLastFocusedForm(const Value: TCustomForm);
protected
procedure WMActivate(var Message: TWMActivate); message WM_ACTIVATE;
procedure WMMouseActivate(var Message: TWMMouseActivate); message WM_MOUSEACTIVATE;
procedure WMSetFocus(var Message: TWMSetFocus); message WM_SETFOCUS;
procedure Notification(AComponent: TComponent; Operation: TOperation); override;
property LastFocusedForm: TCustomForm read FLastFocusedForm write SetLastFocusedForm;
public
class constructor Create;
destructor Destroy; override;
function SetFocusedControl(Control: TWinControl): Boolean; override;
end;
type
TKeyboardFormStyleHook = class(TFormStyleHook)
protected
procedure WMNCActivate(var Message: TWMNCActivate); message WM_NCACTIVATE;
end;
procedure TKeyboardFormStyleHook.WMNCActivate(var Message: TWMNCActivate);
begin
{ avoids the title bar being drawn active for blink }
Message.Active := False;
inherited;
end;
class constructor TKeyboardForm.Create;
begin
TCustomStyleEngine.RegisterStyleHook(TKeyboardForm, TKeyboardFormStyleHook);
end;
destructor TKeyboardForm.Destroy;
begin
LastFocusedForm := nil;
inherited;
end;
procedure TKeyboardForm.Notification(AComponent: TComponent; Operation: TOperation);
begin
if (Operation = opRemove) and (AComponent = FLastFocusedForm) then begin
FLastFocusedForm := nil;
end;
inherited;
end;
function TKeyboardForm.SetFocusedControl(Control: TWinControl): Boolean;
begin
LastFocusedForm := Screen.FocusedForm;
result := inherited;
end;
procedure TKeyboardForm.SetLastFocusedForm(const Value: TCustomForm);
begin
if FLastFocusedForm <> Value then
begin
if FLastFocusedForm <> nil then begin
FLastFocusedForm.RemoveFreeNotification(Self);
end;
FLastFocusedForm := Value;
if FLastFocusedForm <> nil then begin
FLastFocusedForm.FreeNotification(Self);
end;
end;
end;
procedure TKeyboardForm.WMActivate(var Message: TWMActivate);
begin
Message.Active := WA_INACTIVE;
inherited;
end;
procedure TKeyboardForm.WMMouseActivate(var Message: TWMMouseActivate);
begin
inherited;
Message.Result := MA_NOACTIVATE;
end;
procedure TKeyboardForm.WMSetFocus(var Message: TWMSetFocus);
begin
inherited;
if (FLastFocusedForm <> nil) and (message.FocusedWnd <> FLastFocusedForm.Handle) then begin
SendMessage(FLastFocusedForm.Handle, WM_SETFOCUS, 0, 0);
Message.FocusedWnd := FLastFocusedForm.Handle;
end;
end;
The following combination of WMMouseActivate(), WMNCActivate() and reseting focus seems to fulfill your wishes:
The keyboard form (with BorderStyle = bsToolWindow) has message handlers for WM_MOUSEACTIVATE (as you already have) and WM_NCACTIVATE. The latter for having a point where to reset focus to the window with the edit control.
In addition the keyboardform will keep track of which form holds the edit (or other) control that has focus, and does that by introducing a new method for showing, which I called ShowUnfocused() and a field called FocusedForm: THandle.
procedure TKbdForm.ShowUnfocused(FocusedWindow: THandle);
begin
FocusedForm := FocusedWindow;
Show;
end;
procedure TKbdForm.FormShow(Sender: TObject);
begin
SetForegroundWindow(FocusedForm);
end;
procedure TKbdForm.WMMouseActivate(var Message: TWMMouseActivate);
begin
Message.Result := MA_NOACTIVATE;
end;
procedure TKbdForm.WMNCActivate(var Message: TWMNCActivate);
begin
Message.Result := 1; // important
SetForegroundWindow(FocusedForm);
end;
The keyboardform is invoked by the following common code of the edit controls:
procedure TForm17.EditClick(Sender: TObject);
begin
KbdForm.ShowUnfocused(self.Handle);
(Sender as TWinControl).SetFocus;
end;
An alternative to what is said above, could be to set the BorderStyle = bsNone and arrange the dragging of the form with the Mouse Down, Move, Up events directly from the forms surface (or maybe a panel to mimic a top frame), and adding a close button. The challenge would be to get it visually acceptable.
I want my control to receive distinct notifications only when it's parent form (not panel or something else, just the main form of this control) receives and loses focus. Doesn't matter if the focus is switched from another form of the application or between my application and other application, it must be received for both cases. Is it possible? I want to suspend some updates of the control when his form is not active and resume the updates when the form is activated.
Edit: In other words, the control must catch the (TForm.OnActivate + TApplication.OnActivate) and (TForm.OnDeactivate + TApplication.OnDeactivate)
Edit2: If it's not possible both, at least if I can make the control catch the events from TApplication. It's more important than those from TForm.
I want to suspend some updates of the control when his form is not active and resume the updates when the form is activated.
If those updates are done continuously, or are being triggered by a timer or actions, then you could be done with:
type
TMyControl = class(TControl)
private
procedure PerformUpdate;
end;
procedure TMyControl.PerformUpdate;
begin
if Application.Active and HasParent and GetParentForm(Self).Active then
//...
else
//...
end;
...at least if I can make the control catch the events from the application
Catching TApplication.OnActivate and TApplication.OnDeactivate is pretty easy with a TApplicationEvents component:
uses
Vcl.AppEvnts;
type
TMyControl = class(TControl)
private
FActive: Boolean;
FAppEvents: TApplicationEvents;
procedure ApplicationActiveChanged(Sender: TObject);
public
constructor Create(AOwner: TComponent); override;
end;
procedure TMyControl.ApplicationActiveChanged(Sender: TObject);
begin
FActive := Application.Active;
end;
constructor TMyControl.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FAppEvents := TApplicationEvents.Create(Self);
FAppEvents.OnActivate := ApplicationActiveChanged;
FAppEvents.OnDeactivate := ApplicationActiveChanged;
end;
...it's more important than those from the form
Catching the (de)activation of the parenting form can be done in Application.OnIdle. All this combined could result in something like this:
type
TMyControl = class(TControl)
private
FActive: Boolean;
FAppEvents: TApplicationEvents;
FParentForm: TCustomForm;
procedure ApplicationActiveChanged(Sender: TObject);
procedure ApplicationIdle(Sender: TObject; var Done: Boolean);
procedure UpdateActive;
protected
procedure SetParent(AParent: TWinControl); override;
public
constructor Create(AOwner: TComponent); override;
end;
procedure TMyControl.ApplicationActiveChanged(Sender: TObject);
begin
UpdateActive;
end;
procedure TMyControl.ApplicationIdle(Sender: TObject; var Done: Boolean);
begin
UpdateActive;
Done := True;
end;
constructor TMyControl.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FAppEvents := TApplicationEvents.Create(Self);
FAppEvents.OnActivate := ApplicationActiveChanged;
FAppEvents.OnDeactivate := ApplicationActiveChanged;
end;
procedure TMyControl.SetParent(AParent: TWinControl);
begin
inherited SetParent(AParent);
FParentForm := GetParentForm(Self);
end;
procedure TMyControl.UpdateActive;
var
SaveActive: Boolean;
begin
SaveActive := FActive;
FActive := Application.Active and (FParentForm <> nil) and FParentForm.Active;
if Application.Active then
FAppEvents.OnIdle := ApplicationIdle
else
FAppEvents.OnIdle := nil;
if FActive <> SaveActive then
Invalidate;
end;
Because using Application.OnIdle is quite a rigorous method, spare its use like I did above by only assigning it when necessary and speed up its implementation by caching function results like GetParentForm.
Please consider such scenerio:
I have component called TMenuItemSelector which has two published properties: PopupMenu - allows to pick an instance of TPopupMenu from the form and MenuItem which allows to pick any instance of TMenuItem from the form.
I would like to modify property editor for MenuItem property in a way that when PopupMenu is assigned then only menu items from this PopupMenu are visible in a drop down list.
I know that I need to write my own descendant of TComponentProperty and override GetValues method. The problem is that I do not know how to access the form on which TMenuItemSelector is lying.
Original TComponentProperty is using this method to iterate all available instances:
procedure TComponentProperty.GetValues(Proc: TGetStrProc);
begin
Designer.GetComponentNames(GetTypeData(GetPropType), Proc);
end;
However, Designer seems to be precompiled so I have no idea how GetComponentNames works.
This is what I have so far, I guess only thing which I am missing is the implementation of GetValues:
unit uMenuItemSelector;
interface
uses
Classes, Menus, DesignIntf, DesignEditors;
type
TMenuItemSelector = class(TComponent)
private
FPopupMenu: TPopUpMenu;
FMenuItem: TMenuItem;
procedure SetPopupMenu(const Value: TPopUpMenu);
procedure SetMenuItem(const Value: TMenuItem);
published
property PopupMenu: TPopUpMenu read FPopupMenu write SetPopupMenu;
property MenuItem: TMenuItem read FMenuItem write SetMenuItem;
end;
type
TMenuItemProp = class(TComponentProperty)
public
function GetAttributes: TPropertyAttributes; override;
procedure GetValues(Proc: TGetStrProc); override;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterPropertyEditor(TypeInfo(TMenuItem), TMenuItemSelector, 'MenuItem', TMenuItemProp);
RegisterComponents('Test', [TMenuItemSelector]);
end;
{ TMenuItemSelector }
procedure TMenuItemSelector.SetMenuItem(const Value: TMenuItem);
begin
FMenuItem := Value;
end;
procedure TMenuItemSelector.SetPopupMenu(const Value: TPopUpMenu);
begin
FPopupMenu := Value;
end;
{ TMenuItemProperty }
function TMenuItemProp.GetAttributes: TPropertyAttributes;
begin
Result := inherited GetAttributes + [paValueList, paSortList];
end;
procedure TMenuItemProp.GetValues(Proc: TGetStrProc);
begin
//How to filter MenuItems from the form in a way that only
//MenuItems which belong to TMenuItemSelector.PopupMenu are displayed? \
//And how to get to that form?
//inherited;
end;
end.
Anyone could help?
Thanks.
When TMenuItemProp.GetValues() is called, you need to look at the TMenuItemSelector object whose MenuItem property is currently being edited, see if that object has a PopupMenu assigned, and if so then loop through its items as neded, eg:
procedure TMenuItemProp.GetValues(Proc: TGetStrProc);
var
Selector: TMenuItemSelector;
I: Integer;
begin
Selector := GetComponent(0) as TMenuItemSelector;
if Selector.PopupMenu <> nil then
begin
with Selector.PopupMenu.Items do
begin
for I := 0 to Count-1 do
Proc(Designer.GetComponentName(Items[I]));
end;
end else
inherited GetValues(Proc);
end;
BTW, you need to implement TMenuItemSelector and TMenuItemProp in separate packages. With the exception of the RegisterComponents() function, (which is implemented in a runtime package), design-time code is not allowed to be compiled into run-time executables. It is against the EULA, and Embarcadero's design-time pacakges are not allowed to be distributed. You need to implement TMenuItemSelector in a runtime-only package, and then implement TMenuItemProp and Register() in a designtime-only package that Requires the runtime-only package and uses the unit that TMenuItemSelector is declared in, eg:
unit uMenuItemSelector;
interface
uses
Classes, Menus;
type
TMenuItemSelector = class(TComponent)
private
FPopupMenu: TPopUpMenu;
FMenuItem: TMenuItem;
procedure SetPopupMenu(const Value: TPopUpMenu);
procedure SetMenuItem(const Value: TMenuItem);
protected
procedure Notification(AComponent: TComponent; Operation: TOperation); override;
published
property PopupMenu: TPopUpMenu read FPopupMenu write SetPopupMenu;
property MenuItem: TMenuItem read FMenuItem write SetMenuItem;
end;
implementation
{ TMenuItemSelector }
procedure TMenuItemSelector.Notification(AComponent: TComponent; Operation: TOperation);
begin
inherited;
if Operation = opRemove then
begin
if AComponent = FPopupMenu then
begin
FPopupMenu := nil;
FMenuItem := nil;
end
else if AComponent = FMenuItem then
begin
FMenuItem := nil;
end;
end;
end;
procedure TMenuItemSelector.SetMenuItem(const Value: TMenuItem);
begin
if FMenuItem <> Value then
begin
if FMenuItem <> nil then FMenuItem.RemoveFreeNotification(Self);
FMenuItem := Value;
if FMenuItem <> nil then FMenuItem.FreeNotification(Self);
end;
end;
procedure TMenuItemSelector.SetPopupMenu(const Value: TPopUpMenu);
begin
if FPopupMenu <> Value then
begin
if FPopupMenu <> nil then FPopupMenu.RemoveFreeNotification(Self);
FPopupMenu := Value;
if FPopupMenu <> nil then FPopupMenu.FreeNotification(Self);
SetMenuItem(nil);
end;
end;
end.
.
unit uMenuItemSelectorEditor;
interface
uses
Classes, DesignIntf, DesignEditors;
type
TMenuItemSelectorMenuItemProp = class(TComponentProperty)
public
function GetAttributes: TPropertyAttributes; override;
procedure GetValues(Proc: TGetStrProc); override;
end;
procedure Register;
implementation
uses
Menus, uMenuItemSelector;
procedure Register;
begin
RegisterComponents('Test', [TMenuItemSelector]);
RegisterPropertyEditor(TypeInfo(TMenuItem), TMenuItemSelector, 'MenuItem', TMenuItemSelectorMenuItemProp);
end;
{ TMenuItemSelectorMenuItemProp }
function TMenuItemSelectorMenuItemProp.GetAttributes: TPropertyAttributes;
begin
Result := inherited GetAttributes + [paValueList, paSortList] - [paMultiSelect];
end;
procedure TMenuItemSelectorMenuItemProp.GetValues(Proc: TGetStrProc);
var
Selector: TMenuItemSelector;
I: Integer;
begin
Selector := GetComponent(0) as TMenuItemSelector;
if Selector.PopupMenu <> nil then
begin
with Selector.PopupMenu.Items do
begin
for I := 0 to Count-1 do
Proc(Designer.GetComponentName(Items[I]));
end;
end else
inherited GetValues(Proc);
end;
end.
What do I need to do for adding actions support to my component. It is a button component but I guess it is the same for whatever component type it is. Any information or how to will help.
That depends on how you define action support. There is two kinds:
A possibly customized Action property of your component, which is assignable by an Action component
The Action component itself.
An action property
Every TControl descendant has an Action property which execution is by default linked to a left mouse button click. This link is managed by an ActionLink. The default ActionLink is of the type TControlActionLink which takes care of the synchronization of the caption, the hint, the enabled state, etc... of both the Action and that of the Control. If this basis functionality is all that you want, then simply publish the Action property in your component type declaration and the Delphi framework takes care of all, like Serg and LU RD already answered.
If you want your own Action property to be linked to some other condition or event (i.e. other than Click), or if you want to implement an Action property for a specific sub element of your component (that is not a TControl descendant), then you can implement your own custom Action property by defining and implementing a custom ActionLink class.
Suppose your component is some kind of grid which has columns and you want every column to have an action property that should be invoked when the user clicks the title of a column. Since such columns are likely to be of a TCollectionItem type, the column type does not have an action property by default. So you have to implement one yourself. Consider the next example which links the action's caption to the column's title, links the action's enabled state inversely to the column's readonly property and so on...:
unit Unit1;
interface
uses
Classes, ActnList, SysUtils;
type
TColumn = class;
TColumnActionLink = class(TActionLink)
protected
FClient: TColumn;
procedure AssignClient(AClient: TObject); override;
function IsCaptionLinked: Boolean; override;
function IsEnabledLinked: Boolean; override;
function IsOnExecuteLinked: Boolean; override;
function IsVisibleLinked: Boolean; override;
procedure SetCaption(const Value: String); override;
procedure SetEnabled(Value: Boolean); override;
procedure SetOnExecute(Value: TNotifyEvent); override;
procedure SetVisible(Value: Boolean); override;
end;
TColumnActionLinkClass = class of TColumnActionLink;
TColumn = class(TCollectionItem)
private
FActionLink: TColumnActionLink;
FGrid: TComponent;
FOnTitleClick: TNotifyEvent;
FReadOnly: Boolean;
FTitle: String;
FVisible: Boolean;
function DefaultTitleCaption: String;
procedure DoActionChange(Sender: TObject);
function GetAction: TBasicAction;
function IsOnTitleClickStored: Boolean;
function IsReadOnlyStored: Boolean;
function IsVisibleStored: Boolean;
procedure SetAction(Value: TBasicAction);
protected
procedure ActionChanged(Sender: TObject; CheckDefaults: Boolean); dynamic;
procedure DoTitleClick; virtual;
function GetActionLinkClass: TColumnActionLinkClass; virtual;
property ActionLink: TColumnActionLink read FActionLink write FActionLink;
public
destructor Destroy; override;
procedure InitiateAction; virtual;
published
property Action: TBasicAction read GetAction write SetAction;
property OnTitleClick: TNotifyEvent read FOnTitleClick write FOnTitleClick
stored IsOnTitleClickStored;
property ReadOnly: Boolean read FReadOnly write FReadOnly
stored IsReadOnlyStored;
property Title: String read FTitle write FTitle;
property Visible: Boolean read FVisible write FVisible
stored IsVisibleStored;
end;
implementation
{ TColumnActionLink }
procedure TColumnActionLink.AssignClient(AClient: TObject);
begin
FClient := TColumn(AClient);
end;
function TColumnActionLink.IsCaptionLinked: Boolean;
begin
Result := inherited IsCaptionLinked and (Action is TCustomAction) and
(FClient.Title = TCustomAction(Action).Caption);
end;
function TColumnActionLink.IsEnabledLinked: Boolean;
begin
Result := inherited IsEnabledLinked and (Action is TCustomAction) and
(FClient.ReadOnly <> TCustomAction(Action).Enabled);
end;
function TColumnActionLink.IsOnExecuteLinked: Boolean;
begin
Result := inherited IsOnExecuteLinked and
(#FClient.OnTitleClick = #Action.OnExecute);
end;
function TColumnActionLink.IsVisibleLinked: Boolean;
begin
Result := inherited IsVisibleLinked and (Action is TCustomAction) and
(FClient.Visible = TCustomAction(Action).Visible);
end;
procedure TColumnActionLink.SetCaption(const Value: string);
begin
if IsCaptionLinked then
FClient.Title := Value;
end;
procedure TColumnActionLink.SetEnabled(Value: Boolean);
begin
if IsEnabledLinked then
FClient.ReadOnly := not Value;
end;
procedure TColumnActionLink.SetOnExecute(Value: TNotifyEvent);
begin
if IsOnExecuteLinked then
FClient.OnTitleClick := Value;
end;
procedure TColumnActionLink.SetVisible(Value: Boolean);
begin
if IsVisibleLinked then
FClient.Visible := Value;
end;
{ TColumn }
procedure TColumn.ActionChanged(Sender: TObject; CheckDefaults: Boolean);
begin
if Sender is TCustomAction then
with TCustomAction(Sender) do
begin
if not CheckDefaults or (Caption = DefaultTitleCaption) then
FTitle := Caption;
if not CheckDefaults or (not ReadOnly) then
ReadOnly := not Enabled;
if not CheckDefaults or not Assigned(FOnTitleClick) then
FOnTitleClick := OnExecute;
if not CheckDefaults or (Self.Visible = True) then
Self.Visible := Visible;
Changed(False);
end;
end;
function TColumn.DefaultTitleCaption: String;
begin
Result := 'Column' + IntToStr(Index);
end;
destructor TColumn.Destroy;
begin
FreeAndNil(FActionLink);
inherited Destroy;
end;
procedure TColumn.DoActionChange(Sender: TObject);
begin
if Sender = Action then
ActionChanged(Sender, False);
end;
procedure TColumn.DoTitleClick;
begin
if Assigned(FOnTitleClick) then
if (Action <> nil) and (#FOnTitleClick <> #Action.OnExecute) then
FOnTitleClick(Self)
else if FActionLink = nil then
FOnTitleClick(Self)
else if FActionLink <> nil then
if (FGrid <> nil) and not (csDesigning in FGrid.ComponentState) then
begin
if not FActionLink.Execute(FGrid) then
FOnTitleClick(Self);
end
else
if not FActionLink.Execute(nil) then
FOnTitleClick(Self);
end;
function TColumn.GetAction: TBasicAction;
begin
if FActionLink <> nil then
Result := FActionLink.Action
else
Result := nil;
end;
function TColumn.GetActionLinkClass: TColumnActionLinkClass;
begin
Result := TColumnActionLink;
end;
procedure TColumn.InitiateAction;
begin
if FActionLink <> nil then
FActionLink.Update;
end;
function TColumn.IsOnTitleClickStored: Boolean;
begin
Result := (FActionLink = nil) or not ActionLink.IsOnExecuteLinked;
end;
function TColumn.IsReadOnlyStored: Boolean;
begin
Result := (FActionLink = nil) or not FActionLink.IsEnabledLinked;
if Result then
Result := FReadOnly;
end;
function TColumn.IsVisibleStored: Boolean;
begin
Result := (FActionLink = nil) or not FActionLink.IsVisibleLinked;
if Result then
Result := not Visible;
end;
procedure TColumn.SetAction(Value: TBasicAction);
begin
if Value = nil then
FreeAndNil(FActionLink)
else
begin
if FActionLink = nil then
FActionLink := GetActionLinkClass.Create(Self);
FActionLink.Action := Value;
FActionLink.OnChange := DoActionChange;
ActionChanged(Value, csLoading in Value.ComponentState);
if FGrid <> nil then
Value.FreeNotification(FGrid);
end;
Changed(False);
end;
end.
Note that this code is stripped to only the applicable action parts.
Source: www.nldelphi.com.
An action component
An action component is assignable to the action property of an arbitrary component. But since explaining all that is involved with writing such an action component is pretty comprehensive, I will make it easy for myself in providing the example below.
Suppose you want to make a control that provides zoom capabilities and that you also want the corresponding ZoomIn and ZoomOut actions that can be assigned to toolbar buttons.
unit Zoomer;
interface
uses
Classes, Controls, ActnList, Forms, Menus, Windows;
type
TZoomer = class;
TZoomAction = class(TCustomAction)
private
FZoomer: TZoomer;
procedure SetZoomer(Value: TZoomer);
protected
function GetZoomer(Target: TObject): TZoomer;
procedure Notification(AComponent: TComponent; Operation: TOperation);
override;
public
destructor Destroy; override;
function HandlesTarget(Target: TObject): Boolean; override;
procedure UpdateTarget(Target: TObject); override;
published
property Caption;
property Enabled;
property HelpContext;
property HelpKeyword;
property HelpType;
property Hint;
property ImageIndex;
property ShortCut;
property SecondaryShortCuts;
property Visible;
property OnExecute; { This property could be omitted. But if you want to be
able to override the default behavior of this action
(zooming in on a TZoomer component), then you need to
assign this event. From within the event handler
you could invoke the default behavior manually. }
property OnHint;
property OnUpdate;
property Zoomer: TZoomer read FZoomer write SetZoomer;
end;
TZoomInAction = class(TZoomAction)
public
constructor Create(AOwner: TComponent); override;
procedure ExecuteTarget(Target: TObject); override;
end;
TZoomer = class(TCustomControl)
public
procedure ZoomIn;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('RoyMKlever', [TZoomer]);
RegisterActions('Zoomer', [TZoomInAction], nil);
end;
{ TZoomAction }
destructor TZoomAction.Destroy;
begin
if FZoomer <> nil then
FZoomer.RemoveFreeNotification(Self);
inherited Destroy;
end;
function TZoomAction.GetZoomer(Target: TObject): TZoomer;
begin
if FZoomer <> nil then
Result := FZoomer
else if (Target is TZoomer) and TZoomer(Target).Focused then
Result := TZoomer(Target)
else if Screen.ActiveControl is TZoomer then
Result := TZoomer(Screen.ActiveControl)
else
{ This should not happen! HandlesTarget is called before ExecuteTarget,
or the action is disabled }
Result := nil;
end;
function TZoomAction.HandlesTarget(Target: TObject): Boolean;
begin
Result := ((FZoomer <> nil) and FZoomer.Enabled) or
((FZoomer = nil) and (Target is TZoomer) and TZoomer(Target).Focused) or
((Screen.ActiveControl is TZoomer) and Screen.ActiveControl.Enabled);
end;
procedure TZoomAction.Notification(AComponent: TComponent;
Operation: TOperation);
begin
inherited Notification(AComponent, Operation);
if (Operation = opRemove) and (AComponent = FZoomer) then
FZoomer := nil;
end;
procedure TZoomAction.SetZoomer(Value: TZoomer);
begin
if FZoomer <> Value then
begin
if FZoomer <> nil then
FZoomer.RemoveFreeNotification(Self);
FZoomer := Value;
if FZoomer <> nil then
FZoomer.FreeNotification(Self);
end;
end;
procedure TZoomAction.UpdateTarget(Target: TObject);
begin
Enabled := HandlesTarget(Target);
end;
{ TZoomInAction }
constructor TZoomInAction.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
Caption := 'Zoom in';
Hint := 'Zoom in|Zooms in on the selected zoomer control';
ShortCut := Menus.ShortCut(VK_ADD, [ssCtrl]);
end;
procedure TZoomInAction.ExecuteTarget(Target: TObject);
begin
GetZoomer(Target).ZoomIn;
{ For safety, you cóuld check if GetZoomer <> nil. See remark in GetZoomer. }
end;
{ TZoomer }
procedure TZoomer.ZoomIn;
begin
{ implementation of zooming in }
end;
end.
Activating this action (with a click on a toolbar button, or choosing a menu item) calls in the following priority the ZoomIn routine of:
the Zoomer control that you manually have set in the relating property of the action, if done so, and if the action is enabled, otherwise:
the by the application requested Target, but only if that target is a focused Zoomer control, or otherwise:
the active control in the entire application, but only if that is an enabled Zoomer control.
Subsequently, the ZoomOut action is simply added:
type
TZoomOutAction = class(TZoomAction)
public
constructor Create(AOwner: TComponent); override;
procedure ExecuteTarget(Target: TObject); override;
end;
{ TZoomOutAction }
constructor TZoomOutAction.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
Caption := 'Zoom out';
Hint := 'Zoom out|Zooms out on the selected zoomer control';
ShortCut := Menus.ShortCut(VK_SUBTRACT, [ssCtrl]);
end;
procedure TZoomOutAction.ExecuteTarget(Target: TObject);
begin
GetZoomer(Target).ZoomOut;
end;
Note that action components require registration in the IDE for being able to use them design time.
Applicable read food in the Delphi help:
Writing action components,
How actions find their targets,
Registering actions,
What happens when an action fires,
Updating actions,
Setting up action lists.
Source: www.nldelphi.com.
Basic action support is implemented in TControl class, so in the most simple case all you have to do is to inherit your component from TControl descendant and declare Action property as published, ex:
type
TMyGraphicControl = class(TGraphicControl)
published
property Action;
end;
If your component has additional properties that should be linked to TAction properties you should also override ActionChange method.
If your component is already a descendant of TButton then the action support is inherited.
All you need to do is declare the action property as published.