[MSDN文章]向任何应用程序发送击键,从 MFC 应用程序中调用 .NET 以及其他
向任何应用程序发送击键,从 MFC 应用程序中调用 .NET 及其他
下载本文的示例代码: CQA0501.exe (231KB)
问:我正在尝试编写一个应用程序,该应用程序通过向另外一个应用程序发送击键来填充窗体。我是应该发送 WM_KEYDOWN 和 WM_KEYUP 消息,还是有更好的方法?
许多读者
答:虽然通过发送 WM_KEYDOWN 和 WM_KEYUP 消息,您可能使应用程序进行工作,但是,SendInput 是一个专门为发送该消息而设计的 API 函数。它通过采用一组 INPUT 结构(每个输入事件 ─ 击键或鼠标操作 ─ 对应一个结构)对输入(包括击键和鼠标事件)进行了组合。INPUT 结构包含一个联合,该联合可以是 MOUSEINPUT 或 KEYBDINPUT(或是模拟烘烤炉的 HARDWAREINPUT)。对于键盘,它如下所示:
struct KEYBDINPUT {
WORD wVk; // virt key code
WORD wScan; // hw scan code
DWORD dwFlags; // flags—see doc
DWORD time; // time stamp, 0 = dflt
ULONG_PTR dwExtraInfo; // app-defined
};

图 1 Typematic 初始对话框
因此,向另外一个应用程序发送击键基本上是构建一组 INPUT 结构的问题,每个击键(上下)对应一个结构,然后调用 SendInput。为演示它在实际中如何工作,我编写了一个名为 Typematic 的小程序,它让您仅仅按下一个热键就可以快速向窗体中输入您的姓名、地址、电话号码或其他信息。对于 Web 购物狂,这真是一件大好事。在首次运行 Typematic 时,它显示图 1 中的对话框,然后就隐藏起来。此后,您可以按下 <WinKey>+T 重新激活 Typematic,并获得图 2 所示的对话框,以查看一列缩写。键入代表姓名的“n”或代表地址的“a”,Typematic 就会将相关的字符串发送到当前的窗体或应用程序。该缩写在静态表中进行定义,您可以对其进行修改以使用自己的身份:
struct ABBREV {
TCHAR key;
LPCTSTR text;
} MYABBREVS[] = {
{ _T('n'),_T("Elmer Fudd") },
{ _T('a'),_T("1 Bunny Way") },
...
{ 0,NULL}
};

