昨天没事看了一下源程序中的歌词部分,不是很难,可是今天我在写的时候却在一个问题上面卡了很长时间,我没有用源程序里每次显示一句歌词,而是列表显示,并且当前句加粗显示,思路其实很简单,问题出现在列表移动时候的闪烁,对这个闪烁我先是用LABEL控件GDI+重绘,后来是PictureBox控件重绘,效果都不是很好,闪烁依旧存在,这直接让我怀疑GDI+的性能,很多人都说它不如GDI。后来我直接像我MFC里面那样,完全自己绘制,闪烁解决了,整整搞了一个下午,我也是醉了,也有人建议用DX2D来绘制,可能会好很多,毕竟DX绘制3D是很强悍的,我没有去试。先给出运行界面
当然源程序在歌词这块还是存在问题的,这不是重点,现在先分析一下源程序里面获取歌词这块
string exc=@"[a-zA-z]+://[^\s]*[a-zA-z]"; //用于匹配歌词连接的正则表达式 //[a-zA-z] 任意字母 + 多个 [^\s] 非制表符空格之类 string HTML;//保存网页源码 string LrcText; //歌词文本 string lrcAPI="http://geci.me/api/lyric/";//取歌词文件的API string fileName; //保存歌词路径 public string getLrc(string mp3Name) { lrcAPI = "http://geci.me/api/lyric/";//初始化 //此处做本地歌词判断 如果存在 就不需要下载 不存在 就下载 if (File.Exists(string.Format(".\\Lrc\\{0}.Lrc", mp3Name)) == true) { fileName =mp3Name; return "正在解析歌词..."; } else { lrcAPI = lrcAPI + mp3Name; WebClient wc = new WebClient(); wc.Credentials = CredentialCache.DefaultCredentials; // 获取或设置用于对向 Internet 资源的请求进行身份验证的网络凭据。 Encoding enc = Encoding.GetEncoding("UTF-8"); // 如果是乱码就改成 utf-8 / GB2312 Byte[] pageData = wc.DownloadData(lrcAPI); //获取数据 HTML = enc.GetString(pageData); MatchCollection matchs = Regex.Matches(HTML, exc);//开始对歌词进行匹配 if (matchs.Count == 0) { return "没有找到对应的歌词!"; } else { DownloadLrc(matchs[0].Value, mp3Name); //这里使用第一个匹配的 可能不对 这块我没看 return "歌词找到并下载成功!"; } } } public void DownloadLrc(string url,string FileName) { WebClient wc = new WebClient(); wc.Credentials = CredentialCache.DefaultCredentials; // 获取或设置用于对向 Internet 资源的请求进行身份验证的网络凭据。 Encoding enc = Encoding.GetEncoding("UTF-8"); // 如果是乱码就改成 utf-8 / GB2312 try { Byte[] pageData = wc.DownloadData(url); // 从资源下载数据并返回字节数组。 LrcText = enc.GetString(pageData); if (Directory.Exists(".\\Lrc") == false) { Directory.CreateDirectory(".\\Lrc"); } StreamWriter sw = new StreamWriter(String.Format(".\\Lrc\\{0}.Lrc", FileName), false, Encoding.UTF8); sw.Write(LrcText); sw.Flush(); //写入文件 sw.Close(); fileName = FileName; } catch (Exception) { } }
在分析歌词这块,我先给出LRC文件的一些例子,然后对应程序来看
[00:04.56]作词:刘德华&徐继宗 作曲:徐继宗 编曲:Billy Chan [03:44.21][00:10.78] [00:16.44]十七岁那日不要脸 参加了挑战 [00:22.43]明星也有训练班 短短一年太新鲜 [00:27.98]记得四哥 发哥 都已见过面 [00:34.26]后来 荣升主角太突然
//下面是我自己的程序 没有使用源程序里面 对时间字符串匹配 直接计算时间反而更准更有效 txtclass txt = new txtclass(); string excTime = @"(?<=\[).*?(?=\])"; //匹配时间的正则 // (?<=\[) 匹配 ‘[‘ 中间是任意多字符 string excText = @"(?<=\])(?!\[).*"; //匹配歌词的正则 // 寻找最后一个 ‘]’
string[] lrcText = new string[100]; //保存歌词文字 int[] lrcIndex = new int[100]; //保存顺序索引 int[] lrcNumTime = new int[100]; //保存计数时间 在后面判断时间 寻找对应为歌词文本 int total1 = 0; int total2 = 0; public void getLrc(string FileName) { total1 = 0; total2 = 0; string zj; int[] tpNumTime = new int[100]; int numTime; string[] strs = System.IO.File.ReadAllLines(FileName); int hasline = strs.Length; MatchCollection match1; MatchCollection match2; for (int i = 0; i <= hasline; i++) { match1 = Regex.Matches(txt.txtRead(FileName, i), excTime); //匹配集合 match2 = Regex.Matches(txt.txtRead(FileName, i), excText); foreach (var v in match1) { zj = v.ToString(); //获取字符串 try { numTime = int.Parse(zj.Substring(0, 2)) * 60 + int.Parse(zj.Substring(3, 2)); //只是计算分和秒 后面发现歌词偶尔出现偏差 快一格慢一格 lrcNumTime[total1] = numTime; tpNumTime[total1] = numTime; lrcIndex[total1] = total1; foreach (var t in match2) { lrcText[total2] = t.ToString(); } total1++; //递增 total2++; } catch (Exception) { } } } //排序 直接就比较交换了 当然数量少 没有优化的必要了 int tmp1; for(int i=0;i<total1-1;i++) { for(int j=i+1;j<total1;j++) { if (tpNumTime[j] < tpNumTime[i]) { //交换 tmp1 = tpNumTime[j]; tpNumTime[j] = tpNumTime[i]; tpNumTime[i] = tmp1; tmp1 = lrcIndex[j]; lrcIndex[j] = lrcIndex[i]; lrcIndex[i] = tmp1; } } } }
好了,歌词基本获得了,下面我先说一下我的思路。
第一种:使用Panel控件里面加上一个Label控件,然后一次性输出文本,然后移动就行,相当简单,但是实际证明这样效果很差,必须控制行距,这点,一个C#新手是真的不会怎样设置LABEL控件的行距,这个方案就Pass了
第二种:仿照以前的抽奖程序,放上几个Label控件,然后循环移动,这样既能控制行距,又能很好的显示歌词高亮,后续很多功能都可以再加上去
我就手绘一下,其实就是一个数组交换和整体的移动,也是相当简单,我就不贴代码了
第三种:针对上面的问题,背景纯色不会闪烁,但是一旦背景更换成图片后闪烁相当厉害,网上有很多都是用GDI+绘制,当然我也做了相关的,但是效果不是很理想
改变思路 其实就是在一张固定的图片上面绘制文字,不需要添加控件(控件透明属性在每次移动的时候会计算绘制,这是闪烁的根本原因),于是我删掉所有的控件,自己计算文本的
位置,绘制文本,实验证明这种方法速度很快,比单纯使用控件好多了(当然如果能很好的实现效果使用控件是最好的,其实像MFC做界面那完全就是自己控制绘制,要求很高,但是相对的
比C#开放多了,可控性很高,灵活性更好)
这里给出我PictureBox控件的一些代码
//绘制 Bitmap tpBit = new Bitmap(this.Width, this.Height); //建立图片 Graphics tpBitG = Graphics.FromImage(tpBit); //获取绘制GDI //绘制图片 if (bkBitmap != null) tpBitG.DrawImage(bkBitmap, 0, 0, getMainWndRect(), GraphicsUnit.Pixel); //这里需要自己计算位置矩形 //绘制文字 Graphics g = pe.Graphics;
if (text != "") { if (text_bold) tpBitG.DrawString(text, new Font("微软雅黑", text_size, FontStyle.Bold), new SolidBrush(Color.White), new Point(0, 0)); else tpBitG.DrawString(text, new Font("微软雅黑", text_size, FontStyle.Regular), new SolidBrush(Color.White), new Point(0, 0)); } g.DrawImage(tpBit, 0, 0); //绘制 将缓存里面的图片一次性绘制到界面上面来 //释放资源 tpBit.Dispose(); tpBitG.Dispose();
上面就是最简单的双缓存绘制,对比GDI,其实都差不多,但是我不知道最后的效果为什么差那么多,原理是相同的,双缓存就是先在缓存里面绘制好图片,然后一次性绘制到界面上面,DX里面还有三缓存,多缓存,都是这样的道理。
给大家个地址,介绍的很详细 http://blueve.me/archives/633
再来看看后面自己绘制,其实差不多,自己写一个类,保存绘制的相关信息,和原来LABEL控件的内容差不多
private string text; //文本内容 private bool text_bold = false; //文本加粗 private int text_size = 9; //字体大小 private Point location; //保存当前位置 public string Text { get { return text; } set { text = value; } } public bool Text_bold { get { return text_bold; } set { text_bold = value; } } public int Text_size { get { return text_size; } set { text_size = value; } } public Point Location { get { return location; } set { location = value; } }
没写相关的方法,就这些了,然后就是在Panel里面的绘制过程
Graphics g = e.Graphics; g.DrawImage(m_PanelLcrBit, 0, 0); //绘制背景图片 for (int i = 0; i < 8; i++) { if (m_lableLrc[i].Text!="") //绘制文本 { Font tpFont; Point locatePos; if (m_lableLrc[i].Text_bold) //设置字体 tpFont=new Font("微软雅黑",m_lableLrc[i].Text_size, FontStyle.Bold); else tpFont = new Font("微软雅黑", m_lableLrc[i].Text_size, FontStyle.Regular); SizeF sizeF = g.MeasureString(m_lableLrc[i].Text, tpFont); //字符串的宽度 locatePos = new Point((int)(panel_LRC.Width - sizeF.Width) / 2, m_lableLrc[i].Location.Y); //居中显示 g.DrawString(m_lableLrc[i].Text, tpFont, new SolidBrush(Color.White), locatePos); //绘制 } }
当然做完之后最好是关掉Panel控件檫除背景,我在MFC里面都是禁掉檫除背景这块,没有必要。
看上面的代码是不是相当简单,只是我一开始忽略了本质的问题,一味的使用控件来减少自己写代码的行数,这样反而使得程序变得更加冗余,虽然我是个新手,但是很多情况下我都是试着去考虑如何才能是代码看上去更加简洁,逻辑更加的清晰,我现在感觉在写这些的时候定时器是个坏东西,他让你的程序整个的分散了,而且你无法预料到什么时候会出现什么错误,很莫名的错误,以前在写DX3D的时候都直接C++在绘制窗体的里面来控制,没有定时器这一说法,当然里面存在时间控制,这样而言比定时器来触发效率更高,也更准确,当然这必须得靠自己慢慢去写了。
说了一大堆废话,下面在贴一些里面的控制代码
//寻找当前时间索引 try { string time = this.axWindowsMediaPlayer1.Ctlcontrols.currentPositionString; int currentPlayTime = int.Parse(time.Substring(0, 2)) * 60 + int.Parse(time.Substring(3, 2)); //解决头部开始情况 if (currentPlayTime >= 0 && currentPlayTime < LNumtime[Lindex[0]]) return 0; for (int i = 0; i < lrcCount - 1; i++) { if (LNumtime[Lindex[i]] <= currentPlayTime && currentPlayTime < LNumtime[Lindex[i + 1]]) { return i; } } //解决最后情况 return lrcCount - 1; } catch { //解决没开始播放 产生错误的情况 return 0; }
//移动4次 每次移动10 currentMove += lHangDis/4; //首先变更字体 if (currentMove==lHangDis/4) { m_lableLrc[3].Text_size = 9; m_lableLrc[3].Text_bold= false; m_lableLrc[4].Text_size = 10; m_lableLrc[4].Text_bold = true; } for(int i=0;i<8;i++) m_lableLrc[i].Location = new Point(6, m_lableLrc[i].Location.Y - lHangDis / 4); panel_LRC.Invalidate(false); if (currentMove==lHangDis) //移动结束 { //更换位置 LabelLcr toubu = m_lableLrc[0]; toubu.Location = new Point(6, 280); if (lCurrentIndex + 4 < lrcCount) toubu.Text = Ltext[Lindex[lCurrentIndex + 4]]; else toubu.Text = ""; //更换 for (int i = 0; i < 7; i++) m_lableLrc[i] = m_lableLrc[i + 1]; m_lableLrc[7] = toubu; timer6.Enabled = false; }
基本的就这些了,准备睡觉,明天还要看高频,蛋疼。。。。。。。。。。。。