【问题标题】:Best approach to write this control?编写此控件的最佳方法?
【发布时间】:2013-06-27 19:31:42
【问题描述】:

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

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

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

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

【问题讨论】:

  • 另一个聊天示例:Hans: Hi! - Jeff: Hey guys did you hear that there's a newer UI technology that replaces winforms and could help you build things like this really easily?
  • stackoverflow 非常有名,我敢打赌所有程序员都应该知道或至少听说过它。
  • @HighCore 我不能用 WPF 做的一件事是在控件中生成窗口,我当然也想做。
  • @cobra_fast 你可以用 AvalonDock 做到这一点。顺便说一句,“控制范围内的窗口”的概念没有意义。
  • @cobra_fast 您对 WPF 解决方案感兴趣吗?

标签: c# .net wpf winforms controls


【解决方案1】:

好的。忘记winforms。由于缺乏 UI 虚拟化和硬件渲染,它无用、已弃用、丑陋、不允许自定义并且是 Slow as Hell。

这是我对你所描述的看法:

<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));
                                                                 }));
    }
}

结果:

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

编辑:

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

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

<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;
    }
}

结果:

【讨论】:

  • 我对@9​​87654339@不熟悉,所以不太了解你的代码,这个解决方案能否支持消息栏(第3栏)中的Rich text?
  • @kingking 已经做到了。第三列的内容是FlowDocument。
  • 您应该添加代码来演示如何从Rtf 设置消息,这对 OP 会更好。顺便说一句,我想说,这完全可以用Winforms 完成,最困难的情况是让3rd column 只包含1 个RichTextBox(这意味着所有行都使用相同的RichTextBox )。如果每一行都有一个RichTextBox,问题就很容易解决了。
  • @kingking 我看到你喜欢捍卫 winforms,因此如果你有 winforms 解决方案,请继续发布。 IMO,winforms 是一文不值的废话。
  • 不,如果你不是Good winforms,请不要像你一样思考。 winforms 专家可以做许多您无法想象的有趣事情。我正在尝试解决这个问题,以便第三列只包含 1 个RichTextBox,但是如果 FontSize 是固定的,它会更容易。
【解决方案2】:

这是Winforms 中的解决方案。我不是 Winforms 专家,但这个解决方案还可以。我敢打赌,Winforms 专家可以使它比人们想象的要好。我已经尝试解决这个问题,以便第三列仅包含 1 RichTextBox 但有一些麻烦。 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 解决方案。

这是控件的屏幕截图:

【讨论】:

  • 我复制并粘贴了您的代码,它在运行时在richText.Rtf = third; 行中出现InvalidArgumentException。很抱歉,我不会通过 horrible 的代码量。顺便说一句,您正在谈论的所谓的“基线”(无论是什么)问题只是一个用 1 行 XAML 修复的外观问题。你不方便把它变成WPF vs winforms的比赛,因为你肯定会输。
  • 顺便说一句,我发布了 WPF 答案,因为 OP 告诉我他会对它感兴趣。还有,大部分winforms的问题都可以通过提供WPF解决方案轻松解决,只是因为winforms不支持anything,完全没用。
  • @HighCore third 是传入的Rtf,如果你的Rtf 格式不正确,会抛出异常。我测试好了。你觉得这很可怕,但那是Winforms STUFF,你不擅长 Winforms,所以请走开。
  • 我照原样复制并粘贴了您的示例。如果它坏了,我不会修复它。我期待一个可行的解决方案。
  • 老兄,I don't know and I don't care。这是your 代码。 you 应该修复它。我复制并粘贴了your 代码,但它不起作用。我逐字复制了您的行chatWindow.AddItem(DateTime.Now.ToString(), "User name", "Rtf text");。它不起作用。我不会开始挖掘(蹩脚的恐龙)winforms 期望什么。
【解决方案3】:

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

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

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

【讨论】:

  • 您的解决方案无法扩展,实际上是一个 hack。它不会保持滚动同步,所以如果我滚动 listbox3,那么我就会失去与其他列表框的关系。
  • 我的例子给出了一个想法,它不是完整的解决方案!如果要使用 ListBox,滚动条内容可以与 VerticalScroll 属性同步。在 RichTextBox 控件中附加文本可以很好地自下而上。
  • @pasty OP 希望他的消息(在第三列中)具有丰富的格式(粗体、斜体、颜色、任意字体......)。您的解决方案无法满足该要求。换句话说,OP 希望第三列包含某种控件,例如 RichTextBox,而不仅仅是 ListBox
  • @King King 请再读一遍这个问题:但是我将如何创建一个可调整大小列的文本框让我感到困惑(我对创建自己的控件还很陌生)据我所知,Windows 表单是必需的/想要的,而不是 WPF。所以我展示了如何制作一个 3 列的动态控件并且没有实现消息添加 - Cobra_Fast 已经知道如何做到这一点。
  • @pasty 这个...我已经有一个自定义的 RichTextBox 可以显示预格式化的文本并自动滚动到底部...????如果他想要的只是纯文本,我有另一个更好的解决方案。您认为聊天应用程序适合纯文本吗?聊天应用程序甚至需要表情符号等富文本。
猜你喜欢
  • 2014-04-15
  • 1970-01-01
  • 1970-01-01
  • 2017-12-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-08-16
  • 2020-07-09
相关资源
最近更新 更多