[WPF]根据内容自动设置大小的RichTextBox
[WPF]根据文本内容自动设置大小的RichTextBox
周银辉
很怀念windows forms当中的AutoSize属性啊,但可惜的是WPF并没有实现这个属性, 这多少让人有些郁闷。
那就自个写吧,相对比较容易的是TextBox之类的仅仅显示平文本的控件,你可以根据你的文本,字体等等属性构造一个FormattedText
实例,这个实例有Width/Height属性(我还是很怀念Font.MeasureString方法),最让人纠结的是RichTextBox控件,哎,又是它。
思路很简单,监视文本变化,文本变化时调整控件大小:
protected override void OnTextChanged(TextChangedEventArgs e)
{
base.OnTextChanged(e);
AdjustSizeByConent();
}
public void AdjustSizeByConent()
{
//myHeight = ... 取得正确的高度
Height = myHeight;
//myWidth = ... 取得正确的宽度
Width = myWidth;
}
如何获取正确的高度呢,有一个非常捡便宜的方法,分别对Document.ContentStart和Document.ContentEnd调用TextPointer.GetCharacterRect()方法,我们可以获得文档开始处和结束处的内容边框,如下图所示:
注意到两个红色边框了吗,用第二个边框的bottom减去第一个边框的top,就可以得到内容的高度,所以:
Rect rectEnd = Document.ContentEnd.GetCharacterRect(LogicalDirection.Forward);
var height = rectEnd.Bottom - rectStart.Top;
var remainH = rectEnd.Height/2.0;
Height = Math.Min(MaxHeight, Math.Max(MinHeight, height + remainH));
(代码中的remainH 是预留的一点点空白)[updated: 完整代码中抛弃了这种做法,而使用了将height设置为NaN]
那么求宽度时,是不是“同理可证”了(呵呵,如果是在上高中,我可真要这么写了,但程序是严谨的,忽悠不过去的~)
不行!
因为,上面代码中的rectStart和rectEnd宽度始终返回的是0(而高度却返回的是正确的值),不知道为啥。
这导致获取宽度是非常麻烦,下面是一种解决方案,将控件中的文本抽取出来,构造成一个比较复杂的FormattedText,然后由它来求宽度:
代码
// ReSharper disable ConvertToConstant.Local
var remainW = 20;
// ReSharper restore ConvertToConstant.Local
Width = Math.Min(MaxWidth, Math.Max(MinWidth, formattedText.WidthIncludingTrailingWhitespace + remainW));
OK,有人会问了,既然可以通过FormattedText获取宽度,那为啥不能通过它同理可证求高度呢?
不可以的,不信你在RichTextBox中敲几次回车试试,一个回车导致一个段落, richTextBox段落之间是有距离的,默认很大(大得有点不协调),FormattedText是不会计算段落间隔的,所以FormattedText的高度比实际高度要小,够纠结吧。
好了,完整的代码在这里(注意哦,我这里只处理的文本,那我向其中插入图片呢...恩,不work)
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media;
namespace WpfApplication2
{
internal class AutoSizeRichTextBox : RichTextBox
{
public AutoSizeRichTextBox()
{
Height = Double.NaN;//set to nan to enable auto-height
Loaded += ((sender, args) => AdjustSizeByConent());
}
protected override void OnTextChanged(TextChangedEventArgs e)
{
base.OnTextChanged(e);
AdjustSizeByConent();
}
public void AdjustSizeByConent()
{
var formattedText = GetFormattedText(Document);
// ReSharper disable ConvertToConstant.Local
var remainW = 20;
// ReSharper restore ConvertToConstant.Local
Width = Math.Min(MaxWidth, Math.Max(MinWidth, formattedText.WidthIncludingTrailingWhitespace + remainW));
}
private static FormattedText GetFormattedText(FlowDocument doc)
{
var output = new FormattedText(
GetText(doc),
CultureInfo.CurrentCulture,
doc.FlowDirection,
new Typeface(doc.FontFamily, doc.FontStyle, doc.FontWeight, doc.FontStretch),
doc.FontSize,
doc.Foreground);
int offset = 0;
foreach (TextElement textElement in GetRunsAndParagraphs(doc))
{
var run = textElement as Run;
if (run != null)
{
int count = run.Text.Length;
output.SetFontFamily(run.FontFamily, offset, count);
output.SetFontSize(run.FontSize, offset, count);
output.SetFontStretch(run.FontStretch, offset, count);
output.SetFontStyle(run.FontStyle, offset, count);
output.SetFontWeight(run.FontWeight, offset, count);
output.SetForegroundBrush(run.Foreground, offset, count);
output.SetTextDecorations(run.TextDecorations, offset, count);
offset += count;
}
else
{
offset += Environment.NewLine.Length;
}
}
return output;
}
private static IEnumerable<TextElement> GetRunsAndParagraphs(FlowDocument doc)
{
for (TextPointer position = doc.ContentStart;
position != null && position.CompareTo(doc.ContentEnd) <= 0;
position = position.GetNextContextPosition(LogicalDirection.Forward))
{
if (position.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.ElementEnd)
{
var run = position.Parent as Run;
if (run != null)
{
yield return run;
}
else
{
var para = position.Parent as Paragraph;
if (para != null)
{
yield return para;
}
else
{
var lineBreak = position.Parent as LineBreak;
if (lineBreak != null)
{
yield return lineBreak;
}
}
}
}
}
}
private static string GetText(FlowDocument doc)
{
var sb = new StringBuilder();
foreach (TextElement text in GetRunsAndParagraphs(doc))
{
var run = text as Run;
sb.Append(run == null ? Environment.NewLine : run.Text);
}
return sb.ToString();
}
}
}
[Update 2010-07-14]
后来发现,如果文本框被旋转了的话(RenderTransform, RotateTransform.Angle=xxx),当文本框高度改变的时候,文本框在视觉上会有位移(当然Canvas.GetLeft, Canvas.GetTop等值是保持不变的),为了纠正该位移,你可以对文本框(或其他)尝试如下函数:
{
element.UpdateLayout();
double angle = 0.0;
var transformOrigin = element.RenderTransformOrigin;
var rotateTransform = element.GetRenderTransform<RotateTransform>();
if (rotateTransform != null)
{
angle = rotateTransform.Angle * Math.PI / 180;
}
var delta = new Point(element.ActualWidth - oldSize.Width, element.ActualHeight - oldSize.Height);
var x = Canvas.GetLeft(element);
var y = Canvas.GetTop(element);
var dx = delta.Y * transformOrigin.Y * Math.Sin(-angle);
var dy = delta.Y * transformOrigin.Y * (1 - Math.Cos(-angle));
x += dx;
y -= dy;
Canvas.SetLeft(element, x);
Canvas.SetTop(element, y);
}
public static T GetRenderTransform<T>(this UIElement element) where T : Transform
{
if (element.RenderTransform.Value.IsIdentity)
{
element.RenderTransform = CreateSimpleTransformGroup();
}
if (element.RenderTransform is T)
{
return (T)element.RenderTransform;
}
if (element.RenderTransform is TransformGroup)
{
var group = (TransformGroup)element.RenderTransform;
foreach (var t in group.Children)
{
if (t is T)
{
return (T)t;
}
}
}
throw new NotSupportedException("Can not get instance of " + typeof(T).Name + " from " + element + "'s RenderTransform : " + element.RenderTransform);
}