Search code examples
c++windowsclipboard

Rendering clipboard repeatedly


I have an application that uses delayed rendering of the clipboard to paste the current time. But: when the time was rendered once, it will always paste the same time, and not the updated time.

I think I would need to call SetClipboardData(CF_TEXT, nullptr); again, to restore another case of delayed rendering, but I don't know when or where I should do that. Can I detect when the target application has taken the data from the clipboard?

How can I paste the current time each time the user presses Ctrl+V?

#include <windows.h>
#include <thread>
#include <chrono>
#include <ctime>
#include <iomanip>
#include <sstream>

// Get current time with milliseconds (HH:MM:SS.xxx)
void GetTime(std::string& time_str)
{
    auto now = std::chrono::system_clock::now();
    auto in_time_t = std::chrono::system_clock::to_time_t(now);
    auto milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()) % 1000;
    std::tm bt{};
    localtime_s(&bt, &in_time_t);
    std::ostringstream oss;
    oss << std::put_time(&bt, "%H:%M:%S") << '.' << std::setfill('0') << std::setw(3) << milliseconds.count();
    time_str = oss.str();
}

void RenderClipboardData(HWND hwnd) {
    std::string time_str;
    GetTime(time_str);

    EmptyClipboard();

    // Allocate global memory for the clipboard data
    HGLOBAL hGlobal = GlobalAlloc(GMEM_MOVEABLE, time_str.size() + 1);
    if (hGlobal) {
        // Lock the global memory and copy the string into it
        void* pGlobal = GlobalLock(hGlobal);
        if (pGlobal) {
            memcpy(pGlobal, time_str.c_str(), time_str.size() + 1);
            GlobalUnlock(hGlobal);
            SetClipboardData(CF_TEXT, hGlobal);
        }
        else
        {
            // Free the global memory if it wasn't successfully set
            GlobalFree(hGlobal); 
        }
    }
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {

    switch (uMsg) {
    case WM_RENDERFORMAT:
        if (wParam == CF_TEXT) {
            RenderClipboardData(hwnd);
        }
        break;
    case WM_CREATE:
         CreateWindow(
            L"STATIC",
            L"This application pastes the current time on Ctrl+V. ",
            WS_VISIBLE | WS_CHILD,
            10, 10, 600, 100,
            hwnd,
            nullptr,
            reinterpret_cast<LPCREATESTRUCT>(lParam)->hInstance,
            nullptr);

        if (OpenClipboard(hwnd)) {
            EmptyClipboard();
            SetClipboardData(CF_TEXT, nullptr); // Delayed rendering to get the current time
            CloseClipboard();
        }
        break;
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hwnd, uMsg, wParam, lParam);
    }
    return 0;
}

int APIENTRY wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nCmdShow) {

    constexpr wchar_t CLASS_NAME[] = L"SampleWindowClass";

    WNDCLASS wc = {};
    wc.lpfnWndProc = WindowProc;
    wc.hInstance = hInstance;
    wc.lpszClassName = CLASS_NAME;

    RegisterClass(&wc);

    HWND hwnd = CreateWindowEx(
        0,                              // Optional window styles.
        CLASS_NAME,                     // Window class
        L"Delayed Clipboard Rendering", // Window text
        WS_OVERLAPPEDWINDOW,            // Window style

        // Size and position
        CW_USEDEFAULT, CW_USEDEFAULT, 640, 120,

        nullptr,       // Parent window    
        nullptr,       // Menu
        hInstance,     // Instance handle
        nullptr        // Additional application data
    );

    if (hwnd == nullptr) {
        return 0;
    }

    ShowWindow(hwnd, nCmdShow);

    MSG msg = {};
    while (GetMessage(&msg, nullptr, 0, 0)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return 0;
}

I have tried:

  • processing the WM_DESTROYCLIPBOARD message and reset the clipboard to delayed rendering there.

Solution

  • Firstly, you should NOT be calling EmptyClipboard() when rendering the text. Another app that wants the text has already opened the clipboard, so you need to merely put the text on the clipboard, do nothing else.

    Now, to accomplish what you want, you can have your WM_RENDERFORMAT handler post a custom message to your window after rendering the data, and then issue a new delay rendering from that message handler, eg:

    bool SetClipboardDataDelayRendered(HWND hwnd) {
        if (!OpenClipboard(hwnd)) return false;
        EmptyClipboard();
        SetClipboardData(CF_TEXT, nullptr); // Delayed rendering to get the current time
        CloseClipboard();
        return true;
    }
    
    void RenderClipboardData(HWND hwnd) {
        ...
        // DO NOT DO THIS HERE!
        // EmptyClipboard();
        ...
    }
    
    static const UINT WM_SET_CB_DELAY_RENDERED = WM_USER + 100;
    
    LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
    
        switch (uMsg) {
        case WM_CREATE:
            ...
            SendMessage(hwnd, WM_SET_CB_DELAY_RENDERED, 0, 0);
            break;
        case WM_SET_CB_DELAY_RENDERED:
            // keep trying until the clipboard can be opened...
            if (!SetClipboardDataDelayRendered(hwnd))
                PostMessage(hwnd, WM_SET_CB_DELAY_RENDERED, 0, 0);
            break;
        case WM_RENDERFORMAT:
            if (wParam == CF_TEXT) {
                RenderClipboardData(hwnd);
                // the other app has the clipboard open,
                // wait for it to close before resetting it...
                PostMessage(hwnd, WM_SET_CB_DELAY_RENDERED, 0, 0);
            }
            break;
        ...
        }
        return 0;
    }
    

    That being said, do keep in mind that after you request delayed rendering of your data, another app could come along and replace the clipboard contents with its own data, thus your delayed rendering will stop working. You can detect that condition by handling WM_DESTROYCLIPBOARD, and/or using GetClipboardOwner() to make sure your HWND is still the owner. You may need to use a timer or other mechanism to restart your delayed rendering if this happens.