最近做一个抓取qq用户资料的工具,需要获取qq窗口上的消息,以前这种任务是用句柄获取窗口中的信息,现在qq的窗口用的是DirectUI,只有窗口句柄,没有控件句柄,句柄这条路走不通了。不过较新版的qq的部分控件实现了微软的IAccessible接口(称为Microsoft Active Accessibility技术,简称MSAA),可以用另一套函数获取qq窗口的信息。不过要对窗口进行输入还是要靠句柄,上面说过,DirectUI的窗口只有一个句柄,因此模拟输入的时候不需要查找到具体的控件句柄,但要注意获取控件焦点,可能相对传统WinForm的窗口要简单点。
先介绍下和句柄操作相关的函数:
using System.Runtime.InteropServices;
[DllImport("user32.dll", SetLastError =true)]
static extern IntPtr FindWindow(string lpClassName,string lpWindowName);
根据类名和窗口标题查找句柄
[DllImport("user32.dll", SetLastError =true)]
public static extern IntPtr FindWindowEx(IntPtr parentHandle,IntPtr childAfter, string className, string windowTitle);
根据父句柄,前一个句柄,类名和窗口标题查找句柄,这几个信息可以通过VS自带的spy++查询。
[DllImport("user32.dll", EntryPoint ="GetDesktopWindow", CharSet = CharSet.Auto, SetLastError = true)]
static extern IntPtr GetDesktopWindow();
返回桌面窗口句柄,被我用来当前一个函数的父句柄。
[DllImport("user32.dll")]
public extern static int GetWindowText(IntPtr hWnd,StringBuilder lpString, int nMaxCount);
获取指定窗口的标题,WinForm里的控件都是window,但在我们讨论的情况下window就只是窗口了。需要结合StringBuider类使用,不熟悉的可以去预习下。
[DllImport("User32.dll")]
public static extern int GetClassName(int hWnd,StringBuilder lpClassName, int nMaxCount);
获取指定窗口的类名,也是用到StringBuider类。
[DllImport("USER32.DLL")]
public static extern bool SetForegroundWindow(IntPtr hWnd);
将一个窗口显示到最前端。
[DllImport("user32.dll", CharSet =CharSet.Auto, ExactSpelling = true)]
public static extern IntPtr GetForegroundWindow();
返回最前端窗口的句柄。
[DllImport("user32.dll", EntryPoint ="SendMessage")]
static extern int SendMessage(IntPtr hWnd,uint Msg, int wParam, int lParam);
模拟键盘或鼠标的输入,最常用的就是它了,后面具体介绍。
[DllImport("user32.dll", EntryPoint ="PostMessage")]
public static extern IntPtr PostMessage(IntPtr hwnd,uint msg,int wparam, int lparam);
作用和上一个类似,不过SendMessage是等窗口处理完事件后返回,这个是发送消息后立即返回。顺便一提,原型本来是LRESULT SendMessage( HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam);类型改了一下,并不影响功能,只是方便写,其它函数也类似。
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool GetWindowRect(IntPtr hWnd,ref RECT lpRect);
返回一个表示指定窗口位置大小的结构,结构声明如下:
[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
public int Left;//最左坐标
public int Top;//最上坐标
public int Right;//最右坐标
public int Bottom;//最下坐标
}
这里要说一下DirectUI的窗口一般都是用微软的api建立一个窗口,然后隐藏窗口,自己画一个窗口出来,所以窗口的实际大小要比看到的要大,拖动窗口时看到的那个虚线框才是真实的大小。
接下来是MSAA的相关函数:需要引用Accessibility,并using Accessibility;
[DllImport("Oleacc.dll")]
public static extern int AccessibleObjectFromWindow(
IntPtr hwnd,
int dwObjectID,
ref Guid refID,
ref IAccessible ppvObject);
通过句柄获取窗口对于的IAccessible对象,知道我为什么要先介绍句柄相关的函数了吧。用的时候需要包装一下:
private void GetAccessibleWindow(System.IntPtr imWindowHwnd,out IAccessible IACurrent)
{
Guid guidCOM = new Guid(0x618736E0, 0x3C3D, 0x11CF, 0x81, 0xC, 0x0, 0xAA, 0x0, 0x38, 0x9B, 0x71);
AccessibleObjectFromWindow(imWindowHwnd, -4, ref guidCOM, ref IACurrent);
}
[DllImport("Oleacc.dll")]
public static extern int AccessibleChildren(
Accessibility.IAccessible paccContainer,
int iChildStart,
int cChildren,
[Out] object[] rgvarChildren,
out int pcObtained);
获取IAccessible对象的子对象数组,一般包装成下面的形式:
private IAccessible[] GetAccessibleChildren(IAccessible paccContainer)
{
IAccessible[] rgvarChildren =new IAccessible[paccContainer.accChildCount];
int pcObtained;
AccessibleChildren(paccContainer, 0, paccContainer.accChildCount, rgvarChildren,out pcObtained);
return rgvarChildren;
}
public IAccessible GetAccessibleChild(IAccessible paccContainer,int[] array)
{
if (!array.Length.Equals(0))
{
IAccessible[] children=GetAccessibleChildren(paccContainer);
IAccessible result = children[array[0]];
if (result.accChildCount == 0)
{
lb.Text += "error: parent:" + ((IAccessible)result.accParent).accChildCount +" role:" + result.accRole + " state:" + result.accState;
return null;
}
int[] array_1 = new int[array.Length - 1];
for (int i = 0; i < array.Length - 1; i++)
{
array_1[i] = array[i + 1];
}
return GetAccessibleChild(result, array_1);
}
else
{
return paccContainer;
}
}
按层级找到某个对象,array代表需要查找的层级,就是调用了前面的函数。
介绍几个IAccessible类的属性和函数:
accessible.accChildCount 子控件数
accessible.accValue; accessible.accName 控件数值和名字这两个属性vs提示是有的,不过不知为何运行时会报错,要换成下面两个函数才行,还有其它几个属性应该也是这样。
result.get_accValue(0);result.get_accName(0) VS对参数的提示是[object VarChild =Type.Missing],然而填Type.Missing报错,这参数好像表示子空间的序号,0表示查询本控件,嗯,填0就好。
Inspect.exe和AccExplorer32.exe都可以查询窗口的IAccessible层级结构和IAccessible控件的具体信息,第一个功能比较多,后一个有汉化版,大家自己取舍。
再介绍详细介绍一下SendMessage的用法。static extern int SendMessage(IntPtr hWnd,uint Msg, int wParam, int lParam);
Msg 代表操作类型,很好查的,常用的有:
//按下按钮
const int WM_KEYDOWN = 0x100;
//放开按钮
const int WM_KEYUP = 0x101;
//发送字符
const int WM_CHAR = 0x102;
//应用程序发送此消息来设置一个窗口的文本
const int WM_SETTEXT = 0x0C;
//当一个窗口或应用程序要关闭时发送一个信号
const int WM_CLOSE = 0x10;
//当用户选择结束对话框或程序自己调用ExitWindows函数
const int WM_QUERYENDSESSION = 0x11;
//用来结束程序运行,会关闭窗口所属的整个程序
const int WM_QUIT = 0x12;
//按下鼠标左键
const int WM_LBUTTONDOWN = 0x201;
//释放鼠标左键
const int WM_LBUTTONUP = 0x202;
//双击鼠标左键
const int WM_LBUTTONDBLCLK = 0x203;
//使用鼠标滚轮
const int WM_MOUSEWHEEL = 0x020A;
wParam和lParam是32位数。
按钮事件,wParam代表键值,具体可以查;lParam代表点击的点击次数、组合键等信息,msdn上有张表介绍各个位的作用,不过我没用过。
发送字符,每次发送一个char,wParam代表char的值转换成int类型就行,lParam为0。
对于鼠标点击事件,wParam代表组合键,没有的话为0;lParam代表点击的位置,低字为x坐标,高字为y坐标,即x+(y<<16),这个坐标是相对于屏幕而言的。
对于鼠标滚轮事件,wParam高字代表滚动距离,向上为正,向下为负,低字代表组合键,没有的话为0;lParam代表点击的位置,低字为x坐标,高字为y坐标,即x+(y<<16),这个坐标是相对于窗口而言的。
由于只有窗口句柄,使用滚轮事件,发送字符和按钮事件时,需要获取相应区域的焦点,我是用鼠标点击事件做的。
向DirectUI发送文本这件事困扰了我好久,试过SetText事件,没有作用,只能将文本拆成char数组发送字符。但发送中文就会乱码,SendMessage发送中文用的是GBK编码,String是Unicode编码,需要进行转换,相应的函数是有的,要先把String转换成Byte数组再转换成Char数组,解码器有好几种,试了好久发现下面这种组合是可以的。
using System.Text;
char[] charsC = UnicodeEncoding.Unicode.GetChars(UnicodeEncoding.GetEncoding("GBK").GetBytes(strText));
高兴了好久发现,这样字母和数字乱码了。所以把正常的Unicode字符放入另一个数组,两个混着用。
char[] chars = strText.ToCharArray();
英文字符是用八位表示的,中文字符用16位,c#里char是16位的,在charsC里,相邻2个英文字符被放到了一个char里。所以,两个数组的长度是不一样的。GBK编码的中文字符从0x8140到0xfea0,键盘上的英文字符最大为0x7e,用0x7e7e作为分界区分中文和英文字符。啊,这样中英文都能发了。
不过后来的测试中发现有些汉字这样发送也会乱码,于是我决定自己把Byte数组转换成Char数组,以0x7e为界,大于的就属于中文字符,和下一个字节一切放到一个Char里,否则就是英文字符,直接转换成Char类型。
最后我在测试的时候发现直接把Byte数组发过去就行了。。以下是最终的函数:
private void sendText(IntPtr hwnd,String strText)
{
byte[] charCByte = UnicodeEncoding.GetEncoding("GBK").GetBytes(strText);
for (int i = 0; i < charCByte.Length; i++)
{
SendMessage(hwnd, WM_CHAR, (int)charCByte[i], 0);
}
}
本文由本人查询资料整理加工而得,既为方便自己查阅,也为方便他人搜索。水平有限,如有错漏,希望大神能指点,既是帮我,也是帮了其他看贴的同志。