3.Windows中的句柄

在上一篇的MoveWindow例子中有提到"窗口句柄" 那么什么是窗口句柄 什么又是句柄来着?看是看百科吧:

http://baike.baidu.com/view/194921.htm?fr=aladdin

把百科里面的第一段话拷贝过来:

句柄,是整个Windows编程的基础。一个句柄是指使用的一个唯一的整数值,即一个4字节(64位程序中为8字节)长的数值,来标识应用程序中的不同对象和同类对象中的不同的实例,诸如,一个窗口,按钮,图标,滚动条,输出设备,控件或者文件等。应用程序能够通过句柄访问相应的对象的信息,但是句柄不是一个指针,程序不能利用句柄来直接阅读文件中的信息。如果句柄不用在I/O文件中,它是毫无用处的。 句柄是Windows用来标志应用程序中建立的或是使用的唯一整数,Windows使用了大量的句柄来标志很多对象。

其实一个句柄 就是一个数字 这个数字用来唯一标识一个对象 感觉就好比人的一个身份证号码一样 一个号码唯一标识一个人、、、在Window中也是用一个数字去标识一个对象的 有了这些数字Window才能唯一确定一个对象 比如上一篇的MoveWindow函数中的第一个参数 就是一个窗体的句柄 然后函数就会根据这个句柄找到对应的窗体 然后对其进行操作 但是我要强调的是:

不要以为以提到句柄就认为是窗体的句柄 句柄绝不是窗体的专利 只是平时编程的时候窗体句柄用的比较多 还有其他的句柄来着 比如模块句柄(HMODULE) 实例句柄(HINSTANCE) DC句柄(HDC)、、等、、、、还有一点 在Window中尤其是在一些Api函数中看到HWND的时候 不要以为他就只是一个窗体的句柄 在Windows中一个按钮一个文本框什么的 任何一个控件都叫做窗口 只是他们的样式不一样所以看到HWND它可以是一个窗体 也可以是一个按钮 只要是一个Windows的控件都可以称之为窗口

这里说了这么多 估计也差不多了 现在知道句柄大概是个什么东西了 那么这个句柄到底在哪呢?哪呢?哪呢?、、在C#中每个控件都有一个Handle属性(窗体也是一个控件 - Form : ContainerControl : ScrollableControl : Control) 你随便搞一个WinForm工程 然后写上如下代码:

private void Form1_Load(object sender, EventArgs e) {
    this.Text = this.Handle.ToString();
    foreach (Control ctrl in this.Controls) {
        ctrl.Text = ctrl.Handle.ToString();
    }
}
然后随便放一点会显示Text属性的控件上去 然后运行 效果如下:

窗体和每个控件显示的都是自己的句柄  不过注意、、这个句柄不是固定的 千万注意 这个句柄是随机分配的 不是固定的 如果固定的那还得了 那么多程序那么多控件那么多窗体用的过来么、、所以你每次运行程序的时候 看到的显示都不一定是一样的 这里说的每次运行都是不一样的 你就不要理解成运行一次 那么这个程序中的所有句柄在这一次中都是固定的了 如果你一个程序有很多窗体 你关闭了 然后再打开一个窗体那个窗体的句柄又变了 而不是看你程序没有重新运行  一个窗体或者控件被销毁的时候 他的句柄也会被销毁 重新创建的时候又临时给他分配而已

正如上面所以 在Windows中一个控件也叫窗口 所以在上一篇的MoveWindow函数中 第一个参数也可以是其他控件的句柄 比如button1的

有了这些句柄就可以调用很多一些窗口相关的函数了比如Get/SetWindowXXX之类的函数 不过大多数情况下对于自己的程序没有必要应为在.Net中我们可以直接访问或者修改自己程序的窗体或者控件的一些信息 没有必要搞的那么麻烦 而通常情况下 都是为了去访问外界程序的时候 这些东西就很有用了 这里就说道重点了 要访问外界的程序那么首先得知道外界窗体的句柄啊?如果是自己的程序可以通过。XXX.Handle来获得 可是外界的怎么获取?

这里先介绍一个工具spy++在安装vs的时候应该都会有这个东西(我发现有些人的没有不知道是不是安装过程中要勾选啥的 我没有注意)在vs的安装目录下可以找到或者vs工具栏上也有:

如果没有就自己下载一个 这个东西很方便的 如下图:

点击工具上面的望远镜 然后弹出来一个窗体 然后把圆圈拖到指定窗口上就可以了 Handle显示的就是句柄是用16进制显示的Caption就是窗口的标题 而Class就是窗口的类名

然后就可以用那个句柄来写程序了在C#中句柄一般用IntPtr类型 其实用int也可以 反正函数的参数接收一个四字节的参数就是了 但是正如刚才说所一个窗口的句柄并不是固定的 如果现在我用这个句柄写了一个程序 那么这个窗口在没关闭的时候 程序都是有效的 但是如果一旦窗口关了 再打开句柄就不一定一样了 那么程序可能也就废了 所以很明显这个句柄不能在程序里面固定死、、

