怀念经典,一步步开发一个ttplayer千千静听,歌词秀

image

歌词窗口(LyricWindow)完整解析

一、皮肤 XML 中的定义

LyricWindow 对应皮肤文件中的 <lyric_window> 节点。与 <main_window><mini_window> 类似,它定义了一个独立的窗口,专门用于显示滚动歌词。

1.1 典型 XML 结构

<lyric_window position="0,0,400,300" image="lyric_bg.bmp">
    <!-- 标题栏图标 -->
    <title position="8,6,24,22" image="ttplayer.ico"/>
    <!-- 关闭按钮 -->
    <close position="376,6,394,22" image="close.bmp"/>
    <!-- 歌词显示区域 -->
    <lyric position="10,32,390,290" />
</lyric_window>

1.2 关键标签说明

标签 作用 属性
<lyric_window> 根节点,定义整个歌词窗口 position(窗口位置/尺寸),image(背景图片)
<title> 窗口图标(任务栏/标题栏) position(通常负值或小区域),image(图标文件)
<close> 关闭按钮 positionimage(四帧按钮图片)
<lyric> 歌词渲染面板 position(歌词显示区域)——必须存在,否则渲染器会使用兜底布局

注意<lyric> 标签的 position 定义了歌词文本的绘制区域,LyricWindow 会将其解析为 lyricCtl,并在此区域内放置自定义渲染器 LyricRenderer。同时,该区域支持窗口缩放时的自动拉伸(通过 fill 对齐模式)。


二、LyricWindow 源码深度剖析

LyricWindow 继承自 SkinWindow,是 TTPlayer 的内置歌词显示窗口(区别于桌面悬浮歌词)。它负责解析皮肤定义的歌词面板,加载 .lrc 文件,并实时同步播放进度,实现歌词高亮滚动。

2.1 类结构概览

public class LyricWindow extends SkinWindow {
    PlayerEngine playerEngine;
    private LyricRenderer renderer;      // 自定义歌词绘制组件
    private TtSkin.Ctl lyricCtl;         // XML中<lyric>控件的定义
    private Song currentSong;
    // ...
}
  • rendererLyricRenderer 是一个自定义 JComponent(未在此文件中给出),负责绘制歌词文本、高亮当前行、逐字卡拉OK效果等。
  • lyricCtl:保存 <lyric> 标签的布局信息,用于定位渲染器。
  • currentSong:当前歌曲,用于关联歌词文件。

2.2 构造方法

public LyricWindow(TtSkin skin, PlayerEngine engine) {
    super(skin, findWindow(skin, "lyric_window"), false);
    this.playerEngine = engine;
    setTitle(Messages.get("lyric.title"));

    TtSkin.WindowDef def = findWindow(skin, "lyric_window");
    int w = (def != null && def.width > 0) ? def.width : 268;
    setMinSize(w, 150);
    setSize(w, 280);
}
  • 调用父类构造,传入 lyric_windowWindowDef
  • 设置窗口标题(从资源文件读取)。
  • 从皮肤定义中获取窗口宽度,若无效则使用默认值 268,同时设置最小尺寸(宽 w,高 150),初始高度 280

