How to use Clipboard.GetContents from winrt using the windows crate in 2023

So I have this code here, that loads the clipboard data and it's drop effect from winrt, and the code compiles just fine, but when I try to run it, it just hangs.

match Clipboard::GetContent() {
    Ok(clipboard_content) =>
        match clipboard_content.RequestedOperation() {
            Ok(requested_operation) =>
                match clipboard_content.GetStorageItemsAsync() {
                    Ok(storage_items_handle) =>
                        match storage_items_handle.get() { // The code hangs here
                            Ok(storage_items) =>
                                match storage_items.First() {
                                    Ok(storage_items_iter) =>
                                        Ok((
                                            match requested_operation.0 {
                                                5 => DropEffect::Copy,
                                                2 => DropEffect::Move,
                                                _ => DropEffect::default(),
                                            },
                                            storage_items_iter
                                                .filter_map(|storage_item| {
                                                    match storage_item.Path() {
                                                        Ok(storage_item_path) =>
                                                            FileSystemLocation::load_from_string(
                                                                file_systems_handler,
                                                                storage_item_path.to_string()
                                                            ).ok(),
                                                        Err(_) => None,
                                                    }
                                                })
                                                .collect(),
                                        )),
                                    Err(err) => Err(err),
                                }
                            Err(err) => Err(err),
                        }
                    Err(err) => Err(err),
                }

            Err(err) => Err(err),
        }
    Err(err) => Err(err),
}

Now, I'm fairly sure of what the problem is, since I found a couple of issues on github relating to this very problem:

Async function calls that require UI thread such as Clipboard API hang · Issue #317 · microsoft/windows-rs (github.com)
how to use Clipboard.GetContent in winrt-rs · Issue #187 · microsoft/windows-rs (github.com)

But my problem is that both of these threads are from 2020, both of them are closed, and neither of them actually offer any real solutions.

So, now in 2023 I would've thought this issue would be fixed, so what is the proper way of implementing a way to get clipboard data using winrt?

Incase it helps anyone, this is my windows crate version

windows = { version = "0.51.1", features = [
    "ApplicationModel_DataTransfer",
    "Foundation_Collections",
    "Storage",
] }

As it says in that first issue, many Windows APIs require you to pump the message thread, which unfortunately doesn't interact well with standard async executors. A quick search on lib.rs makes irondash_run_loop — Rust GUI library // Lib.rs look like a possible ready solution?

It's not really an issue with the windows crate as such: it's simply generically projecting the Windows API.


Also, not really relevant to your question, but you'll probably be happier using the ? operator or Result::and_then

2 Likes

Yeah I know that my code is very nested without ? operators, I'll change it at some point.

The problem is that I'm still new to the windows api, so I don't even know what you mean by to pump the message thread.

Any chance you could just provide me with a very simple example of how I would use the irondash_run_loop crate, then I can go from there?

Not having used it (I normally have a a UI running), it seems like you can simply call RunLoop::spawn(), passing in your future?

Eg:

RunLoop:: current().spawn(get_clipboard())

...

async fn get_clipboard() -> Result<String> {
  ... your code above
}

Yeah I'll try that later when I can get to my laptop, but what exactly do you mean by "I normally have a a UI running" because I still don't get exactly what a UI thread is.

