☰
VC++静态控件实现滚动字幕:CStatic自绘与定时器驱动指南
2026/10/14 1:03:07 网站建设 项目流程

简介:面向VC++开发者的静态控件滚动字幕实现资料,详解利用SetWindowPos与定时器周期刷新控件位置来模拟滚动效果的原理,涵盖水平滚动、垂直滚动与曲线滚动三种典型方式,同时给出边界检测、背景色与字体颜色设置及透明背景等关键处理。压缩包共89个文件,以C++源文件(h/cpp)、工程配置(dsp/dsw/rc)为主,附带可运行exe、位图与图标资源及说明文档,整体仅642KB,便于对照源码理解或直接编译运行。已有328人学习下载。资源内含“滚动致谢词(一)(二)”与FlowStaticTester三个示例工程,分别覆盖基础水平滚动、垂直滚动及多效果综合测试,可从源码级掌握控件定位与定时器配合技巧,并能快速改造用于动态提示、游戏滚动文字等实际场景,适合刚接触Windows API自定义控件的中级VC++学习者。

1. 老而不旧:VC++ 静态(Static)控制实现滚动字幕,为什么值得自己写

在桌面客户端的顶部或底部挂一条公告,让文字从右往左匀速移动,这是滚动字幕最常见的用途。很多开发者第一反应是找第三方跑马灯控件,或者用带滚动条的 Edit、ListBox 硬撑,实际维护时才发现这些方案要么依赖重、要么样式和现有界面格格不入。VC++ 自带控件里最不起眼的 Static 控制,恰恰是最轻、最可控的载体——在 CStatic 上接管绘制,再加一个定时器控制文本偏移量,几十行代码就能滚出接近主流资讯客户端的字幕效果。这篇笔记就围绕 VC++ 静态(Static)控制实现滚动字幕这件事展开,把绘制机制、最小实现、参数调节和踩坑点一次性讲清楚。

2. 先搞清绘制机制:Static 控件为什么不滚动,以及两条自绘路线

做滚动字幕之前,得先理解 Static 控件在 Windows 图形体系里的位置。它不是一个“组合控件”,也不是一个“容器”,本质上就是一个普通的子窗口,只是窗口类叫 “STATIC”,由系统提供了一组非常有限的绘制逻辑。

2.1 原生 Static 控件为什么不带滚动能力:STATIC 窗口类与 WM_PAINT 流程

Win32 里 Static 控件的绘制完全由系统在 WM_PAINT 里完成,开发者能做的只是通过 SS_ 样式告诉系统“文本靠左、居中还是靠右”。常见的样式包括 SS_LEFT、SS_CENTER、SS_RIGHT,还有带边框的 SS_SUNKEN 和用于自绘的 SS_OWNERDRAW。它没有滚动条,没有动画机制,也不会响应滚轮,更不能自己在内部移动文本位置。

原因在于 Static 控件的实现非常古老。它的 PM_PAINT 处理逻辑大致是:取得当前矩形,按样式调用 DrawText 或 DrawIcon,画完就结束。系统不会为它维护任何“滚动状态”,也不会记住上次画到哪个位置。所以想让 Static 动起来,唯一现实的办法是绕过系统默认绘制,自己控制每一次重绘时文本出现的坐标。

这意味着两件事:第一,滚动逻辑要自己写,最典型的实现方式是定时器加偏移量;第二,绘制过程要自己接管,否则系统会把文本画在一个固定位置,偏移量根本没有机会生效。想通了这两点,滚动字幕的核心思路就清楚了一大半:每次定时器触发,把文本的起始 X 坐标向左移动一个像素,然后让 Static 控件重绘自己。

2.2 翻了一圈替代品:进度条跑马灯、超链接控件、第三方控件,为什么最后回到 CStatic

动手之前,不少人会在网上搜“VC++ 滚动字幕”,搜到的答案五花八门。有的是建议用 ProgressBar 的 PBS_MARQUEE 样式,有的是建议用超链接控件,还有的是让你引入一个用起来很爽但授权不明的第三方类。把这些方案并列在一起对比,才能看出为什么 CStatic 自绘才是长期维护成本最低的路径。

