实现系统滚动条换肤功能

2024-03-25 22:38

本文主要是介绍实现系统滚动条换肤功能,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!


对于Windows系统中各种控件换肤功能,要数滚动条的换肤最难实现了,尤其是控件自带的系统滚动条,如Edit、ListBox、ListView、TreeView等自带的系统滚动条,要想实现其自定义的皮肤功能,用常规办法似乎都无法实现。

对于常规的皮肤定制一般都是通过定制WM_PAINT、WM_ERASEBKGND、WM_CTLCOLORxxx、NM_CUSTOMDRAW来实现。然而系统滚动条的绘制,常规的、很阳光的方法行不通,微软把一条康庄大道堵死了。根据我的观察测试,系统滚动条有许多的消息都对其执行了绘制,这包括WM_NCPAINT、WM_NCMOUSEMOVE、WM_NCMOUSELEAVE、WM_HSCROLL、WM_VSCROLL、WM_KEYDOWN、WM_MOUSEWHEEL等等,这些消息中有些可以定制,但有些没法定制。比如WM_HSCROLL就无法定制,如果我们处理这个消息,就必须自己控制滚动条的范围、位置、翻页值,而这些值我们通常无法得到,微软根本没有告诉我们,对于不同的控件它操控滚动条到底采用何种策略,不晓得。假若我们不去定制这个WM_HSCROLL,而默认的处理它又执行滚动条的绘制,这真是进退无路,叫人束手无策。当然不是完全没有办法,死路一条。为了克服障碍,翻越障碍,每个人有不同的策略,有攀岩翻越的,安全性不高;也有走小路绕行的,比较费力。

在网上反复搜索,自己也仔细研究,基本有两种办法来实现系统滚动条换肤,一种方法是HOOK API,也就是拦截API的办法,还有一种是模拟法。

拦截API,实际上是修改操作系统的API入口。因为系统绘制滚动条是通过各种绘制函数来实现的,比如SetScrollInfo(),基本系统通过这一类函数实现滚动条的绘制,对这个API进行拦截,也就是我们自己写一个SetScrollInfo(),来亲自实现滚动条的绘制,以便由此实现滚动条的自定义绘制,实现我们想要的各种风格的皮肤外观。API的拦截有两种:一种是修改系统所装入内存可执行模块的导入地址,替换成我们所写的伪API的地址,使API调用能够自动跳转到这个伪API上;还有一种是直接修改API函数首地址处的若干机器指令,保存现场,写入跳转指令,使系统在执行到这个API时能自动跳转到我们所写的伪API上。我要说的是,这个拦截办法还真是有些邪门,有安全隐患,病毒就常干这种事情。对操作系统进行这类暴力破解式的修改,很容易引起系统防火墙或反病毒软件报错,Windows原则上不允许干这种事;其二,一旦使用此法的程序因异常而崩溃,原本对操作系统的修改没有还原,这可能会伤害到系统里的所有进程的稳定性,导致死机或运行异常。对于商业性的工程开发,往往很忌讳这一点,都倾向于追求稳定,通常是宁用拙法,不玩巧技;其三,此法有线程同步问题:因为正当你修改程序指令同时,若其他进程里的线程恰好执行到这里时候问题就会爆发出来,单CPU可能没啥问题,系统内存和CPU缓冲可以做到同步,但多核系统就难说了;另外这个方法还有移植性问题:在一种版本的系统中,通过拦截API有效,在另一种版本的系统中,就不见得还有效,毕竟微软实现滚动条的绘制,它没规定一定就用哪个函数来实现,也许新系统中它另有高招,API拦截也就不灵了。

下面我要详细讲的是模拟法了,这个办法不用拦截API,所有的技术实现都约束在系统所允许的范围内,咱一丝不苟的当遵纪守法的良民。

所谓模拟法就是在系统滚动条的区域放置一个模拟窗口,这个窗口专门用于绘制系统滚动条。当然我们也要拦截带滚动条控件的若干消息,以确保模拟窗口的绘制正确。下面只以ListView控件的水平滚动条为例,进行说明,垂直滚动条换肤可以列推。

首先我们要在滚动条的区域上创建一个模拟窗口,恰好覆盖滚动条:

HWND hListView = ...;//ListView窗口的句柄

HWND pWnd = ::GetParent(hListView);

HWND hBuddy = ::CreateWindowEx(WS_EX_NOACTIVATE, "Buddy_Window", "", WS_CLIPSIBLINGS|WS_DISABLED|WS_CHILD, 0, 0, 0, 0, pWnd, NULL, gModule, NULL);

