Reputation: 20474
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:
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
Reputation: 20474
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.
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
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:
Note: Make sure to use the x86 architecture of the generated .NET dll file in Inno Setup.
{tmp}
folder of your Inno Setup project.│ Inno_Script.iss
│
├───{app}
│ (app files here)
│
└───{tmp}
InnoHelper.dll
[Code]
function SetCheckedStateInTasksListItems(tasksListHandle: HWND; checked: Boolean): Integer;
external 'SetCheckedStateInTasksListItems@files:InnoHelper.dll stdcall setuponly';
procedure CheckAllCheckBoxesInTasksList();
var
cbChangedCount: Integer;
begin
cbChangedCount := SetCheckedStateInTasksListItems(WizardForm.TasksList.Handle, True);
MsgBox('Changed checkboxes count: ' + IntToStr(cbChangedCount), mbInformation, MB_OK);
end;
Upvotes: 1
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:
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:
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