一款小工具DeskPinsEx开发笔记

来源:互联网 发布:物流运输软件 编辑:程序博客网 时间:2024/05/18 03:19

【模仿DeskPins的过程坎坎坷坷,本来想两三天的事情,结果做了一周多的时间。记录一下开发这个工具的过程,与诸位分享。】

网上有个叫 DeskPins的小工具,它可以让一个或几个窗口置顶,但是有时候仅置顶并不能满足需要,我还希望能把置顶的窗口透明。于是我便有了要自己写一个DeskPinsEx的想法。当然,自己目前还是在学WinSDK,所以就拿这个项目来练练手。

         DeskPins的主要功能说起来比较简单

1.   它有一个只有托盘图标,没有主窗口,点击这个图标后,鼠标变为大头针样式(这项功能在win8/win7 64位上无法正常实现,估计是由于它装了全局钩子,写在了32位的dll里。要知道DeskPins的制作日期是在2004年左右,当时64位机应该不多作者没有考虑这一点。所以我也暂不考虑鼠标变形^_^),然后选择一个窗口,如果选择了一个子窗口,它会提示“无法使子窗口置顶”,并把父窗口置顶;选择父窗口的话将直接将其置顶。

2.   支持全局自定义快捷键,按下之后把当前活动窗口置顶。对于置顶的窗口,会在右上角靠近窗口最小化位置显示一个大头针的按钮,就像这样:


当然我们还可以看到,当鼠标移动到大头针按钮上时,变成了指针+叉号的形状。点击这个大头针,可把置顶的窗口取消置顶。

3.当然它支持同时置顶多个窗口。

4.另外,当含有大头针的窗口最小化后,它的大头针也会隐藏(这好像是废话~)

它的主要功能大概就是这个样子。我们的目的就是要实现这些功能(其他的还支持自定义设置),再加上支持窗口透明功能。

好,分析完它的功能,开始动手写DeskPinsEx。

 

Step 1.做出支持选择窗口,并将其置顶:

         这个怎么做呢。我的第一个想法是全局钩子,当点击主窗口(简单起见先做出主窗口,后期再去掉)上的一个按钮时,安装全局钩子,捕获鼠标点击事件,即当鼠标左键按下的时候取得鼠标所在点的窗口的句柄,然后一切都好办了。SetWindowPos()置顶/取消置顶。

         Step2.要在选择的窗口标题栏上画一个大头针按钮。这就有点技术含量了。

【方法一】非客户区绘制按钮可以利用GDI绘图函数,捕获WM_NCPAINT及一些非客户区鼠标消息来实现在标题栏画图。但是但是。。经过试验,在标题栏上绘图,我发现绘上的东西被标题栏遮住了!多方了解,原来这个方法不适用于Vista及以后的系统,为什么呢,这是因为从vista开始,windows采用一种新的窗口管理方式,叫做DWM(Desktop Window Manager),有没有觉得从vista开始,系统的标题栏有点怪怪的?不光透明了不说,它还可以任意大小(比如IE中的导航栏,画图工具的快速访问工具栏等)。所以,以前的方法不能用了,因此必须找其他方法。

【方法二】在win8下DeskPins仍然能正常使用,那么它是怎样在标题栏上画了图,还不被标题栏遮住呢?我在这卡了很长时间。后来想,难道那个大头针真的是一张图片么?它能响应按钮单击,并且当鼠标进入/离开的时候能改变鼠标形状,还不被标题栏遮挡。你有没有想到什么类似的东西?对了,就是窗口,于是我猜想,它是不是一个窗口?这便可以祭出VS自带神器——Spy++(想监视64位窗口就选x64的,监视32位窗口选x86的。不是根据操作系统的位来随意使用),来验证猜想,点查找窗口,把光标拖到大头针上,发现它果然是一个窗口,而且利用Spy++还能看到它的详细信息,比如风格、父窗口句柄、所有者窗口句柄等等等等。。。。(不得不说,这个想法是在睡觉之前突然想的,想到之后从床上下来,打开电脑,当看到Spy++侦测到大头针窗口时很是开心)

 

到了这一步,问题似乎简单了不少,创建一个子窗口,它能处理WM_LBUTTONDOWN来实现点击取消选择窗口的置顶。。。然后忽然发现如果只这样做,似乎想实现自动跟随那个置顶的窗口就不太容易了。从网上得到的一个好消息是Windows提供了所有者窗口与被拥有窗口这样一组关系,与父子窗口不同,前一组窗口保证被拥有的窗口永远显示在所有者窗口的前面,并可以在屏幕上任意移动;而后一组窗口是这样的:父窗口的客户区坐标系统将用来确定子窗口的位置,子窗口只能显示在父窗口客户区内部,超出的部分将被裁剪。这两组关系的被拥有窗口/子窗口与所有者窗口/父窗口同时显示或最小化。显然,应使大头针与选择的窗口成为所有者与被所有的关系。

