从零构建现代化Python音频播放器:ttk深度应用与皮肤系统设计

. 引言:为什么选择Python开发现代化播放器?

在数字媒体时代,音频播放器已成为我们日常生活中不可或缺的工具。然而,市场上大多数播放器要么功能臃肿,要么界面陈旧,要么缺乏个性化定制能力。作为一名Python开发者,能否可以用Python打造一款既美观又实用,还能自由换肤的现代化播放器?

1.1 市场现状与痛点分析

当前播放器市场存在几个明显痛点:

痛点 表现 用户影响
界面同质化严重 千篇一律的UI设计 审美疲劳,缺乏个性化
定制化程度低 无法按喜好调整界面 用户体验单一
资源占用过高 功能臃肿,启动缓慢 系统负担重
跨平台兼容差 不同系统需不同版本 使用不便

1.2 技术选型:Python + ttk的优势

为什么选择Python和ttk?

  • 开发效率:Python简洁的语法大幅提升开发效率

  • 生态丰富:丰富的第三方库支持音频处理、可视化等

  • 跨平台:一次编写,多平台运行

  • ttk优势:Themed Tkinter提供现代化控件和主题支持

  • 学习价值:GUI编程是Python开发者必备技能

项目核心技术栈:

  • 界面框架:tkinter/ttk

  • 音频处理:Pygame + Pydub

  • 图像处理:Pillow

  • 皮肤系统:JSON配置 + CSS-like样式

  • 可视化:Canvas + 自定义渲染

2. 系统架构设计

2.1 整体架构概览

复制代码

2.2 分层系统架构设计

2.3 皮肤系统深度解析

2.3.1 皮肤引擎架构
2.3.2 皮肤配置文件结构
bash 复制代码
# skin.json 配置文件结构示例
{
  "metadata": {
    "name": "Dark Modern",
    "author": "Modern Player Team",
    "version": "1.0.0",
    "description": "现代化暗色主题",
    "compatible_version": "1.0.0+"
  },
  "colors": {
    "primary": "#1DB954",
    "secondary": "#535353",
    "background": "#121212",
    "surface": "#181818",
    "card": "#282828",
    "text_primary": "#FFFFFF",
    "text_secondary": "#B3B3B3"
  },
  "fonts": {
    "title": {
      "family": "Segoe UI",
      "size": 16,
      "weight": "bold"
    },
    "subtitle": {
      "family": "Segoe UI", 
      "size": 14,
      "weight": "normal"
    }
  },
  "sizes": {
    "border_radius": 8,
    "padding_small": 4,
    "padding_medium": 8,
    "padding_large": 12,
    "icon_size": 24,
    "control_height": 40
  }
}
2. 3.3 皮肤引擎核心实现
python 复制代码
class SkinEngine:
    """皮肤引擎核心类"""
    
    def __init__(self, root: tk.Tk):
        self.root = root
        self.style = ttk.Style()
        self.current_skin = "default"
        self.skins: Dict[str, SkinConfig] = {}
        self.image_cache: Dict[str, ImageTk.PhotoImage] = {}
        
        # 加载内置皮肤
        self._load_builtin_skins()
    
    def apply_skin(self, skin_name: str):
        """应用皮肤"""
        if skin_name not in self.skins:
            print(f"皮肤 '{skin_name}' 不存在,使用默认皮肤")
            skin_name = "dark"
        
        self.current_skin = skin_name
        skin = self.skins[skin_name]
        
        # 创建ttk样式
        self._create_ttk_styles(skin)
        
        # 配置窗口背景
        self.root.configure(bg=skin.colors["background"])
        
        print(f"应用皮肤: {skin.name}")
    
    def _create_ttk_styles(self, skin: SkinConfig):
        """创建ttk样式"""
        colors = skin.colors
        sizes = skin.sizes
        
        # 创建现代主题
        self.style.theme_create("modern", parent="clam")
        self.style.theme_use("modern")
        
        # 配置基本样式
        self.style.configure(
            ".",
            background=colors["background"],
            foreground=colors["text_primary"],
            fieldbackground=colors["surface"],
            troughcolor=colors["surface"],
            selectbackground=colors["primary"],
            selectforeground=colors["text_primary"],
            borderwidth=0,
            focusthickness=0,
            relief="flat"
        )

3. 核心技术实现解析

3.1 主窗口架构与布局管理

3.2 现代化控件设计与实现

3.2.1 圆角按钮与卡片设计
python 复制代码
"""
现代化控件设计:圆角按钮、卡片、进度条等
"""
import tkinter as tk
from tkinter import ttk
from PIL import Image, ImageDraw, ImageTk
import math

class ModernButton(tk.Canvas):
    """现代化圆角按钮"""
    
    def __init__(self, parent, text="", command=None, width=100, height=40, 
                 radius=8, bg_color="#1DB954", fg_color="white", 
                 hover_color="#1ed760", font=("Segoe UI", 11, "bold")):
        super().__init__(parent, width=width, height=height, 
                        highlightthickness=0, bg=parent.cget("bg"))
        
        self.text = text
        self.command = command
        self.radius = radius
        self.bg_color = bg_color
        self.fg_color = fg_color
        self.hover_color = hover_color
        self.font = font
        self.is_hovered = False
        
        # 创建状态图片
        self.normal_image = self._create_button_image(bg_color)
        self.hover_image = self._create_button_image(hover_color)
        
        # 显示按钮
        self.image_id = self.create_image(width//2, height//2, 
                                         image=self.normal_image)
        self.text_id = self.create_text(width//2, height//2, 
                                       text=text, fill=fg_color, font=font)
        
        # 绑定事件
        self.bind("<Enter>", self._on_enter)
        self.bind("<Leave>", self._on_leave)
        self.bind("<Button-1>", self._on_click)
        self.bind("<ButtonRelease-1>", self._on_release)
    
    def _create_button_image(self, color):
        """创建按钮背景图片"""
        image = Image.new('RGBA', (self.winfo_reqwidth(), 
                                 self.winfo_reqheight()), (0,0,0,0))
        draw = ImageDraw.Draw(image)
        
        # 绘制圆角矩形
        draw.rounded_rectangle(
            [(0, 0), (self.winfo_reqwidth()-1, self.winfo_reqheight()-1)],
            radius=self.radius,
            fill=color
        )
        
        return ImageTk.PhotoImage(image)
    
    def _on_enter(self, event):
        """鼠标进入"""
        self.is_hovered = True
        self.itemconfig(self.image_id, image=self.hover_image)
        self.config(cursor="hand2")
    
    def _on_leave(self, event):
        """鼠标离开"""
        self.is_hovered = False
        self.itemconfig(self.image_id, image=self.normal_image)
        self.config(cursor="")
    
    def _on_click(self, event):
        """鼠标点击"""
        # 添加点击效果
        self.move(self.text_id, 1, 1)
    
    def _on_release(self, event):
        """鼠标释放"""
        self.move(self.text_id, -1, -1)
        if self.command:
            self.command()
    
    def set_text(self, text):
        """设置按钮文字"""
        self.text = text
        self.itemconfig(self.text_id, text=text)
    
    def set_colors(self, bg_color, fg_color, hover_color):
        """设置按钮颜色"""
        self.bg_color = bg_color
        self.fg_color = fg_color
        self.hover_color = hover_color
        
        self.normal_image = self._create_button_image(bg_color)
        self.hover_image = self._create_button_image(hover_color)
        
        self.itemconfig(self.image_id, image=self.normal_image)
        self.itemconfig(self.text_id, fill=fg_color)

class CardFrame(ttk.Frame):
    """现代化卡片框架"""
    
    def __init__(self, parent, padding=12, radius=12, elevation=2, **kwargs):
        # 创建底层Canvas实现阴影效果
        shadow_offset = elevation
        self.canvas = tk.Canvas(parent, highlightthickness=0, bg=parent.cget("bg"))
        self.canvas.pack(fill=tk.BOTH, expand=True)
        
        # 创建主框架
        super().__init__(self.canvas, **kwargs)
        self.parent = parent
        self.padding = padding
        self.radius = radius
        self.elevation = elevation
        
        # 绘制阴影
        self.shadow_id = None
        self.card_id = None
        
        # 放置内容框架
        self.content_frame = ttk.Frame(self, style="Card.TFrame")
        self.content_frame.pack(fill=tk.BOTH, expand=True, padx=padding, pady=padding)
        
        # 初始绘制
        self._draw_card()
        
        # 绑定大小变化事件
        self.bind("<Configure>", self._on_configure)
    
    def _draw_card(self):
        """绘制卡片和阴影"""
        width = self.winfo_reqwidth()
        height = self.winfo_reqheight()
        
        if width <= 1 or height <= 1:
            return
        
        # 清除之前的绘制
        if self.shadow_id:
            self.canvas.delete(self.shadow_id)
        if self.card_id:
            self.canvas.delete(self.card_id)
        
        # 获取主题颜色
        style = ttk.Style()
        bg_color = style.lookup("Card.TFrame", "background")
        if not bg_color:
            bg_color = "#FFFFFF"
        
        # 绘制阴影
        shadow_color = self._adjust_color(bg_color, -20)
        self.shadow_id = self._draw_rounded_rect(
            self.canvas, 
            self.elevation, self.elevation, 
            width - self.elevation*2, height - self.elevation*2,
            self.radius, shadow_color
        )
        
        # 绘制卡片
        self.card_id = self._draw_rounded_rect(
            self.canvas, 
            0, 0, 
            width - self.elevation*2, height - self.elevation*2,
            self.radius, bg_color
        )
        
        # 将内容框架置于卡片上方
        self.canvas.create_window(
            self.elevation, self.elevation,
            window=self, anchor="nw",
            width=width-self.elevation*2, height=height-self.elevation*2
        )
    
    def _draw_rounded_rect(self, canvas, x, y, width, height, radius, color):
        """绘制圆角矩形"""
        points = []
        
        # 四个角
        points.extend([
            x + radius, y,
            x + width - radius, y,
            x + width, y + radius,
            x + width, y + height - radius,
            x + width - radius, y + height,
            x + radius, y + height,
            x, y + height - radius,
            x, y + radius
        ])
        
        return canvas.create_polygon(points, fill=color, smooth=True, outline="")
    
    def _adjust_color(self, color, amount):
        """调整颜色亮度"""
        if color.startswith('#'):
            r = int(color[1:3], 16)
            g = int(color[3:5], 16)
            b = int(color[5:7], 16)
            
            r = max(0, min(255, r + amount))
            g = max(0, min(255, g + amount))
            b = max(0, min(255, b + amount))
            
            return f'#{r:02x}{g:02x}{b:02x}'
        return color
    
    def _on_configure(self, event):
        """大小变化时重绘"""
        self._draw_card()
    
    def get_content_frame(self):
        """获取内容框架"""
        return self.content_frame
