上一篇中,我们讲解的是这个小软件的重构:使用可二进制化的Model类代替拼接字符串的方式,这样做的好处是使得代码可读性更强,更容易维护,当然,也更符合面向对象的思想:处处皆对象。
效果图览
在这一篇中,主要涉及的内容是新增的QQ表情功能。这个功能的设计牵涉到了正则表达式,我们先来看看截图:
弹出选择表情面板:
3个用户的具体聊天内容:
看到GIF图像在跳动
其中有一个用户已经下线
下面是设计的准备工作:
首先,我们需要一个能够支持图片输入的TextBox,这里我选择了这篇文章中介绍的控件:C# 实现IM聊天信息输入显示控件(1)-显示GIF动画图片,这个控件通过QQ自带的ImageOle.dll ActiveX控件实现插入动画表情,所以说在使用之前,需要先利用regsvr32.exe命令注册这个dll,具体命令为:regsvr32.exe ImageOle.dll。
当然做完了之后,直接在VS中添加COM引用即可。
其次,弹出选择表情面板是必不可少的,这里我们利用一个二维的PictureBox数组来存储GIF表情动画,并放置到Panel容器中:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
private void LoadingEmotion() { PictureBox[,] picList = new PictureBox[5,10]; for (int i = 0; i < 5; i++) { for (int j = 0; j < 10; j++) { int emotionSequenceCount = i * 10 + j; picList[i,j] = new PictureBox(); picList[i, j].Height = picList[i, j].Width = 24; picList[i, j].Image = Image.FromFile(".\\Face2\\" + emotionSequenceCount + ".gif"); picList[i, j].Top = i * 24; picList[i, j].Left = j * 24; picList[i, j].Tag = "#(" + emotionSequenceCount + ")#"; picList[i, j].Parent = panImg; picList[i, j].Click += new EventHandler((sender, e) => { this.rSendContent.AppendText("#(" + emotionSequenceCount + ")#"); emotionFlag = false; this.panImg.Visible = emotionFlag; }); panImg.Controls.Add(picList[i,j]); } } }
上面的GIF动画位置是通过对象的Top和Left方法来控制的,非常的方便;同时,把每个GIF表情的代码放到了Tag中进行保存,以方便调用,并且利用了匿名方法来注册PictureBox的点击事件。每次点击图标,会自动在发送文本框中生成类似#(0)#或者#(1)#等的代码,这些代码代表了是哪个表情,比如#(0)#就代表了第一行一列的表情,#(1)#代表了第1行2列的表情,依次类推。
最后就是输入的时候,如何进行表情匹配了。比如用户输入了如下的内容:
Hello Shi#(0)#, How are you today?#(1)##(2)#
其中#(0)#,#(1)#,#(2)#是由我们通过点击表情输入进去的,那么发送到对方的机器上的时候,就需要被解析成
Hello Shi, How are you today?
,该如何进行呢?
其实,我的做法就是在这句话的头部和尾部加上#(S)#和#(E)#标记以区别头尾,
#(S)#Hello Shi#(0)#, How are you today?#(1)##(2)##(E)#
然后,通过如下的正则来进行分段匹配,其中,?=的作用主要是负向前查找,但是不包含本身。具体内容请参见正则表达式点滴2
Regex regex = new Regex(@"(#\([0-9|S|E]+\)#).*?(?=#\([0-9|S|E]+\)#)", RegexOptions.Singleline | RegexOptions.IgnoreCase);
那么得到的结果就被分成了几段:
#(S)#Hello Shi
#(0)#, How are you today?
#(1)#
#(2)#
#(S)#
这就是分成的5段,然后观察这5段就发现,每段开始都是一个图片的标记(#(S)#和#(E)#除外,那是开始结束标志),然后跟着的是一段文本或者是什么都不跟。
那么这样的话,我们再继续对这些段进行区分:
Regex regexImage = new Regex(@"(#\([0-9|S|E]+\)#)", RegexOptions.Singleline | RegexOptions.IgnoreCase); Regex regexPlainText = new Regex(@"(?<=(#\([0-9|S|E]+\)#)).*", RegexOptions.Singleline | RegexOptions.IgnoreCase);
其中
regexImage主要匹配其中的图片标记,比如#(0)#,
regexPlainText主要匹配其中的文本或者空白字段,拿第二段来说,匹配的结果就是:
#(0)#
, How are you today?
这样就将表情和文本完全分离出来了,最后直接将表情文字替换为真实图片,并添加到信息窗体中:
if (!String.IsNullOrEmpty(matchImage.Value)) { if (!matchImage.Value.Contains("#(S)#")) { rAllContent.InsertImageUseImageOle(".\\Face2\\" + matchImage.Value.Replace("#(", string.Empty).Replace(")#", string.Empty) + ".gif"); } rAllContent.AppendText(matchPlainText.Value); }
这里补充一下本新增功能中用到的正则知识:
- 向前查找
从语法上看,一个向前查找模式其实就是一个以?=开头的子表达式,需要匹配的文本跟在=的后面。
比如我们需要知道一些URL用的是http还是https,则可以利用向前查找:
http://www.cnblogs.com
正则匹配为:.+(?=:)
结果为: http
如果利用.+(:) ,则为 http:
- 向后查找
也就是查找出现在被匹配文本之前的字符(但不消费它),操作符是?<=
文本为:ABC0: $12.56
匹配为: (?<=\$)[0-9.]+
如果不佳?<=,结果为$12.56,反之为12.56
- 向前向后查找集合
例如以下文本:
<head>
<title>Ben Forta’s HomePage</title>
</head>
这里我们如果想得到<title>与</title>标签内的内容,但是不包含<title>和</title>标签,如果不利用向前向后查找的话,将显得异常麻烦。利用向前向后匹配,只需要一个正则表达式就可以搞定:
正则匹配为:(?<=<title>).*?(?=</title>)
刚刚说道的向前向后查找,说准确点应该叫做正向前查找和正向后查找。当然,这里还存在这负向前查找和负向后查找:
操作符 |
说明 |
(?=) |
正向前查找 |
(?!) |
负向前查找 |
(?<=) |
正向后查找 |
(?<!) |
负向后查找 |
- 负向后查找
文本为:I paid $30 for 100 apples.
匹配为:\b(?<!\$)\d+\b
这个的意思是查找不带有$符号的数字,这里的匹配结果是100
当然,负向前查找和这个使用方式类似,暂略。
全部代码如下:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
public static void AddContent(string text,ChatRichTextBox rAllContent) { //解析发送的内容,实现表情匹配。 string sendText = "#(S)#" + text + "#(E)#"; Regex regex = new Regex(@"(#\([0-9|S|E]+\)#).*?(?=#\([0-9|S|E]+\)#)", RegexOptions.Singleline | RegexOptions.IgnoreCase); Regex regexImage = new Regex(@"(#\([0-9|S|E]+\)#)", RegexOptions.Singleline | RegexOptions.IgnoreCase); Regex regexPlainText = new Regex(@"(?<=(#\([0-9|S|E]+\)#)).*", RegexOptions.Singleline | RegexOptions.IgnoreCase); MatchCollection matches = regex.Matches(sendText); foreach (Match match in matches) { string matchedValue = match.Value; Match matchImage = regexImage.Match(matchedValue); Match matchPlainText = regexPlainText.Match(matchedValue); if (!String.IsNullOrEmpty(matchImage.Value)) { if (!matchImage.Value.Contains("#(S)#")) { rAllContent.InsertImageUseImageOle(".\\Face2\\" + matchImage.Value.Replace("#(", string.Empty).Replace(")#", string.Empty) + ".gif"); } rAllContent.AppendText(matchPlainText.Value); } } }
源码下载