I'm stuck on a problem in Delphi 7 about event propagation (due to my ignorance).
I am asked to dynamically attach an OnMouseUp event handler on some controls on a form (and I'm fine with that thing), but if the OnMouseUp is present, the OnClick event on that control must not be processed.
Background
If you are asking the reason behind this, well, I'm in charge to modify an old production monitoring application (sigh) that from now on must accommodate a conditional behaviour for some controls, in direct response to a former click on a special function button.
Some of those controls have an OnClick event handler already; the first solution the team came up with was to punctually intervene on each OnClick handler and manage out the contextual actions in relation to the special function button status.
I suggested to take advantage of the Object-Oriented design already in place for the application forms: they all inherit from the same custom ancestor object, so I planned to insert an initialization method there to dynamically attach OnMouseUp events to the controls that are declared to support it in subclasses.
The need
I'm not hereby asking a validation or questioning on the (possible lack of) design goodness about all this (by the way, after a lot of thinking and reasoning it seemed to be the path we can walk with less pain); my problem is that for such design to take place I'd have to let dynamically-attached OnMouseUp event handlers stop event propagation to the pre-existent OnClick events on those controls.
Is it possible with Delphi 7?
Please note, the following does not explicitly answer the question here. It's more a proposal to the concept re-design (redirect OnClick events instead of adding extra OnMouseUp). It's about how to redirect OnClick event handler (if assigned some) of all components (might be filtered, if needed) to another (common) OnClick event handler. It includes also a method for restoring them to the original state.
In the following example I'll try to show you how to replace and then optionally restore the OnClick event handlers (if the component has written some) by the specific one. This is done to all components having the OnClick event published, so you don't need to know in advance if the component class has OnClick event available or not (but it can very simply be modified to use only a specific class).
The code consists from the following:
OnSpecialClick - it is the event handler to what all OnClick events will be binded when you call the ReplaceOnClickEvents procedure, notice that it must be published to be visible for RTTI !!!
Button1Click - represents here the old event handler which should be replaced, it is binded to the Button1.OnClick event at design time
ReplaceOnClickEvents - method, which iterates through all components on the form and checks if the currently iterated one has the OnClick event handler assigned; if so, it stores it into a backup collection and replace this event handler by the OnSpecialClick
RestoreOnClickEvents - method, which restores the original OnClick event handlers; it iterates through the backup collection and assign the event methods to its stored component instances
CheckBox1Click - this check box click event is meant to be the switch between the common and a special mode (CheckBox1 checked state means to be the special mode), only this OnClick event is not replaced by the ReplaceOnClickEvents call (because you wouldn't be able to restore the mode back to normal)
And here it is:
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, TypInfo, StdCtrls, Contnrs;
type
TEventBackup = class
Component: TComponent;
OnClickMethod: TMethod;
end;
type
TForm1 = class(TForm)
Button1: TButton;
CheckBox1: TCheckBox;
procedure Button1Click(Sender: TObject);
procedure CheckBox1Click(Sender: TObject);
private
procedure ReplaceOnClickEvents;
procedure RestoreOnClickEvents;
published
procedure OnSpecialClick(Sender: TObject);
end;
var
Form1: TForm1;
EventBackupList: TObjectList;
implementation
{$R *.dfm}
procedure TForm1.OnSpecialClick(Sender: TObject);
begin
ShowMessage('Hi, I''m an OnSpecialClick event message!');
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
ShowMessage('Hi, I''m just that boring original OnClick event message!');
end;
procedure TForm1.ReplaceOnClickEvents;
var
I: Integer;
Component: TComponent;
EventMethod: TMethod;
EventBackup: TEventBackup;
begin
for I := 0 to ComponentCount - 1 do
begin
Component := Components[I];
if Component = CheckBox1 then
Continue;
if IsPublishedProp(Component, 'OnClick') then
begin
EventMethod := GetMethodProp(Component, 'OnClick');
if Assigned(EventMethod.Code) and Assigned(EventMethod.Data) then
begin
EventBackup := TEventBackup.Create;
EventBackup.Component := Component;
EventBackup.OnClickMethod := EventMethod;
EventBackupList.Add(EventBackup);
EventMethod.Code := MethodAddress('OnSpecialClick');
EventMethod.Data := Pointer(Self);
SetMethodProp(Component, 'OnClick', EventMethod);
end;
end;
end;
end;
procedure TForm1.RestoreOnClickEvents;
var
I: Integer;
EventBackup: TEventBackup;
begin
for I := 0 to EventBackupList.Count - 1 do
begin
EventBackup := TEventBackup(EventBackupList[I]);
SetMethodProp(EventBackup.Component, 'OnClick', EventBackup.OnClickMethod);
end;
EventBackupList.Clear;
end;
procedure TForm1.CheckBox1Click(Sender: TObject);
begin
if CheckBox1.Checked then
ReplaceOnClickEvents
else
RestoreOnClickEvents;
end;
initialization
EventBackupList := TObjectList.Create;
EventBackupList.OwnsObjects := True;
finalization
EventBackupList.Free;
end.
As both TLama and TOndrej have said, there are a few ways to accomplish what you're attempting:
To do if Assigned(Control.OnMouseUp) then Exit; on your OnClick event handler
To "unassign" the OnClick event when assigning OnMouseUp (and vice-versa)
Both approaches will accomplish what you've detailed, though "unassigning" the OnClick event will be best for performance (to an infintismally small extent) since you won't be performing the if statement repeatedly.
Related
I’ve added a mouse wheel handler to my application’s main window, and it seems to work, but not in the way I’m expecting from the MSDN on line documentation.
According to the MSDN help, the result should be set to zero to indicate that the message has been handled, but if I do this, then the routine is called twice. Setting to a non-zero value (in my case –1) results it is only being called once.
Here is some test code which illustrates the problem:
unit Mouse_Wheel_Testing;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
StdCtrls;
type
TForm1 = class(TForm)
Debug: TEdit;
procedure MouseWheelHandler(var Message: TMessage); override;
procedure FormCreate(Sender: TObject);
private
Call_Count: integer;
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
procedure TForm1.FormCreate(Sender: TObject);
begin
Call_Count := 0;
Debug.Text := IntToStr(Call_Count);
end;
procedure TForm1.MouseWheelHandler(var Message: TMessage);
begin
inc(Call_Count);
Debug.Text := IntToStr(Call_Count);
Message.Result := -1;
end;
end.
From the initial question and the following comments there are two parts to this question:
How is a message indicated as handled?
Why do the calls to the message handlers seem so incoherent, MouseWheelHandler is called twice, OnMouseWheel never called depending on Controls on your form?
For the first part, the VCL code assumes that a Message.Result <> 0 indicates a handled message. In your test you set the Message.Result to -1, hence it is handled. As Sertac Akyuz remarks, this might not be what the MSDN defines, but a design decision in the VCL.
For the second part there are two fundamentals. First, the WM_MOUSEWHEEL, a system-generated message. Second, the CM_MOUSEWHEEL, Delphi's own Control Message distributed internally.
MouseWheelHandler handles WM_MOUSEWHEEL
OnMouseWheel (and depending on result and wheel delta OnMouseWheelUp / OnMouseWheelDown) event(s) fired in response to CM_MOUSEWHEEL
Scrolling the mouse wheel let's Windows generate a WM_SCROLLWHEEL which is received in the WndProc of the focused windowed control. This might be the TForm in this case, most likely the TEdit, as only focusable control. This message will be dispatched to the handler MouseWheelHandler. TControl.MouseWheelHandler will try to find the ParentForm it is located in and call the TForm.MouseWheelHandler, first entry to your procedure. Here - normally - Delphi will Perform a CM_MOUSEWHEEL on the Form's focused control. Your overridden procedure however, does not call inherited and disables this. If the message is still unhandled it will be propagated upwards the Parent chain, the Form's MouseWheelHandler is called a second time, second entry to your procedure.
If CM_MOUSEWHEEL messages get generated, they get dispatched to TControl.CMMouseWheel. Here, depending on the result of DoMouseWheel function either the CM_MOUSEWHEEL result is set to 1 or there is a Parent control the same message is performed on the Parent.
TStringGrid e.g. will handle MouseWheel events. The CM_MOUSEWHEELwill not be passed to the parent, no scroll wheel events for your form.
I have a form containing a TFrame. The TFrame contains a ComboBox that is dynamically populated. Each ComboBox entry has an associated object. By the time the overridden destructor for the TFrame is called, the Items in the ComboBox have already been cleared without freeing their associated objects. This happens whether I drop the ComboBox on the form in designer view, or dynamically create it in code with either nil or the TFrame as its owner. I currently use the OnDestroy event of the containing TForm to call a clean-up procedure of the contained TFrame.
Is there a better way that would not need an explicit procedure call by the TFrame's container? Where ideally should the objects added dynamically to the ComboBox be freed?
You say that when the destructor for the TFrame is called, the Items of the ComboBox have already been cleared. That's not the case, ComboBox items are never cleared. When Items is destroyed by the ComboBox, they've got a count of only 0.
When you exit your application and the VCL destroys the form containing the frame and the ComboBox, the native ComboBox control is also destroyed by the OS since it is placed in a window being destroyed. When you later access the items to be able to free your objects in the frame destructor, the VCL have to recreate a native ComboBox control, having an item count of 0.
The solution I'd propose is easy. Don't leave freeing your frame to the framework, instead, destroy your frame in the OnDestroy event of your form. That would be before the underlying window of the form is destroyed, hence you'll be able to access your objects.
form unit
procedure TMyForm.FormDestroy(Sender: TObject);
begin
MyFrame.Free;
end;
frame unit
destructor TMyFrame.Destroy;
var
i: Integer;
begin
for i := 0 to ComboBox1.Items.Count - 1 do
ComboBox1.Items.Objects[i].Free;
inherited;
end;
You could utilize the TFrame's WM_DESTROY handler like this:
unit Unit2;
interface
uses
Windows, Messages, SysUtils, Classes, Controls, Forms, StdCtrls;
type
TFrame1 = class(TFrame)
ComboBox1: TComboBox;
private
procedure WMDestroy(var Msg: TWMDestroy); message WM_DESTROY;
procedure FreeComboBoxItems;
public
constructor Create(AOwner: TComponent); override;
end;
implementation
{$R *.dfm}
constructor TFrame1.Create(AOwner: TComponent);
begin
inherited;
// Add some object items to the ComboBox
ComboBox1.AddItem('a', TButton.Create(nil));
ComboBox1.AddItem('b', TMemoryStream.Create);
ComboBox1.AddItem('c', TList.Create);
end;
procedure TFrame1.WMDestroy(var Msg: TWMDestroy);
begin
// Make sure the TFrame is actually destroying - not recreated
if (csDestroying in ComponentState) then
FreeComboBoxItems;
inherited;
end;
procedure TFrame1.FreeComboBoxItems;
var
I: Integer;
begin
OutputDebugString('TFrame1.FreeComboBoxItems');
with Self.ComboBox1 do
for I := 0 to Items.Count - 1 do
begin
OutputDebugString(PChar(Items.Objects[I].ClassName + '.Free'));
Items.Objects[I].Free;
end;
end;
end.
Another option is to create a Base ancestor TAppBaseForm class and a TAppBaseFrame for the entire application, and derive all your Forms as TAppBaseForm and all Frames as TAppBaseFrame.
This way the TAppBaseForm could notify all it's child TAppBaseFrame that the owner Form is destroyed on TAppBaseForm.FormDestroy event handler. At that point the ComboBox items are still valid (as described by Sertac Akyuz's answer).
Your question isn't really usefull, because - generally speaking - it is discouraged to store data (or objects in your case) in a GUI control. See also David's comment on how to change your design.
What makes the question kind of interresting to answer though is the difference between the combo box being a child of a form directly and being a child of another child of the form (your frame in this case). Apparently, the combo box items are destroyed before the destructor of that frame is called. Obvious alternatives to explore are then: overriding Frame.BeforeDestruction, overriding Frame.DestroyWindowHandle, overriding Frame.DestroyWnd, or catching WM_DESTROY in an overridden Frame.WndProc, but none of them is called before the items are already gone.
The next thing to try is to repeat this for the combo box. It turns out that when WM_DESTROY arrives at the combo box that the items are still there. However, beware of catching that message ónly when the control really is being destroyed, because the VCL might recreate a combo box frequently. Implement it using an interposing class for TComboBox, as follows:
unit Unit2;
interface
uses
Windows, Messages, Classes, Controls, Forms, StdCtrls;
type
TComboBox = class(StdCtrls.TComboBox)
protected
procedure WndProc(var Message: TMessage); override;
end;
TFrame1 = class(TFrame)
ComboBox1: TComboBox;
end;
implementation
{$R *.dfm}
{ TComboBox }
procedure TComboBox.WndProc(var Message: TMessage);
var
I: Integer;
begin
if (Message.Msg = WM_DESTROY) and (csDestroying in ComponentState) then
for I := 0 to Items.Count - 1 do
Items.Objects[I].Free;
inherited WndProc(Message);
end;
end.
Now, to answer your question: "Is this a better way?"
Yes it is, because it offers assurance of the object's destruction at the frame's level. In other words: you don't have to remember to deal with this for every instance seperately.
And no it is not, because this solution requires that the objects in the combo box are allowed to be freed in whatever circumstance which restricts usage to an unnecessary extra boundary.
So, is this answer usefull? Well, if it prevents you from using your current approach, then it is.
Besides, I also found another alternative by setting the frame's Parent property to nil in the containing form OnDestroy handler:
procedure TForm2.FormDestroy(Sender: TObject);
begin
Frame1.Parent := nil;
end;
In this case, you can safely destroy the objects stored in the combo box within the frame's destructor. But this solution is even worse than your current one, because it is not descriptive. Then Frame1.FreeComboObjects is much better.
freeing Combobox.Items.Objects in destructor is too late.
so, according to previous answers it is better and safe to do that this way:
procedure TMyFrame.ClearCBObjects;
var
i: Integer;
begin
for i := 0 to ComboBox1.Items.Count - 1 do
ComboBox1.Items.Objects[i].Free;
end;
destructor TMyFrame.Destroy;
begin
//Free none Component objects
inherited;
end;
destructor TMyForm.Destroy;
begin
MyFrame.ClearCBObjects;
inherited;
end;
I would like to know where is the formshow in delphi 2010 as when I can only see a formcreate in my project.
The reason I am asking is because I need to add Randomize in the FormShow event handler, as shown below:
procedure TfrmWinnaSpree.FormShow(Sender: TObject);
begin
Randomize;
end;
You create the event handler the same way you create almost every event handler in Delphi, by double-clicking the method in the Events tab of the Object Inspector.
Click on the form itself (not any control on the form), then switch to the Object Inspector. Click on the Events tab, and then scroll down to the OnShow event. Double-click in the right half next to the event name, and the IDE will create a new, empty event handler and put the cursor in the right place to start writing code.
procedure TForm3.FormShow(Sender: TObject);
begin
|
end;
However, FormShow is the wrong place to call Randomize, because FormShow executes every time your form is shown, and that can happen more than once. Here's an example (it assumes two forms, Form1 and Form2, autocreated as usual in the .dpr file with the default variable names, which of course is a bad idea - this is to demonstrate a problem with your question's purpose):
procedure TForm2.FormShow(Sender: TObject);
begin
ShowMessage('In FormShow');
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
Form2.Visible := not Form2.Visible;
end;
Run the program and click TForm1.Button1 multiple times; you'll see the In FormShow message every other time you do so.
The proper places for a call to Randomize are:
in your main form's FormCreate
in an initialization section of your main form's unit
unit uMainForm;
interface
...
implementation
...
initialization
Randomize;
end.
in your project source (.dpr) file
program MyGreatApp;
uses
Math,
Vcl.Forms,
uMainForm in 'uMainForm.pas' {Form1};
{$R *.RES}
begin
Randomize;
Application.Initialize;
Application.MainFormOnTaskbar := True;
Application.Title := 'My Super App';
Application.CreateForm(TForm1, Form1);
Application.Run;
end.
Alternatively you can also override the protected method TForm.DoShow:
type
TForm = class(Forms.TForm)
protected
procedure DoShow; override;
end;
implementation
procedure TForm.DoShow;
begin.
// custom show code
inherited;
// custom show code
end;
The advantage over the event-based approach is that you can put your custom code before or after the inherited call.
As I typed this question I realize that it probably should.
Docking a form to a TPageControl calls FormShow when form.Create() is called and when form.ManualDock(pagecontrol,pagecontrol.alClient) is called.
Un-docking the form also calls show and I assume this is because the form is actually 'reset' when you dock/undock?
If this is as designed I'll just refactor the code I dont want to fire there to onCreate (unless that is bad design).
If should or not is more philosophical than technical question. The TForm.OnShow event is fired by performing control message CM_DOCKCLIENT which is used also by the ManualDock function. Internally this message calls the CM_SHOWINGCHANGED what fires the event itself.
In the following example I will use two forms, Form1 (with a page control and a button) and Form2 (empty and dockable). I presume that both are auto created.
The following code is a proof that the OnShow event is fired by the CM_DOCKCLIENT control message. Clicking on the button, the CM_DOCKCLIENT message is performed and Form2's OnShow event is fired.
Code for Form1
procedure TForm1.FormShow(Sender: TObject);
begin
Form2.Show;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
DockObject: TDragDockObject;
begin
DockObject := TDragDockObject.Create(Form2);
try
// sending the CM_DOCKCLIENT message internally performs also the
// CM_SHOWINGCHANGED message which triggers the TForm.OnShow event
PageControl1.Perform(CM_DOCKCLIENT, WPARAM(DockObject), LPARAM(SmallPoint(0, 0)));
finally
DockObject.Free;
end;
end;
And Form2 has only the OnShow event handler
procedure TForm2.FormShow(Sender: TObject);
begin
ShowMessage('FormShow');
end;
An easy workaround is not to dock the Form2 manually by its own (in the OnShow event) but dock it by the creator or let's say by the form which displays it. In my previous example I've displayed the Form2 in the Form1.OnShow event, so I can easily dock it manually there.
procedure TForm1.FormShow(Sender: TObject);
begin
Form2.Show;
Form2.ManualDock(PageControl1);
end;
procedure TForm2.FormShow(Sender: TObject);
begin
// no manual docking of this form by itself
end;
I have added hints to components on my form. When the components receive the focus, I'd like to set the caption of a label component to display the hint.
I have added a TApplicationEvents object and set the OnShowHint event to
procedure TImportFrm.ApplicationEvents1ShowHint(var HintStr: string;
var CanShow: Boolean; var HintInfo: THintInfo);
begin
HelpLbl.Caption := HintStr;
end;
However it seems that the ShowHint event only fires with mouse movements. Is there a way to fire the hint code when components receive focus, without having to implement the OnEnter event for every single component on the form?
Add a handler for TScreen.OnActiveControlChange in your main form's creation, and handle the hints in that event:
type
TForm2=class(TForm)
...
private
procedure ScreenFocusControlChange(Sender: TObject);
end;
implementation
procedure TForm2.FormCreate(Sender: TObject);
begin
Screen.OnActiveControlChange := ScreenFocusControlChange;
end;
procedure TForm2.ScreenFocusControlChange(Sender: TObject);
begin
Label1.Caption := ActiveControl.Hint;
Label1.Update;
end;
Note that Sender won't do you much good; it's always Screen. You can filter (for instance, to only change the Label.Caption for edit controls) by testing the ActiveControl:
if (ActiveControl is TEdit) then
// Update caption of label with ActiveControl.Hint
Note that if you'll need to reassign the event when you show child forms (to an event on that child form), or you'll always be updating the original form's label with the hints. The easiest way to do the reassignment is to give every form an OnActiveControlChange handler, and assign it in the form's OnActivate event and unassign it in the OnDeactivate event:
procedure TForm1.FormActivate(Sender: TObject);
begin
Screen.OnActiveControlChange := Self.ScreenActiveControlChange;
end;
procedure TForm1.FormDeactivate(Sender: TObject);
begin
Screen.OnActiveControlChange := nil;
end;
This will allow you to update controls other than Label1 on each form, and only use the hint changes on forms you want to do so.
A simple solution is to use OnIdle event:
procedure TForm1.ApplicationEvents1Idle(Sender: TObject; var Done: Boolean);
begin
if Assigned(ActiveControl) then
Label1.Caption:= ActiveControl.Hint;
end;
A more advanced solution is to override protected ActiveChanged method of TForm:
type
TForm1 = class(TForm)
...
protected
procedure ActiveChanged; override;
end;
...
procedure TForm1.ActiveChanged;
begin
inherited;
if Assigned(ActiveControl) then
Label1.Caption:= ActiveControl.Hint;
end;
Receiving focus and OnShowHint are quite different events; OnShowHint can be triggered for non-focused control as well.
Why would you need to implement the OnEnter event for every single component? You can create one generic method / event handler like:
procedure TForm1.AnyControlEnter(Sender: TObject);
begin
lbl1.Caption := TControl(Sender).Hint;
end;
and assign it to every component you placed on the form.
You said:
it seems that the ShowHint event only fires with mouse movements
This is a normal behaviour. The problem you have ( it's a guess) is that hints are not fired directly. Don't try to make a workaround, what you try to do with MouseEnter is exactly what is already happening...the only difference is that you'be forget something...
Keep the event ApplicationEvents1ShowHint() as you've initially done but add this in the form constructor event:
Application.HintPause := 1;
And then hints will be displayed (almost) without delay.