3.2.2 自定义进度条与滑块
python 复制代码
class ModernProgressBar(tk.Canvas):
    """现代化进度条"""
    
    def __init__(self, parent, width=300, height=4, 
                 progress_color="#1DB954", bg_color="#E0E0E0",
                 show_handle=False, **kwargs):
        super().__init__(parent, width=width, height=height, 
                        highlightthickness=0, bg=parent.cget("bg"), **kwargs)
        
        self.width = width
        self.height = height
        self.progress_color = progress_color
        self.bg_color = bg_color
        self.show_handle = show_handle
        self.progress = 0.0  # 0.0 to 1.0
        
        # 创建元素
        self.bg_rect = None
        self.progress_rect = None
        self.handle = None
        
        # 绑定事件
        if show_handle:
            self.bind("<Button-1>", self._on_click)
            self.bind("<B1-Motion>", self._on_drag)
            self.config(cursor="hand2")
        
        # 初始绘制
        self._draw()
    
    def _draw(self):
        """绘制进度条"""
        # 清除旧元素
        self.delete("all")
        
        # 绘制背景
        self.bg_rect = self.create_rectangle(
            0, 0, self.width, self.height,
            fill=self.bg_color, outline=""
        )
        
        # 绘制进度
        progress_width = int(self.width * self.progress)
        self.progress_rect = self.create_rectangle(
            0, 0, progress_width, self.height,
            fill=self.progress_color, outline=""
        )
        
        # 绘制滑块
        if self.show_handle and progress_width > 0:
            handle_radius = self.height * 2
            self.handle = self.create_oval(
                progress_width - handle_radius, -handle_radius,
                progress_width + handle_radius, handle_radius * 3,
                fill=self.progress_color, outline=""
            )
    
    def set_progress(self, progress):
        """设置进度"""
        self.progress = max(0.0, min(1.0, progress))
        self._draw()
    
    def set_colors(self, progress_color, bg_color):
        """设置颜色"""
        self.progress_color = progress_color
        self.bg_color = bg_color
        self._draw()
    
    def _on_click(self, event):
        """点击事件"""
        self._update_progress_from_event(event)
    
    def _on_drag(self, event):
        """拖动事件"""
        self._update_progress_from_event(event)
    
    def _update_progress_from_event(self, event):
        """从事件更新进度"""
        x = max(0, min(event.x, self.width))
        self.progress = x / self.width
        self._draw()
        
        # 触发回调
        if hasattr(self, 'on_progress_change'):
            self.on_progress_change(self.progress)
