ReactOS 窗口系统分析(13):滚动条 — scrollbar.c + scrollex.c

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 中有两种形态的滚动条:

  1. 窗口自带滚动条 (窗口样式 WS_HSCROLL / WS_VSCROLL):滚动条位于窗口的非客户区
    (边框内侧),由系统WM_NCPAINT 时统一绘制,属于窗口的一部分,不占用客户区。
  2. 滚动条控件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_VSCROLLwParam 低字是 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 pSBInfoSBINFO 结构(WSBflags + SBDATA Horz/Vert),放在桌面堆
    DesktopHeapAlloc),同时映射到用户态(user32 可用 DesktopPtrToUser 直接读),供
    GetScrollPos/GetScrollRange 这类"快速查询"使用,避免每次查询都过系统调用。
  • PSBINFOEX pSBInfoexSBINFOEX[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_VSCROLLwParam 低字 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)真正使用它:当线程正在跟踪某窗口的

某条滚动条时,GetScrollInfoSIF_TRACKPOS 返回 pSBTrack->posNew 而不是静态的 pos

从而让应用在拖动中查询到"候选位置"。xxxpfnSB 回调、hTimerSB 计时器等字段是向"内核接管

拖动"方向预留的骨架,实际拖动循环仍由 user32 的模态循环驱动。

3.6 SB_* 命令码(psdk/winuser.h L552-L578)

滚动条通知 WM_HSCROLL/WM_VSCROLLwParam 低字取值:

命令码 触发场景 含义
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_HORZrcClient.bottom 往下延伸 SM_CYHSCROLL 高;有边框时左右各扩 1 像素。
  • SB_VERTrcClient.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/xyThumbBottompsbi->dxyLineButton。算法(以垂直为例,水平对称):

  1. Thumb = SM_CXVSCROLL(箭头尺寸);cxy = 滚动条全长(像素)。
  2. 空间不足cxy <= 2*Thumb):Thumb = cxy/2,thumb 尺寸为 0(不显示)。
  3. 无可滚动内容 (两个箭头都 STATE_SYSTEM_UNAVAILABLE
    posMin >= posMax - max(page-1, 0)):thumb 尺寸为 0。
  4. 否则:
    • 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) 是最大有效
      位置。
  5. 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 的内核实现。步骤:

  1. 去掉 SIF_THEMED 位(内部实现细节不外泄)。
  2. 校验 nBarSBID_IS_VALID)与 pSBInfo 存在。
  3. fMask == SIF_ALL 时按 SIF_PAGE|SIF_POS|SIF_RANGE|SIF_TRACKPOS 全取,否则按调用者的掩码。
  4. psi = IntGetScrollInfoFromWindow(Window, nBar)(即 SBINFOEX 里的 SCROLLINFO)拷字段。

NEWco_IntGetScrollInfo (L310-L352):带拖动跟踪感知的版本。读取 pSBTrack(线程的

SBTRACK),当 pSBTrack->nBar == nBar && pSBTrack->spwndTrack == pWnd(本线程正在拖动该窗口的

这条滚动条)时,SIF_TRACKPOS 返回 pSBTrack->posNew(候选位置),否则返回静态 pos