不过还有一个坏消息,与父子窗口不同,父子窗口关系的设置/获得可通过 SetParent()与GetParent()来完成,但是所有者与被所有者窗口的关系,MS只提供一个GetWindow(hwnd,GW_OWNER)来获得所有者窗口,而没有提供SetOwner()等类似的API函数设置所有者窗口。这里又困扰了我很久,不过既然DeskPins已经能做出来了,肯定是有代替方法的。

后来发现可以通过CreateWindowEx(或CreateWindow)时把hParent参数设为父窗口句柄(以前我一直写的是NULL,然后SetParent()),但是并不指定创建窗口风格为WS_CHILD,而是指定为WS_POPUP,这样CreateWindowEx()的返回值与hParent就是所有者与被拥有的关系,而不是父子关系。于是,离所需又进了一步。

Step 3.实现大头针跟随

同样也有N种方法,

【方案一】我已经写了钩子,顺理成章的考虑再写一个全局钩子,比如捕获窗口的WM_SIZE,WM_MOVE等消息来跟随,但是这种方法仔细一想不好写,并且一旦使用全局钩子,还考虑很多细节。因为要判断这个发出消息的窗口有没有一个大头针,而不应只从窗口置顶来看(置顶的窗口可不一定是有大头针的窗口),此外,也不好同时支持多个大头针。再想。。一定还有其他方法。

【方案二】其实Spy++(x86)还能监视指定窗口接收的消息(这是我在做DeskPinsEx过程中偶然学到的这个神器),我们选取DeskPins的大头针,然后发现一连串的WM_TIMER消息,还有WM_WINDOWPOSCHANGED这样的消息。。。。原来如此!给大头针开个线程,然后创建计时器,每几毫秒根据所有者窗口重新设置位置。然后就发现连点击大头针取消置顶、同时支持多个大头针也好办了。

Step 4 To N:

好了。还剩下设置窗口透明度、去掉主窗口、加托盘图标及托盘快捷菜单、全局热键,就算基本上完成了(其实还应该加上设置选项,比如开机自启,大头针颜色等,但是还要去急着写一个Flappy Bird…)。窗口透明度就用一个滚动条(Trackbar)就行了,其实这时候就可以把主窗口风格改为WS_POPUP和WS_EX_TOOLWINDOW,去掉边框和任务栏按钮,并且不ShowWindow(),只留下一个滚动条,当在托盘上点击右键菜单的“设置窗口透明度”后ShowWindow(),拖动滚动条时改变目标窗口的透明度即可。这不论是说还是做都比较简单~。

【几个Tips】

1.  关于钩子:

a) 使用全局钩子时必须写一个dll,然后把钩子处理函数写进dll里,因为不同程序的地址空间是隔离的,不放到dll里,那在程序自身代码里的函数地址对于其他程序是不可见的,自然也就无法实现全局钩子的作用。Win8_64对于WH_JOURNALRECORD以及WH_JOURNALPLAYBAKC的钩子好像做了限制,一直安装失败。所以这里我用了WH_MOUSE_LL代替。

b)  想对64位程序使用全局钩子就必须写到x64方式编译的dll里,对32位程序使用全局钩子必须写win32方式编译的dll里。不可混用。这也就是为什么Spy++(x64)不能捕获32位程序消息的原因,反之亦然。如果一个全局钩子只能捕获自己程序的消息,八成是这种问题。

2. Dll注入

本来用DLL注入方式来实现大头针跟随、点击后删除的功能:但是但是。。。因为从vista开始MS加强了系统安全级别(比如加入了UAC控制),DLL注入的方法需要提升一个程序隐藏权限。。额。。至于是什么权限。。目前未知。

【一些细节】

1. 在大头针处理WM_TIMER时要调用SetWindowPos(),

它的原型如下 :

BOOLSetWindowPos(  HWND hWnd,             // handle to window  HWND hWndInsertAfter,  // placement-order handle  int X,                 // horizontal position  int Y,                 // vertical position  int cx,                // width  int cy,                // height  UINT uFlags            // window-positioning flags);

如果uFlags参数设置不正确,就会出现问题。比如没有设置SWP_NOACTIVATE,那么大头针会获得输入焦点,造成所有者窗口不能被激活;等等
正确的方式应该如下 :
SetWindowPos(hwnd, HWND_TOP, x, y, 0, 0, SWP_NOSIZE | SWP_NOACTIVATE | SWP_NOOWNERZORDER | SWP_NOZORDER);
不调整大小,不激活,不改变大头针和所有者窗口的z顺序.
2.托盘右键菜单TrackPopupMenu()后无法自动消失,只有当选择一个菜单项后才消失,这不是我们希望的。在TrackPopupMenu()之前调用SetForegroundWindow(hwnd);即可解决问题,hwnd是主窗口句柄。
3.不应当允许DeskPinsEx多次加载,这可以在Dll里限制。方法也有多种,比较简单的方法如下:
若FindWindow(lpszClassName,lpszWindowName)返回非空,则说明是第二次运行,应退出。
 
0 0
原创粉丝点击