3.2.3 频谱可视化实现
python 复制代码
class SpectrumVisualizer:
    """高级频谱可视化器"""
    
    def __init__(self, canvas, width, height, num_bars=64):
        self.canvas = canvas
        self.width = width
        self.height = height
        self.num_bars = num_bars
        
        # 频谱参数
        self.fft_size = 2048
        self.sample_rate = 44100
        self.frequency_bins = None
        
        # 可视化参数
        self.bars = []
        self.current_levels = np.zeros(num_bars)
        self.target_levels = np.zeros(num_bars)
        self.history = deque(maxlen=10)  # 历史数据平滑
        
        # 颜色系统
        self.colors = self._create_color_gradient()
        self.gradient = self._create_gradient_image()
        
        # 物理参数
        self.gravity = 0.2
        self.velocity = np.zeros(num_bars)
        self.damping = 0.9
        
        # 性能优化
        self.off_screen = tk.Canvas(self.canvas, width=width, height=height, 
                                   highlightthickness=0, bg='black')
        self.last_draw_time = 0
        self.target_fps = 60
        
        # 初始化
        self._init_frequency_bins()
        self._create_bars()
    
    def _init_frequency_bins(self):
        """初始化频率分箱"""
        # 对数频率分箱(更符合人耳听觉)
        min_freq = 20
        max_freq = 20000
        
        # 对数空间分箱
        log_min = math.log10(min_freq)
        log_max = math.log10(max_freq)
        log_bins = np.logspace(log_min, log_max, self.num_bars, base=10)
        
        # 转换为FFT bin索引
        self.frequency_bins = []
        for freq in log_bins:
            bin_idx = int(freq * self.fft_size / self.sample_rate)
            self.frequency_bins.append(min(bin_idx, self.fft_size // 2 - 1))
    
    def _create_color_gradient(self):
        """创建颜色渐变(基于频率)"""
        colors = []
        for i in range(self.num_bars):
            # 频率越高,颜色越暖
            freq_factor = i / self.num_bars
            
            if freq_factor < 0.3:
                # 低频:蓝色
                r = 0
                g = int(255 * (freq_factor / 0.3))
                b = 255
            elif freq_factor < 0.6:
                # 中频:绿色到黄色
                r = int(255 * ((freq_factor - 0.3) / 0.3))
                g = 255
                b = 0
            else:
                # 高频:红色
                r = 255
                g = int(255 * (1 - (freq_factor - 0.6) / 0.4))
                b = 0
            
            colors.append((r, g, b))
        
        return colors
    
    def _create_gradient_image(self):
        """创建渐变背景图"""
        image = Image.new('RGB', (self.width, self.height), (0, 0, 0))
        draw = ImageDraw.Draw(image)
        
        # 创建垂直渐变
        for y in range(self.height):
            # 计算渐变因子
            factor = y / self.height
            
            # 从深蓝到深黑的渐变
            r = int(10 * (1 - factor))
            g = int(20 * (1 - factor))
            b = int(40 * (1 - factor))
            
            draw.line([(0, y), (self.width, y)], fill=(r, g, b))
        
        return ImageTk.PhotoImage(image)
    
    def _create_bars(self):
        """创建频谱条"""
        bar_width = self.width / self.num_bars
        spacing = bar_width * 0.1
        
        for i in range(self.num_bars):
            x = i * bar_width + spacing
            width = bar_width - spacing * 2
            
            # 创建渐变效果
            color = self.colors[i]
            fill_color = f'#{color[0]:02x}{color[1]:02x}{color[2]:02x}'
            
            bar = self.off_screen.create_rectangle(
                x, self.height,
                x + width, self.height,
                fill=fill_color,
                width=0
            )
            self.bars.append(bar)
    
    def update(self, audio_data: np.ndarray):
        """更新音频数据"""
        if len(audio_data) < self.fft_size:
            return
        
        # 应用窗函数
        window = np.hanning(self.fft_size)
        windowed = audio_data[:self.fft_size] * window
        
        # 计算FFT
        fft = np.fft.rfft(windowed)
        magnitude = np.abs(fft)
        
        # 转换为分贝
        magnitude_db = 20 * np.log10(magnitude + 1e-10)
        
        # 对频率分箱
        binned_levels = np.zeros(self.num_bars)
        for i in range(self.num_bars - 1):
            start_bin = self.frequency_bins[i]
            end_bin = self.frequency_bins[i + 1]
            
            if start_bin < len(magnitude_db):
                segment = magnitude_db[start_bin:end_bin]
                if len(segment) > 0:
                    binned_levels[i] = np.mean(segment)
        
        # 归一化
        if np.max(binned_levels) > np.min(binned_levels):
            normalized = (binned_levels - np.min(binned_levels)) / \
                        (np.max(binned_levels) - np.min(binned_levels))
        else:
            normalized = binned_levels
        
        # 添加历史平滑
        self.history.append(normalized)
        smoothed = np.mean(list(self.history), axis=0)
        
        # 应用物理模拟
        self._apply_physics(smoothed)
        
        # 更新显示
        self._draw_bars()
        
        # 交换缓冲区
        self._swap_buffers()
    
    def _apply_physics(self, target_levels):
        """应用物理模拟(弹簧-阻尼系统)"""
        for i in range(self.num_bars):
            # 计算加速度
            acceleration = (target_levels[i] - self.current_levels[i]) * 0.3
            
            # 应用重力
            acceleration -= self.gravity
            
            # 更新速度
            self.velocity[i] = self.velocity[i] * self.damping + acceleration
            
            # 更新位置
            self.current_levels[i] += self.velocity[i]
            
            # 边界检查
            if self.current_levels[i] < 0:
                self.current_levels[i] = 0
                self.velocity[i] = -self.velocity[i] * 0.5
            elif self.current_levels[i] > 1:
                self.current_levels[i] = 1
                self.velocity[i] = -self.velocity[i] * 0.5
    
    def _draw_bars(self):
        """绘制频谱条"""
        max_height = self.height * 0.8
        
        for i, bar in enumerate(self.bars):
            height = self.current_levels[i] * max_height
            y1 = self.height - height
            
            # 更新条的位置
            coords = self.off_screen.coords(bar)
            if len(coords) == 4:
                x0, y0, x1, y2 = coords
                self.off_screen.coords(bar, x0, y1, x1, self.height)
            
            # 根据高度调整颜色
            brightness = 0.5 + self.current_levels[i] * 0.5
            color = self.colors[i]
            r = int(color[0] * brightness)
            g = int(color[1] * brightness)
            b = int(color[2] * brightness)
            
            self.off_screen.itemconfig(bar, fill=f'#{r:02x}{g:02x}{b:02x}')
        
        # 添加辉光效果
        self._add_glow_effect()
    
    def _add_glow_effect(self):
        """添加辉光效果"""
        # 找到最高点
        max_idx = np.argmax(self.current_levels)
        max_height = self.current_levels[max_idx]
        
        if max_height > 0.7:
            # 计算辉光位置
            bar_width = self.width / self.num_bars
            x = max_idx * bar_width + bar_width / 2
            y = self.height - max_height * self.height * 0.8
            
            # 绘制辉光
            radius = max_height * 20
            self.off_screen.create_oval(
                x - radius, y - radius,
                x + radius, y + radius,
                fill=self.colors[max_idx],
                outline='',
                stipple='gray50'  # 半透明效果
            )
    
    def _swap_buffers(self):
        """交换缓冲区(双缓冲)"""
        # 限制帧率
        current_time = time.time() * 1000
        if current_time - self.last_draw_time < 1000 / self.target_fps:
            return
        
        # 清除主画布
        self.canvas.delete("all")
        
        # 绘制渐变背景
        self.canvas.create_image(0, 0, image=self.gradient, anchor="nw")
        
        # 复制离屏内容
        x = self.canvas.winfo_rootx() - self.off_screen.winfo_rootx()
        y = self.canvas.winfo_rooty() - self.off_screen.winfo_rooty()
        
        self.canvas.create_image(0, 0, image=self.off_screen, anchor="nw")
        
        self.last_draw_time = current_time
    
    def set_color_scheme(self, scheme_name: str):
        """设置颜色方案"""
        schemes = {
            'rainbow': self._create_rainbow_gradient,
            'fire': self._create_fire_gradient,
            'ocean': self._create_ocean_gradient,
            'neon': self._create_neon_gradient
        }
        
        if scheme_name in schemes:
            self.colors = schemes[scheme_name]()
            self._update_bar_colors()
    
    def _update_bar_colors(self):
        """更新条的颜色"""
        for i, bar in enumerate(self.bars):
            color = self.colors[i]
            fill_color = f'#{color[0]:02x}{color[1]:02x}{color[2]:02x}'
            self.off_screen.itemconfig(bar, fill=fill_color)

3.3 播放控制状态机设计

3.4 播放控制器实现

python 复制代码
class PlayerController:
    """播放控制器"""
    
    def __init__(self):
        # 初始化pygame mixer
        pygame.mixer.init(frequency=44100, size=-16, channels=2, buffer=4096)
        
        # 状态变量
        self.current_file = None
        self.playlist = []
        self.current_index = 0
        self.player_state = PlayerState.STOPPED
        self.play_mode = PlayMode.SEQUENCE
        self.volume = 0.7
        self.position = 0.0
        self.duration = 0.0
        
        # 播放队列
        self.play_queue = []
        
        # 回调函数
        self.on_state_change = None
        self.on_position_change = None
        self.on_metadata_change = None
        
        # 当前元数据
        self.current_metadata = None
        
        # 更新线程
        self.update_thread = None
        self.running = False
    
    def _on_playback_end(self):
        """播放结束处理"""
        if self.play_mode == PlayMode.REPEAT_ONE:
            # 单曲循环
            self.seek(0)
            self.play()
        elif self.play_mode == PlayMode.REPEAT_ALL:
            # 列表循环
            self.next_track()
        elif self.play_mode == PlayMode.SHUFFLE:
            # 随机播放
            if self.playlist:
                self.current_index = random.randint(0, len(self.playlist) - 1)
                self._play_current_index()
        else:  # SEQUENCE
            # 顺序播放
            if self.current_index < len(self.playlist) - 1:
                self.next_track()
            else:
                self.stop()

4. 音频处理引擎设计

4.1 音频处理流水线

4.2 音频可视化架构

python 复制代码
class AudioVisualizer:
    """音频可视化引擎"""
    
    def __init__(self, canvas: tk.Canvas, width: int, height: int):
        self.canvas = canvas
        self.width = width
        self.height = height
        self.mode = VisualizationMode.SPECTRUM
        self.bars = []
        self.particles = []
        self.animation_id = None
        
        # 频谱参数
        self.num_bars = 64
        self.bar_width = 3
        self.bar_spacing = 1
        self.current_levels = [0] * self.num_bars
        self.target_levels = [0] * self.num_bars
        self.smoothing = 0.3
        
        # 粒子参数
        self.max_particles = 100
        self.particle_speed = 2.0
        
        # 颜色渐变
        self.colors = self._create_color_gradient()
        
        # 初始化
        self._create_bars()
        self._init_particles()
    
    def _create_color_gradient(self) -> List[str]:
        """创建颜色渐变"""
        colors = []
        for i in range(self.num_bars):
            # 彩虹色渐变
            hue = i / self.num_bars
            r = int(255 * (0.5 + 0.5 * math.sin(2 * math.pi * hue)))
            g = int(255 * (0.5 + 0.5 * math.sin(2 * math.pi * hue + 2 * math.pi / 3)))
            b = int(255 * (0.5 + 0.5 * math.sin(2 * math.pi * hue + 4 * math.pi / 3)))
            colors.append(f'#{r:02x}{g:02x}{b:02x}')
        return colors

5. 事件驱动架构与消息传递

5.1 事件处理流程

5.2 消息总线设计

6. 性能优化策略

6.1 渲染性能优化

6.2 内存管理架构

python 复制代码
class MemoryManager:
    """内存管理器"""
    
    def __init__(self):
        self.object_pools = {}
        self.cache_manager = CacheManager()
        self.memory_monitor = MemoryMonitor()
        
    def get_image(self, image_path: str) -> ImageTk.PhotoImage:
        """获取图片(带缓存)"""
        # 检查缓存
        cached = self.cache_manager.get(f"image_{image_path}")
        if cached:
            return cached
        
        # 加载图片
        image = Image.open(image_path)
        photo = ImageTk.PhotoImage(image)
        
        # 放入缓存
        self.cache_manager.set(f"image_{image_path}", photo, ttl=300)
        
        return photo
    
    def create_round_rect_pool(self, size: Tuple[int, int], radius: int):
        """创建圆角矩形对象池"""
        pool_key = f"round_rect_{size[0]}_{size[1]}_{radius}"
        
        if pool_key not in self.object_pools:
            self.object_pools[pool_key] = ObjectPool(
                create_func=lambda: self._create_round_rect(size, radius),
                max_size=10
            )
        
        return self.object_pools[pool_key]

7. 插件系统设计

7.1 插件架构

8. 响应式布局系统

8.1 布局管理器

python 复制代码
class ResponsiveLayout:
    """响应式布局管理器"""
    
    def __init__(self, container):
        self.container = container
        self.widgets = []
        self.rules = []
        self.current_breakpoint = "desktop"
        
    def add_widget(self, widget, constraints: Dict):
        """
        添加控件和约束
        
        Args:
            widget: tkinter控件
            constraints: {
                'desktop': {'row': 0, 'col': 0, 'rowspan': 1, 'colspan': 1},
                'tablet': {'row': 0, 'col': 0, 'rowspan': 1, 'colspan': 1},
                'mobile': {'row': 0, 'col': 0, 'rowspan': 1, 'colspan': 1}
            }
        """
        self.widgets.append((widget, constraints))
        
    def update_layout(self, width: int, height: int):
        """根据窗口大小更新布局"""
        # 确定断点
        if width < 600:
            breakpoint = "mobile"
        elif width < 1000:
            breakpoint = "tablet"
        else:
            breakpoint = "desktop"
        
        if breakpoint != self.current_breakpoint:
            self.current_breakpoint = breakpoint
            self._apply_layout(breakpoint)
    
    def _apply_layout(self, breakpoint: str):
        """应用特定断点的布局"""
        # 清除所有控件
        for widget, _ in self.widgets:
            widget.grid_remove()
        
        # 应用新布局
        for widget, constraints in self.widgets:
            if breakpoint in constraints:
                layout = constraints[breakpoint]
                widget.grid(**layout, sticky="nsew")

8.2 布局管理器代码实现

python 复制代码
class ResponsiveLayoutManager:
    """响应式布局管理器"""
    
    def __init__(self, root):
        self.root = root
        self.breakpoints = {
            'mobile': 768,      # ≤767px
            'tablet': 1200,     # 768-1199px
            'desktop': float('inf')  # ≥1200px
        }
        self.current_breakpoint = 'desktop'
        self.layout_rules = {}
        self.components = {}
        
        # 绑定窗口大小变化事件
        self.root.bind("<Configure>", self._on_window_resize)
    
    def register_component(self, component_id, component, rules):
        """注册组件及其响应式规则"""
        self.components[component_id] = component
        self.layout_rules[component_id] = rules
    
    def _on_window_resize(self, event):
        """窗口大小变化处理"""
        if event.widget == self.root:
            width = event.width
            self._update_breakpoint(width)
    
    def _update_breakpoint(self, width):
        """更新当前断点"""
        new_breakpoint = None
        
        if width < self.breakpoints['mobile']:
            new_breakpoint = 'mobile'
        elif width < self.breakpoints['tablet']:
            new_breakpoint = 'tablet'
        else:
            new_breakpoint = 'desktop'
        
        if new_breakpoint != self.current_breakpoint:
            self.current_breakpoint = new_breakpoint
            self._apply_layout_rules()
    
    def _apply_layout_rules(self):
        """应用布局规则"""
        for component_id, component in self.components.items():
            rules = self.layout_rules[component_id]
            
            if self.current_breakpoint in rules:
                rule = rules[self.current_breakpoint]
                self._apply_rule(component, rule)
    
    def _apply_rule(self, component, rule):
        """应用单个规则"""
        # 处理显示/隐藏
        if 'visible' in rule:
            if rule['visible']:
                component.pack(**rule.get('pack', {}))
            else:
                component.pack_forget()
        
        # 处理网格布局
        if 'grid' in rule:
            component.grid(**rule['grid'])
        
        # 处理尺寸
        if 'size' in rule:
            if isinstance(component, tk.Canvas):
                component.config(width=rule['size']['width'], 
                               height=rule['size']['height'])
            elif hasattr(component, 'config'):
                if 'width' in rule['size']:
                    component.config(width=rule['size']['width'])
                if 'height' in rule['size']:
                    component.config(height=rule['size']['height'])

# 使用示例
def setup_responsive_layout():
    """设置响应式布局示例"""
    root = tk.Tk()
    layout_manager = ResponsiveLayoutManager(root)
    
    # 创建组件
    sidebar = tk.Frame(root, bg="gray", width=200)
    content = tk.Frame(root, bg="lightgray")
    rightbar = tk.Frame(root, bg="darkgray", width=300)
    
    # 注册组件及规则
    layout_manager.register_component('sidebar', sidebar, {
        'desktop': {'visible': True, 'grid': {'row': 0, 'column': 0, 'sticky': 'ns'}},
        'tablet': {'visible': True, 'grid': {'row': 0, 'column': 0, 'sticky': 'ns'}},
        'mobile': {'visible': False}
    })
    
    layout_manager.register_component('content', content, {
        'desktop': {'visible': True, 'grid': {'row': 0, 'column': 1, 'sticky': 'nsew'}},
        'tablet': {'visible': True, 'grid': {'row': 0, 'column': 1, 'sticky': 'nsew'}},
        'mobile': {'visible': True, 'grid': {'row': 0, 'column': 0, 'sticky': 'nsew'}}
    })
    
    layout_manager.register_component('rightbar', rightbar, {
        'desktop': {'visible': True, 'grid': {'row': 0, 'column': 2, 'sticky': 'ns'}},
        'tablet': {'visible': False},
        'mobile': {'visible': False}
    })
    
    # 配置网格权重
    root.grid_columnconfigure(1, weight=1)
    root.grid_rowconfigure(0, weight=1)
    
    return root, layout_manager

9. 插件系统设计

10. 完整代码实现示例

10.1 主播放器类

python 复制代码
class ModernPlayer:
    """现代播放器主类"""
    
    def __init__(self):
        # 创建主窗口
        self.root = tk.Tk()
        self.root.title("Modern Player - 时尚换肤播放器")
        self.root.geometry("1000x700")
        self.root.minsize(800, 600)
        
        # 初始化组件
        self.skin_engine = SkinEngine(self.root)
        self.player_controller = PlayerController()
        self.visualizer = None
        
        # 状态变量
        self.is_dragging_progress = False
        self.current_skin = "dark"
        
        # 初始化UI
        self._init_ui()
        
        # 应用默认皮肤
        self.skin_engine.apply_skin(self.current_skin)
        
        # 绑定事件
        self._bind_events()
        
        # 设置控制器回调
        self._setup_controller_callbacks()
    
    def _init_ui(self):
        """初始化用户界面"""
        # 主容器
        self.main_container = ttk.Frame(self.root, style="Modern.TFrame")
        self.main_container.pack(fill=tk.BOTH, expand=True, padx=2, pady=2)
        
        # 创建布局
        self._create_left_sidebar()
        self._create_center_content()
        self._create_right_sidebar()
        self._create_bottom_controls()
    
    def _create_left_sidebar(self):
        """创建左侧边栏"""
        sidebar = ttk.Frame(self.main_container, width=200, style="Card.TFrame")
        sidebar.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 2), pady=2)
        sidebar.pack_propagate(False)
        
        # Logo区域
        logo_frame = ttk.Frame(sidebar, style="Card.TFrame")
        logo_frame.pack(fill=tk.X, padx=12, pady=(12, 20))
        
        ttk.Label(
            logo_frame,
            text="♬ MODERN PLAYER",
            style="Title.TLabel"
        ).pack(anchor=tk.W)
        
        ttk.Label(
            logo_frame,
            text="时尚换肤播放器",
            style="Caption.TLabel"
        ).pack(anchor=tk.W)
    
    def _create_bottom_controls(self):
        """创建底部控制栏"""
        bottom_frame = ttk.Frame(self.main_container, style="Card.TFrame")
        bottom_frame.pack(side=tk.BOTTOM, fill=tk.X, padx=2, pady=(2, 0))
        
        # 播放控制按钮
        controls = [
            ("⏮", self._previous_track, 24),
            ("⏸", self._play_pause, 40),
            ("⏭", self._next_track, 24),
        ]
        
        for text, command, size in controls:
            btn = tk.Button(
                btn_container,
                text=text,
                font=("微软雅黑", size),
                bg=self.skin_engine.get_color("card"),
                fg=self.skin_engine.get_color("text_primary"),
                borderwidth=0,
                relief="flat",
                cursor="hand2",
                command=command
            )
            btn.pack(side=tk.LEFT, padx=5)
    
    def run(self):
        """运行播放器"""
        self.root.mainloop()

10.2 主播放器类完整代码实现:

python 复制代码
# -*- coding: utf-8 -*-
"""
Created on Tue Apr  7 18:25:29 2026

@author: HP
"""

#!/usr/bin/env python3
"""
现代换肤音乐播放器 - 完整实现
支持皮肤切换、音频可视化、播放列表管理等功能
"""

import os
import sys
import json
import time
import threading
import queue
from pathlib import Path
from typing import Dict, List, Optional, Tuple, Any, Callable
from dataclasses import dataclass, asdict, field
from enum import Enum, auto
from datetime import timedelta
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import pygame
import pygame.mixer
from PIL import Image, ImageTk, ImageDraw, ImageFont
import random
import wave
import math
from collections import deque
import hashlib

# ============================
# 1. 常量定义与数据模型
# ============================

class PlayerState(Enum):
    """播放器状态"""
    STOPPED = "stopped"
    PLAYING = "playing"
    PAUSED = "paused"
    BUFFERING = "buffering"

class PlayMode(Enum):
    """播放模式"""
    SEQUENCE = "sequence"      # 顺序播放
    REPEAT_ONE = "repeat_one"  # 单曲循环
    REPEAT_ALL = "repeat_all"  # 列表循环
    SHUFFLE = "shuffle"        # 随机播放

