怀念经典,一步步开发一个ttplayer千千静听,主播放器绘制

PlayerWindow 源码完整分析
PlayerWindow 是 TTPlayer 的主窗口类,继承自 SkinWindow。它是整个播放器的核心 UI 容器,负责承载主窗口上的所有控件(播放控制按钮、进度条、音量条、LED 时间显示、频谱可视化、信息标签等),并与 PlayerEngine 交互以实现播放控制、状态同步,同时管理子窗口(歌词窗口、均衡器窗口、播放列表窗口、迷你窗口)的显示与联动。
1. 包声明与导入
package org.ttplayer.ui;
import org.ttplayer.audio.TtVisualizer;
import org.ttplayer.controls.TtButton;
import org.ttplayer.controls.TtLed;
import org.ttplayer.controls.TtTrackBar;
import org.ttplayer.controls.TtVolumeBar;
import org.ttplayer.engine.PlayerEngine;
import org.ttplayer.skin.TtSkin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.*;
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.BufferedImage;
import org.ttplayer.util.Messages;
- 导入自定义控件:
TtButton、TtLed、TtTrackBar、TtVolumeBar、TtVisualizer。 - 导入播放引擎
PlayerEngine和皮肤TtSkin。 - 使用 SLF4J 记录日志。
- 导入 Swing 和 AWT 组件,以及
WindowAdapter用于窗口事件监听。
2. 成员变量
2.1 日志
private static final Logger log = LoggerFactory.getLogger(PlayerWindow.class);
2.2 播放控制按钮(公开字段,供外部访问)
public TtButton btnPlay, btnPause, btnStop, btnPrev, btnNext, btnMute;
public TtButton btnOpen, btnLyric, btnEqualizer, btnPlaylist, btnBrowser;
public TtButton btnMinimize, btnExit, btnMiniMode;
public TtButton btnModeSingle, btnModeLoop, btnModeSlider, btnModeCircle, btnModeRandom;
public TtButton btnSet, btnDeskLrc;
- 所有按钮均为
TtButton类型(四帧精灵图按钮)。 - 公开字段允许外部(如
Main类)访问按钮状态或触发点击。
2.3 核心 UI 组件
private TtTrackBar progressBar; // 进度条
private TtVolumeBar volumeBar; // 音量条
private TtVisualizer visualizer; // 频谱可视化
private TtLed ledLabel; // LED 时间显示
private JLabel infoLabel; // 歌曲信息标签
private JLabel statusLabel; // 状态标签(如“正在播放”)
private JLabel iconLabel; // 窗口图标标签
2.4 业务对象与状态
private final PlayerEngine playerEngine;
private boolean showRemainingTime; // true 显示剩余时间,false 显示已播放时间
private Runnable onToggleMiniMode; // 切换迷你模式回调
private Runnable onHideToTray; // 隐藏到系统托盘回调
2.5 子窗口引用
private LyricWindow lyricWindow;
private EqualizerWindow equalizerWindow;
private PlaylistWindow playlistWindow;
private MiniWindow miniWindow;
2.6 子窗口状态保存(最小化时)
private boolean[] childWindowStatesBeforeMinimize;
- 用于在主窗口最小化时保存子窗口可见性状态,还原时恢复。
3. 构造方法
public PlayerWindow(TtSkin skin, PlayerEngine playerEngine) {
super(skin, findWindow(skin), false);
this.playerEngine = playerEngine;
setTitle("TTPlayer");
TtSkin.WindowDef def = findWindow(skin);
if (def != null && def.width > 0 && def.height > 0) {
setMinSize(def.width, def.height);
} else {
setMinSize(268, 165);
}
Timer updateTimer = new Timer(200, e -> doUpdate());
updateTimer.start();
Timer visualTimer = new Timer(20, e -> refreshSpectrum());
visualTimer.start();
addWindowListener(new WindowAdapter() {
@Override
public void windowIconified(WindowEvent e) {
// 主窗口最小化时,保存子窗口状态并隐藏子窗口
if (lyricWindow != null && equalizerWindow != null && playlistWindow != null) {
childWindowStatesBeforeMinimize = new boolean[]{
lyricWindow.isVisible(),
equalizerWindow.isVisible(),
playlistWindow.isVisible()
};
lyricWindow.setVisible(false);
equalizerWindow.setVisible(false);
playlistWindow.setVisible(false);
}
}
@Override
public void windowDeiconified(WindowEvent e) {
if (childWindowStatesBeforeMinimize != null) {
if (lyricWindow != null) lyricWindow.setVisible(childWindowStatesBeforeMinimize[0]);
if (equalizerWindow != null) equalizerWindow.setVisible(childWindowStatesBeforeMinimize[1]);
if (playlistWindow != null) playlistWindow.setVisible(childWindowStatesBeforeMinimize[2]);
}
}
});
}
- 调用父类
SkinWindow构造,传入player_window的WindowDef。 - 设置窗口标题为
"TTPlayer"。 - 根据皮肤定义设置最小尺寸,若无效则使用默认
268×165。 - 启动两个定时器:
updateTimer:每 200ms 调用doUpdate(),更新播放状态(时间、进度、按钮显隐等)。visualTimer:每 20ms(约 50fps)调用refreshSpectrum(),高频刷新频谱可视化。
- 窗口监听器:
windowIconified:主窗口最小化时,保存歌词、均衡器、播放列表窗口的可见性状态,然后隐藏它们(避免子窗口悬浮在桌面上)。windowDeiconified:主窗口还原时,恢复之前保存的子窗口可见性状态。
findWindow
static TtSkin.WindowDef findWindow(TtSkin skin) {
return LyricWindow.findWindow(skin, "player_window");
}
- 委托给
LyricWindow.findWindow,从skin.getWindows()中查找player_window的定义。
4. 子窗口设置与获取
public void setChildWindows(LyricWindow lyric, EqualizerWindow eq, PlaylistWindow playlist) {
this.lyricWindow = lyric;
this.equalizerWindow = eq;
this.playlistWindow = playlist;
}
public void setMiniWindow(MiniWindow miniWindow) {
this.miniWindow = miniWindow;
}
public LyricWindow getLyricWindow() { return lyricWindow; }
public EqualizerWindow getEqualizerWindow() { return equalizerWindow; }
public PlaylistWindow getPlaylistWindow() { return playlistWindow; }
public MiniWindow getMiniWindow() { return miniWindow; }
- 由外部(如
Main类)在创建子窗口后注入引用。 - Getter 方法供外部获取子窗口引用。
5. buildControls() —— 控件构建核心
buildControls() 遍历 def.elements,根据每个 Ctl 的 tag 创建对应的 UI 组件。
5.1 播放控制按钮
case "play": btnPlay = createButton(c); tip(btnPlay, Messages.get("tooltip.play")); break;
case "pause": btnPause = createButton(c); tip(btnPause, Messages.get("tooltip.pause")); break;
case "stop": btnStop = createButton(c); tip(btnStop, Messages.get("tooltip.stop")); break;
case "prev": btnPrev = createButton(c); tip(btnPrev, Messages.get("tooltip.prev")); break;
case "next": btnNext = createButton(c); tip(btnNext, Messages.get("tooltip.next")); break;
case "mute": btnMute = createButton(c); tip(btnMute, Messages.get("tooltip.mute")); break;
- 使用
createButton(c)从皮肤加载图片并创建TtButton实例。 tip()方法设置工具提示文本(国际化)。
5.2 功能按钮
case "set": btnSet = createButton(c); tip(btnSet, Messages.get("tooltip.settings")); break;
case "open": btnOpen = createButton(c); tip(btnOpen, Messages.get("tooltip.openFile")); break;
case "lyric": btnLyric = createButton(c); tip(btnLyric, Messages.get("menu.lyricShow")); break;
case "equalizer": btnEqualizer = createButton(c); tip(btnEqualizer, Messages.get("menu.equalizer")); break;
case "eq": btnEqualizer = createButton(c); tip(btnEqualizer, Messages.get("menu.equalizer")); break;
case "playlist": btnPlaylist = createButton(c); tip(btnPlaylist, Messages.get("playlist.title")); break;
case "browser": btnBrowser = createButton(c); tip(btnBrowser, Messages.get("tooltip.browser")); break;
case "desklrc_bar": case "desklrc": btnDeskLrc = createButton(c); tip(btnDeskLrc, Messages.get("menu.desktopLyric")); break;
case "minimize": btnMinimize = createButton(c); tip(btnMinimize, Messages.get("tooltip.minimize")); break;
case "exit": btnExit = createButton(c); tip(btnExit, Messages.get("tooltip.quit")); break;
case "close": btnExit = createButton(c); tip(btnExit, Messages.get("tooltip.quit")); break;
equalizer和eq两个标签都映射到btnEqualizer(兼容不同皮肤命名习惯)。exit和close都映射到btnExit。
5.3 播放模式按钮
case "mode_single": btnModeSingle = createButton(c); tip(btnModeSingle, Messages.get("mode.single")); break;
case "mode_loop": btnModeLoop = createButton(c); tip(btnModeLoop, Messages.get("mode.listLoop")); break;
case "mode_slider": btnModeSlider = createButton(c); tip(btnModeSlider, Messages.get("mode.sequential")); break;
case "mode_circle": btnModeCircle = createButton(c); tip(btnModeCircle, Messages.get("mode.singleLoop")); break;
case "mode_random": btnModeRandom = createButton(c); tip(btnModeRandom, Messages.get("mode.random")); break;
- 五种播放模式:单曲、列表循环、顺序播放、单曲循环、随机播放。
- 每个按钮对应一种模式,点击时由外部逻辑(
PlaylistManager或Main类)处理模式切换。
5.4 迷你模式按钮
case "minimode": case "mini": case "miniMode": case "mini_mode":
btnMiniMode = createButton(c);
tip(btnMiniMode, Messages.get("menu.miniMode"));
break;
case "normal": case "normalmode": case "defaultmode": case "default":
btnMiniMode = createButton(c);
tip(btnMiniMode, Messages.get("mode.default"));
break;
minimode等标签创建迷你模式切换按钮。normal等标签也映射到btnMiniMode(用于从迷你模式返回时)。
5.5 皮肤、选项、关于按钮
case "skin":
TtButton btnSkin = createButton(c);
tip(btnSkin, Messages.get("tooltip.skin"));
btnSkin.addActionListener(e -> {
if (playlistWindow != null) playlistWindow.doSkin();
});
break;
case "options": case "option":
TtButton btnOptions = createButton(c);
tip(btnOptions, Messages.get("menu.options"));
btnOptions.addActionListener(e -> {
if (playlistWindow != null) playlistWindow.doOptions();
});
break;
case "about":
TtButton btnAbout = createButton(c);
tip(btnAbout, Messages.get("tooltip.about"));
btnAbout.addActionListener(e -> {
if (playlistWindow != null) playlistWindow.doAbout();
});
break;
- 这些按钮的点击事件委托给
PlaylistWindow的对应方法(doSkin、doOptions、doAbout)。
5.6 图标(icon)
case "icon": createIcon(c); break;
- 调用
createIcon(c)加载窗口图标(支持 BMP、PNG、ICO 格式,有降级机制)。
5.7 信息标签(info、status、stereo)
case "info":
infoLabel = createInfo(c);
infoLabel.setText(Messages.get("app.name"));
break;
case "status":
statusLabel = createInfo(c);
statusLabel.setText(Messages.get("status.stopped"));
break;
case "stereo":
JLabel stereoLabel = createInfo(c);
stereoLabel.setText(Messages.get("player.stereo"));
break;
createInfo(c)从皮肤配置中读取字体、颜色、对齐方式等,创建JLabel。infoLabel显示歌曲信息,初始显示应用名称。statusLabel显示播放状态,初始显示“已停止”。stereoLabel显示声道信息(固定为“立体声”)。
5.8 LED 时间显示(led)
case "led":
ledLabel = createLed(c);
if (ledLabel != null) {
ledLabel.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
showRemainingTime = !showRemainingTime;
}
});
}
break;
- 创建
TtLed组件,用于显示播放时间。 - 添加鼠标点击监听器:点击 LED 区域时切换
showRemainingTime状态(已播放时间 ↔ 剩余时间)。
5.9 频谱可视化(visual)
case "visual": createVisualizer(c); break;
- 调用
createVisualizer(c)创建TtVisualizer组件,并应用皮肤配色。 - 点击频谱区域可循环切换显示模式(由
TtVisualizer.cycleMode()实现)。
5.10 进度条(progress)
case "progress": createProgressBar(c); break;
- 调用
createProgressBar(c)创建TtTrackBar。
5.11 音量条(volume)
case "volume": createVolumeBar(c); break;
- 调用
createVolumeBar(c)创建TtVolumeBar。
5.12 按钮事件绑定(构造完成后)
if (btnPlay != null) {
btnPlay.setVisible(true);
btnPlay.addActionListener(e -> { if (playerEngine != null) playerEngine.playPause(); });
}
if (btnPause != null) {
btnPause.setVisible(false);
btnPause.addActionListener(e -> { if (playerEngine != null) playerEngine.playPause(); });
}
if (btnStop != null) {
btnStop.addActionListener(e -> {
if (playerEngine != null) playerEngine.stop();
if (ledLabel != null) ledLabel.setText("00:00");
if (progressBar != null) progressBar.setValue(0);
});
}
if (btnPrev != null) btnPrev.addActionListener(e -> { if (playerEngine != null) playerEngine.previous(); });
if (btnNext != null) btnNext.addActionListener(e -> { if (playerEngine != null) playerEngine.next(); });
if (btnMute != null) {
btnMute.setSelected(playerEngine != null && playerEngine.isMuted());
btnMute.addActionListener(e -> {
if (playerEngine == null) return;
playerEngine.mute();
btnMute.setSelected(playerEngine.isMuted());
if (miniWindow != null) miniWindow.setMuted(playerEngine.isMuted());
});
}
if (btnExit != null) btnExit.addActionListener(e -> { hideAllWindowsToTray(); });
if (btnMinimize != null) btnMinimize.addActionListener(e -> { hideAllWindowsToTray(); });
if (btnMiniMode != null) btnMiniMode.addActionListener(e -> toggleMiniMode());
- 播放/暂停:
btnPlay和btnPause初始一个可见一个不可见,点击都调用playerEngine.playPause()。 - 停止:调用
playerEngine.stop(),同时重置 LED 和进度条。 - 上一首/下一首:调用
playerEngine.previous()/playerEngine.next()。 - 静音:切换静音状态,同步到迷你窗口。
- 退出/最小化:调用
hideAllWindowsToTray()(隐藏所有窗口到系统托盘)。 - 迷你模式:调用
toggleMiniMode()。
5.13 其他初始化
if (ledLabel != null) {
getContentPane().setComponentZOrder(ledLabel, 0);
}
- 将 LED 时间显示置于最上层(Z-order 0)。
addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
if (e.isPopupTrigger()) showPlayerRightClickMenu(e);
}
@Override
public void mouseReleased(MouseEvent e) {
if (e.isPopupTrigger()) showPlayerRightClickMenu(e);
}
});
- 为主窗口本身添加右键菜单监听,显示播放列表的右键菜单(
playlistWindow.createRightClickMenu())。
6. 进度条创建(createProgressBar)
private void createProgressBar(TtSkin.Ctl ctl) {
byte[] thumbData = skin.getBmp(ctl.thumbImage);
progressBar = new TtTrackBar(thumbData);
progressBar.setBounds(ctl.left, ctl.top, ctl.right - ctl.left, ctl.bottom - ctl.top);
getContentPane().add(progressBar);
addControl(progressBar, ctl);
progressBar.setTrackListener(seconds -> {
if (playerEngine != null) playerEngine.seekTo(seconds);
});
}
- 从皮肤加载滑块图片(
thumbImage)创建TtTrackBar。 - 设置位置和大小,添加到内容面板并注册到控件列表(支持窗口缩放时重新定位)。
- 设置
TrackListener:用户拖动进度条时调用playerEngine.seekTo(seconds)。
7. 音量条创建(createVolumeBar)
private void createVolumeBar(TtSkin.Ctl ctl) {
byte[] fillData = skin.getBmp(ctl.fillImage);
byte[] thumbData = skin.getBmp(ctl.thumbImage);
volumeBar = new TtVolumeBar(fillData, thumbData);
volumeBar.setBounds(ctl.left, ctl.top, ctl.right - ctl.left, ctl.bottom - ctl.top);
getContentPane().add(volumeBar);
addControl(volumeBar, ctl);
if (playerEngine != null) volumeBar.setValue(playerEngine.getVolumePercent());
volumeBar.setVolumeListener(percent -> {
if (playerEngine != null) playerEngine.setGainFromPercent(percent);
if (miniWindow != null) miniWindow.setVolume(percent);
});
}
- 从皮肤加载填充图片(
fillImage)和滑块图片(thumbImage)创建TtVolumeBar。 - 初始化音量值为引擎当前音量。
- 设置
VolumeListener:用户拖动音量条时更新引擎音量,并同步到迷你窗口。
setVolumeFromPercent
public void setVolumeFromPercent(int percent) {
int v = Math.max(0, Math.min(100, percent));
if (volumeBar != null) volumeBar.setValue(v);
if (playerEngine != null) playerEngine.setGainFromPercent(v);
if (miniWindow != null) miniWindow.setVolume(v);
}
- 外部(如迷你窗口滚轮事件)调用此方法调整音量,同步更新音量条、引擎和迷你窗口。
8. 频谱可视化创建(createVisualizer)
private void createVisualizer(TtSkin.Ctl ctl) {
visualizer = new TtVisualizer();
visualizer.setBounds(ctl.left, ctl.top, ctl.right - ctl.left, ctl.bottom - ctl.top);
loadVisualizerSkinStyle();
visualizer.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
if (SwingUtilities.isLeftMouseButton(e) && e.getClickCount() == 1) {
visualizer.cycleMode();
}
}
});
getContentPane().add(visualizer);
addControl(visualizer, ctl);
}
- 创建
TtVisualizer实例。 - 调用
loadVisualizerSkinStyle()从皮肤 XML 加载配色。 - 单击频谱区域切换显示模式(
cycleMode())。 - 添加到内容面板并注册。
loadVisualizerSkinStyle
void loadVisualizerSkinStyle() {
if (visualizer == null) return;
byte[] data = UIUtils.loadSkinXml(skin, "Visual");
if (data == null) return;
try {
Document doc = UIUtils.parseXml(data);
Element visEl = doc.getDocumentElement();
Element vis = (Element) visEl.getElementsByTagName("Visual").item(0);
if (vis == null) return;
visualizer.skinColorTop = UIUtils.getColorAttribute(vis, "SpectrumTopColor", visualizer.skinColorTop);
visualizer.skinColorMid = UIUtils.getColorAttribute(vis, "SpectrumMidColor", visualizer.skinColorMid);
visualizer.skinColorBtm = UIUtils.getColorAttribute(vis, "SpectrumBtmColor", visualizer.skinColorBtm);
visualizer.skinColorPeak = UIUtils.getColorAttribute(vis, "SpectrumPeakColor", visualizer.skinColorPeak);
visualizer.skinColorBlur = UIUtils.getColorAttribute(vis, "BlurScopeColor", visualizer.skinColorBlur);
} catch (Exception ignored) {}
}
- 从皮肤包中加载
Visual.xml或相应节点,提取颜色属性并设置到TtVisualizer的配色字段。
9. 图标创建(createIcon)
createIcon 负责加载窗口图标,支持多种图片格式并有降级机制。
9.1 主流程
private void createIcon(TtSkin.Ctl ctl) {
iconLabel = new JLabel();
iconLabel.setBounds(ctl.left, ctl.top, ctl.right - ctl.left, ctl.bottom - ctl.top);
if (ctl.image != null && !ctl.image.isEmpty()) {
byte[] imageData = skin.getBmp(ctl.image);
if (imageData != null) {
try {
ImageIcon icon = null;
String lowerImage = ctl.image.toLowerCase();
// 先尝试用 ImageIO 通用方法加载
BufferedImage img = ImageIO.read(new ByteArrayInputStream(imageData));
if (img != null) {
int w = ctl.right - ctl.left;
int h = ctl.bottom - ctl.top;
Image scaled = img.getScaledInstance(w, h, Image.SCALE_SMOOTH);
icon = new ImageIcon(scaled);
}
if (icon == null) {
if (lowerImage.endsWith(".ico")) {
icon = loadIco(imageData);
} else {
img = SkinWindow.decodeBmp(imageData, getSkinTransparentColor());
if (img != null) {
int w = ctl.right - ctl.left;
int h = ctl.bottom - ctl.top;
Image scaled = img.getScaledInstance(w, h, Image.SCALE_SMOOTH);
icon = new ImageIcon(scaled);
}
}
}
if (icon != null) {
iconLabel.setIcon(icon);
} else {
tryLoadFallbackIcon(ctl);
}
} catch (Exception e) {
log.error("Error loading icon: {}", e.getMessage(), e);
tryLoadFallbackIcon(ctl);
}
} else {
tryLoadFallbackIcon(ctl);
}
} else {
tryLoadFallbackIcon(ctl);
}
getContentPane().add(iconLabel);
addControl(iconLabel, ctl);
}
- 创建
JLabel作为图标容器。 - 尝试按顺序加载图标:
- 使用
ImageIO.read()通用方法(支持 PNG、JPEG 等)。 - 若失败且文件为
.ico,调用loadIco()专门加载。 - 否则使用
SkinWindow.decodeBmp()解码 BMP。
- 使用
- 若所有方法均失败,调用
tryLoadFallbackIcon()加载内置兜底图标。
9.2 loadIco
private ImageIcon loadIco(byte[] icoData) {
try {
ImageIO.setUseCache(false);
ByteArrayInputStream bais = new ByteArrayInputStream(icoData);
BufferedImage img = ImageIO.read(bais);
if (img != null) {
int w = iconLabel.getWidth();
int h = iconLabel.getHeight();
Image scaled = img.getScaledInstance(w, h, Image.SCALE_SMOOTH);
return new ImageIcon(scaled);
}
bais.reset();
Iterator<ImageReader> readers = ImageIO.getImageReadersByFormatName("ico");
if (readers.hasNext()) {
ImageReader reader = readers.next();
reader.setInput(ImageIO.createImageInputStream(bais));
int count = reader.getNumImages(true);
if (count > 0) {
img = reader.read(0);
int w = iconLabel.getWidth();
int h = iconLabel.getHeight();
Image scaled = img.getScaledInstance(w, h, Image.SCALE_SMOOTH);
return new ImageIcon(scaled);
}
}
} catch (Exception e) {
log.error("Error loading ICO file: {}", e.getMessage(), e);
}
return null;
}
- 先尝试用
ImageIO.read()直接读取 ICO。 - 若失败,通过 ICO 格式的
ImageReader读取,并取第一个图标。
9.3 tryLoadFallbackIcon
private void tryLoadFallbackIcon(TtSkin.Ctl ctl) {
try {
ClassLoader cl = getClass().getClassLoader();
InputStream is = cl.getResourceAsStream("ico/ttplayer_16x16_32bpp.png");
if (is != null) {
BufferedImage img = ImageIO.read(is);
if (img != null) {
Image scaled = img.getScaledInstance(img.getWidth(), img.getHeight(), Image.SCALE_SMOOTH);
iconLabel.setIcon(new ImageIcon(scaled));
}
}
} catch (Exception e) {
log.error("Could not load fallback PNG icon: {}", e.getMessage(), e);
}
}
- 从 classpath 的
ico/目录加载内置 PNG 图标作为兜底。
10. 定时更新(doUpdate)
private void doUpdate() {
if (playerEngine == null) return;
try {
boolean playing = playerEngine.isPlaying();
if (btnPlay != null && btnPause != null) {
btnPlay.setVisible(!playing);
btnPause.setVisible(playing);
}
if (playing) {
int pos = playerEngine.getPosition();
int dur = playerEngine.getDuration();
if (ledLabel != null) {
if (showRemainingTime && dur > 0) {
int rem = Math.max(0, dur - pos);
ledLabel.setText(String.format("-%02d:%02d", rem / 60, rem % 60));
} else {
ledLabel.setText(String.format("%02d:%02d", pos / 60, pos % 60));
}
}
if (progressBar != null && !progressBar.isDragging()) {
if (dur > 0) {
progressBar.setRange(dur);
progressBar.setValue(pos);
}
}
}
if (miniWindow != null && miniWindow.isVisible()) {
miniWindow.updatePlayState(playing);
if (playing && miniWindow.getProgressBar() != null && !miniWindow.getProgressBar().isDragging()) {
int pos = playerEngine.getPosition();
int dur = playerEngine.getDuration();
if (dur > 0) {
miniWindow.getProgressBar().setRange(dur);
miniWindow.getProgressBar().setValue(pos);
}
}
}
} catch (Exception ignored) {}
}
- 每 200ms 执行一次。
- 切换播放/暂停按钮显隐:根据
playing状态显示btnPlay或btnPause。 - 更新 LED 时间显示:
- 若
showRemainingTime为 true 且总时长 > 0,显示剩余时间(负号 + MM:SS)。 - 否则显示已播放时间(MM:SS)。
- 若
- 更新进度条:若用户未拖动进度条,则更新进度条的值。
- 同步迷你窗口:若迷你窗口可见,更新其播放状态和进度条。
11. 频谱刷新(refreshSpectrum)
private void refreshSpectrum() {
if (playerEngine == null) return;
boolean mainVisible = isVisible();
boolean miniVisible = miniWindow != null && miniWindow.isVisible();
if ((visualizer != null && mainVisible) || miniVisible) {
byte[] data = playerEngine.getAudioData();
if (visualizer != null && mainVisible) {
visualizer.updateData(data);
}
if (miniVisible) {
miniWindow.updateVisualizer(data);
}
}
}
- 每 20ms 执行一次(约 50fps)。
- 从
playerEngine.getAudioData()获取最新的 PCM 样本数据。 - 若主窗口可见且有频谱组件,更新主窗口频谱。
- 若迷你窗口可见,更新迷你窗口频谱。
12. 迷你模式切换(toggleMiniMode)
private void toggleMiniMode() {
if (onToggleMiniMode != null) {
onToggleMiniMode.run();
} else {
if (miniWindow == null) {
miniWindow = new MiniWindow(skin, playerEngine);
miniWindow.setOnExitMiniMode(this::exitMiniMode);
miniWindow.setOnToggleLyric(this::toggleLyric);
updateMiniModeDisplay();
}
if (playerEngine != null) {
miniWindow.updatePlayState(playerEngine.isPlaying());
}
miniWindow.setLocation(getX() + getWidth() / 2 - miniWindow.getWidth() / 2,
getY() + getHeight() / 2 - miniWindow.getHeight() / 2);
miniWindow.setVisible(true);
this.setVisible(false);
}
}
- 若外部设置了
onToggleMiniMode回调,则调用它(由Main类处理)。 - 否则使用内部逻辑:
- 若
miniWindow为 null,创建并设置回调。 - 同步播放状态到迷你窗口。
- 将迷你窗口定位到主窗口中心位置。
- 显示迷你窗口,隐藏主窗口。
- 若
exitMiniMode
private void exitMiniMode() {
this.setVisible(true);
if (miniWindow != null) {
miniWindow.setVisible(false);
}
}
- 从迷你模式返回:显示主窗口,隐藏迷你窗口。
toggleLyric
private void toggleLyric() {
if (btnLyric != null) {
btnLyric.doClick();
}
}
- 模拟点击歌词按钮(用于迷你窗口的歌词切换回调)。
updateMiniModeDisplay
public void updateMiniModeDisplay() {
if (miniWindow != null && infoLabel != null) {
miniWindow.setDisplayText(infoLabel.getText());
}
}
- 将主窗口的歌曲信息同步到迷你窗口。
13. 隐藏所有窗口到系统托盘(hideAllWindowsToTray)
private void hideAllWindowsToTray() {
if (onHideToTray != null) {
onHideToTray.run();
} else {
this.setVisible(false);
if (lyricWindow != null) lyricWindow.setVisible(false);
if (equalizerWindow != null) equalizerWindow.setVisible(false);
if (playlistWindow != null) playlistWindow.setVisible(false);
if (miniWindow != null) miniWindow.setVisible(false);
}
}
- 若外部设置了
onHideToTray回调,则调用它(由Main类处理,通常用于最小化到系统托盘)。 - 否则隐藏所有窗口(主窗口 + 所有子窗口)。
14. 其他公开方法
setSongInfo
public void setSongInfo(String title){
if (infoLabel != null) infoLabel.setText(title != null ? title : "");
updateMiniModeDisplay();
}
- 设置歌曲信息标签,并同步到迷你窗口。
setStatusInfo
public void setStatusInfo(String status) {
if (statusLabel != null) statusLabel.setText(status);
}
- 设置状态标签(如“正在播放”、“已暂停”等)。
setPlaylistWindow
public void setPlaylistWindow(PlaylistWindow plWindow) {
this.playlistWindow = plWindow;
}
- 设置播放列表窗口引用(用于右键菜单等操作)。
setOnToggleMiniMode / setOnHideToTray
public void setOnToggleMiniMode(Runnable listener) {
this.onToggleMiniMode = listener;
}
public void setOnHideToTray(Runnable listener) {
this.onHideToTray = listener;
}
- 设置回调监听器,由外部(
Main类)注入。
showPlayerRightClickMenu
private void showPlayerRightClickMenu(MouseEvent e) {
if (playlistWindow == null) return;
JPopupMenu menu = playlistWindow.createRightClickMenu();
menu.show(e.getComponent(), e.getX(), e.getY());
}
- 显示主窗口右键菜单(复用
PlaylistWindow.createRightClickMenu())。
15. 总结
PlayerWindow 是 TTPlayer 的顶层 UI 容器,其职责包括:
- 皮肤化 UI 构建:通过
buildControls()遍历皮肤定义创建所有控件。 - 播放控制:所有按钮事件绑定到
PlayerEngine的对应方法。 - 状态同步:通过
doUpdate()定时更新 LED 时间、进度条、播放/暂停按钮显隐。 - 频谱可视化:通过
refreshSpectrum()高频刷新频谱数据。 - 子窗口管理:管理歌词窗口、均衡器窗口、播放列表窗口、迷你窗口的创建、显示、隐藏和状态保存。
- 迷你模式切换:支持切换到迷你窗口并返回。
- 图标加载:支持多种图片格式,有完善的降级机制。
- 窗口事件处理:最小化时自动隐藏子窗口,还原时恢复。
整个类与 SkinWindow、PlayerEngine、TtSkin 以及各子窗口紧密协作,构成了 TTPlayer 主界面的完整实现。
License
本项目仅供学习和个人使用。皮肤资源版权归原作者所有。
「获取方式」:后台回复 ttplayer


浙公网安备 33010602011771号