图 2重新激活的 Typematic
当然,在实际工作中,您不会硬编码该信息;您应提供可以自定义的用户界面,并在用户配制文件中保存该信息,以使该计算机上的每位用户都拥有不同的设置。
Typematic 显示了其他几个窍门:如何注册一个热键以激活您的应用程序(参见我的 December 2000 专栏)和如何制作接受键盘输入的静态文本控件(您必须处理 WM_GETDLGCODE 并返回 DLGC_WANTCHARS。)
Typematic 定义了一个专门的静态文本控件 CStaticAbbrev,该控件既可以显示缩写,又可以读取快捷键。图 3 显示了源代码。在用户按下热键时,Typematic 唤醒并将焦点赋于 CStaticAbbrev 控件,该控件等待一个字符。在 CStaticAbbrev::OnChar 获得一个与表中缩写相匹配的键时,它隐藏该对话框,然后调用一个 Helper 函数 SendString 发送文本:
// in CStaticAbbrev::OnChar
if (/* find char in ABBREV table */) {
GetParent()->ShowWindow(SW_HIDE); // hide dialog
SendString(abbrev.text); // send text
}
在 Typematic 隐藏自身时,Windows® 自动将焦点还原到之前拥有该焦点的窗口,这样,输入就进入在用户按下热键之前正使用的窗口。相当智能化,不是吗?如果您需要直接输入到一个特定的应用程序或窗口,请确保在调用 SendInput 之前它被激活。SetForegroundWindow 是一个调用函数。
发送击键的所有工作在 SendString 中发生,它构建 INPUT 数组并调用 SendInput(参见 图 3)。SendString 发送 INPUT 结构的一系列 KEYDOWN/KEYUP 对,字符串中的每个字符对应一个 KEYDOWN/KEYUP 对。它使用 KEYEVENTF_UNICODE 标记发送作为 Unicode 字符的字符串。Unicode 使工作更轻松,因为您无需使用 Shift 键来组合大写字母。如果没有 KEYEVENTF_ UNICODE,您就必须先按下 <Shift>,再按下 e 来发送大写的 E。对每个字母使用向上/向下事件,总共需要四次击键。对 Redmondtonians 添加 KEYEVENTF_UNICODE 表示感谢!
如果您正在使用托管 C++ 或其他 Microsoft®.NET 兼容的语言进行编程,则发送击键甚至更简单。有了带有静态函数 Send 的 Framework 类和 SendKey,发送键简直就是小菜一碟。甚至可以使用波浪线语法发送特殊键。例如,“{F1}{BACKSPACE}A”发送 F1、Backspace 和 A。出于娱乐,我编写了 Typematic 的另一个使用 SendKey 的版本 Typematic.NET。目前,SendString 函数是比较常用的:
#pragma managed
void SendString(LPCTSTR str) {
SendKeys::SendWait(str);
}
怎么样才能更简单一些?在我首次尝试时,我自然尝试 SendKeys::Send,而不是 SendWait。为什么我应当等待应用程序完成键输入呢?唉!很遗憾。在我尝试时,由于 Windows.Forms.dll 中某处的 System.InvalidOperationException,Typematic 崩溃了。在我调用公共语言运行库 (CLR) 时,调试器发现了一些糟糕的事情,输出窗口显示如下消息:“Additional information:SendKey cannot run inside this application because the application is not handling windows messages. Either change the application to handle messages, or use the SendKeys.SendWait method.”(无法在此应用程序中运行,因为该应用程序无法处理 Windows 消息。请更改该应用程序以处理消息,或使用 SendKeys.SendWait 方法。)现在,我称之为友好的错误消息!希望大家引以为戒。但是,在您尝试使用 SendKey 之前,我要郑重告知大家,它没有 SendInput可靠。通过针对不同的应用程序(如 Microsoft Internet Explorer、Notepad、MFC 窗体视图或其它喜欢的应用程序)对 Typematic 和 Typematic.NET 进行测试,您自己就可以发现这个问题。出于某些原因,SendKey 并不能始终起作用。我怀疑是焦点和计时问题 ─ 键已经发送,但然后却蒸发了,因为在您看来拥有焦点的窗口实际上还没有获得焦点。因此,尽管与 SendInput 相比,SendKey 更容易使用,功能也更强大,但它不能在所有环境中很好地工作。唉!也许 Redmondtonians 会在下一个版本中修复这个问题。
一个最后警告:众所周知,发送击键来控制另一个应用程序是一种易出问题的方法。您必须正确地获取所有键,并且上下文或用户界面中的细微变化也会致使所有事情出错。如果您正在尝试控制另一个应用程序,请检查脚本系统、编程接口或宏语言。
问:我已经阅读了几篇有关如何禁用系统键组合(如 Ctrl+Alt+Del)的文章,包括您在 2002 年 9 月出版的 MSDN®Magazine 中的文章。但是,我如何才能以编程方式发送 Ctrl+Alt+Del?
William Burns
答:第一个问题演示了如何向任何应用程序中发送击键,但是您无法使用 SendInput 发送 Ctrl+Alt+Del,因为该组合是在操作系统的底层进行处理的。总之,组合击键不是“发送”Ctrl+Alt+Del 的适当方式。您要尝试做什么呢?如果您要调用任务管理器,请使用“taskmgr.exe”调用 ShellExecute。如果正在尝试重新启动,您可以使用 EWX_REBOOT 标记调用 ExitWindowsEx。ExitWindowsEx 具有所有类型的标记,这些标记用于以不同方式关机或重新启动计算机(参见 图 4)。而且,要重新启动,您的应用程序需要 SE_SHUTDOWN_NAME 权限。
有人知道 Ctrl+Alt+Del 的出处吗?如果您知道,请给我发邮件。我将在以后的专栏中提供答案。
问:我可以从 MFC 应用程序中调用 .NET Framework 吗?我想从非托管 MFC 代码中调用托管类,并且我已经尝试通过 #using <mscorlib.dll> 来调用,但是我得到一个消息“/RTC1 incompatible with /clr.”我如何才能从 MFC 应用程序中调用 .NET?
Julian Kinsey
答:当然,您可以从 MFC 应用程序中调用 .NET!正如 Windows 中的其他所有事情一样,只要您了解其中正确的“魔法”,这就是轻而易举的事。在您首次创建一个 MFC 应用程序时,“应用程序向导”为您设置所有种类的编译器选项。C/C++ 代码生成器中的其中一个选项是“基本的运行时检查”。在您创建 MFC 应用程序时,“应用程序向导”在您的调试版本中选择“Both (/RTC1, equiv. to /RTCsu)”进行运行时检查(如检查错误的堆栈框架、未初始化的变量或缓冲区超限和不足等)。这些检查与 /clr 不兼容,因为托管代码完全不同(它是 Microsoft 的中间语言,而不是本地语言),但是在您添加 /clr 时,IDE 不会自动删除 /RTC1。因此,您必须手动删除。
对于项目中的每个 .cpp 文件,将“基本的运行时检查”设置为“默认”。这有点不合适,因为对于本地/非托管函数来说,这种检查是好事。它们帮助您在发布程序之前查找其中的错误。如果仅仅因为想调用一两个 .NET 类就不进行检查,就有点可惜了。是否还有别的方法可以对 .NET 和运行时进行检查?
问题在于使用托管扩展调用框架,您必须将“使用托管扩展”设置为“是”(设置 /clr),并且您只能在项目设置中进行这种更改,它是全局性的。使用托管扩展在您的项目中打开所有模块的 /clr。默认情况下,现在您的所有函数都是托管的。如果您将本地模式设为默认,则可在 stdafx.h 结尾添加一行:
#pragma unmanaged
由于每个模块都包括 stdafx.h,现在您的所有模块都以之前一直使用的本地模式进行编译。在调用框架时,您可以翻转托管代码,如下所示:
#pragma managed
void DoSomethingWithDotNET(...) {
// call framework classes here
// safe from managed functions
}
到目前为止,我一直使用该技巧(将 #pragma unmanaged 放到 stdafx.h 的结尾)。但是,它没有解决运行时检查问题,因为 /clr 仍然不与 /RTC 兼容,即使您的大部分函数是本地的。在混合模式应用程序中,编译器不会让您说“需要时在本地函数中进行运行时检查。”

图 5项目设置
实际上您所需做的只是与实际调用框架的 /clr 兼容。但是在“使用托管扩展”出现在项目范围设置中时,该如何做呢?很简单:您可以始终在模块构建属性的命令行处添加特定的转换。图 5 和图 6 演示了如何进行这种设置。在您的全局项目设置中,将“使用托管扩展”设置为“否”,然后向每个调用框架的模块中添加 /clr。现在,其他所有模块仍然可以使用 /RTC1,并能很好地进行运行时检查。当然,现在必须将 <mscorlib.dll> 和其他所有框架文件从 stdafx.h 移动到托管模块中。如果您愿意,您可以将所有标准的 .NET 包括在一个 UsesDotNet.h 文件中,如下所示:
#using #using #include using namespace System;

图 6模块设置
然后,在调用框架的每个模块中只有 #include “UsesDotNet.h”。您仍然可以在一个模块中使用 #pragma managed/unmanaged 来混合托管代码和本地代码。
还有另一层含义。在向其中一个模块中添加 /clr时,除关闭运行时检查之外,您还必须选择“Not Using Precompliled Headers”。(不使用预编译标头)只有所有模块具有相同的编译选项时,预编译标头信息才能工作;如果一个模块具有 /clr,则它不能使用与其他没有 /clr 的模块相同的预编译标头。嗨!最近计算机发展日新月异,还有谁需要预编译标头呢?如果您真的喜欢,您可以始终通过不同的标头文件(如 UsesDotNet.h)来编译托管模块。
对于使用我所描述方法的实际工作项目,请查看本专栏第一个问题中的 Typematic.NET 应用程序。您可以从 MSDN Magazine Web 站点下载该应用程序。通常都是以普通的本地方式,通过 stdafx.h 使用预编译标头对 Typematic.NET 中的所有模块进行编译,并且这些模块没有托管扩展。SendString 是调用 .NET Framework 的唯一一个函数,它位于相当独立的 SendString.cpp 模块中。该模块使用托管编译处理 /clr,没有运行时检查,也没预编译标头。好好干吧!
请将给 Paul 的问题和评论发送至cppqa@microsoft.com。
Paul DiLascia 是自由作家、顾问以及最权威的 Web/UI 设计师。他是 Windows++: Writing Reusable Windows Code in C++ (Addison-Wesley, 1992) 一书的作者。您可以通过 www.dilascia.com 与Paul 取得联系。
浙公网安备 33010602011771号