4

For a chat client I'm writing I want to create the following control:

enter image description here

It should consist of three user-resizable columns where arbitrary text can be displayed, but still aligned to each other (as you can see with what Jeff says).

I already have a custom RichTextBox which can display preformatted text and automatically scroll to the bottom, but how I would go about creating a textbox with resizable columns puzzles me (I'm fairly new to creating my own controls).

Any pointers as to what too look for or general ideas? Any help appreciated!

Cobra_Fast
  • 15,671
  • 8
  • 57
  • 102

3 Answers3

3

Ok. Forget winforms. It's useless, deprecated, ugly, it doesn't allow customization and is Slow as Hell due to lack of UI virtualization and hardware rendering.

This is my take on what you described:

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

Code behind:

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

Data Item:

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 Helper Class):

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

Result:

enter image description here

  • I have used the FlowDocumentToXAMLConverter from this post
  • The rich content in the third column is shown in a FlowDocumentViewer, but you can change that to use the bindable RichTextBox from the linked post.
  • You can resize the columns by clicking and dragging the Header edges.
  • WPF has built-in UI Virtualization, which means your application will not lag horribly if there are LOTS of rows.
  • You can implement the solution described here to resize the last column when resizing the containing window, thus achieving word-wrapping and resolution independence.
  • Notice that most of the Code-Behind is actually boilerplate to support the example (generate random entries etc). Remove that and it's going to be a really clean solution.
  • WPF Rocks. Just copy and paste my code (together with the Converter from the linked post) in a File -> New Project -> WPF Application and see the results for yourself.

Edit:

as per @KingKing's request, I modified my sample to emulate a chat client.

I added a reference to FsRichTextBox.dll from the above linked CodeProject post.

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

Code Behind:

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

ViewModel:

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

Result:

enter image description here

Community
  • 1
  • 1
