[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 rectStart
=
Document.ContentStart.GetCharacterRect(LogicalDirection.Forward);
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,然后由它来求宽度:
代码
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));
OK,有人会问了,既然可以通过FormattedText获取宽度,那为啥不能通过它同理可证求高度呢?
不可以的,不信你在RichTextBox中敲几次回车试试,一个回车导致一个段落, richTextBox段落之间是有距离的,默认很大(大得有点不协调),FormattedText是不会计算段落间隔的,所以FormattedText的高度比实际高度要小,够纠结吧。
好了,完整的代码在这里(注意哦,我这里只处理的文本,那我向其中插入图片呢...恩,不work)
AutoSizeRichTextBox
using
System;
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等值是保持不变的),为了纠正该位移,你可以对文本框(或其他)尝试如下函数:
private
static
void
AdjustOffsetAfterSizeAdjustedByContent(FrameworkElement element, Size oldSize)
{
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);
}