基于ttk的现代化Python音视频播放器:UI设计与可视化技术深度解析

1. 引言:重新定义Python GUI开发的现代可能性

在Python GUI开发领域,tkinter/ttk长期以来被视为"功能有限"、"界面陈旧"的代表。然而,通过现代化设计理念和深度技术定制,我们可以完全颠覆这一认知。本文将通过一个完整的音视频播放器项目,系统讲解如何利用tkinter/ttk构建具有现代外观、支持皮肤切换、包含丰富可视化效果的专业级应用程序。

1.1 技术选型与学习价值

为什么选择tkinter/ttk作为教学案例?

技术考量 教学价值 实际应用优势
标准库依赖 无额外安装,学习门槛低 部署简单,兼容性强
轻量级框架 代码清晰,易于理解 启动快速,资源占用少
跨平台特性 学习一次,多处适用 Windows/macOS/Linux原生支持
可扩展性 可深入底层定制 满足复杂界面需求
广泛应用 技能适用范围广 工具开发、桌面应用、原型设计

本项目核心技术栈:

  • 界面框架:tkinter/ttk深度定制

  • 音频处理:Pygame + 模拟数据引擎

  • 可视化渲染:Canvas + 自定义绘图引擎

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

  • 架构设计:MVC模式 + 模块化组件

2. 架构设计与实现思路

2.1 整体架构设计

2.2 模块接口设计

python 复制代码
"""
模块接口定义
定义各模块之间的接口契约,确保模块间的松耦合
"""

from abc import ABC, abstractmethod
from typing import Dict, List, Optional, Any, Callable
from enum import Enum, auto

class PlayerState(Enum):
    """播放器状态枚举"""
    STOPPED = auto()
    PLAYING = auto()
    PAUSED = auto()
    BUFFERING = auto()

class PlayMode(Enum):
    """播放模式枚举"""
    SEQUENCE = auto()      # 顺序播放
    REPEAT_ONE = auto()    # 单曲循环
    REPEAT_ALL = auto()    # 列表循环
    SHUFFLE = auto()       # 随机播放

# ==================== 接口定义 ====================

class IPlayerController(ABC):
    """播放控制器接口"""
    
    @abstractmethod
    def play(self) -> bool:
        """开始播放"""
        pass
    
    @abstractmethod
    def pause(self) -> bool:
        """暂停播放"""
        pass
    
    @abstractmethod
    def stop(self) -> bool:
        """停止播放"""
        pass
    
    @abstractmethod
    def next_track(self) -> bool:
        """下一曲"""
        pass
    
    @abstractmethod
    def previous_track(self) -> bool:
        """上一曲"""
        pass
    
    @abstractmethod
    def seek(self, position: float) -> bool:
        """跳转到指定位置"""
        pass
    
    @abstractmethod
    def set_volume(self, volume: float) -> bool:
        """设置音量"""
        pass
    
    @abstractmethod
    def get_state(self) -> PlayerState:
        """获取播放状态"""
        pass
    
    @abstractmethod
    def get_progress(self) -> tuple[float, float]:
        """获取播放进度 (当前时间, 总时长)"""
        pass

class IVisualizer(ABC):
    """可视化器接口"""
    
    @abstractmethod
    def update_data(self, audio_data: List[float]) -> None:
        """更新音频数据"""
        pass
    
    @abstractmethod
    def set_mode(self, mode: str) -> None:
        """设置可视化模式"""
        pass
    
    @abstractmethod
    def set_color_scheme(self, scheme: str) -> None:
        """设置颜色方案"""
        pass
    
    @abstractmethod
    def start_animation(self) -> None:
        """开始动画"""
        pass
    
    @abstractmethod
    def stop_animation(self) -> None:
        """停止动画"""
        pass

class ISkinEngine(ABC):
    """皮肤引擎接口"""
    
    @abstractmethod
    def apply_skin(self, skin_name: str, animate: bool = True) -> bool:
        """应用皮肤"""
        pass
    
    @abstractmethod
    def get_current_colors(self) -> Dict[str, str]:
        """获取当前颜色配置"""
        pass
    
    @abstractmethod
    def register_component(self, component_id: str, 
                          component: Any, style_config: Dict) -> None:
        """注册组件样式"""
        pass

class IPlaylistManager(ABC):
    """播放列表管理器接口"""
    
    @abstractmethod
    def add_track(self, file_path: str, metadata: Dict = None) -> bool:
        """添加曲目"""
        pass
    
    @abstractmethod
    def remove_track(self, index: int) -> bool:
        """移除曲目"""
        pass
    
    @abstractmethod
    def get_track(self, index: int) -> Optional[Dict]:
        """获取曲目信息"""
        pass
    
    @abstractmethod
    def get_current_track(self) -> Optional[Dict]:
        """获取当前曲目"""
        pass
    
    @abstractmethod
    def set_current_index(self, index: int) -> bool:
        """设置当前播放索引"""
        pass

# ==================== 事件系统 ====================

class EventType(Enum):
    """事件类型枚举"""
    PLAYER_STATE_CHANGED = auto()
    PLAYER_PROGRESS_UPDATED = auto()
    PLAYLIST_CHANGED = auto()
    SKIN_CHANGED = auto()
    VOLUME_CHANGED = auto()

class Event:
    """事件对象"""
    
    def __init__(self, event_type: EventType, data: Any = None):
        self.type = event_type
        self.data = data
        self.timestamp = time.time()

class IEventDispatcher(ABC):
    """事件分发器接口"""
    
    @abstractmethod
    def subscribe(self, event_type: EventType, 
                  callback: Callable[[Event], None]) -> None:
        """订阅事件"""
        pass
    
    @abstractmethod
    def unsubscribe(self, event_type: EventType,
                    callback: Callable[[Event], None]) -> None:
        """取消订阅"""
        pass
    
    @abstractmethod
    def dispatch(self, event: Event) -> None:
        """分发事件"""
        pass

3. UI布局设计与实现

3.1 界面布局架构

3.2 布局管理器实现

python 复制代码
"""
现代化布局管理器
实现响应式网格布局系统
"""
import tkinter as tk
from tkinter import ttk
from typing import Dict, List, Tuple, Optional, Any, Callable
import math

class ModernLayoutManager:
    """现代化布局管理器"""
    
    def __init__(self, root_window):
        self.root = root_window
        self.breakpoints = {
            'xs': 0,      # 手机 (<576px)
            'sm': 576,    # 平板 (≥576px)
            'md': 768,    # 小桌面 (≥768px)
            'lg': 992,    # 桌面 (≥992px)
            'xl': 1200,   # 大桌面 (≥1200px)
            'xxl': 1400   # 超大桌面 (≥1400px)
        }
        
        self.current_breakpoint = 'xs'
        self.components = {}  # 注册的组件
        self.layout_configs = {}  # 布局配置
        self.grid_system = GridSystem(columns=12, gutter=16)
        
        # 初始化
        self._setup_event_listeners()
    
    def _setup_event_listeners(self):
        """设置事件监听器"""
        # 窗口大小变化监听
        self.root.bind('<Configure>', self._on_window_resize)
        
        # 初始化时检查一次窗口大小
        self.root.after(100, self._check_window_size)
    
    def _on_window_resize(self, event):
        """窗口大小变化事件处理"""
        if event.widget == self.root:
            self._update_layout(event.width, event.height)
    
    def _check_window_size(self):
        """检查窗口大小"""
        width = self.root.winfo_width()
        height = self.root.winfo_height()
        if width > 1 and height > 1:  # 确保窗口已显示
            self._update_layout(width, height)
    
    def _update_layout(self, width: int, height: int):
        """更新布局"""
        # 确定当前断点
        new_breakpoint = self._get_breakpoint(width)
        
        if new_breakpoint != self.current_breakpoint:
            self.current_breakpoint = new_breakpoint
            self._apply_responsive_rules()
    
    def _get_breakpoint(self, width: int) -> str:
        """根据宽度获取断点"""
        if width >= self.breakpoints['xxl']:
            return 'xxl'
        elif width >= self.breakpoints['xl']:
            return 'xl'
        elif width >= self.breakpoints['lg']:
            return 'lg'
        elif width >= self.breakpoints['md']:
            return 'md'
        elif width >= self.breakpoints['sm']:
            return 'sm'
        else:
            return 'xs'
    
    def register_component(self, component_id: str, component: Any, 
                          layout_rules: Dict):
        """注册组件及其布局规则"""
        self.components[component_id] = {
            'component': component,
            'rules': layout_rules
        }
    
    def _apply_responsive_rules(self):
        """应用响应式规则"""
        for component_id, data in self.components.items():
            component = data['component']
            rules = data['rules']
            
            # 获取当前断点的规则
            current_rule = rules.get(self.current_breakpoint, 
                                    rules.get('default', {}))
            
            if current_rule:
                self._apply_component_rule(component, current_rule)
    
    def _apply_component_rule(self, component, rule: Dict):
        """应用组件规则"""
        # 处理显示/隐藏
        if 'visible' in rule:
            if rule['visible']:
                component.grid()
            else:
                component.grid_remove()
        
        # 处理位置
        if 'position' in rule:
            pos = rule['position']
            if pos == 'absolute':
                component.place(**rule.get('place_config', {}))
            elif pos == 'grid':
                component.grid(**rule.get('grid_config', {}))
            elif pos == 'pack':
                component.pack(**rule.get('pack_config', {}))
        
        # 处理尺寸
        if 'size' in rule:
            size_config = rule['size']
            if 'width' in size_config:
                component.config(width=size_config['width'])
            if 'height' in size_config:
                component.config(height=size_config['height'])
        
        # 处理样式
        if 'style' in rule:
            if hasattr(component, 'config'):
                component.config(**rule['style'])

