I have an Excel AddIn written in Delphi that has a VCL form with a TMemo on it.
When I try to enter text into the Memo the input goes to Excel instead.
When I start the form modal (ShowModal), all works fine but obviously it's not possible to work with the main excel window and the addin's window concurrently.
The issue seems to be the exact similar to this question: Modeless form cannot receive keyboard input in Excel Add-in developed by Delphi
This answer suggests to handle WM_PARENTNOTIFY so I tried the following:
TMyForm = class(TForm)
...
procedure OnParentNotify(var Msg: TMessage); message WM_PARENTNOTIFY;
And in that procedure tried things like SetFocus, WinApi.Windows.SetFocus(self.Handle), SetForeGroundWindows, SetActiveWindow but that doesn't appear to work.
Other suggestions I've read is to run the UI in a different thread (which is of course not possible with VCL) and to install a keyboard hook with SetWindowsHookEx. Obviously that will give us keypress events but not sure what to do with those.
I am not using 3rd party tooling such as Add-In Express but just implementing IDTExtensibility2.
EDIT: more research suggests that Office uses an interface called IMsoComponent and and IMsoComponentManager as a way of tracking the active component in the application. Visual Studio uses these as IOleComponent and IOleComponentManager.
This link and this one suggest to register a new empty IOleComponent/IMsoComponent.
EDIT: MCVE can be fetched here, it's the smallest possible Excel AddIn code that will launch a VCL Form with a TEdit on it. The edit looses keyboard focus as soon as a worksheet is active.
I was having the same kind of problem. I am also implementing IDTExtensibility2 but as I am doing it on C++ I already managed to run the UI on a different thread. But anyway I was not fully happy with this solution. I would still have this problem if I wanted to use a VBA Userform as a TaskPane Window. I did try but as (I guess, didn´t check) the VBA userform will run on the native Excel Thread, just calling it on a different thread (to use as a TaskPane window) just marshalled it, didn´t mean that it was created on a different thread, so as I did try, there was this kind of problem.
I too did read and try to to handle WM_PARENTNOTIFY messages with SetFocus.. on my window but didn´t work.
This both interfaces IOleComponent and IOleComponentManager were new to me. Didn´t find the header files, but could write and implement from the descriptions at the link you shared.
How it worked for me was to register my IOleComponent implementation on every WM_SETCURSOR e WM_IME_SETCONTEXT at my Form Window. (I am not sure if this is exactly the best messages, but did work for me) and Revoke the component on every click back at EXCEL7 window.
The MSOCRINFO options I used to register was msocrfPreTranslateKey and msocadvfModal.
Hope that with this answer I will not receive tons of criticism. I know that it is a very specific issue, the question was with a -1 status when I read it, but was exactly what I needed to finish with this point. So I am just trying to be honest and share back something.
I finally found the solution to this after I decided to have another look at this...
Seems I was on the right track about needing IMsoComponentManager and IMsoComponent.
So first we need to retrieve the ComponentManager:
function GetMsoComponentManager(out ComponentManager: IMsoComponentManager): HRESULT;
var
MessageFilter: IMessageFilter;
ServiceProvider: IServiceProvider;
begin
MessageFilter := nil;
// Get the previous message filter by temporarily registering a new NULL message filter.
Result := CoRegisterMessageFilter(nil, MessageFilter);
if Succeeded(Result) then
begin
CoRegisterMessageFilter(MessageFilter, nil);
if (MessageFilter <> nil) then
begin
try
ServiceProvider := MessageFilter as IServiceProvider;
Result := ServiceProvider.QueryService(IID_IMsoComponentManager,
SID_SMsoComponentManager, ComponentManager);
if Assigned(ComponentManager) then
begin
end;
except
on E: Exception do
begin
Result := E_POINTER;
end;
end;
end;
end;
end;
Then we need to register a dummy component using msocrfPreTranslateAll (or msocrfPreTranslateKey)
procedure TVCLForm.RegisterComponent;
var
RegInfo: MSOCRINFO;
//MsoComponentManager: IMsoComponentManager;
hr: HRESULT;
bRes: Boolean;
begin
if FComponentId = 0 then
begin
FDummyMsoComponent := TDummyMsoComponent.Create;
ZeroMemory(#RegInfo, SizeOf(RegInfo));
RegInfo.cbSize := SizeOf(RegInfo);
RegInfo.grfcrf := msocrfPreTranslateAll or msocrfNeedIdleTime;
RegInfo.grfcadvf := DWORD(msocadvfModal);
bRes := ComponentManager.FRegisterComponent(FDummyMsoComponent, RegInfo,
FComponentId);
Memo1.Lines.Add(Format('FMsoComponentManager.FRegisterComponent: %s (Component ID: %d)', [BoolToStr(bRes, True), FComponentId]));
end
else begin
Memo1.Lines.Add(Format('Component with ID %d was already registered', [FComponentId]));
end;
if FComponentId > 0 then
begin
bRes := ComponentManager.FOnComponentActivate(FComponentId);
Memo1.Lines.Add(Format('FMsoComponentManager.FOnComponentActivate: %s (Component ID: %d)', [BoolToStr(bRes, True), FComponentId]));
end;
end;
Now in the Dummy Component implementation class we must handle FPreTranslateMessage:
function TDummyMsoComponent.FPreTranslateMessage(MSG: pMsg): BOOL;
var
hWndRoot: THandle;
begin
// this is the magic required to make sure non office owned windows (forms)
// receive Window messages. If we return True they will not, however if we
// return False, they will -> so we check if the message was meant for the
// window owner
hWndRoot := GetAncestor(MSG^.hwnd, GA_ROOT);
Result := (hWndRoot <> 0) and (IsDialogMessage(hWndRoot, MSG^));
end;
Finally a good place to to (un)register the Dummy component is when receiving WM_ACTIVATE. For example:
procedure TVCLForm.OnActivate(var Msg: TMessage);
var
bRes: Boolean;
begin
case Msg.WParam of
WA_ACTIVE:
begin
Memo1.Lines.Add('WA_ACTIVE');
RegisterComponent;
end;
WA_CLICKACTIVE:
begin
Memo1.Lines.Add('WA_CLICKACTIVE');
RegisterComponent;
end;
WA_INACTIVE:
begin
Memo1.Lines.Add('WA_INACTIVE');
UnRegisterComponent;
end
else
Memo1.Lines.Add('OTHER/UNKNOWN');
end;
end;
This all seems to work well and does not require intercepting WM_SETCURSOR or WM_IME_SETCONTEXT nor does it need subclassing of the Excel Window.
Once cleaned up will probably write a blog and place all the complete code on Github.
Related
I've noticed, that ShouldSkipPage is called twice per each page - before page is actually shown, and after. You can easily check this, by simply adding Log to function:
function ShouldSkipPage(PageID: Integer): Boolean;
begin
if PageID = wpSelectDir then
Log('ShouldSkipPage for SelectDir was called');
Result := false;
end;
You will see logged message twice (and by executing script in compiler, you can see, that second call occurs after page was shown).
So, can someone explain, why it is called second time, already after page was shown? This makes no sense, and may be confusing and even lead to unexpected deviations in installer logic.
Also, is there any way to prevent second call?
The first call (or actually the first set of calls) is to find the next page to display.
The second call (or actually the second set of calls) is to find, if there's any page to return to (to decide if the Back button should be visible).
This way you can e.g. prevent a user to return back once a certain page is reached.
In general the ShouldSkipPage event function could be called any number of times and at any time. And your code must be able to handle that.
If you want to do special processing before and after a page is changed, use the NextButtonClick/BackButtonClick and the CurPageChanged, not the ShouldSkipPage.
The following example shows, how to prevent a user from modifying an installation, once the "Ready to Install" page is reached:
function ShouldSkipPage(PageID: Integer): Boolean;
begin
Result := False;
if (WizardForm.CurPageID >= wpReady) and (PageID < wpReady) then
begin
Result := True;
end;
end;
There won't be any Back button on the "Ready to Install" page:
I have a component list of more than 80 options,where user can select and then install.
The setup remembers the previous install components and automatically checks the Components, Now if user deselects all, the "Component Exist" Warning Message Box is shown.
Because user deselected all 80 options the list becomes to long and the Message box goes out of screen space and user is now stuck.
I know there is NoUninstallWarning in Messages which has the text for the warning message and takes one argument as %1
Is there a way I can change the argument value , rather than having all options listed in indiviual line , I would like to have them as comma separated?
Or if I can have a Scrollbar in the Message box?
Please help
No, this message is internal and you can't customise it like that without modifying Inno's own source code.
In that situation the user shouldn't be completely stuck -- they should be able to press ESC to return to the component selection window and then select everything again.
A simple way to avoid this problem is to not allow the user to deselect components, once installed. You can do this with a bit of code like this:
var
InstalledComponentsDisabled: Boolean;
procedure CurPageChanged(CurPageId: Integer);
var
i: Integer;
begin
if (CurPageId = wpSelectComponents) and
(WizardForm.PrevAppDir <> '') and
not InstalledComponentsDisabled then begin
InstalledComponentsDisabled := True;
for i := 0 to WizardForm.ComponentsList.Items.Count - 1 do begin
if WizardForm.ComponentsList.Checked[i] then begin
WizardForm.ComponentsList.ItemEnabled[i] := False;
end;
end;
end;
end;
This has a similar effect to making anything already installed on upgrades fixed.
An alternate option is to put disablenouninstallwarning on all of your components and then either implement the messagebox completely yourself, add a bit of static text warning about removing components permanently on the page, or even do something to actually support removing components (eg. [InstallDelete] entries or UninsHs).
Given this scenario:
procedure TForm2.Button1Click(Sender: TObject);
var
Form: TForm;
begin
Form := TForm.Create(nil);
Form.OnClose := FormClosed;
Form.Show;
Sleep(200);
TThread.CreateAnonymousThread(
procedure
begin
TThread.Synchronize( nil,
procedure
begin
Form.Close;
MessageDlg('Testing', mtInformation, [mbok], 0);
end);
end).Start;
end;
procedure TForm2.FormClosed(Sender: TObject; var Action: TCloseAction);
begin
Action := TCloseAction.caFree;
end;
My MessageDlg call is not displayed (the result of this call is always mrCancel (2)).
After digging around, it's related to the OnClose event and setting the Action to caFree.
Changing Form.Close to Form.Free and removing the OnClose event entirely displays MessageDlg ok. Placing MessageDlg before the call to Form.Close works ok. Originally I thought scope of my Form variable might have caused the problem but declaring Formas a private field in TForm2 instance doesn't solve the problem.
My aim was to display a splash form, execute my thread, then through call backs of said thread, close the Splash form and display dialog where appropriate to the user.
For clarity, why does this occur?
What is happening is that the dialog's owning window is the form that is being closed. When the dialog starts its modal message loop, the form is released and takes down its owned windows with it. Including the dialog.
Test this out, to give you more confidence that what I state above is correct, by replacing the call to show the dialog first with
MessageBox(0, ...);
and then with
MessageBox(Form.Handle, ...);
That is, be explicit about the dialog's owner.
The first version, with no owner, will show the dialog. The second won't because it replicates the scenario in your code.
The Windows runtime requires that the messages for a visual window be processed by a message loop running in the same thread that creates that window.
The Windows API's also enforce rules about what operations can be performed on a window from a thread other than that in which the window was created. i.e. very little indeed, other than sending or posting messages to it.
With that information in mind, we can explain what is occurring in your case.
The Form.Close method ultimately closes a form by posting it a message (CM_RELEASE). But in your implementation the message loop responsible for responding to that message - the application main message loop - is blocked as a result of the fact that the message is posted from within a Synchronize() method.
i.e. your Synchronize()d method posts the message to close the form, but that message cannot and will not be processed by that forms window until your Synchronize()d method has completed, and that will not occur until the user has responded to the message box you are presenting in that method.
I hope that helps you understand what is going on in your code.
I have a thread-safe class that I use to get the correct TADOConnection for any given thread. This works great in my executables. It boils down to the following:
function ConnectionForCurrentThread () : TADOConnection;
var
thread : TThread;
begin
thread := TThread.CurrentThread;
result := adoConnectionFactory.ConnectionForID(thread.Handle);
end;
Where the adoConnectionFactory ends up handling a critical section to either return an existing ADO connection that's stored in a list, or making a new one if one isn't already created for that thread handle. Later, I test them to free them with a
if Windows.GetExitCodeThread(threadID, res) then begin
if res <> Windows.STILL_ACTIVE then begin
TADOConnection(connections.Objects[i]).Free;
connections.Delete(i);
end;
end else begin
TADOConnection(connections.Objects[i]).Free;
connections.Delete(i);
end;
This all seems to be working well. Where it's falling over is when used in a SOAP webservice ISAPI dll, hosted by Apache. The Thread.Handle ends up being the same for two simultaneous calls to the web service, so they attempt to share the same ADO Connection, which will occasionally throw exceptions due to the different threads fiddling with the same connection.
My question is, what can I use instead of the CurrentThread.handle? I'd really like this to be self-contained in this function call, so that if I recognize it's in web service dll mode, I would instead do something like:
function ConnectionForCurrentThread () : TADOConnection;
var
thread : TThread;
ct : TContextThing;
id : integer;
begin
if WebServiceMode then begin
ct := TContextThing.CurrentContext;
id := ct.ContextID;
end else begin
thread := TThread.CurrentThread;
id := thread.Handle;
end;
result := adoConnectionFactory.ConnectionForID(id);
end;
But I'm not too sure what that ContextThing might be. Any suggestions? And then on the flip side, how to see that a given context ID is no longer active so that its ADOConnection can be closed.
This is using Delphi XE and the SOAP server application (So, WebBroker, TWebModule, all that jazz).
Thank you.
For an ISAPI application, the following should work (assuming Apache fills in the ISAPI ECB correctly with a unique connection ID).
uses
Web.WebCntxt,
Web.Win.IsapiHttp;
if WebServiceMode then
id := TIsapiRequest(WebContext.Request).ECB^.ConnID
else
...
I have a problem with threads in Delphi. When using TIdHashMessageDigest5 in order to get MD5 from a big file, I have noticed that it takes too much time, and ends up in an application freeze.
I'm thinking about using a separate thread. So I have made a little form where I insert a simple message, a button and a progress bar in style pbstMarquee. I start a thread on the show event of this form.
My problem is: I want to close this form when HashStreamAsHex has finished reading successfully, but how can I do this? I tried calling the Close method on synchronize, but then the form is closed without waiting for that thread to finish. I also tried to use the waitfor method, without success.
Someone can help me with this, giving me some example, or link or similar?
Thanks very much and sorry for my bad english.
About form:
-----------
procedure TFormProgress.FormProgressOnShow(Sender: TObject);
begin
ProgressThread := TProgressThread.Create(True);
ProgressThread.Form := FormProgress;
ProgressThread.FileSrc := uFileSrc;
ProgressThread.Start;
end;
About thread:
-------------
procedure TProgressThread.Execute;
begin
FreeOnTerminate := True;
uFileMD5 := GetFileMd5 (uFileSrc) // function is definited in other unit.
Self.WaitFor;
Synchronize(DoSync);
end;
procedure TProgressThread.DoSync;
begin
oForm.Close;
end;
GetFileMd5 è so defined:
function GetFileMD5(const Src: TFileName): UnicodeString;
var
Md5: TIdHashMessageDigest5;
FileSrc: TFileStream;
StrMd5: UnicodeString;
begin
Md5 := TIdHashMessageDigest5.Create;
try
FileSrc := TFileStream.Create(Src, fmOpenRead);
try
StrMd5 := Md5.HashStreamAsHex(FileSrc);
finally
FileSrc.Free;
end;
finally
Md5.Free;
end;
end;
No one has pointed this out, inside the function no value was returned.
function GetFileMD5(const Src: TFileName): UnicodeString;
var
Md5: TIdHashMessageDigest5;
FileSrc: TFileStream;
StrMd5: UnicodeString;
begin
Md5 := TIdHashMessageDigest5.Create;
try
FileSrc := TFileStream.Create(Src, fmOpenRead);
try
StrMd5 := Md5.HashStreamAsHex(FileSrc);
finally
FileSrc.Free;
end;
finally
Md5.Free;
end;
// You are missing this line, calculated md5 was never returned
Result := StrMd5;
end;
In the code you posted, Self.WaitFor will never return. That waits until the thread has terminated, i.e. its Execute method has completed. But that can't happen because it stops and waits for itself. You should simply remove the call to WaitFor.
I also wonder whether or not Close is the correct way to terminate the form. If it really is a modal form then you should use oForm.ModalResult := mrOK.
I've just seen your edit which includes the definition of GetFileMD5. This function doesn't return a value. You should receive a compiler warning telling you of this—read the compiler warnings, they are very valuable. Write GetFileMD5 like this:
function GetFileMD5(const Src: TFileName): string;
var
Md5: TIdHashMessageDigest5;
FileSrc: TFileStream;
begin
Md5 := TIdHashMessageDigest5.Create;
try
FileSrc := TFileStream.Create(Src, fmOpenRead);
try
Result := Md5.HashStreamAsHex(FileSrc);
finally
FileSrc.Free;
end;
finally
Md5.Free;
end;
end;
My guess: the form is opened in Modal mode (form.ShowModal) and there is something that assigns the form.ModalResult before the calculation has completed.
This would cause the istantaneus closing of the form.
Maybe did you place a TBitButton having the modalresult propery set to mrOk? if you have done so, pressing that button will close the form as soon as the onClick event handler terminates, no matter if there is a running thread.
This is is a good documentation about threading in delphi, with examples, situations. Start reading from the beginning, and I'm sure you will find the answer by yourself. You don't have to read everything, just the first 4-5 chapters.
About the WaitFor
As the people have correctly pointed out, you should not call "Self.WaitFor;".
The "WaitFor" is designed to be called from other threads. The thread may not wait for himself - this is contrary to the logic. For example, I cannot wait for myself - I'm always ready for myself! ;-) So is the thread.
The Best Way to Close a Form From a Thread
The best way to close the form is to send a "WM_CLOSE" message to its window handle using "PostMessage".
Instead of the "Synchronize(DoSync);" do the following: "PostMessage(FormProgress.Handle, WM_CLOSE, 0, 0);".
The "FormProgress" here is the variable that holds the pointer to the "TFormProgress" class instance. Consequently, the "DoSync" is not needed.
On Synchronize
As a rule, the "Synchronize" shows that something is bad about the application design. It's better to design applications without any "Synchronize" at all.