winapirustinno-setupui-automationwindows-rs

Communicate with a TNewCheckBoxList of an Inno Setup in Rust


I am trying to automate the installation of an Inno Setup, I have already been able to communicate with a Button and a Checkbox but I have been stuck for communication with a TNewCheckListBox. If I understood the docs correctly, each TNewCheckBox are created dynamically but I haven't been able to communicate at all with them nor even see them.

I have found a way to get their text, however I cannot get to click on them or to setcheck. I tried the implementation of the CLBN_CHKCHANGE but to no avail.

There is a part of the code :

pub fn check_checklistbox_states() -> Vec<HWND> {
    let mut checkbox_handles: Vec<HWND> = Vec::new();

    if let Some(checklistbox_hwnd) = find_checklistbox() {
        unsafe {
            let count = SendMessageW(checklistbox_hwnd, LB_GETCOUNT, WPARAM(0), LPARAM(0));
            
            let count = count.0 as i32 as usize;
            println!("{:#?}", count);
            for index in 0..count {
                if let Some(text) = get_item_text(checklistbox_hwnd, index) {
                    let state = SendMessageW(checklistbox_hwnd, LB_GETITEMDATA, WPARAM(index as usize), LPARAM(0));
                    let checked = state.0 as i32;
                    let state_text = if checked != 0 {
                        "Checked"
                    } else {
                        "Unchecked"
                    };
                    let debug_checkbox = checklistbox_hwnd.0 as i32;
                    println!("Item {}: {} - {} - {}", index + 1, text, state_text, debug_checkbox);

                    // Example to toggle the check state
                    let new_check_state = if checked != 0 { 0 } else { 1 }; // Toggle between checked (1) and unchecked (0)
                    
                    if (index >= 1) {
                        // set_check(checklistbox_hwnd, index as c_int, new_check_state, BS_CHECKBOX as u32);
                        let _ = PostMessageW(checklistbox_hwnd, CLBN_CHKCHANGE, WPARAM(1), LPARAM(0));
                        println!("sent");
                    }
                    println!("should have clicked");
                    checkbox_handles.push(checklistbox_hwnd);  // This is the handle of the list box, not the item
                }
            }
        }
    } else {
        println!("TNewCheckListBox not found.");
    }

    checkbox_handles
}

Here I try to get the the checkboxes handles but it does not work, I defined the CLBN_CHKCHANGE previously through this constant: const CLBN_CHKCHANGE: u32 = 40; and I also tried this const CLBN_CHKCHANGE: u32 = 0x00000040;

I tried a more complex usage to set_check similarly to the CCheckListBox::SetCheck(int nIndex, int nCheck) found inside the winscp source code.

There it is, I apologize if the code is bad because I am still new to Rust and the conversion of some C++ code that contains the windows API is difficult and it is not full.

I defined the structs here :

const LB_ERR: LRESULT = windows::Win32::Foundation::LRESULT(-1);


#[repr(C)]
struct AfxCheckData {
    m_n_check: c_int,
    m_b_enabled: BOOL,
    m_dw_user_data: DWORD,
}

impl AfxCheckData {
    fn new() -> Self {
        Self {
            m_n_check: 0,
            m_b_enabled: TRUE,
            m_dw_user_data: 0,
        }
    }
}

Then I tried to to make the invalidate_check like the CCheckListBox::InvalidateCheck(int nIndex) to handle errors if I understood correctly.

   fn invalidate_check(listbox_hwnd: HWND, n_index: i32) {
        unsafe {
            let mut rect: RECT = zeroed();
            if SendMessageW(listbox_hwnd, LB_GETITEMRECT, WPARAM(n_index as usize), LPARAM(&mut rect as *mut RECT as isize)) != LB_ERR {

                    InvalidateRect(listbox_hwnd.0 as *mut winapi::shared::windef::HWND__, &rect as *const RECT, 1);
 
            }
        }
    }
    

And finally created the set_check function :

fn set_check(listbox_hwnd: HWND, n_index: i32, n_check: i32, m_n_style: u32) {
    if n_check == 2 && (m_n_style == BS_CHECKBOX as u32 || m_n_style == BS_AUTOCHECKBOX as u32) {
        return;
    }

    unsafe {
        let l_result = SendMessageW(listbox_hwnd, LB_GETITEMDATA, WPARAM(n_index as usize), LPARAM(0));
        println!("{:#?}",l_result);
        if l_result != LB_ERR {

        let p_state: *mut AfxCheckData = if l_result == LRESULT(0) {
            Box::into_raw(Box::new(AfxCheckData::new()))
        } else {
            // unsafe { transmute(l_result) }
            Box::into_raw(Box::new(AfxCheckData::new()))
        };
            (*p_state).m_n_check = n_check;

            if SendMessageW(listbox_hwnd, LB_SETITEMDATA, WPARAM(n_index as usize), LPARAM(p_state as isize)) == LB_ERR {
                eprintln!("Failed to set item data.");
            }

            invalidate_check(listbox_hwnd, n_index);
        }
    }}

