A custom control that I wrote is implicated in a crash when it is destroyed. It is hard to pin down the exact circumstances and it might be a factor that the control is parented by a 3rd party control.
Edit 8 October 2014
I've now got a much better SSCCE that illustrates the crash using only TMediaPlayer (from the Delphi VCL) on TForm. So I've deleted a lot of what I wrote before. Please see the edit history for that. (It turns out that CM_EXIT in the former call stack was a red-herring.)
Here's the SSCCE:
unit Unit1;
interface
uses
System.Classes, Vcl.Controls, Vcl.Forms, Vcl.Menus, Vcl.MPlayer;
type
TForm1 = class(TForm)
MainMenu: TMainMenu;
CrashMenuItem: TMenuItem;
procedure CrashMenuItemClick(Sender: TObject);
procedure FormShow(Sender: TObject);
private
fControl : TMediaPlayer;
end;
var
Form1: TForm1;
implementation
uses
Vcl.Dialogs;
{$R *.dfm}
procedure TForm1.CrashMenuItemClick(Sender: TObject);
begin
ShowMessage('Message');
fControl.Free;
end;
procedure TForm1.FormShow(Sender: TObject);
begin
fControl := TMediaPlayer.Create(Form1);
fControl.Parent := Form1;
end;
end.
The call to ShowMessage immediately before freeing the control is crucial.
After dismissing the dialog, the TMediaPlayer control gets a WM_SETFOCUS.
It's destructor is then called. Inherited TCustomControl.Destroy frees the canvas and then inherited TWinControl.Destroy calls TWinControl.RemoveFocus, so it gets a WM_KILLFOCUS.
TMediaPlayer.WMKillFocus calls Paint directly, which tries to use the freed canvas and crashes.
(Previously I had a custom control where CMFocusChanged called Invalidate. The effect was the same but the call stack was rather more involved.)
My original 3 questions, that NGLN has answered below:
Am I doing something wrong merely calling FreeAndNil(fMyControl)? Must I unparent it before destroying it? But this doesn't seem necessary with any other controls, so more likely that will just hide the underlying bug.
Should my control have something in its destructor to fix this so that TWinControl knows not to try to repaint it?
Is there perhaps a bug in the 3rd party parent control? Is it the case that my control should certainly not receive a WM_PRINTCLIENT message once it has started to be destroyed? (The 3rd party control seems to make an explicit call to its inherited TWinControl.Update when it receives CM_EXIT as a consequence of my control losing focus.)
But the real question remains: Is there anything wrong with the code in my SSCCE, or is there a bug in the Delphi VCL?
(Incidentally, the same problem will occur with any descendent of TCustomControl. I used TMediaPlayer for convenience.)
Am I doing something wrong merely calling FreeAndNil(fMyControl)?
No, every control should be able to be freed at any given time, as long as all references to the control are cleared (nilled) and the instance's code isn't run anymore.
Must I unparent it before destroying it? But this doesn't seem necessary with any other controls, so more likely that will just hide the underlying bug.
No, indeed no need to.
Should my control have something in its destructor to fix this so that TWinControl knows not to try to repaint it?
No, normally there is no need to. The VCL has this all build in already. For testing purposes or as a (temporary) workaround, you could try to override PaintWindow with something like if not (csDestroying in ComponentState) then.
Is there perhaps a bug in the 3rd party parent control? Is it the case that my control should certainly not receive a WM_PRINTCLIENT message once it has started to be destroyed? (The 3rd party control seems to make an explicit call to its inherited TWinControl.Update when it receives CM_EXIT as a consequence of my control losing focus.)
The parent control indeed receives CM_EXIT, because it had a focussed control, and now it has not anymore (ie. Form.ActiveControl = nil). So that's normal behaviour. As for why the parent sends a WM_PRINTCLIENT to the control (how do you know that request comes from the parent? It seems to start at the Update call.) I do not know. To rule out the possibility of a buggy parent, retry your case with a different parent.
Update (due to question edit):
TMediaPlayer.WMKillFocus calls Paint directly...
procedure TMediaPlayer.WMKillFocus(var Message: TWMKillFocus);
begin
Paint;
end;
That is taboo! That is definitely a bug in the VCL. Paint should never be called directly other than by a request for painting via a WM_PAINT message. I have submitted a report on QC.
(Previously I had a custom control where CMFocusChanged called Invalidate. The effect was the same but the call stack was rather more involved.)
...
(Incidentally, the same problem will occur with any descendent of TCustomControl. I used TMediaPlayer for convenience.)
That is not the case with a test here in D7 and XE2.
Related
In a runtime only package, I've defined a TFrame descendant which publishes the OnLoaded event:
type
TMyMethod = procedure() of object;
TMyFrame = class(TFrame)
protected
FOnLoaded : TMyMethod;
procedure Loaded(); override;
published
property OnLoaded : TMyMethod read FOnLoaded write FOnLoaded;
end;
implementation
{$R *.dfm}
procedure TMyFrame.Loaded();
begin
inherited;
if(Assigned(FOnLoaded))
then FOnLoaded();
end;
In a designtime only package, I've registered TMyFrame component as follows:
unit uMyRegistrations;
interface
uses
Classes, uMyFrame;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('MyTestComponents', [
TMyFrame
]);
end;
I've installed the designtime package, I can find TMyFrame in the tool palette and its OnLoaded event is shown in the object inspector.
I've dragged a TMyFrame into a form, then I've assigned the OnLoaded event by doubleclicking from the object inspector.
After assigning the event, I noticed that an access violation error message appears each time I try to open the form's file in Delphi (It let me open the ".pas" file, but I can't switch to visual designer view).
Did I correctly published the OnLoaded event? If so, what else is wrong?
Further Informations:
I'm using Delphi 2007 (don't know if it matters).
The error also appears by doing the same thing with different parent classes (Not only for TFrame descendants).
Updated (somewhat less bogus) answer
You accepted my original answer, but what I wrote was not correct. Rob Kennedy pointed to an article by former Embarcadero developer Allen Bauer on the topic of Assigned.
Allen explains that the Assigned function only tests one pointer of the two pointers in a method pointer. The IDE at design time takes advantage of this by assigning sentinel values to any published method properties (i.e. events). These sentinel values have nil for one of the two pointers in the method pointer (the one that Assigned checks), and an index identifying the property value in the other pointer.
All this means that False is returned when you call Assigned at design time. So long as you check published method pointers with Assigned before calling them, then you will never call them at design time.
So what I originally wrote cannot be true.
So I dug a bit deeper. I used the following very simple code, testing with XE7:
type
TMyControl = class(TGraphicControl)
protected
FSize: Integer;
procedure Loaded; override;
end;
....
procedure TMyControl.Loaded;
begin
inherited;
FSize := InstanceSize;
end;
....
procedure Register;
begin
RegisterComponents('MyTestComponents', [TMyControl]);
end;
This was enough to cause an AV in the IDE at design time whenever the Loaded method was executed.
My conclusion is that the IDE does some rather underhand things when streaming, and your objects are not in a fit state to use when the Loaded method is called. But I don't really have a better understanding than that.
Original (very bogus) answer
You must not execute event handlers at design time, and your code does just that. The reason being that at design time the event handler's code is not available.
The control's code is available, the IDE has loaded it – but the code that implements the event handler is not. That code is not part of the design time package, it is part of the project that is currently open in the IDE. After all, it might not even compile yet!
The Loaded method should defend against this like so:
procedure TMyFrame.Loaded();
begin
inherited;
if not (csDesigning in ComponentState) and Assigned(FOnLoaded) then
FOnLoaded();
end;
I'm having problems using TApplication.ModalPopupMode=pmAuto and I was wondering if my problems were caused by my usage of pmAuto or a bug in delphi.
Simple use case:
Form1(MainForm) and Form3 are permanent forms. (Created in the dpr)
Form2 is created when needed and
freed afterward.
Form3 contains a TComboBox with X items.
Sequence of actions :
Form1 create and show Form2 modal.
Form2 show form3 modal.
Close Form3
Close and free Form2
Show Form3 <---- The TComboBox now contains 0 items.
I use ComboBox as an example, but I guess any controls that saves information in the DestroyWnd procedure and restore it in the CreateWnd procedure isn't working right. I tested TListBox and it displays the same behavior too.
Is it a known fact that one shouldn't mix permanent and temporary form when ModalPopupMode is pmAuto?
If not, is there any known workaround for this problem?
If it's a bug, is this fixed in more recent version of Delphi? (I'm using XE4)
It is not really a bug, just a quirk in how the various windows interact with each other when dealing with modality.
When Form3 is first created, TComboBox.CreateWnd() is called during DFM streaming. When Form3.ShowModal() is called for the first time, Form3 calls RecreateWnd() on itself if its PopupMode is pmNone and Application.ModalPopupMode is not pmNone. OK, so TComboBox.DestroyWnd() gets called, saving the items, then TComboBox.CreateWnd() gets called, restoring the items. Recreating the TComboBox's window during ShowModal() is not ideal, but it works this time.
When Form3.ShowModal() is called the second time, TComboBox.CreateWnd() is called again without a previous call to TComboBox.DestroyWnd()! Since the items have not been saved, they cannot be restored. That is why the TComboBox is empty.
But why does this happen? When Form2 is freed, Form3's window is still associated with Form2's window. The first call to Form3.ShowModal set Form2's window as Form3's parent/owner window. When you close a TForm, it is merely hidden, its window still exists. So, when Form2 and Form3 are closed, they still exist and are linked together, and then when Form2 is destroyed, all of its child and owned windows get destroyed. TComboBox receives a WM_NCDESTROY message, resetting its Handle to 0 without notifying the rest of its code that the window is being destroyed. Thus, TComboBox does not have a chance to save its current items because DestroyWnd() is not called. DestroyWnd() is called only when the VCL itself is destroying the window, not when the OS destroys it.
Now, how can you fix this? You will have to destroy the TComboBox's window, triggering its DestroyWnd() method, before freeing Form2. The trick is that TComboBox.DestroyWnd() will save the items only if the csRecreating flag is enabled in the TComboBox.ControlState property. There are a few different ways you can accomplish that:
call TWinControl.UpdateRecreatingFlag() and TWinControl.DestroyHandle() directly. They are both protected, so you can use an accessor class to reach them:
type
TComboBoxAccess = class(TComboBox)
end;
Form2 := TForm2.Create(nil);
try
Form2.ShowModal;
finally
with TComboBoxAccess(Form3.ComboBox1) do
begin
UpdateRecreatingFlag(True);
DestroyHandle;
UpdateRecreatingFlag(False);
end;
Frm.Free;
end;
Form3.ShowModal;
call TWinControl.RecreateWnd() directly. It is also protected, so you can use an accessor class to reach it:
type
TComboBoxAccess = class(TComboBox)
end;
Form2 := TForm2.Create(nil);
try
Form2.ShowModal;
finally
TComboBoxAccess(Form3.ComboBox1).RecreateWnd;
Frm.Free;
end;
Form3.ShowModal;
The TComboBox window is not actually be created until the next time it is needed, in the subsequent ShowModal().
send the TComboBox window a CM_DESTROYHANDLE message and let TWinControl handle everything for you:
Form2 := TForm2.Create(nil);
try
Form2.ShowModal;
finally
if Form3.ComboBox1.HandleAllocated then
SendMessage(Form3.ComboBox1.Handle, CM_DESTROYHANDLE, 1, 0);
Frm.Free;
end;
Form3.ShowModal;
CM_DESTROYHANDLE is used internally by TWinControl.DestroyHandle() when destroying child windows. When a TWinControl component receives that message, it calls UpdateRecreatingFlag() and DestroyHandle() on itself.
Based on Remy's excellent answer I implemented something that fixes these issues in the whole application. You will need to descend all your modal forms from a custom TForm descendant - TMyModalForm in my example (which, IMO, is always a good practice anyway). All modal forms in my application descend from this. Please notice that I also set PopupMode to pmAuto in CreateParams() before calling the inherited method. This prevents the z-order problem when showing modal windows, but also causes the window handle problem described in your question. Also, I just broadcast the CM_DESTROYHANDLE if action is caHide. This skips an unnecessary notification for MDI child windows and modal windows which are destroyed on close.
BTW, for future reference, this issue still exists in Delphi 10.2.3 Tokyo.
type
TMyModalForm = class(TForm)
protected
procedure DoClose(var Action: TCloseAction); override;
procedure CreateParams(var Params: TCreateParams); override;
end;
procedure TMyModalForm.DoClose(var Action: TCloseAction);
var
Msg: TMessage;
begin
inherited DoClose(Action);
if Action = caHide then
begin
FillChar(Msg, SizeOf(Msg), 0);
Msg.Msg := CM_DESTROYHANDLE;
Msg.WParam := 1;
Broadcast(Msg);
end;
end;
procedure TMyModalForm.CreateParams(var Params: TCreateParams);
begin
PopupMode := pmAuto;
inherited;
end;
end;
I just recently begun using TFrames heavily (OK, yes, I've been living under a rock...). I thought frames supported Message hander method declaration--and I've seen many examples of that. So why does this simple test unit for a TFrame never see the message it posts to itself? (I created the test when I figured out that message handlers weren't being called in my larger application.)
unit JunkFrame;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, ExtCtrls;
const
DO_FORM_INITS = WM_USER + 99;
type
TFrame1 = class(TFrame)
Panel1: TPanel;
private
procedure DoFormInits(var Msg: TMessage); message DO_FORM_INITS;
public
constructor Create(AOwner: TComponent); override;
end;
implementation
{$R *.dfm}
constructor TFrame1.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
PostMessage(self.Handle, DO_FORM_INITS, 0, 0);
end;
procedure TFrame1.DoFormInits(var Msg: TMessage);
begin
ShowMessage('In DoFormInits!');
end;
end.
This frame only contains a TPanel, and the frame is used on a simple mainform which contains only the frame and a Close button.
What am I missing?
I see two possibilities:
Your program hasn't started processing messages yet. Posted messages are only processed when your program calls GetMessage or PeekMessage and then DispatchMessage. That occurs inside Application.Run, so if your program hasn't gotten there yet, then it won't process any posted messages.
Your frame's window handle has been destroyed and re-created. Accessing the Handle property forces the frame's window handle to be created, but if the frame's parent hasn't quite stabilized yet, then it might destroy its own window handle and re-create it. That forces all its children to do the same, so the handle you posted the message to doesn't exist by the time your program starts processing messages.
To fix the first problem, just wait. Your program will start processing messages eventually. To fix the second problem, override your frame's CreateWnd method and post the message there. That method gets called after the window handle has been created, so you avoid forcing the handle to be created prematurely. It's still possible for the handle to be destroyed and re-created, though, and CreateWnd will be called each time that happens, so you'll need to be careful since your initialization message might be posted more than once (but never to the same window handle multiple times). Whether that's correct depends on what kind of initialzation you need to do.
The only explanation for this that I can come up with is that your frame's handle is recreated after you post the message and before the message queue is pumped. Try posting in an OnShow.
I'm was trying to write a dll library in Delphi wih a function that creates an instance of a TFrame descendant and returns it. But when I imported this function in an application, every time I called it I would get an exception like "the 'xxx' control has no parent window". I'm not 100% sure, but the exception appeared in the constructor of that class when any of GUI controls was accessed.
Could you please tell me what the reason of that behaviour is? Should I just use TForm descendants instead or is there a better solution?
Thank you!
About the error
That error message is raised from the Controls.pas unit, from the TWinControl.CreateWnd method. Essentially that code is used to create the Window handle for your TWinControl descendant (TFrame, TButton, TEdit... if it can have keyboard focus it's an TWinControl descendant), and it's actually an very sensible error message: You can't have a Window without an WindowParent, and since we're talking about the VCL here, it makes a lot of sense to try and get the parent window handle from TWinControl.Parent; And that's not assigned.
That's not WHY the error message is popping up. You get to see that error message because some of the code you're using to set up the frame requires an Window handle for some operation. It could be anything, like setting the Caption of some component (that internally requires an window handle do to some calculation). I personally really hate it when that happens. When I create GUI's from code I try to delay the assignment of Parent as much as possible, in an attempt to delay the creation of the window, so I got bitten by this many times.
Specific to your DLL usage, possible fix
I'm going to put my psycho mind reader hat on. Since you need to return a FRAME from your DLL, and you can't return the actual Frame because that's an Delphi-specific object and you're not allowed to return Delphi-specific objects over DLL boundaries, my guess is you're returning an Window Handle, as all the nice API's do, using a function definition like this:
function GiveMeTheNiceFrame:HWND;
The trouble is, that routine requires the creation of the actual Window Handle, by a call to TWinControl.CreateWnd, and in turn that call requires an parent window handle to set up the call to Windows.CreateWindowEx, and the routine can't get an parent window handle, so it errors out.
Try replacing your function with something allong the lines of:
function GiveMeTheNiceFrame(OwnerWindow:HWND):HWND;
begin
Result := TMyNiceFrame.CreateParanted(OwnerWindow).Handle;
end;
... ie: use the CreateParented(AParentWindow:HWND) constructor, not the usual Create(AOwner:TComponent) and pass an owner HWND to your DLL.
There are a few important things to remember:
When using DLLs, both your DLL and your EXE each have an Application instance that are struggling for control. The Controls in your DLL will see the Application instance that belongs to the DLL; the Controls in your EXE will see the Application instance that belongs to the EXE. That struggle is not there when using packages, as then there will only be one Application instance.
Frames are Controls, but they are not Forms.
When using Controls in an application, they cannot visually exist without a parent Control (usually a Form or a container that has a parent hierarchy towards a Form).
Some Controls cannot expose their full functionality unless they exist visually and have a valid parent.
Try to reproduce your problem inside the EXE; if you cannot reproduce, it is probably the first thing in the above list.
--jeroen
Sounds like you simply need to assign the component (a form or part of a form, like a panel) that holds the frame to theframe.parent.
You cannot do GUI work before it is assigned. Frames are parts of forms for reuse, and normally need to assign some parent to them.
Move the GUI code to onshow or a procedure you call explicitely, so that the calling code can assign parent.
Or make the parent a parameter in the function.
I found this (CreateParams is called as part of CreateWnd):
procedure TCustomFrame.CreateParams(var Params: TCreateParams);
begin
inherited;
if Parent = nil then
Params.WndParent := Application.Handle;
end;
And Application.Handle = 0 so it always throws the error later in CreateWnd.
After reading this
Delphi: How to call inherited inherited ancestor on a virtual method?
I have solved it by overriding CreateParams in my frame to miss out the tCustomFrame version:
type
tCreateParamsMethod = procedure(var Params: TCreateParams) of object;
type
tMyScrollingWinControl = class(TScrollingWinControl);
procedure TDelphiFrame.CreateParams(var Params: TCreateParams);
var
Proc: tCreateParamsMethod;
begin
TMethod(Proc).Code := #TMyScrollingWinControl.CreateParams;
TMethod(Proc).Data := Self;
Proc(Params);
end;
Now it's just throwing errors when trying to set the focus on subcontrols, which I think I will fix by intercepting WM_FOCUS but we'll how it goes from here.
function CreateFrame(hwndParent: HWnd): HWnd; stdcall;
var
frame: tFrame;
begin
Result := 0;
try
frame := TDelphiFrame.CreateParented(hwndParent);
Result := frame.Handle;
except on e: Exception do
ShowMessage(e.Message);
end;
end;
You can avoid this message by assigning nil to the parent OnClose event, sometimes it works:
SomeControl.Parent := nil;//Before free your TControl
SomeControl.Free;
I think this is very cool solution. I think it is not tried before :)
I'm using a Dummy Parent (which is a Form).
function MyFrame_Create(hApplication, hwndParent:THandle; X, Y, W, H:Integer):Pointer; stdcall;
var Fr: TMyFrame;
F: TForm;
CurAppHandle: THandle;
begin
CurAppHandle:=Application.Handle;
Application.Handle:=hApplication;
//---
F:=TForm. Create(Application);//Create a dummy form
F.Position:=poDesigned;
F.Width:=0; F.Top:=0; F.Left:=-400; F.Top:=-400;//Hide Form
F.Visible:=True;
//---
Fr:=TMyFrame.Create(Application);
Fr.Parent:=F;//Set Frame's parent
//Fr.ParentWindow:=hwndParent;
Windows.SetParent(Fr.Handle, hwndParent);//Set Frame's parent window
if CurAppHandle>0 then Application.Handle:=CurAppHandle;
//---
Fr.Left:=X;
Fr.Top:=Y;
Fr.Width:=W;
Fr.Height:=H;
Result:=Fr;
end;//MyFrame_Create
procedure MyFrame_Destroy(_Fr:Pointer); stdcall;
var Fr: TMyFrame;
F: TObject;
begin
Fr:=_Fr;
F:=Fr.Parent;
Fr.Parent:=Nil;
if (F is TForm) then F.Free;
//SetParent(Fr.Handle, 0);
//Fr.ParentWindow:=0;
Fr.Free;
end;//MyFrame_Destroy
I have an application that on startup checks some conditions and launches an external program in the OnShow event of the Main Form. The problem is that if there is an error when launching the external program, I want the application to terminate immediately. But there is an issue with that, in that EurekaLog catches my exceptions and somehow disrupts the message loop there by negating all calls to Application.Teminate and any other normal shutdown methods.
So here is my question, would ExitProcess be the best route then to immediately terminating my application when this condition exists?
By the time OnShow has fired, you're too far into the program to decide that you don't really want the program to run. You should make that determination sooner. OnShow is not the place to decide that the form shouldn't be shown.
This is the sort of thing you should check before you even create the main form. Put your checks in the DPR file, and if you determine that the program shouldn't run, then simply call exit.
begin
Application.Initialize;
if not ApplicationShouldReallyStart then
exit;
Application.CreateForm(TMainAppForm, MainAppForm);
Application.Run;
end.
Fill in your own implementation of ApplicationShouldReallyStart. (And it really should be a separate function, not in-line in the DPR file. The IDE gets confused if the begin-end block in the DPR file gets too complex.)
Aside from that, do not call ExitProcess. Call Halt instead. Halt calls ExitProcess, but it also calls unit finalization sections and other Delphi-specific process-shutdown tasks.
Work WITH the system, not AGAINST it! You can't simply die in the middle of things. If you want to die do it within the rules--WM_CLOSE or maybe your own routine that says why it's dying and then sends a WM_CLOSE.
You better send a wmClose message to the window. Else you have a big chance to get into trouble because of other messages send to the form.
I wrote a small application to test a theory and here is what I would suggest.
Call the CLOSE method.
The following example unit closes the application with no errors in D2009.
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs;
type
TForm1 = class(TForm)
procedure FormShow(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.FormShow(Sender: TObject);
begin
close;
end;
end.
While I fully agree with Rob Kennedy here, I want to note, that you may use EurekaLog's routines to control error dialog behaviour.
For example:
uses
ExceptionLog, ECore;
...
begin
ForceApplicationTermination(tbTerminate);
// ... <- Bad code goes there
end;
That way, the application will be closed right after displaying error dialog.