界面控件 - 滚动条ScrollBar

简介:

界面是人机交互的门户,对产品至关重要。在界面开发中只有想不到没有做不到的,有好的想法,当然要尝试着做出来。对滚动条的扩展,现在有很多类是的例子。

VS2015的代码编辑是非常强大的,其中有一个功能可以把滚动态变成MinMap,可以通过Options->Text Editor->C/C++->Scroll Bars中的Behavior选项分类进行打开。

sublime也有这个功能,但没有VS的好用。变成MinMap后整个代码文档变成一个完整的缩微图,在你对代码比较熟悉的情况下,可以非常容易判断并定位到大致的函数位置。同时对特殊状态,如:错误信息,更新信息等进行标识。只要看一眼滚动条就能知道当前文档的情况,可以快速定位到相关文档位置。

优点多多: 当一个文件内容比较长时,在上面标注相关重要内容是非常方便的。用于只要一看滚动条上的相关标示,就能知道相关信息。不用再开辟窗口告诉用户,他想得到的有关信息。

VS可作参考,做这个就比较容易。不知道微软有没有注册版权-_-!!。因此想到扩展Scroll Bar是个不错的主意,但windows系统并没有给这个机会,一般的窗口类都自带系统滚动条。系统自带的完全无法做出这种效果,只能自己做一个覆盖系统默认滚动条。

一、滚动条结构SCROLLINFO