Buddy_Window是你注册的模拟窗口类。注意,窗口一定要有WS_DISABLED风格,这是个关键,这个风格能够确保鼠标操作能够透过模拟窗口,而直接操控到所覆盖的滚动条,模拟窗口本身不接受任何鼠标键盘的输入。另外读取滚动条的矩形以及它的各个元素的可视、可用、压下等状态可通过GetScrollBarInfo()这个API来完成。

完成创建模拟窗口之后,你要给ListView安装一个你写的窗口过程,以拦截各种导致滚动条属性改变的种种消息:

LRESULT CALLBACK MyListViewProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)

{

case WM_LBUTTONDOWN:

    ......

case WM_LBUTTONDBLCLK:

    ......

case WM_NCMOUSEMOVE:

    ......

case WM_NCMOUSELEAVE:

    ...... 

}

......

gOldListViewProc = (WNNPROC)::GetWindowLong(hListView, GWL_WNDPROC);

::SetWindowLong(hListView, GWL_WNDPROC, LONG(&MyListViewProc));//安装窗口过程

 

消息处理

首先我们要拦截WM_NCLBUTTONDOWN 和 WM_NCLBUTTONDBLCLK这两个消息,当你在滚动条上按下鼠标时,就立刻触发WM_NCLBUTTONDOWN,如果连续快速按两下鼠标就还有WM_NCLBUTTONDBLCLK。注意双击时只有一个WM_NCLBUTTONDOWN消息,而不是两个。第二次按鼠标会出现一个WM_NCLBUTTONDBLCLK。实际上这两个消息我们完全可以等而视之,做相同的处理。系统在处理这个两个消息时,都会在其内部处理中触发许多其他消息,其中有若干个WM_HSCROLL、WM_CAPTURECHANGED、WM_NCMOUSELEAVE。我们主要是要处理WM_HSCROLL,因为它最有价值。在你松开鼠标后,系统发送并处理完最后一个WM_HSCROLL之后,这才从WM_NCLBUTTONDOWN或WM_NCLBUTTONDBLCLK中返回:

if(msg == WM_NCLBUTTONDOWN || msg == WM_NCLBUTTONDBLCLK)

{

    //注意默认处理会有N个WM_HSCROLL消息出现,要等你的按下拖拽操作完成后,这个调用才返回

    //在这个调用内部,我估计系统会进入一种消息循环,因为按住左键之后,WM_NCMOUSEMOVE

    //和WM_NCLBUTTONUP都不再触发了。其内部估计是捕捉了WM_NCMOUSEMOVE消息,因之反复刷新滚动

    //盒的位置,若有必要你可安装鼠标钩子,以捕捉鼠标移动消息,以及时刷新模拟窗口中滚动盒的

    //的位置。若只是响应WM_HSCROLL消息,你可能觉得滚动盒的拖拽比较滞,不平滑。

   

    //SetWindowsHookEx(...);

    LRESUTL code = ::CallWindowProc(gOldListViewProc, hListView, msg, wParam, lParam);

    //UnhookWindowsHookEx(...);

   

    return code;

}

 

我建议大家去微软的网站下载ControlSpy 2.0这个小工具,它用来监视控件的消息,这东西很有用。

下面我们再看WM_HSCROLL消息,这个消息一般是系统处理WM_NCLBUTTONDOWN或者WM_NCLBUTTONDBLCLK时产生的,但有时也可能是程序刻意发送的,和滚动条操作没有关系。

if(msg == WM_HSCROLL)

    ::CallWindowProc(gOldListViewProc, hListView, msg, wParam, lParam);

    //1)读取滚动条的数据

    //2)再在模拟窗口上绘制滚动条

    //......

    return 0;

}

另外还有许多其他的消息可能导致滚动条属性的变化,如用户的键盘操作、鼠标滚轮操作可能导致滚动条的Thumb位置发生改变,这些操作分别导致WM_KEYDOWN和WM_MOUSEWHEEL,而系统又在这两个消息中执行滚动条的绘制,因此你必须截获它们,但处理方法同WM_HSCROLL,这里不再啰嗦了。

当我们没有按下鼠标左键时,只是简单地在滚动条移动鼠标时,我们会发现滚动条的按钮和Thumb都会自动高亮,其实这些变化都是系统在WM_NCMOUSEMOVE和WM_NCMOUSELEAVE中绘制完成的。比如当鼠标进入到Thumb上时,它就高亮了,当鼠标离开它,它又变灰色了,你要分别处理WM_NCMOUSEMOVE和WM_NCMOUSELEAVE这两个消息。注意只有ListView应用主题风格之后才可能有WM_NCMOUSELEAVE消息,传统风格的ListView就没有这个消息,只有WM_NCMOUSEMOVE消息,因此处理高亮还真有些麻烦。正是因为主题风格和传统风格的差异,因此我建议你也处理WM_THEMECHANGED消息,以识别不同的主题模式,实现不同高亮处理。看上面那个图,TreeView的滚动条是主题风格的,但ListView的滚动条是传统风格的,但我加了换肤功能。

另外,我们还应当拦截处理ListView的WM_NCPAINT,将滚动条的区域抠掉,不让它绘制滚动条。尽管滚动条被模拟窗口遮住了,但还是有必要这样做,以防止出现意外情况:

if(msg == WM_NCPAINT)

{

    HRGN wRgn = NULL; 

    RECT sbRect = GetScrollBarRect();//读取滚动条矩形的一个函数

    HRGN sRgn = ::CreateRectRgn(sbRect.left, sbRect.top, sbRect.right, sbRect.bottom

    if(wParam == 1)

    {

        RECT wRect;

        ::GetWindowRect(hListView, &wRect);

        wRgn = ::CreateRectRgn(wRect.left, wRect.top, wRect.right, wRect.bottom););

        wRgn = ::CombineRgn(wRng, wRgn, sRgn, RGN_DIFF);

    }

    else

    {

        wRgn = (HRGN)wParam; 

        wRgn = ::CombineRgn(wRng, wRgn, sRgn, RGN_DIFF);

    }

    ::DeleteObject(sRgn);

    ::CallWindowProc(gOldListViewProc, hListView, WM_NCPAINT, WPARAM(wRgn), 0);

    if(wParam == 1)

        ::DeleteObject(wRgn);

    return 0;

}

另外,还要拦截WM_WINDOWPOSCHANGING消息,因为当ListView的位置、尺寸、Z秩序发生改变时要及时调整模拟窗口的位置、尺寸、Z秩序。为何要在WM_WINDOWPOSCHANGING中进行,而不选择在WM_WINDOWPOSCHANGED或WM_MOVE或WM_SIZE中进行呢?因为在WM_WINDOWPOSCHANGING中处理,可让模拟窗口先于ListView调整自己的位置、尺寸和Z,可避免一些绘制问题。实际我在FreeCL中既用了WM_WINDOWPOSCHANGING,也用到WM_WINDOWPOSCHANGED两个消息。在处理WM_WINDOWPOSCHANGING时调整位置、尺寸、Z秩序,在处理WM_WINDOWPOSCHANGED时,强制重绘模拟窗口。

末了还要说明一点的是,系统可能因为用户改变控件尺寸导致其滚动条自动消失或自动显示,或者调整滚动条的可用状态,如你拉宽支持多行显示的Edit控件,会导致滚动条箭头按钮变灰,Thumb消失。这些动作通常都是在WM_WINDOWPOSCHANGED中完成的,因此这个消息也需要拦截处理:

if(msg == WM_WINDOWPOSCHANGED)

{

    LRESULT lReturn = ::CallWindowProc(gOldListViewProc, hListView, WM_WINDOWPOSCHANGED,

        wParam, lParam);

    if(::GetNextWindow(hBuddy, GW_HWNDNEXT) != hListView)

    {   //调整模拟窗口的Z-Order,确保其紧贴ListView之上

        HWND hAbove = ::GetNextWindow(hListView, GW_HWNDPREV);

        UINT flag = SWP_NOACTIVATE|SWP_NOREDRAW|SWP_NOMOVE|SWP_NOSIZE|SWP_NOSENDCHANGING;
            ::SetWindowPos(hBuddy, hAbove?hAbove:HWND_TOP, 0, 0, 0, 0, flag);

    }

    //测试滚动条的显隐状态是否改变,并因之调整模拟窗口的显隐状态

    //如果滚动条仍旧显示或滚动条某些元素的显示状态有变化或控件位置、尺寸可能改变,

    //强制重绘模拟窗口

    //......

    return lReturn;

}

最后要处理WM_SHOWWINDOW和WM_DESTROY,当ListView隐藏时让模拟窗口也随之消失;当它显示时,让模拟窗口也随之显示。当ListView销毁时会触发WM_DESTROY,这时也需要销毁模拟窗口,这很重要。 

