
简介桌面滚动字幕软件 Pro 是一款面向直播提词、门店宣传、企业迎宾及个人专注提醒的 Windows 桌面工具通过置顶浮动、透明度可调的滚动字幕窗口在不干扰鼠标键盘操作的前提下持续展示关键信息。支持纯色/渐变文字、多方向滚动、字体描边阴影与边框背景全自定义并配有暖色、冷色、霓虹等快速配色方案方便零基础用户直接上手。压缩包共 250 个文件、约 131MB包含可直接运行的 exe 与启动脚本、Python 源码及 pyd 扩展、Qt 依赖 dll、qm 资源文件、配置 json 和说明文档既适合直接部署使用也便于开发者基于源码二次开发。功能方面涵盖可视化文本样式编辑、窗口宽高与透明度精确调节、滚动速度及方向控制、背景边框开关等可灵活适配各类展示场景。目前已有 188 人学习浏览对于需要轻量级滚动字幕方案的内容创作者和 Qt 学习者均有不错的参考价值。1. 桌面滚动字幕软件Pro到底在解决什么一台普通电脑变成全天候的提词与通告屏直播间助理举着手写板提示主播背词超市门口用电视循环播放促销信息年会舞台侧面放一台笔记本当提词器——这些场景其实都在等同一个工具一台普通电脑上能跑的桌面滚动字幕软件Pro。这个标题里最值钱的一句是「含源码可二次开发」意味着它不是封装死的黑匣子字体、描边、阴影、渐变、滚动速度、透明度和背景色全部开放给你改。真正让这类软件难做的不是「把字往左挪」这个动作而是把一个半透明、置顶、不挡鼠标操作的浮动字幕窗口稳定地跑在各种分辨率和缩放比例的电脑上。这篇文章想讲清楚三件事它是怎么画出来的、拿到源码后从哪里下手改、哪些参数会让你半夜翻车。适合读这篇的是手里已经有这个 rar 包、或者正准备做一个类似桌面工具的开发者。如果你只想用现成的这篇也能帮你判断「哪里值得改、哪里改不动」。2. 先看懂滚动字幕跑起来的底层三件事异形窗口、GDI绘制与时间驱动2.1 置顶浮动窗口不是玄学分层窗口与点击穿透参数「可置顶浮动窗口」听起来简单把 Form.TopMost 设成 true 就完事。但做过的人都知道只设 TopMost 的窗口会带一个不透明的矩形背景字幕在上面走背景盖住底下所有内容。想要那种「字飘在桌面上、背景完全消失」的效果常见做法是走 Win32 的分层窗口机制也就是给窗口加上 WS_EX_LAYERED 扩展样式让窗口支持逐像素 Alpha 混合。C# WinForms 里改 CreateParams 就能做到。protected override CreateParams CreateParams { get { CreateParams cp base.CreateParams; cp.ExStyle | 0x00080000; // WS_EX_LAYERED允许分层绘制 cp.ExStyle | 0x00000020; // WS_EX_TRANSPARENT鼠标点击穿透 cp.ExStyle | 0x08000000; // WS_EX_NOACTIVATE不抢焦点 cp.ExStyle | 0x08000020; // WS_EX_TOOLWINDOW不在任务栏显示 return cp; } }这段 ExStyle 是整篇代码里最关键的四行。0x00080000 是分层窗口的开关没有它后面的透明度全失效0x00000020 让鼠标事件直接穿透窗体点到底下桌面正常操作电脑不用先挪开字幕0x08000000 防止字幕窗口抢键盘焦点否则你打字时字幕窗会截走按键0x08000020 让它不占任务栏位置。实际发布时这四个值按需求取舍如果要做「鼠标拖拽调整位置」的编辑模式就得在拖拽时临时去掉 0x00000020否则鼠标根本点不到窗口。另一个容易踩的坑是直接用 Form.TransparencyKey 把背景色设成透明。这个做法会带来两个问题一是抗锯齿边缘会残留一圈背景色的毛边字体笔画边缘发虚二是 TransparencyKey 是整体删除某一种颜色没法做「背景半透明但文字不透明」的层次效果。我一般会直接放弃 TransparencyKey改用 UpdateLayeredWindow 配合一张带 Alpha 通道的位图透明度完全由位图每个像素的 Alpha 决定。后面第 3 章的绘制器就是按这个思路写的。2.2 文字向左滚动画布偏移量与双缓冲刷新滚动字幕的动画本质是把整段文字画进一张比窗口更宽的位图里每一帧让位图内容向左偏移若干像素再把对应区域贴到屏幕上。这里的关键不是「怎么画字」而是「怎么控制偏移量」。最蠢但最常见的问题写法是固定每帧移动 1 像素帧率一变滚动速度就忽快忽慢。更可靠的做法是把速度定义成「像素/秒」用时间差驱动。private Stopwatch stopwatch new Stopwatch(); private double offsetX 0; private const double speed 60; // 像素/秒可外置配置 private const int canvasWidth 1920; private void OnTimerTick(object? sender, EventArgs e) { stopwatch.Stop(); double deltaSeconds stopwatch.Elapsed.TotalSeconds; stopwatch.Restart(); offsetX - speed * deltaSeconds; if (offsetX -textWidth) offsetX canvasWidth; DrawToScreen(); } private void DrawToScreen() { using var bitmap new Bitmap(canvasWidth, canvasHeight); using (var g Graphics.FromImage(bitmap)) { g.Clear(Color.Transparent); g.TextRenderingHint TextRenderingHint.AntiAlias; g.DrawString(text, font, brush, (float)offsetX, yPos); } // 省略将 bitmap 传给 UpdateLayeredWindow 贴到分层窗口 }Timer 本身并不精确10ms 的 Timer 实际触发间隔可能在 8 到 20ms 之间波动。用 Stopwatch 算 deltaSeconds然后按 speed * deltaSeconds 计算偏移滚动速度就只取决于 speed 这个值跟机器快慢无关。textWidth 是预先用 Graphics.MeasureString 量出来的文本总宽textWidth 全走完时把 offsetX 重置到画布右端实现循环滚动。offsetX 用 double 而不是 int是为了慢速滚动时每帧偏移 0.5 像素也能被累加出来不会因为整数截断变成一卡一卡的。提示滚动到最右端瞬间画布上同时存在文尾和文首要保证 canvasWidth 至少大于 textWidth否则首尾衔接处会闪出空白。这段逻辑几乎是所有滚动字幕源码的基础。你拿到压缩包后先在源码里找 offsetX、deltaSeconds、UpdateLayeredWindow 这几个关键词就能判断作者用的是「时间驱动」还是「帧驱动」前者值得继续看后者建议直接改掉。3. 把核心源码跑通最小可用的滚动字幕程序应该由哪几个类组成3.1 主窗体、绘制器、字幕模型这三个类谁也别越界一套能二次开发的滚动字幕源码解压后你应该能在里面找到三个职责明确的类字幕模型负责存配置绘制器负责把配置变成位图主窗体负责窗口生命周期和消息循环。我见过最劝退的源码是把这三个职责全塞进一个 Form 的 OnPaint 里改个字体颜色要找半小时。拿到源码先别急着跑按下 CtrlF5 之前先看结构。如果它已经按这三个模块切好你的二次开发成本会低很多。public class SubtitleModel { public string Text { get; set; } ; public string FontFamily { get; set; } 微软雅黑; public float FontSize { get; set; } 48f; public bool IsBold { get; set; } false; public int Speed { get; set; } 60; // 像素/秒 public Color ForeColor { get; set; } Color.White; public Color GradientColor { get; set; } Color.Transparent; public Color StrokeColor { get; set; } Color.Black; public int StrokeWidth { get; set; } 2; public Color ShadowColor { get; set; } Color.FromArgb(120, 0, 0, 0); public int ShadowOffsetX { get; set; } 3; public int ShadowOffsetY { get; set; } 3; public Color BackgroundColor { get; set; } Color.FromArgb(80, 0, 0, 0); public bool ShowBackground { get; set; } false; }这个模型把标题里提到的「纯色渐变、描边阴影、透明度背景」全部落成字段。注意 BackgroundColor 用的 Color.FromArgb(80, 0, 0, 0)第一个参数 80 是背景的 Alpha 值映射到窗口上就是「半透明黑背景」字幕区能看到底下桌面的内容文字本体仍是不透明的。这也是为什么之前强调不要用 TransparencyKey只有逐像素 Alpha 才能同时表达「背景半透明、前景全不透明」。绘制器承接模型向外暴露一个 Render(Bitmap) 方法。字段全部由 SubtitleModel 驱动绘制器内部不写任何硬编码颜色和字体。主窗体只做三件事加载配置、起定时器、把绘制器渲染完的位图通过 UpdateLayeredWindow 贴到窗口。我改这类软件时有个习惯先把模型字段全部外置成 JSON 配置文件改文字、颜色、速度就不用重新编译这一步做完二次开发的地基就稳了。3.2 纯色渐变、描边与阴影GDI 的三种画法对比GDI 给字幕绘制提供了三种能力纯色、渐变、描边阴影分别对应 SolidBrush、LinearGradientBrush 和多次 DrawString。最容易做错的是绘制顺序正确顺序是阴影先画、描边第二、主体字最后。一旦把主体字先画上去描边和阴影全被盖住等于白画。public Bitmap RenderSubtitle(SubtitleModel model, int canvasWidth, int canvasHeight) { var bitmap new Bitmap(canvasWidth, canvasHeight, PixelFormat.Format32bppArgb); using (var g Graphics.FromImage(bitmap)) { g.Clear(Color.Transparent); g.TextRenderingHint TextRenderingHint.AntiAlias; g.SmoothingMode SmoothingMode.AntiAlias; // 第 1 步阴影偏移后使用半透明刷 using var shadowBrush new SolidBrush(model.ShadowColor); g.DrawString(model.Text, font, shadowBrush, x model.ShadowOffsetX, y model.ShadowOffsetY); // 第 2 步描边沿上下左右四个方向外扩绘制 if (model.StrokeWidth 0) { using var strokeBrush new SolidBrush(model.StrokeColor); for (int dx -model.StrokeWidth; dx model.StrokeWidth; dx) { for (int dy -model.StrokeWidth; dy model.StrokeWidth; dy) { if (dx * dx dy * dy model.StrokeWidth * model.StrokeWidth) g.DrawString(model.Text, font, strokeBrush, x dx, y dy); } } } // 第 3 步主体文字纯色或渐变二选一 if (model.GradientColor.A 0) { using var gradientBrush new LinearGradientBrush( new RectangleF(x, y, textWidth, textHeight), model.ForeColor, model.GradientColor, LinearGradientMode.Horizontal); g.DrawString(model.Text, font, gradientBrush, x, y); } else { using var solidBrush new SolidBrush(model.ForeColor); g.DrawString(model.Text, font, solidBrush, x, y); } } return bitmap; }描边这里用了两层循环范围从 -StrokeWidth 到 StrokeWidth再通过圆方程过滤掉四角避免描边变成方块。小字号时 StrokeWidth 建议 1 或 2大字号可以到 4太宽会糊成一团。渐变刷的 LinearGradientMode.Horizontal 表示从左到右渐变改成 Vertical 则是从上到下。这里有个常见误用渐变矩形用的是文字所在的新矩形x, y, textWidth, textHeight如果写成整个画布的矩形渐变范围会铺满窗口文字滚动时颜色分布看起来是静止的很怪。注意描边循环里每次 DrawString 都是独立绘制字号越大开销越高。描边宽度 2 时约画 13 次文字加上阴影和主体一帧要画 15 次。性能不够时把循环降到只画上下左右 4 个方向观感差别不大CPU 能降三分之一。4. 参数调优与避坑实录透明度、渐变与滚动节奏的四个翻车现场4.1 窗口透明了字也跟着变淡分层窗口的 Alpha 叠加顺序现象按网上教程设了 SetLayeredWindowAttributes(hwnd, 0, 128, LWA_ALPHA)整个窗口透明度变成 50%字幕和背景一起变淡直播画面里的字根本看不清。原因SetLayeredWindowAttributes 的 LWA_ALPHA 是对整个窗口统一乘以一个 Alpha 系数它管不了「哪些像素透明、哪些不透明」。背景半透明、文字全不透明这种需求它做不到。解决放弃这个 API改走 UpdateLayeredWindow 逐像素 Alpha。位图里背景像素写 Color.FromArgb(80, 0, 0, 0)文字像素写 Color.FromArgb(255, 255, 255, 255)窗口拿到位图后按像素本身的 Alpha 合成。判断源码是否走了正确方案搜 UpdateLayeredWindow 关键字搜不到就准备自己改。4.2 滚动速度不稳定Timer 间隔和像素步长的取舍现象代码里 Timer 设 100ms、每帧偏移 1 像素字走得慢吞吞改成每帧偏移 5 像素又太快换到一台高配电脑上同样的配置速度明显变快。原因这是典型的「帧驱动」写法把速度和帧率耦在一起。Timer 实际触发间隔受系统负载影响10ms 的 Timer 在高负载下可能变成 30ms 才触发一次而每帧偏移量是固定的帧率一变速度就变。解决速度定义成「像素/秒」每帧偏移量 speed * deltaSeconds。我在 2.2 节给的 Stopwatch 写法可以直接抄。调参时按秒想比如 60 像素/秒适合提词用120 像素/秒适合走马灯公告。速度不是玄学是单位没定对。4.3 渐变总往一个方向偏LinearGradientBrush 的模式与坐标现象设置了从上到下的竖向渐变实际效果却从左上斜到右下或者文字在滚动渐变却像贴纸一样贴在窗口上不动。原因LinearGradientBrush 的渐变方向和范围由构造参数里的矩形决定默认模式下渐变从矩形左上角到右下角并且是 45 度斜向。很多人只传了宽高没指定 LinearGradientMode得到斜向渐变还以为是自己颜色配错了。渐变不跟随文字滚动是因为矩形坐标写的是窗口坐标不是文字当前坐标。解决横向渐变显式传 LinearGradientMode.Horizontal竖向传 Vertical。矩形起点要传 (x offsetX, y)其中 offsetX 是当前滚动偏移。否则文字左右移动时渐变范围固定在窗口上文字颜色却不跟着变视觉上像两个图层在互相穿帮。这里我吃过一次亏调了半小时才发现是坐标没带偏移量。4.4 双屏副屏上错位、DPI 缩放后字发糊现象主屏显示正常拖到副屏字幕偏到屏幕外系统缩放从 100% 调到 150%字边缘发毛、位置不准。原因两个问题叠加。其一是程序没声明 DPI 感知系统把逻辑坐标乘以缩放系数导致画布宽度和窗口宽度不一致其二是副屏可能有不同的缩放比例主屏 100% 副屏 150% 时坐标换算全乱。解决给 exe 加 app.manifest声明 PerMonitorV2 感知级别所有 Bitmap 用真实像素创建不要用 Control.Width 这种可能被缩放的逻辑值多屏定位用 Screen.AllScreens 逐个判断字幕窗口当前所在的屏幕再按该屏幕的 Bounds 和 WorkArea 算左右边界。双屏用户占比不低这个不处理你的软件发布出去十个用户里至少有一个直接弃用。5. 二次开发指南从改参数到换场景如何给这套源码加上真正需要的能力5.1 把字幕文本改成外部可推送本地 Socket 指令通道拿到源码后的第二个常见诉求是不想每次改文字都进配置界面希望直播间运营能远程把新文字推过来。改配置文件广播又要重启来不及。最轻量的方案是给程序开一个本地 TCP 监听端口任何进程都能往里发一行指令字幕实时更新。var listener new TcpListener(IPAddress.Loopback, 9831); listener.Start(); while (true) { using var client await listener.AcceptTcpClientAsync(); using var reader new StreamReader(client.GetStream(), Encoding.UTF8); string? line await reader.ReadLineAsync(); if (string.IsNullOrEmpty(line)) continue; // 指令格式SET|文本内容|前景色十六进制 var parts line.Split(|); if (parts[0] SET parts.Length 2) { model.Text parts[1]; if (parts.Length 3) model.ForeColor ColorTranslator.FromHtml(parts[2]); renderer.Invalidate(); } }监听地址用 IPAddress.Loopback 即 127.0.0.1只允许本机进程连进来防止字幕内容被局域网内其他人篡改。端口可以做成配置项挑 9831 这种不常用端口。协议格式用竖线分隔足够应付简单需求字段多起来就换 JSON 行——但 JSON 解析会引入依赖如果源码是纯 WinForms 项目尽量保持零依赖。推文字最典型的用法直播时将 OBS 的文本源换成本地这个端口由外部脚本把最新的公告直接推过来。5.2 给字幕加暂停、跑马灯回卷与多行队列常用增强点跑通基础滚动之后用户提需求最多的三个点暂停、回卷、排队。暂停不是简单停 Timer因为 Stopwatch 还在计时恢复时会突然跳一大截。正确做法是暂停时记录 Timer 已运行时间恢复时把 Stopwatch 重新开始并补偿中间损耗。跑马灯回卷是常见设计差异——有些场景希望文字滚到末尾后停住过几秒再从右边重新进入而不是一直接着循环。实现时要加一个 EndPauseMillis 状态位分三段状态机滚出、停留、回卷。多行队列适合门店公告场景多条信息轮播。用 ConcurrentQueue 做先进先出当前条滚出屏幕后自动 pop 下一条这个需求改动很小但非常实用。加了 Socket 通道后运营端往队列里 push 三条促销信息字幕机就循环播完再等新数据。这三个改动加起来在一两百行内是全部源码里性价比最高的二次开发方向。5.3 二次开发的边界这套源码能改什么动哪里等于重写拿到源码先判断它的技术栈年龄。WinForms GDI 的架构字库和绘图都在 CPU 端优点是代码直观、依赖少缺点是文字量大或字体特效重时 CPU 占用高。在这个架构下改颜色、改字体、加速滚动、加 Socket 指令都是安全改动但如果想给文字加模糊发光、流动渐变、粒子特效GDI 的管线和现代 GPU 合成完全不一样这时候就不是「改」是重写渲染层。我一般会给二次开发划三条边界。第一条能用 SubtitleModel 字段表达的属于配置改动半天内能交付第二条需要新增绘制步骤但仍在 GDI 能力内的比如加外发光、加文字边框圆角属于渲染器改动要关注每帧耗时变化第三条想换 Direct2D、SkiaSharp 或 WPF 的属于渲染层重写旧源码里只有模型字段能复用绘制代码基本全废。拿到的 rar 如果是第三类需求二次开发成本可能高于从零写别被「含源码」三个字绑架。6. 一个值得收藏的验证技巧用 5 组参数跑一轮回归确认你的改动没破坏滚动二次开发最怕的是「改动前好好的改完不知道哪里坏了」。我给自己定了一套 5 组参数的回归清单每次改完代码就按这 5 组跑一遍十分钟内能判断改动是否引入问题强烈建议你也存一份。组号参数组合重点观察项1纯色文字、无描边、0 透明度背景、速度 60基线滚动是否平滑、无残影250% 半透明背景、2px 描边、纯色文字背景与文字透明度是否分离描边是否遮字3渐变 4px 描边 阴影渐变是否跟随文字描边外扩是否均匀448px 字体、长文本 200 字滚动衔接处是否闪空CPU 是否飙升5窗口拉到副屏、系统缩放 150%坐标是否错位、字体是否发虚这 5 组的选取逻辑是用最小组合覆盖全部特性第 1 组验证纯色无干扰下的滚动完整性第 2 组验证 Alpha 分层第 3 组验证渐变与描边的坐标一致性第 4 组压长文本和大字号第 5 组压多屏环境。任何改动的回归中第 1 组失败说明基础滚动代码坏了后面的组再好看也没用第 3 组失败说明你的渲染顺序或坐标计算出了问题。我有一次给描边循环加了距离过滤第 4 组从满帧掉到明显卡顿顺手打开任务管理器发现 CPU 从 8% 涨到 35%立刻回滚了那处改动。性能回归的阈值很简单长文本下 CPU 占用不要超过 15%超过就是绘制路径里多了重活。这套源码的核心价值不在「少写几百行」在于它把窗口分层、GDI 绘制、时间驱动、Alpha 混合这些桌面开发的基础功演示了一遍。照着它改几个参数、接一个 Socket 端口你的字幕软件就已经跟市面上免费版拉开差距了。改动前记得先给原始文件留一份踩坑了还有后悔药吃。希望这篇能帮到你动起手来少走几个弯路。本文还有配套的精品资源点击获取