对于我正在写的聊天客户端,我想创建以下控件:
它应该由三个用户可调整大小的列组成,其中可以显示任意文本,但仍然彼此对齐(正如您所看到的那样,杰夫所说)。
我已经有了一个自定义RichTextBox
,它可以显示预先格式化的文本并自动滚动到底部,但是如何创建一个带有可调整大小的列的文本框让我很困惑(我对创建自己的控件很新) )。
关于什么也寻找或一般想法的任何指针?任何帮助表示赞赏!
答案 0 :(得分:3)
确定。忘记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));
}));
}
}
结果:
FlowDocumentToXAMLConverter
FlowDocumentViewer
中,但您可以将其更改为使用链接帖子中的可绑定RichTextBox
。File -> New Project -> 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;
}
}
结果:
答案 1 :(得分:1)
以下是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
解决方案。
以下是该控件的屏幕截图:
答案 2 :(得分:0)
一种可能的解决方案是使用具有三列和详细信息视图的ListView控件 - 然后您将获得与显示的WPF解决方案完全相同的结果,但使用Windows窗体。
另一种解决方案是使用DataGridView并创建一个包含三列的表,并为每个新事件添加一行,就像使用ListView控件一样。
在第三列(消息内容所在的位置)的两种情况下,都使用丰富的UI控件以获得良好的文本格式,例如RichTextBox。