总的来讲,模拟法是蛮麻烦的,但比较安全,可靠性高。对开发者来讲,都是对消息进行特定处理,是常规手段,对最终用户来讲,用此法实现的滚动条自绘,完全能够满足要求,具有充分的自由度,甚至可以对滚动条添加更多特定的功能,比如可以在上面添加其他按钮,就算是加动画、加广告也都是可以的。


文章和截图来自这里:http://blog.sina.com.cn/s/blog_4c3538470100gews.html


这篇关于实现系统滚动条换肤功能的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/846464

相关文章

如何使用C#串口通讯实现数据的发送和接收

《如何使用C#串口通讯实现数据的发送和接收》本文详细介绍了如何使用C#实现基于串口通讯的数据发送和接收,通过SerialPort类,我们可以轻松实现串口通讯,并结合事件机制实现数据的传递和处理,感兴趣... 目录1. 概述2. 关键技术点2.1 SerialPort类2.2 异步接收数据2.3 数据解析2.

mybatis-plus 实现查询表名动态修改的示例代码

《mybatis-plus实现查询表名动态修改的示例代码》通过MyBatis-Plus实现表名的动态替换,根据配置或入参选择不同的表,本文主要介绍了mybatis-plus实现查询表名动态修改的示... 目录实现数据库初始化依赖包配置读取类设置 myBATis-plus 插件测试通过 mybatis-plu

Qt把文件夹从A移动到B的实现示例

《Qt把文件夹从A移动到B的实现示例》本文主要介绍了Qt把文件夹从A移动到B的实现示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学... 目录如何移动一个文件? 如何移动文件夹(包含里面的全部内容):如何删除文件夹:QT 文件复制,移动(

Flask 验证码自动生成的实现示例

《Flask验证码自动生成的实现示例》本文主要介绍了Flask验证码自动生成的实现示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习... 目录生成图片以及结果处理验证码蓝图html页面展示想必验证码大家都有所了解,但是可以自己定义图片验证码

VSCode配置Anaconda Python环境的实现

《VSCode配置AnacondaPython环境的实现》VisualStudioCode中可以使用Anaconda环境进行Python开发,本文主要介绍了VSCode配置AnacondaPytho... 目录前言一、安装 Visual Studio Code 和 Anaconda二、创建或激活 conda

使用mvn deploy命令上传jar包的实现

《使用mvndeploy命令上传jar包的实现》本文介绍了使用mvndeploy:deploy-file命令将本地仓库中的JAR包重新发布到Maven私服,文中通过示例代码介绍的非常详细,对大家的学... 目录一、背景二、环境三、配置nexus上传账号四、执行deploy命令上传包1. 首先需要把本地仓中要

JAVA封装多线程实现的方式及原理

《JAVA封装多线程实现的方式及原理》:本文主要介绍Java中封装多线程的原理和常见方式,通过封装可以简化多线程的使用,提高安全性,并增强代码的可维护性和可扩展性,需要的朋友可以参考下... 目录前言一、封装的目标二、常见的封装方式及原理总结前言在 Java 中,封装多线程的原理主要围绕着将多线程相关的操

MySQL中实现多表查询的操作方法(配sql+实操图+案例巩固 通俗易懂版)

《MySQL中实现多表查询的操作方法(配sql+实操图+案例巩固通俗易懂版)》本文主要讲解了MySQL中的多表查询,包括子查询、笛卡尔积、自连接、多表查询的实现方法以及多列子查询等,通过实际例子和操... 目录复合查询1. 回顾查询基本操作group by 分组having1. 显示部门号为10的部门名,员

Windows系统下如何查找JDK的安装路径

《Windows系统下如何查找JDK的安装路径》:本文主要介绍Windows系统下如何查找JDK的安装路径,文中介绍了三种方法,分别是通过命令行检查、使用verbose选项查找jre目录、以及查看... 目录一、确认是否安装了JDK二、查找路径三、另外一种方式如果很久之前安装了JDK,或者在别人的电脑上,想

java导出pdf文件的详细实现方法

《java导出pdf文件的详细实现方法》:本文主要介绍java导出pdf文件的详细实现方法,包括制作模板、获取中文字体文件、实现后端服务以及前端发起请求并生成下载链接,需要的朋友可以参考下... 目录使用注意点包含内容1、制作pdf模板2、获取pdf导出中文需要的文件3、实现4、前端发起请求并生成下载链接使