Last time, we learned why the clipboard history service doesn’t capture rapid changes to clipboard contents. This was a problem for a program whose express purpose was to “load up” the clipboard history. How can that program check that their change made it into the history before changing the clipboard again?
The secret is the Clipboard.
event, which tells you that there has been a change to the clipboard history. It could be that a new item was added, or that the user deleted an item from the history, or that new history items roamed to the system, or that the user decided to clear all of the history.
For simplicity, we’ll assume that the only source of clipboard history changes is the ingestion of new clipboard data that our program added. (If you wanted to be sure, you can look at the clipboard history and see if your string is in it.)
// All error checking elided for expository purposes #include <windows.h> #include <winrt/Windows.ApplicationModel.DataTransfer.h> #include <winrt/Windows.Foundation.h> #include <winrt/Windows.System.h> namespace winrt { using namespace winrt::Windows::ApplicationModel::DataTransfer; using namespace winrt::Windows::Foundation; using namespace winrt::Windows::System; } void SetClipboardText(HWND hwnd, PCWSTR text) { OpenClipboard(hwnd); EmptyClipboard(); auto size = sizeof(wchar_t) * (1 + wcslen(text)); auto clipData = GlobalAlloc(GMEM_MOVEABLE, size); auto buffer = (LPWSTR)GlobalLock(clipData); strcpy_s(buffer, size, text); GlobalUnlock(clipData); SetClipboardData(CF_UNICODETEXT, clipData)); CloseClipboard(); } // Put these strings in the clipboard history for quick access. static constexpr PCWSTR messages[] = { L"314159", // the bug number we want to edit L"e83c5163316f89bfbde7d9ab23ca2e25604af290", // the commit to link the bug to L"Widget polarity was set incorrectly.", // the comment to add }; winrt::IAsyncAction Sample() { co_await winrt::resume_foreground(queue); if (!winrt::Clipboard::IsHistoryEnabled()) { // Oops co_return; } winrt::handle changed(winrt::check_pointer( CreateEventW(nullptr, FALSE, FALSE, nullptr))); auto historyChanged = winrt::Clipboard::HistoryChanged(winrt::auto_revoke, [h = changed.get()](auto&&, auto&&) { SetEvent(h); }); auto tempWindow = CreateWindowExW(0, L"static", nullptr, WS_POPUPWINDOW, 0, 0, 0, 0, nullptr, nullptr, nullptr, nullptr); for (auto message : messages) { SetClipboardText(tempWindow, message); co_await winrt::resume_on_signal(changed.get()); co_await winrt::resume_foreground(queue); } DestroyWindow(tempWindow); } int wmain([[maybe_unused]] int argc, [[maybe_unused]] wchar_t* argv[]) { winrt::init_apartment(); { auto controller = winrt::DispatcherQueueController:: CreateOnDedicatedThread(); Sample(controller.DispatcherQueue()).get(); controller.ShutdownQueueAsync().get(); } winrt::uninit_apartment(); return 0; }
The Windows Runtime clipboard requires access from a UI thread, so we spin up a dispatcher queue to serve as our UI thread. We do it as a separate thread so we can wait on it from the wmain
function. (The main
and wmain
functions cannot be coroutines.)
On the dispatcher queue thread, we first check whether clipboard history is even enabled. If not, then we bail out, because all of this work is pointless, and the program would hang if we tried, because we’d be waiting for a history change event that never happens.
Once we confirm that clipboard history is enabled, we create a kernel event object that we will use to tell us when the clipboard history has changed, and register an event handler that sets the event object. Then we go into our loop, and after setting each string, we wait for the signal before moving on to the next one.
There are still some gaps here: If the user disables clipboard history while the program is running, it will get halfway through the list and then hang waiting for an event that will never occur. But those are fine tuning steps. The essential point is that we are using the history change notification to tell us that the clipboard history service has processed the new clipboard text, so we can move on to the next one. This is just a sketch of the solution.
I’d hardly call this “horrible”; mystifying, perhaps, as I cannot think of a scenario where pre-loading the clipboard would be helpful. Calling this sort of clipboard manipulation “horrible” is greatly overstating things.
As I said, I think it’s horrible because:
1. It encourages using clipboard for things it was never intended to be used (automated placement of data in bulk instead of manual user interaction via GUI)
2. It encourages developers to create such kludge “solutions” to whatever their original problem might have been
And that’s if we ignore potential privacy and/or data loss issues.
Neither of those things equate to “horrible” to me. It’s unorthodox, and there’s almost certainly a way to do what the app devs want without
spamming“preloading” the clipboard, but to label it with that word seems entirely wrong.And in Windows, all apps running in a user’s session have access to the user’s clipboard. There are no privacy or security issues here.
is missing a parameter for queue I think?
By the way, what’s the purpose of the
right after the resume_on_signal one?
> those in the UI of various programs [A] (menus and keyboard shortcuts) for the express purpose of using its contents in a program [B] in which [C] said action was invoked in.
I have a hard time to parse this. Are A, B, C the same program in your scenario? I constantly copy from A then paste into B, and A and B are not the same program, and I don’t know which of A, B counts as C.
> Program that runs on startup because developer put an entry there isn’t user initiated.
I don’t think it’s suggested the program preloading...
A is plurality of programs having cut/copy/paste options in GUI, B and C are any program where user clicked any of said options (can be same program if you are copying from one page to another in same document).
I have yet to hear a convincing user case for this program.
If there’s a need to create pull requests in bulk and enough people needs it, then Microsoft can add an API to GitHub for that instead of helping people create such monstruous kludges.
Pi instead of cake to celebrate 20 years of Git?
Raymond, this is really a horrible thing to help people make. I am surprised you of all people are doing it.
How is a program that lets a developer add information about an open ticket to their clipboard history evil? Presumably there would be a button somewhere in the ticket interface saying something like “add ticket info to clipboard history”. There’s nothing evil, obscure or non-user-initiated about that.
IDK, doesn’t seem so evil to me. The program is already on the other side of the air-tight hatchway.
It's not about where the program is, it's about normalizing messing with the clipboard.
Clipboard is there for users to put things on it and retrieve them by using copy/cut/paste -- it is not there for smart ass programs to create new attack surfaces on already vulnerable by default user privacy.
In the previous article Raymond said the action is user initiated. In my opinion the only user initiated actions that count when it comes to changing clipboard contents are those in the UI of various programs (menus and keyboard shortcuts) for the express purpose of using its contents in a program...