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

image

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;
  • 导入自定义控件:TtButtonTtLedTtTrackBarTtVolumeBarTtVisualizer
  • 导入播放引擎 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_windowWindowDef
  • 设置窗口标题为 "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,根据每个 Ctltag 创建对应的 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;
  • equalizereq 两个标签都映射到 btnEqualizer(兼容不同皮肤命名习惯)。
  • exitclose 都映射到 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;
  • 五种播放模式:单曲、列表循环、顺序播放、单曲循环、随机播放。
  • 每个按钮对应一种模式,点击时由外部逻辑(PlaylistManagerMain 类)处理模式切换。

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 的对应方法(doSkindoOptionsdoAbout)。

5.6 图标(icon

case "icon":        createIcon(c); break;
  • 调用 createIcon(c) 加载窗口图标(支持 BMP、PNG、ICO 格式,有降级机制)。

5.7 信息标签(infostatusstereo

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());
  • 播放/暂停btnPlaybtnPause 初始一个可见一个不可见,点击都调用 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 作为图标容器。
  • 尝试按顺序加载图标:
    1. 使用 ImageIO.read() 通用方法(支持 PNG、JPEG 等)。
    2. 若失败且文件为 .ico,调用 loadIco() 专门加载。
    3. 否则使用 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 状态显示 btnPlaybtnPause
  • 更新 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 类处理)。
  • 否则使用内部逻辑:
    1. miniWindow 为 null,创建并设置回调。
    2. 同步播放状态到迷你窗口。
    3. 将迷你窗口定位到主窗口中心位置。
    4. 显示迷你窗口,隐藏主窗口。

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 容器,其职责包括:

  1. 皮肤化 UI 构建:通过 buildControls() 遍历皮肤定义创建所有控件。
  2. 播放控制:所有按钮事件绑定到 PlayerEngine 的对应方法。
  3. 状态同步:通过 doUpdate() 定时更新 LED 时间、进度条、播放/暂停按钮显隐。
  4. 频谱可视化:通过 refreshSpectrum() 高频刷新频谱数据。
  5. 子窗口管理:管理歌词窗口、均衡器窗口、播放列表窗口、迷你窗口的创建、显示、隐藏和状态保存。
  6. 迷你模式切换:支持切换到迷你窗口并返回。
  7. 图标加载:支持多种图片格式,有完善的降级机制。
  8. 窗口事件处理:最小化时自动隐藏子窗口,还原时恢复。

整个类与 SkinWindowPlayerEngineTtSkin 以及各子窗口紧密协作,构成了 TTPlayer 主界面的完整实现。

License

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

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

image

posted @ 2026-08-31 18:25  tomj2ee  阅读(16)  评论(0)    收藏  举报