wpf C# 操作DirectUI窗口 SendMessage+MSAA
2021-03-22 21:25
最近做一个抓取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_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
对于鼠标滚轮事件,wParam高字代表滚动距离,向上为正,向下为负,低字代表组合键,没有的话为0;lParam代表点击的位置,低字为x坐标,高字为y坐标,即x+(y
由于只有窗口句柄,使用滚轮事件,发送字符和按钮事件时,需要获取相应区域的焦点,我是用鼠标点击事件做的。
向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
????{
????????SendMessage(hwnd, WM_CHAR, (int)charCByte[i], 0);
????}
}
本文由本人查询资料整理加工而得,既为方便自己查阅,也为方便他人搜索。水平有限,如有错漏,希望大神能指点,既是帮我,也是帮了其他看贴的同志。
文章标题:wpf C# 操作DirectUI窗口 SendMessage+MSAA
文章链接:http://soscw.com/index.php/essay/67672.html