winform 音乐播放器

引言

本次项目目的主要为了熟悉axWindowsMediaPlayer,treeview等控件使用,以及学习I/O操作。

技术栈

C# winform

实现效果

设计与实现

使用TreeView实现音乐播放器的侧边栏

        // 调用 LoadFolderStructure 方法,传入音乐文件夹的路径
LoadFolderStructure("D:\\VS2022\\Poject\\MusicPlayer\\MusicPlayer\\music");

// 这个方法用于加载文件夹结构并填充 treeView1
private void LoadFolderStructure(string path)
{
    // 创建一个树节点,表示根目录
    TreeNode rootNode = new TreeNode(path);
    // 展开所有的子节点,以便在树形视图中立即显示所有层级
    rootNode.ExpandAll();
    // 将根节点添加到 treeView1 中
    treeView1.Nodes.Add(rootNode);
    
    // 递归地加载子目录和文件
    LoadSubFolderAndFiles(rootNode, path);
}

// 这个方法用于递归加载子目录和文件,并将它们添加到树形视图中
private void LoadSubFolderAndFiles(TreeNode parentNode, string path)
{
    try
    {
        // 遍历指定路径下的所有子目录
        foreach (string dir in Directory.GetDirectories(path))
        {
            // 获取子目录的名称
            string dirName = Path.GetFileName(dir);
            // 创建一个新的树节点,表示这个子目录
            TreeNode node = new TreeNode(dirName);
            // 将新节点添加到父节点中
            parentNode.Nodes.Add(node);
            // 递归调用 LoadSubFolderAndFiles,继续加载子目录下的内容
            LoadSubFolderAndFiles(node, dir);
        }
        
        // 遍历指定路径下的所有文件
        foreach (string file in Directory.GetFiles(path))
        {
            // 获取文件名
            string fileName = Path.GetFileName(file);
            // 创建一个新的树节点,表示这个文件
            TreeNode fileNode = new TreeNode(fileName);
            // 将文件节点添加到父节点中
            parentNode.Nodes.Add(fileNode);
        }
    }
    catch (UnauthorizedAccessException)
    {
        // 如果没有权限访问某个目录,显示错误消息
        MessageBox.Show("没有权限访问目录:" + path);
    }
    catch (DirectoryNotFoundException)
    {
        // 如果指定的目录不存在,显示错误消息
        MessageBox.Show("目录未找到:" + path);
    }
    catch (Exception ex)
    {
        // 捕获其他任何类型的异常,并显示详细的错误消息
        MessageBox.Show("加载文件夹时发生错误:" + ex.Message);
    }
}
            

使用RichTextBox实现音乐播放器的歌词滚。 歌词变色使用定时器,将axWindowsMediaPlayer获取的实时进度时间与从lrc歌词文件中分离出的时间与歌词对照数组对应实现,使用二分查找实现。

歌词加载策略为,点击音乐文件后自动查找同名的lrc的歌词文件。


       // 此方法用于解析歌词文件中的每一行,提取时间戳和对应的歌词文本,
// 并将它们添加到Lyrics列表中,同时更新RichTextBox的内容。
private void LoadSubFolderAndFiles(string[] lines)
{
    // 清空RichTextBox的现有内容
    richTextBox1.Text = "";

    // 遍历每行歌词
    foreach (string line in lines)
    {
        // 正则表达式用于匹配时间戳格式 "[mm:ss.mm]"
        string pattern = @"\[(\d{2}):(\d{2}\.\d{2})\]";
        Match match = Regex.Match(line, pattern);

        if (match.Success)
        {
            // 解析时间戳的分钟、秒和毫秒部分
            int minutes = int.Parse(match.Groups[1].Value);
            string secondsWithMilliseconds = match.Groups[2].Value;
            int seconds = int.Parse(secondsWithMilliseconds.Substring(0, 2)); // 秒
            int milliseconds = int.Parse(secondsWithMilliseconds.Substring(3)); // 毫秒

            // 创建表示时间戳的TimeSpan对象
            TimeSpan lyricsTimer = new TimeSpan(0, 0, minutes, seconds, milliseconds);

            // 从行中提取歌词文本,去除前导和尾随空白
            string lyricText = line.Substring(line.IndexOf(']') + 1).Trim();

            // 设置RichTextBox的文本对齐方式为居中
            richTextBox1.SelectionAlignment = HorizontalAlignment.Center;

            // 将歌词文本追加到RichTextBox中
            richTextBox1.AppendText(lyricText + Environment.NewLine);

            // 将时间戳和歌词文本作为元组添加到Lyrics列表中
            Lyrics.Add(Tuple.Create(lyricsTimer, lyricText));
        }
    }

    // 对Lyrics列表进行排序,确保时间戳顺序正确
    Lyrics.Sort((a, b) => a.Item1.CompareTo(b.Item1));
}

// 初始化定时器,用于同步歌词显示
// 设置定时器的间隔为100毫秒,即每秒触发10次
updateTimer = new System.Timers.Timer(100);
updateTimer.Elapsed += UpdateTimer_Elapsed;
updateTimer.AutoReset = true;

// 当定时器触发时,此方法将被调用,用于更新歌词显示
private void UpdateTimer_Elapsed(object sender, ElapsedEventArgs e)
{
    // 检查Windows Media Player控件是否可用
    if (axWindowsMediaPlayer1 != null && axWindowsMediaPlayer1.Ctlcontrols != null)
    {
        // 获取当前播放位置的秒数
        double currentPosition = axWindowsMediaPlayer1.Ctlcontrols.currentPosition;
        // 将秒数转换为TimeSpan对象
        TimeSpan currentTimespan = TimeSpan.FromSeconds(currentPosition);

        // 使用二分查找算法找到与当前播放时间最接近的歌词行索引
        int lyricIndex = BinarySearchLyricIndexUsingBuiltIn(Lyrics, currentTimespan);

        // 调用selectLine方法,高亮显示当前歌词行
        selectLine(lyricIndex);
    }
}

// 二分查找算法,用于在已排序的Lyrics列表中查找最接近当前播放时间的歌词行
public static int BinarySearchLyricIndexUsingBuiltIn(List<Tuple<TimeSpan, string>> lyrics, TimeSpan time)
{
    // 创建一个比较器,用于List.BinarySearch方法
    IComparer<Tuple<TimeSpan, string>> comparer = Comparer<Tuple<TimeSpan, string>>.Create((x, y) => x.Item1.CompareTo(y.Item1));

    // 执行二分查找
    int index = lyrics.BinarySearch(new Tuple<TimeSpan, string>(time, null), comparer);

    // 如果找到了精确匹配的时间戳,返回该时间戳的索引
    if (index >= 0) return index;

    // 如果没有找到精确匹配的时间戳,BinarySearch会返回负数
    // 需要将结果取反以得到插入点,即大于等于当前时间的最近歌词行的索引
    index = ~index;

    // 如果当前时间早于所有歌词行,返回第一个歌词行的索引
    if (index == 0) return 0;

    // 如果当前时间晚于所有歌词行,返回最后一个歌词行的索引
    if (index == lyrics.Count) return lyrics.Count - 1;

    // 否则,返回小于当前时间的最近歌词行的索引
    return index - 1;
}

// 此方法用于在RichTextBox中高亮显示指定行的歌词
private void selectLine(int line)
{
    // 调用Invoke方法,确保在UI线程中更新RichTextBox
    this.richTextBox1.Invoke(
        new EventHandler(delegate
        {
            // 获取指定行的第一个字符的索引
            int a = this.richTextBox1.GetFirstCharIndexFromLine(line);
            // 获取下一行的第一个字符的索引
            int b = this.richTextBox1.GetFirstCharIndexFromLine(++line);

            // 如果当前行是最后一行,b应为RichTextBox的总字符长度
            if (a == -1)
                return;
            else if (b == -1)
                b = this.richTextBox1.TextLength - a;
            else
                b = b - a;

            // 选择指定范围内的文本
            this.richTextBox1.Select(a, b);

            // 设置选中文本的颜色为黑色
            this.richTextBox1.SelectionColor = Color.Black;

            // 滚动RichTextBox使当前选中的歌词行可见
            this.richTextBox1.ScrollToCaret();
        }));
}
            

使用axWindowsMediaPlayer作为播放器主体,点击TreeView中的节点后,判断文件类型,若是音乐文件则传入播放器。


// 树形视图选择更改后的事件处理器
private void treeView1_AfterSelect(object sender, TreeViewEventArgs e)
{
    // 如果是第一次选择,跳过本次事件处理,避免初始化时的错误
    if (isFirstSelection)
    {
        isFirstSelection = false;
        return;
    }

    // 获取当前选中的节点
    TreeNode clickNode = e.Node;

    // 确保节点不为空
    if (clickNode != null)
    {
        // 获取节点的完整路径
        string nodeFullPath = clickNode.FullPath;
        
        // 提取文件名(不包含扩展名)
        name = Path.GetFileNameWithoutExtension(nodeFullPath).Trim();
        
        // 检查文件是否存在,并判断是否为音乐文件
        if (File.Exists(nodeFullPath) && IsMusicFile(nodeFullPath))
        {
            // 设置Windows Media Player的URL为当前选择的音乐文件路径
            this.axWindowsMediaPlayer1.URL = nodeFullPath;
            // 将播放位置设置为开头
            axWindowsMediaPlayer1.Ctlcontrols.currentPosition = 0;

            // 如果音乐文件有后缀,尝试找到对应的歌词文件(.lrc格式)
            int lastDotIdex = nodeFullPath.LastIndexOf(".");
            if (lastDotIdex != -1)
            {
                string fileName = nodeFullPath.Substring(0, lastDotIdex);
                string fileExtension = nodeFullPath.Substring(lastDotIdex);
                string pathToLyrics = fileName + ".lrc"; // 构造歌词文件的路径
                
                // 输出歌词文件的路径到控制台,便于调试
                Console.WriteLine(pathToLyrics);

                // 尝试读取歌词文件
                try
                {
                    string[] lines = File.ReadAllLines(pathToLyrics);
                    // 加载歌词文件到Lyrics列表中
                    LoadSubFolderAndFiles(lines);
                    
                    // 开启定时器,用于同步歌词显示
                    updateTimer.Enabled = true;
                }
                catch (FileNotFoundException)
                {
                    // 如果歌词文件不存在,显示错误信息
                    richTextBox1.Text = $"没找到文件:{pathToLyrics} ";
                }
                catch (IOException ex)
                {
                    // 如果发生读写错误,显示错误信息
                    richTextBox1.Text = $"发生了读写错误: {pathToLyrics}: {ex.Message}";
                }
            }

            // 开始播放音乐
            this.axWindowsMediaPlayer1.Ctlcontrols.play();
        }
    }
}

// 检查文件是否为音乐文件
private bool IsMusicFile(string filePath)
{
    string extension = Path.GetExtension(filePath).ToLower();
    return extension == ".mp3" || extension == ".wav" || extension == ".wma" || extension == ".aac" || extension == ".ogg";
}

// Windows Media Player播放状态改变时的事件处理器
private void axWindowsMediaPlayer1_PlayStateChange(object sender, AxWMPLib._WMPOCXEvents_PlayStateChangeEvent e)
{
    // 更新歌曲信息
    UpdateSongInfo();
}

// 更新歌曲信息
private void UpdateSongInfo()
{
    // 检查name变量是否已赋值
    if (name != null)
    {
        // 检查label1控件是否可用
        if (label1 != null)
        {
            // 更新label1的文本为当前播放的音乐文件名
            label1.Text = name;
        }
    }
    else
    {
        // 如果name为空,清空label1的内容(这里应该设置label1.Text = ""而不是label1 = null)
        if (label1 != null)
        {
            label1.Text = "";
        }
    }
}
            

挑战与解决方案

在实现歌词选中的效果时,axWindowsMediaPlayer返回的时间和lrc歌词文件中分离出来的时间对应有些麻烦。最终从lrc中分离歌词与时间对应的数组吗,然后使用二分查找找到最符合的歌词然后选中。

 

 

posted @ 2024-08-13 00:04  =·~·=  阅读(27)  评论(0编辑  收藏  举报