InstFiles Cancel - Allowing a user to cancel installation during InstFiles

From NSIS Wiki
Jump to navigationJump to search
Author: ZeBoxx (talk, contrib)


InstFiles Cancel - why this page exits

An oft-asked inquiry for NSIS is whether it is possible to cancel installation during InstFiles, the process where Sections are evaluated and is typically used for the actual installation of files, modification of registry keys, launching of other (third party) installers, etc.

This InstFiles Cancel topic (IFC for short) presents one possible route to make this possible without requiring any changes to the NSIS code itself.

As most users will be using Modern UI 2 (MUI) as their user interface, this topic specifically addresses making things work while using MUI. Other user interfaces may present their own unique problems or actually make things easier.

The problem superficially: the cancel button

This inquiry stems from the simple fact that the Cancel button is disabled on the InstFiles page, as demonstrated by the below skeleton script:

!include "MUI2.nsh"
 
outfile "test.exe"
ShowInstDetails show
 
!insertmacro MUI_PAGE_INSTFILES
 
Section "Part One"
  DetailPrint "Installing ${__SECTION__} A"
  Sleep 1000
  DetailPrint "Installing ${__SECTION__} B"
  Sleep 1000
  DetailPrint "Installing ${__SECTION__} C"
  Sleep 1000
SectionEnd
 
Section "Part Two"
  DetailPrint "Installing ${__SECTION__} D"
  Sleep 1000
SectionEnd
 
!insertmacro MUI_LANGUAGE "English"

Enabling the cancel button

It is fairly simply to enable the cancel button by using GetDlgItem and EnableWindow, as the below script demonstrates: ( please note that code that goes unmodified is commented out for display purposes, simply remove the first column of semicolons/spaces to get a fully working example )

; !include "MUI2.nsh"
; 
; outfile "test.exe"
; ShowInstDetails show
; 
 
  /*
    We'll need some code to run just before the Modern UI (MUI) dialog is
    displayed, in order to enable the Cancel button.  We can do this through
    MUI's built-in functionality of adding custom functions for each part of
    a dialog's life:
      _PRE : before any code is run (so the dialog doesn't exist yet)
      _SHOW : just before the dialog is displayed (the dialog exists in-memory)
      _LEAVE : after the dialog is closed (usually when going Back/Next)
  */
  !define MUI_PAGE_CUSTOMFUNCTION_SHOW InstFilesShow
 
; !insertmacro MUI_PAGE_INSTFILES
; 
 
  /*
    This is the function we're letting MUI call just before the dialog is
    dispayed.  Using the NSIS GetDlgItem function we can get a 'handle'...
    (hwnd - essentially a number that uniquely identifies an user interface
     element in Windows)
    ...to the Cancel button by specifying its unique identification number
    inside the dialog.
 
    Once that control's handle has been retrieved, we can simply enable that
    control.  In this case, enabling the Cancel button.
  */
  Function InstFilesShow
    GetDlgItem $0 $HWNDPARENT 2
    EnableWindow $0 1
  FunctionEnd
 
; 
; Section "Part One"
;   DetailPrint "Installing ${__SECTION__} A"
;   Sleep 1000
;   DetailPrint "Installing ${__SECTION__} B"
;   Sleep 1000
;   DetailPrint "Installing ${__SECTION__} C"
;   Sleep 1000
; SectionEnd
; 
; Section "Part Two"
;   DetailPrint "Installing ${__SECTION__} D"
;   Sleep 1000
; SectionEnd
; 
; !insertmacro MUI_LANGUAGE "English"

However, if you run this example and press the Cancel button, the installer quits immediately. If that's all you need - you're done.

Asking the user to confirm installation cancelation

More than likely, however, you'll want to handle the user clicking the Cancel button a bit more elegantly. You might want to ask the user if they're sure they want to quit.

Making sure the user wants to cancel - through MUI

You can make use of its built-in MUI_ABORTWARNING functionality as shown in the example below:

; !include "MUI2.nsh"
; 
; outfile "test.exe"
; ShowInstDetails show
;
 
  /*
    Let's use MUI's built-in functionality to present the user with a choice on
    whether or not to abort installation.
  */
  !define MUI_ABORTWARNING
  !define MUI_ABORTWARNING_TEXT "Are you sure you wish to abort installation?"
 
; 
; !define MUI_PAGE_CUSTOMFUNCTION_SHOW InstFilesShow
; !insertmacro MUI_PAGE_INSTFILES
; 
; Function InstFilesShow
;   GetDlgItem $0 $HWNDPARENT 2
;   EnableWindow $0 1
; FunctionEnd
; 
; Section "Part One"
;   DetailPrint "Installing ${__SECTION__} A"
;   Sleep 1000
;   DetailPrint "Installing ${__SECTION__} B"
;   Sleep 1000
;   DetailPrint "Installing ${__SECTION__} C"
;   Sleep 1000
; SectionEnd
; 
; Section "Part Two"
;   DetailPrint "Installing ${__SECTION__} D"
;   Sleep 1000
; SectionEnd
; 
; !insertmacro MUI_LANGUAGE "English"

