VirtualDesktopAccessor

repository·rust·Indexed 22 days ago

https://github.com/ciantic/virtualdesktopaccessor

A library and DLL (winvd) providing functions to access and control Windows 11 Virtual Desktop features. It enables external applications, such as AutoHotkey, to manage desktop lifecycles, move windows, pin applications, set desktop names and wallpapers, and register for virtual desktop change events. Requires Windows 11 24H2 26100.2605 or later.

Tokens
8.1K
Snippets
22
Records
32
Agent score
71%

What's inside virtualdesktopaccessor

  1. COM Reference Counting Rules for Developers

    rust

    When working with COM objects, follow these three fundamental rules for reference management:

    1. In Parameters (Caller to Callee): The caller maintains the reference for the duration of the method call. The callee (you) should not call AddRef or Release on these objects during the synchronous call.
    2. Out Parameters (Callee to Caller): The callee provides the object with a reference already taken. The caller owns the reference and is responsible for calling Release when finished.
    3. Copying Pointers: If you need to store a COM object pointer for use outside the immediate scope of a method call (e.g., in an async handler), you must manually call AddRef to create a copy and Release once the asynchronous work is complete.
  2. Download VirtualDesktopAccessor.dll

    rust

    To use the Virtual Desktop features in external applications like AutoHotkey, download the pre-compiled VirtualDesktopAccessor.dll from the official GitHub releases page.

    Requirements:

    • Windows 11 (requires at least 24H2 26100.2605).
    • Tested with 25H2 OS Build 26200.8117.
    https://github.com/Ciantic/VirtualDesktopAccessor/releases/
  3. How to use ComObjects for Virtual Desktop access

    rust

    The ComObjects struct is the primary interface for interacting with the Windows Virtual Desktop API. Because Virtual Desktop COM objects are sensitive to multi-threaded access, you must wrap your logic in the with_com_objects function. This ensures all COM calls are executed within a single thread, preventing instability.

    with_com_objects provides a reference to a thread-local ComObjects instance to your closure.

    use virtualdesktopaccessor::{with_com_objects, DesktopInternal};
    
    fn main() -> Result<(), Box<dyn std::error::Error>> {
        with_com_objects(|com| {
            let desktops = com.get_desktops()?;
            println!("Found {} desktops", desktops.len());
            Ok(())
        })?;
        Ok(())
    }
  4. Identify desktops using DesktopInternal

    rust

    The DesktopInternal enum is used to reference specific virtual desktops. It allows you to identify a desktop in three ways:

    • Index(u32): The zero-based index of the desktop.
    • Guid(GUID): The unique identifier of the desktop.
    • IndexGuid(u32, GUID): A combination of both the index and the GUID.

    Most ComObjects methods accept DesktopInternal as an argument, allowing you to switch, move windows, or rename desktops using any of these identifiers.

    // Example of creating a DesktopInternal from an index
    let my_desktop = DesktopInternal::Index(0);
  5. Use ComIn for safe COM input parameters

    rust

    In this project's Rust bindings, ComIn<T> is a wrapper used for COM objects passed as input parameters. It ensures the COM object's lifetime is maintained for the duration of the function call without requiring the caller to manually manage AddRef or Release during the call.

    When you receive a COM object as an out parameter (e.g., *mut Option<T>), you should wrap it in ComIn::new(&object) before passing it to a function that expects a ComIn<T> input.

    // Example: Passing an obtained desktop to a switch function
    let mut desktop: Option<IVirtualDesktop> = None;
    get_current_desktop(&mut desktop);
    
    if let Some(desktop) = desktop {
        // Wrap the owned desktop in ComIn to pass it as an input parameter
        switch_desktop(ComIn::new(&desktop));
    }
  6. Use Desktop indices or GUIDs with winvd

    rust

    The winvd crate allows you to interact with Windows Virtual Desktops using either a zero-based integer index or a unique GUID. Any function that accepts a Desktop type can be satisfied by passing an index or a GUID object.

    Common tasks include:

    • Accessing a desktop by index: get_desktop(index)
    • Accessing a desktop by GUID: get_desktop(GUID(uuid_string))
    • Switching desktops: switch_desktop(index_or_guid)
    // Get first desktop name by index
    let name = get_desktop(0).get_name();
    
    // Get second desktop name by index
    let name = get_desktop(1).get_name();
    
    // Get desktop name by GUID
    let name = get_desktop(GUID(some_uuid)).get_name();
    
    // Switch to fifth desktop by index
    switch_desktop(4);
  7. Handle cookie reuse in IVirtualDesktopNotification::register during explorer.exe crashes

    rust

    When using IVirtualDesktopNotification::register, be aware that the Windows shell may reuse cookies if explorer.exe crashes and restarts.

    If you attempt to register a new notification without unregistering the previous one after an explorer crash, the new registration might receive the same cookie ID as the old one. Consequently, attempting to unregister the 'old' cookie will actually unregister the 'new' one.

    Best Practice: Always unregister the existing notification/cookie before attempting to register a new one to ensure clean state management.

  8. Use VirtualDesktopAccessor with AutoHotkey

    rust

    You can use the DLL to automate virtual desktop switching and window management in AutoHotkey. Examples are provided for both AutoHotkey V1 and V2 in the repository.

    • For AutoHotkey V1, refer to example.ahk.
    • For AutoHotkey V2, refer to example.ah2.
  9. Implement IVirtualDesktopNotification for Windows Shell events

    rust

    The IVirtualDesktopNotification interface is used to receive notifications from the Windows shell (e.g., when a virtual desktop is created or destroyed). Because this is an interface implemented by your code that the Windows shell calls, you are the callee.

    According to COM guidance, when the shell passes objects to your implementation as In parameters, the shell has already taken a reference. You must not call Release() on these objects. In the context of windows-rs, you must use the ComIn<T> wrapper for these parameters. Using raw COM objects instead of ComIn<T> will cause the objects to call Release() during drop, leading to memory corruption or crashes after repeated desktop switches.

    pub unsafe trait IVirtualDesktopNotification: IUnknown {
        pub unsafe fn virtual_desktop_created(
            &self,
            monitors: ComIn<IObjectArray>,
            desktop: ComIn<IVirtualDesktop>,
        ) -> HRESULT;
    
        pub unsafe fn virtual_desktop_destroy_begin(
            &self,
            monitors: ComIn<IObjectArray>,
            desktop_destroyed: ComIn<IVirtualDesktop>,
            desktop_fallback: ComIn<IVirtualDesktop>,
        ) -> HRESULT;
        // ...
    }
  10. Reference of exported DLL functions

    rust

    The VirtualDesktopAccessor.dll provides several functions to interact with Windows 11 Virtual Desktops.

    Error Handling: All functions return -1 in case of an error.

    Note on Windows 11 specific features: Some functions (like SetDesktopName, CreateDesktop, etc.) are exclusive to Windows 11.

    fn GetCurrentDesktopNumber() -> i32
    fn GetDesktopCount() -> i32
    fn GetDesktopIdByNumber(number: i32) -> GUID // Untested
    fn GetDesktopNumberById(desktop_id: GUID) -> i32 // Untested
    fn GetWindowDesktopId(hwnd: HWND) -> GUID
    fn GetWindowDesktopNumber(hwnd: HWND) -> i32
    fn IsWindowOnCurrentVirtualDesktop(hwnd: HWND) -> i32
    fn MoveWindowToDesktopNumber(hwnd: HWND, desktop_number: i32) -> i32
    fn GoToDesktopNumber(desktop_number: i32) -> i32
    fn SetDesktopName(desktop_number: i32, in_name_ptr: *const i8) -> i32  // Win11 only
    fn GetDesktopName(desktop_number: i32, out_utf8_ptr: *mut u8, out_utf8_len: usize) -> i32 // Win11 only
    fn RegisterPostMessageHook(listener_hwnd: HWND, message_offset: u32) -> i32
    fn UnregisterPostMessageHook(listener_hwnd: HWND) -> i32
    fn IsPinnedWindow(hwnd: HWND) -> i32
    fn PinWindow(hwnd: HWND) -> i32
    fn UnPinWindow(hwnd: HWND) -> i32
    fn IsPinnedApp(hwnd: i32) -> i32
    fn PinApp(hwnd: i32) -> i32
    fn UnPinApp(hwnd: i32) -> i32
    fn IsWindowOnDesktopNumber(hwnd: HWND, desktop_number: i32) -> i32
    fn CreateDesktop() -> i32 // Win11 only
    fn RemoveDesktop(remove_desktop_number: i32, fallback_desktop_number: i32) -> i32 // Win11 only
  11. Manage Virtual Desktop Lifecycle

    rust

    Functions to create new desktops or remove existing ones.

    // Create a new virtual desktop. 
    // Returns the index of the newly created desktop, or -1 on error.
    int32_t CreateDesktop();
    
    // Remove a virtual desktop.
    // remove_desktop_number: The index of the desktop to remove.
    // fallback_desktop_number: The index of the desktop to switch to if the removal leaves no desktops.
    // Returns 1 on success, -1 on error.
    int32_t RemoveDesktop(int32_t remove_desktop_number, int32_t fallback_desktop_number);
  12. Stop the DesktopEventThread listener

    rust

    If you need to stop the background event listener before the DesktopEventThread object is dropped, call the stop() method. This sends a quit message to the listener thread and waits for the thread to finish (joins it).

    // Explicitly stop the listener and join the thread
    listener_thread.stop().expect("Failed to stop listener thread");