For a chat client I'm writing I want to create the following control:
对于我正在写的聊天客户端,我想创建以下控件:
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).
它应该由三个用户可调整大小的列组成,其中可以显示任意文本,但仍然彼此对齐(正如您可以看到Jeff所说的那样)。
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).
我已经有了一个自定义的RichTextBox,它可以显示预先格式化的文本并自动滚动到底部,但是如何创建一个带有可调整大小的列的文本框让我很困惑(我对创建自己的控件很新)。
Any pointers as to what too look for or general ideas? Any help appreciated!
关于什么也寻找或一般想法的任何指针?任何帮助赞赏!
3 个解决方案
#1
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.
好。忘记winforms。它是无用的,不赞成的,丑陋的,它不允许自定义,并且由于缺乏UI虚拟化和硬件渲染而变得很慢。
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):
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));
}));
}
}
Result:
- 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 bindableRichTextBox
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.
我在这篇文章中使用过FlowDocumentToXAMLConverter
第三列中的丰富内容显示在FlowDocumentViewer中,但您可以更改它以使用链接的帖子中的可绑定RichTextBox。
您可以通过单击并拖动标题边缘来调整列的大小。
WPF具有内置的UI虚拟化,这意味着如果有很多行,您的应用程序将不会出现可怕的延迟。
您可以实现此处描述的解决方案,以在调整包含窗口大小时调整最后一列的大小,从而实现自动换行和解决方案的独立性。
请注意,大多数Code-Behind实际上是支持该示例的样板(生成随机条目等)。删除它,它将是一个非常干净的解决方案。
WPF Rocks。只需将我的代码(与链接文章中的转换器一起)复制并粘贴到文件 - >新项目 - > WPF应用程序中,然后自己查看结果。
Edit:
as per @KingKing's request, I modified my sample to emulate a chat client.
根据@KingKing的请求,我修改了我的示例以模拟聊天客户端。
I added a reference to FsRichTextBox.dll
from the above linked CodeProject post.
我从上面链接的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>
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:
#2
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:
这是Winforms的解决方案。我不是Winforms专家,但这个解决方案没问题。我打赌Winforms专家可以让它比别人想象的更好。我试过解决这个问题,以便第三列只包含1个RichTextBox但是有一些麻烦。 HighCore的解决方案似乎不那么有用。此解决方案在第三列为每个条目提供一个特定的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");
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
.
我还尝试在所有3列中均衡基线(在第一行)。 GetBaseLine方法可以找到确切的基线,但RichTextBox第一行的基线可能只能通过遍历第一行中的所有字符来找到每个字符的SelectionFont,我尝试过这种方法但是表现如此糟糕(几乎不可接受)。所以我尝试了一个近似计算,它使用固定常数0.75与Font Height相乘,确切的速率是CellAscent / LineSpacing。
I hope the OP wants a Winforms
solution, not a WPF
solution.
我希望OP想要一个Winforms解决方案,而不是WPF解决方案。
Here is the screen shot of the control:
这是控件的屏幕截图:
#3
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.
一种可能的解决方案是使用具有三列和详细信息视图的ListView控件 - 然后您将获得与显示的WPF解决方案完全相同的结果,但使用Windows窗体。
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.
另一个解决方案是使用DataGridView并创建一个包含三列的表,并为每个新事件添加一行,就像使用ListView控件一样。
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.
在第三列(消息内容所在的位置)的两种情况下,都使用丰富的UI控件以获得良好的文本格式,例如RichTextBox的。
#1
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.
好。忘记winforms。它是无用的,不赞成的,丑陋的,它不允许自定义,并且由于缺乏UI虚拟化和硬件渲染而变得很慢。
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):
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));
}));
}
}
Result:
- 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 bindableRichTextBox
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.
我在这篇文章中使用过FlowDocumentToXAMLConverter
第三列中的丰富内容显示在FlowDocumentViewer中,但您可以更改它以使用链接的帖子中的可绑定RichTextBox。
您可以通过单击并拖动标题边缘来调整列的大小。
WPF具有内置的UI虚拟化,这意味着如果有很多行,您的应用程序将不会出现可怕的延迟。
您可以实现此处描述的解决方案,以在调整包含窗口大小时调整最后一列的大小,从而实现自动换行和解决方案的独立性。
请注意,大多数Code-Behind实际上是支持该示例的样板(生成随机条目等)。删除它,它将是一个非常干净的解决方案。
WPF Rocks。只需将我的代码(与链接文章中的转换器一起)复制并粘贴到文件 - >新项目 - > WPF应用程序中,然后自己查看结果。
Edit:
as per @KingKing's request, I modified my sample to emulate a chat client.
根据@KingKing的请求,我修改了我的示例以模拟聊天客户端。
I added a reference to FsRichTextBox.dll
from the above linked CodeProject post.
我从上面链接的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>
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:
#2
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:
这是Winforms的解决方案。我不是Winforms专家,但这个解决方案没问题。我打赌Winforms专家可以让它比别人想象的更好。我试过解决这个问题,以便第三列只包含1个RichTextBox但是有一些麻烦。 HighCore的解决方案似乎不那么有用。此解决方案在第三列为每个条目提供一个特定的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");
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
.
我还尝试在所有3列中均衡基线(在第一行)。 GetBaseLine方法可以找到确切的基线,但RichTextBox第一行的基线可能只能通过遍历第一行中的所有字符来找到每个字符的SelectionFont,我尝试过这种方法但是表现如此糟糕(几乎不可接受)。所以我尝试了一个近似计算,它使用固定常数0.75与Font Height相乘,确切的速率是CellAscent / LineSpacing。
I hope the OP wants a Winforms
solution, not a WPF
solution.
我希望OP想要一个Winforms解决方案,而不是WPF解决方案。
Here is the screen shot of the control:
这是控件的屏幕截图:
#3
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.
一种可能的解决方案是使用具有三列和详细信息视图的ListView控件 - 然后您将获得与显示的WPF解决方案完全相同的结果,但使用Windows窗体。
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.
另一个解决方案是使用DataGridView并创建一个包含三列的表,并为每个新事件添加一行,就像使用ListView控件一样。
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.
在第三列(消息内容所在的位置)的两种情况下,都使用丰富的UI控件以获得良好的文本格式,例如RichTextBox的。