Federico Berasategui
  • 43,562
  • 11
  • 100
  • 154
  • I'm not familiar with `WPF` so I don't understand your code much, could this solution support `Rich text` in the message column (the 3rd column)? – King King Jun 30 '13 at 18:21
  • @kingking it already does. The content of the third column is a [FlowDocument](http://msdn.microsoft.com/en-us/library/aa970909.aspx). – Federico Berasategui Jun 30 '13 at 18:23
  • you should add code to demonstrate how to set message from a `Rtf`, that would be better for the OP. BTW, I would like to say that, this is totally able to be done with `Winforms`, the most difficult situation is to make the `3rd column` contain only 1 `RichTextBox` (that means all the rows use the same `RichTextBox`). If each row has a `RichTextBox`, the problem can be solved easily. – King King Jun 30 '13 at 18:29
  • @kingking I see you like to defend winforms, therefore if you have a winforms solution go ahead and post it. IMO, winforms is a worthless piece of crap. – Federico Berasategui Jun 30 '13 at 18:31
  • No, if you are not `Good` at `winforms`, please don't think like as you did. A `winforms` expert can do many things interesting that you can't imagine. I'm trying to solve this so that the 3rd column contains only 1 `RichTextBox`, however if the FontSize is fixed, it will be easier. – King King Jun 30 '13 at 18:33
  • @KingKing "a winforms expert"?? Please.. dude. Go ahead and show me any "winforms expert" that has ever achieved something like [this](http://www.istartedsomething.com/20091124/razorfone-conceptual-windows7-wpf-multi-touch-retail/) in winforms. – Federico Berasategui Jun 30 '13 at 18:39
  • I don't mean `Winforms` is better than `WPF`, it's even much worse than `WPF`, but You are obviously a `Winforms` NEWBIE, I'm sorry but I have to say that. So when you don't have good knowledge of Winforms, please don't say it can't, it's useless, of course it's useless in some cases, but not in this case. If you use some `Winforms third party UI library` you will see how well they deal with winforms. – King King Jun 30 '13 at 18:42
  • @kingKing I'm still waiting for the winforms Razorfone. And I'm not a "newbie" in winforms, the same I'm not a "newbie" in VB6. It's deprecated, I don't give a sh*t about it. Learning a dead technology makes so sense. – Federico Berasategui Jun 30 '13 at 18:44
  • @KingKing here, I edited my answer. go ahead and show me your 20000 lines-of-code winforms that does the same than my 50 line WPF sample. – Federico Berasategui Jun 30 '13 at 19:18
  • If you can, make the baselines of all the 3 columns text equal. they look ugly, don't you see it? – King King Jun 30 '13 at 19:39
  • Congratulations! now your answer is good to the OP's question because the question has been no longer tagged as `winforms`, instead it's been tagged as `wpf`. – King King Jun 30 '13 at 19:47
1

Here is a solution in Winforms. I'm not a Winforms expert but this solution is OK. I bet a Winforms expert can make it better than someone can imagine. I've tried solving this so that the third column contains only 1 RichTextBox but there is some trouble. The HighCore's solution doesn't seem to work that way. This solution provides each entry with one particular RichTextBox at the third column:

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

I also tried equalizing the baselines (at the first line) in all 3 columns. The exact baseline can be found by GetBaseLine method, however the baseline of the first line of a RichTextBox may only be found by looping through all the characters in the first line to get the SelectionFont at each character, I've tried this approach but the performance was so bad (nearly unacceptable). So I've tried an approximate calculation which uses a fixed constant 0.75 to multiply with the Font Height, the exact rate is CellAscent/LineSpacing.

I hope the OP wants a Winforms solution, not a WPF solution.

Here is the screen shot of the control: enter image description here

King King
  • 61,710
  • 16
  • 105
  • 130
  • I copied and pasted your code and it blows at runtime with an `InvalidArgumentException` in the line `richText.Rtf = third;`. I'm sorry I'm NOT going thru this `horrible` amount of code. BTW the supposed "baseline" (whatever that is) issue that you're talking about is only a cosmetic issue that is fixed with 1 line of XAML. It's not convenient for you to turn this into a competion of WPF vs winforms, because you will definitely lose. – Federico Berasategui Jul 02 '13 at 03:40
  • BTW I posted a WPF answer because the OP told me he would be interested in it. Also, most of the winforms problems can easily be solved by providing WPF solutions, simply because winforms doesn't support `anything` and is completely useless. – Federico Berasategui Jul 02 '13 at 03:42
  • @HighCore `third` is the passed in `Rtf`, if your `Rtf` is not well-formatted, the exception will be thrown. I've tested OK. You see it horrible, but that's `Winforms STUFF`, you are not good at Winforms so please go away guy. – King King Jul 02 '13 at 04:14
  • I copied and pasted your example EXACTLY as it is. I'm not going to fix it if it's broken. I expect a working solution. – Federico Berasategui Jul 02 '13 at 13:20
  • @HighCore you passed a wrong text to the method `AddItem`, the `third` requires a `rich text format` not a normal string. – King King Jul 02 '13 at 21:21
  • 1
    again dude, `I don't know and I don't care`. It's `your` code. `you` are supposed to fix it. I copied and pasted `your` code and it doesn't work. I copied your line `chatWindow.AddItem(DateTime.Now.ToString(), "User name", "Rtf text");` verbatim. It doesn't work. I'm not going to start digging around what does the (crappy dinosaur) winforms expect. – Federico Berasategui Jul 02 '13 at 21:33
0

One possible solution would be to use a ListView control with three columns and details view - then you will get exactly the same result as the showed WPF solution but with windows forms.

Another solution would be to use DataGridView and create a table with three columns and add a row for every new event, just like with the ListView control.

In both cases in the third column (where your message content resides) use a rich UI control in order to have nice text formatting e.g. RichTextBox.

keenthinker
  • 7,645
  • 2
  • 35
  • 45
  • 1
    Your solution doesn't scale and is really a hack. It does not maintain the scrolling synchronized, so if I scroll listbox3 then I lose the relationship to the other listboxes. – Federico Berasategui Jun 30 '13 at 17:05
  • My example gives an idea to work on, it is not the complete solution! The scroller contents could be synchronized with the [VerticalScroll](http://msdn.microsoft.com/de-de/library/system.windows.forms.scrollablecontrol.verticalscroll.aspx) property if you want to use ListBox. Appending text in a RichTextBox control would work just fine bottom up. – keenthinker Jun 30 '13 at 17:25
  • @pasty the OP wants his message (in the third column) to have rich format (bold, italic, color, arbitrary font, ...). Your solution can't meet that requirement. In other words, the OP wants the third column to contain some kind of control like a `RichTextBox`, not simply a `ListBox` – King King Jun 30 '13 at 18:19
  • @King King please read the question again: **but how I would go about creating a textbox with resizable columns puzzles me (I'm fairly new to creating my own controls)** and as far as i could see Windows forms is required/wanted and not WPF. So I showed how to make a 3 columns dynamic control and didn't implemented the message adding - Cobra_Fast already knows how to do this. – keenthinker Jun 30 '13 at 21:07
  • @pasty what about this **...I already have a custom RichTextBox which can display preformatted text and automatically scroll to the bottom...**???? If what he wants is just plain text, I have another solution which is better. You think a chat application is good with plain text? A chat application even needs more than richtext such as emoticon. – King King Jun 30 '13 at 22:36
  • @HighCore refer my solution in `Winforms`, it's just a temporary solution in which the third column contains many `RichTextBoxes`, I'll try making the third column contain only 1 `RichTextBox` for all the entries. That way user can select text easily. You may want to make out such a solution in `WPF`? Another problem is the baselines, you may want to make all the baselines of the first line in each entry in all 3 columns equal (Like as I did)? – King King Jul 02 '13 at 02:52