I had to remove transmute because it caused STATUS_ACCESS_VIOLATION errors. I also didn't understand the exact meaning of transmute so I decided not to use it.

Also, if I do this :

let _ = PostMessageW(checklistbox_hwnd, LBN_DBLCLK, WPARAM(1), LPARAM(0));

The checkboxes from the checklistbox disappear.

Finally, this is the results I get from the function :

Item 1: Main game files - Unchecked - 198748
should have clicked
Item 2: Update DirectX - Checked - 198748
LRESULT(
    400048848,
)
sent
should have clicked

The checked and unchecked do not work since even if it is unchecked it will send check.

Here is a picture of the TNewCheckListBox : Picture of the TNewCheckListBox

I hope there is an easier answer for all of that.

Thank you for reading.


Solution

  • So as proposed by IInspectable, I decided to use uiautomation-rs and I then realized that the TNewCheckListBox creates one or multiple CheckBox that do not support any Pattern given by uiautomation-rs nor winapi nor windows-rs to my knowledge.

    For the TogglePattern it gave an Error :

    Failed to toggle the state: Error { code: -2146233079, message: "" }

    Which is a System.InvalidOperationException, thus probably meaning it does not support direct toggling through TogglePattern.

    For the InvokePattern it was just not supported directly.

    The CheckBox also miss the HWND and Classname.

    It is uniquely accessible by the IAccessible and identified by IAccIdentity.

    That would be too complicated and viewing the state of the windows api for rust right now I was too scared to go into that rabbit hole.

    However, on the forum AutoIt I found someone mentioning directly sending the space key.

    The answer was way easier than I thought it would have been.

    So I just did that, there is the code :

    
    
    pub mod windows_ui_automation {
        use uiautomation::{UIAutomation, UIElement};
        use uiautomation::types::UIProperty::{ClassName, NativeWindowHandle, ToggleToggleState};
    
    
        pub fn get_checklistbox() -> Result<UIElement, uiautomation::errors::Error>{
            let automation = UIAutomation::new().unwrap();
    
    
            let checklistbox_element = automation.create_matcher().classname("TNewCheckListBox");
        
            
            let sec_elem = checklistbox_element.find_first();
        
            // println!("{:#?}", sec_elem.unwrap());
    
            sec_elem
        }
    
        pub fn get_checkboxes_from_list() {
            let automation = UIAutomation::new().unwrap();
            let walker = automation.get_control_view_walker().unwrap();
        
            let checklistbox_elem = match get_checklistbox() {
                Ok(elem) => elem,
                Err(e) => {
                    println!("Failed to find checklist box: {}", e);
                    return; // Or handle the error appropriately but for this example no.
                }
            };
        
            if let Ok(child) = walker.get_first_child(&checklistbox_elem) {
                match child {
                    ref ch => {
                        process_element(ch.clone());
                    }
                }
        
                let mut next = child;
                while let Ok(sibling) = walker.get_next_sibling(&next) {
    
                    match sibling{
                        ref sib => {
                            process_element(sib.clone());
                        }
                    }
                    next = sibling;
                }
            }
        }
        
        fn process_element(element: UIElement) {
            match element {
                ref el => {
                    
                    let spec_classname = el.get_property_value(ClassName).unwrap(); // NULL
                    let spec_proc_handle = el.get_property_value(NativeWindowHandle).unwrap();
        
                    let spec_toggle_toggle_state = el.get_property_value(ToggleToggleState).unwrap(); // NULL
                    let spec_control_type = el.get_control_type().unwrap();
        
                    let spec_text_inside = el.get_help_text().unwrap();
                    println!(
                        "ClassName = {:#?} and HWND = {:#?} and ControlType = {:#?} and TTState = {:#?} and Help Text = {:#?}",
                        spec_classname.to_string(),
                        spec_proc_handle.to_string(),
                        spec_control_type.to_string(),
                        spec_toggle_toggle_state.to_string(),
                        spec_text_inside
                    );
        
                    match el.send_keys(" ", 0) {
                        Ok(_) => println!("Space key sent to element."),
                        Err(e) => eprintln!("Failed to send space key: {:?}", e),
                    }
                }
            }
        }
    
    }