If that is all you need - once again, you're done. When the user indicates that they really want to abort installation, the installer quits.

Making sure the user wants to cancel - DIY better

Unfortunately - as you might have noticed from the above example - the installation process continues even as MUI asks the user if they really want to abort. Not much point in asking the user if they want to abort, if the installation finishes during the time they make this incredibly complex decision over coffee.

There's three things that need to be done to fix this...

  1. The installation must be paused temporarily
  2. The question of whether the user wants to abort installation (MUI_ABORTWARNING) must be replaced by our own
  3. The installation must be allowed to continue if the user desires to do so.

Things get a little complex from here, so make sure you really want to do this. To make things a litte -less- complex, we'll be using LogicLib to deal with any If-Then-Else etc. code flow.

Custom abort confirmation

Let's start by creating our own confirmation question using a yes/no MessageBox when the user aborts (which calls the .onUserAbort callback, which is wrapped by MUI):

; !include "MUI2.nsh"
 
  /*
    Let's make use of LogicLib - it essentially adds if-then-else support
    and more to NSIS and unless you need some -very- lean code, I can't
    recommend making use of this header enough.
  */
  !include "LogicLib.nsh"
 
; 
; outfile "test.exe"
; ShowInstDetails show
; 
; !define MUI_ABORTWARNING_TEXT "Are you sure you wish to abort installation?"
;
; !define MUI_PAGE_CUSTOMFUNCTION_SHOW InstFilesShow
; !insertmacro MUI_PAGE_INSTFILES
; 
; Function InstFilesShow
;   GetDlgItem $0 $HWNDPARENT 2
;   EnableWindow $0 1
; FunctionEnd
; 
; Section "Part One"
;   DetailPrint "Installing ${__SECTION__} A"
;   Sleep 1000
;   DetailPrint "Installing ${__SECTION__} B"
;   Sleep 1000
;   DetailPrint "Installing ${__SECTION__} C"
;   Sleep 1000
; SectionEnd
; 
; Section "Part Two"
;   DetailPrint "Installing ${__SECTION__} D"
;   Sleep 1000
; SectionEnd
; 
 
  /*
    We'll want to add some custom handling of the user canceling installation,
    as MUI's default behavior just makes the installer Quit.
    Note the use of one of LogicLib's more esoteric features: executing a
    command in-line so that we can forego on a bunch of GoTo commands and
    labels.
  */
  !define MUI_CUSTOMFUNCTION_ABORT onUserAbort
  Function onUserAbort
    ${If} ${Cmd} `MessageBox MB_YESNO|MB_DEFBUTTON2 "${MUI_ABORTWARNING_TEXT}" IDYES`
      MessageBox MB_OK "User aborted installation.  Your code goes here"
    ${Else}
      /*
        See the documentation for .onUserAbort as to why Abort is called here.
      */
      Abort
    ${EndIf}
  FunctionEnd
 
; 
; !insertmacro MUI_LANGUAGE "English"

Making sure the user aborted from InstFiles

Now you need to figure out if the user aborted from the InstFiles page, and not any other page, so that you'll only run the pausing code where it is needed, without interfering with other pages. You can do this by creating, setting and checking against a Variable.

The below example adds a few dummy pages to test this part of the code on - they'll be removed from later examples.

; !include "MUI2.nsh"
; !include "LogicLib.nsh"
; 
; outfile "test.exe"
; ShowInstDetails show
; 
; !define MUI_ABORTWARNING_TEXT "Are you sure you wish to abort installation?"
; 
 
  /*
    Inserting a page before the InstFiles page so we can test cancellation
    outside of the InstFiles page.
  */
  !insertmacro MUI_PAGE_WELCOME
 
  /*
    Typically you might have the Finish page right after the InstFiles page.
    The MUi_FINISHPAGE_NOAUTOCLOSE define keeps the InstFiles page from
    automatically progressing to that Finish page.
  */
  !define MUI_FINISHPAGE_NOAUTOCLOSE
 
  /*
    We'll need a custom Pre function so that we can set a variable to hold the
    current page.
  */
  !define MUI_PAGE_CUSTOMFUNCTION_PRE InstFilesPre
 
; !define MUI_PAGE_CUSTOMFUNCTION_SHOW InstFilesShow
 
  /*
    And a Leave function to unset the variable holding the current page, just
    so we don't have to rely on the next page to overwrite it.
  */
  !define MUI_PAGE_CUSTOMFUNCTION_LEAVE InstFilesLeave
 
