将SendKeys Ctrl + C发送到外部应用程序(将文本复制到剪贴板)

8
我有一个应用程序,它作为托盘图标存在于系统托盘中。我已经注册了一个热键,当按下时,它将捕获任何应用程序中的当前文本选择,甚至在Web浏览器中也可以。

我的方法是发送组合键{Ctlr + C}以复制文本。然后访问剪贴板并在我的应用程序中使用该文本。

我正在使用VB.NET编程,但任何关于C#甚至C++和Win32_Api的帮助都将不胜感激。

我使用AutoHotkey,在那里,我有一个访问剪贴板文本并正常工作的脚本。

Pause::
clipboard =  ; Start off empty to allow ClipWait to detect when the text has arrived.
Send ^c
ClipWait, 2  ; Wait for the clipboard to contain text.
if ErrorLevel
{
    ;Do nothing after 2 seconds timeout
    return
}
Run https://translate.google.com/#auto/es/%clipboard%
return

由于AutoHotkey是开源的,我下载了代码并尽可能地复制了ClipWait的行为。
我的代码大部分时间都能工作,但有时会出现重要的延迟。我无法访问剪贴板,win32函数IsClipboardFormatAvailable()在一段时间内始终返回False。特别是在尝试从Google Chrome中的可编辑文本框中复制时会出现这种情况。
我尝试了很多不同的方法,包括使用.NET Framework Clipboard Class。我读到问题可能是运行命令的线程没有设置为STA,所以我这样做了。在绝望之余,我还放了一个计时器,但没有完全解决问题。
我也读到了通过监视剪贴板来放置钩子的选项,但除非这是唯一的方法,否则我想避免这样做。
以下是我的VB.NET代码:
Imports System.Runtime.InteropServices
Imports System.Text
Imports System.Threading
Imports Hotkeys
Public Class Form1
    Public m_HotKey As Keys = Keys.F6

    Private Sub RegisterHotkeys()
        Try
            Dim alreaydRegistered As Boolean = False
            ' set the hotkey:
            ''---------------------------------------------------
            ' add an event handler for hot key pressed (or could just use Handles)
            AddHandler CRegisterHotKey.HotKeyPressed, AddressOf hotKey_Pressed
            Dim hkGetText As HotKey = New HotKey("hkGetText",
                            HotKey.GetKeySinModificadores(m_HotKey),
                            HotKey.FormatModificadores(m_HotKey.ToString),
                            "hkGetText")
            Try
                CRegisterHotKey.HotKeys.Add(hkGetText)
            Catch ex As HotKeyAddException
                alreaydRegistered = True
            End Try
        Catch ex As Exception
            CLogFile.addError(ex)
        End Try
    End Sub

    Private Sub hotKey_Pressed(sender As Object, e As HotKeyPressedEventArgs)
        Try
            Timer1.Start()
        Catch ex As Exception
            CLogFile.addError(ex)
        End Try
    End Sub

    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        RegisterHotkeys()
    End Sub

    Function copyText() As String
        Dim result As String = String.Empty
        Clipboard.Clear()
        Console.WriteLine("Control + C")
        SendKeys.SendWait("^c")
        Dim Attempts As Integer = 100
        Do While Attempts > 0
            Try
                result = GetText()
                If result = String.Empty Then
                    Attempts -= 1
                    'Console.WriteLine("Attempts {0}", Attempts)
                    Thread.Sleep(100)
                Else
                    Attempts = 0
                End If

            Catch ex As Exception
                Attempts -= 1
                Console.WriteLine("Attempts Exception {0}", Attempts)
                Console.WriteLine(ex.ToString)
                Threading.Thread.Sleep(100)
            End Try
        Loop
        Return result
    End Function