class VisualizationMode(Enum):
    """可视化模式"""
    SPECTRUM = "spectrum"      # 频谱分析
    WAVEFORM = "waveform"      # 波形显示
    CIRCULAR = "circular"      # 圆形频谱
    PARTICLES = "particles"    # 粒子效果

@dataclass
class AudioMetadata:
    """音频元数据"""
    title: str = ""
    artist: str = ""
    album: str = ""
    duration: float = 0.0
    bitrate: int = 0
    sample_rate: int = 0
    channels: int = 2
    file_path: str = ""
    file_size: int = 0
    
    def get_display_title(self) -> str:
        """获取显示标题"""
        if self.title and self.artist:
            return f"{self.artist} - {self.title}"
        elif self.title:
            return self.title
        else:
            return Path(self.file_path).stem

@dataclass
class SkinConfig:
    """皮肤配置"""
    name: str
    author: str
    version: str
    description: str
    colors: Dict[str, str]
    fonts: Dict[str, Dict]
    sizes: Dict[str, int]
    images: Dict[str, str] = field(default_factory=dict)
    
    @classmethod
    def from_dict(cls, data: Dict) -> 'SkinConfig':
        """从字典创建皮肤配置"""
        return cls(
            name=data.get('name', '未命名皮肤'),
            author=data.get('author', '未知作者'),
            version=data.get('version', '1.0.0'),
            description=data.get('description', ''),
            colors=data.get('colors', {}),
            fonts=data.get('fonts', {}),
            sizes=data.get('sizes', {}),
            images=data.get('images', {})
        )

# ============================
# 2. 音频可视化引擎
# ============================

class AudioVisualizer:
    """音频可视化引擎"""
    
    def __init__(self, canvas: tk.Canvas, width: int, height: int):
        self.canvas = canvas
        self.width = width
        self.height = height
        self.mode = VisualizationMode.SPECTRUM
        self.bars = []
        self.particles = []
        self.animation_id = None
        
        # 频谱参数
        self.num_bars = 64
        self.bar_width = 3
        self.bar_spacing = 1
        self.current_levels = [0] * self.num_bars
        self.target_levels = [0] * self.num_bars
        self.smoothing = 0.3
        
        # 粒子参数
        self.max_particles = 100
        self.particle_speed = 2.0
        
        # 颜色渐变
        self.colors = self._create_color_gradient()
        
        # 初始化
        self._create_bars()
        self._init_particles()
    
    def _create_color_gradient(self) -> List[str]:
        """创建颜色渐变"""
        colors = []
        for i in range(self.num_bars):
            # 彩虹色渐变
            hue = i / self.num_bars
            r = int(255 * (0.5 + 0.5 * math.sin(2 * math.pi * hue)))
            g = int(255 * (0.5 + 0.5 * math.sin(2 * math.pi * hue + 2 * math.pi / 3)))
            b = int(255 * (0.5 + 0.5 * math.sin(2 * math.pi * hue + 4 * math.pi / 3)))
            colors.append(f'#{r:02x}{g:02x}{b:02x}')
        return colors
    
    def _create_bars(self):
        """创建频谱条"""
        total_width = self.num_bars * (self.bar_width + self.bar_spacing) - self.bar_spacing
        start_x = (self.width - total_width) // 2
        
        for i in range(self.num_bars):
            x = start_x + i * (self.bar_width + self.bar_spacing)
            bar = self.canvas.create_rectangle(
                x, self.height,
                x + self.bar_width, self.height,
                fill=self.colors[i],
                width=0,
                tags='visualizer'
            )
            self.bars.append(bar)
    
    def _init_particles(self):
        """初始化粒子系统"""
        for _ in range(self.max_particles):
            particle = {
                'x': random.uniform(0, self.width),
                'y': random.uniform(0, self.height),
                'vx': random.uniform(-1, 1) * self.particle_speed,
                'vy': random.uniform(-2, 0) * self.particle_speed,
                'size': random.uniform(2, 6),
                'life': 1.0,
                'decay': random.uniform(0.01, 0.05),
                'color': self.colors[random.randint(0, len(self.colors) - 1)]
            }
            self.particles.append(particle)
    
    def update(self, audio_data: Optional[List[float]] = None):
        """更新可视化数据"""
        if audio_data is None:
            # 生成测试数据
            audio_data = self._generate_test_data()
        
        if self.mode == VisualizationMode.SPECTRUM:
            self._update_spectrum(audio_data)
        elif self.mode == VisualizationMode.PARTICLES:
            self._update_particles(audio_data)
    
    def _generate_test_data(self) -> List[float]:
        """生成测试音频数据"""
        import math
        t = time.time()
        data = []
        
        for i in range(self.num_bars):
            value = (math.sin(t + i * 0.1) * 0.3 + 
                    math.sin(t * 2 + i * 0.2) * 0.2 +
                    random.random() * 0.1)
            data.append(abs(value))
        
        return data
    
    def _update_spectrum(self, levels: List[float]):
        """更新频谱显示"""
        # 调整数据长度
        if len(levels) > self.num_bars:
            levels = levels[:self.num_bars]
        elif len(levels) < self.num_bars:
            levels = levels + [0] * (self.num_bars - len(levels))
        
        # 应用平滑
        for i in range(self.num_bars):
            self.target_levels[i] = min(max(levels[i], 0), 1)
            self.current_levels[i] = (self.current_levels[i] * (1 - self.smoothing) + 
                                     self.target_levels[i] * self.smoothing)
        
        # 更新显示
        self._draw_bars()
    
    def _draw_bars(self):
        """绘制频谱条"""
        max_height = self.height * 0.8
        
        for i, bar in enumerate(self.bars):
            height = self.current_levels[i] * max_height
            y1 = self.height - height
            
            # 更新条的位置
            coords = self.canvas.coords(bar)
            if len(coords) == 4:
                self.canvas.coords(bar, coords[0], y1, coords[2], self.height)
            
            # 根据高度调整颜色亮度
            if self.current_levels[i] > 0.7:
                bright_color = self._brighten_color(self.colors[i], 1.3)
                self.canvas.itemconfig(bar, fill=bright_color)
            else:
                self.canvas.itemconfig(bar, fill=self.colors[i])
    
    def _update_particles(self, levels: List[float]):
        """更新粒子系统"""
        audio_level = sum(levels) / len(levels) if levels else 0.5
        
        # 更新粒子
        for particle in self.particles:
            # 受音频影响
            particle['vx'] += random.uniform(-0.1, 0.1) * audio_level
            particle['vy'] += random.uniform(-0.1, 0.1) * audio_level
            
            # 更新位置
            particle['x'] += particle['vx']
            particle['y'] += particle['vy']
            
            # 边界检查
            if particle['x'] < 0 or particle['x'] > self.width:
                particle['vx'] *= -0.8
                particle['x'] = max(0, min(self.width, particle['x']))
            
            if particle['y'] < 0 or particle['y'] > self.height:
                particle['vy'] *= -0.8
                particle['y'] = max(0, min(self.height, particle['y']))
            
            # 生命周期
            particle['life'] -= particle['decay']
            if particle['life'] <= 0:
                # 重置粒子
                particle['x'] = random.uniform(0, self.width)
                particle['y'] = self.height
                particle['vx'] = random.uniform(-1, 1) * self.particle_speed
                particle['vy'] = random.uniform(-2, 0) * self.particle_speed
                particle['life'] = 1.0
        
        # 绘制粒子
        self._draw_particles()
    
    def _draw_particles(self):
        """绘制粒子"""
        self.canvas.delete('particle')
        
        for particle in self.particles:
            if particle['life'] > 0:
                x, y = particle['x'], particle['y']
                size = particle['size'] * particle['life']
                
                self.canvas.create_oval(
                    x - size, y - size,
                    x + size, y + size,
                    fill=particle['color'],
                    outline='',
                    tags='particle'
                )
    
    def _brighten_color(self, color: str, factor: float) -> str:
        """调整颜色亮度"""
        if color.startswith('#'):
            r = int(color[1:3], 16)
            g = int(color[3:5], 16)
            b = int(color[5:7], 16)
            
            r = min(255, int(r * factor))
            g = min(255, int(g * factor))
            b = min(255, int(b * factor))
            
            return f'#{r:02x}{g:02x}{b:02x}'
        return color
    
    def set_mode(self, mode: VisualizationMode):
        """设置可视化模式"""
        self.mode = mode
        self.canvas.delete('visualizer')
        self.canvas.delete('particle')
        
        if mode == VisualizationMode.SPECTRUM:
            self._create_bars()
        elif mode == VisualizationMode.PARTICLES:
            self._init_particles()
    
    def animate(self):
        """开始动画"""
        if self.animation_id:
            self.canvas.after_cancel(self.animation_id)
        
        self.update()
        self.animation_id = self.canvas.after(50, self.animate)
    
    def stop(self):
        """停止动画"""
        if self.animation_id:
            self.canvas.after_cancel(self.animation_id)
            self.animation_id = None

# ============================
# 3. 皮肤引擎
# ============================

