欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

【WPF】实现类似QQ聊天消息的界面

程序员文章站 2022-04-03 23:43:17
最近公司有个项目,是要求实现类似 QQ 聊天这种功能的。 如下图 这没啥难的,稍微复杂的也就表情的解析而已。 表情在传输过程中的实现参考了新浪微博,采用半角中括号代表表情的方式。例如:“abc[doge]def”就会显示 abc,然后一个,再 def。 于是动手就干。 创建一个模板控件来进行封装,我 ......

最近公司有个项目,是要求实现类似 qq 聊天这种功能的。

如下图

【WPF】实现类似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>

没啥好说的,就是包了一层而已。

效果:

【WPF】实现类似QQ聊天消息的界面

自我感觉良好,于是乎博主就提交代码,发了个版本到测试环境了。

 

但是,第二天,测试却给博主提了个 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 稍微复杂一点,因为我们需要让一个文本框高仿成一个文字显示控件。

 

感觉应该还行,然后跑起来之后

【WPF】实现类似QQ聊天消息的界面

复制是能复制了,然而我的布局呢?

 

因为一时间也没想到解决办法,于是博主只能回滚代码,把 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】实现类似QQ聊天消息的界面

 

最后,因为现在 wpf 是开源()的了,因此已经蛋疼不已的博主果断提了一个 issue(),希望有遇到同样困难的小伙伴能在上面支持一下,让巨硬早日把 textblock 选择这功能加上。