方案是否原生能否显示文本依赖与成本适合场景
ProgressBar + PBS_MARQUEE是只能显示滑块动画,不显示文字无额外依赖,但语义不对加载等待提示,不适合公告
Edit 控件只读是能显示静态文本,但不支持横向滚动动画无额外依赖,交互太重不需要动画的文本展示
SysLink / 超链接控件是能显示文本,但没有滚动能力无额外依赖,仅适合链接可点击入口,不是字幕
第三方跑马灯控件否能,且控件做得花哨引入 DLL、源码或有未知反馈大型项目有统一视觉规范时考虑
CStatic 子类自绘是完全可控,文本、颜色、速度都能改几十行代码,无外部依赖公告、提示、广告条、弹幕底边

说实话,PBS_MARQUEE 这个方案我见过有人硬用:把 ProgressBar 的文本用别的方式叠加上去,最后效果是一个进度条块在滚动,下面文字纹丝不动,性能和视觉都对不上。SysLink 控件更多是拿来当超链接用,它的绘制逻辑也不适合做高频重绘。第三方控件最大的问题不是功能,而是当你部署到客户机器上时,还要额外编译、分发,一旦客户环境是精简系统,坑就来了。

回到 CStatic 自绘,它具备一个很大的优势:Static 控件在对话框资源里就是最普通的元素,任何 VC++ 工程都能直接使用,不需要引入新的模块,也没有版本兼容问题。滚动逻辑做在一个自己写的类里,项目里任何地方需要字幕都能复用,这才是工程上更稳妥的选择。

2.3 两条自绘路线:子类化 CStatic 重写 OnPaint,还是走 SS_OWNERDRAW

想接管 Static 控件的绘制,常见有两种做法。第一种是给控件加上 SS_OWNERDRAW 样式,让系统发送 WM_DRAWITEM 给父窗口,再由父窗口在 OnDrawItem 里绘制。这条路线适合“控件样式和窗口绑定不紧”的场景,但问题是绘制逻辑会落在对话框代码里,多个字幕控件时就要反复判断控件 ID,代码会变得很难看。

第二种是继承 CStatic,重写 OnPaint。我自己通常用这条路线,原因是滚动逻辑可以被封装成一个独立类,比如下面要写的 CMarqueeStatic。这个类内部处理定时器、偏移量、文本测量,对使用方只暴露几个简单接口:设置文本、开始滚动、停止滚动、设置速度。对话框只需要在初始化时分配后即可,不用关心绘制细节。

选择子类化还有一个实际好处:CStatic 的反射机制已经比较成熟,OnPaint 会被正常响应,不需要像某些自定义控件那样手动处理消息反射。只要在对话框里用 DDX_Control 或 SubclassDlgItem 把资源里的 Static 控件和 CMarqueeStatic 实例绑定,控件就变成了一个会滚动的字幕条。后面所有逻辑都可以在类内部闭环,这是工程维护上最省心的地方。

3. 最小可运行实现:继承 CStatic 写一个 CMarqueeStatic 类

这一章直接给出可以抄进工程的最简实现。目标很明确:在对话框资源里放一个 Static 控件,运行后文本自动从右往左循环滚动。类名就叫 CMarqueeStatic,避免和 MFC 自带类混淆。

3.1 定义 CMarqueeStatic:成员、消息映射和对外接口

头文件要控制的成员不多:文本内容、滚动偏移量、文本宽度缓存、循环间距、定时器 ID 和定时器间隔。对外接口包括设置文本、启动、停止、设置间距和设置步进。代码如下:

// MarqueeStatic.h #pragma once class CMarqueeStatic : public CStatic { public: CMarqueeStatic(); virtual ~CMarqueeStatic(); void SetMarqueeText(const CString& text); // 设置滚动文本,同时重置偏移 void SetGap(int nGap); // 两轮文本之间的空白间距,单位像素 void SetStep(int nStep); // 每个定时器周期移动的像素数 void StartScroll(int nIntervalMs = 30); // 启动滚动,nIntervalMs 是定时器周期 void StopScroll(); // 停止滚动,保留当前状态 protected: afx_msg void OnPaint(); afx_msg void OnTimer(UINT_PTR nIDEvent); DECLARE_MESSAGE_MAP() private: CString m_strText; int m_nOffset; // 滚动累计偏移量,持续增大 int m_nTextWidth; // 文本像素宽度,缓存后避免每帧测量 int m_nGap; // 两个循环周期之间的空白 int m_nStep; // 步进像素 UINT_PTR m_nTimerID; // 定时器 ID UINT m_nTimerMs; // 定时器周期,单位毫秒 };