; !insertmacro MUI_PAGE_INSTFILES
; 
 
  /*
    Some more pages to test on -after- InstFiles.  The last page doesn't have
    a Cancel button, hence there's two.
  */
  !insertmacro MUI_PAGE_WELCOME
  !insertmacro MUI_PAGE_FINISH
 
 
  /*
    A variable that will hold the current page so we can decide on installation
    cancel behavior.
  */
  Var CurrentPage
 
  /*
    This is the Pre function, and here the Current Page variable is set to
    'InstFiles' so that we can separate installation cancel behavior for this
    page from that for other pages.
  */
 
  Function InstFilesPre
    StrCpy $CurrentPage "InstFiles"
  FunctionEnd
 
; 
; Function InstFilesShow
;   GetDlgItem $0 $HWNDPARENT 2
;   EnableWindow $0 1
; FunctionEnd
; 
; Section "Part One"
;   DetailPrint "Installing ${__SECTION__} A"
;   Sleep 1000
;   DetailPrint "Installing ${__SECTION__} B"
;   Sleep 1000
;   DetailPrint "Installing ${__SECTION__} C"
;   Sleep 1000
; SectionEnd
; 
; Section "Part Two"
;   DetailPrint "Installing ${__SECTION__} D"
;   Sleep 1000
; SectionEnd
; 
 
  /*
    This is the Leave function in which we un-set the Current Page variable.
  */
  Function InstFilesLeave
    StrCpy $CurrentPage ""
  FunctionEnd
; 
; !define MUI_CUSTOMFUNCTION_ABORT onUserAbort
; Function onUserAbort
;   ${If} ${Cmd} `MessageBox MB_YESNO|MB_DEFBUTTON2 "${MUI_ABORTWARNING_TEXT}" IDYES`
 
      /*
        Now that we have a variable that holds the current page, we can switch behavior
        based on whether the user cancelled on the InstFiles page, or elsewhere.
      */
      ${If} $CurrentPage == "InstFiles"
        MessageBox MB_OK "User aborted during InstFiles."
      ${Else}
        MessageBox MB_OK "User aborted elsewhere"
      ${EndIf}
 
;   ${Else}
;     Abort
;   ${EndIf}
; FunctionEnd
; 
; !insertmacro MUI_LANGUAGE "English"

So far so good.

Pausing installation while waiting for confirmation

Now you actually need to pause installation. You can do this by setting another Variable when the user is presented with the question of whether they want to abort, which is then checked during installation. If that variable indicates that the user is indeed currently trying to make this decision, then the installation process can simply be paused with a small Do / Loop out of LogicLib, with a short Sleep function in it to slow evaluation down and keep CPU use from going to 100%.

Because you might want to check this repeatedly during a Section, it's best to make this a Macro or Function. See the topic on Macros vs Functions on the upsides and downsides to both. One downside for our purposes is that instructions in macros count against the installation progress, so you might see a twitchy progress bar while the Do / Loop runs and NSIS detects it going back an instruction or two, then forward through them, then back again, etc.