class SkinEngine:
    """皮肤引擎"""
    
    def __init__(self, root: tk.Tk):
        self.root = root
        self.style = ttk.Style()
        self.current_skin = "default"
        self.skins: Dict[str, SkinConfig] = {}
        self.image_cache: Dict[str, ImageTk.PhotoImage] = {}
        
        # 加载内置皮肤
        self._load_builtin_skins()
    
    def _load_builtin_skins(self):
        """加载内置皮肤"""
        # 暗色主题
        dark_skin = SkinConfig(
            name="深色主题",
            author="Modern Player",
            version="1.0.0",
            description="现代化深色主题,护眼舒适",
            colors={
                "primary": "#1DB954",      # Spotify绿
                "secondary": "#535353",
                "background": "#121212",
                "surface": "#181818",
                "card": "#282828",
                "text_primary": "#FFFFFF",
                "text_secondary": "#B3B3B3",
                "text_disabled": "#535353",
                "border": "#404040",
                "hover": "#282828",
                "active": "#1DB954",
                "error": "#E22134",
                "warning": "#FF9800",
                "info": "#2196F3",
                "success": "#4CAF50"
            },
            fonts={
                "title": {"family": "微软雅黑", "size": 16, "weight": "bold"},
                "subtitle": {"family": "微软雅黑", "size": 14, "weight": "normal"},
                "body": {"family": "微软雅黑", "size": 12, "weight": "normal"},
                "caption": {"family": "微软雅黑", "size": 10, "weight": "normal"},
                "button": {"family": "微软雅黑", "size": 11, "weight": "bold"}
            },
            sizes={
                "border_radius": 8,
                "padding_small": 4,
                "padding_medium": 8,
                "padding_large": 12,
                "icon_size": 24,
                "control_height": 40
            }
        )
        
        # 亮色主题
        light_skin = SkinConfig(
            name="浅色主题",
            author="Modern Player",
            version="1.0.0",
            description="清新亮色主题,简洁明了",
            colors={
                "primary": "#1DB954",
                "secondary": "#E0E0E0",
                "background": "#FFFFFF",
                "surface": "#F5F5F5",
                "card": "#FFFFFF",
                "text_primary": "#000000",
                "text_secondary": "#666666",
                "text_disabled": "#9E9E9E",
                "border": "#E0E0E0",
                "hover": "#F5F5F5",
                "active": "#1DB954",
                "error": "#F44336",
                "warning": "#FF9800",
                "info": "#2196F3",
                "success": "#4CAF50"
            },
            fonts=dark_skin.fonts,
            sizes=dark_skin.sizes
        )
        
        # 霓虹主题
        neon_skin = SkinConfig(
            name="霓虹主题",
            author="Modern Player",
            version="1.0.0",
            description="炫酷霓虹主题,赛博朋克风格",
            colors={
                "primary": "#FF00FF",
                "secondary": "#00FFFF",
                "background": "#0A0A0A",
                "surface": "#1A1A1A",
                "card": "#2A2A2A",
                "text_primary": "#FFFFFF",
                "text_secondary": "#CCCCCC",
                "text_disabled": "#666666",
                "border": "#FF00FF",
                "hover": "#330033",
                "active": "#FF00FF",
                "error": "#FF0000",
                "warning": "#FFFF00",
                "info": "#00FFFF",
                "success": "#00FF00"
            },
            fonts={
                "title": {"family": "Consolas", "size": 16, "weight": "bold"},
                "subtitle": {"family": "Consolas", "size": 14, "weight": "normal"},
                "body": {"family": "Consolas", "size": 12, "weight": "normal"},
                "caption": {"family": "Consolas", "size": 10, "weight": "normal"},
                "button": {"family": "Consolas", "size": 11, "weight": "bold"}
            },
            sizes=dark_skin.sizes
        )
        
        self.skins = {
            "dark": dark_skin,
            "light": light_skin,
            "neon": neon_skin
        }
    
    def apply_skin(self, skin_name: str):
        """应用皮肤"""
        if skin_name not in self.skins:
            print(f"皮肤 '{skin_name}' 不存在,使用默认皮肤")
            skin_name = "dark"
        
        self.current_skin = skin_name
        skin = self.skins[skin_name]
        
        # 创建ttk样式
        self._create_ttk_styles(skin)
        
        # 配置窗口背景
        self.root.configure(bg=skin.colors["background"])
        
        print(f"应用皮肤: {skin.name}")
    
    def _create_ttk_styles(self, skin: SkinConfig):
        """创建ttk样式"""
        colors = skin.colors
        sizes = skin.sizes
        
        # 创建现代主题
        self.style.theme_create("modern", parent="clam")
        self.style.theme_use("modern")
        
        # 配置基本样式
        self.style.configure(
            ".",
            background=colors["background"],
            foreground=colors["text_primary"],
            fieldbackground=colors["surface"],
            troughcolor=colors["surface"],
            selectbackground=colors["primary"],
            selectforeground=colors["text_primary"],
            borderwidth=0,
            focusthickness=0,
            relief="flat"
        )
        
        # Frame样式
        self.style.configure(
            "Modern.TFrame",
            background=colors["background"]
        )
        
        self.style.configure(
            "Card.TFrame",
            background=colors["card"],
            relief="flat",
            borderwidth=1
        )
        
        # Label样式
        for font_type, font_config in skin.fonts.items():
            font = (font_config["family"], font_config["size"])
            if font_config.get("weight") == "bold":
                font = font + ("bold",)
            
            self.style.configure(
                f"{font_type.capitalize()}.TLabel",
                background=colors["background"],
                foreground=colors[f"text_{'primary' if font_type == 'title' else 'secondary'}"],
                font=font
            )
        
        # Button样式
        self.style.configure(
            "Primary.TButton",
            background=colors["primary"],
            foreground=colors["text_primary"],
            borderwidth=0,
            focusthickness=0,
            font=skin.fonts["button"],
            padding=(sizes["padding_large"], sizes["padding_medium"])
        )
        
        self.style.map(
            "Primary.TButton",
            background=[
                ("active", colors["active"]),
                ("disabled", colors["text_disabled"])
            ]
        )
        
        # 进度条样式
        self.style.configure(
            "Modern.Horizontal.TProgressbar",
            background=colors["primary"],
            troughcolor=colors["surface"],
            borderwidth=0
        )
    
    def get_color(self, color_name: str) -> str:
        """获取颜色值"""
        skin = self.skins.get(self.current_skin)
        if skin and color_name in skin.colors:
            return skin.colors[color_name]
        return "#000000"
    
    def get_font(self, font_type: str) -> tuple:
        """获取字体配置"""
        skin = self.skins.get(self.current_skin)
        if skin and font_type in skin.fonts:
            config = skin.fonts[font_type]
            font = (config["family"], config["size"])
            if config.get("weight") == "bold":
                font = font + ("bold",)
            return font
        return ("微软雅黑", 12)
    
    def create_round_rect_image(self, width: int, height: int, 
                              radius: int, color: str) -> ImageTk.PhotoImage:
        """创建圆角矩形图片"""
        cache_key = f"{width}_{height}_{radius}_{color}"
        
        if cache_key in self.image_cache:
            return self.image_cache[cache_key]
        
        # 创建透明图片
        image = Image.new('RGBA', (width, height), (0, 0, 0, 0))
        draw = ImageDraw.Draw(image)
        
        # 绘制圆角矩形
        draw.rounded_rectangle(
            [(0, 0), (width-1, height-1)],
            radius=radius,
            fill=color
        )
        
        # 转换为PhotoImage
        photo = ImageTk.PhotoImage(image)
        self.image_cache[cache_key] = photo
        
        return photo

# ============================
# 4. 播放控制器
# ============================

class PlayerController:
    """播放控制器"""
    
    def __init__(self):
        # 初始化pygame mixer
        pygame.mixer.init(frequency=44100, size=-16, channels=2, buffer=4096)
        
        # 状态变量
        self.current_file = None
        self.playlist = []
        self.current_index = 0
        self.player_state = PlayerState.STOPPED
        self.play_mode = PlayMode.SEQUENCE
        self.volume = 0.7
        self.position = 0.0
        self.duration = 0.0
        
        # 播放队列
        self.play_queue = []
        
        # 回调函数
        self.on_state_change = None
        self.on_position_change = None
        self.on_metadata_change = None
        
        # 当前元数据
        self.current_metadata = None
        
        # 更新线程
        self.update_thread = None
        self.running = False
    
    def load_file(self, file_path: str):
        """加载音频文件"""
        try:
            # 停止当前播放
            if pygame.mixer.music.get_busy():
                pygame.mixer.music.stop()
            
            # 加载新文件
            pygame.mixer.music.load(file_path)
            self.current_file = file_path
            
            # 获取音频信息
            self._get_audio_info(file_path)
            
            # 创建元数据
            self.current_metadata = AudioMetadata(
                title=Path(file_path).stem,
                file_path=file_path,
                duration=self.duration
            )
            
            # 通知元数据变化
            if self.on_metadata_change:
                self.on_metadata_change(self.current_metadata)
            
            return True
            
        except Exception as e:
            print(f"加载文件失败: {e}")
            return False
    
    def _get_audio_info(self, file_path: str):
        """获取音频信息"""
        try:
            # 尝试使用wave模块获取WAV文件信息
            if file_path.lower().endswith('.wav'):
                with wave.open(file_path, 'rb') as wav_file:
                    frames = wav_file.getnframes()
                    rate = wav_file.getframerate()
                    self.duration = frames / float(rate)
            else:
                # 其他格式,使用估计值
                file_size = os.path.getsize(file_path)
                # 假设128kbps的MP3
                self.duration = file_size / (128 * 1024 / 8)
        except:
            # 默认值
            self.duration = 180.0  # 3分钟
    
    def play(self):
        """开始播放"""
        if self.current_file:
            pygame.mixer.music.play()
            pygame.mixer.music.set_volume(self.volume)
            self.player_state = PlayerState.PLAYING
            
            # 启动更新线程
            if not self.running:
                self.running = True
                self.update_thread = threading.Thread(target=self._update_loop, daemon=True)
                self.update_thread.start()
            
            # 通知状态变化
            if self.on_state_change:
                self.on_state_change(self.player_state)
            
            return True
        return False
    
    def pause(self):
        """暂停播放"""
        if self.player_state == PlayerState.PLAYING:
            pygame.mixer.music.pause()
            self.player_state = PlayerState.PAUSED
        elif self.player_state == PlayerState.PAUSED:
            pygame.mixer.music.unpause()
            self.player_state = PlayerState.PLAYING
        
        # 通知状态变化
        if self.on_state_change:
            self.on_state_change(self.player_state)
    
    def stop(self):
        """停止播放"""
        pygame.mixer.music.stop()
        self.player_state = PlayerState.STOPPED
        self.position = 0.0
        
        # 通知状态变化
        if self.on_state_change:
            self.on_state_change(self.player_state)
        
        # 通知位置变化
        if self.on_position_change:
            self.on_position_change(0.0, self.duration)
    
    def seek(self, position: float):
        """跳转到指定位置"""
        if self.current_file and self.duration > 0:
            position = max(0, min(position, self.duration))
            pygame.mixer.music.set_pos(position)
            self.position = position
            
            # 通知位置变化
            if self.on_position_change:
                self.on_position_change(self.position, self.duration)
    
    def set_volume(self, volume: float):
        """设置音量"""
        volume = max(0.0, min(1.0, volume))
        self.volume = volume
        pygame.mixer.music.set_volume(volume)
    
    def next_track(self):
        """下一曲"""
        if self.playlist:
            self.current_index = (self.current_index + 1) % len(self.playlist)
            self._play_current_index()
    
    def previous_track(self):
        """上一曲"""
        if self.playlist:
            self.current_index = (self.current_index - 1) % len(self.playlist)
            self._play_current_index()
    
    def _play_current_index(self):
        """播放当前索引的曲目"""
        if 0 <= self.current_index < len(self.playlist):
            track = self.playlist[self.current_index]
            if self.load_file(track['file_path']):
                self.play()
    
    def add_to_playlist(self, file_path: str, metadata: AudioMetadata = None):
        """添加到播放列表"""
        if not metadata:
            metadata = AudioMetadata(
                title=Path(file_path).stem,
                file_path=file_path
            )
        
        self.playlist.append({
            'file_path': file_path,
            'metadata': metadata
        })
    
    def _update_loop(self):
        """更新循环"""
        while self.running:
            if self.player_state == PlayerState.PLAYING:
                # 获取播放位置
                if hasattr(pygame.mixer.music, 'get_pos'):
                    pos = pygame.mixer.music.get_pos() / 1000.0
                    if pos >= 0:
                        self.position = pos
                        
                        # 通知位置变化
                        if self.on_position_change:
                            self.on_position_change(self.position, self.duration)
                
                # 检查是否播放结束
                if not pygame.mixer.music.get_busy() and self.player_state == PlayerState.PLAYING:
                    self._on_playback_end()
            
            time.sleep(0.1)  # 100ms更新一次
    
    def _on_playback_end(self):
        """播放结束处理"""
        if self.play_mode == PlayMode.REPEAT_ONE:
            # 单曲循环
            self.seek(0)
            self.play()
        elif self.play_mode == PlayMode.REPEAT_ALL:
            # 列表循环
            self.next_track()
        elif self.play_mode == PlayMode.SHUFFLE:
            # 随机播放
            if self.playlist:
                self.current_index = random.randint(0, len(self.playlist) - 1)
                self._play_current_index()
        else:  # SEQUENCE
            # 顺序播放
            if self.current_index < len(self.playlist) - 1:
                self.next_track()
            else:
                self.stop()
    
    def cleanup(self):
        """清理资源"""
        self.running = False
        if self.update_thread:
            self.update_thread.join(timeout=1.0)
        pygame.mixer.quit()

# ============================
# 5. 主播放器界面
# ============================

