有时候我们可能想要让一些桌面应用程序自动地执行一些操作,但是这类程序又没有提供一些批量操作的方法或者编程接口。这时最容易的办法恐怕就是使用WindowsAPI来模拟鼠标或键盘的动作来操作这类应用程序了。
假设我们想操纵的应用程序是一个上传文件到网络的程序,整个流程是:登录--->选择要上传的文件--->填写文件信息--->上传。原本这个程序一次只能选择一个文件,只能填写一个文件的信息,只能上传一个文件。当文件很多,比如你要上传整个文件夹时,这显然很麻烦。我们可以先将待上传的文件及其文件信息做成一个待上传文件表,然后控制这个程序循环执行上传单一文件时的操作,讲待上传文件表中的文件批量上传。
要实现这些只需在Qt程序的头文件中包含windows.h文件,如果程序中使用了Windows Common Controls[1],那么还需包含commctrl.h这个头文件。
使用windows7+Qt4.7+Mingw+QtCreator。要使用的函数有以下几个:
1.HWND FindWindowExA(HWND hwndParent, HWND hwndChildAfter, LPCTSTR lpszClass, LPCTSTR lpszWindow);[2]
函数功能:该函数获得一个窗口的句柄,该窗口的类名和窗口名与给定的字符串相匹配。这个函数查找子窗口,从排在给定的子窗口后面的下一个子窗口开始。在查找时不区分大小写。
参数:
hwndParent:要查找子窗口的父窗口句柄。如果hwndParent为0,则函数以桌面窗口为父窗口,查找桌面窗口的所有子窗口。
hwndChildAfter:子窗口句柄。查找从在中的下一个子窗口开始(该参数可枚举子窗口)。如果HwndChildAfter为0,查找从hwndParent的第一个子窗口开始。如果 hwndParent 和 hwndChildAfter 同时为0,则函数查找所有的顶层窗口。
lpszClass:指向一个指定的类名。
lpszWindow:指向一个指定的窗口标题。
返回值:如果函数成功,返回值为具有指定类名和窗口名的窗口句柄。如果函数失败,返回值为0。
2.HWND GetDlgItem(HWND hDlg, int nIDDlgItem);[3]
函数功能:该函数检索指定的对话框中的控件句柄。
参数:
hDlg:标识含有控件的对话框。
nlDDlgltem:指定将被检索的控件标识符。
返回值:如果函数调用成功则返回值为给定控件的窗口句柄。如果函数调用失败,则返回值为NULL,表示为一个无效的对话框句柄或一个不存在的控件。若想获得更多错误信息,请调用GetLastError函数。
备注:可以对任何父子窗口来使用GetDlgltem函数,而不仅只是对话框。只要hDlg参数指定一个父窗口,且子窗口有一个独立的标识符(象CreateWindow中hMenu参数指定的或创建子窗口的CreateWindowEx指定的那样),GetDlgltem就会返回一个有效的句柄到子窗口。
3.LRESULT SendMessageA(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM IParam);
函数功能:该函数将指定的消息发送到一个或多个窗口。此函数为指定的窗口调用窗口程序,直到窗口程序处理完消息再返回。而和函数PostMessage不同,PostMessage是将一个消息寄送到一个线程的消息队列后就立即返回。
参数:
hWnd:其窗口程序将接收消息的窗口的句柄。如果此参数为HWND_BROADCAST,则消息将被发送到系统中所有顶层窗口,包括无效或不可见的非自身拥有的窗口、被覆盖的窗口和弹出式窗口,但消息不被发送到子窗口。
Msg:指定被发送的消息。
wParam:指定附加的消息特定信息。
IParam:指定附加的消息特定信息。
返回值:返回值指定消息处理的结果,依赖于所发送的消息。
很简单吧?仅此3个函数。反复调用即可实现模拟鼠标键盘动作操纵其他程序的功能。调用这些函数时的句柄、标示符、窗口名、消息附加信息等参数从哪儿来呢?这还需要Visual Studio中的Spy++[4]这款工具。相关用法可以看《如何使用spy ++ (How to use Spy ++)》(http://www.cnblogs.com/index/archive/2005/03/29/127619.html)等文章。
有了以上准备我们一步步来控制这个上传文件的程序。整个程序在QtCreator中完成,源文件使用System编码。在main.cpp中加入"QTextCodec::setCodecForTr(QTextCodec::codecForName("GBK"));"、"QTextCodec::setCodecForLocale(QTextCodec::codecForName("GBK"));"、"QTextCodec::setCodecForCStrings(QTextCodec::codecForName("GBK"));"三句来解决乱码问题。
1)根据程序所在路径打开应用程序:
QString fileName=ui->lineEdit_Path->text();//获取程序可执行文件的路径
QProcess *process=new QProcess;
QDir::setCurrent(QFileInfo(fileName).path());//进入程序所在文件夹
process->start(QFileInfo(fileName).fileName());//执行程序
if(process->state()!=QProcess::NotRunning)//如果程序执行成功返回true,失败返回false
return true;
else
return false;
2)自动登录(假设用户名和密码已保存在被操纵的程序中,点击“登录”即可):
HWND pWnd = NULL;
//程序启动可能较慢,需要尝试多次才能找到登录窗口的句柄
while(pWnd == NULL)
{
Sleep(3000);//每次检测间隔3秒钟
pWnd = ::FindWindowExA(NULL,NULL,0,"某程序 登录窗口");
}
//向目标窗口发送点击"登录"按钮的消息
::SendMessageA(::GetDlgItem(pWnd,0x00013C76),BM_CLICK,0,0);
其中参数"0x00013C76"就是该程序登录框中“登录”按钮的标识符。是通过Spy++看到的。
3)选择要上传的文件
3.1)点击“上传文件”按钮(实际是激活"上传文件"对话框):
//找到"上传文件"对话框
//"上传文件"对话框用的是另一个窗口,与程序主窗口不一样,程序主窗口只是负责将其激活
//因此要使用其他的方法获取其句柄,并激活
//由于进程中可能包含多个对话框类,所以仅仅靠"#32770"不一定能找到通过主程序激活的那个对话框
//因此要进一步通过对话框中的内容来判断是否是我们要找的对话框
//因为该程序打开的对话框中包含一个“同意”按钮,我们就以认为包含这个按钮的对话框就是我们要找的对话框
bool isFind=false;
HWND curWnd=NULL;
while(!isFind){
curWnd=FindWindowExA(0,curWnd,"#32770",0); //寻找对话框类
//若该类中包含"Button"类,且Button名为"同意",则curWnd即为"上传文件"对话框的句柄
HWND tWnd=NULL;
tWnd=FindWindowExA(curWnd,0,"Button","同意");
if(tWnd!=NULL)
isFind=true;
}
//激活"上传文件"对话框
ShowWindow(curWnd,SW_SHOWNORMAL);
//点击"添加文件"按钮
::PostMessage(curWnd,WM_COMMAND,0x0000CFB5,0);
这里为何要用WM_COMMAND呢?因为当时用Spy++监测到的就是这个。。。之后会弹出“打开”对话框
3.2)选择文件
//寻找"打开"对话框
HWND childHWND=NULL;
while(childHWND == NULL)
{
Sleep(3000);
childHWND=FindWindowExA(0,0,"#32770","打开");
}
//寻找"ComboBoxEx32"控件,在这个控件中可输入要打开的文件路径、文件名
HWND childHWND_ComboBoxEx32=NULL;
childHWND_ComboBoxEx32=FindWindowExA(childHWND,0,"ComboBoxEx32",0);
//寻找"Button","打开(&O)"控件
HWND childHWND_Button=NULL;
childHWND_Button=FindWindowExA(childHWND,0,"Button","打开(&O)");
//输入要上传的文件所在的目录
QString filePath=QDir::toNativeSeparators(_filePath_);
QByteArray t1=filePath.toLocal8Bit();
char *_filePath=t1.data();//以上3行将QString转换为char*。为保证不出现中文乱码,必须这么转换
::SendMessageA(childHWND_ComboBoxEx32,WM_SETTEXT,0,(LPARAM)_filePath);//填写待上传文件所在路径
//点击"打开"对话框的"打开"Button,打开要上传的文件所在的目录
::SendMessageA(childHWND_Button,BM_CLICK,0,0);
//输入要上传的文件名
QByteArray t2=fileName.toLocal8Bit();
char *_fileName=t2.data();
::SendMessageA(childHWND_ComboBoxEx32,WM_SETTEXT,0,(LPARAM)_fileName);
//点击"打开"对话框的"打开"Button,打开要上传的文件
::SendMessageA(childHWND_Button,BM_CLICK,0,0);
这样选择待上传文件的过程就完成了。“打开”对话框会关闭,“上传文件”对话框再次被激活。这里为何要先进入文件所在目录再选择文件呢?因为这样比直接输入文件的绝对路径然后再点击“打开”要稳定。
4)填写文件信息
4.1)选择文件类型(找到对应的单选框,点击)
QByteArray t4=Category.toLocal8Bit();4.2)输入文件描述信息
char *_Category=t4.data();
//获取相应Category的句柄,并选择
childHWND=::FindWindowExA(curWnd,0,"Button",_Category);
::SendMessageA(childHWND,BM_CLICK,0,0);
QByteArray t10=Introduce.toLocal8Bit();char *_Introduce=t10.data();//获取相应的Introduce(介绍)句柄,并输入介绍childHWND=::GetDlgItem(curWnd,0x0000CF91);::SendMessageA(childHWND,WM_SETTEXT,0,(LPARAM)_Introduce);
5)点击“开始上传”按钮,上传文件
childHWND=::GetDlgItem(curWnd,0x0000CFB3);
::SendMessageA(childHWND,BM_CLICK,0,0);
只要在程序中不断重复这几步,就可以自动批量上传文件啦。简单来说就是找到要控制的句柄,然后向它发送鼠标点击或者文本输入等消息。当你知道句柄的标识符时,可以直接用GetDlgItem函数获取句柄。当你只知道句柄的类名或者窗口名时,可用FindWindowExA函数来获取句柄。至于发送消息,则可用SendMessageA或者PostMessage函数。
[1]Windows Common Controls,http://blog.csdn.net/luckysym/article/details/1341083
Win32 API中本身提供了Windows下许多常用的控件,称为Common Controls。 这些控件与Button、ComboBox等控件不同,不是在user32.dll中实现,而是在Comctrl32.dll中实现,相关的C++原型声明在commctrl.h中。所以,在使用Win32 API编写Windows窗口应用程序时,如果在界面上用到了Common Controls,则必须在链接选项中包含comctrl32.lib库,并在程序初始化时调用InitCommonControls()函数,确保控件被加载。InitCommonControls()函数在commctrl.h中声明,因此程序中需包含该头文件。Common Controls列表如下:Animation、ComboBoxEx、Date_and_Time_Picker、Drag_List_Box、Flat_Scroll_Bar、Header、HotKey、ImageList、IPAddress、List_View、Month_Calendar、Pager、Progress_Bar、Property_Sheets、Rebar、Status Bars、SysLink、Tab、Toolbar、ToolTip、Trackbar、TreeView、Up_and_Down。
[2]FindWindowExA,http://baike.baidu.com/view/5033123.htm
[3]GetDlgItem,http://baike.baidu.com/view/1228297.htm