4

对于我正在编写的聊天客户端,我想创建以下控件:

在此处输入图像描述

它应该由三个用户可调整大小的列组成,其中可以显示任意文本,但仍然相互对齐(正如您可以看到 Jeff 所说的那样)。

我已经有一个自定义RichTextBox,可以显示预格式化的文本并自动滚动到底部,但是我将如何创建一个可调整大小的列的文本框让我感到困惑(我对创建自己的控件还很陌生)。

关于寻找什么或一般想法的任何指示?任何帮助表示赞赏!

4

3 回答 3

3

好的。忘记winforms。它无用、已弃用、丑陋,它不允许自定义,并且由于缺乏 UI 虚拟化和硬件渲染而速度慢得要命。

这是我对您描述的内容的看法:

<Window x:Class="MiscSamples.ThreeColumnChatSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:MiscSamples"
        Title="ThreeColumnChatSample" Height="300" Width="300">
    <Window.Resources>
        <local:FlowDocumentToXamlConverter x:Key="DocumentConverter"/>
    </Window.Resources>
    <ListView ItemsSource="{Binding}" ScrollViewer.HorizontalScrollBarVisibility="Hidden">
        <ListView.View>
            <GridView>
                <GridView.Columns>
                    <GridViewColumn DisplayMemberBinding="{Binding DateTime}"/>
                    <GridViewColumn DisplayMemberBinding="{Binding Sender}"/>
                    <GridViewColumn>
                        <GridViewColumn.CellTemplate>
                            <DataTemplate>
                                <FlowDocumentScrollViewer Document="{Binding Content, Converter={StaticResource DocumentConverter}}"
                                                          VerticalScrollBarVisibility="Hidden" HorizontalScrollBarVisibility="Hidden"/>
                            </DataTemplate>
                        </GridViewColumn.CellTemplate>
                    </GridViewColumn>
                </GridView.Columns>
            </GridView>
        </ListView.View>
    </ListView>
</Window>

后面的代码:

 public partial class ThreeColumnChatSample : Window
    {
        public ObservableCollection<ChatEntry> LogEntries { get; set; }

        private string TestData = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum";
        private List<string> words;
        private int maxword;
        public Random random { get; set; }

        public ThreeColumnChatSample()
        {
            InitializeComponent();

            random = new Random();
            words = TestData.Split(' ').ToList();
            maxword = words.Count - 1;

            DataContext = LogEntries = new ObservableCollection<ChatEntry>();
            Enumerable.Range(0, 100)
                      .ToList()
                      .ForEach(x => LogEntries.Add(GetRandomEntry()));
        }

        private ChatEntry GetRandomEntry()
        {
            return new ChatEntry()
                {
                    DateTime = DateTime.Now,
                    Sender = words[random.Next(0, maxword)],
                    Content = GetFlowDocumentString(string.Join(" ",Enumerable.Range(5, random.Next(10, 50)).Select(x => words[random.Next(0, maxword)])))
                };
        }

        private string GetFlowDocumentString(string text)
        {
            return "<FlowDocument xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'>" +
                   "   <Paragraph>" +
                   "     <Run Text='" + text + "'/>" +
                   "   </Paragraph>" +
                   "</FlowDocument>";
        }
    }

数据项:

public class ChatEntry:PropertyChangedBase
{
    public DateTime DateTime { get; set; }

    private string _content;
    public string Content
    {
        get { return _content; }
        set
        {
            _content = value;
            OnPropertyChanged("Content");
        }
    }

    public string Sender { get; set; }
}

PropertyChangedBase(MVVM 助手类):

public class PropertyChangedBase:INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged(string propertyName)
    {
        Application.Current.Dispatcher.BeginInvoke((Action) (() =>
                                                                 {
                                                                     PropertyChangedEventHandler handler = PropertyChanged;
                                                                     if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
                                                                 }));
    }
}

结果:

在此处输入图像描述

  • 我已经使用了FlowDocumentToXAMLConverter这篇文章中的
  • 第三列中的丰富内容显示在 中,但您可以将其更改为使用链接帖子中FlowDocumentViewer的可绑定内容。RichTextBox
  • 您可以通过单击并拖动标题边缘来调整列的大小。
  • WPF 具有内置的 UI 虚拟化,这意味着如果有很多行,您的应用程序将不会严重滞后。
  • 您可以实施此处描述的解决方案,以在调整包含窗口大小时调整最后一列的大小,从而实现自动换行和分辨率独立性。
  • 请注意,大多数代码隐藏实际上是支持示例的样板(生成随机条目等)。删除它,这将是一个非常干净的解决方案。
  • WPF 摇滚。只需将我的代码(连同链接帖子中的转换器)复制并粘贴到 a 中 File -> New Project -> WPF Application ,然后自己查看结果。

编辑:

根据@KingKing 的要求,我修改了我的示例以模拟聊天客户端。

FsRichTextBox.dll我从上面链接的 CodeProject 帖子中添加了一个引用。