class ModernPlayer:
    """现代播放器主类"""
    
    def __init__(self):
        # 创建主窗口
        self.root = tk.Tk()
        self.root.title("Modern Player - 时尚换肤播放器")
        self.root.geometry("1000x700")
        self.root.minsize(800, 600)
        
        # 初始化组件
        self.skin_engine = SkinEngine(self.root)
        self.player_controller = PlayerController()
        self.visualizer = None
        
        # 状态变量
        self.is_dragging_progress = False
        self.current_skin = "dark"
        
        # 播放列表显示
        self.playlist_listbox = None
        
        # 初始化UI
        self._init_ui()
        
        # 应用默认皮肤
        self.skin_engine.apply_skin(self.current_skin)
        
        # 绑定事件
        self._bind_events()
        
        # 设置控制器回调
        self._setup_controller_callbacks()
        
        # 创建测试播放列表
        self._create_test_playlist()
    
    def _init_ui(self):
        """初始化用户界面"""
        # 主容器
        self.main_container = ttk.Frame(self.root, style="Modern.TFrame")
        self.main_container.pack(fill=tk.BOTH, expand=True, padx=2, pady=2)
        
        # 创建布局
        self._create_left_sidebar()
        self._create_center_content()
        self._create_right_sidebar()
        self._create_bottom_controls()
    
    def _create_left_sidebar(self):
        """创建左侧边栏"""
        sidebar = ttk.Frame(self.main_container, width=200, style="Card.TFrame")
        sidebar.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 2), pady=2)
        sidebar.pack_propagate(False)
        
        # Logo区域
        logo_frame = ttk.Frame(sidebar, style="Card.TFrame")
        logo_frame.pack(fill=tk.X, padx=12, pady=(12, 20))
        
        ttk.Label(
            logo_frame,
            text="♬ MODERN PLAYER",
            style="Title.TLabel"
        ).pack(anchor=tk.W)
        
        ttk.Label(
            logo_frame,
            text="时尚换肤播放器",
            style="Caption.TLabel"
        ).pack(anchor=tk.W)
        
        # 导航菜单
        nav_frame = ttk.Frame(sidebar, style="Card.TFrame")
        nav_frame.pack(fill=tk.X, padx=12, pady=(0, 20))
        
        # 导航按钮
        nav_items = [
            ("📁 我的音乐", self._show_music_library),
            ("🎵 正在播放", self._show_now_playing),
            ("📊 可视化", self._show_visualizer),
            ("🎨 皮肤中心", self._show_skin_center),
            ("⚙️ 设置", self._show_settings)
        ]
        
        for text, command in nav_items:
            btn = ttk.Button(
                nav_frame,
                text=text,
                style="Secondary.TButton",
                command=command
            )
            btn.pack(fill=tk.X, pady=2)
        
        # 播放列表标题
        ttk.Label(
            sidebar,
            text="播放列表",
            style="Subtitle.TLabel"
        ).pack(anchor=tk.W, padx=12, pady=(0, 8))
        
        # 播放列表容器
        playlist_container = ttk.Frame(sidebar, style="Card.TFrame")
        playlist_container.pack(fill=tk.BOTH, expand=True, padx=12, pady=(0, 12))
        
        # 播放列表滚动条
        playlist_scroll = ttk.Scrollbar(playlist_container)
        playlist_scroll.pack(side=tk.RIGHT, fill=tk.Y)
        
        # 播放列表列表框
        self.playlist_listbox = tk.Listbox(
            playlist_container,
            bg=self.skin_engine.get_color("card"),
            fg=self.skin_engine.get_color("text_primary"),
            selectbackground=self.skin_engine.get_color("primary"),
            selectforeground=self.skin_engine.get_color("text_primary"),
            borderwidth=0,
            highlightthickness=0,
            font=self.skin_engine.get_font("body"),
            yscrollcommand=playlist_scroll.set
        )
        self.playlist_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        playlist_scroll.config(command=self.playlist_listbox.yview)
        
        # 绑定选择事件
        self.playlist_listbox.bind('<<ListboxSelect>>', self._on_playlist_select)
    
    def _create_center_content(self):
        """创建中心内容区域"""
        center = ttk.Frame(self.main_container, style="Card.TFrame")
        center.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=2, pady=2)
        
        # 内容标题
        self.content_title = ttk.Label(
            center,
            text="我的音乐",
            style="Title.TLabel"
        )
        self.content_title.pack(anchor=tk.W, padx=20, pady=(20, 10))
        
        # 内容区域
        self.content_frame = ttk.Frame(center, style="Card.TFrame")
        self.content_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=(0, 20))
        
        # 默认显示音乐库
        self._show_music_library()
    
    def _create_right_sidebar(self):
        """创建右侧边栏"""
        right_sidebar = ttk.Frame(self.main_container, width=300, style="Card.TFrame")
        right_sidebar.pack(side=tk.RIGHT, fill=tk.Y, padx=(2, 0), pady=2)
        right_sidebar.pack_propagate(False)
        
        # 当前播放标题
        ttk.Label(
            right_sidebar,
            text="正在播放",
            style="Subtitle.TLabel"
        ).pack(anchor=tk.W, padx=20, pady=(20, 10))
        
        # 专辑封面
        self.album_art_frame = ttk.Frame(right_sidebar, style="Card.TFrame")
        self.album_art_frame.pack(fill=tk.X, padx=20, pady=(0, 20))
        
        # 创建默认专辑封面
        self._create_default_album_art()
        
        # 歌曲信息
        self.song_title = ttk.Label(
            right_sidebar,
            text="未选择歌曲",
            style="Title.TLabel"
        )
        self.song_title.pack(anchor=tk.W, padx=20, pady=(0, 5))
        
        self.song_artist = ttk.Label(
            right_sidebar,
            text="艺术家",
            style="Caption.TLabel"
        )
        self.song_artist.pack(anchor=tk.W, padx=20, pady=(0, 20))
        
        # 可视化区域
        viz_frame = ttk.Frame(right_sidebar, style="Card.TFrame")
        viz_frame.pack(fill=tk.X, padx=20, pady=(0, 20))
        
        ttk.Label(
            viz_frame,
            text="音频可视化",
            style="Caption.TLabel"
        ).pack(anchor=tk.W, pady=(0, 10))
        
        # 创建画布用于可视化
        viz_canvas = tk.Canvas(
            viz_frame,
            bg=self.skin_engine.get_color("card"),
            highlightthickness=0,
            height=150
        )
        viz_canvas.pack(fill=tk.X, pady=(0, 10))
        
        # 初始化可视化器
        self.visualizer = AudioVisualizer(viz_canvas, 280, 150)
        self.visualizer.animate()
        
        # 歌词区域
        ttk.Label(
            right_sidebar,
            text="歌词",
            style="Caption.TLabel"
        ).pack(anchor=tk.W, padx=20, pady=(0, 10))
        
        lyrics_frame = ttk.Frame(right_sidebar, style="Card.TFrame")
        lyrics_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=(0, 20))
        
        lyrics_scroll = ttk.Scrollbar(lyrics_frame)
        lyrics_scroll.pack(side=tk.RIGHT, fill=tk.Y)
        
        self.lyrics_text = tk.Text(
            lyrics_frame,
            bg=self.skin_engine.get_color("card"),
            fg=self.skin_engine.get_color("text_secondary"),
            borderwidth=0,
            highlightthickness=0,
            font=self.skin_engine.get_font("body"),
            wrap=tk.WORD,
            yscrollcommand=lyrics_scroll.set,
            height=8
        )
        self.lyrics_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        lyrics_scroll.config(command=self.lyrics_text.yview)
        
        # 插入示例歌词
        self.lyrics_text.insert(tk.END, "未加载歌词\n\n")
        self.lyrics_text.insert(tk.END, "点击歌曲开始播放\n")
        self.lyrics_text.config(state=tk.DISABLED)
    
    def _create_default_album_art(self):
        """创建默认专辑封面"""
        # 创建画布
        canvas = tk.Canvas(
            self.album_art_frame,
            width=260,
            height=260,
            bg=self.skin_engine.get_color("card"),
            highlightthickness=0
        )
        canvas.pack()
        
        # 绘制音乐符号
        center_x, center_y = 130, 130
        radius = 100
        
        # 绘制圆形背景
        canvas.create_oval(
            center_x - radius, center_y - radius,
            center_x + radius, center_y + radius,
            fill=self.skin_engine.get_color("primary"),
            outline=""
        )
        
        # 绘制音乐符号
        canvas.create_text(
            center_x, center_y - 20,
            text="♫",
            font=("微软雅黑", 60, "bold"),
            fill=self.skin_engine.get_color("text_primary")
        )
        
        canvas.create_text(
            center_x, center_y + 40,
            text="MUSIC",
            font=("微软雅黑", 20, "bold"),
            fill=self.skin_engine.get_color("text_primary")
        )
        
        self.album_canvas = canvas
    
    def _create_bottom_controls(self):
        """创建底部控制栏"""
        bottom_frame = ttk.Frame(self.main_container, style="Card.TFrame")
        bottom_frame.pack(side=tk.BOTTOM, fill=tk.X, padx=2, pady=(2, 0))
        
        # 左侧:播放信息
        info_frame = ttk.Frame(bottom_frame, style="Card.TFrame")
        info_frame.pack(side=tk.LEFT, fill=tk.Y, padx=20, pady=10)
        
        self.now_playing_label = ttk.Label(
            info_frame,
            text="未播放",
            style="Caption.TLabel"
        )
        self.now_playing_label.pack(anchor=tk.W)
        
        self.time_label = ttk.Label(
            info_frame,
            text="00:00 / 00:00",
            style="Caption.TLabel"
        )
        self.time_label.pack(anchor=tk.W)
        
        # 中间:播放控制
        control_frame = ttk.Frame(bottom_frame, style="Card.TFrame")
        control_frame.pack(side=tk.LEFT, expand=True, fill=tk.BOTH, padx=20, pady=10)
        
        # 控制按钮容器
        btn_container = ttk.Frame(control_frame, style="Card.TFrame")
        btn_container.pack(expand=True)
        
        # 播放控制按钮
        controls = [
            ("⏮", self._previous_track, 24),
            ("⏸", self._play_pause, 40),  # 中间按钮更大
            ("⏭", self._next_track, 24),
            ("🔀", self._toggle_shuffle, 20),
            ("🔁", self._toggle_repeat, 20)
        ]
        
        for i, (text, command, size) in enumerate(controls):
            btn = tk.Button(
                btn_container,
                text=text,
                font=("微软雅黑", size),
                bg=self.skin_engine.get_color("card"),
                fg=self.skin_engine.get_color("text_primary"),
                activebackground=self.skin_engine.get_color("hover"),
                activeforeground=self.skin_engine.get_color("text_primary"),
                borderwidth=0,
                relief="flat",
                cursor="hand2",
                command=command
            )
            btn.pack(side=tk.LEFT, padx=5)
            
            # 保存播放/暂停按钮引用
            if text == "⏸":
                self.play_pause_btn = btn
        
        # 进度条
        self.progress_frame = ttk.Frame(control_frame, style="Card.TFrame")
        self.progress_frame.pack(fill=tk.X, pady=(10, 0))
        
        # 自定义进度条
        self.progress_canvas = tk.Canvas(
            self.progress_frame,
            height=4,
            bg=self.skin_engine.get_color("surface"),
            highlightthickness=0
        )
        self.progress_canvas.pack(fill=tk.X)
        
        # 绘制进度条
        self.progress_bar = self.progress_canvas.create_rectangle(
            0, 0, 0, 4,
            fill=self.skin_engine.get_color("primary"),
            outline=""
        )
        
        # 进度滑块
        self.progress_handle = self.progress_canvas.create_oval(
            -6, -4, 6, 8,
            fill=self.skin_engine.get_color("primary"),
            outline=""
        )
        
        # 绑定拖动事件
        self.progress_canvas.bind("<Button-1>", self._on_progress_click)
        self.progress_canvas.bind("<B1-Motion>", self._on_progress_drag)
        self.progress_canvas.bind("<ButtonRelease-1>", self._on_progress_release)
        
        # 右侧:音量控制
        volume_frame = ttk.Frame(bottom_frame, style="Card.TFrame")
        volume_frame.pack(side=tk.RIGHT, fill=tk.Y, padx=20, pady=10)
        
        # 音量图标
        self.volume_btn = tk.Button(
            volume_frame,
            text="🔊",
            font=("微软雅黑", 16),
            bg=self.skin_engine.get_color("card"),
            fg=self.skin_engine.get_color("text_primary"),
            borderwidth=0,
            relief="flat",
            cursor="hand2",
            command=self._toggle_mute
        )
        self.volume_btn.pack(side=tk.LEFT, padx=(0, 10))
        
        # 音量滑块
        self.volume_var = tk.DoubleVar(value=self.player_controller.volume)
        self.volume_scale = ttk.Scale(
            volume_frame,
            from_=0,
            to=1,
            variable=self.volume_var,
            command=self._on_volume_change,
            style="Modern.Horizontal.TScale",
            length=100
        )
        self.volume_scale.pack(side=tk.LEFT)
    
    def _bind_events(self):
        """绑定事件"""
        # 窗口事件
        self.root.bind("<Control-o>", lambda e: self._open_file())
        self.root.bind("<space>", lambda e: self._play_pause())
        self.root.bind("<Left>", lambda e: self._seek_backward())
        self.root.bind("<Right>", lambda e: self._seek_forward())
        self.root.bind("<Control-Left>", lambda e: self._previous_track())
        self.root.bind("<Control-Right>", lambda e: self._next_track())
        self.root.bind("<Escape>", lambda e: self.root.quit())
        
        # 窗口关闭事件
        self.root.protocol("WM_DELETE_WINDOW", self._on_closing)
    
    def _setup_controller_callbacks(self):
        """设置控制器回调"""
        self.player_controller.on_state_change = self._on_player_state_change
        self.player_controller.on_position_change = self._on_position_change
        self.player_controller.on_metadata_change = self._on_metadata_change
    
    def _create_test_playlist(self):
        """创建测试播放列表"""
        test_tracks = [
            {"title": "夜空中最亮的星", "artist": "逃跑计划", "duration": 252},
            {"title": "平凡之路", "artist": "朴树", "duration": 260},
            {"title": "成都", "artist": "赵雷", "duration": 328},
            {"title": "光年之外", "artist": "G.E.M.邓紫棋", "duration": 238},
            {"title": "起风了", "artist": "买辣椒也用券", "duration": 315},
        ]
        
        for track in test_tracks:
            metadata = AudioMetadata(
                title=track["title"],
                artist=track["artist"],
                duration=track["duration"]
            )
            
            self.player_controller.add_to_playlist(
                f"模拟文件/{track['artist']} - {track['title']}.mp3",
                metadata
            )
            
            # 添加到列表显示
            display_text = f"{track['artist']} - {track['title']}"
            self.playlist_listbox.insert(tk.END, display_text)
    
    def _show_music_library(self):
        """显示音乐库页面"""
        # 清除现有内容
        for widget in self.content_frame.winfo_children():
            widget.destroy()
        
        self.content_title.config(text="我的音乐")
        
        # 创建工具栏
        toolbar = ttk.Frame(self.content_frame, style="Card.TFrame")
        toolbar.pack(fill=tk.X, pady=(0, 20))
        
        # 添加文件按钮
        ttk.Button(
            toolbar,
            text="添加文件",
            style="Primary.TButton",
            command=self._open_file
        ).pack(side=tk.LEFT, padx=(0, 10))
        
        ttk.Button(
            toolbar,
            text="添加文件夹",
            style="Secondary.TButton",
            command=self._open_folder
        ).pack(side=tk.LEFT)
        
        # 音乐表格
        columns = ("#", "标题", "艺术家", "时长")
        
        # 使用Treeview显示音乐
        tree = ttk.Treeview(
            self.content_frame,
            columns=columns,
            show="headings",
            style="Modern.Treeview"
        )
        
        # 设置列
        tree.heading("#", text="#")
        tree.heading("标题", text="标题")
        tree.heading("艺术家", text="艺术家")
        tree.heading("时长", text="时长")
        
        tree.column("#", width=50, anchor=tk.CENTER)
        tree.column("标题", width=200)
        tree.column("艺术家", width=150)
        tree.column("时长", width=80, anchor=tk.CENTER)
        
        # 添加滚动条
        scrollbar = ttk.Scrollbar(self.content_frame, orient=tk.VERTICAL, command=tree.yview)
        tree.configure(yscrollcommand=scrollbar.set)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        tree.pack(fill=tk.BOTH, expand=True)
        
        # 添加数据
        for i, track in enumerate(self.player_controller.playlist, 1):
            metadata = track['metadata']
            tree.insert("", tk.END, values=(
                i,
                metadata.title or "未知标题",
                metadata.artist or "未知艺术家",
                str(timedelta(seconds=int(metadata.duration)))[2:7] if metadata.duration > 0 else "00:00"
            ))
        
        # 绑定双击播放
        tree.bind("<Double-1>", lambda e: self._play_from_tree(tree))
    
    def _show_now_playing(self):
        """显示正在播放页面"""
        for widget in self.content_frame.winfo_children():
            widget.destroy()
        
        self.content_title.config(text="正在播放")
        
        # 创建正在播放页面
        now_playing_frame = ttk.Frame(self.content_frame, style="Card.TFrame")
        now_playing_frame.pack(fill=tk.BOTH, expand=True)
        
        if self.player_controller.current_metadata:
            ttk.Label(
                now_playing_frame,
                text=self.player_controller.current_metadata.title or "无标题",
                style="Title.TLabel"
            ).pack(pady=(50, 10))
            
            ttk.Label(
                now_playing_frame,
                text=self.player_controller.current_metadata.artist or "未知艺术家",
                style="Subtitle.TLabel"
            ).pack(pady=(0, 30))
        else:
            ttk.Label(
                now_playing_frame,
                text="没有正在播放的音乐",
                style="Title.TLabel"
            ).pack(pady=50)
            
            ttk.Label(
                now_playing_frame,
                text="请从左侧播放列表选择一首歌曲",
                style="Caption.TLabel"
            ).pack()
    
    def _show_visualizer(self):
        """显示可视化页面"""
        for widget in self.content_frame.winfo_children():
            widget.destroy()
        
        self.content_title.config(text="音频可视化")
        
        # 创建可视化页面
        viz_page = ttk.Frame(self.content_frame, style="Card.TFrame")
        viz_page.pack(fill=tk.BOTH, expand=True, padx=20, pady=20)
        
        # 可视化控制
        control_frame = ttk.Frame(viz_page, style="Card.TFrame")
        control_frame.pack(fill=tk.X, pady=(0, 20))
        
        ttk.Label(
            control_frame,
            text="可视化效果",
            style="Subtitle.TLabel"
        ).pack(anchor=tk.W, pady=(0, 10))
        
        # 效果选择
        effects_frame = ttk.Frame(control_frame, style="Card.TFrame")
        effects_frame.pack(fill=tk.X)
        
        effects = [("频谱分析", VisualizationMode.SPECTRUM),
                  ("粒子效果", VisualizationMode.PARTICLES)]
        
        self.viz_mode = tk.StringVar(value=VisualizationMode.SPECTRUM.value)
        
        for text, mode in effects:
            rb = ttk.Radiobutton(
                effects_frame,
                text=text,
                variable=self.viz_mode,
                value=mode.value,
                command=lambda m=mode: self.visualizer.set_mode(m)
            )
            rb.pack(anchor=tk.W, pady=2)
        
        # 可视化画布
        canvas_frame = ttk.Frame(viz_page, style="Card.TFrame")
        canvas_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 20))
        
        # 创建大画布
        self.viz_canvas = tk.Canvas(
            canvas_frame,
            bg=self.skin_engine.get_color("background"),
            highlightthickness=0
        )
        self.viz_canvas.pack(fill=tk.BOTH, expand=True)
        
        # 绘制示例可视化
        self._draw_example_visualization()
    
    def _draw_example_visualization(self):
        """绘制示例可视化"""
        width = 600
        height = 300
        
        # 清空画布
        self.viz_canvas.delete("all")
        
        # 绘制频谱
        center_x, center_y = width // 2, height // 2
        num_bars = 60
        max_radius = min(center_x, center_y) - 20
        
        for i in range(num_bars):
            angle = 2 * 3.14159 * i / num_bars
            radius = max_radius * 0.5 + random.random() * max_radius * 0.3
            
            x1 = center_x + (radius - 10) * math.cos(angle)
            y1 = center_y + (radius - 10) * math.sin(angle)
            x2 = center_x + radius * math.cos(angle)
            y2 = center_y + radius * math.sin(angle)
            
            # 颜色渐变
            hue = i / num_bars
            r = int(255 * (1 - hue))
            g = int(255 * hue)
            b = int(128 + 127 * math.sin(hue * 3.14159))
            
            self.viz_canvas.create_line(
                x1, y1, x2, y2,
                fill=f'#{r:02x}{g:02x}{b:02x}',
                width=3
            )
    
    def _show_skin_center(self):
        """显示皮肤中心"""
        for widget in self.content_frame.winfo_children():
            widget.destroy()
        
        self.content_title.config(text="皮肤中心")
        
        # 创建皮肤中心页面
        skin_frame = ttk.Frame(self.content_frame, style="Card.TFrame")
        skin_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=20)
        
        ttk.Label(
            skin_frame,
            text="选择主题",
            style="Subtitle.TLabel"
        ).pack(anchor=tk.W, pady=(0, 20))
        
        # 主题网格
        themes_grid = ttk.Frame(skin_frame, style="Card.TFrame")
        themes_grid.pack(fill=tk.BOTH, expand=True)
        
        # 主题列表
        themes = [
            {"name": "深色主题", "key": "dark", "desc": "深邃暗色,保护眼睛"},
            {"name": "浅色主题", "key": "light", "desc": "清新亮色,简洁明了"},
            {"name": "霓虹主题", "key": "neon", "desc": "炫酷霓虹,赛博风格"},
        ]
        
        # 创建主题卡片
        for i, theme in enumerate(themes):
            card = ttk.Frame(themes_grid, style="Card.TFrame")
            
            if i % 3 == 0:
                row = i // 3
                card.grid(row=row, column=0, padx=10, pady=10, sticky="nsew")
            elif i % 3 == 1:
                card.grid(row=row, column=1, padx=10, pady=10, sticky="nsew")
            else:
                card.grid(row=row, column=2, padx=10, pady=10, sticky="nsew")
            
            # 主题预览
            preview = tk.Canvas(
                card,
                width=200,
                height=120,
                bg=self.skin_engine.skins[theme["key"]].colors["primary"],
                highlightthickness=0
            )
            preview.pack(pady=10)
            
            # 绘制预览
            preview.create_rectangle(20, 20, 180, 100, 
                                   fill=self.skin_engine.skins[theme["key"]].colors["background"])
            preview.create_oval(40, 40, 80, 80, 
                              fill=self.skin_engine.skins[theme["key"]].colors["active"])
            
            # 主题信息
            ttk.Label(
                card,
                text=theme["name"],
                style="Body.TLabel"
            ).pack()
            
            ttk.Label(
                card,
                text=theme["desc"],
                style="Caption.TLabel"
            ).pack(pady=(0, 10))
            
            # 应用按钮
            ttk.Button(
                card,
                text="应用主题",
                style="Secondary.TButton",
                command=lambda k=theme["key"]: self._apply_skin(k)
            ).pack(pady=(0, 10))
        
        # 配置网格权重
        for i in range(3):
            themes_grid.columnconfigure(i, weight=1)
        themes_grid.rowconfigure(0, weight=1)
    
    def _show_settings(self):
        """显示设置页面"""
        for widget in self.content_frame.winfo_children():
            widget.destroy()
        
        self.content_title.config(text="设置")
        
        # 创建设置页面
        settings_frame = ttk.Frame(self.content_frame, style="Card.TFrame")
        settings_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=20)
        
        # 播放模式设置
        mode_frame = ttk.Frame(settings_frame, style="Card.TFrame")
        mode_frame.pack(fill=tk.X, pady=(0, 20))
        
        ttk.Label(
            mode_frame,
            text="播放模式:",
            style="Body.TLabel"
        ).pack(anchor=tk.W, pady=(0, 10))
        
        modes = [
            ("顺序播放", PlayMode.SEQUENCE),
            ("随机播放", PlayMode.SHUFFLE),
            ("单曲循环", PlayMode.REPEAT_ONE),
            ("列表循环", PlayMode.REPEAT_ALL)
        ]
        
        self.play_mode_var = tk.StringVar(value=self.player_controller.play_mode.value)
        
        for text, mode in modes:
            rb = ttk.Radiobutton(
                mode_frame,
                text=text,
                variable=self.play_mode_var,
                value=mode.value,
                command=lambda m=mode: setattr(self.player_controller, 'play_mode', m)
            )
            rb.pack(anchor=tk.W, pady=2)
        
        # 关于
        about_frame = ttk.Frame(settings_frame, style="Card.TFrame")
        about_frame.pack(fill=tk.X)
        
        ttk.Label(
            about_frame,
            text="关于 Modern Player",
            style="Body.TLabel"
        ).pack(anchor=tk.W, pady=(0, 10))
        
        ttk.Label(
            about_frame,
            text="版本: 1.0.0\n开发者: Python爱好者\n© 2024 Modern Player",
            style="Caption.TLabel"
        ).pack(anchor=tk.W)
    
    def _apply_skin(self, skin_key: str):
        """应用皮肤"""
        self.current_skin = skin_key
        self.skin_engine.apply_skin(skin_key)
        
        # 更新界面颜色
        self._update_ui_colors()
    
    def _update_ui_colors(self):
        """更新界面颜色"""
        # 更新播放列表颜色
        self.playlist_listbox.config(
            bg=self.skin_engine.get_color("card"),
            fg=self.skin_engine.get_color("text_primary"),
            selectbackground=self.skin_engine.get_color("primary"),
            selectforeground=self.skin_engine.get_color("text_primary")
        )
        
        # 更新歌词文本颜色
        self.lyrics_text.config(
            bg=self.skin_engine.get_color("card"),
            fg=self.skin_engine.get_color("text_secondary")
        )
        
        # 更新专辑封面
        self.album_canvas.config(bg=self.skin_engine.get_color("card"))
        
        # 更新进度条
        self.progress_canvas.config(bg=self.skin_engine.get_color("surface"))
        self.progress_canvas.itemconfig(self.progress_bar, 
                                       fill=self.skin_engine.get_color("primary"))
        self.progress_canvas.itemconfig(self.progress_handle, 
                                       fill=self.skin_engine.get_color("primary"))
    
    def _open_file(self):
        """打开文件"""
        filetypes = [
            ("音频文件", "*.mp3 *.wav *.flac *.ogg *.m4a *.aac"),
            ("所有文件", "*.*")
        ]
        
        filename = filedialog.askopenfilename(filetypes=filetypes)
        if filename:
            self._load_file(filename)
    
    def _open_folder(self):
        """打开文件夹"""
        folder = filedialog.askdirectory()
        if folder:
            import glob
            audio_files = []
            for ext in ['*.mp3', '*.wav', '*.flac', '*.ogg', '*.m4a']:
                audio_files.extend(glob.glob(os.path.join(folder, ext)))
            
            for file in audio_files:
                self._add_to_playlist(file)
    
    def _add_to_playlist(self, filepath: str):
        """添加到播放列表"""
        metadata = AudioMetadata(
            title=Path(filepath).stem,
            artist="未知艺术家",
            file_path=filepath
        )
        
        self.player_controller.add_to_playlist(filepath, metadata)
        
        # 添加到列表显示
        display_text = f"{metadata.artist} - {metadata.title}"
        self.playlist_listbox.insert(tk.END, display_text)
    
    def _load_file(self, filepath: str):
        """加载文件"""
        if self.player_controller.load_file(filepath):
            # 播放
            self.player_controller.play()
            
            # 更新UI
            self._update_now_playing_info()
    
    def _play_pause(self):
        """播放/暂停"""
        self.player_controller.pause()
    
    def _previous_track(self):
        """上一曲"""
        self.player_controller.previous_track()
    
    def _next_track(self):
        """下一曲"""
        self.player_controller.next_track()
    
    def _toggle_shuffle(self):
        """切换随机播放"""
        if self.player_controller.play_mode == PlayMode.SHUFFLE:
            self.player_controller.play_mode = PlayMode.SEQUENCE
        else:
            self.player_controller.play_mode = PlayMode.SHUFFLE
    
    def _toggle_repeat(self):
        """切换循环模式"""
        modes = [PlayMode.REPEAT_ONE, PlayMode.REPEAT_ALL, PlayMode.SEQUENCE]
        current_idx = modes.index(self.player_controller.play_mode) if self.player_controller.play_mode in modes else 0
        self.player_controller.play_mode = modes[(current_idx + 1) % len(modes)]
    
    def _toggle_mute(self):
        """静音/取消静音"""
        if self.player_controller.volume > 0:
            self.last_volume = self.player_controller.volume
            self.player_controller.set_volume(0)
            self.volume_btn.config(text="🔇")
        else:
            volume = self.last_volume if hasattr(self, 'last_volume') else 0.7
            self.player_controller.set_volume(volume)
            self.volume_btn.config(text="🔊")
        
        self.volume_var.set(self.player_controller.volume)
    
    def _on_volume_change(self, value):
        """音量变化"""
        volume = float(value)
        self.player_controller.set_volume(volume)
        
        # 更新音量图标
        if volume == 0:
            self.volume_btn.config(text="🔇")
        elif volume < 0.3:
            self.volume_btn.config(text="🔈")
        elif volume < 0.7:
            self.volume_btn.config(text="🔉")
        else:
            self.volume_btn.config(text="🔊")
    
    def _seek_backward(self):
        """快退10秒"""
        if self.player_controller.player_state in [PlayerState.PLAYING, PlayerState.PAUSED]:
            new_position = max(0, self.player_controller.position - 10)
            self.player_controller.seek(new_position)
    
    def _seek_forward(self):
        """快进10秒"""
        if self.player_controller.player_state in [PlayerState.PLAYING, PlayerState.PAUSED]:
            new_position = min(self.player_controller.duration, self.player_controller.position + 10)
            self.player_controller.seek(new_position)
    
    def _on_progress_click(self, event):
        """进度条点击"""
        self.is_dragging_progress = True
        self._update_progress_from_event(event)
    
    def _on_progress_drag(self, event):
        """进度条拖动"""
        if self.is_dragging_progress:
            self._update_progress_from_event(event)
    
    def _on_progress_release(self, event):
        """进度条释放"""
        if self.is_dragging_progress:
            self._update_progress_from_event(event)
            self.is_dragging_progress = False
    
    def _update_progress_from_event(self, event):
        """从事件更新进度"""
        canvas_width = self.progress_canvas.winfo_width()
        if canvas_width > 0:
            x = max(0, min(event.x, canvas_width))
            progress = x / canvas_width
            
            # 更新进度条显示
            self.progress_canvas.coords(self.progress_bar, 0, 0, x, 4)
            self.progress_canvas.coords(self.progress_handle, x-6, -4, x+6, 8)
            
            # 跳转到指定位置
            if self.player_controller.player_state in [PlayerState.PLAYING, PlayerState.PAUSED]:
                position = progress * self.player_controller.duration
                self.player_controller.seek(position)
    
    def _on_playlist_select(self, event):
        """播放列表选择事件"""
        selection = self.playlist_listbox.curselection()
        if selection:
            self.player_controller.current_index = selection[0]
            self.player_controller._play_current_index()
    
    def _play_from_tree(self, tree):
        """从树形控件播放"""
        selection = tree.selection()
        if selection:
            item = tree.item(selection[0])
            values = item['values']
            
            # 在播放列表中查找对应的歌曲
            for i, track in enumerate(self.player_controller.playlist):
                metadata = track['metadata']
                if (metadata.title == values[1] and 
                    metadata.artist == values[2]):
                    self.player_controller.current_index = i
                    self.player_controller._play_current_index()
                    break
    
    def _on_player_state_change(self, state: PlayerState):
        """播放器状态变化回调"""
        if state == PlayerState.PLAYING:
            self.play_pause_btn.config(text="⏸")
        elif state == PlayerState.PAUSED:
            self.play_pause_btn.config(text="▶")
        elif state == PlayerState.STOPPED:
            self.play_pause_btn.config(text="▶")
    
    def _on_position_change(self, position: float, duration: float):
        """播放位置变化回调"""
        # 格式化时间
        current_str = str(timedelta(seconds=int(position)))[2:7]
        total_str = str(timedelta(seconds=int(duration)))[2:7]
        
        self.time_label.config(text=f"{current_str} / {total_str}")
        
        # 更新进度条
        if not self.is_dragging_progress and duration > 0:
            canvas_width = self.progress_canvas.winfo_width()
            if canvas_width > 0:
                progress = position / duration
                x = progress * canvas_width
                self.progress_canvas.coords(self.progress_bar, 0, 0, x, 4)
                self.progress_canvas.coords(self.progress_handle, x-6, -4, x+6, 8)
    
    def _on_metadata_change(self, metadata: AudioMetadata):
        """元数据变化回调"""
        self._update_now_playing_info()
    
    def _update_now_playing_info(self):
        """更新正在播放信息"""
        if self.player_controller.current_metadata:
            self.song_title.config(text=self.player_controller.current_metadata.title or "未知标题")
            self.song_artist.config(text=self.player_controller.current_metadata.artist or "未知艺术家")
            
            display_text = self.player_controller.current_metadata.get_display_title()
            self.now_playing_label.config(text=display_text)
            
            # 更新歌词
            self.lyrics_text.config(state=tk.NORMAL)
            self.lyrics_text.delete(1.0, tk.END)
            self.lyrics_text.insert(tk.END, f"《{self.player_controller.current_metadata.title}》\n\n")
            self.lyrics_text.insert(tk.END, f"艺术家: {self.player_controller.current_metadata.artist}\n")
            self.lyrics_text.insert(tk.END, f"\n--- 正在播放 ---\n")
            self.lyrics_text.config(state=tk.DISABLED)
    
    def _on_closing(self):
        """窗口关闭事件"""
        # 停止播放
        self.player_controller.stop()
        # 停止可视化动画
        if self.visualizer:
            self.visualizer.stop()
        # 清理资源
        self.player_controller.cleanup()
        # 关闭窗口
        self.root.destroy()
    
    def run(self):
        """运行播放器"""
        self.root.mainloop()

