How does the ShouldSkipPage procedure actually work in Inno Setup? [duplicate] - inno-setup

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:

Related

Why is the ShouldShipPage function called multiple times in Inno Setup [duplicate]

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:

Excel stealing keyboard focus from VCL Form (in AddIn)

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.

Inno Setup skip memo page if empty

I am using the Modular Dependencies scripts found here : http://www.codeproject.com/Articles/20868/NET-Framework-Installer-for-InnoSetup to install .Net and VC++.
Now, it's all well and good, and has allowed me to reduce my installer size by about 6MB.
But I want to provide the user with the fastest setup possible, and when the user already has all the necessary dependencies, he is presented with an empty "memo" screen. That's really less than optimal, and I'd wish to at least skip this step if there is nothing.
I can test if there is any component to install using if (GetArrayLength(products) = 0) then, but I can't find out where to place this test.
In the InitializeSetup function I can't find how to tell "skip this screen", and in the ShouldSkipPage function, I can't find how to tell "skip the Memo page".
Thanks !
It might be this way:
[Code]
function ShouldSkipPage(PageID: Integer): Boolean;
begin
// skip page when we are on the ready page and the memo is empty
Result := (PageID = wpReady) and (WizardForm.ReadyMemo.Text = '');
end;

How do I know if a restart is needed in InnoSetup script?

TL;DR version:
In an InnoSetup script, how can I detect if a restart is needed because of files that were in use?
More detailed version:
I have an Inno Setup script with the following characteristics:
the ShouldSkipPage function is implemented so that all pages (except the welcome page) are skipped unless a custom "Advanced options" checkbox on the welcome page is checked:
function ShouldSkipPage(PageID: Integer): Boolean;
begin
if ((PageID = wpSelectDir) or
(PageID = wpSelectProgramGroup) or
(PageID = wpSelectTasks) or
(PageID = wpFinished) or
(PageID = wpReady)) then
begin
Result := not advancedCheckBox.Checked;
end;
end;
CloseApplications and RestartApplications are set to false (*), and some files have the restartreplace and uninsrestartdelete flags, so a restart will be required to complete the installation if the files were in use
Now, if a restart is needed, I want to show the Finished page regardless of the state of the "Advanced options" checkbox, because I don't want to cause a restart without prompting the user. So my code would be something like that:
function ShouldSkipPage(PageID: Integer): Boolean;
begin
if ((PageID = wpSelectDir) or
(PageID = wpSelectProgramGroup) or
(PageID = wpSelectTasks) or
(PageID = wpReady)) then
begin
Result := not advancedCheckBox.Checked;
end
else if ((PageID = wpFinished)) then
begin
Result := (not advancedCheckBox.Checked) and (not IsRestartNeeded);
end
end;
Unfortunately, there is no IsRestartNeeded function (NeedRestart exists, but it's an event function). I spent a long time looking at the documentation, but I didn't find any function that could give me this information.
The only option I can think of is to look at HKLM\System\CurrentControlSet\Control\Session Manager\PendingFileRenameOperations to see if it contains any of my files, but it's a rather ugly solution...
(*) The files I want to replace or remove are a shell extension and some DLLs used by this extension. The reason why I'm not relying on the Restart Manager is because it doesn't seem to work with explorer.exe: the process is immediately restarted, and my files are locked again.
The usual recommendation is to call MakePendingFileRenameOperationsChecksum near the start of your installation process, and then again whenever you want to check whether a restart will be required. As long as it keeps returning the same value, a restart is not required.
Note that this won't take into account "forced restarts" eg. from you implementing NeedRestart and returning true or from a component marked with the restart flag; you're expected to be able to figure that out on your own, since you're in control of that.

How to get Inno setup to display message with only a cancel button so the install stops

I'm using Inno setup to deliver a software package. It detects the version of Access and pops up a message. I want to make the message tell the user they have downloaded the wrong version and halt the installation. Currently the Inno script uses
itd_downloadafter(NoRuntimePage.ID);
to display a message telling the user they need the AccessRuntime installed. When they user presses next it downloads the AccessRuntime and continues. I want to change this for my new script to tell the user they have the wrong version and then end the install script when they press next or just cancel. Can anyone help me abit on this?
Why to use InitializeSetup ?
If you want to conditionally exit the setup before the wizard starts, don't use InitializeWizard event function with the Abort exception raise. You'll be wasting your time, needed to create the whole wizard form. Use the InitializeSetup event function instead. There you can raise the Abort exception or better return False to its boolean result, and exit the function as was supposed to do - the final effect will be definitely the same.
Internally, the InitializeSetup function, raises just this Abort exception, when you return False to it from your script. Contrary to InitializeWizard event, when the InitializeSetup event is fired, the wizard form is not created yet, so you won't be wasting your time and never used system resources.
Code example:
In the following pseudo-code you need to have a function like UserDownloadedWrongVersion where, if you return True, the setup will be terminated, nothing happens oterwise.
[Code]
function UserDownloadedWrongVersion: Boolean;
begin
// make your check here and return True when you detect a wrong
// version, what causes the setup to terminate; False otherwise
end;
function InitializeSetup: Boolean;
begin
Result := not UserDownloadedWrongVersion;
if not Result then
begin
MsgBox('You''ve downloaded the wrong version. Setup will now exit!',
mbError, MB_OK);
Exit; // <-- or use Abort; instead, but there's no need for panic
end;
end;
** TLama's answer is more accurate. **
You could use the InitializeWizard procedure to run the access check at the beginning... if it fails you should be able to show your message box then call Abort() .
[code]
var CustomPage: TInputQueryWizardPage;
procedure InitializeWizard;
begin;
{your checking Access version and message box}
Abort();
end;

Resources