<Window x:Class="MiscSamples.ThreeColumnChatSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:MiscSamples"
        xmlns:rtb="clr-namespace:FsWpfControls.FsRichTextBox;assembly=FsRichTextBox"
        Title="ThreeColumnChatSample" WindowState="Maximized">
    <Window.Resources>
        <local:FlowDocumentToXamlConverter x:Key="DocumentConverter"/>
    </Window.Resources>
    <Grid>

        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition Height="300"/>
        </Grid.RowDefinitions>

        <ListView ItemsSource="{Binding ChatEntries}" ScrollViewer.HorizontalScrollBarVisibility="Hidden"
                  x:Name="ListView">
            <ListView.View>
                <GridView>
                    <GridView.Columns>
                        <GridViewColumn DisplayMemberBinding="{Binding DateTime}"/>
                        <GridViewColumn DisplayMemberBinding="{Binding Sender}"/>
                        <GridViewColumn>
                            <GridViewColumn.CellTemplate>
                                <DataTemplate>
                                    <FlowDocumentScrollViewer Document="{Binding Content, Converter={StaticResource DocumentConverter}}"
                                                          VerticalScrollBarVisibility="Hidden" HorizontalScrollBarVisibility="Hidden"/>
                                </DataTemplate>
                            </GridViewColumn.CellTemplate>
                        </GridViewColumn>
                    </GridView.Columns>
                </GridView>
            </ListView.View>
        </ListView>

        <GridSplitter Height="3" Grid.Row="1" HorizontalAlignment="Stretch" VerticalAlignment="Top"/>

        <DockPanel Grid.Row="1">
            <Button Content="Send" DockPanel.Dock="Right" VerticalAlignment="Bottom" Margin="2"
                    Click="Send_Click"/>

            <rtb:FsRichTextBox Document="{Binding UserInput,Converter={StaticResource DocumentConverter}, Mode=TwoWay}"
                           DockPanel.Dock="Bottom" Height="300" x:Name="InputBox"/>
        </DockPanel>
    </Grid>
</Window>

代码背后:

public partial class ThreeColumnChatSample : Window
{
    public ChatViewModel ViewModel { get; set; }

    public ThreeColumnChatSample()
    {
        InitializeComponent();

        DataContext = ViewModel = new ChatViewModel();
    }

    private void Send_Click(object sender, RoutedEventArgs e)
    {
        InputBox.UpdateDocumentBindings();

        var entry = ViewModel.AddEntry();

        ListView.ScrollIntoView(entry);
    }
}

视图模型:

public class ChatViewModel:PropertyChangedBase
{
    public ObservableCollection<ChatEntry> ChatEntries { get; set; }
    private string _userInput;
    public string UserInput
    {
        get { return _userInput; }
        set
        {
            _userInput = value;
            OnPropertyChanged("UserInput");
        }
    }

    public string NickName { get; set; }

    public ChatViewModel()
    {
        ChatEntries = new ObservableCollection<ChatEntry>();
        NickName = "John Doe";
    }

    public ChatEntry AddEntry()
    {
        var entry = new ChatEntry {DateTime = DateTime.Now, Sender = NickName};
        entry.Content = UserInput;

        ChatEntries.Add(entry);

        UserInput = null;

        return entry;
    }
}

结果:

在此处输入图像描述

于 2013-06-30T18:06:50.207 回答
1

这是一个解决方案Winforms。我不是 Winforms 专家,但这个解决方案还可以。我敢打赌,Winforms 专家可以使它比人们想象的要好。我已经尝试解决这个问题,以便第三列只包含 1RichTextBox但有一些麻烦。HighCore's solution似乎不是那样工作的。RichTextBox此解决方案在第三列为每个条目提供一个特定的:

public class ChatWindow : SplitContainer
{
    private SplitContainer innerSpliter = new SplitContainer();
    public ChatWindow()
    {
        Type type = typeof(Panel);
        type.GetProperty("DoubleBuffered", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).SetValue(innerSpliter.Panel2, true, null);
        //Initialize some properties
        innerSpliter.Parent = Panel2;
        innerSpliter.Panel2.AutoScroll = true;
        innerSpliter.Dock = DockStyle.Fill;
        SplitterDistance = 50;
        innerSpliter.SplitterDistance = 10;
        BorderStyle = BorderStyle.FixedSingle;
        innerSpliter.BorderStyle = BorderStyle.FixedSingle;
        //-----------------------------            
        Panel1.BackColor = Color.White;
        innerSpliter.Panel1.BackColor = innerSpliter.Panel2.BackColor = Color.White;
    }
    bool adding;
    private Binding GetTopBinding(RichTextBox richText)
    {
        Binding bind = new Binding("Top", richText, "Location");
        bind.Format += (s, e) =>
        {
            Binding b = s as Binding;                           
            if (adding)
            {
                RichTextBox rtb = b.DataSource as RichTextBox;
                if (rtb.TextLength == 0) { e.Value = ((Point)e.Value).Y; return; }
                rtb.SuspendLayout();
                rtb.SelectionStart = 0;
                int i = rtb.SelectionFont.Height;
                int belowIndex = 0;
                while (belowIndex == 0&&i < rtb.Height-6)
                {
                    belowIndex = rtb.GetCharIndexFromPosition(new Point(1, i++));
                }                                        
                float baseLine1 = 0.75f * i; //This is approximate
                float baseLine2 = GetBaseLine(b.Control.Font, b.Control.CreateGraphics());//This is exact
                b.Control.Tag = (baseLine1 > baseLine2 ? baseLine1 - baseLine2 - 2: 0);
                e.Value = ((Point)e.Value).Y + (float)b.Control.Tag;
                rtb.ResumeLayout(false);
            }
            else e.Value = ((Point)e.Value).Y + (float)b.Control.Tag;
        };
        return bind;
    }
    private Binding GetHeightBinding(RichTextBox richText)
    {
        Binding bind = new Binding("Height", richText, "Size");
        bind.Format += (s, e) =>
        {
            Binding b = s as Binding;
            e.Value = ((Size)e.Value).Height - b.Control.Top + ((RichTextBox) b.DataSource).Top;
        };
        return bind;
    }
    private Binding GetWidthBinding(Panel panel)
    {
        Binding bind = new Binding("Width", panel, "Size");
        bind.Format += (s, e) =>
        {                
            e.Value = ((Size)e.Value).Width;
        };
        return bind;
    }
    public void AddItem(string first, string second, string third)
    {
        adding = true;            
        RichTextBox richText = new RichTextBox();
        innerSpliter.Panel2.SuspendLayout();
        Panel1.SuspendLayout();
        innerSpliter.Panel1.SuspendLayout();

        richText.Dock = DockStyle.Top;
        richText.Width = innerSpliter.Panel2.Width;            
        richText.ContentsResized += ContentsResized;                               
        richText.BorderStyle = BorderStyle.None;
        Label lbl = new Label() { Text = first, AutoSize = false, ForeColor = Color.BlueViolet};            
        lbl.DataBindings.Add(GetHeightBinding(richText));                      
        lbl.DataBindings.Add(GetTopBinding(richText));            
        lbl.DataBindings.Add(GetWidthBinding(Panel1));
        lbl.Parent = Panel1;            
        lbl = new Label() { Text = second,  AutoSize = false, ForeColor = Color.BlueViolet };            
        lbl.DataBindings.Add(GetHeightBinding(richText));            
        lbl.DataBindings.Add(GetTopBinding(richText));            
        lbl.DataBindings.Add(GetWidthBinding(innerSpliter.Panel1));
        lbl.Parent = innerSpliter.Panel1;            
        richText.Visible = false;
        richText.Parent = innerSpliter.Panel2;
        richText.Visible = true;
        richText.Rtf = third;            
        richText.BringToFront();             
        innerSpliter.Panel1.ResumeLayout(true);
        innerSpliter.Panel2.ResumeLayout(true);
        Panel1.ResumeLayout(true);
        innerSpliter.Panel2.ScrollControlIntoView(innerSpliter.Panel2.Controls[0]);
        adding = false;
    }
    private void ContentsResized(object sender, ContentsResizedEventArgs e)
    {
        ((RichTextBox)sender).Height = e.NewRectangle.Height + 6;
    }
    private float GetBaseLine(Font font, Graphics g)
    {
        int lineSpacing = font.FontFamily.GetLineSpacing(font.Style);
        int cellAscent = font.FontFamily.GetCellAscent(font.Style);
        return font.GetHeight(g) * cellAscent / lineSpacing;
    }
}
//I provide only 1 AddItem() method, in fact it's enough because normally we don't have requirement to remove a chat line once it's typed and sent.
chatWindow.AddItem(DateTime.Now.ToString(), "User name", "Rtf text");

我还尝试均衡所有 3 列中的基线(在第一行)。确切的基线可以通过GetBaseLine方法找到,但是 RichTextBox 第一行的基线只能通过遍历第一行中的所有字符以获取SelectionFont每个字符来找到,我已经尝试过这种方法,但性能是太糟糕了(几乎不能接受)。所以我尝试了一个近似计算,它使用一个固定常数0.75乘以Font Height,确切的比率是CellAscent/LineSpacing

我希望OP想要一个Winforms解决方案,而不是一个WPF解决方案。

这是控件的屏幕截图: 在此处输入图像描述

于 2013-07-02T02:41:42.243 回答
0

一种可能的解决方案是使用具有三列和详细信息视图的ListView控件 - 然后您将获得与显示的 WPF 解决方案完全相同的结果,但使用 Windows 窗体。

另一种解决方案是使用DataGridView并创建一个包含三列的表并为每个新事件添加一行,就像使用 ListView 控件一样。

在这两种情况下,在第三列(您的消息内容所在的位置)中,都使用丰富的 UI 控件来获得漂亮的文本格式,例如RichTextBox

于 2013-06-30T16:53:21.680 回答