The below example thus uses a Function mainly to prevent the twitchy progress bar issue. Note that the Function is used anywhere you would want to handle this. Typically you would use it at least at the beginning of every section except the first (where the user hasn't really had time to cancel yet, so no point in putting it there).

; !include "MUI2.nsh"
; !include "LogicLib.nsh"
; 
; outfile "test.exe"
; ShowInstDetails show
; 
; !define MUI_ABORTWARNING_TEXT "Are you sure you wish to abort installation?"
;
 
  /*
    Let's get rid of the other pages as we've already established that the page
	detection is solid, and they just clutter up testing at this point.
  */
 
; !define MUI_FINISHPAGE_NOAUTOCLOSE
; !define MUI_PAGE_CUSTOMFUNCTION_PRE InstFilesPre
; !define MUI_PAGE_CUSTOMFUNCTION_SHOW InstFilesShow
; !define MUI_PAGE_CUSTOMFUNCTION_LEAVE InstFilesLeave
; !insertmacro MUI_PAGE_INSTFILES
; 
; Var CurrentPage
 
  /*
    A variable to hold a state indicating that the user is currently making a
    decision on whether or not to abort installation.
  */
  Var UserIsMakingAbortDecision
 
  /*
    A function that pauses as long as the user is deciding on installation
    cancellation - performed by looping over a sleep call as long as the
    appropriate flag is set.
    This function is used anywhere you want the user to be able to actually
    cancel.  This could be after every command, but that might be overkill.
    Using it instead after a group of commands would be more typical.
  */
  Function PauseIfUserIsMakingAbortDecision
    ${DoWhile} $UserIsMakingAbortDecision == "yes"
      Sleep 500
    ${Loop}
  FunctionEnd
  !define PauseIfUserIsMakingAbortDecision `Call PauseIfUserIsMakingAbortDecision`
 
; 
; Function InstFilesPre
;   StrCpy $CurrentPage "InstFiles"
; FunctionEnd
; 
; Function InstFilesShow
;   GetDlgItem $0 $HWNDPARENT 2
;   EnableWindow $0 1
; FunctionEnd
; 
; Section "Part One"
;   DetailPrint "Installing ${__SECTION__} A"
;   Sleep 1000
 
    /*
      Invoking the pause function anywhere the user should be allowed to cancel
    */
    ${PauseIfUserIsMakingAbortDecision}
 
;   DetailPrint "Installing ${__SECTION__} B"
;   Sleep 1000
    ${PauseIfUserIsMakingAbortDecision}
;   DetailPrint "Installing ${__SECTION__} C"
;   Sleep 1000
    ${PauseIfUserIsMakingAbortDecision}
; SectionEnd
; 
; Section "Part Two"
    ${PauseIfUserIsMakingAbortDecision}
;   DetailPrint "Installing ${__SECTION__} D"
;   Sleep 1000
    ${PauseIfUserIsMakingAbortDecision}
; SectionEnd
; 
;   Function InstFilesLeave
;   StrCpy $CurrentPage ""
; FunctionEnd
; 
; !define MUI_CUSTOMFUNCTION_ABORT onUserAbort
; Function onUserAbort
;   StrCpy $UserIsMakingAbortDecision "yes"
;   ${If} ${Cmd} `MessageBox MB_YESNO|MB_DEFBUTTON2 "${MUI_ABORTWARNING_TEXT}" IDYES`
;     StrCpy $UserIsMakingAbortDecision "no"
;     ${If} $CurrentPage == "InstFiles"
; 
;     ${Else}
; 
;     ${EndIf}
;   ${Else}
;     StrCpy $UserIsMakingAbortDecision "no"
;     Abort
;   ${EndIf}
; FunctionEnd
; 
; !insertmacro MUI_LANGUAGE "English"

Now the installation process is actually paused, and we can handle the user canceling mid-installation.

Advanced handling of the user canceling installation

But it's quite possible you actually want continue with your installer 'as is' and present different pages (see the Skipping Pages topic for more information) based on the user having pressed the Cancel button during InstFiles. You could do this all within the onUserAbort callback, but that gets fairly unwieldy.

So you have to stop the installer from actually exiting when the user aborts. You can do this by Aborting the onUserAbort - this is already used in the previous example to let the installer continue if the user has indicated that, on second thought, they do not want to abort installation.

All you have to do, then, is copy that same code to the user making the decision that they -do- want to abort installation, and then somehow skip the remainder of the section and any following sections. This can be done using another Variable to indicate that the user wants to abort, checking that, and then using some tricky Macro and Goto action if the user wanted to abort. We can combine this code with the installer pausing code so that you don't need separate lines for this.

The reason the tricky Macros and Goto are used is because calling the Abort function from within a section will signal an internal 'installation failed' routine that disables the Next button - with nowhere to go to re-enable it (.onInstFailed is called when the user presses the Cancel button -after- installation has already failed).

Please note that, as a result of this handling, .onInstFailed will -not- be called! Make sure you replace this function callback with a different one, and call it manually later on if you need it.

A friendly message printed to the Details View indicates that a Section was aborted by referencing its name using the ${__SECTION__} scope predefine.

; !include "MUI2.nsh"
; !include "LogicLib.nsh"
; 
; outfile "test.exe"
; ShowInstDetails show
; 
; !define MUI_ABORTWARNING_TEXT "Are you sure you wish to abort installation?"
; 
; !define MUI_FINISHPAGE_NOAUTOCLOSE
; !define MUI_PAGE_CUSTOMFUNCTION_PRE InstFilesPre
; !define MUI_PAGE_CUSTOMFUNCTION_SHOW InstFilesShow
; !define MUI_PAGE_CUSTOMFUNCTION_LEAVE InstFilesLeave
; !insertmacro MUI_PAGE_INSTFILES
; 
; Var CurrentPage
; Var UserIsMakingAbortDecision
 
  /*
    A variable acting as a flag indicating whether or not the user aborted installation
  */
  Var UserAborted
 
; 
; Function PauseIfUserIsMakingAbortDecision
;   ${DoWhile} $UserIsMakingAbortDecision == "yes"
;     Sleep 500
;   ${Loop}
; FunctionEnd
; !define PauseIfUserIsMakingAbortDecision `Call PauseIfUserIsMakingAbortDecision`
; 
 
  /*
    A small macro in which we check if the user aborted and, if so, go to a
    label set by another macro to skip any code inbetween the two macros.
    Note that this replaces the PauseIfUserIsMakingAbortDecision macro calls in
    the script, so that you only need one line.
  */
  !macro CheckUserAborted
    ${PauseIfUserIsMakingAbortDecision}
    ${If} $UserAborted == "yes"
      goto _userabort_aborted
    ${EndIf}
  !macroend
  !define CheckUserAborted `!insertmacro CheckUserAborted`
 
  /*
    The 'end' macro serving two purposes:
    1. To insert the goto label that the CheckUserAborted macro refers to
    2. To print out to the Details View that installation was aborted.
  */
  !macro EndUserAborted
    ${CheckUserAborted}
    goto _useraborted_end
    _userabort_aborted:
      DetailPrint "Installation of ${__SECTION__} interrupted.  Installation aborted."
    _useraborted_end:
  !macroend
  !define EndUserAborted `!insertmacro EndUserAborted`
; 
; Function InstFilesPre
;   StrCpy $CurrentPage "InstFiles"
;   StrCpy $UserAborted "no"
; FunctionEnd
; 
; Function InstFilesShow
;   GetDlgItem $0 $HWNDPARENT 2
;   EnableWindow $0 1
; FunctionEnd
; 
; Section "Part One"
;   DetailPrint "Installing ${__SECTION__} A"
;   Sleep 1000
    ${CheckUserAborted}
;   DetailPrint "Installing ${__SECTION__} B"
;   Sleep 1000
    ${CheckUserAborted}
;   DetailPrint "Installing ${__SECTION__} C"
;   Sleep 1000
    ${EndUserAborted}
; SectionEnd
; 
; Section "Part Two"
    ${CheckUserAborted}
;   DetailPrint "Installing ${__SECTION__} D"
;   Sleep 1000
    ${EndUserAborted}
; SectionEnd
; 
; Function InstFilesLeave
;   StrCpy $CurrentPage ""
; FunctionEnd
; 
; !define MUI_CUSTOMFUNCTION_ABORT onUserAbort
; Function onUserAbort
;   StrCpy $UserIsMakingAbortDecision "yes"
;   ${If} ${Cmd} `MessageBox MB_YESNO|MB_DEFBUTTON2 "${MUI_ABORTWARNING_TEXT}" IDYES`
;     StrCpy $UserIsMakingAbortDecision "no"
;     ${If} $CurrentPage == "InstFiles"
 
        /*
          The UserAborted flag is set here
        */
        StrCpy $UserAborted "yes"
        Abort
 
;     ${Else}
; 
;     ${EndIf}
;   ${Else}
;     StrCpy $UserIsMakingAbortDecision "no"
;     Abort
;   ${EndIf}
; FunctionEnd
; 
; !insertmacro MUI_LANGUAGE "English"

This is pretty much the behavior that we need:

  • The user can cancel mid-install
  • They are asked if they are sure they want to cancel
  • While they are being asked, installation is paused
  • If they don't want to cancel the installation continues
  • If they do want to cancel, then no further installation is performed, while
  • The installer can keep running

UI Clean-up and Section Detection

Now there's a few things you should do to clean things up a little...

  • Right now each section reports "Installation aborted". That's a little silly and you can do better because...
  • you can actually identify -which- section the installation was aborted in and...
  • which sections were skipped entirely.
  • The Details View reads 'Completed' even though it may not be so
  • In addition, the InstFiles page's Header texts (normally set through MUI_INSTFILESPAGE_ABORTHEADER_TEXT and _SUBTEXT) don't reflect our canceled state.

Let's address these here.

UI clean-up

The 'completed' text is easily handled by making use of NSIS's CompletedText installer attribute, we'll just need to set a Variable, used by CompletedText, in the last section - creating a "-Post" section specifically for this sort of thing is recommended. ( It can't be handled in the page's Leave function as this text is printed before the Leave function is triggered. )

Setting this also removes any need for the "Installation Aborted" DetailPrint we've been using.

The header texts can be addressed in the same way. As MUI ends up using the texts normally used for a successful installation, we'll have to adjust these to indicate that installation was actually aborted. They are set through the MUI_INSTFILESPAGE_FINISHHEADER_TEXT and MUI_INSTFILESPAGE_FINISHHEADER_SUBTEXT defines, which can take a variable so that we can change them at runtime.

; !include "MUI2.nsh"
; !include "LogicLib.nsh"
; 
; outfile "test.exe"
; ShowInstDetails show
;
; !define MUI_ABORTWARNING_TEXT "Are you sure you wish to abort installation?"
; 
 
  /*
    Set up a new variable that will be used by the CompletedText directive,
    so that we can change it at run-time
  */
  Var CompletedText
  CompletedText $CompletedText
 
; 
; !define MUI_FINISHPAGE_NOAUTOCLOSE
; !define MUI_PAGE_CUSTOMFUNCTION_PRE InstFilesPre
; !define MUI_PAGE_CUSTOMFUNCTION_SHOW InstFilesShow
; !define MUI_PAGE_CUSTOMFUNCTION_LEAVE InstFilesLeave
 
  /*
    Set up new variables that will be used by MUI's header UI handling,
    so that we can change them at run-time
  */
  Var MUI_HeaderText
  Var MUI_HeaderSubText
  !define MUI_INSTFILESPAGE_FINISHHEADER_TEXT "$MUI_HeaderText"
  !define MUI_INSTFILESPAGE_FINISHHEADER_SUBTEXT "$MUI_HeaderSubText"
 
; !insertmacro MUI_PAGE_INSTFILES
; 
; Var CurrentPage
; Var UserIsMakingAbortDecision
; Var UserAborted
; 
; Function PauseIfUserIsMakingAbortDecision
;   ${DoWhile} $UserIsMakingAbortDecision == "yes"
;     Sleep 500
;   ${Loop}
; FunctionEnd
; !define PauseIfUserIsMakingAbortDecision `Call PauseIfUserIsMakingAbortDecision`
; 
; !macro CheckUserAborted
;   ${PauseIfUserIsMakingAbortDecision}
;   ${If} $UserAborted == "yes"
;     goto _userabort_aborted
;   ${EndIf}
; !macroend
; !define CheckUserAborted `!insertmacro CheckUserAborted`
; 
; !macro EndUserAborted
;   ${CheckUserAborted}
;   goto _useraborted_end
;   _userabort_aborted:
;     DetailPrint "Installation of ${__SECTION__} interrupted.  Installation aborted."
;   _useraborted_end:
; !macroend
; !define EndUserAborted `!insertmacro EndUserAborted`
; 
; Function InstFilesPre
;   StrCpy $CurrentPage "InstFiles"
;   StrCpy $UserAborted "no"
; FunctionEnd
; 
; Function InstFilesShow
;   GetDlgItem $0 $HWNDPARENT 2
;   EnableWindow $0 1
; FunctionEnd
; 
; Section "Part One"
;   DetailPrint "Installing ${__SECTION__} A"
;   Sleep 1000
;   ${CheckUserAborted}
;   DetailPrint "Installing ${__SECTION__} B"
;   Sleep 1000
;   ${CheckUserAborted}
;   DetailPrint "Installing ${__SECTION__} C"
;   Sleep 1000
;   ${EndUserAborted}
; SectionEnd
; 
; Section "Part Two"
;   ${CheckUserAborted}
;   DetailPrint "Installing ${__SECTION__} D"
;   Sleep 1000
;   ${EndUserAborted}
; SectionEnd
; 
 
  /*
     A new section, which should be the very last section, that will change the
     'completed' text and MUI Headers depending on whether or not the user
     aborted
  */
  Section -"Post"
    ${If} $UserAborted == "yes"
      StrCpy $CompletedText "Installation aborted."
      StrCpy $MUI_HeaderText "Installation Failed"
      StrCpy $MUI_HeaderSubText "Setup was aborted."
    ${Else}
      StrCpy $CompletedText "Completed"
      StrCpy $MUI_HeaderText "Installation Complete"
      StrCpy $MUI_HeaderSubText "Setup was completed successfully."
    ${EndIf}
  SectionEnd
 
; 
; Function InstFilesLeave
;   StrCpy $CurrentPage ""
; FunctionEnd
; 
; !define MUI_CUSTOMFUNCTION_ABORT onUserAbort
; Function onUserAbort
;   StrCpy $UserIsMakingAbortDecision "yes"
;   ${If} ${Cmd} `MessageBox MB_YESNO|MB_DEFBUTTON2 "${MUI_ABORTWARNING_TEXT}" IDYES`
      StrCpy $UserIsMakingAbortDecision "no"
;     ${If} $CurrentPage == "InstFiles"
;       StrCpy $UserAborted "yes"
;       Abort
;     ${Else}
; 
;     ${EndIf}
;   ${Else}
;     StrCpy $UserIsMakingAbortDecision "no"
;     Abort
;   ${EndIf}
; FunctionEnd
; 
; !insertmacro MUI_LANGUAGE "English"

The user interface should now indicate the installation state correctly.

Section detection

The Section detection bit we can address by creating another Variable that will hold the section name in which we aborted, and comparing against that to determine the message to display. That variable can then also be used to determine the things you might have to address in further pages.

; !include "MUI2.nsh"
; !include "LogicLib.nsh"
; 
; outfile "test.exe"
; ShowInstDetails show
;
; !define MUI_ABORTWARNING_TEXT "Are you sure you wish to abort installation?"
;
; Var CompletedText
; CompletedText $CompletedText
; 
; !define MUI_FINISHPAGE_NOAUTOCLOSE
; !define MUI_PAGE_CUSTOMFUNCTION_PRE InstFilesPre
; !define MUI_PAGE_CUSTOMFUNCTION_SHOW InstFilesShow
; !define MUI_PAGE_CUSTOMFUNCTION_LEAVE InstFilesLeave
; Var MUI_HeaderText
; Var MUI_HeaderSubText
; !define MUI_INSTFILESPAGE_FINISHHEADER_TEXT "$MUI_HeaderText"
; !define MUI_INSTFILESPAGE_FINISHHEADER_SUBTEXT "$MUI_HeaderSubText"
; !insertmacro MUI_PAGE_INSTFILES
; 
; Var CurrentPage
; Var UserIsMakingAbortDecision
; Var UserAborted
 
  /*
    A new variable to hold the name of the Section in which the user aborted installation
  */
  Var SectionAborted
 
;
; Function PauseIfUserIsMakingAbortDecision
;   ${DoWhile} $UserIsMakingAbortDecision == "yes"
;     Sleep 500
;   ${Loop}
; FunctionEnd
; !define PauseIfUserIsMakingAbortDecision `Call PauseIfUserIsMakingAbortDecision`
; 
; !macro CheckUserAborted
;   ${PauseIfUserIsMakingAbortDecision}
;   ${If} $UserAborted == "yes"
;     goto _userabort_aborted
;   ${EndIf}
; !macroend
; !define CheckUserAborted `!insertmacro CheckUserAborted`
; 
; !macro EndUserAborted
;   ${CheckUserAborted}
;   goto _useraborted_end
;   _userabort_aborted:
 
      /*
        Here we check the value of the $SectionAborted variable to determine
        which message to print to the Details View
        If it hasn't been set yet then the current section must be the one in
        which the user aborted installation
      */
      ${If} $SectionAborted == ""
        StrCpy $SectionAborted "${__SECTION__}"
        DetailPrint "${__SECTION__} installation interrupted."
 
      /*
        Otherwise we compare the variable the current section's name - if it
        doesn't match, the current section is one that is being skipped
      */
      ${ElseIf} $SectionAborted != "${__SECTION__}"
        DetailPrint "  ${__SECTION__} installation skipped."
      ${EndIf}
 
;   _useraborted_end:
; !macroend
; !define EndUserAborted `!insertmacro EndUserAborted`
; 
; Function InstFilesPre
;   StrCpy $CurrentPage "InstFiles"
;   StrCpy $UserAborted "no"
; FunctionEnd
; 
; Function InstFilesShow
;   GetDlgItem $0 $HWNDPARENT 2
;   EnableWindow $0 1
; FunctionEnd
; 
; Section "Part One"
;   DetailPrint "Installing ${__SECTION__} A"
;   Sleep 1000
;   ${CheckUserAborted}
;   DetailPrint "Installing ${__SECTION__} B"
;   Sleep 1000
;   ${CheckUserAborted}
;   DetailPrint "Installing ${__SECTION__} C"
;   Sleep 1000
;   ${EndUserAborted}
; SectionEnd
; 
; Section "Part Two"
;   ${CheckUserAborted}
;   DetailPrint "Installing ${__SECTION__} D"
;   Sleep 1000
;   ${EndUserAborted}
; SectionEnd
; 
; Section -"Post"
;   ${If} $UserAborted == "yes"
;     StrCpy $CompletedText "Installation aborted."
;     StrCpy $MUI_HeaderText "Installation Failed"
;     StrCpy $MUI_HeaderSubText "Setup was aborted."
;   ${Else}
;     StrCpy $CompletedText "Completed"
;     StrCpy $MUI_HeaderText "Installation Complete"
;     StrCpy $MUI_HeaderSubText "Setup was completed successfully."
;   ${EndIf}
; SectionEnd
; 
; Function InstFilesLeave
;   StrCpy $CurrentPage ""
; FunctionEnd
; 
; !define MUI_CUSTOMFUNCTION_ABORT onUserAbort
; Function onUserAbort
;   StrCpy $UserIsMakingAbortDecision "yes"
;   ${If} ${Cmd} `MessageBox MB_YESNO|MB_DEFBUTTON2 "${MUI_ABORTWARNING_TEXT}" IDYES`
;     StrCpy $UserIsMakingAbortDecision "no"
;     ${If} $CurrentPage == "InstFiles"
;       StrCpy $UserAborted "yes"
;       Abort
;     ${Else}
; 
;     ${EndIf}
;   ${Else}
;     StrCpy $UserIsMakingAbortDecision "no"
;     Abort
;   ${EndIf}
; FunctionEnd
; 
; !insertmacro MUI_LANGUAGE "English"

And you're done!

..more or less.

Great - now what?

Of course, letting a user cancel mid-installation is just one problem solved. Typically you might want to remove files you installed, restore old files, etc. Handling these is well outside of the scope of this topic, but here are some potentially useful other topics and forum threads:

Full Example

Below is the full example code without comments but with surrounding pages and usert abort messagebox indicators so you can run it without any editing required and check behavior.

!include "MUI2.nsh"
!include "LogicLib.nsh"
 
outfile "test.exe"
ShowInstDetails show
 
!define MUI_ABORTWARNING_TEXT "Are you sure you wish to abort installation?"
 
Var CompletedText
CompletedText $CompletedText
 
!insertmacro MUI_PAGE_WELCOME
 
!define MUI_FINISHPAGE_NOAUTOCLOSE
!define MUI_PAGE_CUSTOMFUNCTION_PRE InstFilesPre
!define MUI_PAGE_CUSTOMFUNCTION_SHOW InstFilesShow
!define MUI_PAGE_CUSTOMFUNCTION_LEAVE InstFilesLeave
Var MUI_HeaderText
Var MUI_HeaderSubText
!define MUI_INSTFILESPAGE_FINISHHEADER_TEXT "$MUI_HeaderText"
!define MUI_INSTFILESPAGE_FINISHHEADER_SUBTEXT "$MUI_HeaderSubText"
!insertmacro MUI_PAGE_INSTFILES
 
!insertmacro MUI_PAGE_WELCOME
!insertmacro MUI_PAGE_FINISH
 
Var CurrentPage
Var UserIsMakingAbortDecision
Var UserAborted
Var SectionAborted
 
Function PauseIfUserIsMakingAbortDecision
  ${DoWhile} $UserIsMakingAbortDecision == "yes"
    Sleep 500
  ${Loop}
FunctionEnd
!define PauseIfUserIsMakingAbortDecision `Call PauseIfUserIsMakingAbortDecision`
 
!macro CheckUserAborted
  ${PauseIfUserIsMakingAbortDecision}
  ${If} $UserAborted == "yes"
    goto _userabort_aborted
  ${EndIf}
!macroend
!define CheckUserAborted `!insertmacro CheckUserAborted`
 
!macro EndUserAborted
  ${CheckUserAborted}
  goto _useraborted_end
  _userabort_aborted:
    ${If} $SectionAborted == ""
      StrCpy $SectionAborted "${__SECTION__}"
      DetailPrint "${__SECTION__} installation interrupted."
    ${ElseIf} $SectionAborted != "${__SECTION__}"
      DetailPrint "  ${__SECTION__} installation skipped."
    ${EndIf}
 
  _useraborted_end:
!macroend
!define EndUserAborted `!insertmacro EndUserAborted`
 
Function InstFilesPre
  StrCpy $CurrentPage "InstFiles"
  StrCpy $UserAborted "no"
FunctionEnd
 
Function InstFilesShow
  GetDlgItem $0 $HWNDPARENT 2
  EnableWindow $0 1
FunctionEnd
 
Section "Part One"
  DetailPrint "Installing ${__SECTION__} A"
  Sleep 1000
  ${CheckUserAborted}
  DetailPrint "Installing ${__SECTION__} B"
  Sleep 1000
  ${CheckUserAborted}
  DetailPrint "Installing ${__SECTION__} C"
  Sleep 1000
  ${EndUserAborted}
SectionEnd
 
Section "Part Two"
  ${CheckUserAborted}
  DetailPrint "Installing ${__SECTION__} D"
  Sleep 1000
  ${EndUserAborted}
SectionEnd
 
Section -"Post"
  ${If} $UserAborted == "yes"
    StrCpy $CompletedText "Installation aborted."
    StrCpy $MUI_HeaderText "Installation Failed"
    StrCpy $MUI_HeaderSubText "Setup was aborted."
  ${Else}
    StrCpy $CompletedText "Completed"
    StrCpy $MUI_HeaderText "Installation Complete"
    StrCpy $MUI_HeaderSubText "Setup was completed successfully."
  ${EndIf}
SectionEnd
 
Function InstFilesLeave
  StrCpy $CurrentPage ""
FunctionEnd
 
!define MUI_CUSTOMFUNCTION_ABORT onUserAbort
Function onUserAbort
  StrCpy $UserIsMakingAbortDecision "yes"
  ${If} ${Cmd} `MessageBox MB_YESNO|MB_DEFBUTTON2 "${MUI_ABORTWARNING_TEXT}" IDYES`
    ${If} $CurrentPage == "InstFiles"
      StrCpy $UserAborted "yes"
      MessageBox MB_OK "User aborted during InstFiles."
      StrCpy $UserIsMakingAbortDecision "no"
      Abort
    ${Else}
      MessageBox MB_OK "User aborted elsewhere"
      StrCpy $UserIsMakingAbortDecision "no"
    ${EndIf}
  ${Else}
    StrCpy $UserIsMakingAbortDecision "no"
    Abort
  ${EndIf}
FunctionEnd
 
!insertmacro MUI_LANGUAGE "English"

Known Issues

  • It's a lot of code changes for something so seemingly simple
  • This document isn't Multiple Languages-friendly at the time of this writing, re-using none of the langstrings defined for MUI, and not respecting RTL in the abort confirmation dialog.
  • When Canceling, the Details View gets an empty line added - the source of which this author has not yet identified.