class GridSystem:
    """网格系统"""
    
    def __init__(self, columns: int = 12, gutter: int = 16):
        self.columns = columns
        self.gutter = gutter
        self.row_gutter = gutter // 2
    
    def create_container(self, parent, **kwargs) -> ttk.Frame:
        """创建网格容器"""
        container = ttk.Frame(parent, **kwargs)
        
        # 配置网格列
        for i in range(self.columns):
            container.columnconfigure(i, weight=1, minsize=0)
        
        return container
    
    def place_widget(self, widget, container: ttk.Frame, 
                    col_span: int = 12, row_span: int = 1,
                    col_start: int = 0, row_start: int = 0,
                    sticky: str = 'nsew', **kwargs):
        """在网格中放置控件"""
        # 计算内边距
        padx = (self.gutter, 0) if col_start + col_span < self.columns else (self.gutter, self.gutter)
        pady = (self.row_gutter, 0) if row_start + row_span < 1 else (self.row_gutter, self.row_gutter)
        
        widget.grid(
            row=row_start,
            column=col_start,
            rowspan=row_span,
            columnspan=col_span,
            sticky=sticky,
            padx=padx,
            pady=pady,
            **kwargs
        )
    
    def calculate_width(self, container_width: int, cols: int) -> int:
        """计算指定列数的宽度"""
        total_gutter = self.gutter * (self.columns - 1)
        available_width = container_width - total_gutter
        column_width = available_width / self.columns
        return int(column_width * cols + self.gutter * (cols - 1))

