Handling WM_DROPFILES message doesn't work in Lazarus - drag-and-drop

I want to create windows GUI application using Lazarus that able to drag file from explorer to the TEdit widget and show the file path.
I had read and tried some delphi tutorials, it said that you need to handle the WM_DROPFILES message, but I still can't get it works. So I'm thinking if I should try the simple way first by making application that able to drag file to TForm instead.
So I followed this example, but it doesn't work too.
Here is the full code:
unit Unit1;
{$mode objfpc}{$H+}
interface
uses
Windows, Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs,
ShellAPI;
type
{ TForm1 }
TForm1 = class(TForm)
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
{ private declarations }
public
{ public declarations }
protected
procedure WMDropFiles(var Msg: TMessage); message WM_DROPFILES;
end;
var
Form1: TForm1;
implementation
{$R *.lfm}
{ TForm1 }
procedure TForm1.FormCreate(Sender: TObject);
begin
DragAcceptFiles(self.Handle, True);
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
DragAcceptFiles(self.Handle, False);
end;
procedure TForm1.WMDropFiles(var Msg: TMessage);
begin
ShowMessage('hello'); // never gets called
end;
end.
The TForm1.FormCreate and TForm1.FormDestroy are working fine but the TForm1.WMDropFiles method never gets called.
Anyone know the solution? Could be the Lazarus/Free-Pascal windows library behavior differs from Delphi's ?
FYI, I'm using lazarus-1.6.0-fpc-3.0.0-win32 on Windows 7 64 bit.