It depends on the OS exactly, but the short version is "a thread that's allowed to talk to UI", and almost always requires you to "pump messages", essentially run a loop that fetches messages from the OS and dispatches them to UI objects (in Windows this is an explicit loop, in others it's normally just a single function call). This is so the OS has a way to coordinate running it's own code with your own, and causes the users UI interaction to be handled in order.

Many non-visible APIs are still considered to be "UI", including input processing and anything to do with the user session, generally for that handling things in order reason.

This caught my interest, so I had a look at it.

These clipboard APIs are particularly tricky to use out of WinRT async APIs, and .get() is never going to work whether or not the thread is a UI thread, because it blocks and waits for results on the only thread that can generate results. The futures version with storage_items_handle.await might work if you could find the right executor.

I was able to get this to work using winit and calling .SetCompleted directly. The error handling in this example is terrible, though:

use windows::{
    ApplicationModel::DataTransfer::Clipboard,
    Foundation::{AsyncOperationCompletedHandler, Collections::IVectorView},
    Storage::IStorageItem,
};
use winit::{event::Event, event_loop::EventLoopBuilder, window::WindowBuilder};

#[derive(Debug)]
struct ClipboardEvent;

fn main() {
    let event_loop = EventLoopBuilder::<ClipboardEvent>::with_user_event()
        .build()
        .unwrap();
    let _window = WindowBuilder::new()
        .with_visible(false)
        .build(&event_loop)
        .unwrap();

    let event_loop_proxy = event_loop.create_proxy();
    let contents = Clipboard::GetContent().unwrap();
    let async_op = contents.GetStorageItemsAsync().unwrap();
    async_op
        .SetCompleted(&AsyncOperationCompletedHandler::new(
            move |info, _status| {
                let items: IVectorView<IStorageItem> = info.unwrap().GetResults().unwrap();
                for item in items.First().unwrap() {
                    println!("{}", item.Path().unwrap());
                }
                event_loop_proxy.send_event(ClipboardEvent).unwrap();
                Ok(())
            },
        ))
        .unwrap();

    event_loop
        .run(|event, elwt| match event {
            Event::UserEvent(ClipboardEvent) => elwt.exit(),
            _ => {}
        })
        .unwrap();
}

Ah yes, you would also need to replace the
.get() with .await, I failed to actually say that! But otherwise with that it should work, it's essentially the same as your example without the extra user event.

Did you need the window?

I checked and no, I don't need to create my own window; winit creates a hidden one itself for processing messages, including the COM tasks used by the clipboard API, and it does a better job.

Hey just wondering do either of you know how to listen to the Clipboard.ContentChanged event in rust?

I know I probably should be able to figure this out for myself, but once again winrt is just being really annoying for me and won't work. I gotta say, it's really not easy trying to debug things when it comes to the windows api.

That turns out to be quite easy:

use windows::{ApplicationModel::DataTransfer::Clipboard, Foundation::EventHandler};
use winit::event_loop::{ControlFlow, EventLoop};

fn main() {
    let event_loop = EventLoop::new().unwrap();
    let _registration = Clipboard::ContentChanged(&EventHandler::new(|_sender, _e| {
        println!("clipboard changed");
        Ok(())
    })).unwrap();
    event_loop.set_control_flow(ControlFlow::Wait);
    event_loop.run(|_event, _elwt| {}).unwrap();
}

I had a bit of trouble getting the right parameter type for ContentChanged(), so I split it out into different lines to get better error message, like let handler = EventHandler::new(...).

And so, I was just a bit confused what I can do with _sender and _e, like is _e is a DataPackageView object? similar to how Clipboard::GetContent() returns a DataPackageView. I'm honestly just wondering since that would mean that my app wouldn't be slowed down by yet another asynchronous request to Clipboard::GetContent().

Also one more thing, aren't you supposed to call RemoveClipboardChanged, or is it automatically cleaned up when event handler is dropped?

just want to mention, winrt is a different API set than the traditional win32 APIs. winrt is mainly for universal windows apps (a.k.a. uwp), although a subset of the APIs are available for desktop programs too.

if you are authorizing an winrt app (most notably, you are using the ApplicationModel APIs), you should have no problem consuming the Clipboard API.

if, however, you are trying to incorporate the winrt Clipboard API into a program without the ApplicationModel framework, you'll very likely run into mysterious errors.

for traditional win32 desktop programs, you'd better use the win32 api, such as the WM_CLIPBOARDUPDATE message.

but either way, you must have a message queue to receive certain os notifications, be it managed by the winrt ApplicationModel, or created by User/GDI APIs such as GetMessage(), or through a GUI library (such as winit) as suggested above.

see:

1 Like

The examples I found in C++ didn't use the event handler parameters and just called GetContent(), so I didn't look into what _e was. Checking now, it looks like both parameters are None in this case.

I didn't bother writing code to remove the event handler, since I didn't write a way for the event loop to exit this time.

:100: to this. To be honest, I was surprised that all I needed was a vague knowledge of how Single-Threaded Apartments work to know why .get() was hanging in the first place, and that nothing else went wrong.

I believe there's a push to get the winrt stuff generally available, they used to have some confusing name in the docs to say what works in win32 but it looks like that's disappeared now?

It's the package identity stuff that will really bite you: if you're in an MSIX package you're in pretty good shape otherwise it seems to mostly be stuff that needs a CoreWindow/View.

This is a surprisingly helpful looking page: https://learn.microsoft.com/en-us/windows/apps/desktop/modernize/desktop-to-uwp-supported-api

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.