Windows当然也提供了去获取一个窗体句柄的函数比如FindWindow还有他的升级版函数FindWindowEx 这两个是用的比较多的 要访问外界的通常情况下你的去找一下看看现在桌面是有没有这个窗体的存在 那么要怎么去找?先来看看第一个函数的函数签名和说明:

函数功能:
    该函数获得一个顶层窗口的句柄,该窗口的类名和窗口名与给定的字符串相匹配。
    这个函数不查找子窗口。在查找时不区分大小写。
函数型:
    HWND FindWindow(LPCTSTR IpClassName,LPCTSTR IpWindowName);
参数:
    IpClassName :指向一个指定了类名的空结束字符串,或一个标识类名字符串的成员的指针。
        如果该参数为一个成员,则它必须为前次调用theGlobafAddAtom函数产生的全局成员。
        该成员为16位,必须位于IpClassName的低 16位,高位必须为 0。
    IpWindowName:指向一个指定了窗口名(窗口标题)的空结束字符串。
        如果该参数为空,则为所有窗口全匹配。
返回值:
    如果函数成功,返回值为具有指定类名和窗口名的窗口句柄;如果函数失败,返回值为NULL。
    
若想获得更多错误信息,请调用GetLastError函数。

备注:Windows CE:若类名是一个成员,它必须是从 RegisterClass返回的成员。
速查:
    Windows NT:3.1以上版本;Windows:95以上版本;Windows CE:1.0以上版本;
    头文件:Winuser.h;库文件:user32.lib; Unicode:
    在 Windows NT上实现为 Unicode和 ANSI两种版本。

函数里面两个参数类型可能看着有点别扭啊 在C#中写成string就可以了(LPCTSTR LP表示long Pointer长指针 CT表示const常量、、) 其实两个参数简单来说就是第一是窗口的类名 第二个是窗口的标题 什么"空结束字符串"在这里你不需要知道(比如字符串"ABC"在内存中要用四个字节来表示分别是ABC个一个字节还有一个字节是0x00一个空的数据来表示这个字符串到尾了 在C#中暂时可以不用去管这些细节的东西)或许窗口的标题你知道(你就当是Text属性吧) 然后就是类名 注意 这个类名不是写程序时候那个class的名字的那个类名 这里我也说不清楚 如果用过CreateWindow函数 或许你就知道这个类名是啥了、、

这两个参数 你可以指定其中一个值 也可以两个一起 因为你可能只想找一下桌面看看有没有记事本的窗体 而记事本显示的是啥标题是不固定的得看文本文件的名字 所以现在来写一个程序 点击一个按钮然后去找桌面看看有没有记事本 如果有 那么调用MoveWindow函数将它移动到左上角去 代码如下:

[DllImport("user32.dll")]
public static extern bool 
    MoveWindow(IntPtr hWnd, int x, int y, int width, int height, bool bRePaint);
[DllImport("user32.dll")]
public static extern IntPtr FindWindow(string strClassName, string strText);

private void button1_Click(object sender, EventArgs e) {
    IntPtr hWnd = FindWindow("Notepad", null);
    if (IntPtr.Zero != hWnd)
        MoveWindow(hWnd, 100, 100, 200, 200, true);
    else
        MessageBox.Show("Not found window");
}
- -!、、我这人呢比较懒 没啥特殊要求DllImport里面一般不会加上那些可选参数 所以你看到的别人的代码申明方式可能和我的不一样我没写那么全  不过注意调用FindWindow的时候如果你不知道窗体的名字 传null就是了 类名同理 不要传【""】 空字符和空是两个区别 一定注意啊  当然写的时候 你可以把GetLastError加上 .Net对此有封装的:
private void button1_Click(object sender, EventArgs e) {
    IntPtr hWnd = FindWindow("Notepad", null);
    if (IntPtr.Zero != hWnd)
        MoveWindow(hWnd, 100, 100, 200, 200, true);
    else {
        int errNum = Marshal.GetLastWin32Error();
        if (errNum != 0) {
            MessageBox.Show(new Win32Exception(errNum).Message);
        }
        MessageBox.Show("Not found window");
    }
}
像上面一样如果没有找到窗口看看是不是可能调用出错了 如果没有出错 那就表示可能真的没有找到窗体了(- -!估计这个函数也应该不会存在啥调用错误的问题) 然后运行程序 如果桌面没有记事本 那么就会提示 没有找到窗口 如果有的话 那么就会返回找到的第一个记事本窗口的句柄 是的 是第一个如果你桌面开了好几个记事本 那么程序始终是只对一个生效的 其他的没用 所以才有了FindWindow的升级版本函数FindWindowEx(这里你可能有点郁闷了 微软为什么搞两个出来 直接把原来FindWindow的代码改一下不就对了 的确可以那样干 但那是这样干就悲剧了 假设winXX诞生的时候直接把FindWindow的代码改成FindWindowEx的代码只留一个函数 那么估计上面写的那个程序在winXX上就运行不了了 因为FindWindowEx需要接收四个参数而上面写的代码是两个参数的 调用的时候就悲剧了 所以这是为了兼容以前老程序才没有直接把就的函数替换掉而是写一个新的函数 有很多带有Ex后最的函数)

