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

 

 

 

 

 

posted @ 2010-06-22 14:13  周银辉  阅读(11132)  评论(9编辑  收藏  举报