【上一篇】简单的音频播放器(一)
这次为播放器填加了很多更好用的功能,把第一篇文章中的很多坑给填上了,比较大的提升在于播放列表的功能更充足(查看歌曲详情、歌曲搜索、歌曲删除等),同时也加上了Windows7任务栏的控制按钮,加入了播放模式的选择,专辑封面的显示等,主界面如下图:
【引用】:Microsoft.VisualBasic、Microsoft.WindowsAPICodePack、Microsoft.WindowsAPICodePack.Shell、
PresentationCore、taglib-sharp(蓝色的引用需要自行下载进行添加,vs内不包含)
【窗体】:①主界面:
①歌曲详情窗体
②删除歌曲窗体
【歌曲信息类】:用于存储歌曲信息和封装与歌曲信息有关的方法,我一直比较喜欢这样的写法,能使得代码结
构更清晰。
using Shell32; using System; using System.Collections.Generic; using System.Drawing; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; namespace SimpleMediaPlayer { public class SongsInfo { private string fileName; //1 private string filePath; private string filesize; //2 private string artist; //13 private string album; //14 private Image albumImage; private string year; //15 private string originName; //21 private string duration; //27 private string byteRate; //28 public string FileName { get => fileName; set => fileName = value; } public string FilePath { get => filePath; set => filePath = value; } public string Filesize { get => filesize; set => filesize = value; } public string Artist { get => artist; set => artist = value; } public string Album { get => album; set => album = value; } public Image AlbumImage { get => albumImage; set => albumImage = value; } public string Year { get => year; set => year = value; } public string OriginName { get => originName; set => originName = value; } public string Duration { get => duration; set => duration = value; } public string ByteRate { get => byteRate; set => byteRate = value; } public SongsInfo(string fPath) { SetSongInfo(fPath); SetAlbumArt(fPath); } private void SetSongInfo(string strPath) { try { ShellClass sh = new ShellClass(); Folder dir = sh.NameSpace(Path.GetDirectoryName(strPath)); FolderItem item = dir.ParseName(Path.GetFileName(strPath)); fileName = dir.GetDetailsOf(item, 0); if (fileName == string.Empty) fileName = "未知"; FilePath = strPath; filesize = dir.GetDetailsOf(item, 1); if (filesize == string.Empty) filesize = "未知"; artist = dir.GetDetailsOf(item, 13); if (artist == string.Empty) artist = "未知"; album = dir.GetDetailsOf(item, 14); if (album == string.Empty) album = "未知"; year = dir.GetDetailsOf(item, 15); if (year == string.Empty) year = "未知"; OriginName = dir.GetDetailsOf(item, 21); if (OriginName == string.Empty) OriginName = "未知"; duration = dir.GetDetailsOf(item, 27); if (duration == string.Empty) duration = "未知"; byteRate = dir.GetDetailsOf(item, 28); if (byteRate == string.Empty) byteRate = "未知"; //输出0-50位信息 //for (int i = -1; i < 50; i++) //{ // string str = dir.GetDetailsOf(item, i); // Console.WriteLine(i + " && " + str); //} } catch(Exception ex) { Console.WriteLine(ex); } } //获取歌曲封面 private void SetAlbumArt(string strPath) { if(strPath != "" && strPath != null) { TagLib.File file = TagLib.File.Create(strPath); if (file.Tag.Pictures.Length > 0) { var bin = (byte[])(file.Tag.Pictures[0].Data.Data); albumImage = Image.FromStream(new MemoryStream(bin)).GetThumbnailImage(100, 100, null, IntPtr.Zero); } else { albumImage = Properties.Resources.DefaultAlbum; } } else { albumImage = Properties.Resources.DefaultAlbum; } } } }
TagLib库用于处理媒体文件,例如视频、音频和照片等,例如解析照片、从文件中加载图片、加载音频文件等。
在这里我们就是使用了它从文件中加载图片以获得音频文件的封面图片,如果没有封面图片,则设置为默认封面。
Load事件:窗体加载时主要是设置一些程序状态(播放列表数据显示绑定,播放列表的歌单历史记录读取)
private void Form1_Load(object sender, EventArgs e) { this.odlgFile.Multiselect = true; lstSongList.DisplayMember = "fileName"; lstSongList.ValueMember = "filePath"; ReloadStatus(); ReadHistory(); //设置开机自启 //StarUp("1"); }
Shown事件:设置windows7任务栏略缩图与按钮功能(详见另外一篇博客)
private ThumbnailToolbarButton ttbbtnPlayPause; private ThumbnailToolbarButton ttbbtnPre; private ThumbnailToolbarButton ttbbtnNext; private void Form1_Shown(object sender, EventArgs e) { ttbbtnPlayPause = new ThumbnailToolbarButton(Properties.Resources.ttbbtnPlay, "播放"); ttbbtnPlayPause.Enabled = true; ttbbtnPlayPause.Click += new EventHandler(btnPlay_Click); ttbbtnPre = new ThumbnailToolbarButton(Properties.Resources.ttbbtnPre, "上一首"); ttbbtnPre.Enabled = true; ttbbtnPre.Click += new EventHandler(btnBack_Click); ttbbtnNext = new ThumbnailToolbarButton(Properties.Resources.ttbbtnNext, "下一首"); ttbbtnNext.Enabled = true; ttbbtnNext.Click += new EventHandler(btnNext_Click); TaskbarManager.Instance.ThumbnailToolbars.AddButtons(this.Handle, ttbbtnPre, ttbbtnPlayPause, ttbbtnNext); //裁剪任务栏略缩图到封面pictureBox TaskbarManager.Instance.TabbedThumbnail.SetThumbnailClip(this.Handle, new Rectangle(pbAlbumImage.Location, pbAlbumImage.Size)); }
Closed事件:保存播放列表历史记录、释放内存
private void Form1_FormClosed(object sender, FormClosedEventArgs e) { SaveHistory(); Application.Exit(); this.Dispose(); }
tsmiOpenFile_Click事件:①需要注意的是调用listbox的BeginUpdate()与EndUpdate()事件,等待歌单更新完成,防止闪烁。②保存播放历史至文本AddSongsToList()函数:添加歌曲到播放列表前,需要调用ExistInList()查询播放列表里是否已经存在(文件路径相同则视为同一首歌曲),如果存在则忽略添加。
private void tsmiOpenFile_Click(object sender, EventArgs e) { this.odlgFile.InitialDirectory = @"C:\Users\Rhine\Desktop"; this.odlgFile.Filter = "媒体文件|*.mp3;*.wav;*.wma;*.avi;*.mpg;*.asf;*.wmv"; if (odlgFile.ShowDialog() == DialogResult.OK) { lstSongList.BeginUpdate(); for (int i = 0; i < odlgFile.FileNames.Length; i++) { string path = odlgFile.FileNames[i]; AddSongsToList(path); } lstSongList.EndUpdate(); } //用于更新列表序列(在歌曲查找中会用到) UpdataSongList(); SaveHistory(); } private void AddSongsToList(string path) { if (!IsExistInList(path)) { SongsInfo songsInfo = new SongsInfo(path); lstSongList.Items.Add(songsInfo); WMPLib.IWMPMedia media = AxWmp.newMedia(path); AxWmp.currentPlaylist.appendItem(media); } } private bool IsExistInList(string path) { for (int i = 0; i < lstSongList.Items.Count; i++) { if (path == ((SongsInfo)lstSongList.Items[i]).FilePath) return true; } return false; }
没什么需要说,直接贴代码。
private void btnPlay_Click(object sender, EventArgs e) { if (AxWmp.playState.ToString() == "wmppsPlaying") //播放->暂停 { AxWmp.Ctlcontrols.pause(); btnPlay.BackgroundImage = Resources.播放Hover; //改变任务栏播放按钮图标 ttbbtnPlayPause.Icon = Resources.ttbbtnPlay; return; } else if(AxWmp.playState.ToString() == "wmppsPaused") //暂停->播放 { AxWmp.Ctlcontrols.play(); btnPlay.BackgroundImage = Resources.暂停Hover; ttbbtnPlayPause.Icon = Resources.ttbbtnPause; return; } //双击播放列表 if (lstSongList.SelectedIndex >= 0) { Play(lstSongList.SelectedIndex); } else MessageBox.Show("请选择要播放的曲目"); } } private void btnStop_Click(object sender, EventArgs e) { AxWmp.Ctlcontrols.stop(); } private void btnBack_Click(object sender, EventArgs e) { if(lstSongList.SelectedIndex == -1) MessageBox.Show("请先添加曲目至目录"); else if(lstSongList.SelectedIndex > 0) { AxWmp.Ctlcontrols.stop(); lstSongList.SelectedIndex -= 1; Play(lstSongList.SelectedIndex); } else { AxWmp.Ctlcontrols.stop(); lstSongList.SelectedIndex = lstSongList.Items.Count - 1; Play(lstSongList.SelectedIndex); } } private void btnNext_Click(object sender, EventArgs e) { if (lstSongList.SelectedIndex == -1) MessageBox.Show("请先添加曲目至目录"); else if (lstSongList.SelectedIndex < lstSongList.Items.Count-1) { AxWmp.Ctlcontrols.stop(); lstSongList.SelectedIndex += 1; Play(lstSongList.SelectedIndex); } else { AxWmp.Ctlcontrols.stop(); lstSongList.SelectedIndex = 0; Play(lstSongList.SelectedIndex); } }小标题写的播放核心,是因为这部分是保证程序正常播放音乐并显示播放信息的最重要的部分,主要是play()函数、播放进度同步计时器、状态信息显示与状态响应。
SongsInfo currPlaySong = new SongsInfo(null); private int jumpSongIndex; //手动选中需要在随机过程中跳过的歌曲 private void Play(int index) { lstSongList.SelectedIndex = index; if (AxWmp.playState.ToString() == "wmppsPlaying") //播放->其他状态 { AxWmp.Ctlcontrols.pause(); btnPlay.BackgroundImage = Resources.播放Hover; //改变任务栏按钮图标 ttbbtnPlayPause.Icon = Resources.ttbbtnPlay; return; } if (AxWmp.playState.ToString() != "wmppsPaused") //更改播放路径并播放 { //重新生成随机播放序列 BuildRandomList(lstSongList.Items.Count); jumpSongIndex = index; currPlaySong = (SongsInfo)lstSongList.Items[index]; AxWmp.URL = currPlaySong.FilePath; AxWmp.Ctlcontrols.play(); } else //暂停->播放 AxWmp.Ctlcontrols.play(); btnPlay.BackgroundImage = Resources.暂停Hover; ttbbtnPlayPause.Icon = Resources.ttbbtnPause; ttbbtnPlayPause.Tooltip = "暂停"; } private void timerPlay_Tick(object sender, EventArgs e) { string currPlayTime = null; currPlayTime = AxWmp.Ctlcontrols.currentPositionString; txtTime.Text = currPlayTime + "/" + currPlaySong.Duration.Remove(0, 3); //设置进度条位置 tkbMove.Value = (int)AxWmp.Ctlcontrols.currentPosition; } private void ReloadStatus() { //设置封面pictureBox的默认图片 pbAlbumImage.Image = Properties.Resources.DefaultAlbum; txtSongName.Text = "Windows Media Player"; txtTime.Text = "00:00/00:00"; tsslCurrentPlayState.Text = "就绪"; tkbVol.Value = tkbVol.Maximum / 2; lbVolumeVal.Text = "音量:"; tkbMove.Value = 0; //如果没有选中播放列表,默认选中第一项 if (lstSongList.Items.Count > 0 && lstSongList.SelectedIndex == -1) { lstSongList.SelectedIndex = 0; } } //播放状态改变事件 private void AxWmp_PlayStateChange(object sender, AxWMPLib._WMPOCXEvents_PlayStateChangeEvent e) { switch (e.newState) { case 0: // Stopped tsslCurrentPlayState.Text = "未知状态"; break; case 1: // Stopped tsslCurrentPlayState.Text = "停止"; timerPlay.Stop(); ReloadStatus(); break; case 2: // Paused tsslCurrentPlayState.Text = "暂停"; timerPlay.Stop(); break; case 3: // Playing tsslCurrentPlayState.Text = "正在播放"; timerPlay.Start(); txtSongName.Text = currPlaySong.FileName; pbAlbumImage.Image = currPlaySong.AlbumImage; tkbMove.Maximum = (int)AxWmp.currentMedia.duration; break; case 4: // ScanForward tsslCurrentPlayState.Text = "ScanForward"; break; case 5: // ScanReverse tsslCurrentPlayState.Text = "ScanReverse"; break; case 6: // Buffering tsslCurrentPlayState.Text = "正在缓冲"; break; case 7: // Waiting tsslCurrentPlayState.Text = "Waiting"; break; case 8: // MediaEnded tsslCurrentPlayState.Text = "MediaEnded"; break; case 9: // Transitioning tsslCurrentPlayState.Text = "正在连接"; break; case 10: // Ready tsslCurrentPlayState.Text = "准备就绪"; break; case 11: // Reconnecting tsslCurrentPlayState.Text = "Reconnecting"; break; case 12: // Last tsslCurrentPlayState.Text = "Last"; break; default: tsslCurrentPlayState.Text = ("Unknown State: " + e.newState.ToString()); break; } //Console.WriteLine(tsslCurrentPlayState.Text); //播放完毕后的操作 if (AxWmp.playState.ToString() == "wmppsMediaEnded") { Console.WriteLine(lstSongList.SelectedIndex + ":播放完毕"); //播放路径的获取与播放模式有关,在下一个小节里贴代码 string path = GetPath(); WMPLib.IWMPMedia media = AxWmp.newMedia(path); AxWmp.currentPlaylist.appendItem(media); } }play()函数中与播放按钮单击事件中对播放状态的判定的代码有冗余,需要进行重构。
TImer的间隔为50ms,(目前没有找到合适的间隔时间以获得精确的时间同步)。
状态改变事件内的case中给出了播放器的所有状态。
这部分在另外一篇博客里有单独的介绍,不详细说了,直接贴代码。
SongsInfo currPlaySong = new SongsInfo(null); public enum PlayMode { Shuffle = 0, SingleLoop, ListLoop }; public PlayMode currPlayMode = PlayMode.Shuffle; private int[] randomList; //随机播放序列 private int randomListIndex = 0; //序列索引 private int jumpSongIndex; //手动选中需要在随机过程中跳过的歌曲 //播放模式切换按钮单击事件 private void btnPlayMode_Click(object sender, EventArgs e) { if (currPlayMode == PlayMode.ListLoop) currPlayMode = PlayMode.Shuffle; else currPlayMode += 1; if (currPlayMode == PlayMode.SingleLoop) btnPlayMode.BackgroundImage = Properties.Resources.循环播放; else if (currPlayMode == PlayMode.ListLoop) btnPlayMode.BackgroundImage = Properties.Resources.顺序播放; else btnPlayMode.BackgroundImage = Properties.Resources.随机播放; } //获取播放歌曲路径 private string GetPath() { int currIndex = lstSongList.SelectedIndex; if (currPlayMode == PlayMode.ListLoop) //列表循环 { if (currIndex != lstSongList.Items.Count - 1) { currIndex += 1; } else currIndex = 0; } else if (currPlayMode == PlayMode.SingleLoop) //单曲循环 { //do nothing } else //随机播放 { if (randomListIndex > randomList.Length - 1) { //当局结束 StarNewRound(); } //匹配到需要跳过的歌曲 if (randomList[randomListIndex] == jumpSongIndex) { if (randomListIndex == randomList.Length - 1) { //当局结束 StarNewRound(); } else randomListIndex++; } currIndex = randomList[randomListIndex++]; } lstSongList.SelectedIndex = currIndex; currPlaySong = (SongsInfo)lstSongList.Items[currIndex]; return ((SongsInfo)lstSongList.Items[currIndex]).FilePath; } private void StarNewRound() { //重新生成随机序列 BuildRandomList(lstSongList.Items.Count); //第二轮开始 播放所有歌曲 不跳过 jumpSongIndex = -1; } private void BuildRandomList(int songListCount) { randomListIndex = 0; randomList = new int[songListCount]; //初始化序列 for (int i = 0; i < songListCount; i++) { randomList[i] = i; } //随机序列 for (int i = songListCount - 1; i >= 0; i--) { Random r = new Random(Guid.NewGuid().GetHashCode()); int j = r.Next(0, songListCount - 1); swap(randomList, i, j); } //输出序列 //for (int i = 0; i < songListCount; i++) //{ // Console.Write(randomList[i] + " "); //} //Console.WriteLine(" "); } private void swap(int[] arr, int a, int b) { int temp = arr[a]; arr[a] = arr[b]; arr[b] = temp; }
直接看代码,简单明了。
private void tkbVol_ValueChanged(object sender, EventArgs e) { AxWmp.settings.volume = tkbVol.Value; //滑动滑块时,将音量label用于显示值 lbVolumeVal.Text = tkbVol.Value.ToString() + "%"; } private void tkbVol_MouseHover(object sender, EventArgs e) { lbVolumeVal.Text = tkbVol.Value.ToString() + "%"; } private void tkbVol_MouseLeave(object sender, EventArgs e) { lbVolumeVal.Text = "音量:"; } private void tkbMove_Scroll(object sender, EventArgs e) { AxWmp.Ctlcontrols.currentPosition = (double)this.tkbMove.Value; }
没有太多的研究,网上资料很多,暂时没有想好将这个功能加到什么地方,只是用来实验而已。
//调用方法,传参1即可实现开机自启 private void StarUp(string flag) { string path = Application.StartupPath; string keyName = path.Substring(path.LastIndexOf("//") + 1); Microsoft.Win32.RegistryKey Rkey = Microsoft.Win32.Registry.CurrentUser.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", true); if(flag.Equals("1")) { if(Rkey == null) { Rkey = Microsoft.Win32.Registry.CurrentUser.CreateSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run"); } Rkey.SetValue(keyName, path + @"\SimpleMediaPlayer.exe"); } else { if(Rkey != null) { Rkey.DeleteValue(keyName, false); } } }
系统托盘最值得注意的地方就是,窗口关闭时,一定要手动释放掉notifyIcon的资源,否则会造成程序退出,但系统托盘图标不会消失的结果。
private void Form1_SizeChanged(object sender, EventArgs e) { //当窗口最小化时显示系统托盘 if (this.WindowState == FormWindowState.Minimized) { this.Visible = false; notifyIcon1.Visible = true; } } private void notifyIcon1_DoubleClick(object sender, EventArgs e) { this.Visible = true; notifyIcon1.Visible = false; this.WindowState = FormWindowState.Normal; } //********菜单项 //打开 private void tsmiOpenForm_Click(object sender, EventArgs e) { this.Visible = true; notifyIcon1.Visible = false; this.WindowState = FormWindowState.Normal; } //退出 private void tsmiQuit_Click(object sender, EventArgs e) { notifyIcon1.Visible = false; notifyIcon1.Dispose(); this.Close(); }
1.播放列表双击事件:功能是控制播放器的播放与暂停,做好逻辑判断就能实现功能,流程图如下(发现do nothing没有连到结束…不过不影响阅读)
private void lstSong_MouseDoubleClick(object sender, MouseEventArgs e) { int index = lstSongList.IndexFromPoint(e.Location); if (index != ListBox.NoMatches) { if (currPlaySong.FilePath == ((SongsInfo)lstSongList.Items[index]).FilePath) { //选中歌曲为正在播放的歌曲 if (AxWmp.playState.ToString() == "wmppsPlaying") { AxWmp.Ctlcontrols.pause(); btnPlay.BackgroundImage = Resources.播放; ttbbtnPlayPause.Icon = Resources.ttbbtnPlay; } else if (AxWmp.playState.ToString() == "wmppsPaused") { AxWmp.Ctlcontrols.play(); btnPlay.BackgroundImage = Resources.暂停; ttbbtnPlayPause.Icon = Resources.ttbbtnPause; } } else { //选中的为其他歌曲 BuildRandomList(lstSongList.Items.Count); jumpSongIndex = index; currPlaySong = (SongsInfo)lstSongList.Items[index]; AxWmp.URL = currPlaySong.FilePath; AxWmp.Ctlcontrols.play(); btnPlay.BackgroundImage = Resources.暂停; ttbbtnPlayPause.Icon = Resources.ttbbtnPause; } lstSongList.SelectedIndex = index; } }
2.播放列表右键菜单:右键菜单目前是对歌曲提供操作(详情、删除、打开文件位置),所以在播放列表的其它
位置不显示菜单,如果后续需要为播放列表加入个性化的操作,还需要重写菜单,菜单如下。
其中详情、删除歌曲,是通过打开新窗体,在新窗体中显示
a.【详情】:将当前选中的歌曲(即currSelectedSong)作为参数,传入SongInfoDetailForm中,直接读取信息。
主窗体中菜单的详情item单击事件代码:
private void tsmiSongInfoDetail_Click(object sender, EventArgs e) { SongInfoDetailForm songInfoDetailForm = new SongInfoDetailForm(currSelectedSong); songInfoDetailForm.Show(); }
详情窗体代码:
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace SimpleMediaPlayer { public partial class SongInfoDetailForm : Form { private SongsInfo currentSongInfo; public SongInfoDetailForm(SongsInfo sInfo) { InitializeComponent(); currentSongInfo = sInfo; } private void SongInfoDetailForm_Load(object sender, EventArgs e) { txtSongName.Text = currentSongInfo.FileName; txtArtist.Text = currentSongInfo.Artist; txtAlbum.Text = currentSongInfo.Album; txtYear.Text = currentSongInfo.Year; txtDuration.Text = currentSongInfo.Duration; txtByteRate.Text = currentSongInfo.ByteRate; txtFilePath.Text = currentSongInfo.FilePath; } } }
显示效果:
b.【删除歌曲】:设计这个功能时,我希望能够同时实现两个功能,①仅从播放列表移除歌曲②询问用户是否删除源文件。在设计过程中发现调用MessageBox不能够直接实现(因为我需要有个CheckBox来勾选是否删除源文件),所以我先想到了重写MessageBox,不过发现实现起来不如直接新写一个窗体简洁直观。所以最后通过新写一个窗体来实现。
主窗体中菜单的删除歌曲item单击事件代码:
private void tsmiRemoveSongFromList_Click(object sender, EventArgs e) { DeleteSongFormList deleteSongFormList = new DeleteSongFormList(currSelectedSong.FilePath); if(deleteSongFormList.ShowDialog() == DialogResult.OK) { lstSongList.Items.RemoveAt(lstSongList.SelectedIndex); SaveHistory(); } }
删除歌曲窗体代码:
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; using Microsoft.VisualBasic.FileIO; namespace SimpleMediaPlayer { public partial class DeleteSongFormList : Form { public bool ReturnIsDelete { get; protected set; } private string filePath; public DeleteSongFormList(string path) { InitializeComponent(); filePath = path; } private void btnYes_Click(object sender, EventArgs e) { ReturnIsDelete = true; if(cbIsDeleteLocalFile.Checked == true) { //删除本地文件 DeleteLocalFile(); } DialogResult = DialogResult.OK; } private void btnCancel_Click(object sender, EventArgs e) { ReturnIsDelete = false; DialogResult = DialogResult.Cancel; } private void DeleteLocalFile() { try { if(File.Exists(filePath)) { FileSystem.DeleteFile(filePath, UIOption.OnlyErrorDialogs, RecycleOption.SendToRecycleBin); } else { MessageBox.Show("文件不存在"); } } catch(Exception) { } } } }
注:写一个新窗体,需要解决的问题是,要把新窗体的值返回回来(CheckBox是否勾选,点击的是哪一个按钮)。解决方案通过①在DeleteSongForm里建立一个public的bool属性ReturnIsDelete来使主窗口中获取CheckBox的选中状态②通过DialogResult来返回按钮的点击结果。至于删除文件为什么要用VB库里的FileSystem.DeleteFile()函数,是因为system.IO里通过的File.Delete()函数不能使得删除文件加入到回收站,删除文件先放入回收站才会更合理一些,而不是直接彻底删除。
c.【打开文件位置】:打开文件位置,这个很简单,不多说。
private void tsmiOpenFilePath_Click(object sender, EventArgs e) { System.Diagnostics.Process.Start("Explorer.exe","/select,"+currSelectedSong.FilePath); }
d.【气泡提示】:listbox不能够完整的显示歌曲名,除了使用在鼠标悬停时通过气泡提示显示完整歌名,其他我也没有找到在不调整listbox长宽的前提下,更好的解决办法。
private void lstSongList_MouseMove(object sender, MouseEventArgs e) { int index = lstSongList.IndexFromPoint(e.Location); if (index != ListBox.NoMatches && preIndex != index) { preIndex = index; CreateTooltip(index); } } private void CreateTooltip(int index) { //悬停显示持续时长 tooltip1.AutoPopDelay = 5000; //悬停显示响应时间 tooltip1.InitialDelay = 100; //是否一直显示 tooltip1.ShowAlways = false; //重显间隔时间 tooltip1.ReshowDelay = 100; //淡入淡出效果 tooltip1.UseFading = true; tooltip1.SetToolTip(lstSongList, ((SongsInfo)lstSongList.Items[index]).FileName); }
显示效果:
截图出来感觉位置有点奇怪……,因为截图的原因,差了个鼠标图标显示是在鼠标位置的右下角。
这部分写的比较多和详细,因为第二次修改主要是在加入和修改与播放列表相关的功能,但其实还有欠缺,比如还没有设置序号,也没有针对播放列表显示的个性化功能,大小也不可更改等等,其实我觉得listbox的功能有限,listview从功能性与显示效果来讲会更好,有兴趣可以自己进行尝试。
列表的搜索功能也是比较重要的功能,尤其是在播放列表内歌曲比较多的时候。我一开是设计需求就是,①当搜素框中存在字符时,则进行匹配,显示匹配结果到listbox;不存在字符时,显示完整播放列表。所以当我们想再次还原到原本的播放列表时,就需要一个List来存储原本的播放列表(originListSong),通过读取originListSong来还原。当播放列表更新(添加、删除)的时候,也得要将originListSong进行更新(UpdataOringinSongList())。
②能够匹配字符、单词、句子,无需输入完整歌名,并且把结果直接再显示在listbox中。所以很容易发现,正则表达式是最符合在这里用来进行字符串匹配的。显然可以在TextBox的TextChanged事件中对输入字符来进行匹配,实时获取结果。
private void UpdataOringinSongList() { oringinListSong = new List(); for (int i = 0; i < lstSongList.Items.Count; i++) { oringinListSong.Add((SongsInfo)lstSongList.Items[i]); } } private void txtSreachSongName_TextChanged(object sender, EventArgs e) { Console.WriteLine(txtSreachSongName.Text); lbNotFoundSong.Visible = false; if (txtSreachSongName.Text == "") { ReadHistory(); return; } else { List sreachList = new List(); string strSreach = txtSreachSongName.Text; Regex r = new Regex(Regex.Escape(strSreach), RegexOptions.IgnoreCase); if (oringinListSong.Count > 0) { lstSongList.BeginUpdate(); for (int i = 0; i < oringinListSong.Count; i++) { Match m = r.Match(((SongsInfo)oringinListSong[i]).FileName); if (m.Success) { sreachList.Add(oringinListSong[i]); } } lstSongList.EndUpdate(); } if (sreachList.Count > 0) { lstSongList.Items.Clear(); lstSongList.BeginUpdate(); for (int i = 0; i < sreachList.Count; i++) { lstSongList.Items.Add(sreachList[i]); } lstSongList.EndUpdate(); } else { lstSongList.Items.Clear(); lbNotFoundSong.Visible = true; } } }
唯独需要需要单独说一说的就只有下面这行代码
Regex r = new Regex(Regex.Escape(strSreach), RegexOptions.IgnoreCase);Regex.Escape(strSreach)方法是防止匹配的字符串中存在* 、/ 、(、)等正则表达式中所保留的关键字符会崩
溃的问题;RegexOptions.IgnoreCase忽略大小写。
读取和保存播放历史,我选择的是将播放列表中的文件路径逐个保存到txt文本,使用"};{"来进行隔开,读取的时候使用stringSplit()将文本分开,保存到字符串数组中,再遍历数组逐条读取,添加到播放列表。当读取播放历史的文本文件不存在时,自动创建一个新的文本文件。
private void SaveHistory() { string saveString = ""; for (int i = 0; i < lstSongList.Items.Count; i++) { saveString += ((SongsInfo)lstSongList.Items[i]).FilePath + "};{"; } File.WriteAllText(Application.StartupPath + "\\songListHistory.txt", saveString); } private void ReadHistory() { string readString = ""; string historyFilePath = Application.StartupPath + "\\songListHistory.txt"; if (File.Exists(historyFilePath)) { readString = File.ReadAllText(historyFilePath); if (readString != "") { string[] arr = readString.Split(new string[] { "};{" }, StringSplitOptions.None); lstSongList.BeginUpdate(); foreach (string path in arr) { //目录文件不存在 if (path != null && path != "") { if(File.Exists(path)) { AddSongsToList(path); } } } lstSongList.EndUpdate(); } } else File.Create(historyFilePath); UpdataOringinSongList(); }
整个播放器就算是写完了,但功能依旧还没有做完,更高级一些的功能,如文件的拖拽添加、歌词同步、也没有可供用户自定义的功能接口等等。对了,还有这个shi一样的UI…(逃),或许有时间再改一改程序,再补写一篇后续优化的博客吧。就这样:)
码云地址:简单播放器
【下一篇】简单的音频播放器(三)