class CardWidget(ttk.Frame):
    """卡片组件"""
    
    def __init__(self, parent, title: str = None, 
                 padding: int = 16, elevation: int = 1, **kwargs):
        super().__init__(parent, **kwargs)
        
        self.title = title
        self.padding = padding
        self.elevation = elevation
        
        # 创建卡片内容
        self._create_card()
    
    def _create_card(self):
        """创建卡片"""
        # 卡片标题
        if self.title:
            title_label = ttk.Label(
                self,
                text=self.title,
                font=('Microsoft YaHei', 12, 'bold')
            )
            title_label.pack(anchor='w', padx=self.padding, 
                           pady=(self.padding, self.padding//2))
        
        # 内容容器
        self.content_frame = ttk.Frame(self)
        self.content_frame.pack(fill=tk.BOTH, expand=True, 
                               padx=self.padding, pady=(0, self.padding))
    
    def get_content_frame(self) -> ttk.Frame:
        """获取内容容器"""
        return self.content_frame

class ThreeColumnLayout:
    """三栏布局实现"""
    
    def __init__(self, parent, layout_manager: ModernLayoutManager):
        self.parent = parent
        self.layout_manager = layout_manager
        
        # 创建主容器
        self.main_container = ttk.Frame(parent)
        self.main_container.pack(fill=tk.BOTH, expand=True)
        
        # 创建三栏
        self._create_columns()
        
        # 注册布局规则
        self._register_layout_rules()
    
    def _create_columns(self):
        """创建三栏"""
        # 左侧边栏
        self.left_sidebar = CardWidget(self.main_container, title="播放列表")
        self.left_sidebar.grid(row=0, column=0, sticky='ns', padx=(0, 8))
        
        # 中心内容区
        self.center_content = CardWidget(self.main_container, title="正在播放")
        self.center_content.grid(row=0, column=1, sticky='nsew', padx=4)
        
        # 右侧边栏
        self.right_sidebar = CardWidget(self.main_container, title="播放控制")
        self.right_sidebar.grid(row=0, column=2, sticky='ns', padx=(8, 0))
        
        # 配置权重
        self.main_container.columnconfigure(1, weight=1)
        self.main_container.rowconfigure(0, weight=1)
    
    def _register_layout_rules(self):
        """注册布局规则"""
        # 左侧边栏规则
        self.layout_manager.register_component(
            'left_sidebar',
            self.left_sidebar,
            {
                'xs': {'visible': False},
                'sm': {'visible': True, 'position': 'grid', 
                      'grid_config': {'row': 0, 'column': 0, 'sticky': 'ns'}},
                'md': {'visible': True, 'position': 'grid',
                      'grid_config': {'row': 0, 'column': 0, 'sticky': 'ns'}},
                'lg': {'visible': True, 'position': 'grid',
                      'grid_config': {'row': 0, 'column': 0, 'sticky': 'ns'}},
                'xl': {'visible': True, 'position': 'grid',
                      'grid_config': {'row': 0, 'column': 0, 'sticky': 'ns'}},
                'xxl': {'visible': True, 'position': 'grid',
                       'grid_config': {'row': 0, 'column': 0, 'sticky': 'ns'}}
            }
        )
        
        # 中心内容区规则
        self.layout_manager.register_component(
            'center_content',
            self.center_content,
            {
                'xs': {'visible': True, 'position': 'grid',
                      'grid_config': {'row': 0, 'column': 0, 'columnspan': 1, 'sticky': 'nsew'}},
                'sm': {'visible': True, 'position': 'grid',
                      'grid_config': {'row': 0, 'column': 1, 'columnspan': 2, 'sticky': 'nsew'}},
                'md': {'visible': True, 'position': 'grid',
                      'grid_config': {'row': 0, 'column': 1, 'columnspan': 2, 'sticky': 'nsew'}},
                'lg': {'visible': True, 'position': 'grid',
                      'grid_config': {'row': 0, 'column': 1, 'columnspan': 1, 'sticky': 'nsew'}},
                'xl': {'visible': True, 'position': 'grid',
                      'grid_config': {'row': 0, 'column': 1, 'columnspan': 1, 'sticky': 'nsew'}},
                'xxl': {'visible': True, 'position': 'grid',
                       'grid_config': {'row': 0, 'column': 1, 'columnspan': 1, 'sticky': 'nsew'}}
            }
        )
        
        # 右侧边栏规则
        self.layout_manager.register_component(
            'right_sidebar',
            self.right_sidebar,
            {
                'xs': {'visible': False},
                'sm': {'visible': False},
                'md': {'visible': True, 'position': 'grid',
                      'grid_config': {'row': 0, 'column': 3, 'sticky': 'ns'}},
                'lg': {'visible': True, 'position': 'grid',
                      'grid_config': {'row': 0, 'column': 2, 'sticky': 'ns'}},
                'xl': {'visible': True, 'position': 'grid',
                      'grid_config': {'row': 0, 'column': 2, 'sticky': 'ns'}},
                'xxl': {'visible': True, 'position': 'grid',
                       'grid_config': {'row': 0, 'column': 2, 'sticky': 'ns'}}
            }
        )
    
    def get_left_sidebar(self) -> CardWidget:
        """获取左侧边栏"""
        return self.left_sidebar
    
    def get_center_content(self) -> CardWidget:
        """获取中心内容区"""
        return self.center_content
    
    def get_right_sidebar(self) -> CardWidget:
        """获取右侧边栏"""
        return self.right_sidebar

4. 播放控制逻辑设计

4.1 播放器状态机设计

4.2 播放控制器实现

python 复制代码
"""
播放控制器实现
基于状态机的播放控制逻辑
"""
import time
import random
from typing import List, Dict, Optional, Tuple, Callable
from enum import Enum, auto
from dataclasses import dataclass, field
from collections import deque

@dataclass
class TrackInfo:
    """曲目信息"""
    title: str
    artist: str
    album: str
    duration: float  # 秒
    file_path: str
    metadata: Dict = field(default_factory=dict)

class PlayerController:
    """播放控制器"""
    
    def __init__(self, event_dispatcher=None):
        # 状态管理
        self.state = PlayerState.STOPPED
        self.play_mode = PlayMode.SEQUENCE
        
        # 播放列表
        self.playlist: List[TrackInfo] = []
        self.current_index = 0
        self.shuffled_indices: List[int] = []
        
        # 播放控制
        self.position = 0.0  # 当前播放位置(秒)
        self.volume = 0.7
        self.is_muted = False
        self.last_volume = 0.7
        
        # 事件系统
        self.event_dispatcher = event_dispatcher
        
        # 模拟播放
        self._play_start_time = 0
        self._paused_at = 0
        self._is_simulating = False
        self._simulation_thread = None
        
        # 回调函数
        self.on_state_change: Optional[Callable] = None
        self.on_progress_change: Optional[Callable] = None
        self.on_track_change: Optional[Callable] = None
    
    def _get_current_track(self) -> Optional[TrackInfo]:
        """获取当前曲目"""
        if not self.playlist:
            return None
        
        if 0 <= self.current_index < len(self.playlist):
            return self.playlist[self.current_index]
        return None
    
    def play(self) -> bool:
        """开始播放"""
        if self.state == PlayerState.PLAYING:
            return True
        
        if self.state == PlayerState.PAUSED:
            # 从暂停状态恢复
            self.state = PlayerState.PLAYING
            self._resume_simulation()
        else:
            # 从头开始播放
            track = self._get_current_track()
            if not track:
                return False
            
            self.state = PlayerState.PLAYING
            self.position = 0.0
            self._start_simulation(track.duration)
        
        # 触发事件
        self._dispatch_state_change()
        if self.on_state_change:
            self.on_state_change(self.state)
        
        return True
    
    def _start_simulation(self, duration: float):
        """开始模拟播放"""
        self._play_start_time = time.time()
        self._is_simulating = True
        self._start_progress_updater(duration)
    
    def _resume_simulation(self):
        """恢复模拟播放"""
        self._play_start_time = time.time() - self.position
        self._is_simulating = True
    
    def _start_progress_updater(self, duration: float):
        """启动进度更新器"""
        def update_progress():
            while self._is_simulating and self.state == PlayerState.PLAYING:
                current_time = time.time()
                self.position = current_time - self._play_start_time
                
                # 限制位置不超过时长
                if self.position > duration:
                    self.position = duration
                    self._on_playback_end()
                    break
                
                # 触发进度更新
                if self.on_progress_change:
                    self.on_progress_change(self.position, duration)
                
                if self.event_dispatcher:
                    self.event_dispatcher.dispatch(Event(
                        EventType.PLAYER_PROGRESS_UPDATED,
                        {'current': self.position, 'total': duration}
                    ))
                
                time.sleep(0.1)  # 100ms更新一次
        
        import threading
        self._simulation_thread = threading.Thread(
            target=update_progress, 
            daemon=True
        )
        self._simulation_thread.start()
    
    def pause(self) -> bool:
        """暂停播放"""
        if self.state != PlayerState.PLAYING:
            return False
        
        self.state = PlayerState.PAUSED
        self._paused_at = time.time()
        self._is_simulating = False
        
        # 触发事件
        self._dispatch_state_change()
        if self.on_state_change:
            self.on_state_change(self.state)
        
        return True
    
    def stop(self) -> bool:
        """停止播放"""
        if self.state == PlayerState.STOPPED:
            return True
        
        self.state = PlayerState.STOPPED
        self.position = 0.0
        self._is_simulating = False
        
        # 触发事件
        self._dispatch_state_change()
        if self.on_state_change:
            self.on_state_change(self.state)
        
        return True
    
    def _on_playback_end(self):
        """播放结束处理"""
        self.stop()
        
        # 根据播放模式处理
        if self.play_mode == PlayMode.REPEAT_ONE:
            # 单曲循环
            self.play()
        elif self.play_mode == PlayMode.REPEAT_ALL:
            # 列表循环
            self.next_track()
        elif self.play_mode == PlayMode.SHUFFLE:
            # 随机播放
            self._play_random_track()
        else:  # SEQUENCE
            # 顺序播放
            if self.current_index < len(self.playlist) - 1:
                self.next_track()
    
    def _play_random_track(self):
        """播放随机曲目"""
        if not self.playlist:
            return
        
        # 初始化洗牌列表
        if not self.shuffled_indices:
            self.shuffled_indices = list(range(len(self.playlist)))
            random.shuffle(self.shuffled_indices)
        
        # 获取下一个随机索引
        if not self.shuffled_indices:
            self.shuffled_indices = list(range(len(self.playlist)))
            random.shuffle(self.shuffled_indices)
        
        next_index = self.shuffled_indices.pop(0)
        self.current_index = next_index
        self.play()
    
    def next_track(self) -> bool:
        """下一曲"""
        if not self.playlist:
            return False
        
        if self.play_mode == PlayMode.SHUFFLE:
            self._play_random_track()
        else:
            if self.current_index < len(self.playlist) - 1:
                self.current_index += 1
                self.play()
            elif self.play_mode == PlayMode.REPEAT_ALL:
                self.current_index = 0
                self.play()
            else:
                self.stop()
        
        return True
    
    def previous_track(self) -> bool:
        """上一曲"""
        if not self.playlist:
            return False
        
        if self.current_index > 0:
            self.current_index -= 1
            self.play()
        elif self.play_mode == PlayMode.REPEAT_ALL:
            self.current_index = len(self.playlist) - 1
            self.play()
        
        return True
    
    def seek(self, position: float) -> bool:
        """跳转到指定位置"""
        track = self._get_current_track()
        if not track:
            return False
        
        # 限制位置范围
        position = max(0, min(position, track.duration))
        
        if self.state == PlayerState.PLAYING:
            self._play_start_time = time.time() - position
        elif self.state == PlayerState.PAUSED:
            self.position = position
        
        # 触发进度更新
        if self.on_progress_change:
            self.on_progress_change(position, track.duration)
        
        return True
    
    def set_volume(self, volume: float) -> bool:
        """设置音量"""
        volume = max(0.0, min(1.0, volume))
        self.volume = volume
        
        if self.event_dispatcher:
            self.event_dispatcher.dispatch(Event(
                EventType.VOLUME_CHANGED,
                {'volume': volume}
            ))
        
        return True
    
    def toggle_mute(self) -> bool:
        """切换静音"""
        if self.is_muted:
            self.is_muted = False
            self.set_volume(self.last_volume)
        else:
            self.is_muted = True
            self.last_volume = self.volume
            self.set_volume(0.0)
        
        return True
    
    def set_play_mode(self, mode: PlayMode) -> bool:
        """设置播放模式"""
        self.play_mode = mode
        if mode == PlayMode.SHUFFLE:
            self.shuffled_indices = list(range(len(self.playlist)))
            random.shuffle(self.shuffled_indices)
        
        return True
    
    def add_to_playlist(self, track: TrackInfo) -> bool:
        """添加到播放列表"""
        self.playlist.append(track)
        
        if self.event_dispatcher:
            self.event_dispatcher.dispatch(Event(
                EventType.PLAYLIST_CHANGED,
                {'action': 'add', 'track': track}
            ))
        
        return True
    
    def remove_from_playlist(self, index: int) -> bool:
        """从播放列表移除"""
        if 0 <= index < len(self.playlist):
            removed = self.playlist.pop(index)
            
            if self.event_dispatcher:
                self.event_dispatcher.dispatch(Event(
                    EventType.PLAYLIST_CHANGED,
                    {'action': 'remove', 'track': removed}
                ))
            
            return True
        return False
    
    def _dispatch_state_change(self):
        """分发状态变化事件"""
        if self.event_dispatcher:
            self.event_dispatcher.dispatch(Event(
                EventType.PLAYER_STATE_CHANGED,
                {'state': self.state}
            ))
    
    def get_state(self) -> PlayerState:
        """获取播放状态"""
        return self.state
    
    def get_progress(self) -> Tuple[float, float]:
        """获取播放进度"""
        track = self._get_current_track()
        if track:
            return self.position, track.duration
        return 0.0, 0.0
    
    def get_current_track_info(self) -> Optional[TrackInfo]:
        """获取当前曲目信息"""
        return self._get_current_track()

5. 音频可视化技术

5.1 可视化架构设计

5.2 音频数据模拟器

python 复制代码
"""
音频数据模拟器
生成模拟音频数据用于可视化演示
"""
import numpy as np
import math
import time
from typing import List, Tuple, Optional
from dataclasses import dataclass
from enum import Enum, auto

class AudioSignalType(Enum):
    """音频信号类型"""
    SINE = auto()           # 正弦波
    SQUARE = auto()         # 方波
    SAWTOOTH = auto()       # 锯齿波
    NOISE = auto()          # 噪声
    CHORD = auto()          # 和弦
    BEAT = auto()           # 节拍

@dataclass
class AudioSignalConfig:
    """音频信号配置"""
    signal_type: AudioSignalType
    frequency: float = 440.0  # 频率 (Hz)
    amplitude: float = 1.0    # 振幅 (0.0-1.0)
    phase: float = 0.0        # 相位
    duration: float = 1.0     # 时长 (秒)
    sample_rate: int = 44100  # 采样率

class AudioDataSimulator:
    """音频数据模拟器"""
    
    def __init__(self, sample_rate: int = 44100):
        self.sample_rate = sample_rate
        self.time_offset = 0.0
        self.signals: List[AudioSignalConfig] = []
        self.beat_detector = BeatDetector()
        
        # 添加默认信号
        self._add_default_signals()
    
    def _add_default_signals(self):
        """添加默认信号"""
        # 基础频率信号
        self.signals.extend([
            AudioSignalConfig(
                signal_type=AudioSignalType.SINE,
                frequency=440.0,  # A4
                amplitude=0.5,
                phase=0.0
            ),
            AudioSignalConfig(
                signal_type=AudioSignalType.SINE,
                frequency=554.37,  # C#5
                amplitude=0.3,
                phase=0.2
            ),
            AudioSignalConfig(
                signal_type=AudioSignalType.SINE,
                frequency=659.25,  # E5
                amplitude=0.2,
                phase=0.4
            )
        ])
    
    def generate_audio_data(self, duration: float = 0.1) -> np.ndarray:
        """生成音频数据"""
        num_samples = int(self.sample_rate * duration)
        t = np.linspace(
            self.time_offset,
            self.time_offset + duration,
            num_samples,
            endpoint=False
        )
        
        # 更新时间偏移
        self.time_offset += duration
        
        # 生成混合信号
        mixed_signal = np.zeros(num_samples)
        
        for signal_config in self.signals:
            signal = self._generate_signal(t, signal_config)
            mixed_signal += signal
        
        # 添加噪声
        noise = np.random.normal(0, 0.05, num_samples)
        mixed_signal += noise
        
        # 归一化
        if np.max(np.abs(mixed_signal)) > 0:
            mixed_signal = mixed_signal / np.max(np.abs(mixed_signal))
        
        # 检测节拍
        self.beat_detector.process_signal(mixed_signal)
        
        return mixed_signal
    
    def _generate_signal(self, t: np.ndarray, 
                        config: AudioSignalConfig) -> np.ndarray:
        """生成单个信号"""
        if config.signal_type == AudioSignalType.SINE:
            return self._generate_sine_wave(t, config)
        elif config.signal_type == AudioSignalType.SQUARE:
            return self._generate_square_wave(t, config)
        elif config.signal_type == AudioSignalType.SAWTOOTH:
            return self._generate_sawtooth_wave(t, config)
        elif config.signal_type == AudioSignalType.NOISE:
            return self._generate_noise(len(t), config)
        elif config.signal_type == AudioSignalType.CHORD:
            return self._generate_chord(t, config)
        else:
            return np.zeros(len(t))
    
    def _generate_sine_wave(self, t: np.ndarray, 
                           config: AudioSignalConfig) -> np.ndarray:
        """生成正弦波"""
        return (config.amplitude * 
                np.sin(2 * np.pi * config.frequency * t + config.phase))
    
    def _generate_square_wave(self, t: np.ndarray, 
                             config: AudioSignalConfig) -> np.ndarray:
        """生成方波"""
        raw = np.sin(2 * np.pi * config.frequency * t + config.phase)
        return config.amplitude * np.sign(raw)
    
    def _generate_sawtooth_wave(self, t: np.ndarray, 
                               config: AudioSignalConfig) -> np.ndarray:
        """生成锯齿波"""
        phase = (config.frequency * t + config.phase / (2 * np.pi)) % 1.0
        return config.amplitude * (2 * phase - 1)
    
    def _generate_noise(self, num_samples: int, 
                       config: AudioSignalConfig) -> np.ndarray:
        """生成噪声"""
        return config.amplitude * np.random.normal(0, 1, num_samples)
    
    def _generate_chord(self, t: np.ndarray, 
                       config: AudioSignalConfig) -> np.ndarray:
        """生成和弦"""
        # 大三和弦
        frequencies = [
            config.frequency,           # 根音
            config.frequency * 1.25,    # 大三度
            config.frequency * 1.5,     # 纯五度
        ]
        
        amplitudes = [config.amplitude * 0.6, 
                     config.amplitude * 0.4, 
                     config.amplitude * 0.3]
        
        chord = np.zeros_like(t)
        for freq, amp in zip(frequencies, amplitudes):
            chord += amp * np.sin(2 * np.pi * freq * t)
        
        return chord
    
    def get_beat_strength(self) -> float:
        """获取节拍强度"""
        return self.beat_detector.get_beat_strength()
    
    def get_spectrum_data(self, audio_data: np.ndarray, 
                         fft_size: int = 2048) -> np.ndarray:
        """获取频谱数据"""
        if len(audio_data) < fft_size:
            # 补零
            padded = np.zeros(fft_size)
            padded[:len(audio_data)] = audio_data
            audio_data = padded
        
        # 应用窗函数
        window = np.hanning(fft_size)
        windowed = audio_data[:fft_size] * window
        
        # 计算FFT
        fft = np.fft.rfft(windowed)
        magnitude = np.abs(fft)
        
        # 转换为分贝
        magnitude_db = 20 * np.log10(magnitude + 1e-10)
        
        return magnitude_db
    
    def get_frequency_bins(self, num_bins: int = 64, 
                          fft_size: int = 2048) -> List[int]:
        """获取频率分箱"""
        # 对数频率分箱
        min_freq = 20
        max_freq = 20000
        
        log_min = np.log10(min_freq)
        log_max = np.log10(max_freq)
        log_bins = np.logspace(log_min, log_max, num_bins, base=10)
        
        # 转换为FFT bin索引
        bin_indices = []
        for freq in log_bins:
            bin_idx = int(freq * fft_size / self.sample_rate)
            bin_indices.append(min(bin_idx, fft_size // 2 - 1))
        
        return bin_indices

class BeatDetector:
    """节拍检测器"""
    
    def __init__(self, history_size: int = 10):
        self.history_size = history_size
        self.energy_history = deque(maxlen=history_size)
        self.last_beat_time = 0
        self.beat_threshold = 1.5
        
    def process_signal(self, signal: np.ndarray) -> float:
        """处理信号,返回节拍强度"""
        # 计算信号能量
        energy = np.mean(signal ** 2)
        
        # 添加到历史
        self.energy_history.append(energy)
        
        # 计算平均能量
        if len(self.energy_history) > 1:
            avg_energy = np.mean(list(self.energy_history))
        else:
            avg_energy = energy
        
        # 检测节拍
        beat_strength = 0.0
        if avg_energy > 0 and len(self.energy_history) > 1:
            # 计算瞬时能量与平均能量的比值
            instant_ratio = energy / avg_energy
            
            # 检测节拍
            if instant_ratio > self.beat_threshold:
                current_time = time.time()
                if current_time - self.last_beat_time > 0.1:  # 防抖动
                    self.last_beat_time = current_time
                    beat_strength = min(1.0, instant_ratio - self.beat_threshold)
        
        return beat_strength
    
    def get_beat_strength(self) -> float:
        """获取节拍强度"""
        if not self.energy_history:
            return 0.0
        
        current_energy = self.energy_history[-1]
        if len(self.energy_history) > 1:
            avg_energy = np.mean(list(self.energy_history))
            if avg_energy > 0:
                instant_ratio = current_energy / avg_energy
                return max(0.0, instant_ratio - 1.0)
        
        return 0.0

6. 完整播放器实现

python 复制代码
"""
完整播放器实现
单文件可运行的现代化音视频播放器Demo
"""

import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import json
import time
import random
import math
import threading
import numpy as np
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Tuple, Any, Callable
from enum import Enum, auto
from collections import deque
from pathlib import Path
from PIL import Image, ImageTk, ImageDraw, ImageFilter
import colorsys

# ==================== 数据类型定义 ====================

class PlayerState(Enum):
    """播放器状态"""
    STOPPED = auto()
    PLAYING = auto()
    PAUSED = auto()
    BUFFERING = auto()

class PlayMode(Enum):
    """播放模式"""
    SEQUENCE = "sequence"
    REPEAT_ONE = "repeat_one"
    REPEAT_ALL = "repeat_all"
    SHUFFLE = "shuffle"

class VisualizationMode(Enum):
    """可视化模式"""
    SPECTRUM = "spectrum"
    WAVEFORM = "waveform"
    PARTICLE = "particle"
    CIRCULAR = "circular"

@dataclass
class TrackInfo:
    """曲目信息"""
    title: str
    artist: str
    album: str
    duration: float
    file_path: str
    metadata: Dict = field(default_factory=dict)

# ==================== 事件系统 ====================

class EventType(Enum):
    """事件类型"""
    PLAYER_STATE_CHANGED = auto()
    PLAYER_PROGRESS_UPDATED = auto()
    PLAYLIST_CHANGED = auto()
    VOLUME_CHANGED = auto()
    SKIN_CHANGED = auto()
    VISUALIZATION_CHANGED = auto()

@dataclass
class Event:
    """事件对象"""
    type: EventType
    data: Any = None
    timestamp: float = field(default_factory=time.time)

class EventDispatcher:
    """事件分发器"""
    
    def __init__(self):
        self.subscribers: Dict[EventType, List[Callable]] = {}
        
    def subscribe(self, event_type: EventType, 
                  callback: Callable[[Event], None]):
        """订阅事件"""
        if event_type not in self.subscribers:
            self.subscribers[event_type] = []
        self.subscribers[event_type].append(callback)
    
    def unsubscribe(self, event_type: EventType, 
                   callback: Callable[[Event], None]):
        """取消订阅"""
        if event_type in self.subscribers:
            if callback in self.subscribers[event_type]:
                self.subscribers[event_type].remove(callback)
    
    def dispatch(self, event: Event):
        """分发事件"""
        if event.type in self.subscribers:
            for callback in self.subscribers[event.type]:
                try:
                    callback(event)
                except Exception as e:
                    print(f"事件处理错误: {e}")

# ==================== 播放控制器 ====================

class PlayerController:
    """播放控制器"""
    
    def __init__(self, event_dispatcher: EventDispatcher = None):
        self.state = PlayerState.STOPPED
        self.play_mode = PlayMode.SEQUENCE
        self.playlist: List[TrackInfo] = []
        self.current_index = 0
        self.position = 0.0
        self.volume = 0.7
        self.is_muted = False
        self.last_volume = 0.7
        self.event_dispatcher = event_dispatcher
        self._simulation_running = False
        
        # 模拟播放线程
        self._simulation_thread = None
        
        # 创建测试播放列表
        self._create_test_playlist()
    
    def _create_test_playlist(self):
        """创建测试播放列表"""
        test_tracks = [
            TrackInfo("夜空中最亮的星", "逃跑计划", "世界", 252, "模拟文件/逃跑计划 - 夜空中最亮的星.mp3"),
            TrackInfo("平凡之路", "朴树", "猎户星座", 260, "模拟文件/朴树 - 平凡之路.mp3"),
            TrackInfo("成都", "赵雷", "无法长大", 328, "模拟文件/赵雷 - 成都.mp3"),
            TrackInfo("光年之外", "G.E.M.邓紫棋", "另一个童话", 238, "模拟文件/G.E.M.邓紫棋 - 光年之外.mp3"),
            TrackInfo("起风了", "买辣椒也用券", "起风了", 315, "模拟文件/买辣椒也用券 - 起风了.mp3"),
        ]
        self.playlist.extend(test_tracks)
    
    def _get_current_track(self) -> Optional[TrackInfo]:
        """获取当前曲目"""
        if 0 <= self.current_index < len(self.playlist):
            return self.playlist[self.current_index]
        return None
    
    def _simulate_playback(self):
        """模拟播放"""
        track = self._get_current_track()
        if not track:
            return
        
        start_time = time.time()
        
        while self._simulation_running and self.state == PlayerState.PLAYING:
            elapsed = time.time() - start_time
            self.position = elapsed
            
            if self.position >= track.duration:
                self._on_playback_end()
                break
            
            # 发送进度更新事件
            if self.event_dispatcher:
                self.event_dispatcher.dispatch(Event(
                    EventType.PLAYER_PROGRESS_UPDATED,
                    {'current': self.position, 'total': track.duration}
                ))
            
            time.sleep(0.1)  # 100ms更新一次
    
    def _on_playback_end(self):
        """播放结束处理"""
        self.stop()
        
        if self.play_mode == PlayMode.REPEAT_ONE:
            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()
        else:  # SEQUENCE
            if self.current_index < len(self.playlist) - 1:
                self.next_track()
    
    def play(self):
        """开始播放"""
        if self.state == PlayerState.PLAYING:
            return
        
        if self.state == PlayerState.PAUSED:
            self.state = PlayerState.PLAYING
        else:
            self.state = PlayerState.PLAYING
            self.position = 0.0
            self._simulation_running = True
            self._simulation_thread = threading.Thread(
                target=self._simulate_playback, daemon=True
            )
            self._simulation_thread.start()
        
        # 发送状态变化事件
        if self.event_dispatcher:
            self.event_dispatcher.dispatch(Event(
                EventType.PLAYER_STATE_CHANGED,
                {'state': self.state}
            ))
    
    def pause(self):
        """暂停播放"""
        if self.state == PlayerState.PLAYING:
            self.state = PlayerState.PAUSED
            self._simulation_running = False
            
            if self.event_dispatcher:
                self.event_dispatcher.dispatch(Event(
                    EventType.PLAYER_STATE_CHANGED,
                    {'state': self.state}
                ))
    
    def stop(self):
        """停止播放"""
        self.state = PlayerState.STOPPED
        self.position = 0.0
        self._simulation_running = False
        
        if self.event_dispatcher:
            self.event_dispatcher.dispatch(Event(
                EventType.PLAYER_STATE_CHANGED,
                {'state': self.state}
            ))
    
    def next_track(self):
        """下一曲"""
        if not self.playlist:
            return
        
        if self.play_mode == PlayMode.SHUFFLE:
            self.current_index = random.randint(0, len(self.playlist) - 1)
        else:
            if self.current_index < len(self.playlist) - 1:
                self.current_index += 1
            elif self.play_mode == PlayMode.REPEAT_ALL:
                self.current_index = 0
            else:
                self.stop()
                return
        
        self.play()
    
    def previous_track(self):
        """上一曲"""
        if not self.playlist:
            return
        
        if self.current_index > 0:
            self.current_index -= 1
        elif self.play_mode == PlayMode.REPEAT_ALL:
            self.current_index = len(self.playlist) - 1
        else:
            return
        
        self.play()
    
    def seek(self, position: float):
        """跳转到指定位置"""
        track = self._get_current_track()
        if track:
            self.position = max(0, min(position, track.duration))
    
    def set_volume(self, volume: float):
        """设置音量"""
        self.volume = max(0.0, min(1.0, volume))
        
        if self.event_dispatcher:
            self.event_dispatcher.dispatch(Event(
                EventType.VOLUME_CHANGED,
                {'volume': self.volume}
            ))
    
    def toggle_mute(self):
        """切换静音"""
        if self.is_muted:
            self.is_muted = False
            self.set_volume(self.last_volume)
        else:
            self.is_muted = True
            self.last_volume = self.volume
            self.set_volume(0.0)
    
    def set_play_mode(self, mode: PlayMode):
        """设置播放模式"""
        self.play_mode = mode
    
    def get_state(self) -> PlayerState:
        """获取播放状态"""
        return self.state
    
    def get_progress(self) -> Tuple[float, float]:
        """获取播放进度"""
        track = self._get_current_track()
        if track:
            return self.position, track.duration
        return 0.0, 0.0
    
    def get_current_track(self) -> Optional[TrackInfo]:
        """获取当前曲目"""
        return self._get_current_track()

# ==================== 皮肤引擎 ====================

class SkinEngine:
    """皮肤引擎"""
    
    def __init__(self, root):
        self.root = root
        self.style = ttk.Style()
        self.current_skin = "dark"
        self.skins = self._create_skins()
        self.image_cache = {}
        
        # 应用默认皮肤
        self.apply_skin(self.current_skin)
    
    def _create_skins(self) -> Dict[str, Dict]:
        """创建皮肤配置"""
        return {
            "dark": {
                "name": "深色主题",
                "colors": {
                    "primary": "#1DB954",
                    "secondary": "#535353",
                    "background": "#121212",
                    "surface": "#181818",
                    "card": "#282828",
                    "text_primary": "#FFFFFF",
                    "text_secondary": "#B3B3B3",
                    "text_disabled": "#535353",
                    "border": "#404040",
                    "hover": "#282828",
                    "active": "#1DB954",
                },
                "fonts": {
                    "title": ("Microsoft YaHei", 16, "bold"),
                    "subtitle": ("Microsoft YaHei", 14),
                    "body": ("Microsoft YaHei", 12),
                    "caption": ("Microsoft YaHei", 10),
                }
            },
            "light": {
                "name": "浅色主题",
                "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",
                },
                "fonts": {
                    "title": ("Microsoft YaHei", 16, "bold"),
                    "subtitle": ("Microsoft YaHei", 14),
                    "body": ("Microsoft YaHei", 12),
                    "caption": ("Microsoft YaHei", 10),
                }
            }
        }
    
    def apply_skin(self, skin_name: str):
        """应用皮肤"""
        if skin_name not in self.skins:
            skin_name = "dark"
        
        self.current_skin = skin_name
        skin = self.skins[skin_name]
        
        # 创建主题
        self.style.theme_create("modern", parent="clam")
        self.style.theme_use("modern")
        
        # 配置基础样式
        self.style.configure(
            ".",
            background=skin["colors"]["background"],
            foreground=skin["colors"]["text_primary"],
            fieldbackground=skin["colors"]["surface"],
            troughcolor=skin["colors"]["surface"],
            selectbackground=skin["colors"]["primary"],
            selectforeground=skin["colors"]["text_primary"],
            borderwidth=0,
            focusthickness=0,
            relief="flat"
        )
        
        # 配置窗口背景
        self.root.configure(bg=skin["colors"]["background"])
        
        # 创建自定义样式
        self._create_custom_styles(skin)
        
        # 发送皮肤变化事件
        print(f"应用皮肤: {skin['name']}")
    
    def _create_custom_styles(self, skin: Dict):
        """创建自定义样式"""
        colors = skin["colors"]
        fonts = skin["fonts"]
        
        # Frame样式
        self.style.configure(
            "Card.TFrame",
            background=colors["card"],
            relief="flat",
            borderwidth=1
        )
        
        self.style.configure(
            "Surface.TFrame",
            background=colors["surface"],
            relief="flat",
            borderwidth=0
        )
        
        # Label样式
        for font_name, font_config in fonts.items():
            self.style.configure(
                f"{font_name.capitalize()}.TLabel",
                background=colors["background"],
                foreground=colors[f"text_{'primary' if font_name == 'title' else 'secondary'}"],
                font=font_config
            )
        
        # Button样式
        self.style.configure(
            "Primary.TButton",
            background=colors["primary"],
            foreground=colors["text_primary"],
            borderwidth=0,
            focusthickness=0,
            font=("Microsoft YaHei", 11, "bold"),
            padding=(12, 8)
        )
        
        self.style.map(
            "Primary.TButton",
            background=[
                ("active", colors["active"]),
                ("disabled", colors["text_disabled"])
            ]
        )
    
    def get_color(self, color_name: str) -> str:
        """获取颜色"""
        skin = self.skins.get(self.current_skin, self.skins["dark"])
        return skin["colors"].get(color_name, "#000000")
    
    def get_font(self, font_name: str) -> tuple:
        """获取字体"""
        skin = self.skins.get(self.current_skin, self.skins["dark"])
        return skin["fonts"].get(font_name, ("Microsoft YaHei", 12))

# ==================== 音频可视化器 ====================

class AudioVisualizer(tk.Canvas):
    """音频可视化器"""
    
    def __init__(self, parent, width=400, height=200, **kwargs):
        super().__init__(parent, width=width, height=height, 
                        highlightthickness=0, bg='black', **kwargs)
        self.width = width
        self.height = height
        
        # 可视化参数
        self.mode = VisualizationMode.SPECTRUM
        self.num_bars = 64
        self.bars = []
        self.current_levels = [0] * self.num_bars
        self.target_levels = [0] * self.num_bars
        
        # 颜色系统
        self.colors = self._create_color_gradient()
        
        # 动画
        self.animation_id = None
        self.is_animating = False
        
        # 初始化
        self._create_bars()
        self.animate()
    
    def _create_color_gradient(self) -> List[str]:
        """创建颜色渐变"""
        colors = []
        for i in range(self.num_bars):
            hue = i / self.num_bars
            r, g, b = colorsys.hsv_to_rgb(hue, 1.0, 1.0)
            colors.append(f'#{int(r*255):02x}{int(g*255):02x}{int(b*255):02x}')
        return colors
    
    def _create_bars(self):
        """创建频谱条"""
        bar_width = self.width / self.num_bars
        bar_spacing = bar_width * 0.1
        
        for i in range(self.num_bars):
            x = i * bar_width + bar_spacing
            width = bar_width - bar_spacing * 2
            
            bar = self.create_rectangle(
                x, self.height,
                x + width, self.height,
                fill=self.colors[i],
                width=0
            )
            self.bars.append(bar)
    
    def _generate_test_data(self) -> List[float]:
        """生成测试数据"""
        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(self, data: List[float] = None):
        """更新可视化数据"""
        if data is None:
            data = self._generate_test_data()
        
        # 调整数据长度
        if len(data) > self.num_bars:
            data = data[:self.num_bars]
        elif len(data) < self.num_bars:
            data = data + [0] * (self.num_bars - len(data))
        
        # 平滑过渡
        smoothing = 0.3
        for i in range(self.num_bars):
            self.target_levels[i] = min(max(data[i], 0), 1)
            self.current_levels[i] = (self.current_levels[i] * (1 - smoothing) + 
                                     self.target_levels[i] * 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
            y = self.height - height
            
            # 更新条的位置
            coords = self.coords(bar)
            if len(coords) == 4:
                self.coords(bar, coords[0], y, coords[2], self.height)
            
            # 根据高度调整颜色亮度
            if self.current_levels[i] > 0.7:
                bright_color = self._brighten_color(self.colors[i], 1.3)
                self.itemconfig(bar, fill=bright_color)
            else:
                self.itemconfig(bar, fill=self.colors[i])
    
    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 animate(self):
        """开始动画"""
        if self.animation_id:
            self.after_cancel(self.animation_id)
        
        self.update()
        self.animation_id = self.after(50, self.animate)
    
    def set_mode(self, mode: VisualizationMode):
        """设置可视化模式"""
        self.mode = mode
        # 可以在这里根据模式重新初始化可视化

# ==================== 现代化按钮组件 ====================

class ModernButton(tk.Canvas):
    """现代化按钮"""
    
    def __init__(self, parent, text="按钮", command=None, 
                 width=100, height=40, radius=8, **kwargs):
        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 = kwargs.get('bg_color', '#1DB954')
        self.fg_color = kwargs.get('fg_color', 'white')
        self.hover_color = kwargs.get('hover_color', '#1ED760')
        self.is_hovered = False
        
        # 创建按钮
        self._create_button()
        
        # 绑定事件
        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(self):
        """创建按钮"""
        width, height = self.winfo_reqwidth(), self.winfo_reqheight()
        
        # 绘制圆角矩形
        points = []
        points.extend([self.radius, 0])
        points.extend([width - self.radius, 0])
        points.extend([width, self.radius])
        points.extend([width, height - self.radius])
        points.extend([width - self.radius, height])
        points.extend([self.radius, height])
        points.extend([0, height - self.radius])
        points.extend([0, self.radius])
        points.extend([self.radius, 0])
        
        self.bg_id = self.create_polygon(
            points, fill=self.bg_color, smooth=True, outline=""
        )
        
        # 添加文字
        self.text_id = self.create_text(
            width//2, height//2,
            text=self.text, fill=self.fg_color,
            font=("Microsoft YaHei", 11, "bold")
        )
    
    def _on_enter(self, event):
        """鼠标进入事件"""
        self.is_hovered = True
        self.itemconfig(self.bg_id, fill=self.hover_color)
        self.config(cursor="hand2")
    
    def _on_leave(self, event):
        """鼠标离开事件"""
        self.is_hovered = False
        self.itemconfig(self.bg_id, fill=self.bg_color)
        self.config(cursor="")
    
    def _on_click(self, event):
        """鼠标点击事件"""
        # 添加点击效果:文字稍微下移
        self.move(self.text_id, 0, 1)
    
    def _on_release(self, event):
        """鼠标释放事件"""
        self.move(self.text_id, 0, -1)
        if self.command:
            self.command()
    
    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.itemconfig(self.bg_id, fill=self.bg_color)
        self.itemconfig(self.text_id, fill=self.fg_color)
    
    def set_text(self, text):
        """设置按钮文字"""
        self.text = text
        self.itemconfig(self.text_id, text=text)

# ==================== 现代化进度条组件 ====================

class ModernProgressBar(tk.Canvas):
    """现代化进度条"""
    
    def __init__(self, parent, width=300, height=4, 
                 progress_color="#1DB954", bg_color="#535353",
                 show_handle=False, **kwargs):
        super().__init__(parent, width=width, height=height, 
                        highlightthickness=0, bg=parent.cget("bg"))
        
        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
        
        # 绘制背景
        self.bg_rect = self.create_rectangle(
            0, 0, width, height,
            fill=bg_color, outline=""
        )
        
        # 绘制进度
        self.progress_rect = self.create_rectangle(
            0, 0, 0, height,
            fill=progress_color, outline=""
        )
        
        # 绘制滑块
        self.handle = None
        if show_handle:
            self.handle = self.create_oval(
                -6, -4, 6, 8,
                fill=progress_color, outline=""
            )
        
        # 绑定事件
        if show_handle:
            self.bind("<Button-1>", self._on_click)
            self.bind("<B1-Motion>", self._on_drag)
            self.config(cursor="hand2")
    
    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.set_progress(x / self.width)
        
        # 触发回调
        if hasattr(self, 'on_progress_change'):
            self.on_progress_change(self.progress)
    
    def set_progress(self, progress):
        """设置进度"""
        self.progress = max(0.0, min(1.0, progress))
        self._draw()
    
    def _draw(self):
        """绘制进度条"""
        progress_width = int(self.width * self.progress)
        
        # 更新进度条
        self.coords(self.progress_rect, 0, 0, progress_width, self.height)
        
        # 更新滑块
        if self.handle:
            self.coords(self.handle, 
                       progress_width - 6, -4,
                       progress_width + 6, 8)
    
    def set_colors(self, progress_color, bg_color):
        """设置颜色"""
        self.progress_color = progress_color
        self.bg_color = bg_color
        self.itemconfig(self.progress_rect, fill=progress_color)
        self.itemconfig(self.bg_rect, fill=bg_color)
        if self.handle:
            self.itemconfig(self.handle, fill=progress_color)

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

class ModernPlayer:
    """现代化播放器主类"""
    
    def __init__(self):
        # 创建主窗口
        self.root = tk.Tk()
        self.root.title("Modern Player - 现代化播放器")
        self.root.geometry("1200x800")
        self.root.minsize(800, 600)
        
        # 初始化组件
        self.event_dispatcher = EventDispatcher()
        self.skin_engine = SkinEngine(self.root)
        self.player_controller = PlayerController(self.event_dispatcher)
        self.visualizer = None
        
        # 状态变量
        self.is_dragging_progress = False
        self.current_skin = "dark"
        self.current_visualization_mode = VisualizationMode.SPECTRUM
        
        # 播放列表显示
        self.playlist_listbox = None
        
        # 初始化UI
        self._init_ui()
        
        # 绑定事件
        self._bind_events()
        self._setup_event_listeners()
        
        # 创建测试播放列表
        self._create_test_playlist()
    
    def _init_ui(self):
        """初始化用户界面"""
        # 主容器
        self.main_container = ttk.Frame(self.root, style="Surface.TFrame")
        self.main_container.pack(fill=tk.BOTH, expand=True, padx=2, pady=2)
        
        # 创建布局
        self._create_top_toolbar()
        self._create_main_content()
        self._create_bottom_controls()
    
    def _create_top_toolbar(self):
        """创建顶部工具栏"""
        toolbar = ttk.Frame(self.main_container, style="Card.TFrame", height=60)
        toolbar.pack(fill=tk.X, padx=8, pady=8)
        toolbar.pack_propagate(False)
        
        # Logo
        logo_label = ttk.Label(toolbar, text="♬ MODERN PLAYER", 
                             style="Title.TLabel")
        logo_label.pack(side=tk.LEFT, padx=16)
        
        # 搜索框
        search_frame = ttk.Frame(toolbar, style="Surface.TFrame")
        search_frame.pack(side=tk.RIGHT, padx=16)
        
        search_entry = ttk.Entry(search_frame, width=30)
        search_entry.pack(side=tk.LEFT, padx=8, pady=8)
        
        search_button = ModernButton(
            search_frame, text="搜索", width=80, height=32
        )
        search_button.pack(side=tk.LEFT, padx=(0, 8), pady=8)
        
        # 皮肤切换按钮
        skin_button = ModernButton(
            toolbar, text="切换皮肤", width=100, height=32,
            command=self._toggle_skin
        )
        skin_button.pack(side=tk.RIGHT, padx=8, pady=8)
    
    def _create_main_content(self):
        """创建主内容区"""
        content_frame = ttk.Frame(self.main_container, style="Surface.TFrame")
        content_frame.pack(fill=tk.BOTH, expand=True, padx=8, pady=(0, 8))
        
        # 三栏布局
        self._create_left_sidebar(content_frame)
        self._create_center_content(content_frame)
        self._create_right_sidebar(content_frame)
    
    def _create_left_sidebar(self, parent):
        """创建左侧边栏"""
        sidebar = ttk.Frame(parent, width=240, style="Card.TFrame")
        sidebar.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 8))
        sidebar.pack_propagate(False)
        
        # 导航菜单
        nav_items = [
            ("🎵 发现音乐", self._show_discover),
            ("📻 我的电台", self._show_radio),
            ("📁 本地音乐", self._show_local),
            ("❤️ 我的喜欢", self._show_favorites),
            ("📥 下载管理", self._show_downloads)
        ]
        
        for text, command in nav_items:
            btn = ModernButton(
                sidebar, text=text, 
                width=200, height=40,
                bg_color=self.skin_engine.get_color("surface"),
                fg_color=self.skin_engine.get_color("text_primary"),
                hover_color=self.skin_engine.get_color("hover"),
                command=command
            )
            btn.pack(pady=4, padx=16)
        
        # 播放列表标题
        playlist_label = ttk.Label(sidebar, text="播放列表", 
                                 style="Subtitle.TLabel")
        playlist_label.pack(anchor=tk.W, padx=16, pady=(20, 8))
        
        # 播放列表容器
        playlist_container = ttk.Frame(sidebar, style="Surface.TFrame")
        playlist_container.pack(fill=tk.BOTH, expand=True, 
                               padx=16, pady=(0, 16))
        
        # 播放列表滚动条
        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, parent):
        """创建中心内容区"""
        center = ttk.Frame(parent, style="Card.TFrame")
        center.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, 
                   padx=(0, 8))
        
        # 专辑封面显示
        album_frame = ttk.Frame(center, style="Surface.TFrame")
        album_frame.pack(fill=tk.BOTH, expand=True, padx=16, pady=16)
        
        # 专辑封面画布
        self.album_canvas = tk.Canvas(
            album_frame,
            bg=self.skin_engine.get_color("surface"),
            highlightthickness=0
        )
        self.album_canvas.pack(fill=tk.BOTH, expand=True)
        
        # 绘制默认专辑封面
        self._draw_default_album_art()
    
    def _draw_default_album_art(self):
        """绘制默认专辑封面"""
        width = 400
        height = 400
        
        # 创建默认专辑封面图片
        image = Image.new('RGB', (width, height), 
                         self.skin_engine.get_color("surface"))
        draw = ImageDraw.Draw(image)
        
        # 绘制音乐符号
        center_x, center_y = width // 2, height // 2
        radius = min(width, height) // 3
        
        # 绘制圆形背景
        draw.ellipse(
            [center_x - radius, center_y - radius,
             center_x + radius, center_y + radius],
            fill=self.skin_engine.get_color("primary")
        )
        
        # 绘制音乐符号
        draw.text((center_x, center_y - 20), "♫", 
                 fill="white", 
                 font=ImageFont.truetype("arial", 60),
                 anchor="mm")
        draw.text((center_x, center_y + 40), "MUSIC", 
                 fill="white", 
                 font=ImageFont.truetype("arial", 30),
                 anchor="mm")
        
        # 转换为PhotoImage
        self.album_image = ImageTk.PhotoImage(image)
        
        # 显示图片
        self.album_canvas.create_image(0, 0, image=self.album_image, anchor="nw")
    
    def _create_right_sidebar(self, parent):
        """创建右侧边栏"""
        sidebar = ttk.Frame(parent, width=300, style="Card.TFrame")
        sidebar.pack(side=tk.RIGHT, fill=tk.Y)
        sidebar.pack_propagate(False)
        
        # 当前播放信息
        now_playing_frame = ttk.Frame(sidebar, style="Surface.TFrame")
        now_playing_frame.pack(fill=tk.X, padx=16, pady=16)
        
        self.song_title = ttk.Label(now_playing_frame, text="未播放", 
                                  style="Title.TLabel")
        self.song_title.pack(anchor=tk.W)
        
        self.song_artist = ttk.Label(now_playing_frame, text="艺术家", 
                                   style="Caption.TLabel")
        self.song_artist.pack(anchor=tk.W)
        
        # 音频可视化
        viz_frame = ttk.Frame(sidebar, style="Surface.TFrame")
        viz_frame.pack(fill=tk.X, padx=16, pady=(0, 16))
        
        viz_label = ttk.Label(viz_frame, text="音频可视化", 
                            style="Subtitle.TLabel")
        viz_label.pack(anchor=tk.W, pady=(0, 8))
        
        # 可视化模式选择
        mode_frame = ttk.Frame(viz_frame, style="Surface.TFrame")
        mode_frame.pack(fill=tk.X, pady=(0, 8))
        
        modes = [
            ("频谱", VisualizationMode.SPECTRUM),
            ("波形", VisualizationMode.WAVEFORM),
            ("粒子", VisualizationMode.PARTICLE)
        ]
        
        for text, mode in modes:
            btn = ModernButton(
                mode_frame, text=text, 
                width=60, height=30,
                bg_color=self.skin_engine.get_color("surface"),
                fg_color=self.skin_engine.get_color("text_primary"),
                hover_color=self.skin_engine.get_color("hover"),
                command=lambda m=mode: self._set_visualization_mode(m)
            )
            btn.pack(side=tk.LEFT, padx=2)
        
        # 可视化画布
        self.viz_canvas = AudioVisualizer(
            viz_frame,
            width=268,
            height=150
        )
        self.viz_canvas.pack(fill=tk.X)
    
    def _create_bottom_controls(self):
        """创建底部控制栏"""
        controls = ttk.Frame(self.main_container, style="Card.TFrame", height=80)
        controls.pack(fill=tk.X, padx=8, pady=(0, 8))
        controls.pack_propagate(False)
        
        # 左侧:播放信息
        info_frame = ttk.Frame(controls, style="Surface.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(controls, style="Surface.TFrame")
        control_frame.pack(side=tk.LEFT, expand=True, fill=tk.BOTH, padx=20, pady=10)
        
        # 控制按钮容器
        btn_container = ttk.Frame(control_frame, style="Surface.TFrame")
        btn_container.pack(expand=True)
        
        # 播放控制按钮
        controls_config = [
            ("⏮", 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_config):
            btn = ModernButton(
                btn_container,
                text=text,
                command=command,
                width=size*2,
                height=size*2,
                radius=size,
                bg_color=self.skin_engine.get_color("surface"),
                fg_color=self.skin_engine.get_color("text_primary"),
                hover_color=self.skin_engine.get_color("hover")
            )
            btn.pack(side=tk.LEFT, padx=5)
            
            # 保存播放/暂停按钮引用
            if text == "⏸":
                self.play_pause_btn = btn
        
        # 进度条
        self.progress_frame = ttk.Frame(control_frame, style="Surface.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(controls, style="Surface.TFrame")
        volume_frame.pack(side=tk.RIGHT, fill=tk.Y, padx=20, pady=10)
        
        # 音量图标
        self.volume_btn = ModernButton(
            volume_frame,
            text="🔊",
            width=30,
            height=30,
            radius=15,
            bg_color=self.skin_engine.get_color("surface"),
            fg_color=self.skin_engine.get_color("text_primary"),
            hover_color=self.skin_engine.get_color("hover"),
            command=self._toggle_mute
        )
        self.volume_btn.pack(side=tk.LEFT, padx=(0, 10))
        
        # 音量滑块
        self.volume_slider = ModernProgressBar(
            volume_frame,
            width=100,
            height=4,
            progress_color=self.skin_engine.get_color("primary"),
            bg_color=self.skin_engine.get_color("surface"),
            show_handle=True
        )
        self.volume_slider.pack(side=tk.LEFT)
        self.volume_slider.set_progress(self.player_controller.volume)
        self.volume_slider.on_progress_change = self._on_volume_change
    
    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_event_listeners(self):
        """设置事件监听器"""
        # 播放状态变化
        self.event_dispatcher.subscribe(
            EventType.PLAYER_STATE_CHANGED,
            self._on_player_state_changed
        )
        
        # 播放进度更新
        self.event_dispatcher.subscribe(
            EventType.PLAYER_PROGRESS_UPDATED,
            self._on_player_progress_updated
        )
        
        # 音量变化
        self.event_dispatcher.subscribe(
            EventType.VOLUME_CHANGED,
            self._on_volume_changed
        )
    
    def _create_test_playlist(self):
        """创建测试播放列表"""
        for track in self.player_controller.playlist:
            display_text = f"{track.artist} - {track.title}"
            self.playlist_listbox.insert(tk.END, display_text)
    
    def _on_player_state_changed(self, event: Event):
        """播放状态变化事件处理"""
        state = event.data['state']
        
        if state == PlayerState.PLAYING:
            self.play_pause_btn.set_text("⏸")
            
            # 更新当前播放信息
            track = self.player_controller.get_current_track()
            if track:
                self.song_title.config(text=track.title)
                self.song_artist.config(text=track.artist)
                self.now_playing_label.config(
                    text=f"{track.artist} - {track.title}"
                )
        elif state in [PlayerState.PAUSED, PlayerState.STOPPED]:
            self.play_pause_btn.set_text("▶")
    
    def _on_player_progress_updated(self, event: Event):
        """播放进度更新事件处理"""
        current = event.data['current']
        total = event.data['total']
        
        # 格式化时间
        current_str = self._format_time(current)
        total_str = self._format_time(total)
        
        self.time_label.config(text=f"{current_str} / {total_str}")
        
        # 更新进度条
        if not self.is_dragging_progress and total > 0:
            progress = current / total
            canvas_width = self.progress_canvas.winfo_width()
            if canvas_width > 0:
                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_volume_changed(self, event: Event):
        """音量变化事件处理"""
        volume = event.data['volume']
        
        # 更新音量图标
        if volume == 0:
            self.volume_btn.set_text("🔇")
        elif volume < 0.3:
            self.volume_btn.set_text("🔈")
        elif volume < 0.7:
            self.volume_btn.set_text("🔉")
        else:
            self.volume_btn.set_text("🔊")
    
    def _format_time(self, seconds: float) -> str:
        """格式化时间显示"""
        minutes = int(seconds // 60)
        seconds = int(seconds % 60)
        return f"{minutes:02d}:{seconds:02d}"
    
    def _on_playlist_select(self, event):
        """播放列表选择事件"""
        selection = self.playlist_listbox.curselection()
        if selection:
            self.player_controller.current_index = selection[0]
            self.player_controller.play()
    
    def _toggle_skin(self):
        """切换皮肤"""
        if self.current_skin == "dark":
            self.current_skin = "light"
        else:
            self.current_skin = "dark"
        
        self.skin_engine.apply_skin(self.current_skin)
        
        # 更新UI颜色
        self._update_ui_colors()
    
    def _update_ui_colors(self):
        """更新UI颜色"""
        # 更新播放列表颜色
        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._draw_default_album_art()
        
        # 更新进度条
        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")
        )
        
        # 更新音量滑块
        self.volume_slider.set_colors(
            self.skin_engine.get_color("primary"),
            self.skin_engine.get_color("surface")
        )
    
    def _set_visualization_mode(self, mode: VisualizationMode):
        """设置可视化模式"""
        self.current_visualization_mode = mode
        if self.viz_canvas:
            self.viz_canvas.set_mode(mode)
    
    def _play_pause(self):
        """播放/暂停"""
        if self.player_controller.get_state() == PlayerState.PLAYING:
            self.player_controller.pause()
        else:
            self.player_controller.play()
    
    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.set_play_mode(PlayMode.SEQUENCE)
        else:
            self.player_controller.set_play_mode(PlayMode.SHUFFLE)
    
    def _toggle_repeat(self):
        """切换循环模式"""
        if self.player_controller.play_mode == PlayMode.SEQUENCE:
            self.player_controller.set_play_mode(PlayMode.REPEAT_ONE)
        elif self.player_controller.play_mode == PlayMode.REPEAT_ONE:
            self.player_controller.set_play_mode(PlayMode.REPEAT_ALL)
        else:
            self.player_controller.set_play_mode(PlayMode.SEQUENCE)
    
    def _toggle_mute(self):
        """静音/取消静音"""
        self.player_controller.toggle_mute()
    
    def _on_volume_change(self, volume: float):
        """音量变化"""
        self.player_controller.set_volume(volume)
    
    def _seek_backward(self):
        """快退10秒"""
        current, total = self.player_controller.get_progress()
        if total > 0:
            new_position = max(0, current - 10)
            self.player_controller.seek(new_position)
    
    def _seek_forward(self):
        """快进10秒"""
        current, total = self.player_controller.get_progress()
        if total > 0:
            new_position = min(total, current + 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)
            
            # 跳转到指定位置
            current, total = self.player_controller.get_progress()
            if total > 0:
                position = progress * total
                self.player_controller.seek(position)
    
    def _open_file(self):
        """打开文件"""
        filetypes = [
            ("音频文件", "*.mp3 *.wav *.flac *.ogg *.m4a *.aac"),
            ("所有文件", "*.*")
        ]
        
        filename = filedialog.askopenfilename(filetypes=filetypes)
        if filename:
            # 这里可以添加实际的文件加载逻辑
            print(f"打开文件: {filename}")
    
    def _show_discover(self):
        """显示发现音乐"""
        print("显示发现音乐")
    
    def _show_radio(self):
        """显示我的电台"""
        print("显示我的电台")
    
    def _show_local(self):
        """显示本地音乐"""
        print("显示本地音乐")
    
    def _show_favorites(self):
        """显示我的喜欢"""
        print("显示我的喜欢")
    
    def _show_downloads(self):
        """显示下载管理"""
        print("显示下载管理")
    
    def _on_closing(self):
        """窗口关闭事件"""
        # 停止播放
        self.player_controller.stop()
        # 停止可视化动画
        if self.viz_canvas:
            self.viz_canvas.after_cancel(self.viz_canvas.animation_id)
        # 关闭窗口
        self.root.destroy()
    
    def run(self):
        """运行播放器"""
        self.root.mainloop()

# ==================== 主程序入口 ====================

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

7. 知识点总结与展望

7.1 核心知识点总结

通过本文的实现,我们系统掌握了以下关键技术:

  1. tkinter/ttk深度定制

    • 样式系统的完全控制

    • 自定义控件的开发方法

    • 主题切换与皮肤系统实现

  2. 现代化UI设计

    • 响应式布局系统

    • 卡片化设计模式

    • 交互动画与过渡效果

    • 色彩系统与字体系统

  3. 播放控制逻辑

    • 状态机设计模式

    • 事件驱动架构

    • 播放列表管理

    • 进度控制与时间管理

  4. 音频可视化技术

    • 频谱分析与FFT实现

    • 实时数据渲染优化

    • 多种可视化模式切换

    • 物理模拟与动画效果

  5. 软件架构设计

    • 模块化设计思想

    • 接口与抽象类定义

    • 松耦合组件设计

    • 事件系统与消息传递

7.2 技术难点与解决方案

技术难点 解决方案 学习收获
界面现代化 Canvas自定义绘制 + 阴影效果 掌握现代化UI实现原理
响应式布局 断点系统 + 布局管理器 学习多设备适配策略
性能优化 双缓冲技术 + 数据降采样 掌握GUI性能优化方法
状态管理 状态机模式 + 事件系统 学习复杂状态管理
数据可视化 实时渲染 + 物理模拟 掌握动态数据展示技术

7.3 结语

Python tkinter/ttk作为Python的标准GUI库,虽然历史悠久,但通过现代化的设计理念和技术创新,完全可以创建出功能强大、外观时尚的专业级应用程序。本文通过一个完整的音视频播放器项目,系统展示了从基础到高级的GUI开发技术。

技术学习的核心在于实践和创新。希望本文不仅能够帮助读者掌握tkinter/ttk的深度应用技术,更能够启发大家在Python GUI开发领域的创新思维。无论你是初学者还是有经验的开发者,都可以从这个项目中获得有价值的经验和启发。

记住:最好的学习方式是动手实践。建议读者在理解本文内容的基础上,尝试扩展和优化这个播放器项目,添加自己的创意和功能。通过不断的实践和探索,你将在Python GUI开发的道路上不断进步。

相关推荐
Freak嵌入式2 小时前
MicroPython LVGL基础知识和概念:时序与动态效果
开发语言·python·github·php·gui·lvgl·micropython
zhangzeyuaaa2 小时前
Python 中的 Map 和 Reduce 详解
开发语言·python
Black蜡笔小新3 小时前
GB28181视频汇聚平台EasyCVR构建智慧环保可视化监测解决方案,赋能生态可持续发展
音视频
七夜zippoe3 小时前
Java技术未来展望:GraalVM、Quarkus、Helidon等新趋势探讨
java·开发语言·python·quarkus·graaivm·helidon
m0_738120723 小时前
网络安全编程——Python编写基于UDP的主机发现工具(解码IP header)
python·网络协议·tcp/ip·安全·web安全·udp
北冥有羽Victoria3 小时前
OpenCLI 操作网页 从0到1完整实操指南
vscode·爬虫·python·github·api·ai编程·opencli
handsomestWei3 小时前
scikit-learn数据预处理模块
python·机器学习·scikit-learn
w_t_y_y3 小时前
机器学习常用的python包(二)工具箱scikit-learn
python·机器学习·scikit-learn
用户8356290780513 小时前
Python 自动拆分 Word 文档教程:按分节符与分页符处理
后端·python