成员变量里最容易忽略的是 m_nTextWidth 和 m_nGap 这两个值。m_nTextWidth 决定了一帧里文本到底占多宽,m_nGap 决定了文本滚出右边界后多久再次出现。如果只做偏移不缓存宽度,OnPaint 每次都要调用 GDI 测量文本,滚动频繁时会白白损耗性能。定时器 ID 固定为 1 没问题,但如果同一个对话框里有多个滚动字幕,每个实例请用不同的 ID,或者干脆让 ID 由构造函数传入。

3.2 OnPaint 双缓冲绘制:文本测量、循环周期与剪裁

绘制是整个实现的核心。先创建内存 DC 和兼容位图,所有绘制在内存中完成,最后一次 BitBlt 输出到屏幕,这样能避免直接往屏幕 DC 上画导致的闪烁。绘制逻辑按四个步骤走:先把控件背景填成期望的颜色,再测量文本宽度,然后计算文本起点,最后循环画出一个以上的文本副本直到填满控件区域。具体代码如下:

// MarqueeStatic.cpp #include "MarqueeStatic.h" BEGIN_MESSAGE_MAP(CMarqueeStatic, CStatic) ON_WM_PAINT() ON_WM_TIMER() END_MESSAGE_MAP() CMarqueeStatic::CMarqueeStatic() : m_nOffset(0) , m_nTextWidth(0) , m_nGap(40) , m_nStep(1) , m_nTimerID(1) , m_nTimerMs(30) { } CMarqueeStatic::~CMarqueeStatic() { StopScroll(); } void CMarqueeStatic::SetMarqueeText(const CString& text) { m_strText = text; m_nOffset = 0; m_nTextWidth = 0; // 强制下次 OnPaint 重新测量 if (GetSafeHwnd()) Invalidate(FALSE); // 只重绘,不擦背景 } void CMarqueeStatic::SetGap(int nGap) { m_nGap = nGap > 0 ? nGap : 0; } void CMarqueeStatic::SetStep(int nStep) { m_nStep = nStep > 0 ? nStep : 1; } void CMarqueeStatic::StartScroll(int nIntervalMs) { if (nIntervalMs > 0) m_nTimerMs = nIntervalMs; if (GetSafeHwnd()) SetTimer(m_nTimerID, m_nTimerMs, NULL); } void CMarqueeStatic::StopScroll() { if (GetSafeHwnd()) KillTimer(m_nTimerID); } void CMarqueeStatic::OnPaint() { CPaintDC dc(this); CRect rc; GetClientRect(&rc); // 双缓冲:所有绘制先落在内存位图,最后一次性输出 CDC memDC; memDC.CreateCompatibleDC(&dc); CBitmap bmp; bmp.CreateCompatibleBitmap(&dc, rc.Width(), rc.Height()); CBitmap* pOldBmp = memDC.SelectObject(&bmp); // 背景色取对话框默认底色,避免控件区域出现一块突兀的补丁 memDC.FillSolidRect(&rc, GetSysColor(COLOR_BTNFACE)); CFont* pFont = GetFont(); CFont* pOldFont = memDC.SelectObject(pFont); memDC.SetBkMode(TRANSPARENT); memDC.SetTextColor(RGB(0, 0, 0)); // 文本宽度只在内容或字体变化后测一次 if (m_nTextWidth == 0 && !m_strText.IsEmpty()) m_nTextWidth = memDC.GetTextExtent(m_strText).cx; if (m_nTextWidth > 0) { // 周期 = 文本宽度 + 间距,滚动到这个值就从头开始 int period = m_nTextWidth + m_nGap; int phase = m_nOffset % period; int x = rc.right - phase; // 从右边界起画,x 小于右边界后文本逐步露出来 while (x + m_nTextWidth > rc.left) { memDC.TextOut(x, rc.top, m_strText); x += period; } } memDC.SelectObject(pOldFont); memDC.SelectObject(pOldBmp); dc.BitBlt(0, 0, rc.Width(), rc.Height(), &memDC, 0, 0, SRCCOPY); }

这段代码有两个值得留意的设计。第一,x 从 rc.right - phase 开始,phase 在 0 到 period-1 之间变化,所以 phase 等于 0 时文本完全位于控件右边界之外,随着 phase 增大文本缓慢滑入,这是经典的“从右进入、向左滑动”效果。第二,while 循环根据周期不断画下一个副本,这样即使文本宽度超过控件宽度,也能保证控件表面始终有内容在滚动,不会出现中间突然空白一大块的情况。

周期和偏移用取模而不是让 m_nOffset 无限增长,是为了长时间运行的稳定性。一个桌面工具可能连续运行几十天,m_nOffset 一直递增迟早会达到 int 上限,取模之后数值始终可控,视觉上也完全等价。

