[WPF]根据内容自动设置大小的RichTextBox

时间:2021-02-12 00:59:03

                                            [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()方法,我们可以获得文档开始处和结束处的内容边框,如下图所示:
[WPF]根据内容自动设置大小的RichTextBox 

 注意到两个红色边框了吗,用第二个边框的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,然后由它来求宽度:
 [WPF]根据内容自动设置大小的RichTextBox代码

 

            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)

[WPF]根据内容自动设置大小的RichTextBox[WPF]根据内容自动设置大小的RichTextBoxAutoSizeRichTextBox
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);
        }