#Region "Win32"

    <DllImport("User32.dll", SetLastError:=True)>
    Private Shared Function IsClipboardFormatAvailable(format As UInteger) As <MarshalAs(UnmanagedType.Bool)> Boolean
    End Function

    <DllImport("User32.dll", SetLastError:=True)>
    Private Shared Function GetClipboardData(uFormat As UInteger) As IntPtr
    End Function

    <DllImport("User32.dll", SetLastError:=True)>
    Private Shared Function OpenClipboard(hWndNewOwner As IntPtr) As <MarshalAs(UnmanagedType.Bool)> Boolean
    End Function

    <DllImport("User32.dll", SetLastError:=True)>
    Private Shared Function CloseClipboard() As <MarshalAs(UnmanagedType.Bool)> Boolean
    End Function

    <DllImport("Kernel32.dll", SetLastError:=True)>
    Private Shared Function GlobalLock(hMem As IntPtr) As IntPtr
    End Function

    <DllImport("Kernel32.dll", SetLastError:=True)>
    Private Shared Function GlobalUnlock(hMem As IntPtr) As <MarshalAs(UnmanagedType.Bool)> Boolean
    End Function

    <DllImport("Kernel32.dll", SetLastError:=True)>
    Private Shared Function GlobalSize(hMem As IntPtr) As Integer
    End Function

    Private Const CF_UNICODETEXT As UInteger = 13UI
    Private Const CF_TEXT As UInteger = 1UI

#End Region

    Public Shared Function GetText() As String
        If Not IsClipboardFormatAvailable(CF_UNICODETEXT) AndAlso Not IsClipboardFormatAvailable(CF_TEXT) Then
            Return Nothing
        End If

        Try
            If Not OpenClipboard(IntPtr.Zero) Then
                Return Nothing
            End If

            Dim handle As IntPtr = GetClipboardData(CF_UNICODETEXT)
            If handle = IntPtr.Zero Then
                Return Nothing
            End If

            Dim pointer As IntPtr = IntPtr.Zero

            Try
                pointer = GlobalLock(handle)
                If pointer = IntPtr.Zero Then
                    Return Nothing
                End If

                Dim size As Integer = GlobalSize(handle)
                Dim buff As Byte() = New Byte(size - 1) {}

                Marshal.Copy(pointer, buff, 0, size)

                Return Encoding.Unicode.GetString(buff).TrimEnd(ControlChars.NullChar)
            Finally
                If pointer <> IntPtr.Zero Then
                    GlobalUnlock(handle)
                End If
            End Try
        Finally
            CloseClipboard()
        End Try
    End Function

    Private Sub Timer1_Tick(sender As Object, e As EventArgs) Handles Timer1.Tick
        Try
            Timer1.Stop()
            Dim ThreadA As Thread
            ThreadA = New Thread(AddressOf Me.copyTextThread)
            ThreadA.SetApartmentState(ApartmentState.STA)
            ThreadA.Start()
        Catch ex As Exception
            CLogFile.addError(ex)
        End Try
    End Sub

    Sub copyTextThread()
        Dim result As String = copyText()
        If result <> String.Empty Then
            MsgBox(result)
        End If
    End Sub
End Class

我在其他类似的问题中也进行了搜索,但没有找到解决我的问题的最终方案: 发送Ctrl+C到先前活动的窗口 如何使用本地Win32 API从聚焦窗口获取所选文本?

你是什么意思?能否请你再具体一些? - Oscar Hermosilla
我知道我的方法目前会破坏剪贴板的内容,但如果我成功了,我可以先保存剪贴板的内容,然后在完成后再加载旧内容。 - Oscar Hermosilla
1
哪种语言?你忘了Java。 - Thomas Matthews
计时器的滴答间隔是多少? - Ryan Roos
计时器间隔为100毫秒。 - Oscar Hermosilla
显示剩余9条评论
2个回答

2
在这种情况下,VB.Net实际上提供了一种解决问题的方法。它被称为SendKeys.Send(<key>),您可以使用参数SendKeys.Send("^(c)")。这将发送Ctrl+C到计算机,根据this msdn-article

2

把AutoHotkey放回衣柜,抛弃你对IsClipboardFormatAvailable的需求。

使用微软提供的全局键盘钩子:RegisterHotKey函数非常有效,
唯一需要注意的是它不能单独与F6一起使用,你需要按下Alt +、Ctrl +或Shift +。

下载示例winform应用程序并亲自体验:

https://code.msdn.microsoft.com/CppRegisterHotkey-7bd897a8 C++ https://code.msdn.microsoft.com/CSRegisterHotkey-e3f5061e C# https://code.msdn.microsoft.com/VBRegisterHotkey-50af3179 VB.Net

如果上述链接失效,我在this answer中包含了C#源代码。

策略:

  1. 监视最后一个活动窗口

  2. (可选)保存剪贴板的当前状态(以便恢复)

  3. 将SetForegroundWindow()设置为最后一个活动窗口的句柄

  4. SendKeys.Send("^c");

  5. (可选)重置步骤2中保存的剪贴板值

代码:

