引言

本次项目目的主要为了熟悉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中分离歌词与时间对应的数组吗,然后使用二分查找找到最符合的歌词然后选中。

winform 音乐播放器的更多相关文章

  1. 小菜学习Winform(二)WMPLib实现音乐播放器

    前言 现在网上有很多的音乐播放器,但好像都不是.net平台做的,在.net中实现音乐文件的播放功能很简单,下面就简单实现下. SoundPlayer类 在.net提供了音乐文件的类:SoundPlay ...

  2. C# 一款属于自己的音乐播放器

    本文利用C# 调用Windows自带的Windows Media Player 打造一款属于自己的音乐播放器,以供学习分享使用,如有不足之处,还请指正. 概述 Windows Media Player ...

  3. SE Springer小组之《Spring音乐播放器》可行性研究报告三、四

    3 对现有系统的分析 由于本次可行性分析主要是建立在团队自行实现一个音乐软件的目标上,并不是在一个现有系统的基础上开发改进的新系统.因此这里将分析一款市面上已经存在的音乐软件(以下称为W音乐),并为之 ...

  4. 卡拉OK效果的实现-iOS音乐播放器

    自己编写的音乐播放器偶然用到这个模块,发现没有思路,而且上网搜了搜,关于这方面的文章不是很多,没找到满意的结果,然后自己也是想了想,最终实现了这种效果,想通了发现其实很简单. 直接上原理: 第一种: ...

  5. 【大结局】《从案例中学习JavaScript》之酷炫音乐播放器(四)

    这是之前写的用H5制作的音乐播放器,前三节其实已经做得差不多了,音轨的制作原理已经在上一节说明,不过一直还没有和音乐对接. 本章作为该系列的一个完结篇,我会专门把动态音轨的实现代码贴出来,demo地址 ...

  6. Andriod小项目——在线音乐播放器

    转载自: http://blog.csdn.net/sunkes/article/details/51189189 Andriod小项目——在线音乐播放器 Android在线音乐播放器 从大一开始就已 ...

  7. Android开发6:Service的使用(简单音乐播放器的实现)

    前言 啦啦啦~各位好久不见啦~博主最近比较忙,而且最近一次实验也是刚刚结束~ 好了不废话了,直接进入我们这次的内容~ 在这篇博文里我们将学习Service(服务)的相关知识,学会使用 Service ...

  8. android 音乐播放器

    本章以音乐播放器为载体,介绍android开发中,通知模式Notification应用.主要涉及知识点Notification,seekbar,service. 1.功能需求 完善音乐播放器 有播放列 ...

  9. 音乐播放器 EasyMusic (一)

    EasyMusic 一. 代码获取 github 上链接为 https://github.com/VincentWYJ/EasyMusic, 感兴趣的朋友可以同步下来看, 欢迎提出宝贵意见或建议. 1 ...

  10. iOS音乐播放器相关

    iOS音乐播放器框架主要有两大类:AvPlayer.AvaudioPlayer AvPlayer 能播放本地及网络歌曲 AvaudioPlayer 能播放本地歌曲.有相关代理方法(其实也可以播放网络歌 ...

随机推荐

  1. Apache SeaTunnel 4月回顾:明星贡献者与技术突破

    各位热爱 SeaTunnel 的小伙伴们,SeaTunnel 社区 4 月份月报来啦!这里将记录 SeaTunnel 社区每月的重要更新,欢迎关注! 月度 Merge 之星 感谢以下小伙伴 4 月为 ...

  2. [题解] [ABC221H] Count Multiset - DP

    [ABC221H] Count Multiset 题面翻译 输入两个正整数 \(N,M\),并存在一个集合,问你一个长度为 \(k\) 的合法集合存在多少个?你需要回答 \(k\) 的值为 \(1\) ...

  3. 什么是状态机?用C语言实现进程5状态模型

    前言 状态机在实际工作开发中应用非常广泛,在刚进入公司的时候,根据公司产品做流程图的时候,发现自己经常会漏了这样或那样的状态,导致整体流程会有问题,后来知道了状态机这样的东西,发现用这幅图就可以很清晰 ...

  4. Go进程内存占用那些事(一)

    为什么要探究这个问题? 作为基础设施供应商,自己的服务占用多少内存,为什么要占用这么多内存,需要能说的清楚.作为一个云计算开发,这点问题都弄不清楚,说不过去. § 0x01 范围 讨论的只限于Linu ...

  5. mysql学习问题记录

    Q: 问题 MySQL在创建外键索引时,使用工具会出现创建完成但是闪一下就没了 使用CONSTRAINT '外键索引名' FOREIGN KEY ('xx') REFERENCES 数据库名 (xx) ...

  6. Vue开发转到React开发,Prettier - Code formatter失效的问题

    Vue转到React,Prettier - Code formatter失效,按下Ctrl+S无效,需要手动格式化一次 然后选择默认的格式化方式 之后按下Ctrl+S就可以进行格式化啦!!!

  7. Pipenv 使用

    Pipenv 是 Python 官方推荐的依赖管理工具,旨在简化 pip 和 virtualenv 的使用.其使用 Pipfile 和 Pipfile.lock 来管理项目的依赖和虚拟环境. 安装 p ...

  8. LaTeX 编译中文文档

    介绍 LaTeX 原生不支持中文.为了添加中文的功能,我们需要引入宏包.XeLaTeX 原生支持中文.不过由于默认使用的字体是英文字体,我们需要设置中文字体之后才能用.不过由于一些原因,在使用 LaT ...

  9. webpack系列-webpack内置插件ProvidePlugin的应用(定义全局变量,例如vue引入jquery全局使用)

    vue+webpack使用ProvidePlugin插件引入jquery 先看一个实例,webpack+vue引入jquery并全局使用,这儿指通过配置,不是在静态页面使用script标签直接引入jq ...

  10. A股迎来中报季,合合信息文档解析技术辅助大模型深度解读财报

    财务报告是公众和投资者了解企业经营状况的主要信源之一.步入8月中下旬,上市公司进入了中报披露高峰期.据东方财富Choice数据统计,截至8月14日数据,A股有超过1715只个股公布了2024年半年度业 ...