DragAcceptFiles is not true (for Lazarus), since it is a platform-dependent code ))
There is the correct cross-platform code: OnDropFiles - Only works with dock icon, not with Application Form
He doesn't use "Windows", "Messages" and "ShellAPI".
1 Set property "AllowDropFiles" of MainForm to True;
2 Declaration of the procedure:
type
{ TMainForm }
TMainForm = class(TForm)
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
procedure OnApplicationDropFiles(Sender: TObject; const FileNames: array of String);
public
end;
3 The procedure:
procedure TMainForm.FormCreate(Sender: TObject);
begin
Application.AddOnDropFilesHandler(#OnApplicationDropFiles);
end;
procedure TMainForm.OnApplicationDropFiles(Sender: TObject; const FileNames: array of String);
begin
ShowMessage('Files dropped');
end;
procedure TMainForm.FormDestroy(Sender: TObject);
begin
Application.RemoveOnDropFilesHandler(#OnApplicationDropFiles);
end;

Below answer solely elaborates on Win32WidgetSet which the question is apparently about.
First, as an answer to the question asked, the reason why WM_DROPFILES handler of the form is not called is that, simply the message is not delivered to the respective window procedure of the control class that the message is sent. LCL code is selective in what messages are delivered. Some details are in next paragraph, safe to skip...
After the message is dispatched (DispatchMessage of the Win32 API in procedure TWin32WidgetSet.AppProcessMessages of "win32object.inc" in "Win32Int.pp", which is the main message loop), the window procedure of the control's window (function WindowProc of "win32callback.inc" in "Win32Int.pp") constructs a WindowProcHelper object and calls its DoWindowProc (still in "win32callback.inc") function. This function has a huge message case, and it is here decided if a message will make it through the DeliverMessage function (which is in "LCLMessageGlue.pp"). WM_DROPFILES is handled differently and then it is not "deliver"ed. Any message which makes it to DeliverMessage is delivered to the control class' window procedure which the control it is sent belongs to (TControl.WndProc in "control.inc" of "controls.pp", or any override, f.i. TWinControl.WndProc in "wincontrol.inc" of "controls.pp") if it is a control, or dispatched otherwise.
Second, to achieve the desired behavior of handling dropped files on the edit control, one obvious solution, mentioned in so many places - even in lazarus' documentation as linked in a comment to the question, is to subclass the window of the control. Your subclass will be delivered the message before LCL has a chance to handle it, hence you can act on the message.
But, once you trace the code in TWindowProcHelper.HandleDropFiles in "win32callback.inc", it becomes apparent how easy it would be to set up the special handling of WM_DROPFILES in LCL so that only the edit control handles dropped files. Normally this is for handling at the form level as already mentioned in a previous answer, but a form also actually acts on messages received on behalf of its children.
No need to go into specifics as it is just implementation detail and I don't know if it is intended but, set AllowDropFiles of the form to true, and then in the OnCreate handler of the form, unregister the form as a drop target (which is automatically registered) and register the edit.
procedure TForm1.FormCreate(Sender: TObject);
begin
AllowDropFiles:= True;
DragAcceptFiles(Handle, False);
DragAcceptFiles(Edit1.Handle, True);
end;
Only the edit will accept files, but you'd handle it still on the form's event handler.
procedure TForm1.FormDropFiles(Sender: TObject; const FileNames: array of String);
begin
if Length(FileNames) > 0 then
Edit1.Text := FileNames[0];
end;
It is also possible to use Application.OnDropFiles after the same setup, but I don't see any advantage over the previous method.

procedure TForm1.FormDropFiles(Sender: TObject; const FileNames: array of String);
var i : Integer;
aTxt: String;
begin
showmessage('oh it works, this is filename #1 ' + filenames[0])
end;

Related

Handling NOTICE events from PostgreSQL in a separate thread in Delphi with Devart's PgDAC components

I want to execute long queries in a separate thread in order to be able to abort them and also to give feedback to the users. All of this is working but I sometimes get Access Violations because, I think, the processing of the OnNotice events is not done the right way and I would like to know the proper way of doing this.
I am using Devart's PgDAC and OmniThreadLibrary on Delphi 2010.
The PostgreSQL code that I execute is a stored procedure that contains things like :
RAISE NOTICE 'ad: %',myad.name;
Here are the interesting parts of my code :
procedure TFDecomptes.FormCreate(Sender: TObject);
begin
ThreadConnection := TPgConnection.Create(Self);
ThreadConnection.Assign(DM.PgConnection1);
end;
This ThreadConnection is the TPgConnection that will be used to execute the query (within a separate thread).
procedure TFDecomptes.BInterruptClick(Sender: TObject);
begin
ThreadConnection.BreakExec;
end;
This is what the "Interrupt query" button does. I'm not sure this is very "thread safe" since it is used in the main thread but does something on the TPgConnection dedicated to the query-execution thread.
procedure TFDecomptes.OmniEventMonitor1TaskMessage(const task: IOmniTaskControl; const msg: TOmniMessage);
begin
case msg.MsgID of
1: begin
CalculationError:=msg.MsgData.AsString;
end;
end;
end;
This is where I show errors happening during the thread execution (like SQL errors or query cancellation).
procedure TFDecomptes.PgConnectionNotice(Sender: TObject; Errors: TPgErrors);
var s:String;
begin
s:=Errors[Errors.Count-1].ToString;
if copy(s,1,4)='ad: ' then begin
delete(s,1,4);
LAD.Caption:=s;
end;
end;
This is the OnNotice event processing. All it is doing is modify a Label's caption.
procedure InternalExecQuery(const task: IOmniTask);
Var q:TPgSQL;
begin
q:=Task.Param['pgsql'];
Try
q.Execute;
Except
On E:Exception do task.Comm.Send(1,e.Message);
End;
end;
procedure TFDecomptes.StartClick(Sender: TObject);
begin
ThreadConnection.OnNotice:=PgConnectionNotice;
Timer1.Enabled:=True;
CalculationTask := CreateTask(InternalExecQuery, 'CalculeDecomptes')
.MonitorWith(OmniEventMonitor1)
.SetParameter('pgsql', PgSQL)
.Run;
end;
And this is how the query is run.
So the PgConnectionNotice event (running in the main thread) is attached to the ThreadConnection (used in the query-execution thread) and this is what I suspect to be generating these random access violations.
I don't know how to handle this. Should I use some kind of lock when inside PgConnectionNotice (Synchronize ?).
This is what I tried :
procedure TFDecomptes.OmniEventMonitor1TaskMessage(const task: IOmniTaskControl; const msg: TOmniMessage);
begin
case msg.MsgID of
1: begin
CalculationError:=msg.MsgData.AsString;
end;
2: begin
lad.caption:='Here';
end;
end;
end;
procedure TFDecomptes.PgConnectionNotice(Sender: TObject; Errors: TPgErrors);
begin
// I am not using the passed string in this test
CalculationTask.Comm.Send(2,Errors[Errors.Count-1].ToString);
end;
The message sent in PgConnectionNotice (with MsgId=2) is never received by OmniEventMonitor1TaskMessage.
I have tried using CalculationTask.Invoke but didn't understand how to call it in order to pass a string parameter (I don't think Delphi 2010 allows for anonymous functions).
When I tried the simpler action of cancelling the query like this, it stopped cancelling the query :
procedure TFDecomptes.DoTheInterrupt;
begin
ThreadConnection.BreakExec;
end;
procedure TFDecomptes.BInterruptClick(Sender: TObject);
begin
CalculationTask.Invoke(DoTheInterrupt);
end;
So I guess I shouldn't do the calls via CalculationTask. Should I store the task created in InternalExecQuery in a global variable and use that ?
The main problem is that I was confusing IOmniTask and IOmniTaskControl. IOmniTask is the background's interface that should be used to send messages to the main thread while IOmniTaskControl is the main thread's interface, used to talk to the background tasks.
So using CalculationTask (which is a IOmniTaskControl) inside PgConnectionNotice is a double mistake : since PgConnectionNotice is fired from within the background thread, I was sending messages to the background thread, from the background thread, using a main thread's variable.
So I added a global variable named RunningTask :
Var RunningTask : IOmniTask;
Set it to nil in the form's OnCreate and modify the task's code like this :
procedure InternalExecQuery(const task: IOmniTask);
Var q:TPgSQL;
begin
RunningTask := task;
try
q:=Task.Param['pgsql'];
Try
q.Execute;
Except
On E:Exception do task.Comm.Send(1,e.Message);
End;
finally
RunningTask := Nil;
end;
end;
And the OnNotice event now looks like :
procedure TFDecomptes.PgConnectionNotice(Sender: TObject; Errors: TPgErrors);
begin
if RunningTask=Nil then
// do nothing, old, pending notices
else
RunningTask.Comm.Send(2,Errors[Errors.Count-1].ToString);
end;
I know it is not clean to define a global variable although I know there will be at most one background task. I probably should have stored the IOmniTask reference within ThreadConnection because this is what Sender is in PgConnectionNotice.

Delphi Change main form while application is running

I have this problem. When I hide my main form, then the taskbar icon of my application is also hidden. I saw a new question about this problem as well and the answers didn't really help. They suggested to minimize it, but I do not want to minimize the application.
Is it possible to change the main form while the application is already running?
for instance. I have two forms. when I want to hide the one form and show the other form, then the taskbar icon should stay at the taskbar and the main form should switch to the other form.
I am using Delphi XE6 and it is a VCL Forms application.
I have also seen a different old question about changing main form while runtime, but it is very old and still based on Delphi 6.
Is it possible to change the main form while the application is already running?
It is not possible to change the VCL main form whilst the program is running. This property is determined once and for all when the program starts.
One possible way to proceed for you is to arrange for the secondary form, the form that is not the main form, to have a button on the taskbar. Do that by making it unowned, or by using the WS_EX_APPWINDOW extended window style.
Update
Well, you can change Application.MainForm, but it requires you to destroy the current main form, and then create a new one.
It is not possible to change the Application.MainForm once it has been assigned. However, you do not need to, either. The simpliest solution to this issue would be to create a blank hidden TForm to act as the real Application.MainForm and let it manage the taskbar normally, and then you can show/hide any secondary TForm objects when needed, where your desired "MainForm" is a secondary form not the real MainForm.
As David Heffernan already said it is not possible to change the Main Form of an already running application. This is the limitation of the windows itself.
What you can do is cheat and never actually change the Main Form but only make it look like you did.
How do you achieve that?
Step 1: Add code to the second Form to make its own Taskbar button
procedure TWorkForm.CreateParams(var Params: TCreateParams);
begin
inherited;
Params.ExStyle := Params.ExStyle or WS_EX_APPWINDOW;
end;
Step 2: Dynamically create the second Form just before switching to it. Upon its creation previously added code will create a new Taskbar button for your second form.
Step 3: Now hide you actual Main Form. Hiding it will also hide the Taskbar button belonging to it. So you end up still having one Taskbar button shown and it is the one belonging to your second Form.
Step 4: In order to allow your second Form to terminate your application at its closure call Close method of your true Main Form from your second Forms OnClose or OnFormCloseQuery event.
If you wanna be able to switch back to your true Main Form call Show method of your Main Form instead of Close method.
This approach allows us of swapping forms pretty quickly so only most keen users will notice short animation of Taskbar button.
NOTE: If your second for is a complex one and because of that takes some time to create you might wanna create it hidden and then once its creation process is finished show it and do the swap. Otherwise you might end up with two Taskbar buttons being shown at same time which I believe you wanna avoid.
Here is a short example:
- LoginForm is a true Main Form that is created at the application startup
- WorkForm is the Form on which user will spend most of time after logging in and this one is created in login process
Login Form code:
unit ULoginForm;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;
type
TLoginForm = class(TForm)
BLogIn: TButton;
procedure BLogInClick(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
LoginForm: TLoginForm;
//Global variable to tell us if we are only logging out or closing our program
LoggingOut: Boolean;
implementation
uses Unit2;
{$R *.dfm}
procedure TLoginForm.BLogInClick(Sender: TObject);
begin
//Create second Form
Application.CreateForm(TWorkForm, WorkForm);
//Hide Main Form
Self.Hide;
//Don't forget to clear login fields
end;
end.
Work form code:
unit UWorkForm;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;
type
TWorkForm = class(TForm)
BLogOut: TButton;
//Used in overriding forms creating parameters so we can add its own Taskbar button
procedure CreateParams(var Params: TCreateParams); override;
procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean);
procedure BLogOutClick(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
WorkForm: TWorkForm;
implementation
uses Unit1;
{$R *.dfm}
procedure TWorkForm.BLogOutClick(Sender: TObject);
begin
//Set to true so we know we are in the process of simply logging out
LoggingOut := True;
//Call close method to begin closing the current Form
Close;
end;
procedure TWorkForm.CreateParams(var Params: TCreateParams);
begin
inherited;
Params.ExStyle := Params.ExStyle or WS_EX_APPWINDOW;
end;
procedure TWorkForm.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
begin
//Check to see if we are in the process of simply logging out
if not LoggingOut then
begin
//If we are not in the process of logging out close the Main Form
LoginForm.Close;
//and then allow closing of current form
CanClose := True;
end
else
begin
//But if we are in the process of simply logging out show the Main Form
LoginForm.Show;
//Reset the LoggingOut to false
LoggingOut := False;
//and then alow closing of current form
CanClose := True;
end;
end;
end.
I implemented this in the same way #DavidHeffernan had already suggested and as yet have not come across any issues, it might not be the best way but it worked for me and what i was trying to achieve, which was have "normal" feeling behaviour when the user minimised their MainWork form.
Code looked similar to this:
procedure TfrmLogin.btnLoginClick(Sender: TObject);
begin
frmMainWork := TfrmMain.Create(Application);
try
Pointer((#Application.MainForm)^) := frmMainWork;
frmLogin.Hide;
frmMainWork.ShowModal;
finally
Pointer((#Application.MainForm)^) := frmLogin;
frmLogin.Show;
FreeAndNil(frmMainWork);
end;
end;
hope this helps :-)
You can change main form.
Do a variable F: ^TForm, then set it to #Application.MainForm. After that you can set main form as via F^ := YourAnotherForm.
If you set Application.MainFormOnTaskbar to false in your startup routine (in the .dpr file), then the VCL will create a hidden form whose sole purpose is to provide the taskbar icon. This is an older approach and is generally not recommended, but as long as other windows are visible, it would let you hide your main form without the application disappearing from the taskbar.
You can also provide an Application.OnGetMainFormHandle event handler to change your Application.MainFormHandle at runtime (but not Application.MainForm). MainFormHandle affects stuff like the owner of modal popup dialogs.
More information on Application.MainFormOnTaskbar and the disadvantages of disabling it: This gets complicated in a hurry. The short version is in the VCL docs, which explain that several of Windows' newer features (like live taskbar thumbnails) that were introduced in Vista require MainFormOnTaskbar := True. There's a lot more background reading in this SO discussion.
I've had an additional issue working with Delphi XE2 MDI apps that are also COM servers. My main form Main calls a secondary form MenuForm which contains shared icons and popup menus for the whole app.
In my example, the app works perfectly when run standalone.
Main gets created, and then creates a MenuForm at the end of FormCreate.
All good.
But when called from a COM server, Main.FormCreate is called first, but somehow MenuForm gets assigned to Application.MainForm. There is a race condition in the underlying RTL. This causes havoc when trying to create the first SDI child, as Application.MainForm is not MDI.
I tried working around this by Main.FormCreate posting a message back to itself, to delay creation of MenuForm until after Main.FormCreate has returned.
But there is still a race condition here - MenuForm was still assigned to Application.MainForm.
I was eventually able to work around this using code to poll Application.MainForm every 10ms, with a maximum of 10 seconds. I also had to remove any reference in Main to MenuForm's icon list (in the .dfm) until after MenuForm was created explicitly - otherwise MenuForm would get created implicitly at the end of MainForm.create.
Hope this helps someone!
const
CM_INITIAL_EVENT = WM_APP + 400;
TmainForm = class(TForm)
...
procedure afterCreate(var Message: TMessage); message CM_INITIAL_EVENT;
...
end;
procedure TmainForm.FormCreate(Sender : TObject);
begin
...
...standard init code
...
postmessage( handle, CM_INITIAL_EVENT, 0, 0 );
End;
procedure TmainForm.AfterCreate(var Message: TMessage);
var
i: Integer;
begin
//must assign these AFTER menuform has been created
if menuForm = nil then
begin
//wait for mainform to get assigned
//wait up to 10*1000ms = 10 seconds
for i := 0 to 1000 do
begin
if Application.Mainform = self then break;
sleep(10);
end;
Application.CreateForm(TmenuForm, menuForm);
menuForm.parent := self;
end;
//NOW we can assign the icons
Linktothisfilterfile1.SubMenuImages := menuForm.treeIconList;
ActionManager.Images := menuForm.treeIconList;
allFilters.Images := menuForm.treeIconList;
MainMenu.Images := menuForm.treeIconList;
...
end;
In the current Delphi implementation, I am sure that there are no consequences using a pointer to change Application.MainForm.
If you look into TApplication class, you can see that FMainForm is used only to check if the application has at least one form, and iterates the main loop inside the TApplication.Run method while the FMainForm exists. If you do not want to use a pointer to hack this property, you can implement your own TApplication class, like TMyApplication, copy all the routines inside it and define MainForm property to read and write
Delete the *.dproj file. Delete the following line in the *.dpr file. You get rid of all your troubles :)
Application.MainFormOnTaskbar := True;
Sorry for my bad English.

Delphi2006 - How to create in DLL a message pump for a new window in a thread?

I have multithreaded application that loads my custom dll.
In this dll I need to create a window.
I'm doing it by creating new thread and inside it I'm trying to create this window, but I have got error that tells me: EInvalidOperation - Canvas does not allow drawing.
By searching in the net, I have discovered that I need custom message pump for that thread.
So, my question is, how properly do this?
What I do now is:
- external app is loading dll
- than this app in separte thread is calling Init function from dll
- Init function creates thread
- TMyThread is declared as:
type
TMyThread = class(TThread)
private
Form: TMyForm;
FParentHWnd: HWND;
FRunning: Boolean;
protected
procedure Execute; override;
public
constructor Create(parent_hwnd: HWND); reintroduce;
end;
constructor TMyThread.Create(parent_hwnd: HWND);
begin
inherited Create(False); // run after create
FreeOnTerminate:=True;
FParentHWnd:=parent_hwnd;
FRunning:=False;
end;
procedure TMyThread.Execute;
var
parent_hwnd: HWND;
Msg: TMsg;
XRunning: LongInt;
begin
if not Terminated then begin
try
try
parent_hwnd:=FParentHWnd;
Form:=TMyForm.Create(nil); // <-- here is error
Form.Show;
FRunning:=True;
while FRunning do begin
if PeekMessage(Msg, 0, 0, 0, PM_NOREMOVE) then begin
if Msg.Message <> WM_QUIT then
Application.ProcessMessages
else
break;
end;
Sleep(1);
XRunning:=GetProp(parent_hwnd, 'XFormRunning');
if XRunning = 0 then
FRunning:=False;
end;
except
HandleException; // madExcept
end;
finally
Terminate;
end;
end;
end;
The exception EInvalidOperation - Canvas does not allow drawing is fired before thread reaches my existing message pump code.
What do I do wrong or what is the right way to make it work?
Thanks for any help.
To create second GUI thread in a DLL, I must do things exactly as in standard application.
Can anyone confirm my thinking?
In the DLL begin...end. section I do:
begin
Application.CreateForm(THiddenForm, HiddenForm);
Application.Run;
end.
In the TMyThread.Execute I must do:
procedure TMyThread.Execute;
begin
if not Terminated then begin
try
try
Application.CreateForm(TMyForm, Form);
???? how to make a thread that has remained in this place until you close this window ???
except
HandleException; // madExcept
end;
finally
Terminate;
end;
end;
end;
Is this the right way? Could it be that simple?
The simplest way to run a message queue in a thread is as follows:
procedure PerformThreadLoop;
var
Msg: TMsg;
begin
while GetMessage(Msg, 0, 0, 0) and not Terminated do begin
Try
TranslateMessage(Msg);
DispatchMessage(Msg);
Except
Application.HandleException(Self);
End;
end;
end;
And in your thread procedure would look like this:
procedure TMyThread.Execute
begin
InitialiseWindows;
PerformThreadLoop;
end;
All that said, what you are attempting is not going to work. You appear to be trying to use VCL components away from the main thread. That is specifically not allowed. The VCL's threading model dictates that all VCL code is run on the main thread. Your attempts to create a VCL form away from the main thread are doomed to failure.
I would question your desire to create a new thread. A Delphi DLL can show VCL forms provided that it runs those forms out of the thread that loaded and called the DLL. You can call Show from that thread and show a modeless form. This means that you are relying on the host application's message queue to deliver messages to your windows. By and large this can be made to work. If your form is modal then you can simply call ShowModal and the form will be serviced by the standard Delphi modal message loop.
So my advice to you is to keep all your GUI in the host app's GUI thread. If your DLL is expected to show GUI, and is also expected to do that away from the host app's GUI thread then you are in trouble. But I think that's highly unlikely to be the case.
Earlier (a year ago) I stated this: "To create second GUI thread in a DLL, I must do things exactly as in standard application".
This is exactly what everybody who is searching for this solution should do.
Let me explain, step by step:
we must add our application object to our thread:
type
TMyThread = class(TThread)
private
ThreadApplication: TApplication;
now some modification to definition of procedure TMyThread.Execute;
procedure TMyThread.Execute;
begin
if not Terminated then begin
try
ThreadApplication:=TApplication.Create(nil);
try
ThreadApplication.Initialize;
ThreadApplication.CreateForm(TMyForm, Form);
ThreadApplication.Run;
finally
ThreadApplication.Free;
end;
finally
Terminate;
end;
end;
end;
so, this is it, now we have message pump in a second GUI thread in a DLL.
Recently I found confirmation to this solution in a Delphi-Jedi blog, wrote by Christian Wimmer:
http://blog.delphi-jedi.net/2008/05/27/winlogon-notification-package/
Thank You very much.

Delphi onshow main form / modal form

I have a project which has a main form and some other forms.
When the app loads it needs to carry out some tasks and show the results in a modal form on top of the main form.
The problem I have is that if I put the call to the function to do the tasks / creates and Show the modal form in the main forms onshow event the modal form appears but the main form does not until the modal form is closed, which is what I would expect to happen. To counter this I have added a timer to the main form and start it on the main forms onshow event the timer calls the function to do the tasks / create and show the modal form. So now the main form appears before the modal form.
However I cannot see this being the best solution and was wondering if anyone could offer a better one.
I am using Delphi 7
Colin
One commonly used option is to post yourself a message in the form's OnShow. Like this:
const
WM_SHOWMYOTHERFORM = WM_USER + 0;
type
TMyMainForm = class(TForm)
procedure FormShow(Sender: TObject);
protected
procedure WMShowMyOtherForm(var Message: TMessage); message WM_SHOWMYOTHERFORM;
end;
...
procedure TMyMainForm.FormShow(Sender: TObject);
begin
PostMessage(Handle, WM_SHOWMYOTHERFORM, 0, 0);
end;
procedure TMyMainForm.WMShowMyOtherForm(var Message: TMessage);
begin
inherited;
with TMyOtherForm.Create(nil) do begin
try
ShowModal;
finally
Free;
end;
end;
end;
Why dont you use the MainForm OnActivate event like so?
procedure TMyMainForm.FormActivate(Sender: TObject);
begin
//Only execute this event once ...
OnActivate := nil;
//and then using the code David Heffernan offered ...
with TMyOtherForm.Create(nil) do begin
try
ShowModal;
finally
Free;
end;
end;
Setting the event to nil will ensure that this code is only run once, at startup.
The OnShow event is fired immediately before the call to the Windows API function ShowWindow is made. It is this call to ShowWindow that actually results in the window appearing on the screen.
So you ideally need something to run immediately after the call to ShowWindow. It turns out that the VCL code that drives all this is inside a TCustomForm message handler for CM_SHOWINGCHANGED. That message handler fires the OnShow event and then calls ShowWindow. So an excellent solution is to show your modal form immediately after the handler for CM_SHOWINGCHANGED runs. Like this:
type
TMyMainForm = class(TForm)
private
FMyOtherFormHasBeenShown: Boolean;
protected
procedure CMShowingChanged(var Message: TMessage); message CM_SHOWINGCHANGED;
end;
.....
procedure TMyMainForm.CMShowingChanged(var Message: TMessage);
begin
inherited;
if Showing and not FMyOtherFormHasBeenShown then begin
FMyOtherFormHasBeenShown := True;
with TMyOtherForm.Create(nil) do begin
try
ShowModal;
finally
Free;
end;
end;
end;
end;

Make sure Form does not get focus when routine is run

I have 2 forms - The first form has a bunch of edits, comboboxes, etc, and the 2nd form has a Webbrowser.
I have a routine in the 2nd form, that loads HTML into the Webbrowser, and that routine is fired in the OnChange event of all my controls on the first form.
The problem is, that my focused control on my first form looses focus, when the 2nd form is loading the HTML into the webbrowser.
How can I make sure that the 2nd form does not get focus when the routine is fired? Or, more importantly - make sure that the focused control on my first form, does not loose focus?
What you're trying to do goes against the VCL, and probably goes against usual user expectations: When a new window is shown, unless it's a tool window, the usual (and expected) behavior is to move focus to it. The Win32 Api to show a window is ShowWindow and it'll activate the window, unless the SW_SHOWNOACTIVATE flag (or one of it's variants) is specified.
When you make a VCL form visible, a call to that function is also made. The call to ShowWindow is buried in procedure TCustomForm.CMShowingChanged(var Message: TMessage), a 135 lines procedure that hard-codes the SW_SHOWNORMAL flag (ie: an Activating flag) for the ShowWindow call that the VCL makes. Unfortunately that's a big piece of code and overriding it is not going to be easy. If this was my program I'd probably attempt changing the code in place: I'd add a DoNotActivate:Boolean flag to TCustomForm and change the single line of code in CMShowingChanged that calls ShowWindow for non-MDI forms to take that flag into account and simply call ShowWindow(Handle, SW_SHOWNOACTIVATE). If changing the VCL is not something you'd do light-hearted, you can use the following hacky solution:
The trick I'm suggesting is to create the new form (the one holding TWebBrowser) but do NOT set it's Visible property to True. Instead, make manual calls to ShowWindow(Handle, SW_SHOWNOACTIVATE) to show the form without activating it. Because this code would no longer code through the usual Delphi VCL, owned controls would not automatically be created and shown, so the ShowWindow(...) call needs to be made recursively, for all TWinControl descendants of the form:
procedure TForm15.Button1Click(Sender: TObject);
var F: TForm16;
procedure RecursiveShowNoActivate(W: TWinControl);
var i:Integer;
begin
ShowWindow(W.Handle, SW_SHOWNOACTIVATE);
for i:=0 to W.ControlCount-1 do
if W.Controls[i] is TWinControl then
RecursiveShowNoActivate(TWinControl(W.Controls[i]));
end;
begin
F := TForm16.Create(Application);
F.Top := Top + height; // So the new form doesn't overlap mine
RecursiveShowNoActivate(F);
F.WebBrowser1.Navigate('http://msdn.microsoft.com/en-us/library/ms123401');
end;
There's an other catch with this code: Make sure you navigate to a web page that doesn't have a form that's automatically focused. Navigating to microsoft's MSDN library might be unusual for a code sample, but that canonical example (www.google.com) sets focus to the search form.
One simple solution can be to disable the form containing the web browser control. A disabled window will not gain the focus.
When the OnDocumentComplete event of TWebControl is fired, the browser control is ready to gain focus. Disable the form here, and post yourself a message so that you can enable the form shortly:
const
UM_POSTENABLE = WM_USER + 12;
type
TForm2 = class(TForm)
WebBrowser1: TWebBrowser;
procedure WebBrowser1DocumentComplete(ASender: TObject;
const pDisp: IDispatch; var URL: OleVariant);
private
procedure UMPostEnable(var Msg: TMessage); message UM_POSTENABLE;
end;
var
Form2: TForm2;
implementation
uses Unit1;
{$R *.dfm}
procedure TForm2.WebBrowser1DocumentComplete(ASender: TObject;
const pDisp: IDispatch; var URL: OleVariant);
begin
if Screen.ActiveForm = Form1 then begin
Enabled := False;
PostMessage(Handle, UM_POSTENABLE, 0, 0);
end;
end;
procedure TForm2.UMPostEnable(var Msg: TMessage);
begin
Enabled := True;
end;
Note that, according to the documentation, OnDocumentComplete can be fired multiple times. But since each call will receive a matching user message, this wouldn't be a problem.
procedure TForm1.FormCreate(Sender: TObject);
begin
Screen.OnActiveFormChange := ActiveFormChanged;
end;
procedure TForm1.ActiveFormChanged(Sender: TObject);
begin
if not (csDestroying in ComponentState) then
if ActiveControl <> nil then
ActiveControl.SetFocus
end;
procedure TForm1.EditOrComboChange(Sender: TObject);
begin
Form2.WebBrowser.SetFocus;
end;
Edit
Maybe this is not the most elegant way. As an alternative I tried
Form2.WebBrowser.Enabled := False;
to prevent the focus exchange at all. This keeps the focus on the edit control and strangely enough does the disabled WebBrowser update to the new page, but more mysteriously, this hides the caret in the edit control on Form1.