ElektroStudios
ElektroStudios

Reputation: 20474

Check all task items in Inno Setup that are check boxes, ignoring radio button controls

I have the following (an attempt of) universal function to select all task items in Inno Setup:

procedure CheckAllTasks();
var
  I: Integer;
begin
  for I := 0 to WizardForm.TasksList.Items.Count - 1 do begin
    if WizardForm.TasksList.ItemEnabled[I] then begin
      WizardForm.TasksList.Checked[I] := True;
    end;
  end;
end;

This approach has the inconvenient that it also affects current radio button selection by selecting the last radio buttons in the task list items array as shown in the following example:

enter image description here

Then, I just would like to ignore radio buttons using this function.

I was analyzing the TNewCheckListBox and its class inheritances expecting to find some base method to help me identify the type of the control, but I was not able. The functions WizardForm.TasksList.ItemObject[Index] and WizardForm.TasksList.Items.Objects[Index] does not seem to return a TObject convertible to TComponent or TControl.

The main issue I'm facing is that WizardForm.TasksList.Items is a TStrings class with which I can't figure out which control correspond to each of those string items.

Please note that I'm aware of how to edit the [Tasks] section to add a top level item check box in the installer UI that will easy enable me to check/uncheck all items remaining to that group. That is not what I'm looking for.


Update: Since Inno Setup API doesn't seem to provide a direct access for this purpose, my current approach is to use Windows API in the following way:

Using SendMessage function with ListBox messages, concretely LB_GETITEMRECT to retrieve each item RECT (defined as TRect structure in Inno Setup), I could then call WindowFromPoint function to retrieve a handle to the item window. Finally, I could call GetClassName or RealGetWindowClass function to determine whether the item is a Label (TNewStaticText and etc), a CheckBox (TCustomCheckBox and etc) or a RadioButton (TRadioButton and etc).

My knowledge of pascal-script is too limited to implement this without errors, but this is what I've tried by my own:

[Setup]
AppName=test
AppVersion=1
DefaultDirName={tmp}\test

[Tasks]
Name: taskCheckBox; Description: My CheckBox
Name: taskRadioButton; Description: My RadioButton; Flags: exclusive
[Code]
const
  // https://learn.microsoft.com/en-us/windows/win32/controls/lb-getitemrect
  LB_GETITEMRECT = $0198;
  LB_ERR = -1;
  
function SendMessageIntRect(hWnd: HWND; msg: UINT; wParam: Integer; var lParam: TRect): Integer;
  external '[email protected] stdcall setuponly';

function WindowFromPoint(pt: TPoint): HWND; 
  external '[email protected] stdcall setuponly';

function ClientToScreen(hWnd: HWND; var pt: TPoint): Boolean;
  external '[email protected] stdcall setuponly';

function GetListBoxItemHandle(listHandle: HWND; itemIndex: Integer): HWND;
var
  itemRect: TRect;
  itemPoint: TPoint;
  itemHandle: HWND;
begin
  if SendMessageIntRect(listHandle, LB_GETITEMRECT, itemIndex, itemRect) <> LB_ERR then begin
    itemPoint.X := itemRect.Left;
    itemPoint.Y := itemRect.Top;
    if ClientToScreen(listHandle, itemPoint) then begin
      itemHandle := WindowFromPoint(itemPoint);
      Result := itemHandle;
    end;
  end;
end;
  
procedure CurPageChanged(CurPageID: Integer);
var
  i: Integer;
  listHandle: HWND;
  itemHandle: HWND;
begin
  if CurPageID = wpSelectTasks then begin
    listHandle:= WizardForm.TasksList.Handle;
      if listHandle <> 0 then begin
        for i := 0 to WizardForm.TasksList.Items.Count - 1 do begin
           itemHandle := GetListBoxItemHandle(listHandle, i); 
           MsgBox('ItemHandle: ' + IntToStr(itemHandle), mbInformation, MB_OK);
        end;
      end;
  end;
end;

The first and principal problem I'm having with the code above is with the line itemHandle := WindowFromPoint(itemPoint);, for some unexpected reason when calling that function the installer blocks / stops responding for some seconds until it crashes (the process terminates itself).

Maybe my WindowFromPoint signature is wrong defined?. I've also tried using a custom type definition of original Win32 POINT instead of directly using TPoint which I don't know at all how it is defined.


