May 9th, 2025

How can I wait for Clipboard History to recognize a clipboard change before I change it again?

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.History­Changed 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.

Topics
Code

Author

Raymond has been involved in the evolution of Windows for more than 30 years. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie-jeebies. The Web site spawned a book, coincidentally also titled The Old New Thing (Addison Wesley 2007). He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information.

11 comments

  • Mike Morrison 23 hours ago

    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.

    • Igor Levicki 2 hours ago · Edited

      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.

      • Mike Morrison 16 minutes ago

        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.

  • Antanas Uršulis
    winrt::IAsyncAction Sample()

    is missing a parameter for queue I think?

    By the way, what’s the purpose of the

    co_await resume_foreground

    right after the resume_on_signal one?

  • GL 4 days ago

    > 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...

    Read more
    • Igor Levicki 2 hours ago

      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.

  • alan robinson 5 days ago

    Pi instead of cake to celebrate 20 years of Git?

  • Igor Levicki

    Raymond, this is really a horrible thing to help people make. I am surprised you of all people are doing it.

    • aaaaaa 123456789 4 days ago

      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.

    • alan robinson 5 days ago

      IDK, doesn’t seem so evil to me. The program is already on the other side of the air-tight hatchway.

      • Igor Levicki 4 days ago

        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...

        Read more