I have a project (Delphi 10 Seattle, win32) with a many menus and many items in those menus. Some of the menu items are created at design time, some of them at run time.
What I'm looking to do is log some information about the TMenuItem, such as the name/caption, timestamp, etc. when the OnClick event is triggered.
I could simply add a procedure call to the start of every function which is assigned to the TMenuItem OnClick event but I was wondering if there was a more elegant solution.
Also to note, I have tried Embarcadero's AppAnalytics but I found it didn't give me the information or flexibility I wanted and was rather pricey.
Edit: I'll add some more information detailing what options I have considered (which I probably should've done to start with).
The simple adding a function to every menuitem click I want to log, which would mean doing this for a lot of functions and would have to add it to every new menu item added.
procedure TSomeForm.SomeMenuItem1Click(Sender: TObject);
var
item : TMenuItem;
begin
item := Sender as TMenuItem;
LogMenuItem(item); // Simple log function added to the start of each menuitem click
end;
By 'more elegant solution' I mean would it be possible to add a 'hook' so that all TMenuItem OnClick events triggered another procedure (which would do the logging) before calling the procedure assigned to the OnClick Event.
Or another option I considered was creating a class which inherited from TMenuItem which would override TMenuItem.Click and do the logging before generating the OnClick event. But then I didn't know how that would work for the design time menu items without a lot of work remaking the menus.
This is much easier to achieve using actions. That has the benefit that you'll pick up actions invoked by UI elements other than menus, for instance toolbars, buttons etc.
Use an action list, or an action manager, as you prefer. For example, with an action list the action list object has an OnExecute event that fires when any action is executed. You can listen for that event and there log the details of the action being executed.
I absolutely agree that actions is the way to go, but for completeness sake and those cases where you quickly want to debug an application using old-style menus, here's a unit that you can use with menu items. It will even work if the menu items have actions linked to them, but it won't work for any other controls with actions like a TActionMainMenuBar. All the debugging code is in this unit to keep your normal code clutter-free. Just add the unit to the uses clause and call StartMenuLogging with any applicable component, e.g. a menu component, form component or even Application! Any menu items in the tree under it will be hooked. So you can potentially debug all menu-clicks in all forms with just those two lines in your production code. You can use StopMenuLogging to stop, but it's optional. Warning: This unit was not tested properly - I took an old debug unit I wrote and cleaned it up for this purpose and just tested it superficially.
unit LogMenuClicks;
interface
uses
Classes;
procedure StartMenuLogging(AComponent: TComponent);
procedure StopMenuLogging(AComponent: TComponent);
procedure StopAllMenuLogging;
implementation
uses
SysUtils,
Menus;
type
PLoggedItem = ^TLoggedItem;
TLoggedItem = record
Item: TMenuItem;
OldClickEvent: TNotifyEvent;
end;
TLogManager = class(TComponent)
private
FList: TList;
FLog: TFileStream;
procedure Delete(Index: Integer);
function FindControl(AItem: TMenuItem): Integer;
procedure LogClick(Sender: TObject);
protected
procedure Notification(AComponent: TComponent; Operation: TOperation); override;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
procedure AddControl(AItem: TMenuItem);
procedure RemoveControl(AItem: TMenuItem);
end;
var
LogMan: TLogManager = nil;
{ TLogManager }
constructor TLogManager.Create(AOwner: TComponent);
begin
inherited;
FLog := TFileStream.Create(ChangeFileExt(ParamStr(0), '.log'), fmCreate or fmShareDenyWrite);
FList := TList.Create;
end;
destructor TLogManager.Destroy;
var
i: Integer;
begin
i := FList.Count - 1;
while i >= 0 do
Delete(i);
FList.Free;
FLog.Free;
inherited;
end;
procedure TLogManager.Notification(AComponent: TComponent; Operation: TOperation);
begin
if Operation = opRemove then
RemoveControl(TMenuItem(AComponent));
inherited;
end;
procedure TLogManager.Delete(Index: Integer);
var
li: PLoggedItem;
begin
li := FList[Index];
with li^ do
begin
Item.RemoveFreeNotification(Self);
Item.OnClick := OldClickEvent;
end;
Dispose(li);
FList.Delete(Index);
end;
function TLogManager.FindControl(AItem: TMenuItem): Integer;
begin
Result := FList.Count - 1;
while (Result >= 0) and (PLoggedItem(FList[Result]).Item <> AItem) do
Dec(Result);
end;
procedure TLogManager.AddControl(AItem: TMenuItem);
var
li: PLoggedItem;
begin
if not Assigned(AItem) then
Exit;
if FindControl(AItem) >= 0 then
Exit;
New(li);
li.Item := AItem;
li.OldClickEvent := AItem.OnClick;
AItem.OnClick := LogClick;
FList.Add(li);
AItem.FreeNotification(Self);
end;
procedure TLogManager.RemoveControl(AItem: TMenuItem);
var
i: Integer;
begin
if Assigned(AItem) then
begin
i := FindControl(AItem);
if i >= 0 then
Delete(i);
end;
end;
procedure TLogManager.LogClick(Sender: TObject);
var
s: string;
begin
s := Format('%s: %s' + sLineBreak, [TComponent(Sender).Name, FormatDateTime('', Now)]);
FLog.WriteBuffer(s[1], Length(s));
PLoggedItem(FList[FindControl(TMenuItem(Sender))]).OldClickEvent(Sender);
end;
procedure StartMenuLogging(AComponent: TComponent);
procedure CheckControls(Comp: TComponent);
var
i: Integer;
begin
if Comp is TMenuItem then
LogMan.AddControl(TMenuItem(Comp))
else
for i := 0 to Comp.ComponentCount - 1 do
CheckControls(Comp.Components[i]);
end;
begin
if not Assigned(LogMan) then
LogMan := TLogManager.Create(nil);
CheckControls(AComponent);
end;
procedure StopMenuLogging(AComponent: TComponent);
procedure CheckControls(Comp: TComponent);
var
i: Integer;
begin
if Comp is TMenuItem then
LogMan.RemoveControl(TMenuItem(Comp))
else
for i := 0 to Comp.ComponentCount - 1 do
CheckControls(Comp.Components[i]);
end;
begin
if Assigned(LogMan) then
CheckControls(AComponent);
end;
procedure StopAllMenuLogging;
begin
LogMan.Free;
end;
initialization
finalization
if Assigned(LogMan) then
LogMan.Free;
end.
Related
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.
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.
By Long Press, I mean pressing a button / panel and hold for a period (say 2 seconds) without releasing or dragging around. It is common in mobile phone and touch device.
I had tried using Gesture, checked toPressAndHold in TabletOptions and Checked all in InteractiveGestureOptions but long pressing cause no OnGesture Call.
Another implementation I can think of is adding a timer, start it in MouseDown and end it in either Timer Fired, StartDrag, MouseUp or MouseLeave. However, as I want to add this behavior to several different buttons and panel component, I would have to override a brunch of procedure in each class and copy the code around for each component.
Is there a better way of achieving that?
Edit :
To NGLN
Woo, great piece of work! Together with your answer to those scrolling effects, VCL can almost achieve mobile OS look and feel!
Your code work perfectly with common controls but I got 2 issues in my case
Long Clicking on the form cannot be detected (of cause as the form
is not parent of itself) I shift the Find FChild Code to separate
procedure and call from both WMParentNotify and FormMouseDown to
solve it.
I got some custom button which has some disabled HTML
labels (Header, Caption, Footer) covering up the label original
surface, Using your code, FChild will be one of those label but it
do not get MouseCapture. I add the below line to overcome it :
while not TControlAccess(FChild).Enabled do
FChild := FChild.Parent;
Finally, for some more complicated controls like TCategoryButtons or TListBox, the user of the event might need to check not against the whole control but a specify item in the control. So I think we need to save the original CursorPos and fire another event when the timer triggered to let manual determination of whether it meet the long press condition or not. If yes or event not assigned, then use your default code for determination.
All in all, we can just create a LongPress supported form / panel to host all other controls. This is much more easier then implementing the LongPress feature Component by Component! Great Thanks!
Edit2 :
To NGLN
Thanks again for your component version, which is excellent approach, not needing to do any modification to existing components and can detect long press everywhere!
For your information, I had do several modification to suit my own need.
TCustomForm vs TWinControl : As most of my application has only 1 main form and all other visual units are my own created frame (not from TFrame but TScrollingWinControl with ccpack support), assuming TCustomForm do not work for me. So I had deleted property form (but retain FForm for ActiveControl) and create a published property Host : TWinControl to act as the parent host. In that way, I can also limit the detection to some limited panel. When Assigning Host, I check and find the FForm using GetParentForm(FHost).
Disabled Controls : As I said previously, I got some disabled TJvHTLabel covering my buttons and your component work on the labels. I can of cause find back the button by the label, but I think it would be more convenient if it had been handled by the new component. So I add a property SkipDisabled and if set to turn, loop in its parent line to find first enabled control.
I add a PreserveFocus property to let component user choose to keep last activecontrol or not.
Controls with items. I changed your TLongPressEvent, adding the ClickPos as the 2nd parameter. So, I can now use the ClickPos to find which item in a list box or the like had been long held.
It seems to me that FindVCLWindow is having same effect with your FindControlAtPos?
Thank you again for your great work.
At every left mouse button click, WM_PARENTNOTIFY is send to all (grand) parents of the clicked control. So this can be used for tracking the starting point of a long press, and the duration of a press can be timed with a timer. What is left is to decide when a press should be called a long press. And to wrap this all up in a nice component of course.
In the component written below, the OnLongPress event handler is fired when the following conditions are met:
after the interval, the control still has mouse capture, or still has focus, or is disabled,
after the interval, the mouse has not moved more then Mouse.DragThreshold.
Some explanation on the code:
It temporarily replaces the control's OnMouseUp event handler, otherwise consecutive clicks might also result in a long press. The intermediate event handler disables the tracking timer, calls the original event handler and replaces it back.
After the long press, the active control is reset, because I thought a long press is not done with the intention to focus the control. But that's just my guess, and it might be candidate for a property.
Also tracks for long presses on the form itself (rather then only its childs).
Has a customized FindControlAtPos routine which performs a deep search on an arbitrary window. Alternatives were (1) TWinControl.ControlAtPos, but it searches just one level deep, and (2) Controls.FindDragTarget, but despite the AllowDisabled parameter, it is not able of finding disabled controls.
unit LongPressEvent;
interface
uses
Classes, Controls, Messages, Windows, Forms, ExtCtrls;
type
TLongPressEvent = procedure(Control: TControl) of object;
TLongPressTracker = class(TComponent)
private
FChild: TControl;
FClickPos: TPoint;
FForm: TCustomForm;
FOldChildOnMouseUp: TMouseEvent;
FOldFormWndProc: TFarProc;
FOnLongPress: TLongPressEvent;
FPrevActiveControl: TWinControl;
FTimer: TTimer;
procedure AttachForm;
procedure DetachForm;
function GetDuration: Cardinal;
procedure LongPressed(Sender: TObject);
procedure NewChildMouseUp(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
procedure NewFormWndProc(var Message: TMessage);
procedure SetDuration(Value: Cardinal);
procedure SetForm(Value: TCustomForm);
procedure StartTracking;
protected
procedure Notification(AComponent: TComponent; Operation: TOperation);
override;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
property Form: TCustomForm read FForm write SetForm;
published
property Duration: Cardinal read GetDuration write SetDuration
default 1000;
property OnLongPress: TLongPressEvent read FOnLongPress
write FOnLongPress;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('Samples', [TLongPressTracker]);
end;
function FindControlAtPos(Window: TWinControl;
const ScreenPos: TPoint): TControl;
var
I: Integer;
C: TControl;
begin
for I := Window.ControlCount - 1 downto 0 do
begin
C := Window.Controls[I];
if C.Visible and PtInRect(C.ClientRect, C.ScreenToClient(ScreenPos)) then
begin
if C is TWinControl then
Result := FindControlAtPos(TWinControl(C), ScreenPos)
else
Result := C;
Exit;
end;
end;
Result := Window;
end;
{ TLongPressTracker }
type
TControlAccess = class(TControl);
procedure TLongPressTracker.AttachForm;
begin
if FForm <> nil then
begin
FForm.HandleNeeded;
FOldFormWndProc := Pointer(GetWindowLong(FForm.Handle, GWL_WNDPROC));
SetWindowLong(FForm.Handle, GWL_WNDPROC,
Integer(MakeObjectInstance(NewFormWndProc)));
end;
end;
constructor TLongPressTracker.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FTimer := TTimer.Create(Self);
FTimer.Enabled := False;
FTimer.Interval := 1000;
FTimer.OnTimer := LongPressed;
if AOwner is TCustomForm then
SetForm(TCustomForm(AOwner));
end;
destructor TLongPressTracker.Destroy;
begin
if FTimer.Enabled then
begin
FTimer.Enabled := False;
TControlAccess(FChild).OnMouseUp := FOldChildOnMouseUp;
end;
DetachForm;
inherited Destroy;
end;
procedure TLongPressTracker.DetachForm;
begin
if FForm <> nil then
begin
if FForm.HandleAllocated then
SetWindowLong(FForm.Handle, GWL_WNDPROC, Integer(FOldFormWndProc));
FForm := nil;
end;
end;
function TLongPressTracker.GetDuration: Cardinal;
begin
Result := FTimer.Interval;
end;
procedure TLongPressTracker.LongPressed(Sender: TObject);
begin
FTimer.Enabled := False;
if (Abs(FClickPos.X - Mouse.CursorPos.X) < Mouse.DragThreshold) and
(Abs(FClickPos.Y - Mouse.CursorPos.Y) < Mouse.DragThreshold) and
(((FChild is TWinControl) and TWinControl(FChild).Focused) or
(TControlAccess(FChild).MouseCapture or (not FChild.Enabled))) then
begin
FForm.ActiveControl := FPrevActiveControl;
if Assigned(FOnLongPress) then
FOnLongPress(FChild);
end;
TControlAccess(FChild).OnMouseUp := FOldChildOnMouseUp;
end;
procedure TLongPressTracker.NewChildMouseUp(Sender: TObject;
Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
FTimer.Enabled := False;
if Assigned(FOldChildOnMouseUp) then
FOldChildOnMouseUp(Sender, Button, Shift, X, Y);
TControlAccess(FChild).OnMouseUp := FOldChildOnMouseUp;
end;
procedure TLongPressTracker.NewFormWndProc(var Message: TMessage);
begin
case Message.Msg of
WM_PARENTNOTIFY:
if TWMParentNotify(Message).Event = WM_LBUTTONDOWN then
StartTracking;
WM_LBUTTONDOWN:
StartTracking;
end;
with Message do
Result := CallWindowProc(FOldFormWndProc, FForm.Handle, Msg, WParam,
LParam);
end;
procedure TLongPressTracker.Notification(AComponent: TComponent;
Operation: TOperation);
begin
inherited Notification(AComponent, Operation);
if (AComponent = FForm) and (Operation = opRemove) then
DetachForm;
if (AComponent = FChild) and (Operation = opRemove) then
begin
FTimer.Enabled := False;
FChild := nil;
end;
end;
procedure TLongPressTracker.SetDuration(Value: Cardinal);
begin
FTimer.Interval := Value;
end;
procedure TLongPressTracker.SetForm(Value: TCustomForm);
begin
if FForm <> Value then
begin
DetachForm;
FForm := Value;
FForm.FreeNotification(Self);
AttachForm;
end;
end;
procedure TLongPressTracker.StartTracking;
begin
FClickPos := Mouse.CursorPos;
FChild := FindControlAtPos(FForm, FClickPos);
FChild.FreeNotification(Self);
FPrevActiveControl := FForm.ActiveControl;
FOldChildOnMouseUp := TControlAccess(FChild).OnMouseUp;
TControlAccess(FChild).OnMouseUp := NewChildMouseUp;
FTimer.Enabled := True;
end;
end.
To get this component working, add it to a package, or use this runtime code:
...
private
procedure LongPress(Control: TControl);
end;
...
procedure TForm1.FormCreate(Sender: TObject);
begin
with TLongPressTracker.Create(Self) do
OnLongPress := LongPress;
end;
procedure TForm1.LongPress(Control: TControl);
begin
Caption := 'Long press occurred on: ' + Sender.ClassName;
end;
I'm using Delphi XE2. I build a custom TComboBox so that I can easily add key/string pairs and handle the cleanup in the component's destructor.
All if not (csDesigning in ComponentState) code is omitted for brevity.
interface
type
TKeyRec = class(TObject)
Key: string;
Value: string;
end;
TMyComboBox = class(TComboBox)
public
destructor Destroy; override;
procedure AddItemPair(const Key, Value: string);
end;
implementation
destructor TMyComboBox.Destroy;
var i: Integer;
begin
for i := 0 to Self.Items.Count - 1 do
Self.Items.Objects[i].Free;
Self.Clear;
inherited;
end;
procedure TMyComboBox.AddItemPair(const Key, Value: string);
var rec: TKeyRec;
begin
rec := TKeyRec.Create;
rec.Key := Key;
rec.Value := Value;
Self.Items.AddObject(Value, rec);
end;
When the application closes, the destructor is called, but the Items.Count property is inaccessible because the TComboBox must have a parent control to access this property. By the time the destructor is called, it no longer has a parent control.
I saw this problem once before and had to store the objects in a separate TList and free them separately. But that only worked because the order that I added them to the TList was always the same as the strings added to the combo box. When the user selected a string, I could use the combo box index to find the correlating object in the TList. If the combo box is sorted, then the indexes won't match, so I can't always use that solution.
Has anyone else seen this? How did you workaround the issue? It would be really nice to be able to free the objects in the component's destructor!
You can override function GetItemsClass:
function GetItemsClass: TCustomComboBoxStringsClass; override;
It is called by Combo to create Items (by default it is TComboBoxStrings probably).
Then you can create your own TComboBoxStrings descendant, for example TComboBoxStringObjects, where
you can free object linked with item (when item deleted).
After reading the link from Sertac (David Heffernan's comment and NGLN's answer), I believe a solution that stores the objects in a managed list and not in a GUI control is the best. To that end, I have create a combo box that descends from TCustomComboBox. This lets me promote all the properties except for Sorted to published. This keeps the internal FList in sync with the strings in the combo boxes Items property. I just make sure they are sorted the way I want before adding them...
The following shows what I did. I only included the essential code (less range checking) for brevity, but included some conditional logic that allows the combo box to be used without objects as well.
FList is properly destroyed in the destructor, freeing all objects without any run-time exceptions and the object list is managed within the component itself instead of having to manage it elsewhere -- making it very portable. It works when the control is added to a form at design-time, or when it is created at run-time. I hope this is useful to someone else!
interface
type
TMyComboBox = class(TCustomComboBox)
private
FList: TList;
FUsesObjects: Boolean;
function GetKey: string;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
procedure AddItemPair(const Key, Value: string);
procedure ClearAllItems;
procedure DeleteItem(const Index: Integer);
property Key: string read GetKey;
published
// all published properties (except Sorted) from TComboBox
end;
implementation
type
TKeyRec = class(TObject)
Key: string;
Value: string;
end;
function TMyComboBox.GetKey: string;
begin
if not FUsesObjects then
raise Exception.Create('Objects are not used.');
Result := TKeyRec(FList.Items[ItemIndex]).Key;
end;
constructor TMyComboBox.Create(AOwner: TComponent);
begin
inherited;
if not (csDesigning in ComponentState) then
begin
FUsesObjects := False;
FList := TList.Create;
end;
end;
destructor TMyComboBox.Destroy;
begin
if not (csDesigning in ComponentState) then
begin
ClearAllItems;
FreeAndNil(FList);
end;
inherited;
end;
procedure TMyComboBox.AddItemPair(const Key, Value: string);
var rec: TKeyRec;
begin
FUsesObjects := True;
rec := TKeyRec.Create;
rec.Key := Key;
rec.Value := Value;
FList.Add(rec);
Items.Add(Value);
end;
procedure TMyComboBox.ClearAllItems;
var i: Integer;
begin
if not (csDesigning in ComponentState) then
begin
if FUsesObjects then
begin
for i := 0 to FList.Count - 1 do
TKeyRec(FList.Items[i]).Free;
FList.Clear;
end;
if not (csDestroying in ComponentState) then
Clear; // can't clear if the component is being destroyed or there is an exception, 'no parent window'
end;
end;
procedure TMyComboBox.DeleteItem(const Index: Integer);
begin
if FUsesObjects then
begin
TKeyRec(FList.Items[Index]).Free;
FList.Delete(Index);
end;
Items.Delete(Index);
end;
end.
There's a way that avoid the need of rewrite the component to use another list to save the objects. The solution is to use the WM_DESTROY message along with the ComponentState property.
When the component is about to be destroyed, its state change to csDestroying, so the next time it receives a WM_DESTROY message it will be not part of the window recreation process.
We use this method in our component library sucessfully.
TMyCombo = class(TCombobox)
...
procedure WMDestroy(var message: TMessage); message WM_DESTROY;
...
procedure TMyCombo.WMDestroy(var message: TMessage);
var
i: integer;
begin
if (csDestroying in ComponentState) then
for i:=0 to Items.Count - 1 do
Items.Objects[i].Free;
inherited;
end;
By Long Press, I mean pressing a button / panel and hold for a period (say 2 seconds) without releasing or dragging around. It is common in mobile phone and touch device.
I had tried using Gesture, checked toPressAndHold in TabletOptions and Checked all in InteractiveGestureOptions but long pressing cause no OnGesture Call.
Another implementation I can think of is adding a timer, start it in MouseDown and end it in either Timer Fired, StartDrag, MouseUp or MouseLeave. However, as I want to add this behavior to several different buttons and panel component, I would have to override a brunch of procedure in each class and copy the code around for each component.
Is there a better way of achieving that?
Edit :
To NGLN
Woo, great piece of work! Together with your answer to those scrolling effects, VCL can almost achieve mobile OS look and feel!
Your code work perfectly with common controls but I got 2 issues in my case
Long Clicking on the form cannot be detected (of cause as the form
is not parent of itself) I shift the Find FChild Code to separate
procedure and call from both WMParentNotify and FormMouseDown to
solve it.
I got some custom button which has some disabled HTML
labels (Header, Caption, Footer) covering up the label original
surface, Using your code, FChild will be one of those label but it
do not get MouseCapture. I add the below line to overcome it :
while not TControlAccess(FChild).Enabled do
FChild := FChild.Parent;
Finally, for some more complicated controls like TCategoryButtons or TListBox, the user of the event might need to check not against the whole control but a specify item in the control. So I think we need to save the original CursorPos and fire another event when the timer triggered to let manual determination of whether it meet the long press condition or not. If yes or event not assigned, then use your default code for determination.
All in all, we can just create a LongPress supported form / panel to host all other controls. This is much more easier then implementing the LongPress feature Component by Component! Great Thanks!
Edit2 :
To NGLN
Thanks again for your component version, which is excellent approach, not needing to do any modification to existing components and can detect long press everywhere!
For your information, I had do several modification to suit my own need.
TCustomForm vs TWinControl : As most of my application has only 1 main form and all other visual units are my own created frame (not from TFrame but TScrollingWinControl with ccpack support), assuming TCustomForm do not work for me. So I had deleted property form (but retain FForm for ActiveControl) and create a published property Host : TWinControl to act as the parent host. In that way, I can also limit the detection to some limited panel. When Assigning Host, I check and find the FForm using GetParentForm(FHost).
Disabled Controls : As I said previously, I got some disabled TJvHTLabel covering my buttons and your component work on the labels. I can of cause find back the button by the label, but I think it would be more convenient if it had been handled by the new component. So I add a property SkipDisabled and if set to turn, loop in its parent line to find first enabled control.
I add a PreserveFocus property to let component user choose to keep last activecontrol or not.
Controls with items. I changed your TLongPressEvent, adding the ClickPos as the 2nd parameter. So, I can now use the ClickPos to find which item in a list box or the like had been long held.
It seems to me that FindVCLWindow is having same effect with your FindControlAtPos?
Thank you again for your great work.
At every left mouse button click, WM_PARENTNOTIFY is send to all (grand) parents of the clicked control. So this can be used for tracking the starting point of a long press, and the duration of a press can be timed with a timer. What is left is to decide when a press should be called a long press. And to wrap this all up in a nice component of course.
In the component written below, the OnLongPress event handler is fired when the following conditions are met:
after the interval, the control still has mouse capture, or still has focus, or is disabled,
after the interval, the mouse has not moved more then Mouse.DragThreshold.
Some explanation on the code:
It temporarily replaces the control's OnMouseUp event handler, otherwise consecutive clicks might also result in a long press. The intermediate event handler disables the tracking timer, calls the original event handler and replaces it back.
After the long press, the active control is reset, because I thought a long press is not done with the intention to focus the control. But that's just my guess, and it might be candidate for a property.
Also tracks for long presses on the form itself (rather then only its childs).
Has a customized FindControlAtPos routine which performs a deep search on an arbitrary window. Alternatives were (1) TWinControl.ControlAtPos, but it searches just one level deep, and (2) Controls.FindDragTarget, but despite the AllowDisabled parameter, it is not able of finding disabled controls.
unit LongPressEvent;
interface
uses
Classes, Controls, Messages, Windows, Forms, ExtCtrls;
type
TLongPressEvent = procedure(Control: TControl) of object;
TLongPressTracker = class(TComponent)
private
FChild: TControl;
FClickPos: TPoint;
FForm: TCustomForm;
FOldChildOnMouseUp: TMouseEvent;
FOldFormWndProc: TFarProc;
FOnLongPress: TLongPressEvent;
FPrevActiveControl: TWinControl;
FTimer: TTimer;
procedure AttachForm;
procedure DetachForm;
function GetDuration: Cardinal;
procedure LongPressed(Sender: TObject);
procedure NewChildMouseUp(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
procedure NewFormWndProc(var Message: TMessage);
procedure SetDuration(Value: Cardinal);
procedure SetForm(Value: TCustomForm);
procedure StartTracking;
protected
procedure Notification(AComponent: TComponent; Operation: TOperation);
override;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
property Form: TCustomForm read FForm write SetForm;
published
property Duration: Cardinal read GetDuration write SetDuration
default 1000;
property OnLongPress: TLongPressEvent read FOnLongPress
write FOnLongPress;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('Samples', [TLongPressTracker]);
end;
function FindControlAtPos(Window: TWinControl;
const ScreenPos: TPoint): TControl;
var
I: Integer;
C: TControl;
begin
for I := Window.ControlCount - 1 downto 0 do
begin
C := Window.Controls[I];
if C.Visible and PtInRect(C.ClientRect, C.ScreenToClient(ScreenPos)) then
begin
if C is TWinControl then
Result := FindControlAtPos(TWinControl(C), ScreenPos)
else
Result := C;
Exit;
end;
end;
Result := Window;
end;
{ TLongPressTracker }
type
TControlAccess = class(TControl);
procedure TLongPressTracker.AttachForm;
begin
if FForm <> nil then
begin
FForm.HandleNeeded;
FOldFormWndProc := Pointer(GetWindowLong(FForm.Handle, GWL_WNDPROC));
SetWindowLong(FForm.Handle, GWL_WNDPROC,
Integer(MakeObjectInstance(NewFormWndProc)));
end;
end;
constructor TLongPressTracker.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FTimer := TTimer.Create(Self);
FTimer.Enabled := False;
FTimer.Interval := 1000;
FTimer.OnTimer := LongPressed;
if AOwner is TCustomForm then
SetForm(TCustomForm(AOwner));
end;
destructor TLongPressTracker.Destroy;
begin
if FTimer.Enabled then
begin
FTimer.Enabled := False;
TControlAccess(FChild).OnMouseUp := FOldChildOnMouseUp;
end;
DetachForm;
inherited Destroy;
end;
procedure TLongPressTracker.DetachForm;
begin
if FForm <> nil then
begin
if FForm.HandleAllocated then
SetWindowLong(FForm.Handle, GWL_WNDPROC, Integer(FOldFormWndProc));
FForm := nil;
end;
end;
function TLongPressTracker.GetDuration: Cardinal;
begin
Result := FTimer.Interval;
end;
procedure TLongPressTracker.LongPressed(Sender: TObject);
begin
FTimer.Enabled := False;
if (Abs(FClickPos.X - Mouse.CursorPos.X) < Mouse.DragThreshold) and
(Abs(FClickPos.Y - Mouse.CursorPos.Y) < Mouse.DragThreshold) and
(((FChild is TWinControl) and TWinControl(FChild).Focused) or
(TControlAccess(FChild).MouseCapture or (not FChild.Enabled))) then
begin
FForm.ActiveControl := FPrevActiveControl;
if Assigned(FOnLongPress) then
FOnLongPress(FChild);
end;
TControlAccess(FChild).OnMouseUp := FOldChildOnMouseUp;
end;
procedure TLongPressTracker.NewChildMouseUp(Sender: TObject;
Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
FTimer.Enabled := False;
if Assigned(FOldChildOnMouseUp) then
FOldChildOnMouseUp(Sender, Button, Shift, X, Y);
TControlAccess(FChild).OnMouseUp := FOldChildOnMouseUp;
end;
procedure TLongPressTracker.NewFormWndProc(var Message: TMessage);
begin
case Message.Msg of
WM_PARENTNOTIFY:
if TWMParentNotify(Message).Event = WM_LBUTTONDOWN then
StartTracking;
WM_LBUTTONDOWN:
StartTracking;
end;
with Message do
Result := CallWindowProc(FOldFormWndProc, FForm.Handle, Msg, WParam,
LParam);
end;
procedure TLongPressTracker.Notification(AComponent: TComponent;
Operation: TOperation);
begin
inherited Notification(AComponent, Operation);
if (AComponent = FForm) and (Operation = opRemove) then
DetachForm;
if (AComponent = FChild) and (Operation = opRemove) then
begin
FTimer.Enabled := False;
FChild := nil;
end;
end;
procedure TLongPressTracker.SetDuration(Value: Cardinal);
begin
FTimer.Interval := Value;
end;
procedure TLongPressTracker.SetForm(Value: TCustomForm);
begin
if FForm <> Value then
begin
DetachForm;
FForm := Value;
FForm.FreeNotification(Self);
AttachForm;
end;
end;
procedure TLongPressTracker.StartTracking;
begin
FClickPos := Mouse.CursorPos;
FChild := FindControlAtPos(FForm, FClickPos);
FChild.FreeNotification(Self);
FPrevActiveControl := FForm.ActiveControl;
FOldChildOnMouseUp := TControlAccess(FChild).OnMouseUp;
TControlAccess(FChild).OnMouseUp := NewChildMouseUp;
FTimer.Enabled := True;
end;
end.
To get this component working, add it to a package, or use this runtime code:
...
private
procedure LongPress(Control: TControl);
end;
...
procedure TForm1.FormCreate(Sender: TObject);
begin
with TLongPressTracker.Create(Self) do
OnLongPress := LongPress;
end;
procedure TForm1.LongPress(Control: TControl);
begin
Caption := 'Long press occurred on: ' + Sender.ClassName;
end;