实现系统滚动条换肤功能

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

相关文章

不懂推荐算法也能设计推荐系统

本文以商业化应用推荐为例,告诉我们不懂推荐算法的产品,也能从产品侧出发, 设计出一款不错的推荐系统。 相信很多新手产品,看到算法二字,多是懵圈的。 什么排序算法、最短路径等都是相对传统的算法(注:传统是指科班出身的产品都会接触过)。但对于推荐算法,多数产品对着网上搜到的资源,都会无从下手。特别当某些推荐算法 和 “AI”扯上关系后,更是加大了理解的难度。 但,不了解推荐算法,就无法做推荐系

基于人工智能的图像分类系统

目录 引言项目背景环境准备 硬件要求软件安装与配置系统设计 系统架构关键技术代码示例 数据预处理模型训练模型预测应用场景结论 1. 引言 图像分类是计算机视觉中的一个重要任务,目标是自动识别图像中的对象类别。通过卷积神经网络(CNN)等深度学习技术,我们可以构建高效的图像分类系统,广泛应用于自动驾驶、医疗影像诊断、监控分析等领域。本文将介绍如何构建一个基于人工智能的图像分类系统,包括环境

水位雨量在线监测系统概述及应用介绍

在当今社会,随着科技的飞速发展,各种智能监测系统已成为保障公共安全、促进资源管理和环境保护的重要工具。其中,水位雨量在线监测系统作为自然灾害预警、水资源管理及水利工程运行的关键技术,其重要性不言而喻。 一、水位雨量在线监测系统的基本原理 水位雨量在线监测系统主要由数据采集单元、数据传输网络、数据处理中心及用户终端四大部分构成,形成了一个完整的闭环系统。 数据采集单元:这是系统的“眼睛”,

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

嵌入式QT开发:构建高效智能的嵌入式系统

摘要: 本文深入探讨了嵌入式 QT 相关的各个方面。从 QT 框架的基础架构和核心概念出发,详细阐述了其在嵌入式环境中的优势与特点。文中分析了嵌入式 QT 的开发环境搭建过程,包括交叉编译工具链的配置等关键步骤。进一步探讨了嵌入式 QT 的界面设计与开发,涵盖了从基本控件的使用到复杂界面布局的构建。同时也深入研究了信号与槽机制在嵌入式系统中的应用,以及嵌入式 QT 与硬件设备的交互,包括输入输出设

C++11第三弹:lambda表达式 | 新的类功能 | 模板的可变参数

🌈个人主页: 南桥几晴秋 🌈C++专栏: 南桥谈C++ 🌈C语言专栏: C语言学习系列 🌈Linux学习专栏: 南桥谈Linux 🌈数据结构学习专栏: 数据结构杂谈 🌈数据库学习专栏: 南桥谈MySQL 🌈Qt学习专栏: 南桥谈Qt 🌈菜鸡代码练习: 练习随想记录 🌈git学习: 南桥谈Git 🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈�

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

【Prometheus】PromQL向量匹配实现不同标签的向量数据进行运算

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Python全栈,前后端开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi

让树莓派智能语音助手实现定时提醒功能

最初的时候是想直接在rasa 的chatbot上实现,因为rasa本身是带有remindschedule模块的。不过经过一番折腾后,忽然发现,chatbot上实现的定时,语音助手不一定会有响应。因为,我目前语音助手的代码设置了长时间无应答会结束对话,这样一来,chatbot定时提醒的触发就不会被语音助手获悉。那怎么让语音助手也具有定时提醒功能呢? 我最后选择的方法是用threading.Time