3.3 定时器驱动滚动:SetTimer、OnTimer 与速度换算

滚动是由 WM_TIMER 驱动的。每到一个定时器周期,OnTimer 里就把偏移量增加 m_nStep,然后调用 Invalidate(FALSE) 触发重绘。用 FALSE 参数很关键,它告诉系统不需要先擦除背景,配合双缓冲就几乎不会闪烁。实现如下:

void CMarqueeStatic::OnTimer(UINT_PTR nIDEvent) { if (nIDEvent == m_nTimerID) { m_nOffset += m_nStep; // 每次移动 nStep 像素 Invalidate(FALSE); // 不擦背景,下一帧直接覆盖 } CStatic::OnTimer(nIDEvent); }

把这段代码和 OnPaint 合在一起看,滚动链路其实只有三步:定时器触发 → 偏移量加步进 → 重绘。速度怎么算?每秒移动像素数 = 1000 ÷ 定时器周期 × 步进。比如周期 30ms、步进 1,就是每秒大约 33 像素;周期 30ms、步进 2,就是每秒大约 67 像素。

在对话框里使用这个类,只需要在 OnInitDialog 里做三件事:子类化、设置文本、启动滚动。代码也很直白:

// CMainDialog::OnInitDialog 中调用 m_ticker.SubclassDlgItem(IDC_STATIC_TICKER, this); m_ticker.SetMarqueeText(_T("系统公告:本周五 18:00 进行版本更新,请提前保存工作内容。")); m_ticker.SetGap(80); // 两轮文本之间拉开 80 像素,视觉上更透气 m_ticker.SetStep(1); // 每个定时器周期移动 1 像素 m_ticker.StartScroll(30); // 每 30 毫秒重绘一帧

SubclassDlgItem 能把对话框资源里的 Static 控件和 CMarqueeStatic 实例绑定,绑定之后控件原有的静态文本样式会立即失效,因为 OnPaint 已经被接管。如果你更习惯 MFC 的控件关联方式,也可以用 DDX_Control,效果一样,只是需要在 DoDataExchange 里先声明变量。

4. 把滚动参数调舒服:文本宽度、帧率、步进与多行扩展

代码能跑起来之后,真正影响观感的是参数。字幕滚得太快像抽风,太慢像卡死,间距太小显得拥挤,太大又让人觉得断片。这一章把每个参数背后的计算逻辑说清楚,方便按实际产品需求调整。

4.1 先测文本宽度:GetTextExtent 与缓存策略

文本宽度决定滚动周期。如果你把周期设得太短,文本还没完全露出右边界就开始下一轮,字幕就像被压缩了一样。周期设得太长,两轮文本之间空白时间太久,观众会以为字幕停了。所以第一步是准确拿到当前字体下文本的像素宽度。

MFC 里测量文本宽度最简单的方法是用 CDC::GetTextExtent,它的返回值是 CSize,cx 就是宽度侯,单位是像素。需要注意,这个方法的结果严重依赖当前选入 DC 的字体,测量时必须先选入和控件一致的字体,否则拿到的是默认字体宽度,遇到中文字符会偏差很大。这也是我在 OnPaint 里先 SelectObject 字体、再测宽度的原因。

我一般把测量结果缓存在 m_nTextWidth 里,只有两种情况下重新测量:SetMarqueeText 被调用时置零,以及收到 WM_SETTINGCHANGE 或 WM_DPICHANGED 时置零。字体没有变化的普通滚动过程里,每帧都重复调用 GetTextExtent 是白白浪费 CPU。

补充一个细节:文本里如果混有全角中文和半角数字,宽度不是字数的倍数关系。中文字符通常是数字字符的两倍宽,用“字数 × 固定值”粗算一定会偏。GetTextExtent 是 GDI 的底层计算,能正确处理字体表里的字符宽度,所以不要自己去估算。

4.2 帧率、速度与步进:30ms、50ms 与 1 像素的视觉关系

定时器周期和步进是决定滚动速度的两个旋钮。把它们拆开控制比只用一个参数更灵活,因为步进决定移动的粒度,周期决定刷新频率。下面是几个常用组合的实际速度对比:

定时器周期步进实际速度适用场景
50ms1 像素20 像素/秒大字号标题、公告栏,稳重不急促
30ms1 像素33 像素/秒常规字幕条,通用默认选择
30ms2 像素67 像素/秒长文本快速滚动,适合跑马灯
16ms1 像素60 像素/秒接近极限,但定时器精度不够稳定

需要说明的是,Windows 定时器 WM_TIMER 的精度是毫秒级,但它依赖消息循环,实际触发时间会受系统负载影响。16ms 这个值在繁忙的 UI 线程上经常被拉长到 30ms 甚至更久,所以不要追求过高的理论帧率。30ms 周期配合 1 像素步进,视觉上足够平滑,大多数用户看不出中间有割裂感。

如果觉得 33 像素/秒太慢,我更建议把步进改成 2 而不是把定时器周期压到 16ms。原因是步进变大只会让文本移动更快,但刷新频率保持不变,视觉均匀性更好。步进改成非整数没有意义,因为 GDI 的坐标是整数,1.5 像素的偏移在取整后会出现忽快忽慢的抖动,这是我在实际中踩过的坑。

4.3 超长文本与多行字幕:单行滚动不是唯一答案

当文本宽度远远大于控件宽度时,上面的 while 循环依然有效,它会自动画多个文本副本,让控件表面始终被内容填满。这种情况下 gap 参数更加重要,因为如果 gap 太小,字幕会变成一串连续不断的文字流,可读性很差。

如果产品需求是多行公告,横向滚动其实不是最优解。多行 Static 控件高度有限,让每行文字都横着滚,视觉上会非常拥挤。我处理这种需求时更常用纵向轮播:把 OnPaint 里的坐标从 X 轴改成 Y 轴,每帧向上移动一个像素,一行滚出去后下一行接上来。核心逻辑完全沿用 CMarqueeStatic 的定时器和双缓冲,只是把rc.right - phase换成rc.bottom - phase,把 TextOut 的第二个参数由固定 top 改成按行号计算出来的 y。这样一套框架可以同时支撑横滚和竖滚,后面要扩展不过瘾。

5. 避坑记录:滚动字幕最常见的 5 个翻车现场

滚动字幕代码量不大,但坑往往集中在那几十行的交互细节里。下面是五个我实际见过、也亲手踩过的常见问题,每条都按现象、原因、解决的思路整理。

5.1 字幕闪烁、拖影,整个控件区域一直在抖

现象:字幕滚起来后文字边缘发虚,控件区域明显闪烁,尤其在中文小字号下特别明显。

原因:最常见的是 Invalidate(TRUE) 引起的背景擦除和重绘竞争。TRUE 会先擦除整个客户区背景,再触发 WM_PAINT 去画文字,两者交错时就会闪。另一个因素是直接往屏幕 DC 上画,系统性能稍有波动就拖出残影。

解决:把 Invalidate 的参数改成 FALSE,并且所有绘制先画到内存 DC,最后用 BitBlt 一次性提交。如果还没好,重写 OnEraseBkgnd 直接返回 TRUE,因为背景色已经在 OnPaint 里用 FillSolidRect 填充过,不需要系统再擦一次。

BOOL CMarqueeStatic::OnEraseBkgnd(CDC* /*pDC*/) { return TRUE; // 背景由 OnPaint 负责,系统不要插手 }

5.2 中文乱码,TextOut 输出一串问号

现象:在 Debug 和 Release 下表现不同,Release 下中文变成问号或方块。

原因:工程字符集没有统一。项目是 MBCS 编译,而字符串字面量用的是 UTF-8 源码,或者字体不支持中文字符。另外 Visual Studio 老版本工程会把字符集默认设成多字节,CString 按字节处理,TextOut 的中文传给 GDI 时被截断。

解决:在项目属性 → 配置属性 → 常规 → 字符集里改成“使用 Unicode 字符集”,所有字符串统一用_T()包裹。字体方面,用系统库里明确支持中文的字体如“微软雅黑”或“宋体”,不要用那些字形很炫但内置字符集不全的艺术字体。

5.3 子类化后 OnPaint 不执行,滚动完全不生效

现象:界面上的 Static 控件还是老样子,文字纹丝不动,也没有走自己的绘制函数。

原因:最常见的是调用时机不对。有人在对话框构造函数里就调用了 SubclassDlgItem,此时控件句柄还没有创建,子类化设置丢失。还有一种情况是在资源里给 Static 控件设了 SS_OWNERDRAW 样式,而类里只处理了 OnPaint,没处理 WM_DRAWITEM,消息绕到父窗口去了。

解决:统一把 SubclassDlgItem 放在 OnInitDialog 末尾调用,确认控件 ID 和实际资源一致。不要重复调用同一控件的 SubclassDlgItem,否则会出现句柄断言。同时去掉 SS_OWNERDRAW,自绘逻辑集中在 OnPaint 里维护。

5.4 背景色不对齐,Static 区域出现一块难看的补丁

现象:对话框背景是浅灰色,字幕控件区域却是白色或另一个颜色,像一块膏药贴在界面上。

原因:Static 控件的默认背景由 WM_CTLCOLORSTATIC 决定,父对话框通常会返回一个刷子刷成对话框底色。但你的 OnPaint 里自己 FillSolidRect 用的是 COLOR_BTNFACE 或 COLOR_WINDOW,这两者在某些主题下不相等,于是和对话框底板分家。

解决:取对话框实际使用的刷子颜色来填充。最简单的方法是在 OnPaint 里把填充色改成GetSysColor(COLOR_BTNFACE),但这在自绘对话框里不一定准。更稳妥的办法是向父窗口发送 WM_CTLCOLORSTATIC 拿到刷子句柄再填,或者在对话框的 OnCtlColor 里统一定义一个背景色并让字幕类读取。关键是背景色要从父级拿,别自己另立一套。

5.5 高 DPI 下字幕跑偏、截断,文本宽度失效

现象:系统缩放从 100% 切到 150%,字幕偏移明显不准,文本右边界被裁掉一段。

原因:OnPaint 里缓存了 m_nTextWidth,但系统字体在 DPI 变化后没有重新测量。字体变大了,宽度缓存还是旧的,绘制起点就会出错。更麻烦的是对话框资源会根据 DPI 自动调整控件尺寸,而你的偏移量还在用旧的像素坐标。

解决:在工程里正确声明 DPI 感知,并在 WM_DPICHANGED 时把 m_nTextWidth 置零,重新测量。如果使用动态缩放字体,用MulDiv根据新 DPI 重算字体高度再创建字体。不要假设程序永远运行在 100% 缩放下,现在的显示器几乎全都是 125% 或 150% 起步。

6. 进阶玩法:鼠标暂停、渐变遮罩与性能验证

滚动字幕做到能跑只是第一步,放进真实产品还要考虑交互和观感。这里给三个 30 分钟就能加完的进阶能力,以及一个用于验证性能的小方法。

关于鼠标交互,做法是让字幕在鼠标悬停时暂停,离开后恢复。进入 OnMouseMove 时调用 StopScroll,离开边界时需要用到 TrackMouseEvent 监听 WM_MOUSELEAVE,核心代码如下:

void CMarqueeStatic::OnMouseMove(UINT nFlags, CPoint point) { if (!m_bHovered) { m_bHovered = TRUE; StopScroll(); TRACKMOUSEEVENT tme = { sizeof(tme), TME_LEAVE, GetSafeHwnd(), 0 }; TrackMouseEvent(&tme); } CStatic::OnMouseMove(nFlags, point); }

渐变遮罩可以让字幕边缘不再生硬地裁掉。实现思路是在双缓冲位图提交前,对左边缘一块区域做渐变填充,让文字临近边界时透明度逐步降低。用 GDI+ 的 LinearGradientBrush 就能实现,每帧创建一个 24 像素宽的渐变带即可。要注意的是渐变遮罩本身也是一次绘制开销,宽度控制在 20 到 30 像素之间,视觉和性能能兼顾。

性能验证只需要在 OnTimer 里记录重绘消耗,不要凭感觉判断卡不卡。代码很简单:

LONGLONG t1 = GetTickCount64(); Invalidate(FALSE); UpdateWindow(); LONGLONG t2 = GetTickCount64(); TRACE(_T("frame cost: %lld ms\n"), t2 - t1);

我的习惯是把这个测量代码放在一个条件编译开关里,平时不输出,做性能测试时打开。实测中,普通的 500 像素宽双缓冲字幕,单帧耗时通常在 1ms 量级;如果耗时经常超过 20ms,问题多半出在字体测量没有缓存,或者句柄创建泄漏导致 GDI 对象堆积,优先检查这两处。

最后说一句个人经验:滚动字幕这类小需求,最容易翻车的不是写不出滚动逻辑,而是把滚动逻辑和对话框业务代码粘在一起,导致后来想复用又要拆一遍。坚持把 CMarqueeStatic 做成独立类,把文本、间距、步进、定时器全部参数化,后面任何一个新界面想加字幕,十分钟就能接入。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询