typedef struct tagSCROLLINFO { UINT cbSize; // 结构尺寸 UINT fMask; // 需要处理或返回的相关选项参数 int nMin; // 滚动条的最小值 int nMax; // 最大滚动范围 UINT nPage; // 一页有效行数 int nPos; // 当前滚动条 int nTrackPos; // 拖拽滚筒条时的位置 } SCROLLINFO, *LPCSCROLLINFO;

正常我们用到的的滚动条有下面一些元素,画了张图更容易理解点。

可以看到如果我们默认最小值=0时,实际需要的只有三个滚动条参数,最大可以滚动的范围nMax,一页可显示的行数nPage 和 当前行的显示位置nPos 这三个参数。

1、滚动数量计算

如滚动条能正常滚动到最后一行的位置。有100行记录,滚动最下面界面显示最后一行。为方便计算没条记录的行高固定。

  • nPage = ClientHeight / rowHeight
  • nMax = rowCount - 1 + nPage - 1

正常情况计算是总行数-1, 就是 nMax的值,应为要允许滚动到记录最后一条,因此需要多加一页的滚动数量。就产生了上面的nMax的公式。

// 只要超过1行就能滚动,因此需要多加一页的行数。
si.nMax   = rowCount - 1 + yClient / rowHeight - 1; si.nPage = yClient / rowHeight;

2、滑块尺寸计算

滑块的尺寸的尺寸按照上图的定义,滚动条的高度除以滚动范围再乘上每页数量可以得到。

滑块尺寸 = 滚动条高度 / 有效范围 * 每页数量

上述计算公式存在另外一个问题,如果记录量很大时就杯具,滑块会非常小,用户很难用鼠标抓到滑块。眼神犀利和伸手敏捷的才能完成这种艰巨任务。因此为滑容易定位的最小值。

 s = max((float)((r.bottom - r.top) / (si.nMax - si.nMin) * si.nPage), 20.0f);

3、滑块位置计算

在图上看滑块有效的滑动位置是需要扣除滑块的大小的。

实际滑动位置 = (滚动条高度 - 滑块尺寸) / (有效范围 - 每页数量 + 1) * 当前行位置

有除法,就会存一些精度问题,所以算出位置后要检查一下位置是否超出客户区。

v = 0; if (si.nPos > 0) v = (int)((r.bottom - r.top - s) / (float)(si.nMax - si.nMin + 1 - si.nPage) * si.nPos); // 由于精度问题,可能滑块位置会超界。超界就取最大值 if (v && v + (int)s > r.bottom) v = r.bottom - (int)s;

二、滚动条信息

做滚动条肯定需要记录上述说的相关信息。为简易期间直接使用这个结构体记录static SCROLLINFO si;。在设置和获取时直接使用。

自定义滚动条不能使用SetScrollInfo 或 GetScrollInfo ,这两个是给系统提供的。需要另外增加2个消息,系统已经提供了两个消息,可以直接使用。SBM_SETSCROLLINFO 和 SBM_GETSCROLLINFO 处理滚动信息。

参数:

  • wParam --- SBM_SETSCROLLINFO 是否需要刷新, SBM_GETSCROLLINFO 无效。
  • lParam --- *SCROLLINFO 结构体指针。

前面在SCROLLINFO 结构中有提到fMask,在设置和获取中需要使用。

Mask 值:

  • SIF_RANGE --- 设置行数范围参数 nMin ~ nMax 这两个参数值有效。
  • SIF_PAGE --- 每页行数 nPage 参数值有效
  • SIF_POS --- 位置值 nPos 参数值有效
  • SIF_ALL --- 所有参数值都有效。

其他对应值可以参考MSDN文档。

case SBM_SETSCROLLINFO:
    if (!lParam) return 0; // 设置滚动条信息 psrcsi = (SCROLLINFO *)lParam; if (psrcsi->fMask & SIF_RANGE) { // 设置内容行数 si.nMax = psrcsi->nMax; si.nMin = psrcsi->nMin; } if (psrcsi->fMask & SIF_PAGE) // 每页能显示多少行 si.nPage = psrcsi->nPage; if (psrcsi->fMask & SIF_POS) // 行显示位置 si.nPos = psrcsi->nPos; // wParam = true 刷新滚动区域 if (wParam) InvalidateRect(hwnd, NULL, false); return 0; case SBM_GETSCROLLINFO: //这里简单处理,应该和SBM_SETSCROLLINFO一样判断获取的信息。 if (lParam) *(SCROLLINFO *)lParam = si; return 0;

三、滚动条绘制

有基本的滚动条结构信息后,要绘制出自己的滚动条就很简单了。需要主意的一点,当滚动条尺寸小到比滑块尺寸还小时,就不要再绘制滑块了。

技巧: 在填充色块时可以使用ExtTextOut函数进行填充,在不使用双缓存的情况下,能减少闪烁,而且不需要申请画刷。

static void mlCGFillColor(HDC hdc, RECT *r, unsigned int color) { // 实际使用ExtTextOut要比FillRect填充颜色效果好,能减少绘制中闪烁问题。而且不要申请画刷。 SetBkColor(hdc, color); ExtTextOut(hdc, 0, 0, ETO_OPAQUE, r, 0, 0, 0); } // WM_PAINT Code // 绘制背景色 GetClientRect(hwnd, &r); mlCGFillColor(hdc, &r, 0xcccccc); // 计算滑块大小,过小时不绘制 if (r.bottom - r.top > 30 && si.nMax && (si.nMax - si.nMin) >= (int)si.nPage) { // 滑块计算 // 大小 = 滚动条高度 / 有效范围 * 每页数量 // 最小20, 内容比较多,降低用于鼠标定位到滚动条拖动的难度。 s = max((float)((r.bottom - r.top) / (si.nMax - si.nMin) * si.nPage), 20.0f); // 实际滑动位置 // = (滚动条高度 - 滑块尺寸) / (有效范围 - 每页数量 + 1) * 当前行位置 // 实际滚动的位置会比实际少一页的数量。 // v = 0; if (si.nPos > 0) v = (int)((r.bottom - r.top - s) / (float)(si.nMax - si.nMin + 1 - si.nPage) * si.nPos); // 由于精度问题,可能滑块位置会超界。超界就取最大值 if (v && v + (int)s > r.bottom) v = r.bottom - (int)s; // 绘制滑块 r.left++; r.right--; r.top = v ; r.bottom = r.top + (int)s; // 拖拽时滑块颜色反一下 mlCGFillColor(hdc, &r, dragState ? 0x999999 : 0x666666e); InflateRect(&r, -1, -1); mlCGFillColor(hdc, &r, dragState ? 0x666666e : 0x999999); }

四、响应鼠标消息

鼠标消息,主要是左键点击定位、拖拽和滚轮滚动翻页。

1、点击定位响应

鼠标左击滚动条位置,可以直接定位到对应行。这个功能就是第一个需求,根据标识快速定位到记录行。实际的核心就是,鼠标所在滚动条的实际位置,计算出对应的行数。

计算方法:
nPos = (鼠标位置 - 半滑块尺寸) / 每象素滑动量

  • 每象素滑动量 = 有效高度 / 可滑动数量
  • 有效高度 = 总高度 - 滑块尺寸
  • 可滑动数量 = 总量 - 每页数量

根据上述公式提取出一个通用方法,计算像素位置位置对应行数的关系函数。

// 计算滚动条位置
static int calcVertThumPos(int postion, SCROLLINFO *si, int s) { float thumsize; // 滑块的大小 float pxSize; // 每象素可滑动量 int scrollCnt; // 可滑动数量 // 滑块尺寸 = 滚动条高度 / 有效范围 * 每页数量 // 最小 20 thumsize = max((float)((s) / (si->nMax - si->nMin) * si->nPage), 20.0f); if (postion <= (int)(thumsize / 2)) return 0; if (postion >= s - (thumsize / 2)) return (si->nMax - si->nMin + 1) - si->nPage; // 计算方法: // 每象素滑动量 = 有效高度 / 可滑动数量 // 有效高度 = 总高度 - 滑块尺寸 // 可滑动数量 = 总量 - 每页数量 scrollCnt = (si->nMax - si->nMin + 1) - si->nPage; pxSize = (float)(s - thumsize) / (float)scrollCnt; // // 计算方法: // 位置 = (鼠标位置 - 半滑块尺寸) / 每象素滑动量 return (int)((postion - thumsize / 2.0) / pxSize); }

鼠标左击事件响应WM_LBUTTONDOWN消息。

si.nTrackPos = calcVertThumPos(GET_Y_LPARAM(lParam), &si, height); if (si.nPos != si.nTrackPos) PostMessage(GetParent(hwnd), WM_VSCROLL, SB_THUMBTRACK, (LPARAM)hwnd);

使用calcVertThumPos 函数计算出滚动位置,并通过WM_VSCROLL消息请求滚动。

注意:MSDN中 WM_VSCROLL的lParam参数约定,当自定义控件是通常传自定义控件的hWnd。用于区分系统默认的还是自定义控件的。

点击定位就搞定了。

2、拖拽滑块滚响应

拖拽滚动需要同时用到3消息WM_LBUTTONDOWNWM_MOUSEMOVE 和WM_LBUTTONUP。当鼠标按下时准备拖拽,拖拽中滚动记录,鼠标释放结束拖拽。

正常情况鼠标按下就需要锁定鼠标SetCapture,但有时只是点击一下,并没有需要拖拽。或内部处理更多的控制时,直接锁定不太好。因此在拖拽时加了个dragState的状态,只有准备拖拽了才进行锁定鼠标操作。

另外一种情况,某些异常情况,锁定的鼠标会丢失。如断点调试一定会打断,这时候会出现不想要的情况。没在拖拽状态,鼠标已经松开了,滑块还是会跟着鼠标移动。

case WM_LBUTTONDOWN:
    ... dragState = 1; // 准备拖动滚动条 InvalidateRect(hwnd, NULL, false); return 0; case WM_MOUSEMOVE: if (dragState == 1) { SetCapture(hwnd); // 有拖拽准备,锁定鼠标鼠标 dragState = 2; } else if (dragState == 2) { if (!(wParam & MK_LBUTTON)) { dragState = 0; // 防止中间中断,导致出现无效拖拽 if (GetCapture() == hwnd) ReleaseCapture(); } else { // 在锁定状态下拖动滚动条定位。 si.nTrackPos = calcVertThumPos(GET_Y_LPARAM(lParam), &si, height); if (si.nTrackPos != si.nPos) PostMessage(GetParent(hwnd), WM_VSCROLL, SB_THUMBTRACK, (LPARAM)hwnd); } } return 0; case WM_LBUTTONUP: if (dragState == 2) ReleaseCapture(); // 释放鼠标锁定 if (dragState) { dragState = 0; // 清除状态 InvalidateRect(hwnd, NULL, false); } return 0;

鼠标滚轮响应

滚动事件WM_MOUSEWHEEL,鼠标滚动翻页时会有一个精度。慢慢滚动滚轮会上移或下移3行左右,当快速滚动时,会很容易到文档末尾。这个就是一般说的精度。

这里默认处理,滚动一次滚轮移动三行。获得滚动行数量后就可以直接在当前数量nPos上累加。注意一下,不要超界。

// 鼠标滚动支持
accumDelta = GET_WHEEL_DELTA_WPARAM(wParam) / WHEEL_DELTA; // 滚动精度120一个单位 si.nTrackPos = si.nPos - accumDelta * 3; // 每滚一次3行 if (si.nTrackPos < 0) si.nTrackPos = 0; else if (si.nTrackPos > (int)((si.nMax - si.nMin + 1) - si.nPage)) si.nTrackPos = (si.nMax - si.nMin + 1) - si.nPage; if (si.nPos != si.nTrackPos) PostMessage(GetParent(hwnd), WM_VSCROLL, SB_THUMBTRACK, (LPARAM)hwnd);

~OK 啦

简单的滚动条就完成,然后后面的标识之类的就可以在这个基础上尽情发挥。随你完全由你掌控滚动条。

最终的效果:

完整代码:

蘑菇房 moguf.com

相关消息和API:

本文转自莫水千流博客园博客,原文链接:http://www.cnblogs.com/zhoug2020/p/5789015.html,如需转载请自行联系原作者
相关文章
|
缓存 NoSQL Java
Spring Cache 缓存原理与 Redis 实践
Spring Cache 缓存原理与 Redis 实践
704 0
|
8月前
|
安全 JavaScript 开发者
Python 自动化办公神器|一键转换所有文档为 PDF
本文介绍一个自动化批量将 Word、Excel、PPT、TXT、HTML 及图片转换为 PDF 的 Python 脚本。支持多格式识别、错误处理与日志记录,适用于文档归档、报告整理等场景,大幅提升办公效率。仅限 Windows 平台,需安装 Office 及相关依赖。
411 0
|
8月前
|
JavaScript 安全 前端开发
如何开发人事及OA管理系统的薪酬管理板块?(附架构图+流程图+代码参考)
本文介绍了如何构建一个高效、合规的企业薪酬管理系统,涵盖薪酬模块的重要性、核心功能、系统架构设计、数据模型、开发实现及安全合规要点。内容包括薪酬配置、数据导入、自动化计算、审批发放、工资条生成与安全分发、报表看板、权限审计等关键环节,并提供详细的业务流程、架构图、核心代码示例及落地开发技巧。适用于HR、财务及技术人员快速搭建薪酬管理系统,提升发薪效率,降低人工错误与合规风险。
|
数据采集 JSON API
深入解析:使用 Python 爬虫获取淘宝店铺所有商品接口
本文介绍如何使用Python结合淘宝开放平台API获取指定店铺所有商品数据。首先需注册淘宝开放平台账号、创建应用并获取API密钥,申请接口权限。接着,通过构建请求、生成签名、调用接口(如`taobao.items.search`和`taobao.item.get`)及处理响应,实现数据抓取。代码示例展示了分页处理和错误处理方法,并强调了调用频率限制、数据安全等注意事项。此技能对开发者和数据分析师极具价值。
|
12月前
|
机器学习/深度学习 人工智能 算法
小米7B参数推理大模型首次开源!Xiaomi MiMo:数学代码双杀,超越32B巨头
小米开源的MiMo推理大模型通过联动预训练与强化学习算法,在7B参数规模下实现数学推理与代码生成能力的突破性提升,技术报告显示其性能超越部分32B级模型。
1656 74
小米7B参数推理大模型首次开源!Xiaomi MiMo:数学代码双杀,超越32B巨头
|
7月前
|
自然语言处理 监控 API
小红书爆文解码:用API分析互动数据,精准指导创作方向
在内容为王时代,爆文背后有科学公式!通过小红书API抓取百万笔记数据,提炼出点赞转化率、收藏价值系数、评论情感值三大核心指标,揭秘爆文特征不等式与内容元素矩阵,手把手教你用数据驱动创作,实现从0到百万曝光的逆袭!
735 0
|
Linux 编译器 测试技术
【C++】CentOS环境搭建-快速升级G++版本
通过上述任一方法,您都可以在CentOS环境中高效地升级G++至所需的最新版本,进而利用C++的新特性,提升开发效率和代码质量。
940 63
|
开发者
Qt中的事件该如何学习?(附带案例)
事件是Qt中比较重要的一部分,在初期如果理解不当学习可能会比较困难,这里提一嘴当初教我的那位老师水平是真的高,让我很轻易的就理解了事件的概念。 在平时我们见到那些界面上的某些快捷键就有可能是事件做的,例如ESC关闭窗口,Enter提交或者登录这种类似的,这也是事件的强大之处。
733 1
|
网络协议 网络架构 数据格式
TCP/IP基础:工作原理、协议栈与网络层
TCP/IP(传输控制协议/互联网协议)是互联网通信的基础协议,支持数据传输和网络连接。本文详细阐述了其工作原理、协议栈构成及网络层功能。TCP/IP采用客户端/服务器模型,通过四个层次——应用层、传输层、网络层和数据链路层,确保数据可靠传输。网络层负责IP寻址、路由选择、分片重组及数据包传输,是TCP/IP的核心部分。理解TCP/IP有助于深入掌握互联网底层机制。
2042 2
|
存储 Oracle 关系型数据库
Oracle 代码异常查询(六)
Oracle 代码异常查询
743 0