Search code examples
c#wpfwindow

Send Ctrl+C to previous active window


I'm trying to write a small application that's sits in the system tray. I've registered a hotkey. When the hotkey is fired and the application is activated I want to send Ctrl+C to the last active window so I can get the highlighted text into the clipboard.

This is what I got so far:

    //http://stackoverflow.com/questions/9468476/switch-to-last-active-application-like-alt-tab

    [DllImport("user32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    static extern bool IsWindowVisible(IntPtr hWnd);

    [DllImport("user32.dll")]
    static extern IntPtr GetLastActivePopup(IntPtr hWnd);

    [DllImport("user32.dll", ExactSpelling = true)]
    static extern IntPtr GetForegroundWindow();

    [DllImport("user32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    static extern bool SetForegroundWindow(IntPtr hWnd);

    const uint GA_PARENT = 1;
    const uint GA_ROOT = 2;
    const uint GA_ROOTOWNER = 3;

    public static IntPtr GetPreviousWindow()
    {
        IntPtr activeAppWindow = GetForegroundWindow();
        if (activeAppWindow == IntPtr.Zero)
            return IntPtr.Zero;

        IntPtr prevAppWindow = GetLastActivePopup(activeAppWindow);
        return IsWindowVisible(prevAppWindow) ? prevAppWindow : IntPtr.Zero;
    }

    public static void FocusToPreviousWindow()
    {
        IntPtr prevWindow = GetPreviousWindow();
        if (prevWindow != IntPtr.Zero)
            SetForegroundWindow(prevWindow);
    }

    ...

    private static void OnHotKeyFired()
    {
        FocusToPreviousWindow();

        SendKeys.SendWait("^(c)");

        _viewModel.Input = Clipboard.GetText();

        new UIWindow(_viewModel).ShowDialog();
    }

But I can't get the SendKeys to work. In most apps nothing happpens, meaning ctrl-c is not fired. In Ms Word a copyright sign (c) is inserted in my document when SendWait is executed.

UPDATE:

I've tried with WM_COPY and SendMessage:

private static void OnHotKeyFired()
{
    IntPtr handle = GetPreviousWindow();
    SendMessage(handle, WM_COPY, IntPtr.Zero, IntPtr.Zero);
    _viewModel.Input = Clipboard.GetText();
    ...

And it works in Word but not in Excel, Notepad, Visual Studio


Solution

  • I think your issue is related to timing.

    To set the windows focus and to do the actual copy to clipboard you need to wait for the window to get focus, and to wait for the clipboard to be updated.

    There are a couple of ways to deal with these ugly win32 things.

    For the clipboard content. I compare the original content to the current content. I set the original content to string.empty if its an image or some other none text data. I then await a function that checks the clipboard for changes.

    For the SetForegroundWindow I currently add a delay in my async function. You could also probably find a win32 api call to wait for this window to properly be put in the foreground.

    I do both of these in an async function and await it so as to no block.

    SendKeys should work with SendWait("^c"). SendWait("^(c)") will not always work as noted in the other answers. However the ctrl+c copy to clipboard doesn't happen instantly.

    Point p;
    if (GetCursorPos(out p))
    {
        IntPtr ptr = WindowFromPoint(p);
        if (ptr != IntPtr.Zero)
        {                    
            SetForegroundWindow(ptr);
    
            //wait for window to get focus quick and ugly
            // probably a cleaner way to wait for windows to send a message
            // that it has updated the foreground window
            await Task.Delay(300);
    
            //try to copy text in the current window
            SendKeys.Send("^c");
    
            await WaitForClipboardToUpdate(originalClipboardText);
       }
    }
    
    }
    
        private static async Task WaitForClipboardToUpdate(string originalClipboardText)
        {
            Stopwatch sw = Stopwatch.StartNew();
    
            while (true)
            {
                if (await DoClipboardCheck(originalClipboardText)) return;
    
                if (sw.ElapsedMilliseconds >= 1500) throw new Exception("TIMED OUT WAITING FOR CLIPBOARD TO UPDATE.");
            }
        }
    
        private static async Task<bool> DoClipboardCheck(string originalClipboardText)
        {
            await Task.Delay(10);
    
            if (!Clipboard.ContainsText()) return false;
    
            var currentText = Clipboard.GetText();
    
            Debug.WriteLine("current text: " + currentText + " original text: " + originalClipboardText);
    
            return currentText != originalClipboardText;
        }
    
    
       [DllImport("user32.dll")]
        private static extern bool GetCursorPos(out Point pt);
    
        [DllImport("user32.dll", EntryPoint = "WindowFromPoint", CharSet = CharSet.Auto, ExactSpelling = true)]
        private static extern IntPtr WindowFromPoint(Point pt);
    
        // Activate an application window.
        [DllImport("USER32.DLL")]
        public static extern bool SetForegroundWindow(IntPtr hWnd);
    
        [DllImportAttribute("user32.dll", EntryPoint = "GetForegroundWindow")]
        public static extern IntPtr GetForegroundWindow();
    
        [DllImport("user32.dll", SetLastError = true)]
        static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId);
    

    Also - A real quick and dirty way to quickly validate/check if timing is your issue is you could put a Thread.Sleep(1000); after your SetForegroundWindow, and SendKeys.SendWait calls.