Skip to content

fix: 修复任务栏歌词窗口偶尔漂移到屏幕顶部 - #261

Open
abkkkbb wants to merge 1 commit into
SPlayer-Dev:devfrom
abkkkbb:fix/taskbar-lyric-embed-recovery
Open

fix: 修复任务栏歌词窗口偶尔漂移到屏幕顶部#261
abkkkbb wants to merge 1 commit into
SPlayer-Dev:devfrom
abkkkbb:fix/taskbar-lyric-embed-recovery

Conversation

@abkkkbb

@abkkkbb abkkkbb commented Sep 11, 2026

Copy link
Copy Markdown

改动类型

  • 新功能(feat)
  • 缺陷修复(fix)
  • 重构 / 优化(不改变对外行为)
  • 文档(docs)
  • 其他(请在「改动说明」中注明)

是否包含破坏性变更

  • 是(请在「改动说明」中详细描述)

  • 是(请在「改动说明」中详细描述)

改动说明

修复任务栏歌词窗口偶尔「漂到屏幕顶部」并永久停在那里的问题。

现象:开启任务栏歌词后,歌词窗口偶尔会出现在屏幕顶部(水平位置与宽高仍是任务栏空余区域那组值),且不会再回到任务栏,只有把「任务栏歌词」开关关开一次才恢复。

根因:歌词窗口靠 SetParent 嵌入任务栏,其位置坐标是「相对任务栏」的——原生侧返回的布局矩形里 y 恒为 0(即任务栏顶边),applyLayout 原样交给 win.setBounds()。父关系一旦被外部解除(例如 SetParent(hwnd, NULL)),同一组数字会被按屏幕坐标解释,窗口就精确落在屏幕顶部;而此后每次布局仍然传 y=0,等于反复把它按在顶部,因此不会自行恢复。

原实现只在窗口 ready-to-show 时嵌入一次,之后没有任何地方校验「窗口是否还挂在任务栏上」,所以一旦失效就是永久的。

改动

  • native/taskbar-lyric/src/lib.rs:新增 probeWindow(hwndPtr) -> { alive, embedded }——用 IsWindow 判断句柄是否仍有效,用 GetAncestor(GA_PARENT) 与当前 explorer 的 Shell_TrayWnd 比对判断是否仍嵌在任务栏上;
  • electron/main/window/taskbarLyric.ts:记住嵌入时的 HWND,每 500ms 校验一次;父关系被解除则「隐藏 → 重新 SetParent → 重放布局(setBounds + setShape)」,HWND 变化时用新句柄重新嵌入;HWND 已失效(explorer 重启)时不对窗口做任何操作;
  • electron/main/window/taskbarLyricEmbed.ts(新增):判定逻辑抽为纯函数并补充单测。

范围:不改变窗口样式与嵌入方式、不引入新日志、不处理「窗口被销毁后重建」(explorer 重启后的恢复维持现状)。

关联 Issue

测试情况

平台:Windows 11 25H2(内部版本 26200)

  1. 单元测试:新增 6 条,覆盖判定全部分支;pnpm test 通过(node 40/40、vitest 57/57)。
  2. 手动验收:对歌词窗口执行 SetParent(hwnd, NULL),模拟「父关系被外部解除」这一失效场景:
修复前 修复后
注入后 20 秒内始终停在屏幕顶部(y=0),应用无任何反应 1 秒内自行回到任务栏
  • 连续注入 6 次,每次均在 1 秒内自愈;窗口句柄与位置尺寸保持一致(rect=(715,816) 570×48,父窗口 = Shell_TrayWnd);
  • 自愈后截图确认歌词正文 / 翻译 / 封面在任务栏中正常渲染,排除「位置正确但画面为空」的情况。
  1. 静态检查pnpm formatpnpm typecheck(node + web)、pnpm lintcargo test --workspace 均通过。
复现/验收方法(Windows,普通权限即可)

该问题依赖「窗口父关系被外部解除」这一时机,我也是使用过程偶然发生过几次,靠碰运气很难复现,因此用下面的片段主动制造该场景:找到嵌在任务栏里的歌词窗口,把它从任务栏上摘下来(SetParent(hwnd, NULL)),即可在修复前稳定复现「跳到屏幕顶部且不再恢复」,修复后应在 1 秒内自愈。

Add-Type -TypeDefinition @'
using System; using System.Text; using System.Runtime.InteropServices;
public class TbTest {
  public delegate bool E(IntPtr h, IntPtr l);
  [DllImport("user32.dll")] public static extern bool EnumWindows(E cb, IntPtr l);
  [DllImport("user32.dll")] public static extern bool EnumChildWindows(IntPtr p, E cb, IntPtr l);
  [DllImport("user32.dll")] public static extern IntPtr SetParent(IntPtr c, IntPtr p);
  [DllImport("user32.dll")] public static extern IntPtr GetAncestor(IntPtr h, uint f);
  [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr h, out RECT r);
  [DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern int GetWindowTextW(IntPtr h, StringBuilder s, int n);
  [DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern int GetClassNameW(IntPtr h, StringBuilder s, int n);
  [StructLayout(LayoutKind.Sequential)] public struct RECT { public int L,T,R,B; }
  static string Txt(IntPtr h){ var s=new StringBuilder(256); GetWindowTextW(h,s,256); return s.ToString(); }
  static string Cls(IntPtr h){ var s=new StringBuilder(256); GetClassNameW(h,s,256); return s.ToString(); }
  public static IntPtr Widget() {
    IntPtr tb = IntPtr.Zero;
    EnumWindows((h,l) => { if (Cls(h)=="Shell_TrayWnd") { tb=h; return false; } return true; }, IntPtr.Zero);
    IntPtr w = IntPtr.Zero;
    if (tb != IntPtr.Zero) EnumChildWindows(tb, (c,l) => {
      if (Cls(c)=="Chrome_WidgetWin_1" && Txt(c)=="Taskbar Lyric") { w=c; return false; } return true; }, IntPtr.Zero);
    return w;
  }
  public static string State() {
    var w = Widget(); RECT r; GetWindowRect(w, out r);
    return string.Format("hwnd={0} rect=({1},{2}) {3}x{4} parent={5}[{6}]", w, r.L, r.T, r.R-r.L, r.B-r.T, GetAncestor(w,1), Cls(GetAncestor(w,1)));
  }
  public static void Detach() { SetParent(Widget(), IntPtr.Zero); }
}
'@ -ErrorAction Stop
"注入前: $([TbTest]::State())"
[TbTest]::Detach()                     # 制造失效:父关系被解除
Start-Sleep -Seconds 3
"注入后: $([TbTest]::State())"          # 修复前会停在屏幕顶部(y=0),修复后应已回到任务栏

截图 / 录屏

taskbar-lyric-drift-to-top

自查清单

  • 本 PR 只包含一个主要功能 / 修复,没有夹带无关改动
  • 已在本地完整测试通过;AI 生成的代码同样自行测试并审阅过,未做未经验证的提交
  • 已运行 pnpm format,并确认 pnpm typecheckpnpm lint 通过
  • 改动涉及原生模块时已 pnpm build:native 验证;未手写 native/*/index.d.ts
  • 已向 dev 分支提交

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant