【问题标题】:Dynamic Chat Window动态聊天窗口
【发布时间】:2014-09-11 15:02:00
【问题描述】:

我对 wpf gui 的性能有疑问。

首先我会解释我做了什么。 我从数据库中读取了不同的聊天数据,主要是文本,但有时文本中间会有一个图标,比如笑脸或类似的图标。或者,没有文字只是一个图像。

我通过使用 Flowdocument 并使用带有内联的 Textblock 来完成这一切。哦,我忘了,我用的是wpf,对不起。

这很好用,但是目前 Flowdocument 将被绘制到 RichTextbox 或 FlowdocumentReader,它需要很长时间并且 gui 冻结。我考虑过虚拟化,但 RichTextBox 不使用它。所以我的下一个想法是使用一个列表框,并为每个 Chatbubble 设置一个 Richtextbox 项目。一个聊天可以包含大约 20.000 个聊天气泡。 所以现在我想使用数据绑定,但我没有找到绑定文本块内联的方法。

现在有一些代码。

<DataTemplate x:Key="MessageDataTemplate" DataType="{x:Type classes:Message}">
            <Grid>
                <RichTextBox x:Name="rtbChat"
                    SpellCheck.IsEnabled="False"
                    VerticalScrollBarVisibility="Auto"
                    VerticalContentAlignment="Stretch">
                    <FlowDocument
                        FontFamily="Century Gothic"
                        FontSize="12"
                        FontStretch="UltraExpanded">
                        <Paragraph>
                            <Figure>
                                <BlockUIContainer>
                                    <Border>
                                        <Border>
                                            <Grid>
                                                <Grid.ColumnDefinitions>
                                                    <ColumnDefinition Width="150"/>
                                                    <ColumnDefinition Width="80"/>
                                                </Grid.ColumnDefinitions>
                                                <Grid.RowDefinitions>
                                                    <RowDefinition Height="15"/>
                                                    <RowDefinition Height="Auto"/>
                                                </Grid.RowDefinitions>
                                                <TextBlock x:Name="tUser"
                                                    Foreground="Gray"
                                                    TextAlignment="Right"
                                                    FontSize="10"
                                                    Grid.Row="0"
                                                    Grid.Column="1"
                                                    Text="{Binding displayUserName}"/>

                                                <TextBlock x:Name="tTime"
                                                    Foreground="Gray"
                                                    TextAlignment="Left"
                                                    FontSize="10"
                                                    Grid.Row="0"
                                                    Grid.Column="0"
                                                    Text="{Binding sendTime}"/>

                                                <TextBlock x:Name="tMessage"
                                                    Foreground="Black"
                                                    TextAlignment="Justify"
                                                    FontSize="12"
                                                    Height="NaN"
                                                    TextWrapping="Wrap"
                                                    Grid.Row="1"
                                                    Grid.Column="0"
                                                    Grid.ColumnSpan="2"
                                                    Text="{Binding contentText}"   />
                                                <Image x:Name="tImage"
                                                    Grid.Row="1"
                                                    Grid.Column="0"
                                                    Grid.ColumnSpan="2"
                                                    Height="NaN"
                                                    Source="{Binding imageSend}"/>
                                            </Grid>
                                        </Border>
                                    </Border>
                                </BlockUIContainer>
                            </Figure>
                        </Paragraph>
                    </FlowDocument>
                </RichTextBox>
            </Grid>
        </DataTemplate>

所以这不是最终的,我正在将它从源代码移植到 xaml 并且此时缺少一些 setter。

我已经对时间进行了基准测试,一切正常,sqlite 需要 10 毫秒,构建 FlowDocument 大约需要 4 秒,但在 RichTextBox 中绘制 FlowDocument 最多需要 5 分钟。我知道这就是为什么要绘制孔框,以及不可见的部分。

我希望这是可以理解的,如果不问我:)

这里是移植到 xaml 之前的源代码。

        var rtBox = new RichTextBox
        {
            //IsEnabled = false,
            BorderThickness = new Thickness(0, 0, 0, 0)
        };
        var doc = new FlowDocument();

        Contact contact = null;
        contact = _mess.remote_resource != "" ? _contacts.Find(x => x._jid == _mess.remote_resource) : _contacts.Find(x => x._jid == _mess.key_remote_jid);

        var para = new Paragraph();

        //--- Style of the message -----
        para.Padding = new Thickness(0);

        BlockUIContainer blockUI = new BlockUIContainer();
        blockUI.Margin = new Thickness(0, 0, 0, 0);
        blockUI.Padding = new Thickness(0);
        blockUI.TextAlignment = _mess.key_from_me == 1 ? TextAlignment.Right : TextAlignment.Left;

        Border bShadow = new Border();
        bShadow.Width = 231;
        bShadow.BorderBrush = Brushes.LightGray;
        bShadow.BorderThickness = new Thickness(0, 0, 0, 1);

        Border b2 = new Border();
        b2.Width = 230;
        b2.BorderBrush = Brushes.Gray;
        b2.Background = Brushes.White;
        b2.BorderThickness = new Thickness(0.5);
        b2.Padding = new Thickness(2);

        Grid g = new Grid();
        g.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(150,GridUnitType.Star) });
        g.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(80) });
        g.RowDefinitions.Add(new RowDefinition() { Height = new GridLength(15) });
        g.RowDefinitions.Add(new RowDefinition() { Height = new GridLength(25,GridUnitType.Auto) });

        TextBlock tUser = new TextBlock()
        {
            Foreground = Brushes.Gray,
            TextAlignment = TextAlignment.Right,
            FontSize = 10,
        };
        tUser.SetValue(Grid.RowProperty, 0);
        tUser.SetValue(Grid.ColumnProperty, 1);
        if(contact != null)
            tUser.Text = _mess.key_from_me == 1 ? "ich" : (contact._displayName == "" ? Whatsapp.Contacs.convertJidToNumber(_mess.remote_resource) : contact._displayName);
        else
        {
            tUser.Text = Whatsapp.Contacs.convertJidToNumber(_mess.remote_resource);
        }

        TextBlock tTime = new TextBlock()
        {
            Foreground = Brushes.Gray,
            TextAlignment = TextAlignment.Left,
            FontSize = 10,
        };
        tTime.SetValue(Grid.RowProperty, 0);
        tTime.SetValue(Grid.ColumnProperty, 0);
        tTime.Text = UnixTime.TimeReturnUnix2DateUtc(_mess.timestamp, timeZone).ToString();

        TextBlock tMessage = new TextBlock()
        {
            Foreground = Brushes.Black,
            TextAlignment = TextAlignment.Justify,
            FontSize = 12,
            Height = Double.NaN,
            TextWrapping = TextWrapping.Wrap

        };

        tMessage.SetValue(Grid.RowProperty, 1);
        tMessage.SetValue(Grid.ColumnProperty, 0);
        tMessage.SetValue(Grid.ColumnSpanProperty, 2);



        for (var i = 0; i < _mess.data.Length; i += Char.IsSurrogatePair(_mess.data, i) ? 2 : 1)
        {
            var x = Char.ConvertToUtf32(_mess.data, i);

            if (EmojiConverter.EmojiDictionary.ContainsKey(x))
            {

                //Generate new Image from Emoji
                var emoticonImage = new Image
                {
                    Width = 20,
                    Height = 20,
                    Margin = new Thickness(0, -5, 0, -5),
                    Source = EmojiConverter.EmojiDictionary[x]
                };

                //add grafik to FlowDocument
                tMessage.Inlines.Add(emoticonImage);
            }
            else
            {
                tMessage.Inlines.Add(new Run("" + _mess.data[i]));
            }
        }

        g.Children.Add(tUser);
        g.Children.Add(tTime);
        g.Children.Add(tMessage);

        b2.Child = g;
        bShadow.Child = b2;

        blockUI.Child = bShadow;

        Figure fig = new Figure(blockUI);
        fig.Padding = new Thickness(0);
        fig.Margin = new Thickness(0);
        fig.Height = new FigureLength(0, FigureUnitType.Auto);

        para.Inlines.Add(fig);

        doc.Blocks.Add(para);
        rtBox.Document = doc;
        msgList.Add(rtBox);

问候并感谢您的帮助。

【问题讨论】:

  • 发布一个更小但完整的代码示例,以便人们可以将其复制/粘贴到他们的办公桌上,然后为您提供帮助。
  • 所以你的内存中可能有 20,000 个RichTextBox?这看起来不是个好主意。
  • 没什么,一个“朋友”在这家“他们”工作的公司写的一个软件使用一个包含 40,000 个单元格的表格制作了一个散点图。两次。在一个 xaml 中。并且对于每个模态对话框都有新的 xml,也就是 4000+ 行 xml。它奏效了。所以我不会太担心:) [实际上,不,相反,担心很多。]
  • 所以我知道这不是最好的解决方案 :) 我对所有其他想法持开放态度。我的问题是我会显示这样的文本:bla bla (smiley as png) bla bla (smiley smiley) bla
  • 您有没有想过将每条消息的内容转换为视觉效果?

标签: c# wpf xaml


【解决方案1】:

当然,一种方法是使用ListBox 进行虚拟化。可以说更好的方法是动态加载所需的消息或进行自己的虚拟化控制(默认ListBox 虚拟化的问题包括您必须一次性滚动整个项目才能使虚拟化工作......这可能会很糟糕在某些情况下,从用户体验的角度来看。)

从它仍然需要永远加载的声音来看,您设置的虚拟化无法正常工作......

要使虚拟化正常工作,您需要让 ListBox 模板中的 ScrollViewer 具有 CanContentScroll=True。即:

<ListBox ScrollViewer.CanContentScroll="True" .... >

或者给ListBox一个类似下面的模板:

<ControlTemplate>
    <Border BorderBrush="{TemplateBinding Border.BorderBrush}"
            BorderThickness="{TemplateBinding Border.BorderThickness}"
            Background="{TemplateBinding Panel.Background}"
            SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}">
        <ScrollViewer Focusable="False"
                      Padding="{TemplateBinding Control.Padding}"
                      MaxHeight="{TemplateBinding Control.MaxHeight}"
                      CanContentScroll="True">
            <ItemsPresenter SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}" />
        </ScrollViewer>
    </Border>
</ControlTemplate>

另外,除非您想实际选择以前的消息,否则ListBox 可能不是您想要的,而您实际上想要ItemsControl?有关更多信息,请参阅Virtualizing an ItemsControl?

附加 1 - 平滑滚动 + 虚拟化:

见下文 - 如果您还想要平滑滚动,可能值得查看 TreeView - 请参阅 http://classpattern.com/smooth-scrolling-with-virtualization-wpf-list.html#.VBHWtfldXSg - 虽然我不能保证这是否真的有效,只是我自己发现了它!

补充 2 - 澄清 RE 需要的元素

就像下面我的 cmets 一样,如果你不编辑所有内容,你可以去掉所有标签:

&lt;Grid&gt;&lt;RichTextBox&gt;&lt;FlowDocument&gt;&lt;Paragraph&gt;&lt;Figure&gt;

在数据模板中。您可能无法将消息的 Text 绑定到 DataTemplate 中的 contentText,并且必须有一些幕后代码来动态生成 TextBlock 的内联。

附加 3 - 如何绑定 TextBlock 以包含 XAML 中的图像等

好的,总的来说(忽略一些样式),我建议如下:

<DataTemplate x:Key="MessageDataTemplate" DataType="{x:Type classes:Message}">
    <Border>
        <Border>
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="150"/>
                    <ColumnDefinition Width="80"/>
                </Grid.ColumnDefinitions>
                <Grid.RowDefinitions>
                    <RowDefinition Height="15"/>
                    <RowDefinition Height="Auto"/>
                </Grid.RowDefinitions>
                <TextBlock x:Name="tUser"
                    Foreground="Gray"
                    TextAlignment="Right"
                    FontSize="10"
                    Grid.Row="0"
                    Grid.Column="1"
                    Text="{Binding displayUserName}" />
                <TextBlock x:Name="tTime"
                    Foreground="Gray"
                    TextAlignment="Left"
                    FontSize="10"
                    Grid.Row="0"
                    Grid.Column="0"
                    Text="{Binding sendTime}" />
                <TextBlock x:Name="tMessage"
                    Foreground="Black"
                    TextAlignment="Justify"
                    FontSize="12"
                    Height="NaN"
                    TextWrapping="Wrap"
                    Grid.Row="1"
                    Grid.Column="0"
                    Grid.ColumnSpan="2"
                    classes:TextBlockInlineBinder.Inlines="{Binding contentInlines}" />
                <Image x:Name="tImage"
                    Grid.Row="1"
                    Grid.Column="0"
                    Grid.ColumnSpan="2"
                    Height="NaN"
                    Source="{Binding imageSend}" />
            </Grid>
        </Border>
    </Border>
</DataTemplate>

注意消息TextBlock 上的classes:TextBlockInlineBinder.Inlines="{Binding contentInlines}" 行。这是为了能够绑定到Inlines... 基本上这不是依赖属性,所以不能直接绑定!

相反,我们可以使用下面的自定义静态类TextBlockInlineBinder 创建一个静态依赖属性以添加到我们的TextBlock,当它更新时,它会运行InlinesChanged 方法来更新内联:

public static class TextBlockInlineBinder
{
    #region Static DependencyProperty Implementation

    public static readonly DependencyProperty InlinesProperty =
        DependencyProperty.RegisterAttached("Inlines",
        typeof(IEnumerable<Inline>),
        typeof(TextBlockInlineBinder),
        new UIPropertyMetadata(new Inline[0], InlinesChanged));

    public static string GetInlines(DependencyObject obj)
    {
        return (string)obj.GetValue(InlinesProperty);
    }

    public static void SetInlines(DependencyObject obj, string value)
    {
        obj.SetValue(InlinesProperty, value);
    }

    #endregion

    private static void InlinesChanged(DependencyObject sender, 
                                       DependencyPropertyChangedEventArgs e)
    {
        var value = e.NewValue as IEnumerable<Inline>;
        var textBlock = sender as TextBlock;
        textBlock.Inlines.Clear();
        textBlock.Inlines.AddRange(value);
    }
} 

最后,绑定(我已绑定到您的Message 类上的contentInlines 属性)需要为IEnumerable&lt;Inline&gt; 类型,即类似于:

public IEnumerable<Inline> contentInlines
{
    get {
        var inlines = new List<Inline>();
        for (var i = 0; i < _mess.data.Length; i += Char.IsSurrogatePair(_mess.data, i) ? 2 : 1)
        {
            var x = Char.ConvertToUtf32(_mess.data, i);

            if (EmojiConverter.EmojiDictionary.ContainsKey(x))
            {
                //Generate new Image from Emoji
                var emoticonImage = new Image
                {
                    Width = 20,
                    Height = 20,
                    Margin = new Thickness(0, -5, 0, -5),
                    Source = EmojiConverter.EmojiDictionary[x]
                };
                inlines.Add(emoticonImage);
            }
            else
            {
                inlines.Add(new Run("" + _mess.data[i]));
            }
        }
        return inlines;
    }
}

【讨论】:

  • 听起来,无论您如何显示消息(尽管在富文本框中不是最好的方式 - 如果您只是包括笑脸,可能动态生成的文本块和图片标签可能需要更少的时间来加载(!)),如果您可以在对话中有 20000 条消息,您将需要某种形式的虚拟化(!)
  • 所以你的意思是我应该创建例如 textbox(bla bla)textbox(png) textbox(bla bla) ?这在 ItemsControl 中,所以我可以滚动?应该始终可以滚动孔文本。为了更好地理解,您可以像 whatsapp 或类似的那样对其进行成像。
  • 如果您只是希望能够显示,而不是编辑文本,那么文本块和图像会更好,并且需要更少的开销。如果您希望能够编辑每个单独的帖子,并移动图像等,那么 RichTextBox 可能会更容易。 (尽管您必须考虑您是否真的希望用户能够做 RTB 允许他们做的所有强大的事情 - 例如使用 Ctrl+B 等使事情变得大胆)
  • 上面的代码适用于ListBox,但根据我评论中的链接,可以使用ItemsControl 完成类似的事情 - 老实说,ListBox 可能更容易使用:)。完成的默认虚拟化是为了让尽可能多的整体项目在任何时候都显示在屏幕上,但没有部分项目。如果您希望平滑滚动和虚拟化,我刚刚读到了TreeView,例如这里classpattern.com/… - 虽然我自己不能保证:)
  • @nefas - 抱歉,应该是 TextBlocks - 即 msdn.microsoft.com/en-us/library/… 所以 基本上 你需要做的就是摆脱标签:
    只需使用第一个 Border 标签即可启动您的 DataTemplate。
猜你喜欢
  • 1970-01-01
  • 2010-10-18
  • 1970-01-01
  • 1970-01-01
  • 2022-01-23
  • 1970-01-01
  • 1970-01-01
  • 2021-04-10
  • 1970-01-01
相关资源
最近更新 更多