# PySide6 悬浮字幕窗:透明置顶、双层字幕、屏幕共享隐身(第 4 篇)

发布时间:2026/8/23 7:58:05
# PySide6 悬浮字幕窗:透明置顶、双层字幕、屏幕共享隐身(第 4 篇) 纯语音转文字系列第 1 篇系统设计与四层流水线第 2 篇音频采集 语音活动检测第 3 篇流式 ASR —— 从 3 秒延迟重构到亚秒级增量解码第 4 篇PySide6 悬浮字幕窗本篇第 5 篇工程化与打包发布悬浮字幕窗是整条流水线的最后一层——把 ASR 输出的文本实时渲染到屏幕上。它需要同时满足三个约束透明置顶 点击穿透鼠标直接穿过它操作底层窗口像不存在一样双层字幕说话时灰色实时增长句末自动切换为白色终稿边说边看不抖动屏幕共享隐身核心功能开会共享屏幕时对方看不到字幕。通过SetWindowDisplayAffinity一行代码实现面试、答辩、远程汇报场景的刚需一、本篇要解决的事ASR 识别出文本后怎么把字幕实时、优雅地显示在屏幕上需求很具体需求说明实时ASR 线程识别完 → 字幕立刻刷新不能有轮询延迟不挡操作透明、置顶、点击穿透可读性强中间结果和最终结果要有区分边说边出字防暴露屏幕共享时对方看不到字幕面试/会议刚需二、线程安全Qt Signal/Slot 机制问题ASR 在后台线程UI 在主线程Qt 的 UI 只能在主线程事件循环中更新。ASR 识别在后台线程运行如果直接操作 UI 控件轻则界面卡顿重则崩溃。解法Signal/Slot —— 跨线程的快递柜Signal/Slot 可以用一个类比理解┌─────────────────┐ ┌─────────────────┐ │ ASR 工作线程 │ │ UI 主线程 │ │ │ │ │ │ 识别出文本后 │ emit() │ 事件循环中 │ │ 把结果放进 ────┼──────────→ 快递柜取出结果 │ │ 快递柜 │ (不阻塞) │ 立刻刷新 UI │ │ │ │ │ └─────────────────┘ └─────────────────┘类比Signal 就像快递投递——ASR 线程把包裹文本放进快递柜emit不用等收件人UI当面签收UI 主线程在自己的事件循环里轮询快递柜取出包裹后立刻处理。全程无阻塞、无轮询延迟。classPipeline(QObject):# 定义两个信号终稿 中间稿subtitle_updatedSignal(str)# 终稿字幕一句话说完subtitle_provisional_updatedSignal(str)# 中间字幕边说边出字# ---- ASR 工作线程中 ----defon_partial_result(self,text):self.subtitle_provisional_updated.emit(text)# emit 后立刻返回不阻塞defon_final_result(self,text):self.subtitle_updated.emit(text)# 终稿信号# ---- UI 主线程中 ----pipeline.subtitle_updated.connect(overlay_window.update_subtitle)pipeline.subtitle_provisional_updated.connect(overlay_window.update_provisional_subtitle)为什么没有轮询延迟emit()触发时Qt 内部自动将调用投递到接收者所在线程的事件队列UI 事件循环下一轮就执行。ASR 线程只负责投递不等待 UI 处理完毕——这就是事件驱动的本质。三、双层字幕设计效果对比流式识别的天然问题是中间结果和最终结果不一致解码器随音频增长不断修正。如果都用同一种样式字幕会跳来跳去观感很差。效果对比单层 vs 双层❌ 单层字幕只有终稿——用户看到字幕反复跳变第 0.8s: 你好 第 1.6s: 你好现在可 ← 突然变长 第 2.4s: 你好现在可以听到 ← 又变了 第 3.2s: 你好现在可以听到我讲话吗 ← 终于稳定体验字幕不断闪烁跳变像在纠错干扰阅读。✅ 双层字幕中间稿 终稿——灰色实时增长句末切白色第 0.8s: [灰色] 你好 第 1.6s: [灰色] 你好现在可 第 2.4s: [灰色] 你好现在可以听到 第 3.2s: [白色] 你好现在可以听到我讲话吗 ← 句末自动切换为正式样式体验灰色部分像草稿安静增长白色部分像定稿稳定呈现既看到了实时过程又不被中间修正干扰。实现两层信号、两种样式信号含义UI 样式subtitle_provisional_updated语音进行中的部分结果灰色文字 虚线边框“识别中…”subtitle_updated语音段结束的完整结果白色正式样式终稿classOverlayWindow(QWidget):defupdate_subtitle(self,text):# 终稿正式白色样式self._subtitle_label.setStyleSheet(FINAL_STYLE)self._subtitle_label.setText(textor等待语音...)defupdate_provisional_subtitle(self,text):# 中间稿临时灰色样式self._subtitle_label.setStyleSheet(PROVISIONAL_STYLE)# 灰色 虚线边框self._subtitle_label.setText(textor识别中...)四、悬浮窗透明 置顶 点击穿透PySide6 本身支持窗口透明度和置顶但点击穿透鼠标点击穿透窗口落到下面的应用必须调用 Windows API。完整实现带注释importctypes# 第一步Qt 窗口标志 # FramelessWindowHint → 无边框去掉标题栏# WindowStaysOnTopHint → 始终置顶不被其他窗口遮挡# Tool → 工具窗口不占用任务栏AltTab 不显示self.setWindowFlags(Qt.FramelessWindowHint|Qt.WindowStaysOnTopHint|Qt.Tool)# 第二步半透明背景 # WA_TranslucentBackground → 允许窗口背景透明配合 QSS 的 rgba 实现半透明self.setAttribute(Qt.WA_TranslucentBackground)# setWindowOpacity → 整体不透明度0.73 73% 不透明既有存在感又不遮挡底层self.setWindowOpacity(0.73)# 第三步点击穿透Windows API# 原理给窗口添加 WS_EX_TRANSPARENT 扩展样式告诉系统这个窗口不接收鼠标事件# WS_EX_LAYERED 配合 WS_EX_TRANSPARENT 使用确保透明窗口正确传递点击事件hwndint(self.winId())# 获取窗口的原生 Windows 句柄GWL_EXSTYLE-20# GetWindowLong 的索引扩展样式WS_EX_TRANSPARENT0x00000020# 扩展样式标志鼠标穿透WS_EX_LAYERED0x00080000# 扩展样式标志分层窗口支持透明# 读取当前扩展样式按位或上我们的标志再写回ex_stylectypes.windll.user32.GetWindowLongW(hwnd,GWL_EXSTYLE)ctypes.windll.user32.SetWindowLongW(hwnd,GWL_EXSTYLE,ex_style|WS_EX_TRANSPARENT|WS_EX_LAYERED)效果字幕悬浮在屏幕角落鼠标点击会穿透它操作下面的应用不干扰任何操作始终置顶不被遮挡。五、屏幕共享隐身面试/会议场景的关键功能这是整个悬浮字幕窗最有价值的功能。远程面试、在线答辩、共享屏幕演示等场景中用户打开了语音转字幕辅助自己但绝不能让对方看到字幕——否则显得不专业甚至引发信任危机。SetWindowDisplayAffinity一行代码解决这个问题字幕只在你自己的屏幕上可见屏幕共享/录屏/截屏全部看不到它。原理Windows 窗口显示亲和性 APIWindows 10 2004 提供了WDA_EXCLUDEFROMCAPTURE标志设置后窗口正常显示但所有屏幕捕获包括 OBS、腾讯会议、Zoom 共享屏幕都会忽略该窗口。# WDA_EXCLUDEFROMCAPTURE 0x00000011# 作用将窗口从所有屏幕捕获截屏/录屏/共享中排除# 效果窗口在你屏幕上正常显示但对方共享屏幕时完全看不到它WDA_EXCLUDEFROMCAPTURE0x00000011# 调用 Windows API 设置显示亲和性# hwnd窗口句柄必须在 show() 之后获取resultctypes.windll.user32.SetWindowDisplayAffinity(hwnd,WDA_EXCLUDEFROMCAPTURE)兼容性检测该 API 仅在 Windows 10 2004build 19041上支持。调用前需检测系统能力不支持时降级为普通置顶importplatformdefis_capture_protection_supported()-bool:检查当前 Windows 版本是否支持 WDA_EXCLUDEFROMCAPTURE# Windows 10 2004 (build 19041) 才支持此 APIifplatform.system()!Windows:returnFalsebuildint(platform.version().split(.)[-1])returnbuild19041defenable_capture_protection(self):启用屏幕共享隐身不支持时优雅降级ifnotis_capture_protection_supported():logger.warning(当前系统不支持屏幕共享隐身需要 Windows 10 2004)return# 降级字幕仍然正常置顶显示只是不隐身hwndint(self.winId())WDA_EXCLUDEFROMCAPTURE0x00000011ctypes.windll.user32.SetWindowDisplayAffinity(hwnd,WDA_EXCLUDEFROMCAPTURE)logger.info(屏幕共享隐身已启用)注意必须在窗口show()之后调用此时winId()才有效否则会静默失败。六、完整信号链路ASR 线程 UI 主线程 │ │ ├── emit(provisional) ────────────→ 灰色中间字幕边说边出字 │ (每 0.8s 一次) │ ├── emit(final) ─────────────────→ 白色终稿 可选历史记录 │ │ └── VAD 静音 1s → flush 合并缓冲 ──→ 可选写入字幕历史 / 导出 SRT七、字幕区样式参考self._subtitle_labelQLabel(等待语音...)self._subtitle_label.setWordWrap(True)# 自动换行self._subtitle_label.setStyleSheet( QLabel { background-color: rgba(15, 23, 42, 200); /* 半透明深蓝灰底色 */ color: #E2E8F0; /* 浅灰白文字 */ border: 1px solid rgba(99, 102, 241, 25); /* 极淡的紫色细边框 */ border-radius: 8px; /* 圆角 */ padding: 10px 12px; /* 内边距 */ font-size: 14px; line-height: 1.6; /* 行高提升可读性 */ } )八、小结特性实现方式跨线程安全Signal/Slot 事件驱动无轮询无阻塞双层字幕中间稿灰色/虚线与终稿白色正式分离体验不抖动透明置顶 穿透WS_EX_TRANSPARENTWS_EX_LAYEREDWA_TranslucentBackground屏幕共享隐身SetWindowDisplayAffinity(WDA_EXCLUDEFROMCAPTURE)下一篇收官工程化与打包发布——性能调优、字幕导出 SRT、PyInstaller 打包成单文件 exe。本文代码与数据来自真实项目实战。