How can I programmatically click a TNewCheckBox
control in another application using C++?
It is an Inno Setup installer that I am trying to communicate with.
This is my already existent code, it allows me to get the handle of the TNewCheckBox
and of a few others.
It checks whether or not I have a certain amount of RAM, and if I have it or less, it will then check the checkbox.
I managed to get the checkbox handle and send a message using SendMessage
, I also tried PostMessage
and SendDlgItemMessage
but to no avail since it still refuses to check it.
I have searched on the Internet but there is nothing related to this.
There is my code:
#include <Windows.h>
#include <iostream>
HWND EnumChildWindowsRecursive(HWND hwndParent)
{
HWND hwndChild = FindWindowExW(hwndParent, NULL, NULL, NULL);
if (hwndChild == NULL)
{
return NULL;
}
wchar_t szClassName[256];
GetClassNameW(hwndChild, szClassName, sizeof(szClassName) / sizeof(wchar_t));
wchar_t szWindowText[256];
GetWindowTextW(hwndChild, szWindowText, sizeof(szWindowText) / sizeof(wchar_t));
std::wcout << L"Handle: " << hwndChild << L", Class Name: " << szClassName << L", Text: " << szWindowText << std::endl;
if (_wcsicmp(szClassName, L"TNewCheckBox") == 0 && _wcsicmp(szWindowText, L"Limit installer to 2 GB of RAM usage") == 0)
{
return hwndChild;
}
HWND hwndSubChild = EnumChildWindowsRecursive(hwndChild);
if (hwndSubChild != NULL)
return hwndSubChild;
return EnumChildWindowsRecursive(FindWindowExW(hwndParent, hwndChild, NULL, NULL));
}
void ClickCheckBox(HWND hwndCheckBox)
{
SendMessage(hwndCheckBox, BM_CLICK, 0, 0);
}
int GetSystemRAM()
{
MEMORYSTATUSEX statex;
statex.dwLength = sizeof(statex);
if (GlobalMemoryStatusEx(&statex))
{
DWORDLONG totalMemoryInBytes = statex.ullTotalPhys;
int totalMemoryInGB = static_cast<int>(totalMemoryInBytes / (1024 * 1024 * 1024));
return totalMemoryInGB;
}
return -1;
}
int main()
{
HWND hwnd = FindWindowW(NULL, L"Setup - Test Installer version 1.0");
if (hwnd == NULL)
{
std::cout << L"Window title not found" << std::endl;
return 0;
}
std::cout << L"Found window with title" << std::endl;
HWND hwndCheckBox = EnumChildWindowsRecursive(hwnd);
if (hwndCheckBox == NULL)
{
std::cout << "Checkbox not found" << std::endl;
return 0;
}
std::cout << "Checkbox found" << std::endl;
DWORD ramSizeGB = GetSystemRAM();
if (ramSizeGB <= 8)
{
std::cout << "8 GB of RAM or less" << std::endl;
std::cout << "Checkbox handle: 0x" << std::hex << reinterpret_cast<uintptr_t>(hwndCheckBox) << std::endl;
ClickCheckBox(hwndCheckBox);
std::cout << "Checkbox clicked" << std::endl;
}
else
{
std::cout << "More than 8 GB of RAM" << std::endl;
}
return 0;
}
It even says that the checkbox is found and is clicked but nothing is seen on the setup.
Here is a basic Inno Setup so that we can reproduce the same issue I find myself in.
[Setup]
AppName=Test Installer
AppVersion=1.0
DefaultDirName={pf}\TestInstaller
OutputDir=Output
OutputBaseFilename=TestInstallerSetup
[Components]
Name: "CheckBox"; Description: "Include checkbox functionality"; Types: full custom; Flags: fixed
[Code]
var
CheckBox: TNewCheckBox;
procedure InitializeWizard;
begin
CheckBox := TNewCheckBox.Create(WizardForm);
CheckBox.Parent := WizardForm;
CheckBox.Left := 8;
CheckBox.Top := WizardForm.ClientHeight - 100;
CheckBox.Caption := 'Enable checkbox functionality';
CheckBox.Checked := True;
end;
procedure CurStepChanged(CurStep: TSetupStep);
begin
if CurStep = ssPostInstall then
begin
if CheckBox.Checked then
begin
MsgBox('Checkbox is checked', mbInformation, MB_OK);
end
else
begin
MsgBox('Checkbox is unchecked', mbInformation, MB_OK);
end;
end;
end;
Now, if you start the Inno Setup and then the C++ script you will be able to reproduce what I have.
I also replaced this part of my code
ClickCheckBox(hwndCheckBox);
std::cout << "Checkbox clicked" << std::endl;
to this
if (SendMessage(hwndCheckBox, BM_GETCHECK, 0, 0) == BST_CHECKED)
{
std::cout << "Checkbox clicked successfully" << std::endl;
}
else
{
std::cout << "Failed to click the checkbox" << std::endl;
}
And the checkbox state is always negative even when I manually check it and rerun the script, I will still receive "Failed to click the checkbox".
My code and the installer are both separated and thus this can be tried on any other Inno Installer that contain a TNewCheckBox
as long as you change the title to the corresponding one.
The code is run after the installer is run and still open.
There is no direct relation between them, the code does not run the installer, the installer still have to be run manually and once it is open and on the right window that contain the TNewCheckBox
, we can then start the script separately as long as the title used is the same.
The only interaction between them is when trying to get the handle of the TNewCheckBox
, sending a message and trying to get the state of it.
Your code actually works for me.
Most likely the problem is that you run the installer with Administrator privileges, while you run your console application without them.
For obvious security reasons, a process that does not have Administrator privileges, cannot control another process that does have Administrator privileges.
See also the comment by @HansPassant at Sending BM_CLICK message to Windows 10 application not working.
One trivial explanation is that the installer runs elevated, like all installers do, but your automation app does not. A non-elevated app cannot commandeer an elevated one, UAC was primarily invented to stop shatter attacks. Not otherwise different on Win7 btw.
And another comment therein by @DavidHeffernan:
You are going about this the wrong way. Don't fake input. Improve your installer so that it can accept command line arguments and perform unattended or silent installation.
Personally, I'd make your app request Administrator privileges and make it itself run the installer (implicitly with the Administrator privileges).