这是我如何修改Microsoft示例项目的方法,用以下代码替换mainform.cs构造函数:

namespace CSRegisterHotkey
{
public partial class MainForm : Form
{
    [DllImport("User32.dll")]
    static extern int SetForegroundWindow(IntPtr point);

    WinEventDelegate dele = null;
    delegate void WinEventDelegate(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime);

    [DllImport("user32.dll")]
    static extern IntPtr SetWinEventHook(uint eventMin, uint eventMax, IntPtr hmodWinEventProc, WinEventDelegate lpfnWinEventProc, uint idProcess, uint idThread, uint dwFlags);

    private const uint WINEVENT_OUTOFCONTEXT = 0;
    private const uint EVENT_SYSTEM_FOREGROUND = 3;

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

    [DllImport("user32.dll")]
    static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int count);

    //Another way if SendKeys doesn't work (watch out for this with newer operating systems!)
    [DllImport("user32.dll")]
    static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, uint dwExtraInfo);

    //----

    HotKeyRegister hotKeyToRegister = null;

    Keys registerKey = Keys.None;

    KeyModifiers registerModifiers = KeyModifiers.None;

    public MainForm()
    {
        InitializeComponent();

        dele = new WinEventDelegate(WinEventProc);
        IntPtr m_hhook = SetWinEventHook(EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND, IntPtr.Zero, dele, 0, 0, WINEVENT_OUTOFCONTEXT);
    }

    private string GetActiveWindowTitle()
    {
        const int nChars = 256;
        IntPtr handle = IntPtr.Zero;
        StringBuilder Buff = new StringBuilder(nChars);
        handle = GetForegroundWindow();

        if (GetWindowText(handle, Buff, nChars) > 0)
        {
            lastHandle = handle;
            return Buff.ToString();
        }
        return null;
    }

    public void WinEventProc(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
    {
        txtLog.Text += GetActiveWindowTitle() + "\r\n";
    }

在主窗体中将HotKeyPressed事件更改为以下内容:
void HotKeyPressed(object sender, EventArgs e)
{
    //if (this.WindowState == FormWindowState.Minimized)
    //{
    //    this.WindowState = FormWindowState.Normal;
    //}
    //this.Activate();

    //Here is the magic
    SendCtrlCKey(lastHandle);
}

private void SendCtrlCKey(IntPtr mainWindowHandle)
{
    SetForegroundWindow(mainWindowHandle);
    //IMPORTANT - Wait for the window to regain focus
    Thread.Sleep(300); 
    SendKeys.Send("^c");

    //Comment out the next 3 lines in Release
#if DEBUG 
    this.Activate();
    MessageBox.Show(Clipboard.GetData(DataFormats.Text).ToString());
    SetForegroundWindow(mainWindowHandle);
#endif
}

//Optional example of how to use the keybd_event encase with newer Operating System the SendKeys doesn't work
private void SendCtrlC(IntPtr hWnd)
{
    uint KEYEVENTF_KEYUP = 2;
    byte VK_CONTROL = 0x11;
    SetForegroundWindow(hWnd);
    keybd_event(VK_CONTROL, 0, 0, 0);
    keybd_event(0x43, 0, 0, 0); //Send the C key (43 is "C")
    keybd_event(0x43, 0, KEYEVENTF_KEYUP, 0);
    keybd_event(VK_CONTROL, 0, KEYEVENTF_KEYUP, 0);// 'Left Control Up
}

警告:

  1. 如果您的应用程序面向使用各种键盘的国际用户,则使用SendKeys.Send可能会产生不可预测的结果,应避免使用。参考:模拟键盘输入 <- 该方法不起作用!!!
  1. 在操作系统更改时要小心,如此处所述:https://superuser.com/questions/11308/how-can-i-determine-which-process-owns-a-hotkey-in-windows

研究:

使用C#检测活动窗口的变化而无需轮询
使用Sendkeys模拟CTRL+C失败
是否可能发送一个WM_COPY消息,将文本复制到其他位置而不是剪贴板?
全局热键释放(keyup)?(WIN32 API)
C#使用Sendkey函数将按键发送到另一个应用程序
如何在使用Visual Studio 2010创建的Excel Add-In中执行.Onkey事件?
如何将任何应用程序的选定文本获取到Windows窗体应用程序中
剪贴板事件C#
如何在C#中监视剪贴板内容更改?

尽情享受:

enter image description here


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接