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

歌词窗口(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> |
关闭按钮 | position,image(四帧按钮图片) |
<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;
// ...
}
renderer:LyricRenderer是一个自定义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_window的WindowDef。 - 设置窗口标题(从资源文件读取)。
- 从皮肤定义中获取窗口宽度,若无效则使用默认值
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实例,传入this(LyricWindow自身)作为父容器引用(用于获取播放进度等)。 - 设置其边界为
<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.xml或Skin.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


浙公网安备 33010602011771号