来看看FindWindowEx的签名 只看签名 说明就不发上来了 自己百度

HWND FindWindowEx(HWND hwndParent,HWND hwndChildAfter,LPCTSTR lpszClass,LPCTSTR lpszWindow);
多了前面两个参数根据HWND你可以知道 是两个窗口句柄而第一个是父窗口的句柄 第二个是一个窗口句柄的标识 也是就是如果给他一个值 那么就在父窗口(hwndParent)中的(hwndChildAfter)之后的窗口开始找 而后面两个参数不变 这里先来看一张图 是spy++的:

如果你没有看到如图的东西 点击一下圆圈那个图标、、你看到的是一个桌面的层次结构 要知道桌面也是一个窗体 而我们程序的窗体是在桌面中(就好比桌面是窗体 而我们的程序的窗体是桌面上的一个控件) 所以一看到的最外面的是Desktop然后里面有许许多多的窗口 窗口下可能又有窗口 这就是子父关系、、这里一定注意了 窗口或者窗体什么的 不仅仅是Form那个窗体 一个控件也叫窗口、、FindWindow只能找到一个 但是FindWindowEx却可以找到多个 把代码换成这样就可以了:

IntPtr hWnd = IntPtr.Zero;
while ((hWnd = FindWindowEx(IntPtr.Zero, hWnd, "Notepad", null)) != IntPtr.Zero) 
{
    MoveWindow(hWnd, 100, 100, 200, 200, true);
}

如果进去第一个参数传入空(NULL = 0)的话那么就会以桌面为父窗口 第二个参数同理如果是空以父窗口的第一个子窗口开始筛选 如果找到一个记事本的句柄 那么移动然后继续循环以桌面为父窗口开始而子窗口则是刚才找到的那个 那么会从它之后继续找 直到找不到为止 这样所有的记事本窗口都会跑到左上角去

通过FindWindowEx后面两个传null写一个递归调用 也可以做一个spy++一样的层次效果:

TreeNode node = new TreeNode("Desktop");
GetDesktopMap(IntPtr.Zero, IntPtr.Zero, node);
treeView1.Nodes.Add(node);
treeView1.ExpandAll();

private void GetDesktopMap(IntPtr hParent, IntPtr hAfter, TreeNode treeNode) {
    while ((hAfter = FindWindowEx(hParent, hAfter, null, null)) != IntPtr.Zero) {
        TreeNode node = new TreeNode(hAfter.ToString("X").PadLeft(8, '0'));
        treeNode.Nodes.Add(node);
        GetDesktopMap(hAfter, IntPtr.Zero, node);//用找到的句柄作为父窗口找子窗口
    }
}

运行效果如下:

左边是spy++的 右边是上面代码的效果或许你觉 两边怎么显示不一样 Windows桌面随时都在变化 我的代码运行的时候只获取了一次spy++也没有刷新 而spy++怎么获取的我也不知道 所以看到当然有点出入而且你仔细看 排列的顺序也有点不一样 但是结构都是差不多的 要像做成spy++那种效果 你还得调用GetWindowText来获取窗口标题 然后用GetClassName来获取类名 然后拼接到节点上显示就可以了 还有左边的图标 亮的表示窗口的Visable是true反之false  当然如果真要做一个spy++ 也不简单、、起码得对Windows有一点了解 spy++虽然很多时候被用来获取句柄什么的 但是不只这点功能spy++是Jeffrey写的 [Windows核心编程]一书也是出自他之手、、、很NB的一个人物、、、

这一篇就简单介绍到这里 如果找寻一个窗体可以事先就用spy++去看看相应的性息 对于一个窗口来说 一般类名是不会动不动就变过去变过来的 看程序的作者怎么想了 还有标题也是根据程序的情况而已 一般通过这两者性息去找一个窗体 当然要获取一个窗口句柄不只是这两个函数 还有其他的 只是这两个常用而已

还有一点 不要一提到句柄就只知道窗口句柄 还有其他的句柄 只是窗口句柄用都比较多而已(主要还是看你写什么样的程序) 还有 在Windows中一个控件 也叫窗口、、


添加时间:2014-05-10 23:19:06 编辑时间:2016-11-10 00:28:12 阅读:3179 
C#Windows编程 C#Win32
还没有人留言 要不你来抢一个沙发?
  • 编写评论

      我觉得区分大小写是一个码农的基本素质
[访问统计] 今天:28 总数:271142 提示:未成年人 请在大人陪同下浏览本站内容 还有:世界上最帅的码农 -> 石头 RSS:http://st233.com/rss Powered by -> Crystal_lz