2.3 控件构建(buildControls

for (TtSkin.Ctl c : def.elements) {
    switch (c.tag) {
        case "close":
            TtButton btnClose = createButton(c);
            btnClose.addActionListener(e -> setVisible(false));
            break;
        case "title":
            createTitleImage(c);
            break;
        case "lyric":
            lyricCtl = c;
            renderer = new LyricRenderer(this);
            renderer.setBounds(c.left, c.top, c.right - c.left, c.bottom - c.top);
            getContentPane().add(renderer);
            // 将渲染器注册为可拉伸控件(fill 模式)
            TtSkin.Ctl fillCtl = new TtSkin.Ctl();
            fillCtl.left = c.left;
            fillCtl.top = c.top;
            fillCtl.right = c.right;
            fillCtl.bottom = c.bottom;
            fillCtl.align = "fill";
            addControl(renderer, fillCtl);
            break;
    }
}
loadLyricStyle();
  • 关闭按钮:点击后隐藏窗口(setVisible(false))。
  • 标题图标:调用 createTitleImage(c) 设置窗口图标。
  • 歌词渲染器
    • 创建 LyricRenderer 实例,传入 thisLyricWindow 自身)作为父容器引用(用于获取播放进度等)。
    • 设置其边界为 <lyric>position
    • 将其添加到 contentPane
    • 关键:创建一个临时的 Ctl 对象(fillCtl),复制 <lyric> 的坐标,并设置 align="fill",然后通过 addControl(renderer, fillCtl) 注册到父类的布局管理器。这样,当窗口缩放时,LyricRenderer 会随着窗口拉伸(类似于 BorderLayout.CENTER)。
  • 最后调用 loadLyricStyle() 加载歌词样式(字体、颜色等)。

