ReactOS 窗口系统分析(13):滚动条 --- scrollbar.c + scrollex.c
本文是"窗口系统三大主线之窗口管理"系列第 13 册(对应总览文档第 5.5 节)。
主题:滚动条子系统 ------ 内核侧
win32ss/user/ntuser/scrollbar.c(1466 行)负责滚动条信息管理与绘制,
win32ss/user/ntuser/scrollex.c(662 行)负责窗口内容区域滚动,用户态
win32ss/user/user32/controls/scrollbar.c(1755 行)实现滚动条控件(SB_CTL)的窗口过程与thumb 拖动跟踪循环。
本文全部内容基于 ReactOS 实际源码分析,行号以 2026 年 8 月代码为准,禁止编造。
1. 概述
1.1 本册定位
滚动条是窗口管理子系统中最"看得见"的基础设施之一。几乎每个文本编辑器、列表框、树形控件、网页
浏览器都要靠滚动条浏览内容。Win32 中有两种形态的滚动条:
- 窗口自带滚动条 (窗口样式
WS_HSCROLL/WS_VSCROLL):滚动条位于窗口的非客户区
(边框内侧),由系统 在WM_NCPAINT时统一绘制,属于窗口的一部分,不占用客户区。 - 滚动条控件 (
SB_CTL,系统类"ScrollBar",样式SBS_HORZ/SBS_VERT):一个独立
的子窗口,位于父窗口的客户区内,有自己的窗口过程(ScrollBarWndProc)、焦点、插入符与
SBM_*系列消息。
无论哪种形态,滚动条的逻辑模型 完全相同:一个"位置"(pos)在一个"范围"(range:
[nMin, nMax])内移动,可选地有一个"页面大小"(page)决定 thumb 的比例与翻页步长。用户通过
鼠标点击箭头/滑道或拖动 thumb 产生 WM_HSCROLL / WM_VSCROLL 通知,应用程序在收到通知后调用
SetScrollPos 之类的 API 更新位置并重绘内容。
窗口管理主线上的地位 :滚动条与菜单(_11/_12 册)一样,是 DefWindowProc 之外另一条由
系统接管交互的"内部控件"。它同时横跨三条主线:绘制归属"窗口显示"(nonclient.c 的
WM_NCPAINT 分支调用 IntDrawScrollBar)、消息通知归属"窗口管理"(WM_VSCROLL/WM_HSCROLL)、
鼠标拖动归属"鼠标控制"(capture + 模态循环)。本文按窗口管理主线展开,但会交叉引用其他主线。
1.2 三个文件的职责划分
| 文件 | 行数 | 职责 |
|---|---|---|
win32ss/user/ntuser/scrollbar.c |
1466 | 滚动条信息(SBINFO/SBINFOEX)创建与维护、Set/GetScrollInfo、Show/EnableScrollBar、滚动条绘制(箭头/滑道/thumb)、内核侧控件窗口过程、NtUser* 系统调用 |
win32ss/user/ntuser/scrollex.c |
662 | 窗口内容区域滚动:ScrollWindowEx/ScrollWindow/ScrollDC 的内核实现(位图移动 + 暴露区重绘 + 子窗口位移) |
win32ss/user/user32/controls/scrollbar.c |
1755 | 用户态滚动条控件:ScrollBar 系统类注册、控件窗口过程(ScrollBarWndProc_common)、thumb 拖动跟踪循环(ScrollTrackScrollBar)、命中测试、键盘导航、公共 API 封装(SetScrollInfo 等) |
一个容易混淆的事实需要首先澄清:thumb 拖动跟踪循环并不在内核 。scrollex.c 的名字容易让人
误以为它处理"滚动条扩展交互",实际上它处理的是"滚动窗口内容"(scroll window content,即
ScrollWindowEx 系列)。真正的拖动循环 ScrollTrackScrollBar 在用户态 user32 中实现(与 Wine
结构一致),它通过 GetMessage 模态循环 + 鼠标捕获驱动,内核只提供 SBTRACK 结构(挂在
THREADINFO.pSBTrack,见 scroll.h)与 NEWco_IntGetScrollInfo 这个读取跟踪期位置的辅助函数。
1.3 整体架构图
┌────────────────────────────────────────────────────────────────────┐
│ 应用程序 │
│ SetScrollInfo / ScrollWindowEx / WM_VSCROLL 处理 │
└───────────────┬──────────────────────────────────┬─────────────────┘
│ user32.dll │
│ │ 窗口过程收到 WM_VSCROLL/WM_HSCROLL
┌────────────┴───────────┐ ┌─────────┴──────────┐
│ controls/scrollbar.c │ │ Windows 控件/应用 │
│ 控件窗口过程 + │ │ 处理通知后调用 │
│ 拖动跟踪循环 │ │ SetScrollInfo 等 │
└────────────┬───────────┘ └─────────┬──────────┘
│ NtUser* 系统调用 │ NtUser* 系统调用
┌───────────────┴────────────────────────────────────┴─────────────────┐
│ win32k.sys │
│ ┌──────────────────────── scrollbar.c ───────────────────────────┐ │
│ │ 信息管理:co_IntCreateScrollBars / co_IntSetScrollInfo / │ │
│ │ co_IntGetScrollInfo / co_UserShowScrollBar / │ │
│ │ NtUserEnableScrollBar │ │
│ │ 绘制:IntDrawScrollBar → IntDrawScrollArrows + │ │
│ │ IntDrawScrollInterior(箭头/滑道/thumb) │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ ┌──────────────────────── scrollex.c ───────────────────────────┐ │
│ │ 区域滚动:NtUserScrollWindowEx → IntScrollWindowEx → │ │
│ │ UserScrollDC(BitBlt 移动 + 暴露区区域计算) │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │ IntDrawScrollBar(WM_NCPAINT 分支,nonclient.c 调用) │
│ ┌───────┴────────────────────────────────────────────────────────┐ │
│ │ GDI:NtGdiBitBlt / NtGdiPatBlt / DrawFrameControl(DFC_SCROLL) │ │
│ └────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────┘
1.4 数据流总览
SetScrollInfo(应用)
→ NtUserSetScrollInfo(scrollbar.c L1340,系统调用)
→ co_IntSetScrollInfo(L454)
├── co_IntCreateScrollBars 确保 SBINFO/SBINFOEX 存在
├── 更新 nMin/nMax/nPage/nPos(做范围/位置夹取)
├── 决定 SA_SSI_HIDE / SA_SSI_SHOW / SA_SSI_REPAINT_ARROWS 动作
├── co_UserShowScrollBar(改 WS_VSCROLL 样式 + SWP_FRAMECHANGED)
└── 重绘:IntRefeshScrollInterior 或 co_UserRedrawWindow
用户拖动 thumb(控件)
→ ScrollTrackScrollBar(user32 模态循环)
→ IntScrollHandleScrollEvent(WM_LBUTTONDOWN / WM_MOUSEMOVE / WM_SYSTIMER / WM_LBUTTONUP)
├── SendMessage(父, WM_VSCROLL, SB_THUMBTRACK, pos) ← 连续通知
└── SendMessage(父, WM_VSCROLL, SB_THUMBPOSITION | SB_ENDSCROLL, 0)
应用在 WM_VSCROLL 中
→ SetScrollPos / ScrollWindowEx
→ NtUserScrollWindowEx(scrollex.c L570)→ IntScrollWindowEx → UserScrollDC
└── NtGdiBitBlt 移动位图 + 暴露区 co_UserRedrawWindow 重绘
2. 设计动机
2.1 窗口滚动条与控件统一
Windows 从 3.x 时代起就把"窗口滚动条"与"滚动条控件"统一在同一个内部代码路径中。好处是:
- 同一套信息结构 :无论
SB_HORZ/SB_VERT(窗口自带)还是SB_CTL(控件),滚动条的核心
状态都是"范围 + 位置 + 页面大小"三元组,Win32 用SCROLLINFO统一表达,系统调用
NtUserSetScrollInfo/NtUserSBGetParms不区分形态,只区分nBar参数。 - 同一套绘制代码 :
IntDrawScrollBar接受SB_HORZ/SB_VERT/SB_CTL三种参数,唯一差异是
矩形来源(窗口坐标 vs 客户区坐标)与方向判定(SBS_VERT样式)。 - 同一套通知协议 :
WM_HSCROLL/WM_VSCROLL的wParam低字是SB_*命令码,高字是 thumb
位置;控件形态时lParam是控件句柄,窗口形态时lParam为 0。应用程序处理逻辑完全一致。
统一还体现在消息上:控件形态时,GetScrollInfo(hwnd, SB_CTL, ...) 会被封装层改写成
SendMessage(hwnd, SBM_GETSCROLLINFO, ...)(user32 RealGetScrollInfo L1512),把请求路由到
控件窗口过程;而窗口形态则直接走 NtUserSBGetParms。同一 API、两种路由 是 Win32 滚动条设计
的一大特色。
2.2 数据双通道:SBINFO 与 SBINFOEX
WND 中有两个滚动条字段(ntuser.h L762 附近):
PSBINFO pSBInfo:SBINFO结构(WSBflags+SBDATA Horz/Vert),放在桌面堆
(DesktopHeapAlloc),同时映射到用户态(user32 可用DesktopPtrToUser直接读),供
GetScrollPos/GetScrollRange这类"快速查询"使用,避免每次查询都过系统调用。PSBINFOEX pSBInfoex:SBINFOEX[3]数组(每个滚动条一个:HORZ/VERT/CTL),放在分页池
(ExAllocatePoolWithTag(PagedPool, ..., TAG_SBARINFO)),每个元素是
{ SCROLLBARINFO ScrollBarInfo; SCROLLINFO ScrollInfo; }。ScrollInfo是权威状态
(SetScrollInfo 的落点),ScrollBarInfo缓存绘制所需几何与rgstate状态数组。
双通道的动机:绘制 需要频繁访问完整信息(含 rgstate、thumb 几何),查询 (GetScrollPos
这种 Win3.1 老 API)只需位置,追求极快。把高频查询字段放进桌面堆让 user32 免系统调用直接读,
把完整状态放进内核池避免污染桌面堆。这种"双份存储"在滚动条子系统里表现得最典型。
2.3 内核绘制 vs 用户态控件
窗口自带滚动条由 win32k 在内核绘制(WM_NCPAINT → nonclient.c → IntDrawScrollBar),因为
非客户区本属系统管辖;而滚动条控件是一个普通子窗口,理论上它的绘制(WM_PAINT)由 user32 的
控件窗口过程完成。有意思的是 ReactOS 把绘制函数复制了两份 :内核 IntDrawScrollBar/
IntDrawScrollInterior/IntDrawScrollArrows(scrollbar.c)与用户态同名函数
(controls/scrollbar.c)几乎逐行相同。代码注释显示这些函数"Ported from WINE20020904",是历史
迁移痕迹------内核版本供窗口自带滚动条使用,用户态版本供控件使用,两处依赖的 GDI 调用
(NtGdiPatBlt/NtGdiSelectBrush/DrawFrameControl)分别以内核直调与 user32 封装形式出现。
2.4 拖动反馈与 SB_THUMBTRACK
thumb 拖动是滚动条交互中最敏感的部分。用户按住 thumb 移动时,系统连续 发送
WM_VSCROLL(wParam 低字 SB_THUMBTRACK,高字当前候选位置),让应用可以即时滚动内容形成
"跟手"效果;松开时发送 SB_THUMBPOSITION(最终位置)与 SB_ENDSCROLL。为了在拖动中保持
跟手且不闪烁,user32 维护"正在拖动的 thumb 是否已显示"标志(ScrollMovingThumb),只有位置
真正变化时才重绘 thumb(IntScrollDrawMovingThumb)。拖动期间窗口内容不 随 thumb 移动
(那是应用在 SB_THUMBTRACK 中调用 ScrollWindowEx 的结果),系统只负责 thumb 本身的反馈,
这种"系统反馈"与"内容滚动"解耦正是滚动条协议的精髓。
3. 核心数据结构
3.1 SBDATA / SBINFO(ntuser.h L503-L516)
c
typedef struct tagSBDATA
{
INT posMin; // 范围下限
INT posMax; // 范围上限
INT page; // 页面大小
INT pos; // 当前位置
} SBDATA, *PSBDATA;
typedef struct tagSBINFO
{
INT WSBflags; // ESB_* 箭头使能标志(ESB_ENABLE_BOTH 等)
SBDATA Horz; // 水平滚动条快速数据
SBDATA Vert; // 垂直滚动条快速数据
} SBINFO, *PSBINFO;
SBDATA 是"位置四元组"的极简形态,SBINFO 把两个方向合并。注意 SB_CTL 没有自己的
SBDATA ------控件形态时 IntGetSBData 直接返回 SBCalc(见 3.3)的地址,把
SBCALC 结构伪装成 SBDATA(两者前四个字段完全相同:posMin/posMax/page/pos)。这是
源码中一处精妙的"结构前缀兼容"技巧(scrollbar.c L89-L96)。
3.2 SBINFOEX 与两个 IntGet 宏(scroll.h L26-L37)
c
typedef struct _SBINFOEX
{
SCROLLBARINFO ScrollBarInfo; // 绘制几何 + 部件状态
SCROLLINFO ScrollInfo; // 权威滚动信息(含 nTrackPos)
} SBINFOEX, *PSBINFOEX;
#define IntGetScrollbarInfoFromWindow(Window, i) \
((PSCROLLBARINFO)(&((Window)->pSBInfoex + i)->ScrollBarInfo))
#define IntGetScrollInfoFromWindow(Window, i) \
((LPSCROLLINFO)(&((Window)->pSBInfoex + i)->ScrollInfo))
pSBInfoex 指向 SBINFOEX[3] 数组,下标即 SB_HORZ(0)/SB_VERT(1)/SB_CTL(2)(宏
SBOBJ_TO_SBID(Obj) == ((Obj) - OBJID_HSCROLL) 把 OBJID_HSCROLL(0)/OBJID_VSCROLL(1)/
OBJID_CLIENT(2) 映射成同一下标)。两个宏分别取几何+状态、权威信息。SIF_THEMED
(0x80)是 ReactOS 特有的 SCROLLINFO.fMask 扩展位,表示该滚动条走主题绘制。
3.3 SBCALC(ntuser.h L518-L536)
c
typedef struct tagSBCALC
{
INT posMin; // 同 SBDATA
INT posMax;
INT page;
INT pos;
INT pxTop; // 垂直 thumb 上沿像素
INT pxBottom; // 垂直 thumb 下沿像素
INT pxLeft; // 水平 thumb 左沿像素
INT pxRight; // 水平 thumb 右沿像素
INT cpxThumb; // thumb 像素尺寸
INT pxUpArrow; // 上/左箭头区
INT pxDownArrow; // 下/右箭头区
INT pxStart; // 拖动起始
INT pxThumbBottom;
INT pxThumbTop;
INT cpx; // 滚动条全长(像素)
INT pxMin; // 拖动最小位置
} SBCALC, *PSBCALC;
SBCALC 是滚动条控件的附加字节(cbwndExtra = sizeof(SBWND)-sizeof(WND))中保存的几何计算
缓存。控件形态时它既当 SBDATA 用(前 4 字段),又存像素级几何。SBWND 定义在 ntuser.h
L775-L777:
c
typedef struct _SBWND
{
WND wnd;
SBCALC SBCalc;
} SBWND, *PSBWND;
3.4 SCROLLINFO / SCROLLBARINFO(psdk/winuser.h L3386、L3855)
c
typedef struct tagSCROLLINFO {
UINT cbSize; // sizeof(SCROLLINFO)
UINT fMask; // SIF_* 位标志
int nMin; // 范围下限
int nMax; // 范围上限
UINT nPage; // 页面大小(0 = 无比例 thumb)
int nPos; // 当前位置
int nTrackPos; // 拖动中的候选位置(只读,SB_THUMBTRACK 期间有效)
} SCROLLINFO,*LPSCROLLINFO;
#define CCHILDREN_SCROLLBAR 5
typedef struct tagSCROLLBARINFO {
DWORD cbSize; // sizeof(SCROLLBARINFO)
RECT rcScrollBar; // 滚动条矩形(窗口坐标或客户区坐标)
int dxyLineButton; // 箭头(线按钮)的宽/高
int xyThumbTop; // thumb 顶端(相对滚动条起点)
int xyThumbBottom; // thumb 底端
int reserved;
DWORD rgstate[CCHILDREN_SCROLLBAR+1]; // 6 个部件的 STATE_SYSTEM_* 状态
} SCROLLBARINFO,*PSCROLLBARINFO;
rgstate 的 6 个槽位对应 SBRG_* 常量(见 3.10),值域为 STATE_SYSTEM_PRESSED(0x8)/
STATE_SYSTEM_UNAVAILABLE(0x1)/STATE_SYSTEM_INVISIBLE(0x8000)/STATE_SYSTEM_OFFSCREEN
(0x10000) 等可访问性(MSAA)状态位。GetScrollBarInfo 就是把这个结构原样拷给应用。
3.5 SBTRACK(scroll.h L5-L24)
c
typedef struct tagSBTRACK
{
ULONG fHitOld:1; // 位域:上一次命中
ULONG fTrackVert:1; // 跟踪垂直滚动条
ULONG fCtlSB:1; // 是控件
ULONG fTrackRecalc:1; // 需要重算 thumb
PWND spwndTrack; // 被跟踪窗口
PWND spwndSB;
PWND spwndSBNotify; // 通知接收窗口(控件时为父窗口)
RECT rcTrack;
PFN_SCROLLBAR xxxpfnSB; // 内部回调(USER32xxx 风格,当前未启用)
UINT cmdSB; // 当前 SB_* 命令
UINT_PTR hTimerSB; // 重复计时器
INT dpxThumb;
INT pxOld; // 上次像素位置
INT posOld; // 上次逻辑位置
INT posNew; // 跟踪中的新位置(GetScrollInfo 的 nTrackPos 来源)
INT nBar; // SB_HORZ/SB_VERT/SB_CTL
PSBCALC pSBCalc;
} SBTRACK, *PSBTRACK;
SBTRACK 是内核为"拖动跟踪"预留的上下文,挂在 THREADINFO.pSBTrack(win32.h L121)。当前
代码中只有 NEWco_IntGetScrollInfo(scrollbar.c L310)真正使用它:当线程正在跟踪某窗口的
某条滚动条时,GetScrollInfo 的 SIF_TRACKPOS 返回 pSBTrack->posNew 而不是静态的 pos,
从而让应用在拖动中查询到"候选位置"。xxxpfnSB 回调、hTimerSB 计时器等字段是向"内核接管
拖动"方向预留的骨架,实际拖动循环仍由 user32 的模态循环驱动。
3.6 SB_* 命令码(psdk/winuser.h L552-L578)
滚动条通知 WM_HSCROLL/WM_VSCROLL 的 wParam 低字取值:
| 命令码 | 值 | 触发场景 | 含义 |
|---|---|---|---|
SB_LINEUP / SB_LINELEFT |
0 | 点击上/左箭头 | 上/左移一行 |
SB_LINEDOWN / SB_LINERIGHT |
1 | 点击下/右箭头 | 下/右移一行 |
SB_PAGEUP / SB_PAGELEFT |
2 | 点击 thumb 上方/左方滑道 | 上/左翻一页 |
SB_PAGEDOWN / SB_PAGERIGHT |
3 | 点击 thumb 下方/右方滑道 | 下/右翻一页 |
SB_THUMBPOSITION |
4 | 松开 thumb | 拖动结束,高字为最终位置 |
SB_THUMBTRACK |
5 | 拖动 thumb 中 | 连续通知,高字为候选位置 |
SB_TOP / SB_LEFT |
6 | 键盘 Home | 跳到最小值 |
SB_BOTTOM / SB_RIGHT |
7 | 键盘 End | 跳到最大值 |
SB_ENDSCROLL |
8 | 松开/离开滚动条 | 滚动结束,无位置信息 |
注意:SB_TOP/SB_BOTTOM(Home/End)通常由应用在键盘处理中自行构造,滚动条交互本身(箭头/
滑道/thumb)只产生 0-5 和 8。
3.7 SIF_* 标志(psdk/winuser.h L1243-L1249)
SCROLLINFO.fMask 指示 SetScrollInfo/GetScrollInfo 操作哪些字段:
| 标志 | 值 | 含义 |
|---|---|---|
SIF_RANGE |
0x0001 | 操作 nMin/nMax |
SIF_PAGE |
0x0002 | 操作 nPage |
SIF_POS |
0x0004 | 操作 nPos |
SIF_DISABLENOSCROLL |
0x0008 | 范围放不下时禁用箭头而非隐藏滚动条 |
SIF_TRACKPOS |
0x0010 | 读取 nTrackPos(拖动中候选位置) |
SIF_ALL |
0x0017 | SIF_RANGE|SIF_PAGE|SIF_POS|SIF_TRACKPOS |
SIF_PREVIOUSPOS |
0x1000 | (ReactOS 扩展,undocuser.h)返回值给旧位置 |
SIF_THEMED |
0x0080 | (ReactOS 扩展)走主题绘制 |
SIF_ALL 注意点:它是 0x17,即 RANGE|PAGE|POS|TRACKPOS,不含 SIF_DISABLENOSCROLL
与 ReactOS 的两个扩展位。
3.8 ESB_* 箭头使能标志(psdk/winuser.h L556-L563)
| 标志 | 值 | 含义 |
|---|---|---|
ESB_ENABLE_BOTH |
0 | 两个箭头都启用 |
ESB_DISABLE_LTUP |
1 | 禁用上/左箭头 |
ESB_DISABLE_RTDN |
2 | 禁用下/右箭头 |
ESB_DISABLE_BOTH |
3 | 两个箭头都禁用 |
EnableScrollBar(hwnd, nBar, ESB_*) 设置 SBINFO.WSBflags,同时把对应箭头在
SCROLLBARINFO.rgstate[SBRG_TOPRIGHTBTN/SBRG_BOTTOMLEFTBTN] 置为
STATE_SYSTEM_UNAVAILABLE,绘制时箭头变灰(DFCS_INACTIVE)。
3.9 滚动条绘制区域布局(ASCII)
垂直滚动条(水平方向旋转 90° 即得水平滚动条):
┌──────────────┐ ┌────────────────────────┐
│ ▲ │ │ SBRG_TOPRIGHTBTN (0) │ 上箭头(dxyLineButton 高)
├──────────────┤ ├────────────────────────┤
│ │ │ SBRG_PAGEUPRIGHT (2) │ 上滑道(top rect)
│ │ │ │
│ ┌────────┐ │ │ │
│ │ thumb │ │ │ SBRG_SCROLLBOX (3) │ thumb(xyThumbTop~xyThumbBottom)
│ └────────┘ │ │ │
│ │ │ │
│ │ │ SBRG_PAGEDOWNLEFT (4) │ 下滑道(bottom rect)
├──────────────┤ ├────────────────────────┤
│ ▼ │ │ SBRG_BOTTOMLEFTBTN (5) │ 下箭头
└──────────────┘ └────────────────────────┘
rcScrollBar SBRG_SCROLLBAR (0) = 整条滚动条
对应 scrollbar.c 的常量(L12-L47):
c
#define SCROLL_NOWHERE 0x00 /* 滚动条外 */
#define SCROLL_TOP_ARROW 0x01 /* 上/左箭头 */
#define SCROLL_TOP_RECT 0x02 /* thumb 与上箭头之间的滑道 */
#define SCROLL_THUMB 0x03 /* thumb */
#define SCROLL_BOTTOM_RECT 0x04 /* thumb 与下箭头之间的滑道 */
#define SCROLL_BOTTOM_ARROW 0x05 /* 下/右箭头 */
#define SBRG_SCROLLBAR 0 /* 整条滚动条 */
#define SBRG_TOPRIGHTBTN 1 /* 上/右箭头按钮 */
#define SBRG_PAGEUPRIGHT 2 /* 上/右翻页区 */
#define SBRG_SCROLLBOX 3 /* thumb */
#define SBRG_PAGEDOWNLEFT 4 /* 下/左翻页区 */
#define SBRG_BOTTOMLEFTBTN 5 /* 下/左箭头按钮 */
#define SCROLL_FIRST_DELAY 200 /* 按住按钮后的首次重复延迟 (ms) */
#define SCROLL_REPEAT_DELAY 50 /* 重复滚动间隔 (ms) */
#define SCROLL_TIMER 0 /* 滚动计时器 ID */
#define SCROLL_MIN_RECT 4 /* 两箭头之间的最小矩形 */
#define SCROLL_MIN_THUMB 6 /* thumb 最小像素 */
#define SCROLL_ARROW_THUMB_OVERLAP 0
#define MINTRACKTHUMB 8 /* thumb 最小跟踪尺寸 */
SCROLL_* 是命中码 (用户态 IntScrollHitTest 产出),SBRG_* 是 rgstate 数组下标 ;
STATE_SYSTEM_PRESSED 时按下的部件画成凹陷(BLACKNESS/DFCS_PUSHED)。
3.10 动作标志 SA_SSI_*(scrollbar.c L37-L40)
co_IntSetScrollInfo 内部根据参数变化计算"接下来要做什么绘制动作":
c
#define SA_SSI_HIDE 0x0001 /* 隐藏滚动条 */
#define SA_SSI_SHOW 0x0002 /* 显示滚动条 */
#define SA_SSI_REFRESH 0x0004 /* 刷新内部 */
#define SA_SSI_REPAINT_ARROWS 0x0008 /* 重画箭头(整条重绘) */
以及 CHANGERGSTATE 宏(L49-L52),用于 IntEnableScrollBar 对比并更新某个部件的状态:
c
#define CHANGERGSTATE(item, status) \
if(Info->rgstate[(item)] != (status)) \
Chg = TRUE; \
Info->rgstate[(item)] = (status);
3.11 命中判定与可见性
滚动条矩形 rcScrollBar 的坐标系规则(IntGetScrollBarRect 的注释):窗口自带滚动条用窗口
坐标,控件用客户区坐标。三种形态的矩形来源:
SB_HORZ:rcClient.bottom往下延伸SM_CYHSCROLL高;有边框时左右各扩 1 像素。SB_VERT:rcClient.right往右延伸SM_CXVSCROLL宽(WS_EX_LEFTSCROLLBAR时往左);
有边框时上下各扩 1 像素。SB_CTL:直接取整个客户区(控件窗口的客户区就是滚动条本身)。
WS_EX_LAYOUTRTL(从右向左布局)时 mirror_rect 把矩形水平镜像(scrollbar.c L68-L74),
把"右侧垂直滚动条"翻到左侧。
4. 滚动条信息管理(scrollbar.c 上半场)
4.1 延迟创建:co_IntCreateScrollBars / IntDestroyScrollBars
创建(scrollbar.c L757-L807):
c
BOOL FASTCALL
co_IntCreateScrollBars(PWND Window)
{
if (Window->pSBInfo && Window->pSBInfoex)
return TRUE; // 已存在
Size = 3 * (sizeof(SBINFOEX));
Window->pSBInfoex = ExAllocatePoolWithTag(PagedPool, Size, TAG_SBARINFO);
RtlZeroMemory(Window->pSBInfoex, Size);
Window->pSBInfo = DesktopHeapAlloc(Window->head.rpdesk, sizeof(SBINFO));
RtlZeroMemory(Window->pSBInfo, sizeof(SBINFO));
Window->pSBInfo->Vert.posMax = 100; // 默认范围 [0,100]
Window->pSBInfo->Horz.posMax = 100;
co_WinPosGetNonClientSize(Window, &Window->rcWindow, &Window->rcClient);
for(s = SB_HORZ; s <= SB_VERT; s++) {
psbi->cbSize = sizeof(SCROLLBARINFO);
for (i = 0; i < CCHILDREN_SCROLLBAR + 1; i++)
psbi->rgstate[i] = 0;
IntGetScrollBarRect(Window, s, &(psbi->rcScrollBar));
IntCalculateThumb(Window, s, psbi, pSBData);
}
return TRUE;
}
要点:
- 懒初始化 :
pSBInfo/pSBInfoex首次被访问时才分配,调用点是
co_IntSetScrollInfo(L480)、co_IntGetScrollBarInfo(L687)、co_IntSetScrollBarInfo(L741)、
NtUserEnableScrollBar(L1273)、NtUserSetScrollBarInfo(L1438)。窗口创建本身不分配。 - 双存储 :
pSBInfoex(分页池,3 个 SBINFOEX)与pSBInfo(桌面堆,SBINFO)分开分配;
若第二次分配失败,第一次的内存不会回滚(源码如此,属已知瑕疵)。 - 默认范围 :
posMax = 100,即新滚动条默认[0,100]。 - 初始几何 :创建时即用
IntGetScrollBarRect+IntCalculateThumb算出矩形与 thumb 位置,
保证第一次绘制前几何已就绪。 - 客户区修正 :调用
co_WinPosGetNonClientSize重算 rcClient------当窗口创建带WS_VSCROLL
时客户区宽度要扣掉滚动条宽度(与 winpos.c 联动)。
销毁 (L809-L821):IntDestroyScrollBars 释放两处内存并置 NULL。调用点是窗口销毁路径
(window.c L746),ExitProcessCallback/co_UserFreeWindow 清理窗口时连带释放。
4.2 IntGetSBData:SB_CTL 的 SBCALC 桥接
IntGetSBData(L76-L101)根据 nBar 返回 PSBDATA:
c
case SB_HORZ: return &pSBInfo->Horz;
case SB_VERT: return &pSBInfo->Vert;
case SB_CTL:
if (pwnd->cbwndExtra < (sizeof(SBWND)-sizeof(WND))) { ERR(...); return 0; }
pSBWnd = (PSBWND)pwnd;
return (PSBDATA)&pSBWnd->SBCalc; // 结构前缀兼容!
控件形态时校验 cbwndExtra 足以容纳 SBCALC,然后把 SBCALC 的前 4 个字段当作 SBDATA
返回。这样所有上层代码(co_IntSetScrollInfo 等)可以无差别地通过 PSBDATA 读写位置四元组,
无论背后是 SBINFO 还是控件附加字节。user32 侧有一份几乎相同的 IntGetSBData
(controls/scrollbar.c L89-L114),只是把 pwnd->pSBInfo 先经 DesktopPtrToUser 转成用户态
指针------因为桌面堆同时映射到用户态。
4.3 IntGetScrollBarRect:滚动条矩形计算
IntGetScrollBarRect(L103-L163)计算 rcScrollBar 并返回是否垂直:
c
*lprect = Wnd->rcClient;
RECTL_vOffsetRect(lprect, -Wnd->rcWindow.left, -Wnd->rcWindow.top); // 转窗口坐标
if (Wnd->ExStyle & WS_EX_LAYOUTRTL) mirror_rect(&Wnd->rcWindow, lprect);
case SB_HORZ:
lprect->top = lprect->bottom;
lprect->bottom += UserGetSystemMetrics(SM_CYHSCROLL);
if (Wnd->style & WS_BORDER) { lprect->left--; lprect->right++; }
else if (Wnd->style & WS_VSCROLL) { lprect->right++; }
break;
case SB_VERT:
if (Wnd->ExStyle & WS_EX_LEFTSCROLLBAR) { lprect->right = lprect->left; lprect->left -= SM_CXVSCROLL; }
else { lprect->left = lprect->right; lprect->right += SM_CXVSCROLL; }
if (Wnd->style & WS_BORDER) { lprect->top--; lprect->bottom++; }
else if (Wnd->style & WS_HSCROLL) { lprect->bottom++; }
break;
case SB_CTL:
IntGetClientRect(Wnd, lprect); // 客户区即滚动条
vertical = !!(Wnd->style & SBS_VERT);
break;
细节:
- 基准是
rcClient经平移得到窗口坐标(-rcWindow.left/top),即窗口坐标。 - 水平滚动条贴客户区底边往下
SM_CYHSCROLL高;垂直滚动条贴客户区右边往外SM_CXVSCROLL
宽(WS_EX_LEFTSCROLLBAR时贴左边往左)。 - 边框扩展:窗口有
WS_BORDER时滚动条向两侧各扩 1 像素压住边框;没有边框但与另一方向滚动条
相邻时扩 1 像素避免缝隙。这些 ±1 是多年的像素级打磨。 SM_CXVSCROLL/SM_CYHSCROLL是系统度量:垂直滚动条宽度(含箭头)/水平滚动条高度。
4.4 IntCalculateThumb:thumb 尺寸/位置计算
IntCalculateThumb(L165-L245)根据范围/位置/页面大小把 thumb 的像素几何算进
psbi->xyThumbTop/xyThumbBottom 与 psbi->dxyLineButton。算法(以垂直为例,水平对称):
Thumb = SM_CXVSCROLL(箭头尺寸);cxy= 滚动条全长(像素)。- 空间不足 (
cxy <= 2*Thumb):Thumb = cxy/2,thumb 尺寸为 0(不显示)。 - 无可滚动内容 (两个箭头都
STATE_SYSTEM_UNAVAILABLE且
posMin >= posMax - max(page-1, 0)):thumb 尺寸为 0。 - 否则:
ThumbBox:有 page 时至少MINTRACKTHUMB(8)像素,并按比例放大
max(EngMulDiv(cxy, page, posMax-posMin+1), ThumbBox);无 page 时为SM_CXHTHUMB
(thumb 最小宽度)。- 可用滑动区
cxy -= 2*Thumb(扣掉两个箭头)。 ThumbPos = Thumb + EngMulDiv(cxy - ThumbBox, pos - posMin, mx - posMin):
thumb 顶 = 上箭头高 + 按位置比例折算的偏移,mx = posMax - max(page-1, 0)是最大有效
位置。
psbi->dxyLineButton = Thumb:箭头尺寸返回给调用者。
EngMulDiv(a,b,c) 是 GDI 引擎的 a*b/c 64 位中间量整数运算(防溢出)。这是从 WINE
20020904 移植的经典算法(文件头注释注明 "Ported from WINE20020904")。
4.5 co_IntGetScrollInfo / NEWco_IntGetScrollInfo
co_IntGetScrollInfo (L263-L308):NtUserSBGetParms 的内核实现。步骤:
- 去掉
SIF_THEMED位(内部实现细节不外泄)。 - 校验
nBar(SBID_IS_VALID)与pSBInfo存在。 fMask == SIF_ALL时按SIF_PAGE|SIF_POS|SIF_RANGE|SIF_TRACKPOS全取,否则按调用者的掩码。- 从
psi = IntGetScrollInfoFromWindow(Window, nBar)(即 SBINFOEX 里的 SCROLLINFO)拷字段。
NEWco_IntGetScrollInfo (L310-L352):带拖动跟踪感知的版本。读取 pSBTrack(线程的
SBTRACK),当 pSBTrack->nBar == nBar && pSBTrack->spwndTrack == pWnd(本线程正在拖动该窗口的
这条滚动条)时,SIF_TRACKPOS 返回 pSBTrack->posNew(候选位置),否则返回静态 pos。
这实现了 GetScrollInfo 在 SB_THUMBTRACK 期间"拿到应用还没 SetScrollInfo 的临时位置"的
语义。注意它直接用 pSBData(SBDATA 指针)而非 SBINFOEX 的 SCROLLINFO------参数来源不同,
两份数据通过 co_IntSetScrollInfo 保持同步。
4.6 co_IntSetScrollInfo:核心状态机
co_IntSetScrollInfo(L454-L668)是滚动条信息管理的心脏 ,返回"当前位置"(或
SIF_PREVIOUSPOS 时的旧位置)。执行流程:
第一步:参数校验
c
if(!SBID_IS_VALID(nBar)) { EngSetLastError(ERROR_INVALID_PARAMETER); return FALSE; }
if(!co_IntCreateScrollBars(Window)) return FALSE;
if (lpsi->cbSize != sizeof(SCROLLINFO) &&
lpsi->cbSize != (sizeof(SCROLLINFO) - sizeof(lpsi->nTrackPos))) { ... return 0; }
if ((lpsi->fMask & ~SIF_THEMED) & ~(SIF_ALL | SIF_DISABLENOSCROLL | SIF_PREVIOUSPOS)) { ... return 0; }
cbSize 允许两种尺寸:完整 SCROLLINFO,或不含 nTrackPos 的旧式尺寸 (兼容老程序)。
fMask 只允许 SIF_ALL|SIF_DISABLENOSCROLL|SIF_PREVIOUSPOS(加 SIF_THEMED 被掩掉)。
第二步:落数据(写两份存储)
c
if (lpsi->fMask & SIF_PAGE) { Info->nPage = pSBData->page = lpsi->nPage; bChangeParams = TRUE; }
if (lpsi->fMask & SIF_POS) { OldPos = Info->nPos; Info->nPos = pSBData->pos = lpsi->nPos; }
if (lpsi->fMask & SIF_RANGE) { ... Info->nMin/nMax = pSBData->posMin/posMax = ...; }
SIF_PAGE/SIF_RANGE 变化置 bChangeParams = TRUE(几何可能变);SIF_RANGE 中
nMin > nMax 的异常输入被夹成 nMin == nMax。
第三步:范围/位置夹取
c
MaxPage = abs(Info->nMax - Info->nMin) + 1;
if (Info->nPage > MaxPage) pSBData->page = Info->nPage = MaxPage; // page 超界截断
MaxPos = Info->nMax + 1 - (int)max(Info->nPage, 1);
if (Info->nPos < Info->nMin) pSBData->pos = Info->nPos = Info->nMin; // pos 下界
else if (Info->nPos > MaxPos) pSBData->pos = Info->nPos = MaxPos; // pos 上界
注意 MaxPos = nMax + 1 - max(nPage,1):最大位置要保证"一页内容"能完整显示(page 语义)。
第四步:仅 DISABLENOSCROLL 时提前返回
c
if (!(lpsi->fMask & SIF_ALL)) // 只有 SIF_DISABLENOSCROLL / SIF_PREVIOUSPOS
return lpsi->fMask & SIF_PREVIOUSPOS ? OldPos : pSBData->pos;
第五步:显隐/禁用决策
c
new_flags = Window->pSBInfo->WSBflags;
if (Info->nMin + (int)max(Info->nPage, 1) > Info->nMax)
{ // 内容放不进一页 → 隐藏或禁用
if (lpsi->fMask & SIF_DISABLENOSCROLL) { new_flags = ESB_DISABLE_BOTH; bChangeParams = TRUE; }
else if ((nBar != SB_CTL) && bChangeParams) action = SA_SSI_HIDE;
}
else if ((lpsi->fMask & ~SIF_THEMED) != SIF_PAGE)
{ // 内容放得下且不只改了 page → 显示/启用
if ((nBar != SB_CTL) && bChangeParams) { new_flags = ESB_ENABLE_BOTH; action |= SA_SSI_SHOW; }
else if (nBar == SB_CTL) new_flags = ESB_ENABLE_BOTH;
}
if (Window->pSBInfo->WSBflags != new_flags) { ...; action |= SA_SSI_REPAINT_ARROWS; }
这就是 Windows 经典的"滚动条自动隐藏"逻辑:内容不超过一页时,窗口滚动条自动消失(除非应用
显式传 SIF_DISABLENOSCROLL,则改为禁用箭头);控件形态不自动隐藏(因为控件是独立窗口,
显隐由应用控制)。
第六步:执行动作 + 重绘
c
if (action & SA_SSI_HIDE) co_UserShowScrollBar(Window, nBar, FALSE, FALSE);
else {
if (action & SA_SSI_SHOW)
if (co_UserShowScrollBar(Window, nBar, TRUE, TRUE)) return ...; // SetWindowPos 已重绘
bVisible = 按 nBar 判定 (WS_HSCROLL/WS_VSCROLL/WS_VISIBLE);
if (bRedraw && bVisible) {
if (!(Info->fMask & SIF_THEMED)) {
if (action & SA_SSI_REPAINT_ARROWS)
co_UserRedrawWindow(Window, &UpdateRect, 0, RDW_INVALIDATE | RDW_FRAME); // 整条
else
IntRefeshScrollInterior(Window, nBar, psbi); // 仅内部
} else {
if (bChangeParams || (OldPos != pSBData->pos))
co_UserRedrawWindow(Window, &UpdateRect, 0, RDW_INVALIDATE | RDW_FRAME);
}
}
}
if (bChangeParams && (nBar == SB_HORZ || nBar == SB_VERT) && (lpsi->fMask & SIF_DISABLENOSCROLL))
IntEnableScrollBar(nBar == SB_HORZ, psbi, Window->pSBInfo->WSBflags);
return lpsi->fMask & SIF_PREVIOUSPOS ? OldPos : pSBData->pos;
重绘策略值得细说:非主题模式只重画"内部"(滑道+thumb,IntRefeshScrollInterior)以省时;
箭头状态变化(SA_SSI_REPAINT_ARROWS)或显示/隐藏时才整条 RDW_INVALIDATE|RDW_FRAME。
主题模式(SIF_THEMED)则参数一变化就整条重绘。
4.7 co_IntGetScrollBarInfo / co_IntSetScrollBarInfo
co_IntGetScrollBarInfo (L670-L722):填充 SCROLLBARINFO。步骤:
Bar = SBOBJ_TO_SBID(idObject)(OBJID_* → SB_*),校验合法性。co_IntCreateScrollBars确保存在。IntGetScrollBarRect+IntCalculateThumb刷新几何(每次查询都重算,保证最新)。- 计算
rgstate[0](整条滚动条):- 样式没带
WS_HSCROLL/WS_VSCROLL→STATE_SYSTEM_INVISIBLE; posMin >= posMax - max(page-1,0)(无可滚动内容)→STATE_SYSTEM_UNAVAILABLE(可见时)
或STATE_SYSTEM_OFFSCREEN(不可见时);- 控件被禁用(
WS_DISABLED)→STATE_SYSTEM_UNAVAILABLE。
- 样式没带
RtlCopyMemory(psbi, sbi, sizeof(SCROLLBARINFO))返回。
注意第 4 步控件禁用判断的写法是 if (Bar == SB_CTL && !(Window->style & WS_DISABLED))------
!(WS_DISABLED) 表示"未禁用"却置 UNAVAILABLE,这是源码里一个明显笔误(语义与意图相反),
分析时如实记录。
co_IntSetScrollBarInfo (L724-L755):把用户态写入的 nTrackPos/reserved/rgstate 拷回
内核(NtUserSetScrollBarInfo 的实体)。rgstate 是拖动中箭头按下状态同步的关键:user32 在
IntUpdateScrollArrows 里改完本地副本后调用它落盘,绘制才能读到按下状态。
4.8 IntEnableScrollBar / co_UserShowScrollBar
IntEnableScrollBar (L823-L859):按 ESB_* 命令更新 rgstate[SBRG_TOPRIGHTBTN]/
rgstate[SBRG_BOTTOMLEFTBTN] 为 STATE_SYSTEM_UNAVAILABLE(禁用)或 0(启用),返回是否有
变化(Chg)。ESB_DISABLE_LTUP 对水平条禁左箭头(TOPRIGHTBTN)、对垂直条禁上箭头
(TOPRIGHTBTN)------注意"上/左"在水平与垂直条上对应同一个 SBRG 槽位,靠 Horz 参数区分
方向语义。
co_UserShowScrollBar (L862-L905):显示/隐藏滚动条的核心。SB_CTL 形态直接
co_WinPosShowWindow(Wnd, fShowH ? SW_SHOW : SW_HIDE)(控件是独立窗口);窗口形态则改样式位
再触发边框重算:
c
case SB_BOTH:
case SB_HORZ:
if (fShowH) set_bits |= WS_HSCROLL; else clear_bits |= WS_HSCROLL;
if (nBar == SB_HORZ) break;
/* Fall through */
case SB_VERT:
if (fShowV) set_bits |= WS_VSCROLL; else clear_bits |= WS_VSCROLL;
break;
old_style = IntSetStyle(Wnd, set_bits, clear_bits);
if (样式实际变化) {
co_WinPosSetWindowPos(Wnd, 0,0,0,0,0,
SWP_NOSIZE|SWP_NOMOVE|SWP_NOACTIVATE|SWP_NOZORDER|SWP_FRAMECHANGED);
return TRUE;
}
return FALSE;
关键:改 WS_VSCROLL 样式后必须带 SWP_FRAMECHANGED 走 co_WinPosSetWindowPos,让 winpos
重算客户区(滚动条占用宽度变化)并触发 WM_NCPAINT 重绘。这正是"滚动条显隐会改变客户区尺寸"
的实现机制,与 winpos.c 深度耦合。
5. 滚动条绘制(scrollbar.c 下半场)
5.1 IntDrawScrollBar:总入口
IntDrawScrollBar(L1082-L1131)是绘制总入口,由 WM_NCPAINT 分支(nonclient.c L1289/
L1296/L1300)调用:
c
switch (Bar) {
case SB_HORZ: Vertical = FALSE; break;
case SB_VERT: Vertical = TRUE; break;
case SB_CTL: Vertical = (Wnd->style & SBS_VERT) != 0; break;
default: return;
}
if (!co_IntGetScrollBarInfo(Wnd, IntScrollGetObjectId(Bar), &Info)) return;
if (RECTL_bIsEmptyRect(&Info.rcScrollBar)) return;
if (Info.dxyLineButton) IntDrawScrollArrows(DC, &Info, Vertical); // 先画两个箭头
IntDrawScrollInterior(Wnd, DC, Bar, Vertical, &Info); // 再画滑道+thumb
if (Wnd == pti->MessageQueue->spwndFocus && Bar == SB_CTL) { // 控件有焦点时
if (Vertical) co_IntSetCaretPos(Info.rcScrollBar.top + 1, Info.dxyLineButton + 1);
else co_IntSetCaretPos(Info.dxyLineButton + 1, Info.rcScrollBar.top + 1);
}
要点:
- 先经
co_IntGetScrollBarInfo拿最新几何(含按下状态),空矩形直接跳过。 - 绘制顺序:箭头 → 内部(滑道 + thumb)。
- 控件获得焦点时把插入符定位到 thumb 位置(与 user32 控件 WM_SETFOCUS 创建插入符呼应)。
5.2 IntDrawScrollArrows
IntDrawScrollArrows(L1019-L1055)绘制上下(左右)两个箭头:
c
RectLT = RectRB = ScrollBarInfo->rcScrollBar;
if (Vertical) {
ScrollDirFlagLT = DFCS_SCROLLUP; ScrollDirFlagRB = DFCS_SCROLLDOWN;
RectLT.bottom = RectLT.top + ScrollBarInfo->dxyLineButton;
RectRB.top = RectRB.bottom - ScrollBarInfo->dxyLineButton;
} else {
ScrollDirFlagLT = DFCS_SCROLLLEFT; ScrollDirFlagRB = DFCS_SCROLLRIGHT;
RectLT.right = RectLT.left + ScrollBarInfo->dxyLineButton;
RectRB.left = RectRB.right - ScrollBarInfo->dxyLineButton;
}
if (rgstate[SCROLL_TOP_ARROW] & STATE_SYSTEM_PRESSED) ScrollDirFlagLT |= DFCS_PUSHED | DFCS_FLAT;
if (rgstate[SCROLL_TOP_ARROW] & STATE_SYSTEM_UNAVAILABLE) ScrollDirFlagLT |= DFCS_INACTIVE;
if (rgstate[SCROLL_BOTTOM_ARROW] & STATE_SYSTEM_PRESSED) ScrollDirFlagRB |= DFCS_PUSHED | DFCS_FLAT;
if (rgstate[SCROLL_BOTTOM_ARROW] & STATE_SYSTEM_UNAVAILABLE) ScrollDirFlagRB |= DFCS_INACTIVE;
DrawFrameControl(hDC, &RectLT, DFC_SCROLL, ScrollDirFlagLT);
DrawFrameControl(hDC, &RectRB, DFC_SCROLL, ScrollDirFlagRB);
DrawFrameControl(DFC_SCROLL, DFCS_SCROLLUP/...) 是画系统箭头按钮的标准途径(实现于
draw.c / user32 draw 代码),DFCS_PUSHED 按下凹陷、DFCS_INACTIVE 灰色禁用、DFCS_FLAT
扁平化(按下时不画立体边)。
5.3 IntDrawScrollInterior
IntDrawScrollInterior(L907-L1017)画滑道与 thumb,是绘制细节最密的一段:
- 颜色 :
SB_CTL走GetControlBrush(pWnd, hDC, WM_CTLCOLORSCROLLBAR)(父窗口可自定义
颜色,失败退回COLOR_SCROLLBAR系统色);窗口滚动条走DefWndControlColor(hDC, CTLCOLOR_SCROLLBAR)。hSaveBrush = NtGdiSelectBrush(hDC, hBrush)换入画刷。 - 滑道矩形 :垂直时
Rect= 滚动条去掉上下箭头(top + dxyLineButton到
bottom - dxyLineButton)的中段;水平同理。 - 无 thumb (
!xyThumbBottom):整段PATCOPY平涂,直接返回。 - 有 thumb :
ThumbTop -= dxyLineButton(把 thumb 顶转成相对滑道的偏移)。- 上滑道
NtGdiPatBlt(..., ThumbTop, TopSelected ? BLACKNESS : PATCOPY):按下时用
BLACKNESS画黑(凹陷),否则平涂。 - 下滑道类似,从
ThumbTop + ThumbSize画到滑道底。 DrawEdge(hDC, &Rect, EDGE_RAISED, BF_RECT | BF_MIDDLE):thumb 画凸起立体边框。
NtGdiSelectBrush(hDC, hSaveBrush)还原画刷。
TopSelected/BottomSelected 来自 rgstate[SCROLL_TOP_RECT]/rgstate[SCROLL_BOTTOM_RECT]
的 STATE_SYSTEM_PRESSED------即用户按住滑道时,对应区域被"画黑"形成按下反馈。整段逻辑
与 user32 版本(controls/scrollbar.c L116-L229)逐行对应。
5.4 IntRefeshScrollInterior 与重绘策略
IntRefeshScrollInterior(L1067-L1080)是"只刷新内部"的便捷封装:
c
hdc = UserGetDCEx(pWnd, NULL, DCX_CACHE | ((nBar == SB_CTL) ? 0 : DCX_WINDOW));
if (hdc) {
co_IntGetScrollBarInfo(pWnd, IntScrollGetObjectId(nBar), psbi); // 刷新几何/状态
IntDrawScrollInterior(pWnd, hdc, nBar, Vertical, psbi); // 重画滑道+thumb
UserReleaseDC(pWnd, hdc, FALSE);
}
DCX_WINDOW 用于窗口滚动条(画在窗口坐标系,含非客户区),DCX_CACHE 用缓存 DC。
SetScrollPos 只移动 thumb 时走这条路,避免整条重绘的闪烁。IntScrollGetObjectId(L1057-
L1065)做 SB_* → OBJID_* 反向映射(SB_VERT→OBJID_VSCROLL、SB_HORZ→OBJID_HSCROLL、
其余→OBJID_CLIENT)。
6. NtUser 系统调用层(scrollbar.c 尾部)
6.1 NtUserSetScrollInfo(L1340-L1378)
c
DWORD APIENTRY NtUserSetScrollInfo(HWND hWnd, int fnBar, LPCSCROLLINFO lpsi, BOOL bRedraw)
{
UserEnterExclusive();
if (!(Window = UserGetWindowObject(hWnd)) || UserIsDesktopWindow(Window) || UserIsMessageWindow(Window))
goto Cleanup; // 桌面/消息窗口拒绝
UserRefObjectCo(Window, &Ref); // CO 引用保护
Status = MmCopyFromCaller(&ScrollInfo, lpsi, sizeof(SCROLLINFO) - sizeof(ScrollInfo.nTrackPos));
Ret = co_IntSetScrollInfo(Window, fnBar, &ScrollInfo, bRedraw);
...
UserLeave();
return Ret; // 返回当前(或旧)位置
}
细节:
- 拒绝桌面窗口与消息窗口(它们不允许有滚动条语义)。
- 从调用者拷贝时有意少拷 nTrackPos (
sizeof - sizeof(nTrackPos))------nTrackPos 是只读
输出,用户态 SetScrollInfo 不应该写它。 - 引用计数用
UserRefObjectCo(CO = 调用者拥有引用),防止回调/重入期间窗口被销毁。 - 返回 DWORD 位置:user32 的
SetScrollInfo把它作为返回值,配合SIF_PREVIOUSPOS可拿旧值。
6.2 NtUserSBGetParms(L1191-L1249)
GetScrollInfo 的内核入口,签名很"丑"(文件注释直接写 "Ugly NtUser API"):
c
BOOL APIENTRY NtUserSBGetParms(HWND hWnd, int fnBar, PSBDATA pSBData, LPSCROLLINFO lpsi)
{
UserEnterShared(); // 只读,共享锁
_SEH2_TRY { RtlCopyMemory(&psi, lpsi, sizeof(SCROLLINFO)); if (pSBData) RtlCopyMemory(&SBDataSafe, pSBData, sizeof(SBDATA)); }
_SEH2_EXCEPT(...) { ... goto Exit; }
Window = UserGetWindowObject(hWnd);
Ret = co_IntGetScrollInfo(Window, fnBar, &SBDataSafe, &psi);
_SEH2_TRY { RtlCopyMemory(lpsi, &psi, sizeof(SCROLLINFO)); } _SEH2_EXCEPT(...) { ... }
...
}
- 拷贝
SCROLLINFO与可选的SBDATA时用 SEH 保护(用户指针可能非法)。 pSBData参数是 ReactOS 特设的"额外输出":user32 的SBM_GETSCROLLINFO处理
(controls/scrollbar.c L1423)会把控件附加字节里的SBCALC传进来,让内核把权威数据拷进
控件自身的几何缓存,实现控件与内核数据同步。- 走共享锁 (
UserEnterShared),与 Set 的独占锁区分,支持并发读。
6.3 NtUserShowScrollBar(L1380-L1402)
c
DWORD APIENTRY NtUserShowScrollBar(HWND hWnd, int nBar, DWORD bShow)
{
Window = UserGetWindowObject(hWnd);
ret = co_UserShowScrollBar(Window, nBar,
(nBar == SB_VERT) ? 0 : bShow, // 只有 HORZ/BOTH 时才有水平参数
(nBar == SB_HORZ) ? 0 : bShow); // 只有 VERT/BOTH 时才有垂直参数
...
}
bShow 被拆成 fShowH/fShowV 两个参数传给 co_UserShowScrollBar:SB_HORZ 时只有水平
参数有效,SB_VERT 时只有垂直参数有效,SB_BOTH 两个都生效,SB_CTL 时第一个参数
(fShowH)决定显示/隐藏(co_UserShowScrollBar 的 SB_CTL 分支只用 fShowH)。
6.4 NtUserEnableScrollBar(L1251-L1338)
c
if (!(Window = UserGetWindowObject(hWnd)) || UserIsDesktopWindow(Window) || UserIsMessageWindow(Window))
goto Cleanup;
co_IntCreateScrollBars(Window);
OrigArrows = Window->pSBInfo->WSBflags;
Window->pSBInfo->WSBflags = wArrows; // 直接覆盖 WSBflags
if (wSBflags == SB_CTL) {
if (wArrows == ESB_DISABLE_BOTH || wArrows == ESB_ENABLE_BOTH)
IntEnableWindow(hWnd, (wArrows == ESB_ENABLE_BOTH)); // 控件整体启用/禁用
Ret = TRUE; goto Cleanup;
}
if (wSBflags != SB_BOTH && !SBID_IS_VALID(wSBflags)) { ...ERROR_INVALID_PARAMETER... }
switch(wSBflags) {
case SB_BOTH: InfoV = IntGetScrollbarInfoFromWindow(Window, SB_VERT); /* fall through */
case SB_HORZ: InfoH = IntGetScrollbarInfoFromWindow(Window, SB_HORZ); break;
case SB_VERT: InfoV = IntGetScrollbarInfoFromWindow(Window, SB_VERT); break;
}
if (InfoV) Chg = IntEnableScrollBar(FALSE, InfoV, wArrows);
if (InfoH) Chg = (IntEnableScrollBar(TRUE, InfoH, wArrows) || Chg);
ERR("FIXME: EnableScrollBar wSBflags %u wArrows %u Chg %d\n", ...);
Ret = Chg;
goto Cleanup; // FIXME ------ 后续的 OrigArrows 比较代码是死代码
几个值得记录的点:
SB_CTL形态把EnableScrollBar映射为窗口启用/禁用 (IntEnableWindow),符合 Windows
语义(禁用控件即禁用整个滚动条)。- 非控件形态只更新
rgstate箭头状态并返回"是否有变化",重绘由 user32 完成 ------注释
"Done in user32: SCROLL_RefreshScrollBar(hwnd, nBar, TRUE, TRUE)",user32 的
EnableScrollBar(controls/scrollbar.c L1478)在系统调用返回非零后调用
SCROLL_RefreshScrollBar重绘整条。 - 函数尾部有一段
if (OrigArrows == wArrows)的代码,但前面已经goto Cleanup,属于不可达的
残留(源码中带 FIXME 注释)。
6.5 NtUserGetScrollBarInfo / NtUserSetScrollBarInfo(L1151-L1189 / L1406-L1464)
Get :MmCopyFromCaller 取 cbSize 校验 → UserGetWindowObject → co_IntGetScrollBarInfo
→ MmCopyToCaller 回写。user32 的 GetScrollBarInfo(controls/scrollbar.c L1543)调用后还要
把 rcScrollBar 加上窗口原点转成屏幕坐标(内核返回的是窗口坐标)。
Set :SBOBJ_TO_SBID 校验 → co_IntCreateScrollBars → 拷贝 SETSCROLLBARINFO(即
{ nTrackPos; reserved; rgstate[6] })→ 写入 psi->nTrackPos、sbi->reserved、
sbi->rgstate。它是拖动中"内核状态 ← 用户态按下状态"的回写通道。
6.6 ScrollBarWndProc(内核侧控件窗口过程,L1133-L1149)
c
LRESULT APIENTRY ScrollBarWndProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam)
{
switch(Msg) {
case WM_ENABLE:
if (pWnd->pSBInfo)
pWnd->pSBInfo->WSBflags = wParam ? ESB_ENABLE_BOTH : ESB_DISABLE_BOTH;
break;
}
return lResult;
}
这是 win32k 为 FNID_SCROLLBAR 窗口注册的内核侧过程,目前只处理 WM_ENABLE 同步
WSBflags。真正的控件窗口过程在 user32(ScrollBarWndProc_common),两处通过 FNID 关联:
窗口创建时 user32 调 NtUserSetWindowFNID(Wnd, FNID_SCROLLBAR)(controls/scrollbar.c L1240)
标记控件身份。
7. 区域滚动:scrollex.c
7.1 区域滚动算法总述
ScrollWindowEx 是"滚动窗口内容"的标准 API。它的算法是移动位图 + 暴露区重绘:
滚动前 滚动后(dx, dy)
┌─────────────┬──────┐ ┌──────┬──────────────┐
│ │ │ │ │ │
│ 内容 │ 暴露 │ │ 新暴露│ 移动过来的 │
│ (A) │ 区(B)│ → BitBlt → │ (B') │ 内容(A) │
│ │ │ │ │ │
└─────────────┴──────┘ └──────┴──────────────┘
B 区域无法从原内容得到 → 加入更新区域,由 WM_PAINT 重绘
具体而言 IntScrollWindowEx 做四件事:
- 移动位图 :
UserScrollDC用NtGdiBitBlt把源矩形(rcSrc)拷到目标矩形
(rcDst = rcSrc + (dx,dy),并与裁剪区相交)。 - 计算暴露区 :移动后未被覆盖的矩形带(
rcDst与rcSrc的差)加入更新区域。 - 重绘 :
co_UserRedrawWindow用更新区域触发WM_PAINT(可带SW_ERASE擦背景)。 - 移动子窗口 (
SW_SCROLLCHILDREN):遍历子窗口,把rcWindow/rcClient平移
(dx,dy)并发送WM_MOVE。
7.2 UserScrollDC:位图移动与暴露区计算
UserScrollDC(scrollex.c L98-L224)是纯 GDI 层的移动函数,返回区域类型
(NULLREGION/SIMPLEREGION/COMPLEXREGION/ERROR)。核心逻辑:
c
/* 1. 裁剪区与滚动区求交 */
GdiGetClipBox(hDC, &rcClip);
if (prcClip) RECTL_bIntersectRect(&rcClip, &rcClip, prcClip);
if (prcScroll) { rcScroll = *prcScroll; RECTL_bIntersectRect(&rcSrc, &rcClip, prcScroll); }
else rcSrc = rcClip;
/* 2. 目标矩形 = 源 + 偏移,并裁剪到可见 */
rcDst = rcSrc;
RECTL_vOffsetRect(&rcDst, dx, dy);
RECTL_bIntersectRect(&rcDst, &rcClip, &rcDst);
/* 3. 位图移动:把源内容搬到目标位置 */
NtGdiBitBlt(hDC, rcDst.left, rcDst.top, rcDst.right-rcDst.left, rcDst.bottom-rcDst.top,
hDC, rcDst.left-dx, rcDst.top-dy, SRCCOPY, CLR_INVALID, 0);
NtGdiBitBlt 用 SRCCOPY 把"目标位置偏移 -dx/-dy 处的原内容"拷到目标位置,即整幅位图平移。
注意源坐标 rcDst.left-dx 就是 rcSrc.left(因为 rcDst = rcSrc + (dx,dy)),用回读方式表达
更简洁。
暴露区(更新区域)计算是重头戏(L157-L223):
c
/* 从移动后、裁剪后的滚动区开始 */
rcDst = rcScroll;
RECTL_vOffsetRect(&rcDst, dx, dy);
RECTL_bIntersectRect(&rcDst, &rcDst, &rcClip);
REGION_SetRectRgn(RgnOwn, rcDst.left, rcDst.top, rcDst.right, rcDst.bottom);
/* 并上源矩形 */
RgnTmp = IntSysCreateRectpRgnIndirect(&rcSrc);
IntGdiCombineRgn(RgnOwn, RgnOwn, RgnTmp, RGN_OR);
/* 减去"源中原本可见的部分"(这些内容已被 BitBlt 搬走,不算暴露) */
IntGdiCombineRgn(RgnTmp, RgnTmp, pDC->prgnVis, RGN_AND);
REGION_bOffsetRgn(RgnTmp, dx, dy);
Result = IntGdiCombineRgn(RgnOwn, RgnOwn, RgnTmp, RGN_DIFF);
数学上:暴露区 = (移动后的滚动区 ∪ 源矩形) − (源矩形 ∩ 窗口可见区域)平移(dx,dy)。直观解释:
位图移动后,需要重绘的是"新位置区域中那些不是从原可见内容搬过来的部分"。pDC->prgnVis 是
DC 的可见区域(vis.c 合成),只有原本可见的源像素才真正被搬走;被遮挡部分的源像素从未画过,
搬走后仍是空洞,必须算进暴露区。
函数同时支持三种输出通道(可组合):
RgnUpdate(PREGION,内核区域对象):暴露区累加到这里。hrgnUpdate(HRGN,GDI 区域句柄):先REGION_LockRgn加锁,结束时解锁。prcUpdate(RECT*):REGION_GetRgnBox(RgnOwn, prcUpdate)取包围盒。
代码还特别注释 "DO NOT Unlock DC while messing with prgnVis!"------在区域运算期间持有 DC 锁,
防止可见区域被并发修改。
7.3 IntScrollWindowEx:完整流程
IntScrollWindowEx(scrollex.c L226-L454)是区域滚动的总调度,返回区域类型
(NULLREGION=无需重绘等)。逐步展开:
① 前置检查与矩形求交
c
if (!Window || !IntIsWindowDrawable(Window)) return ERROR;
IntGetClientRect(Window, &rcClip);
if (prcScroll) RECTL_bIntersectRect(&rcScroll, &rcClip, prcScroll);
else rcScroll = rcClip;
if (prcClip) RECTL_bIntersectRect(&rcClip, &rcClip, prcClip);
if (rcClip 为空 || (dx == 0 && dy == 0)) return NULLREGION; // 没动,直接返回
IntIsWindowDrawable 检查窗口可见可绘制;prcScroll/prcClip 都与客户区求交,保证不越界。
② 更新区域准备
c
RgnUpdate = IntSysCreateRectpRgn(0,0,0,0);
if (hrgnUpdate) { // 拷贝调用者传入的初始更新区域(不持锁,因为后面有用户态回调)
RgnTemp = REGION_LockRgn(hrgnUpdate);
IntGdiCombineRgn(RgnUpdate, RgnTemp, NULL, RGN_COPY);
REGION_UnlockRgn(RgnTemp);
}
注释明确解释:必须拷贝 hrgnUpdate,因为之后要向用户态发消息(回调),期间不能持有区域锁。
③ 选择 DC 类型
c
if (flags & SW_SCROLLWNDDCE) { // ScrollWindow 语义:窗口 DC
dcxflags = DCX_USESTYLE;
if (!(Window->pcls->style & (CS_OWNDC|CS_CLASSDC))) dcxflags |= DCX_CACHE; // 非自有 DC 用缓存
if (flags & SW_SCROLLCHILDREN && Window->style & WS_CLIPCHILDREN)
dcxflags |= DCX_CACHE|DCX_NOCLIPCHILDREN;
} else { // ScrollWindowEx 语义:缓存 DC
dcxflags = DCX_CACHE|DCX_USESTYLE;
if (flags & SW_SCROLLCHILDREN) dcxflags |= DCX_NOCLIPCHILDREN;
}
hDC = UserGetDCEx(Window, 0, dcxflags);
SW_SCROLLWNDDCE 区分 ScrollWindow(用窗口 DC,可含非客户区)与 ScrollWindowEx
(只用客户区缓存 DC)。DCX_NOCLIPCHILDREN 在滚动子窗口时避免子窗口裁剪干扰位图移动。
④ 滚动中的插入符
c
rcCaret = rcScroll;
hwndCaret = co_IntFixCaret(Window, &rcCaret, flags); // 若插入符在滚动区内则隐藏
Result = UserScrollDC(hDC, dx, dy, &rcScroll, &rcClip, NULL, RgnUpdate, prcUpdate);
UserReleaseDC(Window, hDC, FALSE);
co_IntFixCaret(L13-L57)检测插入符(caret)矩形是否与滚动区相交:相交则
co_UserHideCaret(0) 隐藏并返回 caret 窗口,滚动完成后(见⑧)按新位置
co_IntSetCaretPos(rcCaret.left + dx, rcCaret.top + dy) 重放并 co_UserShowCaret。这是防止
"caret 跟随滚动而撕裂"的标准处理。
⑤ 滚动造成的额外损伤
c
RgnTemp = IntSysCreateRectpRgn(0,0,0,0);
if (co_IntGetUpdateRgn(Window, RgnTemp, FALSE) != NULLREGION) {
// 滚动位图时窗口可能已有其他更新区域(比如别的失效),
// 把滚动产生的损伤加进去一起重绘
if (hrgnUpdate) { RgnWinupd = ...; IntGdiCombineRgn(RgnWinupd, RgnTemp, 0, RGN_COPY); }
REGION_bOffsetRgn(RgnTemp, dx, dy);
IntGdiCombineRgn(RgnTemp, RgnTemp, RgnClip, RGN_AND);
if (hrgnUpdate) IntGdiCombineRgn(RgnWinupd, RgnWinupd, RgnTemp, RGN_OR);
co_UserRedrawWindow(Window, NULL, RgnTemp, rdw_flags); // 立即重绘暴露区
REGION_Delete(RgnClip);
}
co_IntGetUpdateRgn(L62-L96)取窗口当前更新区域(与客户区求交、与父窗口裁剪相交、平移到
客户区坐标)。滚动后旧更新区域也要跟着平移 (dx,dy),否则会画错位置。rdw_flags 由
SW_ERASE|SW_INVALIDATE 组合决定是否擦背景:
c
rdw_flags = (flags & SW_ERASE) && (flags & SW_INVALIDATE) ? RDW_INVALIDATE | RDW_ERASE : RDW_INVALIDATE;
⑥ 子窗口移动(SW_SCROLLCHILDREN)
c
if (flags & SW_SCROLLCHILDREN) {
IntGetClientOrigin(Window, &ClientOrigin);
for (Child = Window->spwndChild; Child; Child = Child->spwndNext) {
rcChild = Child->rcWindow; // 记录滚动前位置
RECTL_vOffsetRect(&rcChild, -ClientOrigin.x, -ClientOrigin.y);
RECTL_vOffsetRect(&Child->rcWindow, dx, dy); // 平移 rcWindow/rcClient
RECTL_vOffsetRect(&Child->rcClient, dx, dy);
if (!prcScroll || RECTL_bIntersectRect(&rcDummy, &rcChild, &rcScroll)) {
if (UserIsDesktopWindow(Window->spwndParent))
lParam = MAKELONG(Child->rcClient.left, Child->rcClient.top);
else
lParam = MAKELONG(rcChild.left + dx, rcChild.top + dy);
co_IntSendMessage(UserHMGetHandle(Child), WM_MOVE, 0, lParam); // 通知子窗口
}
}
}
子窗口在滚动时整体位移 :直接改 rcWindow/rcClient(注意这是窗口坐标的平移,两个矩形
的差值不变),然后给每个(与滚动区相交的)子窗口发 WM_MOVE,lParam 带新位置。父窗口是桌面
窗口时位置用客户区原点表示。源码注释提到 Wine 还会发 WM_POSCHANGING/WM_POSCHANGED,ReactOS
这里只发 WM_MOVE("windows sometimes a WM_MOVE")。
⑦ 重绘滚动暴露区
c
if (flags & (SW_INVALIDATE | SW_ERASE)) {
co_UserRedrawWindow(Window, NULL, RgnUpdate, rdw_flags |
((flags & SW_SCROLLCHILDREN) ? RDW_ALLCHILDREN : RDW_NOCHILDREN));
}
RgnUpdate 是"本次滚动产生的暴露区",SW_INVALIDATE 时异步置为窗口更新区域(WM_PAINT 时重绘),
SW_ERASE 时擦背景。子窗口模式用 RDW_ALLCHILDREN 让子窗口一起失效。
⑧ 恢复插入符 + 回写调用者更新区域
c
if (hwndCaret && (CaretWnd = UserGetWindowObject(hwndCaret))) {
co_IntSetCaretPos(rcCaret.left + dx, rcCaret.top + dy);
co_UserShowCaret(CaretWnd);
}
if (hrgnUpdate && (Result != ERROR)) {
RgnTemp = REGION_LockRgn(hrgnUpdate);
IntGdiCombineRgn(RgnTemp, RgnUpdate, RgnWinupd, RGN_OR); // 滚动损伤 + 本次暴露区
REGION_UnlockRgn(RgnTemp);
}
最终把"滚动暴露区(RgnUpdate)∪ 额外损伤(RgnWinupd)"合并回调用者的 hrgnUpdate,调用者
(应用)随后可对 hrgnUpdate 做 GetUpdateRgn 等处理。
7.4 co_IntFixCaret:滚动中的插入符
co_IntFixCaret(L13-L57)前面已述,补充细节:
c
hWndCaret = CaretInfo->hWnd; // 队列中的 caret 所有者
WndCaret = ValidateHwndNoErr(hWndCaret);
if (WndCaret == Window ||
((flags & SW_SCROLLCHILDREN) && IntIsChildWindow(Window, WndCaret))) {
// caret 属于本窗口(或其子窗口)且在滚动区内 → 隐藏并返回
rcCaret = caret 矩形;
if (RECTL_bIntersectRect(lprc, lprc, &rcCaret)) {
co_UserHideCaret(0);
lprc->left = pt.x; lprc->top = pt.y; // 记录 caret 原点供滚动后重放
return hWndCaret;
}
}
return 0;
caret 信息(位置 Pos、尺寸 Size、所属窗口 hWnd)存在队列的 CaretInfo(THRDCARETINFO),
由 caret.c 维护。滚动手法上,caret 与滚动内容是一体的,隐藏-滚动-重放避免撕裂。
7.5 co_IntGetUpdateRgn
co_IntGetUpdateRgn(L62-L96)是"旧式 GetUpdateRgn":
c
if (bErase) co_IntPaintWindows(Window, RDW_NOCHILDREN, FALSE); // 先擦背景
Window->state &= ~WNDS_UPDATEDIRTY;
if (Window->hrgnUpdate == NULL) { REGION_SetRectRgn(Rgn,0,0,0,0); return NULLREGION; }
UpdateRgn = REGION_LockRgn(Window->hrgnUpdate);
Rect = Window->rcClient;
IntIntersectWithParents(Window, &Rect); // 与祖先窗口可见区求交
REGION_SetRectRgn(Rgn, Rect...);
RegionType = IntGdiCombineRgn(Rgn, Rgn, UpdateRgn, RGN_AND);
REGION_bOffsetRgn(Rgn, -Window->rcClient.left, -Window->rcClient.top); // 转客户区坐标
REGION_UnlockRgn(UpdateRgn);
return RegionType;
WNDS_UPDATEDIRTY 标志表示更新区域"脏",读取后清除。滚动算法用它判断"滚动之前窗口已有失效
区域",这些区域需要随滚动平移。
7.6 IntScrollWindow
IntScrollWindow(L457-L466)是 ScrollWindow(无 Ex 版本)的内核实现:
c
BOOL FASTCALL IntScrollWindow(PWND pWnd, int dx, int dy,
CONST RECT *lpRect, CONST RECT *prcClip)
{
return IntScrollWindowEx(pWnd, dx, dy, lpRect, prcClip, 0, NULL,
(lpRect ? 0 : SW_SCROLLCHILDREN) | (SW_ERASE|SW_INVALIDATE|SW_SCROLLWNDDCE)) != ERROR;
}
lpRect == NULL 时滚整个客户区并带动子窗口(SW_SCROLLCHILDREN),否则只滚指定矩形。
SW_SCROLLWNDDCE 表示走窗口 DC。user32 的 ScrollWindow(windows/window.c L1906)直接把这
组标志透传给 NtUserScrollWindowEx,与内核 IntScrollWindow 等价。
7.7 NtUserScrollWindowEx / NtUserScrollDC
NtUserScrollWindowEx(L570-L660)系统调用包装:
c
UserEnterExclusive();
Window = UserGetWindowObject(hWnd);
if (!Window || !IntIsWindowDrawable(Window)) goto Cleanup; // 不可绘制 → ERROR
UserRefObjectCo(Window, &Ref);
_SEH2_TRY { // Probe 用户指针,安全拷贝 prcScroll/prcClip
if (prcUnsafeScroll) { ProbeForRead(prcUnsafeScroll, sizeof(*prcUnsafeScroll), 1); rcScroll = *prcUnsafeScroll; }
if (prcUnsafeClip) { ProbeForRead(prcUnsafeClip, sizeof(*prcUnsafeClip), 1); rcClip = *prcUnsafeClip; }
}
Result = IntScrollWindowEx(Window, dx, dy, prcUnsafeScroll ? &rcScroll : NULL,
prcUnsafeClip ? &rcClip : NULL, hrgnUpdate,
prcUnsafeUpdate ? &rcUpdate : NULL, flags);
if (prcUnsafeUpdate) { // 滚动完成后再回写 prcUpdate
_SEH2_TRY { ProbeForWrite(prcUnsafeUpdate, sizeof(*prcUnsafeUpdate), 1); *prcUnsafeUpdate = rcUpdate; }
}
prcUnsafeUpdate 的回写放在滚动之后 ,注释专门说明:"Probe here, to not fail on invalid
pointer before scrolling"------即使调用者给的是坏指针,滚动内容也要先完成(Windows 语义)。
NtUserScrollDC (L474-L561):对 HDC 的滚动(不限于窗口 DC),核心是
UserScrollDC(hDC, dx, dy, ...) 直接复用位图移动逻辑,prcUnsafeUpdate 回写同样在滚动后。
7.8 子窗口移动的坐标系细节
IntScrollWindowEx ⑥ 中的 lParam 计算值得单独强调:
rcChild先在滚动前换算成"相对父客户区"的坐标(减ClientOrigin)。- 父窗口的父是桌面窗口时,子窗口位置直接取新
rcClient.left/top(桌面坐标系下
rcClient 原点即窗口位置)。 - 否则
lParam = MAKELONG(rcChild.left + dx, rcChild.top + dy):滚动前相对位置 + 位移。
WM_MOVE 的 lParam 低字 x、高字 y,语义是"窗口的新位置(相对父客户区)"。子窗口滚动后
GetWindowRect 的结果自动变化(rcWindow 已平移),而应用在 WM_MOVE 里读到的坐标正是这个
lParam。这是 ScrollWindowEx 保证"子窗口跟着内容一起滚"的关键。
8. 用户态滚动条控件(user32 controls/scrollbar.c)
8.1 类注册:SCROLL_builtin_class
c
const struct builtin_class_descr SCROLL_builtin_class =
{
L"ScrollBar", /* 类名 */
CS_DBLCLKS | CS_VREDRAW | CS_HREDRAW | CS_PARENTDC, /* 类样式 */
ScrollBarWndProcA, /* ANSI 窗口过程 */
ScrollBarWndProcW, /* Unicode 窗口过程 */
sizeof(SBWND)-sizeof(WND), /* 附加字节:容纳 SBCALC */
IDC_ARROW, /* 光标 */
0 /* 背景画刷(不擦背景,自己画) */
};
系统类 "ScrollBar" 由 user32 启动时经 builtin_class 机制注册(与 "Button"、"Edit" 等同
机制)。cbWndExtra 恰好容纳 SBCALC(与内核 IntGetSBData 的 cbwndExtra 校验呼应)。
CS_PARENTDC 使控件直接用父窗口 DC 省内存;CS_VREDRAW|CS_HREDRAW 保证尺寸变化即重绘。
8.2 ScrollBarWndProc_common:控件窗口过程
ScrollBarWndProc_common(L1216-L1458)是 A/W 共用的控件窗口过程,消息处理全景:
| 消息 | 处理 |
|---|---|
WM_CREATE |
IntScrollCreateScrollBar:NtUserSetScrollInfo(SB_CTL, range[0,0], FALSE) 初始化 + 按 SBS_*ALIGN 样式把窗口 MoveWindow 到对齐位置 |
WM_ENABLE |
转发 SBM_ENABLE_ARROWS(wParam ? ESB_ENABLE_BOTH : ESB_DISABLE_BOTH) |
WM_LBUTTONDBLCLK/WM_LBUTTONDOWN |
SBS_SIZEGRIP 时发 WM_SYSCOMMAND SC_SIZE+WMSZ_BOTTOMRIGHT(尺寸柄拖动窗口),否则 ScrollTrackScrollBar(Wnd, SB_CTL, Pt) 进入拖动循环 |
WM_LBUTTONUP/WM_MOUSEMOVE/WM_SYSTIMER |
IntScrollHandleScrollEvent 继续跟踪 |
WM_KEYDOWN |
IntScrollHandleKbdEvent:VK_PRIOR/NEXT/HOME/END/UP/DOWN 映射为 SB_PAGEUP/PAGEDOWN/TOP/BOTTOM/LINEUP/LINEDOWN 发给父窗口 |
WM_KEYUP |
ShowCaret |
WM_SETFOCUS |
按 thumb 尺寸 CreateCaret(1 像素宽位图 + 尺寸)、SetCaretPos、ShowCaret |
WM_KILLFOCUS |
隐藏 caret、InvalidateRect thumb 区域、DestroyCaret |
WM_ERASEBKGND |
返回 1(不擦,全部由 WM_PAINT 画) |
WM_GETDLGCODE |
返回 DLGC_WANTARROWS(方向键留给控件) |
WM_PAINT |
SBS_SIZEGRIP → IntScrollDrawSizeGrip;SBS_SIZEBOX → 填色;否则 IntDrawScrollBar(Wnd, Dc, SB_CTL) |
SBM_GETPOS |
返回 pSBWnd->SBCalc.pos(直接读附加字节) |
SBM_GETRANGE |
写 SBCalc.posMin/posMax 到 wParam/lParam |
SBM_ENABLE_ARROWS |
EnableScrollBar(Wnd, SB_CTL, wParam) |
SBM_SETPOS |
`SCROLLINFO{SIF_POS |
SBM_SETRANGE/SBM_SETRANGEREDRAW |
{SIF_RANGE} → IntSetScrollInfo,后者带重绘 |
SBM_SETSCROLLINFO |
IntSetScrollInfo(Wnd, lParam, wParam) |
SBM_GETSCROLLINFO |
NtUserSBGetParms(Wnd, SB_CTL, &SBCalc, lParam)(借道内核同步 SBCalc) |
SBM_GETSCROLLBARINFO |
NtUserGetScrollBarInfo(Wnd, OBJID_CLIENT, lParam) |
0x00e5 等 |
WARN 未实现的 Win32 消息 |
| default | DefWindowProcW/A |
开头有一段 ReactOS 特有的 FNID 校验(L1219-L1251):控件创建时若 fnid 为空则在
WM_CREATE 时 NtUserSetWindowFNID(Wnd, FNID_SCROLLBAR),其他消息先走 DefWindowProc;
已标记则校验 fnid == FNID_SCROLLBAR,不一致报错。
IntScrollCreateScrollBar(L1074-L1145)的对齐逻辑按 SBS_LEFTALIGN/SBS_RIGHTALIGN/
SBS_TOPALIGN/SBS_BOTTOMALIGN/SBS_SIZEBOXTOPLEFTALIGN/SBS_SIZEBOXBOTTOMRIGHTALIGN
调整窗口位置,尺寸取 SM_CXVSCROLL+1 / SM_CYHSCROLL+1。
8.3 命中测试:IntScrollHitTest
IntScrollHitTest(L438-L470)把鼠标点映射为 SCROLL_* 命中码:
c
if ((Dragging && !IntScrollPtInRectEx(&rcScrollBar, Pt, Vertical)) ||
!PtInRect(&rcScrollBar, Pt)) return SCROLL_NOWHERE;
ThumbPos = xyThumbTop; ThumbSize = xyThumbBottom - ThumbPos; ArrowSize = dxyLineButton;
if (Vertical) {
if (Pt.y < top + ArrowSize) return SCROLL_TOP_ARROW; // 上箭头
if (Pt.y >= bottom - ArrowSize) return SCROLL_BOTTOM_ARROW; // 下箭头
if (!ThumbPos) return SCROLL_TOP_RECT; // 无 thumb → 都算上滑道
Pt.y -= top;
if (Pt.y < ThumbPos) return SCROLL_TOP_RECT; // 上滑道
if (Pt.y >= ThumbPos + ThumbSize) return SCROLL_BOTTOM_RECT; // 下滑道
}
return SCROLL_THUMB; // 命中 thumb
IntScrollPtInRectEx(L410-L436)在拖动(Dragging)时把命中矩形外扩 :垂直条水平方向外扩
8 倍宽度、垂直方向外扩 2 倍宽度,让鼠标拖出滚动条后仍能继续跟踪(Windows 行为)。水平条对称
(横向 2 倍、纵向 8 倍)。
8.4 尺寸换算:IntScrollGetScrollBarRect / IntScrollGetThumbVal
IntScrollGetScrollBarRect (L484-L599)是 user32 侧与内核 IntGetScrollBarRect +
IntCalculateThumb 对应的几何计算(带 ArrowSize/ThumbSize/ThumbPos 三个输出),逻辑
一致,用于控件 WM_SETFOCUS 时创建 caret。
IntScrollGetThumbVal (L607-L646)做像素 → 逻辑位置 的反向换算,是拖动时计算
SB_THUMBTRACK 位置的依据:
c
Pixels = 滚动条全长 - 2*dxyLineButton; // 可用滑动区
Pixels -= (xyThumbBottom - xyThumbTop); // 再扣掉 thumb 自身
if (Pixels <= 0) return pSBData->posMin;
Pos = Pos - dxyLineButton; // thumb 顶相对滑道起点
if (Pos < 0) Pos = 0; if (Pos > Pixels) Pos = Pixels;
if (!pSBData->page) Pos *= posMax - posMin; // 无 page:线性到 [min,max]
else Pos *= posMax - posMin - page + 1; // 有 page:减掉页长
return pSBData->posMin + ((Pos + Pixels / 2) / Pixels); // 四舍五入
(Pos + Pixels/2) / Pixels 是整除四舍五入。无 page 时位置线性铺满整个范围,有 page 时
最大位置收敛到 posMax - page + 1(与内核 MaxPos 公式一致)。
8.5 键盘导航:IntScrollHandleKbdEvent
IntScrollHandleKbdEvent(L708-L755)把按键翻译成滚动通知发父窗口:
c
switch(wParam) {
case VK_PRIOR: wParam = SB_PAGEUP; break; // PageUp
case VK_NEXT: wParam = SB_PAGEDOWN; break; // PageDown
case VK_HOME: wParam = SB_TOP; break; // Home
case VK_END: wParam = SB_BOTTOM; break; // End
case VK_UP: wParam = SB_LINEUP; break;
case VK_DOWN: wParam = SB_LINEDOWN; break;
default: return;
}
SendMessageW(GetParent(Wnd),
(GetWindowLongPtrW(Wnd, GWL_STYLE) & SBS_VERT) ? WM_VSCROLL : WM_HSCROLL,
wParam, (LPARAM)Wnd);
首次 KEYDOWN 先 HideCaret(Wnd) 防闪烁;WM_KEYUP 时 ShowCaret。lParam & 0x40... 的
PFD_DOUBLEBUFFER_DONTCARE 检查是防止误隐藏 caret 的保护(来自 PaintDC 标志复用)。
9. thumb 拖动跟踪循环
9.1 入口:ScrollTrackScrollBar(模态循环)
ScrollTrackScrollBar(L1155-L1201)是拖动跟踪的模态消息循环,两个入口:
- 控件:
WM_LBUTTONDOWN→ScrollTrackScrollBar(Wnd, SB_CTL, Pt)(L1285)。 - 窗口滚动条:
WM_SYSCOMMAND SC_VSCROLL/SC_HSCROLL→ user32 defwnd.cDefWndTrackScrollBar
(L113-L135)→ScrollTrackScrollBar(Wnd, SB_HORZ/VERT, Pt)(坐标转客户区后)。
c
VOID FASTCALL ScrollTrackScrollBar(HWND Wnd, INT SBType, POINT Pt)
{
if (SBType != SB_CTL) { // 窗口滚动条:屏幕坐标 → 窗口客户区坐标
pwnd = ValidateHwnd(Wnd);
XOffset = pwnd->rcClient.left - pwnd->rcWindow.left;
YOffset = pwnd->rcClient.top - pwnd->rcWindow.top;
ScreenToClient(Wnd, &Pt);
Pt.x += XOffset; Pt.y += YOffset;
}
IntScrollHandleScrollEvent(Wnd, SBType, WM_LBUTTONDOWN, Pt); // 初始化跟踪
do {
if (!GetMessageW(&Msg, 0, 0, 0)) break; // 取消息
if (CallMsgFilterW(&Msg, MSGF_SCROLLBAR)) continue; // 钩子过滤
if (Msg.message == WM_LBUTTONUP || Msg.message == WM_MOUSEMOVE ||
(Msg.message == WM_SYSTIMER && Msg.wParam == SCROLL_TIMER)) {
Pt.x = LOWORD(Msg.lParam) + XOffset; // 坐标还原
Pt.y = HIWORD(Msg.lParam) + YOffset;
IntScrollHandleScrollEvent(Wnd, SBType, Msg.message, Pt);
} else { // 其他消息照常分发
TranslateMessage(&Msg);
DispatchMessageW(&Msg);
}
if (!IsWindow(Wnd)) { ReleaseCapture(); break; } // 窗口没了就退出
} while (Msg.message != WM_LBUTTONUP && GetCapture() == Wnd);
}
要点:
- 循环条件 :
WM_LBUTTONUP或失去捕获才结束。鼠标捕获在IntScrollHandleScrollEvent的
WM_LBUTTONDOWN分支里SetCapture(Wnd),WM_LBUTTONUP分支ReleaseCapture。 - 只处理三类消息 :
WM_LBUTTONUP/WM_MOUSEMOVE/WM_SYSTIMER(SCROLL_TIMER 即重复滚动
计时器),其余消息照常走TranslateMessage/DispatchMessageW------模态但不过滤,保证其他
窗口消息(如 WM_PAINT)不饿死。 MSGF_SCROLLBAR是CallMsgFilter的消息过滤器钩子点,应用可在此拦截滚动消息。- 坐标:lParam 是客户区坐标,非控件时再加 XOffset/YOffset 补回窗口坐标偏移(因为
IntScrollHandleScrollEvent 期望"绘制坐标")。
9.2 IntScrollHandleScrollEvent:事件处理状态机
IntScrollHandleScrollEvent(L763-L1066)是拖动跟踪的核心状态机,处理四类事件
(WM_LBUTTONDOWN/WM_MOUSEMOVE/WM_LBUTTONUP/WM_SYSTIMER)。它维护一组静态跟踪状态:
c
static POINT PrevPt; /* 计时器事件用:上一次鼠标位置 */
static UINT TrackThumbPos; /* 跟踪开始时的 thumb 像素位置 */
static INT LastClickPos; /* 按下点在滚动条内的位置 */
static INT LastMousePos; /* 上一次鼠标在滚动条内的位置 */
WM_LBUTTONDOWN(初始化跟踪):
c
HideCaret(Wnd); // 拖动期间隐藏 caret
ScrollTrackVertical = Vertical;
ScrollTrackHitTest = HitTest = IntScrollHitTest(&ScrollBarInfo, Vertical, Pt, FALSE);
LastClickPos = Vertical ? (Pt.y - rcScrollBar.top) : (Pt.x - rcScrollBar.left);
LastMousePos = LastClickPos;
TrackThumbPos = ScrollBarInfo.xyThumbTop;
PrevPt = Pt;
if (SBType == SB_CTL && (GetWindowLongPtrW(Wnd, GWL_STYLE) & WS_TABSTOP)) SetFocus(Wnd);
SetCapture(Wnd); // 捕获鼠标
if (rgstate[ScrollTrackHitTest] != STATE_SYSTEM_UNAVAILABLE)
IntUpdateScrollArrows(Wnd, Dc, &ScrollBarInfo, &NewInfo, SBType, ScrollTrackHitTest, Vertical, TRUE);
SBS_SIZEGRIP|SBS_SIZEBOX 样式有独立的尺寸柄拖动分支(L799-L823):WM_LBUTTONDOWN 隐藏
caret、SetCapture、置命中 SCROLL_THUMB;WM_MOUSEMOVE 取祖父窗口客户区做矩形;WM_LBUTTONUP
释放捕获;WM_SYSTIMER 用 PrevPt。尺寸柄不产生滚动通知。
按命中码分派的滚动动作(switch ScrollTrackHitTest):
| 命中码 | 按下/计时器时动作 | 移出命中区时 |
|---|---|---|
SCROLL_TOP_ARROW |
SendMessage(WM_VSCROLL, SB_LINEUP) + SetSystemTimer(SCROLL_FIRST_DELAY/SCROLL_REPEAT_DELAY) + 箭头按下状态 |
取消按下 + KillSystemTimer |
SCROLL_TOP_RECT |
SendMessage(WM_VSCROLL, SB_PAGEUP) + 计时器 |
KillSystemTimer |
SCROLL_THUMB |
见 9.3 | --- |
SCROLL_BOTTOM_RECT |
SendMessage(WM_VSCROLL, SB_PAGEDOWN) + 计时器 |
KillSystemTimer |
SCROLL_BOTTOM_ARROW |
SendMessage(WM_VSCROLL, SB_LINEDOWN) + 计时器 + 箭头按下状态 |
取消按下 + KillSystemTimer |
计时器重复(WM_SYSTIMER) :SCROLL_TIMER(ID 0)第一次用 SCROLL_FIRST_DELAY(200ms)
建立(按住后延迟响应),之后每次用 SCROLL_REPEAT_DELAY(50ms)重设,形成按住连续滚动。
SetSystemTimer/KillSystemTimer 是 user32 对内核计时器的封装(timer.c)。鼠标移出命中区
(HitTest != ScrollTrackHitTest)时立即取消按下态并杀计时器------松手滑出滚动条即停止滚动。
9.3 拖动中的连续反馈:SB_THUMBTRACK
SCROLL_THUMB 分支是拖动反馈的核心(L940-L986):
c
case SCROLL_THUMB:
if (Msg == WM_LBUTTONDOWN) {
ScrollTrackingWin = Wnd; ScrollTrackingBar = SBType;
ScrollTrackingPos = TrackThumbPos + LastMousePos - LastClickPos; // 拇指跟着鼠标走
ScrollTrackingVal = IntScrollGetThumbVal(Wnd, SBType, &ScrollBarInfo, Vertical, ScrollTrackingPos);
NewInfo.nTrackPos = ScrollTrackingVal;
NtUserSetScrollBarInfo(Wnd, IntScrollGetObjectId(SBType), &NewInfo); // 写内核 nTrackPos
IntScrollDrawMovingThumb(Dc, &ScrollBarInfo, Vertical); // 画新位置 thumb
}
else if (Msg == WM_LBUTTONUP) {
ScrollTrackingWin = 0; ScrollTrackingVal = 0;
IntDrawScrollInterior(Wnd, Dc, SBType, Vertical, &ScrollBarInfo); // 恢复常规绘制
}
else { /* WM_MOUSEMOVE */
UINT Pos;
if (!IntScrollPtInRectEx(&rcScrollBar, Pt, Vertical)) Pos = LastClickPos; // 拖出边界锁住
else { Pt = IntScrollClipPos(&rcScrollBar, Pt);
Pos = Vertical ? (Pt.y - rcScrollBar.top) : (Pt.x - rcScrollBar.left); }
if (Pos != LastMousePos || !ScrollMovingThumb) { // 位置真的变了才动
LastMousePos = Pos;
ScrollTrackingPos = TrackThumbPos + Pos - LastClickPos;
ScrollTrackingVal = IntScrollGetThumbVal(..., ScrollTrackingPos);
NewInfo.nTrackPos = ScrollTrackingVal;
NtUserSetScrollBarInfo(...); // 同步内核 nTrackPos
IntScrollDrawMovingThumb(Dc, &ScrollBarInfo, Vertical);
SendMessageW(WndOwner, Vertical ? WM_VSCROLL : WM_HSCROLL,
MAKEWPARAM(SB_THUMBTRACK, ScrollTrackingVal), (LPARAM)WndCtl); // ★连续通知
}
}
break;
拖动反馈的三要素:
- 位置换算 :thumb 新像素位置 = 按下时 thumb 位置 + 鼠标位移(
TrackThumbPos + Pos - LastClickPos),再经IntScrollGetThumbVal转成逻辑位置。 - 内核同步 :每次变化
NtUserSetScrollBarInfo写nTrackPos,这样应用在 SB_THUMBTRACK
处理中调用GetScrollInfo能读到一致值。 - 连续通知 :
SendMessage(WM_VSCROLL, MAKEWPARAM(SB_THUMBTRACK, val), ctl)------每个
MOUSEMOVE 若位置变化就同步发通知(SendMessage 是同步的,应用处理完才继续)。应用在通知里
调用SetScrollPos+ScrollWindowEx滚动内容,形成"跟手"。
IntScrollDrawMovingThumb(L274-L301)把 thumb 临时画到新位置:
c
Pos = ScrollTrackingPos; // 像素位置
MaxSize = 滚动条全长 - dxyLineButton - thumb 高; // 边界
if (Pos < dxyLineButton) Pos = dxyLineButton;
else if (MaxSize < Pos) Pos = MaxSize;
OldTop = xyThumbTop;
xyThumbBottom = Pos + (xyThumbBottom - xyThumbTop);
xyThumbTop = Pos;
IntDrawScrollInterior(ScrollTrackingWin, Dc, ScrollTrackingBar, Vertical, ScrollBarInfo);
xyThumbBottom = OldTop + (xyThumbBottom - xyThumbTop); // 恢复原值
xyThumbTop = OldTop;
ScrollMovingThumb = !ScrollMovingThumb; // 翻转"已画"标志
拖动期间反复改 xyThumbTop/Bottom 画完再还原,ScrollMovingThumb 标志用于 WM_MOUSEMOVE
分支的 !ScrollMovingThumb 条件------若鼠标没动过(无新位置),也要重画一次以消除上一次的
"移动 thumb"残影(成对翻转保证奇偶一致)。
9.4 结束:SB_THUMBPOSITION / SB_ENDSCROLL
WM_LBUTTONUP 的收尾(L1045-L1063):
c
if (Msg == WM_LBUTTONUP) {
HitTest = ScrollTrackHitTest;
ScrollTrackHitTest = SCROLL_NOWHERE; /* 终止跟踪 */
if (SCROLL_THUMB == HitTest) {
UINT Val = IntScrollGetThumbVal(..., TrackThumbPos + LastMousePos - LastClickPos);
SendMessageW(WndOwner, ..., MAKEWPARAM(SB_THUMBPOSITION, Val), (LPARAM)WndCtl); // 最终位置
}
/* SB_ENDSCROLL doesn't report thumb position */
SendMessageW(WndOwner, ..., SB_ENDSCROLL, (LPARAM)WndCtl); // 滚动结束
ScrollTrackingWin = 0;
}
而按下后第一帧若命中 thumb 还要立即发一次 SB_THUMBTRACK(L1034-L1043),保证"点中 thumb
还没动"也有初始通知。释放路径顺序:SB_THUMBPOSITION(最终值)→ SB_ENDSCROLL(结束) ,
应用按协议在 SB_ENDSCROLL 里做"放下 thumb 后的收尾"(如重排数据)。
9.5 内核 SBTRACK 与 NEWco_IntGetScrollInfo
用户态的拖动(ScrollTrackingWin/Bar/Pos/Val 静态变量)与内核 SBTRACK 是两套并存的
跟踪状态:
- user32 的静态变量驱动绘制与通知(IntScrollDrawMovingThumb、SendMessage)。
- 内核
SBTRACK(挂THREADINFO.pSBTrack)目前只在NEWco_IntGetScrollInfo里用于
SIF_TRACKPOS的特判(scrollbar.c L346-L349):拖动该窗口该条时返回posNew。
也就是说:应用在拖动中调用 GetScrollInfo 读 nTrackPos 的正确值来自内核 pSBTrack->posNew;
但当前代码路径里 pSBTrack 是否被 user32 填过,取决于拖动实现是否设置它------ReactOS 当前拖动
循环没有写 pSBTrack(它走 NtUserSetScrollBarInfo 同步 nTrackPos),因此
NEWco_IntGetScrollInfo 的分支主要面向未来/兼容 Wine 的服务器端实现。这是代码中"骨架已备、
通路未全"的典型实例,如实记录。
9.6 非客户区滚动条(DefWndTrackScrollBar)
窗口自带滚动条的拖动由 user32 defwnd.c 承接。完整链路:
鼠标在垂直滚动条上按下(命中 HTVSCROLL,nonclient.c WM_NCHITTEST)
→ nonclient.c 产生 WM_NCLBUTTONDOWN
→ 窗口过程 DefWindowProc 处理 WM_NCLBUTTONDOWN(defwnd.c L807 附近)
→ 命中 HTVSCROLL/HTHSCROLL 时发 WM_SYSCOMMAND SC_VSCROLL/SC_HSCROLL
→ DefWndHandleSysCommand(defwnd.c L140)case SC_VSCROLL/SC_HSCROLL(L175-L181)
→ DefWndTrackScrollBar(L113):校验低 4 位命中码 == HTVSCROLL/HTHSCROLL
→ ScrollTrackScrollBar(Wnd, SB_VERT/SB_HORZ, Pt) ← 进入同一模态循环
而内核 nonclient.c 的 WM_NCLBUTTONDOWN 分支(L1629-L1636):
co_IntSendMessage(WM_SYSCOMMAND, SC_HSCROLL + HTHSCROLL, ...)------命中码直接并入系统命令。
注意这条路径发的是 WM_SYSCOMMAND,user32 的 DefWindowProc 才认识;应用自定义处理
WM_NCLBUTTONDOWN 时需自行转发。
10. 滚动条公共 API 封装(user32)
10.1 SetScrollInfo / SetScrollPos / SetScrollRange
SetScrollInfo (L1684-L1710):带 API 钩子(guah)封装,未挂钩时走 RealSetScrollInfo
(L1665-L1676):
c
INT WINAPI RealSetScrollInfo(HWND Wnd, int SBType, LPCSCROLLINFO Info, BOOL bRedraw)
{
if (SB_CTL == SBType)
return SendMessageW(Wnd, SBM_SETSCROLLINFO, (WPARAM)bRedraw, (LPARAM)Info); // 路由到控件
else
return NtUserSetScrollInfo(Wnd, SBType, Info, bRedraw); // 窗口滚动条
}
SB_CTL 走 SBM_SETSCROLLINFO 消息路由(控件窗口过程处理),窗口滚动条直接系统调用。返回
值为当前(或 SIF_PREVIOUSPOS 时的旧)位置。
SetScrollPos (L1718-L1727):构造 {SIF_POS|SIF_PREVIOUSPOS} 的 SCROLLINFO 调
SetScrollInfo。
SetScrollRange(L1735-L1755):
c
pWnd = ValidateHwnd(hWnd);
if (((LONGLONG)nMaxPos - nMinPos) > MAXLONG) { // 范围超限检查
SetLastError(ERROR_INVALID_SCROLLBAR_RANGE);
return FALSE;
}
ScrollInfo.fMask = SIF_RANGE;
SetScrollInfo(hWnd, nBar, &ScrollInfo, bRedraw);
return TRUE;
ERROR_INVALID_SCROLLBAR_RANGE(0x595)是 Win32 为此保留的错误码。注意范围差用
LONGLONG 防止 nMaxPos - nMinPos 整型溢出。
10.2 GetScrollInfo / GetScrollPos / GetScrollRange
RealGetScrollInfo(L1512-L1538):
c
if (SB_CTL == SBType) return SendMessageW(Wnd, SBM_GETSCROLLINFO, 0, (LPARAM)Info);
pWnd = ValidateHwnd(Wnd);
if (SBType < SB_HORZ || SBType > SB_VERT) { SetLastError(ERROR_INVALID_PARAMETER); return FALSE; }
if (!pWnd->pSBInfo) { SetLastError(ERROR_NO_SCROLLBARS); return FALSE; } // 未创建过滚动条
pSBData = IntGetSBData(pWnd, SBType);
return NtUserSBGetParms(Wnd, SBType, pSBData, Info);
ERROR_NO_SCROLLBARS(0x596)也是专用错误码:窗口从未有滚动条信息时(pSBInfo == NULL,
懒创建未触发)查询失败。
GetScrollPos (L1593-L1622)与 GetScrollRange (L1630-L1663)是 Win3.1 时代的快速 API,
不走系统调用 :ValidateHwnd 后直接读桌面堆里的 pSBData->pos / posMin/posMax
(user32 能读因为桌面堆映射到用户态),SB_CTL 形态则消息路由到 SBM_GETPOS/
SBM_GETRANGE。这就是"数据双通道"设计的受益者------极快的滚轮/箭头滚动路径。
10.3 EnableScrollBar / GetScrollBarInfo
EnableScrollBar (L1478-L1510):NtUserEnableScrollBar 成功后调用
SCROLL_RefreshScrollBar(hwnd, nBar, TRUE, TRUE)(L691-L700:GetDCEx + IntDrawScrollBar +
ReleaseDC)重绘整条。与内核侧注释 "Done in user32" 呼应。
GetScrollBarInfo (L1543-L1553):调 NtUserGetScrollBarInfo 后把 rcScrollBar 平移窗口
原点转屏幕坐标(可访问性工具需要)。
10.4 API 钩子机制(guah)
SetScrollInfo/GetScrollInfo/EnableScrollBar 都走 LoadUserApiHook() +
BeginIfHookedUserApiHook() 模式:若应用挂钩了 USER API(SetWindowsHookEx 的 WH_CALLWNDPROC
之类间接挂钩内部 API,或 guah 表),走 guah.SetScrollInfo 等代理;否则直接进内核。这是
ReactOS user32 的统一 API 钩子框架(guah 全局表),保证滚动条 API 与其余 USER API 行为
一致。
11. 调用链与子系统关系
11.1 SetScrollInfo 全链路(mermaid)
winpos.c / painting.c win32k (scrollbar.c) user32 (controls/scrollbar.c) 应用程序 winpos.c / painting.c win32k (scrollbar.c) user32 (controls/scrollbar.c) 应用程序 #mermaid-svg-E9FPSy9qkGRsvReP{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-E9FPSy9qkGRsvReP .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-E9FPSy9qkGRsvReP .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-E9FPSy9qkGRsvReP .error-icon{fill:#552222;}#mermaid-svg-E9FPSy9qkGRsvReP .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-E9FPSy9qkGRsvReP .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-E9FPSy9qkGRsvReP .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-E9FPSy9qkGRsvReP .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-E9FPSy9qkGRsvReP .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-E9FPSy9qkGRsvReP .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-E9FPSy9qkGRsvReP .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-E9FPSy9qkGRsvReP .marker{fill:#333333;stroke:#333333;}#mermaid-svg-E9FPSy9qkGRsvReP .marker.cross{stroke:#333333;}#mermaid-svg-E9FPSy9qkGRsvReP svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-E9FPSy9qkGRsvReP p{margin:0;}#mermaid-svg-E9FPSy9qkGRsvReP .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-E9FPSy9qkGRsvReP text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-E9FPSy9qkGRsvReP .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-E9FPSy9qkGRsvReP .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-E9FPSy9qkGRsvReP .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-E9FPSy9qkGRsvReP .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-E9FPSy9qkGRsvReP #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-E9FPSy9qkGRsvReP .sequenceNumber{fill:white;}#mermaid-svg-E9FPSy9qkGRsvReP #sequencenumber{fill:#333;}#mermaid-svg-E9FPSy9qkGRsvReP #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-E9FPSy9qkGRsvReP .messageText{fill:#333;stroke:none;}#mermaid-svg-E9FPSy9qkGRsvReP .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-E9FPSy9qkGRsvReP .labelText,#mermaid-svg-E9FPSy9qkGRsvReP .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-E9FPSy9qkGRsvReP .loopText,#mermaid-svg-E9FPSy9qkGRsvReP .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-E9FPSy9qkGRsvReP .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-E9FPSy9qkGRsvReP .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-E9FPSy9qkGRsvReP .noteText,#mermaid-svg-E9FPSy9qkGRsvReP .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-E9FPSy9qkGRsvReP .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-E9FPSy9qkGRsvReP .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-E9FPSy9qkGRsvReP .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-E9FPSy9qkGRsvReP .actorPopupMenu{position:absolute;}#mermaid-svg-E9FPSy9qkGRsvReP .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-E9FPSy9qkGRsvReP .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-E9FPSy9qkGRsvReP .actor-man circle,#mermaid-svg-E9FPSy9qkGRsvReP line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-E9FPSy9qkGRsvReP :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} alt需要 SHOW/HIDE alt非主题箭头变化/主题 altbRedraw 且可见 SetScrollInfo(hwnd, SB_VERT, &si, TRUE)NtUserSetScrollInfoco_IntCreateScrollBars(懒创建 SBINFO/SBINFOEX)co_IntSetScrollInfo:落数据/夹取/显隐决策co_UserShowScrollBar → IntSetStyle + SWP_FRAMECHANGEDco_WinPosSetWindowPos 重算客户区IntRefeshScrollInterior(只重画滑道+thumb)co_UserRedrawWindow(RDW_INVALIDATE|RDW_FRAME)返回当前位置返回
11.2 拖动跟踪链路(mermaid)
应用窗口过程 win32k user32 (controls/scrollbar.c) 窗口过程/DefWindowProc 鼠标输入 (mouse.c) 应用窗口过程 win32k user32 (controls/scrollbar.c) 窗口过程/DefWindowProc 鼠标输入 (mouse.c) #mermaid-svg-SP56KGf1xwCwVzWr{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-SP56KGf1xwCwVzWr .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-SP56KGf1xwCwVzWr .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-SP56KGf1xwCwVzWr .error-icon{fill:#552222;}#mermaid-svg-SP56KGf1xwCwVzWr .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-SP56KGf1xwCwVzWr .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-SP56KGf1xwCwVzWr .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-SP56KGf1xwCwVzWr .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-SP56KGf1xwCwVzWr .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-SP56KGf1xwCwVzWr .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-SP56KGf1xwCwVzWr .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-SP56KGf1xwCwVzWr .marker{fill:#333333;stroke:#333333;}#mermaid-svg-SP56KGf1xwCwVzWr .marker.cross{stroke:#333333;}#mermaid-svg-SP56KGf1xwCwVzWr svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-SP56KGf1xwCwVzWr p{margin:0;}#mermaid-svg-SP56KGf1xwCwVzWr .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-SP56KGf1xwCwVzWr text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-SP56KGf1xwCwVzWr .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-SP56KGf1xwCwVzWr .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-SP56KGf1xwCwVzWr .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-SP56KGf1xwCwVzWr .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-SP56KGf1xwCwVzWr #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-SP56KGf1xwCwVzWr .sequenceNumber{fill:white;}#mermaid-svg-SP56KGf1xwCwVzWr #sequencenumber{fill:#333;}#mermaid-svg-SP56KGf1xwCwVzWr #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-SP56KGf1xwCwVzWr .messageText{fill:#333;stroke:none;}#mermaid-svg-SP56KGf1xwCwVzWr .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-SP56KGf1xwCwVzWr .labelText,#mermaid-svg-SP56KGf1xwCwVzWr .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-SP56KGf1xwCwVzWr .loopText,#mermaid-svg-SP56KGf1xwCwVzWr .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-SP56KGf1xwCwVzWr .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-SP56KGf1xwCwVzWr .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-SP56KGf1xwCwVzWr .noteText,#mermaid-svg-SP56KGf1xwCwVzWr .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-SP56KGf1xwCwVzWr .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-SP56KGf1xwCwVzWr .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-SP56KGf1xwCwVzWr .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-SP56KGf1xwCwVzWr .actorPopupMenu{position:absolute;}#mermaid-svg-SP56KGf1xwCwVzWr .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-SP56KGf1xwCwVzWr .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-SP56KGf1xwCwVzWr .actor-man circle,#mermaid-svg-SP56KGf1xwCwVzWr line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-SP56KGf1xwCwVzWr :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} loop每次 WM_MOUSEMOVE(位置变化) WM_NCLBUTTONDOWN(HTVSCROLL) 或控件 WM_LBUTTONDOWNWM_SYSCOMMAND SC_VSCROLL → DefWndTrackScrollBarScrollTrackScrollBar:SetCapture + 模态 GetMessage 循环NtUserSetScrollBarInfo(nTrackPos)SendMessage(WM_VSCROLL, SB_THUMBTRACK, pos)SetScrollPos + ScrollWindowEx(内容滚动)SendMessage(WM_VSCROLL, SB_THUMBPOSITION, pos)SendMessage(WM_VSCROLL, SB_ENDSCROLL)ReleaseCapture,循环结束
11.3 绘制链路(mermaid)
#mermaid-svg-tx1ZOuhI1kcFKJOy{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-tx1ZOuhI1kcFKJOy .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-tx1ZOuhI1kcFKJOy .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-tx1ZOuhI1kcFKJOy .error-icon{fill:#552222;}#mermaid-svg-tx1ZOuhI1kcFKJOy .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-tx1ZOuhI1kcFKJOy .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-tx1ZOuhI1kcFKJOy .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-tx1ZOuhI1kcFKJOy .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-tx1ZOuhI1kcFKJOy .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-tx1ZOuhI1kcFKJOy .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-tx1ZOuhI1kcFKJOy .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-tx1ZOuhI1kcFKJOy .marker{fill:#333333;stroke:#333333;}#mermaid-svg-tx1ZOuhI1kcFKJOy .marker.cross{stroke:#333333;}#mermaid-svg-tx1ZOuhI1kcFKJOy svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-tx1ZOuhI1kcFKJOy p{margin:0;}#mermaid-svg-tx1ZOuhI1kcFKJOy .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-tx1ZOuhI1kcFKJOy .cluster-label text{fill:#333;}#mermaid-svg-tx1ZOuhI1kcFKJOy .cluster-label span{color:#333;}#mermaid-svg-tx1ZOuhI1kcFKJOy .cluster-label span p{background-color:transparent;}#mermaid-svg-tx1ZOuhI1kcFKJOy .label text,#mermaid-svg-tx1ZOuhI1kcFKJOy span{fill:#333;color:#333;}#mermaid-svg-tx1ZOuhI1kcFKJOy .node rect,#mermaid-svg-tx1ZOuhI1kcFKJOy .node circle,#mermaid-svg-tx1ZOuhI1kcFKJOy .node ellipse,#mermaid-svg-tx1ZOuhI1kcFKJOy .node polygon,#mermaid-svg-tx1ZOuhI1kcFKJOy .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-tx1ZOuhI1kcFKJOy .rough-node .label text,#mermaid-svg-tx1ZOuhI1kcFKJOy .node .label text,#mermaid-svg-tx1ZOuhI1kcFKJOy .image-shape .label,#mermaid-svg-tx1ZOuhI1kcFKJOy .icon-shape .label{text-anchor:middle;}#mermaid-svg-tx1ZOuhI1kcFKJOy .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-tx1ZOuhI1kcFKJOy .rough-node .label,#mermaid-svg-tx1ZOuhI1kcFKJOy .node .label,#mermaid-svg-tx1ZOuhI1kcFKJOy .image-shape .label,#mermaid-svg-tx1ZOuhI1kcFKJOy .icon-shape .label{text-align:center;}#mermaid-svg-tx1ZOuhI1kcFKJOy .node.clickable{cursor:pointer;}#mermaid-svg-tx1ZOuhI1kcFKJOy .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-tx1ZOuhI1kcFKJOy .arrowheadPath{fill:#333333;}#mermaid-svg-tx1ZOuhI1kcFKJOy .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-tx1ZOuhI1kcFKJOy .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-tx1ZOuhI1kcFKJOy .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-tx1ZOuhI1kcFKJOy .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-tx1ZOuhI1kcFKJOy .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-tx1ZOuhI1kcFKJOy .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-tx1ZOuhI1kcFKJOy .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-tx1ZOuhI1kcFKJOy .cluster text{fill:#333;}#mermaid-svg-tx1ZOuhI1kcFKJOy .cluster span{color:#333;}#mermaid-svg-tx1ZOuhI1kcFKJOy div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-tx1ZOuhI1kcFKJOy .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-tx1ZOuhI1kcFKJOy rect.text{fill:none;stroke-width:0;}#mermaid-svg-tx1ZOuhI1kcFKJOy .icon-shape,#mermaid-svg-tx1ZOuhI1kcFKJOy .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-tx1ZOuhI1kcFKJOy .icon-shape p,#mermaid-svg-tx1ZOuhI1kcFKJOy .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-tx1ZOuhI1kcFKJOy .icon-shape .label rect,#mermaid-svg-tx1ZOuhI1kcFKJOy .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-tx1ZOuhI1kcFKJOy .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-tx1ZOuhI1kcFKJOy .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-tx1ZOuhI1kcFKJOy :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
WM_NCPAINT
nonclient.c NC_HandleNCPaint
IntIsScrollBarVisible?
IntDrawScrollBar pWnd hDC SB_VERT/SB_HORZ
co_IntGetScrollBarInfo: 几何+rgstate
IntDrawScrollArrows: DrawFrameControl DFC_SCROLL
IntDrawScrollInterior: NtGdiPatBlt 滑道 + DrawEdge thumb
控件 WM_PAINT
user32 IntDrawScrollBar SB_CTL
IntGetScrollBarInfo → NtUserGetScrollBarInfo
IntDrawScrollArrows + IntDrawScrollInterior 用户态版
11.4 与 defwnd 的关系
- user32 defwnd.c :
DefWndHandleSysCommand的SC_VSCROLL/SC_HSCROLL分支(L175-L181)
把"非客户区滚动条上的鼠标按下"转成DefWndTrackScrollBar→ScrollTrackScrollBar,是窗口
滚动条拖动的唯一入口。 - 内核 defwnd.c(ntuser) :
IntDefWindowProc不直接处理滚动条;WM_VSCROLL/WM_HSCROLL
默认返回 0(应用不处理则无事发生)。滚动条显隐后的客户区变化由 winpos 的
SWP_FRAMECHANGED路径在WM_NCCALCSIZE中处理(NC_HandleNCCalcSize),defwnd 只负责
把WM_NCCALCSIZE结果应用到rcClient。任务说明中"WM_NCCREATE 初始化 SBINFO"的提法在
当前源码中不成立------SBINFO 由co_IntCreateScrollBars延迟创建 (首个 Set/Get/绘制入口),
窗口创建时的滚动条初始化是 window.c L2561-L2569 调co_UserShowScrollBar(它最终也会触发
co_IntCreateScrollBars)。此处按真实源码澄清。
11.5 与 nonclient / draw 的关系
- nonclient.c :
WM_NCPAINT(L1302)在画边框后按WS_VSCROLL/WS_HSCROLL与
IntIsScrollBarVisible(L790-L799)判定,调IntDrawScrollBar(L1289-L1301);两个滚动条
并存时先画垂直再画水平,右下角画尺寸柄(DFCS_SCROLLSIZEGRIP,L1285)。WM_NCHITTEST
(L2155-L2177)判定HTVSCROLL/HTHSCROLL/HTSIZE(右下角尺寸柄),是拖动入口的命中
基础。 - draw.c :
DrawFrameControl(DFC_SCROLL 系列)与DrawEdge(EDGE_RAISED)是箭头与 thumb
立体效果的实际绘制者(GDI 直调封装),DefWndControlColor提供默认滚动条颜色。
11.6 与 winpos / window 的关系
- winpos.c :
co_UserShowScrollBar改样式后必须SWP_FRAMECHANGED走
co_WinPosSetWindowPos重算客户区;co_IntCreateScrollBars创建时也调
co_WinPosGetNonClientSize同步 rcClient。 - window.c :窗口创建完成(L2561-L2569)按样式初始化滚动条;
co_UserFreeWindow/销毁路径(L746)调IntDestroyScrollBars释放两处内存。
11.7 与 mouse / 队列的关系
- mouse.c :命中
HTVSCROLL等非客户区时产生WM_NCLBUTTONDOWN,滚动条拖动期间的鼠标
消息被SetCapture重定向到滚动条窗口(队列spwndCapture),ScrollTrackScrollBar的
循环条件GetCapture() == Wnd正是基于捕获。 - 队列 :
USER_MESSAGE_QUEUE.CaretInfo供co_IntFixCaret隐藏/重放插入符;
THREADINFO.pSBTrack挂内核跟踪上下文;SetSystemTimer(timer.c)驱动按住重复滚动。
12. 源码索引
12.1 文件清单
| 文件 | 角色 |
|---|---|
win32ss/user/ntuser/scrollbar.c |
内核滚动条:信息管理、绘制、NtUser 系统调用、内核控件窗口过程 |
win32ss/user/ntuser/scrollex.c |
内核区域滚动:ScrollWindowEx/ScrollWindow/ScrollDC 实现 |
win32ss/user/ntuser/scroll.h |
SBTRACK、SBINFOEX 访问宏、SBID 宏、内部函数声明 |
win32ss/include/ntuser.h |
SBDATA/SBINFO/SBCALC/SBINFOEX/SBWND 定义,WND.pSBInfo/pSBInfoex |
win32ss/user/user32/controls/scrollbar.c |
用户态滚动条控件:窗口过程、拖动循环、公共 API |
win32ss/user/user32/windows/defwnd.c |
DefWndTrackScrollBar:SC_VSCROLL/SC_HSCROLL 入口(L113/L175) |
win32ss/user/ntuser/nonclient.c |
WM_NCPAINT 滚动条绘制、WM_NCHITTEST、IntIsScrollBarVisible(L790) |
win32ss/user/ntuser/window.c |
窗口创建时滚动条初始化(L2561)、销毁(L746) |
sdk/include/psdk/winuser.h |
SCROLLINFO/SCROLLBARINFO/SB_/SIF_ /ESB_/SW_ 常量 |
12.2 函数索引(scrollbar.c,行号)
| 函数 | 行号 | 说明 |
|---|---|---|
mirror_rect |
L68 | RTL 布局镜像 |
IntGetSBData |
L76 | SB_* → PSBDATA(SB_CTL 用 SBCALC 前缀) |
IntGetScrollBarRect |
L103 | 计算 rcScrollBar(窗口坐标/客户区坐标) |
IntCalculateThumb |
L165 | thumb 几何计算(EngMulDiv 比例) |
co_IntGetScrollInfo |
L263 | GetScrollInfo 内核实现 |
NEWco_IntGetScrollInfo |
L310 | 带 SBTRACK 跟踪感知的版本 |
co_IntSetScrollInfo |
L454 | SetScrollInfo 内核状态机(核心) |
co_IntGetScrollBarInfo |
L670 | 填充 SCROLLBARINFO + rgstate |
co_IntSetScrollBarInfo |
L724 | 回写 nTrackPos/rgstate |
co_IntCreateScrollBars |
L757 | 懒创建 SBINFO/SBINFOEX |
IntDestroyScrollBars |
L809 | 释放滚动条内存 |
IntEnableScrollBar |
L823 | 更新箭头 rgstate |
co_UserShowScrollBar |
L862 | 显隐 + FRAMECHANGED |
IntDrawScrollInterior |
L907 | 滑道 + thumb 绘制 |
IntDrawScrollArrows |
L1019 | 箭头绘制(DFC_SCROLL) |
IntScrollGetObjectId |
L1057 | SB_* → OBJID_* |
IntRefeshScrollInterior |
L1067 | 只刷新内部 |
IntDrawScrollBar |
L1082 | 绘制总入口 |
ScrollBarWndProc |
L1133 | 内核侧控件过程(WM_ENABLE) |
NtUserGetScrollBarInfo |
L1151 | 系统调用 |
NtUserSBGetParms |
L1191 | GetScrollInfo 系统调用(含 SBDATA 输出) |
NtUserEnableScrollBar |
L1251 | 系统调用 |
NtUserSetScrollInfo |
L1340 | 系统调用 |
NtUserShowScrollBar |
L1380 | 系统调用 |
NtUserSetScrollBarInfo |
L1406 | 系统调用 |
12.3 函数索引(scrollex.c / user32 controls/scrollbar.c)
scrollex.c:
| 函数 | 行号 | 说明 |
|---|---|---|
co_IntFixCaret |
L13 | 滚动中的插入符隐藏/重放 |
co_IntGetUpdateRgn |
L62 | 旧式 GetUpdateRgn(滚动损伤合并) |
UserScrollDC |
L98 | 位图移动 + 暴露区区域计算 |
IntScrollWindowEx |
L226 | 区域滚动主函数 |
IntScrollWindow |
L457 | ScrollWindow 内核实现 |
NtUserScrollDC |
L474 | ScrollDC 系统调用 |
NtUserScrollWindowEx |
L570 | ScrollWindowEx 系统调用 |
user32 controls/scrollbar.c:
| 函数 | 行号 | 说明 |
|---|---|---|
SCROLL_builtin_class |
L76 | "ScrollBar" 系统类描述符 |
IntGetSBData |
L89 | 用户态 SB_* → PSBDATA |
IntDrawScrollInterior |
L116 | 滑道 + thumb(用户态版) |
IntDrawScrollArrows |
L231 | 箭头(用户态版) |
IntScrollDrawMovingThumb |
L274 | 拖动 thumb 绘制 |
IntScrollGetObjectId |
L303 | SB_* → OBJID_* |
IntGetScrollBarInfo |
L313 | NtUserGetScrollBarInfo 封装 |
IntUpdateScrollArrows |
L321 | 更新箭头按下状态并落盘 |
IntDrawScrollBar |
L341 | 控件绘制总入口 |
IntScrollPtInRectEx |
L410 | 拖动外扩命中矩形 |
IntScrollHitTest |
L438 | 命中测试 |
IntScrollGetScrollBarRect |
L484 | 控件几何计算(caret 用) |
IntScrollGetThumbVal |
L607 | 像素 → 逻辑位置 |
IntScrollClipPos |
L651 | 坐标夹取 |
IntScrollDrawSizeGrip |
L673 | 尺寸柄绘制 |
SCROLL_RefreshScrollBar |
L691 | 整条重绘 |
IntScrollHandleKbdEvent |
L708 | 键盘导航 |
IntScrollHandleScrollEvent |
L763 | 拖动事件状态机(核心) |
IntScrollCreateScrollBar |
L1074 | WM_CREATE 初始化 + 对齐 |
ScrollTrackScrollBar |
L1155 | 模态拖动循环 |
IntSetScrollInfo |
L1204 | 控件 SetScrollInfo + 事件通知 |
ScrollBarWndProc_common |
L1216 | 控件窗口过程 |
EnableScrollBar |
L1478 | 公共 API |
RealGetScrollInfo |
L1512 | GetScrollInfo 实际实现 |
GetScrollBarInfo |
L1543 | 公共 API(屏幕坐标修正) |
GetScrollInfo |
L1561 | 公共 API |
GetScrollPos |
L1593 | 快速读位置(免系统调用) |
GetScrollRange |
L1630 | 快速读范围 |
RealSetScrollInfo |
L1665 | SetScrollInfo 实际实现 |
SetScrollInfo |
L1684 | 公共 API |
SetScrollPos |
L1718 | 公共 API |
SetScrollRange |
L1735 | 公共 API |
12.4 关键行号速查
| 位置 | 内容 |
|---|---|
scrollbar.c L37-L52 |
SA_SSI_* 动作标志、SBRG_*、CHANGERGSTATE |
scrollbar.c L454-L668 |
co_IntSetScrollInfo(显隐自动决策) |
scrollbar.c L757-L807 |
co_IntCreateScrollBars(懒创建) |
scrollbar.c L862-L905 |
co_UserShowScrollBar(SWP_FRAMECHANGED) |
scrollbar.c L907-L1017 |
IntDrawScrollInterior |
scrollex.c L98-L224 |
UserScrollDC(暴露区算法) |
scrollex.c L226-L454 |
IntScrollWindowEx |
controls/scrollbar.c L763-L1066 |
IntScrollHandleScrollEvent(拖动状态机) |
controls/scrollbar.c L1155-L1201 |
ScrollTrackScrollBar(模态循环) |
controls/scrollbar.c L1216-L1458 |
ScrollBarWndProc_common |
ntuser.h L503-L536 |
SBDATA/SBINFO/SBCALC |
ntuser.h L598-L602 |
SBINFOEX |
ntuser.h L762-L777 |
WND.pSBInfoex / SBWND |
scroll.h L5-L24 |
SBTRACK |
scroll.h L33-L40 |
IntGetScrollbarInfoFromWindow 等宏 |
nonclient.c L790-L799 |
IntIsScrollBarVisible |
nonclient.c L1289-L1301 |
WM_NCPAINT 调 IntDrawScrollBar |
nonclient.c L1629-L1636 |
WM_NCLBUTTONDOWN → SC_VSCROLL/SC_HSCROLL |
user32 defwnd.c L113-L135 / L175-L181 |
DefWndTrackScrollBar 入口 |
window.c L2561-L2569 |
创建时初始化滚动条 |
13. 小结
滚动条子系统展现了 ReactOS 窗口管理"三层交织"的典型形态:
- 数据层 :
SBINFO(桌面堆,用户态可直读)与SBINFOEX(分页池,权威状态)双通道,
SBCALC以结构前缀复用SBDATA,SBTRACK为内核跟踪预留骨架。 - 交互层 :user32 的
ScrollTrackScrollBar模态循环 +IntScrollHandleScrollEvent
状态机承担全部鼠标/键盘/计时器交互,SB_THUMBTRACK连续通知 +SB_THUMBPOSITION/
SB_ENDSCROLL收尾形成完整拖动协议;按住箭头/滑道由 200ms/50ms 系统计时器驱动重复滚动。 - 绘制与内容层 :
IntDrawScrollBar族负责箭头(DFC_SCROLL)/滑道(PatBlt)/thumb
(DrawEdge)绘制;scrollex.c的IntScrollWindowEx+UserScrollDC以"BitBlt 移动 +
暴露区区域运算 + 子窗口平移"完成内容滚动,与应用在 WM_VSCROLL 中调用 ScrollWindowEx
的协议闭环。
值得记住的几个"反直觉"事实:
- GetScrollPos/GetScrollRange 不走系统调用 (直接读桌面堆);只有 Set 侧和
GetScrollInfo/GetScrollBarInfo 过内核。 - 拖动循环在用户态 ,内核
SBTRACK目前只是半成品骨架。 - scrollex.c 与"滚动条控件"无关,它只做窗口内容区域滚动。
- 窗口滚动条自动隐藏 由
co_IntSetScrollInfo的内容/页面比较驱动,SIF_DISABLENOSCROLL
可改为禁用箭头而非隐藏。 - 滚动条显隐会改变客户区尺寸,因此必须走
SWP_FRAMECHANGED重算------这是它与 winpos.c
最深的耦合点。
本文档基于 ReactOS 源代码(2026 年 8 月)撰写。滚动条相关代码有大量 "Ported from
WINE20020904" 的历史印记,且内核/用户态各保留一份绘制实现,阅读时注意对照。