这实现了 GetScrollInfoSB_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。步骤:

  1. Bar = SBOBJ_TO_SBID(idObject)(OBJID_* → SB_*),校验合法性。
  2. co_IntCreateScrollBars 确保存在。
  3. IntGetScrollBarRect + IntCalculateThumb 刷新几何(每次查询都重算,保证最新)。
  4. 计算 rgstate[0](整条滚动条):
    • 样式没带 WS_HSCROLL/WS_VSCROLLSTATE_SYSTEM_INVISIBLE
    • posMin >= posMax - max(page-1,0)(无可滚动内容)→ STATE_SYSTEM_UNAVAILABLE(可见时)
      STATE_SYSTEM_OFFSCREEN(不可见时);
    • 控件被禁用(WS_DISABLED)→ STATE_SYSTEM_UNAVAILABLE
  5. 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_FRAMECHANGEDco_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,是绘制细节最密的一段:

  1. 颜色SB_CTLGetControlBrush(pWnd, hDC, WM_CTLCOLORSCROLLBAR)(父窗口可自定义
    颜色,失败退回 COLOR_SCROLLBAR 系统色);窗口滚动条走 DefWndControlColor(hDC, CTLCOLOR_SCROLLBAR)hSaveBrush = NtGdiSelectBrush(hDC, hBrush) 换入画刷。
  2. 滑道矩形 :垂直时 Rect = 滚动条去掉上下箭头(top + dxyLineButton
    bottom - dxyLineButton)的中段;水平同理。
  3. 无 thumb!xyThumbBottom):整段 PATCOPY 平涂,直接返回。
  4. 有 thumb
    • ThumbTop -= dxyLineButton(把 thumb 顶转成相对滑道的偏移)。
    • 上滑道 NtGdiPatBlt(..., ThumbTop, TopSelected ? BLACKNESS : PATCOPY):按下时用
      BLACKNESS 画黑(凹陷),否则平涂。
    • 下滑道类似,从 ThumbTop + ThumbSize 画到滑道底。
    • DrawEdge(hDC, &Rect, EDGE_RAISED, BF_RECT | BF_MIDDLE):thumb 画凸起立体边框。
  5. 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_VSCROLLSB_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;                            // 返回当前(或旧)位置
}

细节:

  • 拒绝桌面窗口与消息窗口(它们不允许有滚动条语义)。
  • 从调用者拷贝时有意少拷 nTrackPossizeof - 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_UserShowScrollBarSB_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)

GetMmCopyFromCallercbSize 校验 → UserGetWindowObjectco_IntGetScrollBarInfo

MmCopyToCaller 回写。user32 的 GetScrollBarInfo(controls/scrollbar.c L1543)调用后还要

rcScrollBar 加上窗口原点转成屏幕坐标(内核返回的是窗口坐标)。

SetSBOBJ_TO_SBID 校验 → co_IntCreateScrollBars → 拷贝 SETSCROLLBARINFO(即

{ nTrackPos; reserved; rgstate[6] })→ 写入 psi->nTrackPossbi->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 做四件事:

  1. 移动位图UserScrollDCNtGdiBitBlt 把源矩形(rcSrc)拷到目标矩形
    rcDst = rcSrc + (dx,dy),并与裁剪区相交)。
  2. 计算暴露区 :移动后未被覆盖的矩形带(rcDstrcSrc 的差)加入更新区域。
  3. 重绘co_UserRedrawWindow 用更新区域触发 WM_PAINT(可带 SW_ERASE 擦背景)。
  4. 移动子窗口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);

NtGdiBitBltSRCCOPY 把"目标位置偏移 -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(与内核 IntGetSBDatacbwndExtra 校验呼应)。

CS_PARENTDC 使控件直接用父窗口 DC 省内存;CS_VREDRAW|CS_HREDRAW 保证尺寸变化即重绘。

8.2 ScrollBarWndProc_common:控件窗口过程

ScrollBarWndProc_common(L1216-L1458)是 A/W 共用的控件窗口过程,消息处理全景:

消息 处理
WM_CREATE IntScrollCreateScrollBarNtUserSetScrollInfo(SB_CTL, range[0,0], FALSE) 初始化 + 按 SBS_*ALIGN 样式把窗口 MoveWindow 到对齐位置
WM_ENABLE 转发 SBM_ENABLE_ARROWSwParam ? 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 像素宽位图 + 尺寸)、SetCaretPosShowCaret
WM_KILLFOCUS 隐藏 caret、InvalidateRect thumb 区域、DestroyCaret
WM_ERASEBKGND 返回 1(不擦,全部由 WM_PAINT 画)
WM_GETDLGCODE 返回 DLGC_WANTARROWS(方向键留给控件)
WM_PAINT SBS_SIZEGRIPIntScrollDrawSizeGripSBS_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_CREATENtUserSetWindowFNID(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_KEYUPShowCaretlParam & 0x40...

PFD_DOUBLEBUFFER_DONTCARE 检查是防止误隐藏 caret 的保护(来自 PaintDC 标志复用)。


9. thumb 拖动跟踪循环

9.1 入口:ScrollTrackScrollBar(模态循环)

ScrollTrackScrollBar(L1155-L1201)是拖动跟踪的模态消息循环,两个入口:

  • 控件:WM_LBUTTONDOWNScrollTrackScrollBar(Wnd, SB_CTL, Pt)(L1285)。
  • 窗口滚动条:WM_SYSCOMMAND SC_VSCROLL/SC_HSCROLL → user32 defwnd.c DefWndTrackScrollBar
    (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_SCROLLBARCallMsgFilter 的消息过滤器钩子点,应用可在此拦截滚动消息。
  • 坐标: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;

拖动反馈的三要素:

  1. 位置换算 :thumb 新像素位置 = 按下时 thumb 位置 + 鼠标位移(TrackThumbPos + Pos - LastClickPos),再经 IntScrollGetThumbVal 转成逻辑位置。
  2. 内核同步 :每次变化 NtUserSetScrollBarInfonTrackPos,这样应用在 SB_THUMBTRACK
    处理中调用 GetScrollInfo 能读到一致值。
  3. 连续通知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_CTLSBM_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.cDefWndHandleSysCommandSC_VSCROLL/SC_HSCROLL 分支(L175-L181)
    把"非客户区滚动条上的鼠标按下"转成 DefWndTrackScrollBarScrollTrackScrollBar,是窗口
    滚动条拖动的唯一入口。
  • 内核 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.cWM_NCPAINT(L1302)在画边框后按 WS_VSCROLL/WS_HSCROLL
    IntIsScrollBarVisible(L790-L799)判定,调 IntDrawScrollBar(L1289-L1301);两个滚动条
    并存时先画垂直再画水平,右下角画尺寸柄(DFCS_SCROLLSIZEGRIP,L1285)。WM_NCHITTEST
    (L2155-L2177)判定 HTVSCROLL/HTHSCROLL/HTSIZE(右下角尺寸柄),是拖动入口的命中
    基础。
  • draw.cDrawFrameControl(DFC_SCROLL 系列)与 DrawEdge(EDGE_RAISED)是箭头与 thumb
    立体效果的实际绘制者(GDI 直调封装),DefWndControlColor 提供默认滚动条颜色。

11.6 与 winpos / window 的关系

  • winpos.cco_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.CaretInfoco_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 窗口管理"三层交织"的典型形态:

  1. 数据层SBINFO(桌面堆,用户态可直读)与 SBINFOEX(分页池,权威状态)双通道,
    SBCALC 以结构前缀复用 SBDATASBTRACK 为内核跟踪预留骨架。
  2. 交互层 :user32 的 ScrollTrackScrollBar 模态循环 + IntScrollHandleScrollEvent
    状态机承担全部鼠标/键盘/计时器交互,SB_THUMBTRACK 连续通知 + SB_THUMBPOSITION/
    SB_ENDSCROLL 收尾形成完整拖动协议;按住箭头/滑道由 200ms/50ms 系统计时器驱动重复滚动。
  3. 绘制与内容层IntDrawScrollBar 族负责箭头(DFC_SCROLL)/滑道(PatBlt)/thumb
    (DrawEdge)绘制;scrollex.cIntScrollWindowEx + 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" 的历史印记,且内核/用户态各保留一份绘制实现,阅读时注意对照。

相关推荐
Sagittarius_A*1 小时前
【好靶场】PHP反序列化入门练习2
开发语言·web安全·信息安全·php·代码审计·反序列化
NoteStream1 小时前
MATLAB绘制带置信区间的折线散点图(科研绘图模板)
开发语言·matlab
tianyu2342 小时前
Java 正则表达式终极指南:从 Pattern 到 Matcher,从双重转义到性能优化,一篇全搞定
java·开发语言·正则表达式·group·find·pattern·matcher
caimouse4 小时前
ReactOS 窗口系统分析(23):键盘 — keyboard.c + kbdlayout.c
c语言·开发语言·计算机外设
Emily156853598705 小时前
Emerson 1C31129G03 Analog Input Module
java·开发语言·前端·plc·emerson·1c31129g03·input module
就叫_这个吧9 小时前
Java递归方法实现面包屑导航
java·开发语言
fīɡЙtīиɡ ℡10 小时前
AI 应用系统设计
java·开发语言·人工智能
今天AI了吗11 小时前
Python 基础语法(一):常量、变量、输入输出与运算符
开发语言·数据库·人工智能·python·sql·深度学习·机器学习
TELL52112 小时前
selenium webdriver 第二次初始化的异常
开发语言·python