您现在的位置是:首页 > 文章详情

wpf C# 操作DirectUI窗口 SendMessage+MSAA

日期:2018-08-13点击:295
原文: wpf C# 操作DirectUI窗口 SendMessage+MSAA

最近做一个抓取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);

    }

}

本文由本人查询资料整理加工而得,既为方便自己查阅,也为方便他人搜索。水平有限,如有错漏,希望大神能指点,既是帮我,也是帮了其他看贴的同志。

原文链接:https://yq.aliyun.com/articles/677810
关注公众号

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。

持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。

文章评论

共有0条评论来说两句吧...

文章二维码

扫描即可查看该文章

点击排行

推荐阅读

最新文章