2.4 歌词样式加载(loadLyricStyle

private void loadLyricStyle() {
    byte[] data = UIUtils.loadSkinXml(skin, "Lyric");
    if (data == null) return;
    Document doc = UIUtils.parseXml(data);
    Element lyricNode = (Element) doc.getDocumentElement().getElementsByTagName("Lyric").item(0);
    if (lyricNode == null) return;

    String fontSpec = UIUtils.getAttribute(lyricNode, "Font", "");
    if (renderer != null) {
        renderer.textColor = UIUtils.getColorAttribute(lyricNode, "TextColor", renderer.textColor);
        renderer.highlightColor = UIUtils.getColorAttribute(lyricNode, "HilightColor", renderer.highlightColor);
        renderer.bgColor = UIUtils.getColorAttribute(lyricNode, "BkgndColor", renderer.bgColor);
        renderer.wordColor = UIUtils.getColorAttribute(lyricNode, "HilightWordColor", renderer.wordColor);
        if (!fontSpec.isEmpty()) applyFontSpec(fontSpec);
    }
}
  • 从皮肤包中读取名为 "Lyric" 的 XML 文件(可能是 Lyric.xmlSkin.xml 中的 <Lyric> 节点)。
  • 解析其中的属性,设置 LyricRenderer 的:
    • TextColor:未高亮歌词颜色。
    • HilightColor:当前行高亮颜色。
    • BkgndColor:背景色(若渲染器支持)。
    • HilightWordColor:逐字高亮时单个字的颜色(可能用于卡拉OK效果)。
    • Font:字体规格字符串(格式类似 lfHeight, ... , fontName),通过 applyFontSpec 解析为 Font 对象并应用到渲染器。

2.5 字体规格解析(applyFontSpec

private void applyFontSpec(String fontSpec) {
    String[] parts = fontSpec.split(",");
    int lfHeight = Integer.parseInt(parts[0].trim());
    int size = Math.max(12, Math.min(28, Math.abs(lfHeight)));
    int style = Font.PLAIN;
    if (parts.length > 4 && Integer.parseInt(parts[4].trim()) >= 700) style = Font.BOLD;
    String fontName = parts.length > 13 ? parts[13].trim() : null;

    renderer.lyricFont = FontUtils.getLyricFont(fontName, style, size);
    renderer.currentFont = FontUtils.getLyricFont(fontName, Font.BOLD, size + 2);
}
  • 解析类似 Windows LOGFONT 结构的字符串(通常来自皮肤配置)。
  • 提取高度(lfHeight)并转换为字号(取绝对值,限制在 12~28 之间)。
  • 检测 lfWeight(第5个字段)是否 ≥ 700,决定是否加粗。
  • 提取字体名称(第14个字段,索引13)。
  • 分别设置 lyricFont(普通歌词)和 currentFont(当前行高亮字体,加粗且稍大 2px)。

2.6 歌词加载(loadLyrics

public void loadLyrics(Song song) {
    this.currentSong = song;
    if (renderer == null) return;
    if (song == null || song.filePath == null) {
        renderer.setLyrics(null);
        return;
    }
    File audioFile = new File(song.filePath);
    File lrcFile = LRCParser.findLRCFile(audioFile);
    if (lrcFile != null) {
        try {
            LRCParser.LRCData data = new LRCParser().parse(lrcFile);
            renderer.setLyrics(data.lines);
            renderer.adjustFontToLyrics(); // 自动适配字体
            return;
        } catch (IOException ignored) {}
    }
    renderer.setLyrics(null);
}
  • 根据歌曲路径查找同名 .lrc 文件(通过 LRCParser.findLRCFile)。
  • 若找到,解析为 LRCData,传递给渲染器。
  • 调用 adjustFontToLyrics()(类似 DesktopLyricWindow 的字体适配),确保特殊字符正常显示。
  • 若未找到歌词,则清空渲染器(显示“无歌词”提示)。

2.7 在线搜索歌词(searchLyricsOnline

public void searchLyricsOnline() {
    LyricSearchDialog dialog = new LyricSearchDialog(this, currentSong);
    dialog.setOnLyricDownloaded(() -> {
        if (currentSong != null) loadLyrics(currentSong);
    });
    dialog.setVisible(true);
}
  • 弹出 LyricSearchDialog(一个自定义搜索对话框),让用户在线搜索并下载歌词。
  • 下载完成后,通过回调重新加载歌词文件,刷新显示。

2.8 窗口重布局(repositionControls

protected void repositionControls() {
    super.repositionControls();
    if (renderer != null && lyricCtl == null) {
        // 如果皮肤未定义<lyric>,则用默认边距布局
        int margin = 6;
        int titleBottom = 28;
        renderer.setBounds(margin, titleBottom, getWidth() - margin * 2, getHeight() - titleBottom - 10);
    }
}
  • 父类 repositionControls 会依据 addControl 注册的控件和 align 属性进行布局。
  • <lyric> 未定义(皮肤异常),则手动设置渲染器位置,留出标题栏空间。

三、与 DesktopLyricWindow 的对比

特性 LyricWindow(内置歌词窗口) DesktopLyricWindow(桌面悬浮歌词)
窗口类型 普通窗口(带标题栏、边框) 透明无边框悬浮窗
背景 皮肤背景图片 + 渲染器背景色 半透明圆角黑色背景
歌词渲染 通过 LyricRenderer 绘制,支持多行滚动 单行/双行,支持逐字高亮
样式来源 皮肤 XML(Lyric.xml<Lyric> 节点) 程序内部硬编码颜色和字体
交互 可拖拽、关闭、缩放 可拖拽、右键菜单切换单/双行
使用场景 作为播放器的一个子窗口,与主窗口协同 独立浮在桌面上,始终置顶

四、总结

LyricWindow 是 TTPlayer 皮肤系统中负责歌词显示的标准窗口组件。它通过与 TtSkin 配合,解析 <lyric_window> 节点及其子控件,创建包含歌词渲染面板的窗口。歌词渲染委托给专门的 LyricRenderer(未给出完整实现,但从交互可知其支持多行滚动、高亮当前行、逐字卡拉OK效果)。同时,它还集成了在线歌词搜索功能,增强了实用性。

该类的设计遵循了 TTPlayer 的 UI 框架约定:

  • 继承 SkinWindow,复用皮肤加载和控件管理逻辑。
  • 使用 buildControls 模式化创建控件。
  • 通过 loadLyricStyle 从皮肤扩展 XML 中加载自定义样式。
  • PlayerEngine 配合,实现歌词与播放进度的同步(渲染器内部会定期查询引擎位置)。

对于希望实现类似歌词窗口的开发者,LyricWindow 提供了一个清晰的参考架构:将布局、样式、数据源和渲染逻辑分层分离,既保持了代码的整洁,又保证了高度的可定制性。

License

本项目仅供学习和个人使用。皮肤资源版权归原作者所有。

「获取方式」:后台回复 ttplayer

image

posted @ 2026-08-28 11:45  tomj2ee  阅读(4)  评论(0)    收藏  举报