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中分离歌词与时间对应的数组吗,然后使用二分查找找到最符合的歌词然后选中。