Update: I'm 99% convinced that the issue is about the function definitions or the POINT/TPoint structure definition, because whenever I call any of the these functions: WindowFromPoint, WindowFromPhysicalPoint, ChildWindowFromPoint, ChildWindowFromPointEx, RealChildWindowFromPoint with any value like for example an empty point (x=0, y=0) the installer just freezes and closes itself.

I'm using Inno Setup 6.3.3 Unicode.

Upvotes: 1

Views: 170

Answers (2)

ElektroStudios
ElektroStudios

Reputation: 20474

INTRODUCTION TO THE SOLUTION

The following solution consists to use an external programming language like VB.NET (or else C#), which offers easier, efficient and unrestricted interaction with the IAccessible interface and thus the elements in the TasksLists window.

Using this language I've wrote a function with name SetCheckedStateInTasksListItems that uses the IAccessible interface and related Windows API function AccessibleChildren to enumerate child elements within the TasksList control, specifically targeting checkboxes (AccessibleRole.CheckButton) and modifying their checked state based on the provided checked parameter.

The targeted checkboxes are clicked using mouse messages through SendMessage function.

Note that custom checkboxes that are disabled or hidden are ignored by default.


VB.NET SOURCE-CODE

Below I share the complete API written VB.NET. It may seem like a lot of code, but in reality there is a lot of documentation that makes it seem bigger. This API only contains what is strictly essential to carry out this solution.

Note: No external assemblies are required to deploy in Inno Setup, just our compiled .NET dll file, which should size around 14kb.

File Name: InnoHelperAPI.vb (InnoHelper.dll)

Imports Accessibility

Imports InnoHelper.WinAPI
Imports InnoHelper.WinAPI.Types
Imports NativeMethods = InnoHelper.WinAPI.NativeMethods

Imports System.ComponentModel
Imports System.Runtime.InteropServices
Imports System.Security
Imports System.Threading
Imports System.Windows.Forms

''' <summary>
''' Provides helper methods related to Inno Setup interactions.
''' </summary>
Public NotInheritable Class InnoHelperAPI

    Private Sub New()
    End Sub

#Region " Public (Export) Methods "

    ''' <summary>
    ''' Sets the checked state of all checkboxes in an Inno Setup tasks list.
    ''' </summary>
    ''' 
    ''' <remarks>
    ''' <para></para>
    ''' This function uses the <see cref="IAccessible"/> interface 
    ''' and related function <see cref="NativeMethods.AccessibleChildren"/> 
    ''' to enumerate child elements within the tasks list control, 
    ''' specifically targeting  checkboxes (<see cref="AccessibleRole.CheckButton"/>) 
    ''' and modifying their checked state based on the provided <paramref name="checked"/> parameter.
    ''' <para></para>
    ''' The targeted checkboxes are clicked using mouse messages through 
    ''' <see cref="NativeMethods.SendMessage"/> function.
    ''' <para></para>
    ''' <i>* Note that checkboxes that are disabled, hidden, and offscreen are ignored by default.</i>
    ''' </remarks>
    ''' 
    ''' <param name="tasksListHandle">
    ''' The handle to the tasks list window.
    ''' </param>
    ''' 
    ''' <param name="checked">
    ''' Specifies whether checkboxes should be checked or unchecked.
    ''' </param>
    ''' 
    ''' <returns>
    ''' The total amount of checkboxes whose state was changed.
    ''' </returns>
    <DllExport(CallingConvention.StdCall, ExportName:=NameOf(SetCheckedStateInTasksListItems))>
    Public Shared Function SetCheckedStateInTasksListItems(<MarshalAs(UnmanagedType.SysInt)> tasksListHandle As IntPtr,
                                                           <MarshalAs(UnmanagedType.Bool)> checked As Boolean
                                                           ) As <MarshalAs(UnmanagedType.I4)> Integer

        If tasksListHandle = IntPtr.Zero Then
            Throw New ArgumentNullException(paramName:=NameOf(tasksListHandle))
        End If

        Dim itemsCount As Integer =
            NativeMethods.SendMessage(tasksListHandle, Constants.LB_GETCOUNT,
                                      IntPtr.Zero, IntPtr.Zero).ToInt32()
        If itemsCount = Constants.LB_ERR Then
            Throw New Win32Exception(Marshal.GetLastWin32Error())
        ElseIf itemsCount = 0 Then
            Return 0
        End If

        Dim wizardFormHandle As IntPtr = NativeMethods.GetAncestor(tasksListHandle, Constants.GA_ROOT)
        If wizardFormHandle = IntPtr.Zero Then
            Throw New Win32Exception(Marshal.GetLastWin32Error())
        End If

        ' Makes the wizard form a top-most window on the screen until listbox items are processed.
        If Not NativeMethods.SetWindowPos(wizardFormHandle, New IntPtr(Constants.HWND_TOPMOST),
                                           0, 0, 0, 0, Constants.SWP_NOMOVE Or Constants.SWP_NOSIZE) Then
            Throw New Win32Exception(Marshal.GetLastWin32Error())
        End If

        Dim topItemIndex As IntPtr =
            NativeMethods.SendMessage(tasksListHandle, Constants.LB_GETTOPINDEX,
                                      IntPtr.Zero, IntPtr.Zero)

        Dim accContainer As IAccessible = Nothing
        Dim accContainerResult As Integer =
            NativeMethods.AccessibleObjectFromWindow(tasksListHandle, Constants.OBJID_CLIENT,
                                                     New Guid("618736E0-3C3D-11CF-810C-00AA00389B71"), accContainer)
        If accContainerResult <> 0 Then
            Marshal.ThrowExceptionForHR(accContainerResult)
        End If

        Dim childs As Object() = New Object(itemsCount - 1) {}
        Dim childsObtained As Integer
        Dim childsResult As Integer =
            NativeMethods.AccessibleChildren(accContainer, 0, itemsCount, childs, childsObtained)
        If childsObtained <> itemsCount OrElse childsResult <> 0 Then
            Marshal.ThrowExceptionForHR(childsResult)
        End If

        Dim cbChangedCount As Integer
        Dim isTopCbSelected As Boolean

        For i As Integer = 0 To (childsObtained - 1)
            ' Note: The index 'childIdx' is one-based, meaning first child starts from 1.
            Dim childIdx As Integer = CInt(childs(i))

            ' Ensures that the current child is visible in the listbox window.
            If NativeMethods.SendMessage(tasksListHandle, Constants.LB_SETTOPINDEX,
                                         New IntPtr(childIdx - 1), IntPtr.Zero).ToInt32() = Constants.LB_ERR Then
                Throw New Win32Exception(Marshal.GetLastWin32Error())
            End If

            Dim childRole As Integer = CInt(accContainer.accRole(childIdx))
            If childRole = AccessibleRole.CheckButton Then
                ' Sending LB_SETCURSEL message fixes a checkbox selection issue
                ' when wizard form is resized and listbox is scrolled.
                If Not isTopCbSelected Then
                    isTopCbSelected =
                        (NativeMethods.SendMessage(tasksListHandle, Constants.LB_SETCURSEL,
                                                   New IntPtr(childIdx - 1), IntPtr.Zero).ToInt32() <> Constants.LB_ERR)
                End If

                Dim cbStates As AccessibleStates = CType(accContainer.accState(childIdx), AccessibleStates)
                ' Ignore disabled or hidden checkboxes.
                If cbStates.HasFlag(AccessibleStates.Unavailable) OrElse
                   cbStates.HasFlag(AccessibleStates.Invisible) OrElse
                   cbStates.HasFlag(AccessibleStates.Offscreen) Then
                    Continue For
                End If

                Dim cbNeedsStateChange As Boolean =
                    (checked AndAlso Not cbStates.HasFlag(AccessibleStates.Checked)) OrElse
                    (Not checked AndAlso cbStates.HasFlag(AccessibleStates.Checked))

                If cbNeedsStateChange Then
                    Dim cbClientRect As Types.RECT
                    If NativeMethods.SendMessage(tasksListHandle, Constants.LB_GETITEMRECT,
                                                  New IntPtr(childIdx - 1), cbClientRect).ToInt32() = Constants.LB_ERR Then
                        Throw New Win32Exception(Marshal.GetLastWin32Error())
                    End If

                    Dim cbClientPoint As New Types.POINT(cbClientRect.Left + 1, cbClientRect.Top + 1)
                    InnoHelperAPI.SendMouseLeftButtonClickToWindow(tasksListHandle, cbClientPoint, msgSendDelayMs:=0)
                    cbChangedCount += 1
                End If
            End If
        Next i

        ' Restore listbox top-index item, and remove wizard window from top-most.
        If NativeMethods.SendMessage(tasksListHandle, Constants.LB_SETTOPINDEX,
                                     topItemIndex, IntPtr.Zero).ToInt32() = Constants.LB_ERR Then
            Throw New Win32Exception(Marshal.GetLastWin32Error())
        End If
        If Not NativeMethods.SetWindowPos(wizardFormHandle, New IntPtr(Constants.HWND_NOTOPMOST),
                                          0, 0, 0, 0, Constants.SWP_NOMOVE Or Constants.SWP_NOSIZE) Then
            Throw New Win32Exception(Marshal.GetLastWin32Error())
        End If

        Return cbChangedCount
    End Function

#End Region

#Region " Private Methods "

    ''' <summary>
    ''' <b>*** FOR INTERNAL USE ONLY ***</b>
    ''' <para></para>
    ''' Creates a LONG Param (LParam) value for a window message, from two <see cref="Integer"/> values.
    ''' <para></para>
    ''' You must call this method if you need to use negative values.
    ''' <para></para>
    ''' <seealso cref="Windows.Forms.Message.LParam"/>
    ''' </summary>
    '''
    ''' <remarks>
    ''' <see href="https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-makelparam"/>
    ''' </remarks>
    '''
    ''' <example> This is a code example.
    ''' <code language="VB.NET">
    ''' Dim lParam as IntPtr = MakeLParam(Integer.MaxValue, Integer.MaxValue)
    ''' </code>
    ''' </example>
    '''
    ''' <param name="lo">
    ''' The low-order <see cref="Integer"/> value.
    ''' </param>
    ''' 
    ''' <param name="hi">
    ''' The high-order <see cref="Integer"/> value.
    ''' </param>
    '''
    ''' <returns>
    ''' The resulting LParam value.
    ''' </returns>
    <DebuggerStepThrough>
    Friend Shared Function MakeLParam(lo As Integer, hi As Integer) As IntPtr
        Dim loBytes As Byte() = {BitConverter.GetBytes(lo)(0), BitConverter.GetBytes(lo)(1)}
        Dim hiBytes As Byte() = {BitConverter.GetBytes(hi)(0), BitConverter.GetBytes(hi)(1)}
        Dim combined As Byte() = loBytes.Concat(hiBytes).ToArray()

        Return New IntPtr(BitConverter.ToInt32(combined, 0))
    End Function

    ''' <summary>
    ''' <b>*** FOR INTERNAL USE ONLY ***</b>
    ''' <para></para>
    ''' Synchronously sends a left mouse button click on the specified coordinates 
    ''' relative to the client area of the specified window.
    ''' </summary>
    ''' 
    ''' <param name="hWnd">
    ''' A handle to the target window.
    ''' </param>
    ''' 
    ''' <param name="clientPt">
    ''' The coordinates within the client area of the window, 
    ''' where to send the mouse button click.
    ''' </param>
    ''' 
    ''' <param name="msgSendDelayMs">
    ''' Optional milliseconds to wait between sending WM_#BUTTON_DOWN + WM_#BUTTON_UP messages. 
    ''' Default value is zero (no wait).
    ''' </param>
    <DebuggerStepThrough>
    Friend Shared Sub SendMouseLeftButtonClickToWindow(hWnd As IntPtr, clientPt As POINT, Optional msgSendDelayMs As Integer = 0)

        Dim lParam As IntPtr = InnoHelperAPI.MakeLParam(clientPt.X, clientPt.Y)

        Dim msgLButtonDownResult As IntPtr =
            NativeMethods.SendMessage(hWnd, Constants.WM_LBUTTONDOWN, New IntPtr(Constants.MK_LBUTTON), lParam)
        If msgLButtonDownResult <> IntPtr.Zero Then
            Throw New Win32Exception(Marshal.GetLastWin32Error())
        End If

        If msgSendDelayMs <> 0 Then
            Thread.Sleep(msgSendDelayMs)
        End If

        Dim msgLButtonUpResult As IntPtr =
            NativeMethods.SendMessage(hWnd, Constants.WM_LBUTTONUP, IntPtr.Zero, lParam)
        If msgLButtonUpResult <> IntPtr.Zero Then
            Throw New Win32Exception(Marshal.GetLastWin32Error())
        End If

    End Sub

#End Region

End Class

''' <summary>
''' This class encapsulates functionality for interacting with the Windows operating system,
''' providing types, functions and constants defined in Windows API.
''' </summary>
Friend NotInheritable Class WinAPI

    Private Sub New()
    End Sub

    ''' <summary>
    ''' Provides constant values used across Windows API, 
    ''' such as object identifiers and window messages.
    ''' </summary>
    Friend NotInheritable Class Constants

        Private Sub New()
        End Sub

        ''' <remarks>
        ''' <see href="https://learn.microsoft.com/en-us/windows/win32/winauto/object-identifiers"/>
        ''' </remarks>
        Friend Const OBJID_CLIENT As Integer = &HFFFFFFFC

        ''' <remarks>
        ''' <see href="https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getancestor"/>
        ''' </remarks>
        Friend Const GA_ROOT As UInteger = &H2

        ''' <remarks>
        ''' <see href="https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-deferwindowpos"/>
        ''' </remarks>
        Friend Const HWND_TOPMOST As Integer = -&H1

        ''' <remarks>
        ''' <see href="https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-deferwindowpos"/>
        ''' </remarks>
        Friend Const HWND_NOTOPMOST As Integer = -&H2

        ''' <remarks>
        ''' <see href="https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowpos"/>
        ''' </remarks>
        Friend Const SWP_NOMOVE As Integer = &H2

        ''' <remarks>
        ''' <see href="https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowpos"/>
        ''' </remarks>
        Friend Const SWP_NOSIZE As Integer = &H1

        ''' <remarks>
        ''' <see href="https://learn.microsoft.com/en-us/windows/win32/controls/lb-getcount"/>
        ''' </remarks>
        Friend Const LB_GETCOUNT As Integer = &H18B

        ''' <remarks>
        ''' <see href="https://learn.microsoft.com/en-us/windows/win32/controls/lb-gettopindex"/>
        ''' </remarks>
        Friend Const LB_GETTOPINDEX As Integer = &H18E

        ''' <remarks>
        ''' <see href="https://learn.microsoft.com/en-us/windows/win32/controls/lb-settopindex"/>
        ''' </remarks>
        Friend Const LB_SETTOPINDEX As Integer = &H197

        ''' <remarks>
        ''' <see href="https://learn.microsoft.com/en-us/windows/win32/controls/lb-setcursel"/>
        ''' </remarks>
        Friend Const LB_SETCURSEL As Integer = &H186

        ''' <remarks>
        ''' Constant defined in <b>WinUser.h</b> file.
        ''' </remarks>
        Friend Const LB_ERR As Integer = -&H1

        ''' <remarks>
        ''' <see href="https://learn.microsoft.com/en-us/windows/win32/controls/lb-getitemrect"/>
        ''' </remarks>
        Friend Const LB_GETITEMRECT As Integer = &H198

        ''' <remarks>
        ''' <see href="https://learn.microsoft.com/en-us/windows/win32/inputdev/wm-lbuttondown#parameters"/>
        ''' </remarks>
        Friend Const MK_LBUTTON As Integer = &H1

        ''' <remarks>
        ''' <see href="https://learn.microsoft.com/en-us/windows/win32/inputdev/wm-lbuttondown"/>
        ''' </remarks>
        Friend Const WM_LBUTTONDOWN As Integer = &H201

        ''' <remarks>
        ''' <see href="https://learn.microsoft.com/en-us/windows/win32/inputdev/wm-lbuttonup"/>
        ''' </remarks>
        Friend Const WM_LBUTTONUP As Integer = &H202

    End Class

    ''' <summary>
    ''' Provides type definitions used across Windows API.
    ''' </summary>
    Friend NotInheritable Class Types

        Private Sub New()
        End Sub

        <StructLayout(LayoutKind.Sequential)>
        Friend Structure POINT
            Public X As Integer
            Public Y As Integer

            Public Sub New(x As Integer, y As Integer)
                Me.X = x
                Me.Y = y
            End Sub
        End Structure

        <StructLayout(LayoutKind.Sequential)>
        Friend Structure RECT
            Public Left As Integer
            Public Top As Integer
            Public Right As Integer
            Public Bottom As Integer

            Public Sub New(left As Integer, top As Integer, right As Integer, bottom As Integer)
                Me.Left = left
                Me.Top = top
                Me.Right = right
                Me.Bottom = bottom
            End Sub
        End Structure

    End Class

    ''' <summary>
    ''' Provides Platform Invoke (P/Invoke) definitions to enable interaction with 
    ''' native Windows API functions that are required by this program.
    ''' </summary>
    <SuppressUnmanagedCodeSecurity>
    Friend NotInheritable Class NativeMethods

        Private Sub New()
        End Sub

#Region " OleAcc.dll "

        ''' <remarks>
        ''' <see href="https://learn.microsoft.com/en-us/windows/win32/api/oleacc/nf-oleacc-accessibleobjectfromwindow"/>
        ''' </remarks>
        <DllImport("OleAcc.dll", SetLastError:=False)>
        Friend Shared Function AccessibleObjectFromWindow(hWnd As IntPtr,
                                                          objId As Integer,
                                                    ByRef refId As Guid,
                                              <Out> ByRef refAcc As IAccessible
        ) As Integer ' HRESULT
        End Function

        ''' <remarks>
        ''' <see href="https://learn.microsoft.com/en-us/windows/win32/api/oleacc/nf-oleacc-accessiblechildren"/>
        ''' </remarks>
        <DllImport("oleacc.dll", SetLastError:=False)>
        Friend Shared Function AccessibleChildren(accContainer As IAccessible,
                                                  startChild As Integer,
                                                  childsCount As Integer,
                                                  <Out> <MarshalAs(UnmanagedType.LPArray, ArraySubType:=UnmanagedType.Struct)>
                                                  childs As Object(),
                                            ByRef refChildsObtained As Integer
        ) As Integer ' HRESULT
        End Function

#End Region

#Region " User32.dll "

        ''' <remarks>
        ''' <see href="https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getancestor"/>
        ''' </remarks>
        <DllImport("user32.dll", SetLastError:=True)>
        Friend Shared Function GetAncestor(hWnd As IntPtr,
                                           flags As UInteger
        ) As IntPtr
        End Function

        ''' <remarks>
        ''' <see href="https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-sendmessage"/>
        ''' </remarks>
        <DllImport("User32.dll", SetLastError:=True)>
        Friend Shared Function SendMessage(hWnd As IntPtr,
                                           msg As Integer,
                                           wParam As IntPtr,
                                           lParam As IntPtr
        ) As IntPtr
        End Function

        ''' <remarks>
        ''' <see href="https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-sendmessage"/>
        ''' </remarks>
        <DllImport("User32.dll", SetLastError:=True)>
        Friend Shared Function SendMessage(hWnd As IntPtr,
                                           msg As Integer,
                                           wParam As IntPtr,
                                     ByRef refLParam As Types.RECT
        ) As IntPtr
        End Function

        ''' <remarks>
        ''' <see href="https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowpos"/>
        ''' </remarks>
        <DllImport("user32.dll", SetLastError:=True)>
        Friend Shared Function SetWindowPos(hWnd As IntPtr,
                                            hWndInsertAfter As IntPtr,
                                            x As Integer,
                                            y As Integer,
                                            width As Integer,
                                            height As Integer,
                                            flags As UInteger
        ) As Boolean
        End Function

#End Region

    End Class

End Class

VB.NET SOURCE-CODE COMPILATION INSTRUCTIONS

In order to properly compile the source-code above to build a Dynamic-Link Library (DLL) file that allow us to use in Inno Setup's Pascal Script, it is required to install the DllExport NuGet package by 3F in our Visual Studio project and follow the post-installation instructions, which basically is to run the DllExport.bat script file located at the top level of our VS solution directory, and click the 'installed' checkbox in the graphical interface shown after running the batch file:

DllExport

Note: Make sure to use the x86 architecture of the generated .NET dll file in Inno Setup.


USAGE EXAMPLE

  1. Put the (x86) compiled .NET dll file in the {tmp} folder of your Inno Setup project.
│ Inno_Script.iss
│
├───{app}
│       (app files here)
│
└───{tmp}
        InnoHelper.dll
  1. Import the function from the .NET dll file:
[Code]

function SetCheckedStateInTasksListItems(tasksListHandle: HWND; checked: Boolean): Integer;
  external 'SetCheckedStateInTasksListItems@files:InnoHelper.dll stdcall setuponly';
  1. Call the function as you wish:
procedure CheckAllCheckBoxesInTasksList();
var
  cbChangedCount: Integer;

begin
  cbChangedCount := SetCheckedStateInTasksListItems(WizardForm.TasksList.Handle, True);
  MsgBox('Changed checkboxes count: ' + IntToStr(cbChangedCount), mbInformation, MB_OK);

end;

DEMONSTRATION

enter image description here

Upvotes: 1

Martin Prikryl
Martin Prikryl

Reputation: 202514

I'm afraid that there's no API to check is an item is checkbox or radio button. Not even WinAPI would help as the individual items are not separate controls. The TasksList: TNewCheckListBox is plain List Box control. The checkboxes/radiobuttons are custom-drawn by Inno Setup.

As you have found out, the TNewCheckListBox implements accessibility/UI automation API. It can theoretically be used to query the checkboxes/radiobuttons. But implementing that API in Inno Setup is at the very least huge task, if possible at all.


What you can do:

  • You can detect the radio buttons by checking their caption or index
  • For larger amount of tasks, it might be worth implementing some preprocessor macro/tag that you could use in Tasks section to tag the radio buttons. For implementation, see the end of the answer.

Though for your specific task of implementing the "check all", you can avoid the need to specifically detect the checkboxes/radiobuttons. Instead you can:

  • Remember the current state
  • Check all
  • Look for ranges of unchecked items followed by checked one (= range of radio buttons)
  • Restore their previous selection

This should do (the implementation is currently limited to only enabled single-level items, but can be improved):

procedure CheckAllTasks;
var
  Count, I, CheckedI: Integer;
  TasksList: TNewCheckListBox;
  ItemsChecked: array of Boolean;
begin
  TasksList := WizardForm.TasksList;
  Count := TasksList.Items.Count;
  SetArrayLength(ItemsChecked, Count);

  // Remember the selection
  for I := 0 to Count - 1 do
  begin
    if (not TasksList.ItemEnabled[I]) or
       (TasksList.ItemLevel[I] > 0) or
       (TasksList.State[I] = cbGrayed) then
      RaiseException('Not supported item');

    ItemsChecked[I] := TasksList.Checked[I];
  end;

  // (Try to) Check all
  for I := 0 to Count - 1 do
  begin
    TasksList.Checked[I] := True;
  end;

  // Identify radio button ranges and restore their previous selection
  for I := 0 to Count - 1 do
  begin
    // The first item of the radio button range
    if not TasksList.Checked[I] then
    begin
      CheckedI := -1;
      // Look for the end of the range by finding the first selected item
      while (not TasksList.Checked[I]) and (I < Count) do
      begin
        // Check what radio button in the range was previously selected
        if ItemsChecked[I] then CheckedI := I;
        Inc(I);
      end;
      // Restore the selection
      if CheckedI >= 0 then
        TasksList.Checked[CheckedI] := True;
    end;
  end;
end;

Alternative implementation using Tasks "tagging":

#dim ExclusiveItems[100]
#define Items 0
#define Group

#define Exclusive() ExclusiveItems[Items] = 1, Items++, "exclusive"
#define NonExclusive() ExclusiveItems[Items] = 0, Items++, ""
#define StartGroup(Desc) Group = Desc, NonExclusive()

[Tasks]
#expr StartGroup("Supported versions")
Name: net9; Description: ".NET 9"; GroupDescription: {#Group}; Flags: {#NonExclusive}
Name: net8; Description: ".NET 8"; GroupDescription: {#Group}; Flags: {#NonExclusive}
Name: net6; Description: ".NET 6"; GroupDescription: {#Group}; Flags: {#NonExclusive}
#expr StartGroup("Out of Support Versions")
Name: net7; Description: ".NET 7"; GroupDescription: {#Group}; Flags: {#NonExclusive}
...
#expr StartGroup("System Tweaks")
Name: dis; Description: "Disable..."; GroupDescription: {#Group}; Flags: {#Exclusive}
Name: en; Description: "Enable"; GroupDescription: {#Group}; Flags: {#Exclusive}
[Code]
function IsExclusive(I: Integer): Boolean;
begin
  if WizardForm.TasksList.Items.Count <> {#Items} then
    RaiseException('Not all items were tagged');

  #define IsExclusive(I) \
    (I >= Items) ? \
      "" : ((ExclusiveItems[I] == 1 ? \
             "(I = " + Str(I) + ") or " : "") + IsExclusive(I + 1))
  Result := {#IsExclusive(0)}(1 = 2);
end;

Now you can use not IsExclusive(I) in your CheckAllTasks procedure.

Upvotes: 1

Related Questions