【WPF】实现类似QQ聊天消息的界面
最近公司有个项目,是要求实现类似 qq 聊天这种功能的。
如下图
这没啥难的,稍微复杂的也就表情的解析而已。
表情在传输过程中的实现参考了新浪微博,采用半角中括号代表表情的方式。例如:“abc[doge]def”就会显示 abc,然后一个,再 def。
于是动手就干。
创建一个模板控件来进行封装,我就叫它 chatmessagecontrol,有一个属性 text,表示消息内容。内部使用一个 textblock 来实现。
于是博主三下五除二就写出了以下代码:
c#
[templatepart(name = textblocktemplatename, type = typeof(textblock))] public class chatmessagecontrol : control { public static readonly dependencyproperty textproperty = dependencyproperty.register(nameof(text), typeof(string), typeof(chatmessagecontrol), new propertymetadata(default(string), ontextchanged)); private const string textblocktemplatename = "part_textblock"; private static readonly dictionary<string, string> emotions = new dictionary<string, string> { ["doge"] = "pack://application:,,,/wpfqqchat;component/images/doge.png", ["喵喵"] = "pack://application:,,,/wpfqqchat;component/images/喵喵.png" }; private textblock _textblock; static chatmessagecontrol() { defaultstylekeyproperty.overridemetadata(typeof(chatmessagecontrol), new frameworkpropertymetadata(typeof(chatmessagecontrol))); } public string text { get => (string)getvalue(textproperty); set => setvalue(textproperty, value); } public override void onapplytemplate() { _textblock = (textblock)gettemplatechild(textblocktemplatename); updatevisual(); } private static void ontextchanged(dependencyobject d, dependencypropertychangedeventargs e) { var obj = (chatmessagecontrol)d; obj.updatevisual(); } private void updatevisual() { if (_textblock == null) { return; } _textblock.inlines.clear(); var buffer = new stringbuilder(); foreach (var c in text) { switch (c) { case '[': _textblock.inlines.add(buffer.tostring()); buffer.clear(); buffer.append(c); break; case ']': var current = buffer.tostring(); if (current.startswith("[")) { var emotionname = current.substring(1); if (emotions.containskey(emotionname)) { var image = new image { width = 16, height = 16, source = new bitmapimage(new uri(emotions[emotionname])) }; _textblock.inlines.add(new inlineuicontainer(image)); buffer.clear(); continue; } } buffer.append(c); _textblock.inlines.add(buffer.tostring()); buffer.clear(); break; default: buffer.append(c); break; } } _textblock.inlines.add(buffer.tostring()); } }
因为这篇博文只是个演示,这里博主就只放两个表情好了,并且耦合在这个控件里。
xaml
<style targettype="local:chatmessagecontrol"> <setter property="template"> <setter.value> <controltemplate targettype="local:chatmessagecontrol"> <textblock x:name="part_textblock" textwrapping="wrap" /> </controltemplate> </setter.value> </setter> </style>
没啥好说的,就是包了一层而已。
效果:
自我感觉良好,于是乎博主就提交代码,发了个版本到测试环境了。
但是,第二天,测试却给博主提了个 bug。消息无法选择、复制。
在 uwp 里,textblock 控件是有 istextselectionenabled 属性的,然而 wpf 并没有。这下头大了,于是博主去查了一下 *,大佬们回答都是说用一个 isreadonly 为 true 的 textbox 来实现。因为我这里包含了表情,所以用 richtextbox 来实现吧。不管行不行,先试试再说。
在原来的代码上修改一下,反正表情解析一样的,但这里博主为了方便写 blog,就新开一个控件好了。
c#
[templatepart(name = richtextboxtemplatename, type = typeof(richtextbox))] public class chatmessagecontrolv2 : control { public static readonly dependencyproperty textproperty = dependencyproperty.register(nameof(text), typeof(string), typeof(chatmessagecontrolv2), new propertymetadata(default(string), ontextchanged)); private const string richtextboxtemplatename = "part_richtextbox"; private static readonly dictionary<string, string> emotions = new dictionary<string, string> { ["doge"] = "pack://application:,,,/wpfqqchat;component/images/doge.png", ["喵喵"] = "pack://application:,,,/wpfqqchat;component/images/喵喵.png" }; private richtextbox _richtextbox; static chatmessagecontrolv2() { defaultstylekeyproperty.overridemetadata(typeof(chatmessagecontrolv2), new frameworkpropertymetadata(typeof(chatmessagecontrolv2))); } public string text { get => (string)getvalue(textproperty); set => setvalue(textproperty, value); } public override void onapplytemplate() { _richtextbox = (richtextbox)gettemplatechild(richtextboxtemplatename); updatevisual(); } private static void ontextchanged(dependencyobject d, dependencypropertychangedeventargs e) { var obj = (chatmessagecontrolv2)d; obj.updatevisual(); } private void updatevisual() { if (_richtextbox == null) { return; } _richtextbox.document.blocks.clear(); var paragraph = new paragraph(); var buffer = new stringbuilder(); foreach (var c in text) { switch (c) { case '[': paragraph.inlines.add(buffer.tostring()); buffer.clear(); buffer.append(c); break; case ']': var current = buffer.tostring(); if (current.startswith("[")) { var emotionname = current.substring(1); if (emotions.containskey(emotionname)) { var image = new image { width = 16, height = 16, source = new bitmapimage(new uri(emotions[emotionname])) }; paragraph.inlines.add(new inlineuicontainer(image)); buffer.clear(); continue; } } buffer.append(c); paragraph.inlines.add(buffer.tostring()); buffer.clear(); break; default: buffer.append(c); break; } } paragraph.inlines.add(buffer.tostring()); _richtextbox.document.blocks.add(paragraph); } }
xaml
<style targettype="local:chatmessagecontrolv2"> <setter property="foreground" value="black" /> <setter property="template"> <setter.value> <controltemplate targettype="local:chatmessagecontrolv2"> <richtextbox x:name="part_richtextbox" minheight="0" background="transparent" borderbrush="transparent" borderthickness="0" foreground="{templatebinding foreground}" isreadonly="true"> <richtextbox.resources> <resourcedictionary> <style targettype="paragraph"> <setter property="margin" value="0" /> <setter property="padding" value="0" /> <setter property="textindent" value="0" /> </style> </resourcedictionary> </richtextbox.resources> <richtextbox.contextmenu> <contextmenu> <menuitem command="applicationcommands.copy" header="复制" /> </contextmenu> </richtextbox.contextmenu> </richtextbox> </controltemplate> </setter.value> </setter> </style>
xaml 稍微复杂一点,因为我们需要让一个文本框高仿成一个文字显示控件。
感觉应该还行,然后跑起来之后
复制是能复制了,然而我的布局呢?
因为一时间也没想到解决办法,于是博主只能回滚代码,把 bug 先晾在那里了。
经过了几天上班带薪拉屎之后,有一天博主在厕所间玩着宝石连连消的时候突然灵光一闪。对于 textblock 来说,只是不能选择而已,布局是没问题的。对于 richtextbox 来说,布局不正确是由于 wpf 在测量与布局的过程中给它分配了无限大的宽度。那么,能不能将两者结合起来,textblock 做布局,richtextbox 做功能呢?想到这里,博主关掉了宝石连连消,擦上屁股,开始干活。
c#
[templatepart(name = textblocktemplatename, type = typeof(textblock))] [templatepart(name = richtextboxtemplatename, type = typeof(richtextbox))] public class chatmessagecontrolv3 : control { public static readonly dependencyproperty textproperty = dependencyproperty.register(nameof(text), typeof(string), typeof(chatmessagecontrolv3), new propertymetadata(default(string), ontextchanged)); private const string richtextboxtemplatename = "part_richtextbox"; private const string textblocktemplatename = "part_textblock"; private static readonly dictionary<string, string> emotions = new dictionary<string, string> { ["doge"] = "pack://application:,,,/wpfqqchat;component/images/doge.png", ["喵喵"] = "pack://application:,,,/wpfqqchat;component/images/喵喵.png" }; private richtextbox _richtextbox; private textblock _textblock; static chatmessagecontrolv3() { defaultstylekeyproperty.overridemetadata(typeof(chatmessagecontrolv3), new frameworkpropertymetadata(typeof(chatmessagecontrolv3))); } public string text { get => (string)getvalue(textproperty); set => setvalue(textproperty, value); } public override void onapplytemplate() { _textblock = (textblock)gettemplatechild(textblocktemplatename); _richtextbox = (richtextbox)gettemplatechild(richtextboxtemplatename); updatevisual(); } private static void ontextchanged(dependencyobject d, dependencypropertychangedeventargs e) { var obj = (chatmessagecontrolv3)d; obj.updatevisual(); } private void updatevisual() { if (_textblock == null || _richtextbox == null) { return; } _textblock.inlines.clear(); _richtextbox.document.blocks.clear(); var paragraph = new paragraph(); var buffer = new stringbuilder(); foreach (var c in text) { switch (c) { case '[': _textblock.inlines.add(buffer.tostring()); paragraph.inlines.add(buffer.tostring()); buffer.clear(); buffer.append(c); break; case ']': var current = buffer.tostring(); if (current.startswith("[")) { var emotionname = current.substring(1); if (emotions.containskey(emotionname)) { { var image = new image { width = 16, height = 16 };// 占位图像不需要加载 source 了 _textblock.inlines.add(new inlineuicontainer(image)); } { var image = new image { width = 16, height = 16, source = new bitmapimage(new uri(emotions[emotionname])) }; paragraph.inlines.add(new inlineuicontainer(image)); } buffer.clear(); continue; } } buffer.append(c); _textblock.inlines.add(buffer.tostring()); paragraph.inlines.add(buffer.tostring()); buffer.clear(); break; default: buffer.append(c); break; } } _textblock.inlines.add(buffer.tostring()); paragraph.inlines.add(buffer.tostring()); _richtextbox.document.blocks.add(paragraph); } }
c# 代码相当于把两者结合起来而已。
xaml
<style targettype="local:chatmessagecontrolv3"> <setter property="foreground" value="black" /> <setter property="template"> <setter.value> <controltemplate targettype="local:chatmessagecontrolv3"> <grid> <textblock x:name="part_textblock" padding="6,0,6,0" ishittestvisible="false" opacity="0" textwrapping="wrap" /> <richtextbox x:name="part_richtextbox" width="{binding elementname=part_textblock, path=actualwidth}" minheight="0" background="transparent" borderbrush="transparent" borderthickness="0" foreground="{templatebinding foreground}" isreadonly="true"> <richtextbox.resources> <resourcedictionary> <style targettype="paragraph"> <setter property="margin" value="0" /> <setter property="padding" value="0" /> <setter property="textindent" value="0" /> </style> </resourcedictionary> </richtextbox.resources> <richtextbox.contextmenu> <contextmenu> <menuitem command="applicationcommands.copy" header="复制" /> </contextmenu> </richtextbox.contextmenu> </richtextbox> </grid> </controltemplate> </setter.value> </setter> </style>
xaml 大体也是将两者结合起来,但是把 textblock 设置为隐藏(但占用布局),而 richtextbox 则绑定 textblock 的宽度。
至于为啥 textblock 有一个左右边距为 6 的 padding 嘛。在运行之后,博主发现,richtextbox 的内容会离左右有一定的距离,但是没找到相关的属性能够设置,如果正在看这篇博文的你,知道相关的属性的话,可以在评论区回复一下,博主我将会万分感激。
最后是我们的效果啦。
最后,因为现在 wpf 是开源()的了,因此已经蛋疼不已的博主果断提了一个 issue(),希望有遇到同样困难的小伙伴能在上面支持一下,让巨硬早日把 textblock 选择这功能加上。
推荐阅读
-
使用 electron 实现类似新版 QQ 的登录界面效果(阴影、背景动画、窗体3D翻转)
-
使用 electron 实现类似新版 QQ 的登录界面效果(阴影、背景动画、窗体3D翻转)
-
(仿QQ聊天消息列表加载)wp7 listbox 列表项逐一加载的一种实现方式,以及加入渐显动画
-
基于Java的Socket类Tcp网络编程实现实时聊天互动程序:QQ聊天界面的搭建
-
【WPF】实现类似QQ聊天消息的界面
-
(仿QQ聊天消息列表加载)wp7 listbox 列表项逐一加载的一种实现方式,以及加入渐显动画
-
QQ聊天消息展示和评论提交功能使用JavaScript实现的代码详解
-
基于Java的Socket类Tcp网络编程实现实时聊天互动程序:QQ聊天界面的搭建
-
javascript - 想做一个类似于QQ的网页版聊天功能,如何实现??
-
【WPF】实现类似QQ聊天消息的界面