# ============================
# 6. 主程序入口
# ============================

def main():
    """主函数"""
    # 创建播放器实例
    player = ModernPlayer()
    
    # 运行主循环
    try:
        player.run()
    except KeyboardInterrupt:
        print("\n播放器被用户中断")
    except Exception as e:
        print(f"播放器运行出错: {e}")
        import traceback
        traceback.print_exc()

if __name__ == "__main__":
    main()

11. 总结与展望

通过本项目的实现,我们掌握了现代化Python GUI开发的完整技术栈:

  1. ttk深度应用:掌握了现代化控件的使用和定制

  2. 皮肤系统设计:实现了可扩展的皮肤引擎

  3. 音频处理:构建了完整的音频播放和处理流水线

  4. 架构设计:实践了分层架构和模块化设计思想

  5. 性能优化:学习了GUI程序的性能优化技巧

相关推荐
C'ᴇsᴛ.小琳 ℡2 小时前
架构重构的技术
重构·架构
吃一根烤肠2 小时前
NumPy 内置函数与数组运算完全指南
python·numpy
Mr_Xuhhh2 小时前
深入理解Java高级特性:反射、枚举与Lambda表达式实战指南
开发语言·python
派大星~课堂2 小时前
【力扣-94.二叉树的中序遍历】Python笔记
笔记·python·leetcode
SQVIoMPLe3 小时前
python-langchain框架(3-7-提取pdf中的图片 )
python·langchain·pdf
Ulyanov3 小时前
音视频分离与音频处理核心技术深度解析 从MP4到高品质音乐文件的完整技术实现
python·音视频
萝卜白菜。3 小时前
TongWeb8.0 JNDI缓存
开发语言·python·缓存
xiaoshuaishuai83 小时前
PyCharm性能调优
ide·python·pycharm
2501_921930833 小时前
Flutter for OpenHarmony三方库适配实战:image_picker 图片视频选择
flutter·音视频