Click here to Skip to main content
15,897,371 members
Articles / Desktop Programming / WPF

CodeBox 2: An Extended and Improved Version of the CodeBox with Line Numbers

Rate me:
Please Sign up or sign in to vote.
4.83/5 (20 votes)
10 Oct 2009CPOL9 min read 141.2K   3.4K   65  
A WPF textbox supporting line numbering, highlighting, underlines, and strikethroughs.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.ComponentModel;
using System.Globalization;
using CodeBoxControl.Decorations;
using System.Diagnostics;
using System.Reflection;

namespace CodeBoxControl
{
    /// <summary>
    ///  A control to view or edit styled text<kssksk> 
    /// </summary>
 public partial class CodeBox:TextBox
     {
     /// <summary>
     /// Timer used to redo failed renders
     /// </summary>
     private System.Windows.Threading.DispatcherTimer renderTimer;

     /// <summary>
     /// Used to cached the render in case of invalid textbox properties.
     /// </summary>
     private CodeBoxRenderInfo renderinfo = new CodeBoxRenderInfo();


     /// <summary>
     /// Has the scroll event on the scrollviewer been enabled.
     /// </summary>
     bool mScrollingEventEnabled ;
 
     public CodeBox()
     {
          
         this.TextChanged += new TextChangedEventHandler(txtTest_TextChanged);
         this.Background = new SolidColorBrush(Colors.Transparent);
         this.Foreground = new SolidColorBrush(Colors.Transparent);
         this.VerticalScrollBarVisibility = ScrollBarVisibility.Auto;
         this.TextWrapping = TextWrapping.Wrap ;
         renderTimer = new System.Windows.Threading.DispatcherTimer();
         renderTimer.IsEnabled = false;
         renderTimer.Tick += new EventHandler(renderTimer_Tick);
         renderTimer.Interval = TimeSpan.FromMilliseconds(50);
         InitializeComponent();
         this.AcceptsReturn = true;
     }

     void renderTimer_Tick(object sender, EventArgs e)
     {
         renderTimer.IsEnabled = false;
          this.InvalidateVisual();
        
     }
   

     public static DependencyProperty BaseForegroundProperty = DependencyProperty.Register("BaseForeground", typeof(Brush), typeof(CodeBox),
new FrameworkPropertyMetadata( new SolidColorBrush(Colors.Black), FrameworkPropertyMetadataOptions.AffectsRender));

     [Bindable(true)]
     public Brush BaseForeground
     {
         get { return (Brush)GetValue(BaseForegroundProperty); }
         set { SetValue(BaseForegroundProperty, value); }
     }

     public static DependencyProperty CodeBoxBackgroundProperty = DependencyProperty.Register("CodeBoxBackground", typeof(Brush), typeof(CodeBox),
new FrameworkPropertyMetadata(new SolidColorBrush(Colors.White), FrameworkPropertyMetadataOptions.AffectsRender));

     [Bindable(true)]
     public Brush CodeBoxBackground
     {
         get { return (Brush)GetValue(CodeBoxBackgroundProperty); }
         set { SetValue(CodeBoxBackgroundProperty, value); }
     }




     #region LineNumber Properties

     public static DependencyProperty LineNumberForegroundProperty = DependencyProperty.Register("LineNumberForeground", typeof(Brush), typeof(CodeBox),
new FrameworkPropertyMetadata(new SolidColorBrush(Colors.Gray ), FrameworkPropertyMetadataOptions.AffectsRender));
     [Category("LineNumbers")]
     public Brush LineNumberForeground
     {
         get { return (Brush)GetValue(LineNumberForegroundProperty); }
         set { SetValue(LineNumberForegroundProperty, value); }
     }


     public static DependencyProperty LineNumberMarginWidthProperty = DependencyProperty.Register("LineNumberMarginWidth", typeof(double), typeof(CodeBox),
new FrameworkPropertyMetadata(15.0, FrameworkPropertyMetadataOptions.AffectsRender));
     [Category("LineNumbers")]
     public double  LineNumberMarginWidth
     {
         get { return (Double)GetValue(LineNumberMarginWidthProperty); }
         set { SetValue(LineNumberMarginWidthProperty, value); }
     }


     public static DependencyProperty StartingLineNumberProperty = DependencyProperty.Register("StartingLineNumber", typeof(int), typeof(CodeBox),
new FrameworkPropertyMetadata(1, FrameworkPropertyMetadataOptions.AffectsRender));
     [Category("LineNumbers")]
     public int StartingLineNumber
     {
         get { return (int)GetValue(StartingLineNumberProperty); }
         set { SetValue(StartingLineNumberProperty, value); }
     }


 
     #endregion

     void txtTest_TextChanged(object sender, TextChangedEventArgs e)
     {
          this.InvalidateVisual();
         
     }


     private List<Decoration> mDecorations = new List<Decoration>();
     /// <summary>
     /// List of the Decorative attributes assigned to the text
     /// </summary>
     public List<Decoration> Decorations
     {
         get { return mDecorations; }
         set { mDecorations = value; }
     }

     public static DependencyProperty DecorationSchemeProperty = DependencyProperty.Register("DecorationScheme", typeof(DecorationScheme), typeof(CodeBox),
new FrameworkPropertyMetadata(new DecorationScheme(), FrameworkPropertyMetadataOptions.AffectsRender));

     /// <summary>
     /// The DecorationScheme used for the CodeBox
     /// </summary>
     public DecorationScheme DecorationScheme
     {
         get { return (DecorationScheme)GetValue(DecorationSchemeProperty); }
         set { SetValue(DecorationSchemeProperty, value); }

     }

     FormattedText formattedText;
       int previousFirstChar = -1;
       #region OnRender

     /// <summary>
     /// Overrides render and divides into the designer and nondesigner cases.
     /// </summary>
     /// <param name="drawingContext"></param>
       protected override void OnRender(DrawingContext drawingContext)
       {
           
           EnsureScrolling();
           base.OnRender(drawingContext);

           if (System.ComponentModel.DesignerProperties.GetIsInDesignMode(this))
           {
              OnRenderDesigner(drawingContext);
           }
           else
           {
               if (this.LineCount == 0)
               {
                   ReRenderLastRuntimeRender(drawingContext);
                   renderTimer.IsEnabled = true;
               }
               else
               {
                   OnRenderRuntime(drawingContext);
               }
           }
       }

     /// <summary>
     ///The main render code
     /// </summary>
     /// <param name="drawingContext"></param>
       protected void OnRenderRuntime(DrawingContext drawingContext)
       {
           drawingContext.PushClip(new RectangleGeometry(new Rect(0, 0, this.ActualWidth, this.ActualHeight)));//restrict drawing to textbox
           drawingContext.DrawRectangle(CodeBoxBackground, null, new Rect(0, 0, this.ActualWidth, this.ActualHeight));//Draw Background
           if (this.Text == "") return;

               int firstLine = GetFirstVisibleLineIndex();// GetFirstLine();
               int firstChar = (firstLine == 0) ? 0 : GetCharacterIndexFromLineIndex(firstLine);// GetFirstChar();
               string visibleText = VisibleText;
               if (visibleText == null) return;

               Double leftMargin = 4.0 + this.BorderThickness.Left;
               Double topMargin = 2.0 + this.BorderThickness.Top;

               formattedText = new FormattedText(
                      this.VisibleText,
                       CultureInfo.GetCultureInfo("en-us"),
                       FlowDirection.LeftToRight,
                       new Typeface(this.FontFamily.Source),
                       this.FontSize,
                       BaseForeground);  //Text that matches the textbox's
               formattedText.Trimming = TextTrimming.None;

               ApplyTextWrapping(formattedText);

               Pair visiblePair = new Pair(firstChar, visibleText.Length);
               Point renderPoint =   GetRenderPoint(firstChar);
               
                //Generates the prepared decorations for the BaseDecorations
               Dictionary<EDecorationType, Dictionary<Decoration, List<Geometry>>> basePreparedDecorations 
                   = GeneratePreparedDecorations(visiblePair, DecorationScheme.BaseDecorations);
               //Displays the prepared decorations for the BaseDecorations
               DisplayPreparedDecorations(drawingContext, basePreparedDecorations, renderPoint);

               //Generates the prepared decorations for the Decorations
               Dictionary<EDecorationType, Dictionary<Decoration, List<Geometry>>> preparedDecorations 
                   = GeneratePreparedDecorations(visiblePair, mDecorations);
               //Displays the prepared decorations for the Decorations
               DisplayPreparedDecorations(drawingContext, preparedDecorations, renderPoint);

               ColorText(firstChar, DecorationScheme.BaseDecorations);//Colors According to Scheme
               ColorText(firstChar, mDecorations);//Colors Acording to Decorations
               drawingContext.DrawText(formattedText, renderPoint);

               if (this.LineNumberMarginWidth > 0) //Are line numbers being used
               { //Even if we gey this far it is still possible for the line numbers to fail
                   if (this.GetLastVisibleLineIndex() != -1)
                   {
                       FormattedText lineNumbers = GenerateLineNumbers();
                       drawingContext.DrawText(lineNumbers, new Point(3, renderPoint.Y));
                       renderinfo.LineNumbers = lineNumbers;
                   }
                   else
                   {
                       drawingContext.DrawText(renderinfo.LineNumbers, new Point(3, renderPoint.Y));
                   }
               }

           //Cache information for possible rerender
            renderinfo.BoxText = formattedText;
            renderinfo.BasePreparedDecorations = basePreparedDecorations;
            renderinfo.PreparedDecorations = preparedDecorations;
       }

     /// <summary>
     /// Render logic for the designer
     /// </summary>
     /// <param name="drawingContext"></param>
       protected void OnRenderDesigner(DrawingContext drawingContext)
       {
            
           int firstChar = 0;
           
           Double leftMargin = 4.0 + this.BorderThickness.Left;
           Double topMargin = 2.0 + this.BorderThickness.Top;


           string visibleText = VisibleText;

           formattedText = new FormattedText(
                  this.Text,
                   CultureInfo.GetCultureInfo("en-us"),
                   FlowDirection.LeftToRight,
                   new Typeface(this.FontFamily.Source),
                   this.FontSize,
                   BaseForeground);  //Text that matches the textbox's
           formattedText.Trimming = TextTrimming.None;

           string lineNumberString = "1\n2\n3\n";
           FormattedText lineNumbers = new FormattedText(
                 lineNumberString,
                   CultureInfo.GetCultureInfo("en-us"),
                   FlowDirection.LeftToRight,
                   new Typeface(this.FontFamily.Source),
                   this.FontSize,
                   LineNumberForeground);


           previousFirstChar = firstChar;
           Pair visiblePair = new Pair(firstChar, Text.Length);

           drawingContext.PushClip(new RectangleGeometry(new Rect(0, 0, this.ActualWidth, this.ActualHeight)));//restrict text to textbox
           Point renderPoint = new Point(this.LineNumberMarginWidth + leftMargin, topMargin);
          
           drawingContext.DrawRectangle(CodeBoxBackground ,null ,new Rect(0, 0, this.ActualWidth, this.ActualHeight));

           Dictionary<Decoration, List<Geometry>> hilightgeometryDictionary = PrepareGeometries(visiblePair, formattedText,mDecorations , EDecorationType.Hilight, HilightGeometryMaker);
           DisplayGeometry(drawingContext, hilightgeometryDictionary, renderPoint);

           Dictionary<Decoration, List<Geometry>> strikethroughGeometryDictionary = PrepareGeometries(visiblePair, formattedText, mDecorations, EDecorationType.Strikethrough, StrikethroughGeometryMaker);
           DisplayGeometry(drawingContext, strikethroughGeometryDictionary, renderPoint);

           Dictionary<Decoration, List<Geometry>> underlineGeometryDictionary = PrepareGeometries(visiblePair, formattedText, mDecorations, EDecorationType.Underline, UnderlineGeometryMaker);
           DisplayGeometry(drawingContext, underlineGeometryDictionary, renderPoint);
           ColorText(firstChar, mDecorations);

           drawingContext.DrawText(lineNumbers, new Point(3, renderPoint.Y));
           drawingContext.DrawText(formattedText, renderPoint);

       }


     /// <summary>
     /// Performs the last successful render again.
     /// </summary>
     /// <param name="drawingContext"></param>
       protected void ReRenderLastRuntimeRender(DrawingContext drawingContext)
       {
           drawingContext.DrawText(renderinfo.BoxText, renderinfo.RenderPoint);
           DisplayPreparedDecorations(drawingContext, renderinfo.PreparedDecorations, renderinfo.RenderPoint);
           DisplayPreparedDecorations(drawingContext, renderinfo.BasePreparedDecorations, renderinfo.RenderPoint);
           if (this.LineNumberMarginWidth > 0) //Are line numbers being used
           {
               drawingContext.DrawText(renderinfo.LineNumbers, new Point(3, renderinfo.RenderPoint.Y));
           }
       }


     /// <summary>
     /// Performs the EDecorationType.TextColor decorations in the formattted text.
     /// </summary>
     /// <param name="firstChar"></param>
     /// <param name="decorations"></param>
       private void ColorText(int firstChar, List<Decoration> decorations)
       {
           if (decorations != null)
           {
               foreach (Decoration dec in decorations)
               {
                   if (dec.DecorationType == EDecorationType.TextColor)
                   {
                       List<Pair> ranges = dec.Ranges(this.Text);
                       foreach (Pair p in ranges)
                       {
                           if (p.End > firstChar && p.Start < firstChar + formattedText.Text.Length)
                           {
                               int adjustedStart = Math.Max(p.Start - firstChar, 0);
                               int adjustedLength = Math.Min(p.Length + Math.Min(p.Start - firstChar, 0), formattedText.Text.Length - adjustedStart);
                               formattedText.SetForegroundBrush(dec.Brush, adjustedStart, adjustedLength);
                           }
                       }
                   }
               }
           }
       }


       public void ApplyTextWrapping(FormattedText formattedText)
       {
           switch (this.TextWrapping)
           {
               case TextWrapping.NoWrap:
                   break;
               case TextWrapping.Wrap:
                   formattedText.MaxTextWidth = this.ViewportWidth; //Used with Wrap only
                   break;
               case TextWrapping.WrapWithOverflow:
                   formattedText.SetMaxTextWidths(VisibleLineWidthsIncludingTrailingWhitespace());
                   break;
           }

       }

     /// <summary>
     /// Displays the Decorations for a List of Decorations
     /// </summary>
     /// <param name="drawingContext">The drawing Context from the OnRender</param>
     /// <param name="visiblePair">The pair representing the first character of the Visible text with respect to the whole text</param>
     /// <param name="renderPoint">The Point representing the offset from (0,0) for rendering</param>
     /// <param name="decorations">The List of Decorations</param>
       private void DisplayDecorations(DrawingContext drawingContext, Pair visiblePair, Point renderPoint , List<Decoration> decorations)
       {
           Dictionary<Decoration, List<Geometry>> hilightgeometryDictionary = PrepareGeometries(visiblePair, formattedText, decorations, EDecorationType.Hilight, HilightGeometryMaker);
           DisplayGeometry(drawingContext, hilightgeometryDictionary, renderPoint);

           Dictionary<Decoration, List<Geometry>> strikethroughGeometryDictionary = PrepareGeometries(visiblePair, formattedText, decorations, EDecorationType.Strikethrough, StrikethroughGeometryMaker);
           DisplayGeometry(drawingContext, strikethroughGeometryDictionary, renderPoint);

           Dictionary<Decoration, List<Geometry>> underlineGeometryDictionary = PrepareGeometries(visiblePair, formattedText, decorations, EDecorationType.Underline, UnderlineGeometryMaker);
           DisplayGeometry(drawingContext, underlineGeometryDictionary, renderPoint);
           
       }

     /// <summary>
     /// The first part of the split version of Display decorations.
     /// </summary>
     /// <param name="visiblePair">The pair representing the first character of the Visible text with respect to the whole text</param>
     /// <param name="decorations">The List of Decorations</param>
     /// <returns></returns>
       private Dictionary<EDecorationType, Dictionary<Decoration, List<Geometry>>> GeneratePreparedDecorations(Pair visiblePair, List<Decoration> decorations)
       {
           Dictionary<EDecorationType, Dictionary<Decoration, List<Geometry>>> preparedDecorations = new Dictionary<EDecorationType, Dictionary<Decoration, List<Geometry>>>();
           Dictionary<Decoration, List<Geometry>> hilightgeometryDictionary = PrepareGeometries(visiblePair, formattedText, decorations, EDecorationType.Hilight, HilightGeometryMaker);
           preparedDecorations.Add(EDecorationType.Hilight, hilightgeometryDictionary);
           Dictionary<Decoration, List<Geometry>> strikethroughGeometryDictionary = PrepareGeometries(visiblePair, formattedText, decorations, EDecorationType.Strikethrough, StrikethroughGeometryMaker);
           preparedDecorations.Add(EDecorationType.Strikethrough, strikethroughGeometryDictionary);
           Dictionary<Decoration, List<Geometry>> underlineGeometryDictionary = PrepareGeometries(visiblePair, formattedText, decorations, EDecorationType.Underline, UnderlineGeometryMaker);
           preparedDecorations.Add(EDecorationType.Underline, underlineGeometryDictionary);
           return preparedDecorations;
       }

     /// <summary>
     /// The second half of the  DisplayDecorations.
     /// </summary>
     /// <param name="drawingContext">The drawing Context from the OnRender</param>
     /// <param name="preparedDecorations">The previously prepared decorations</param>
     /// <param name="renderPoint">The Point representing the offset from (0,0) for rendering</param>
       private void DisplayPreparedDecorations(DrawingContext drawingContext, Dictionary<EDecorationType, Dictionary<Decoration, List<Geometry>>> preparedDecorations, Point renderPoint)
       {
           DisplayGeometry(drawingContext, preparedDecorations[EDecorationType.Hilight], renderPoint);
           DisplayGeometry(drawingContext, preparedDecorations[EDecorationType.Strikethrough ], renderPoint);
           DisplayGeometry(drawingContext, preparedDecorations[EDecorationType.Underline], renderPoint);
       }
       #endregion




    /// <summary>
    /// Gets the Renderpoint, the top left corner of the first character displayed. Note that this can 
    /// have negative vslues when the textbox is scrolling.
    /// </summary>
    /// <param name="firstChar">The first visible character</param>
    /// <returns></returns>
     private Point GetRenderPoint(int firstChar)
     {
         try
         {
            Rect cRect = GetRectFromCharacterIndex(firstChar);
            Point  renderPoint = new Point(cRect.Left, cRect.Top);
            if (!Double.IsInfinity(cRect.Top))
            {
                renderinfo.RenderPoint = renderPoint;
            }
            else
            {
                 this.renderTimer.IsEnabled = true;
            }
            return renderinfo.RenderPoint;
         }
         catch
         {
              this.renderTimer.IsEnabled = true;
             return renderinfo.RenderPoint;
         }
     }

     private void DisplayGeometry(DrawingContext drawingContext, Dictionary<Decoration, List<Geometry>> geometryDictionary ,Point renderPoint )
        {
            foreach (Decoration dec in geometryDictionary.Keys)
            {
                List<Geometry> GeomList = geometryDictionary[dec];
                foreach (Geometry g in GeomList)
                {
                    g.Transform = new System.Windows.Media.TranslateTransform(renderPoint.X, renderPoint.Y);
                    drawingContext.DrawGeometry(dec.Brush, null, g);
                }
            }
        }


     private Dictionary<Decoration, List<Geometry>> PrepareGeometries(Pair pair,FormattedText visibleFormattedText, List<Decoration> decorations,EDecorationType decorationType,GeometryMaker gMaker)
        {
            Dictionary<Decoration, List<Geometry>> geometryDictionary = new Dictionary<Decoration, List<Geometry>>();
            foreach (Decoration dec in decorations)
            {
                List<Geometry> geomList = new List<Geometry>();
                if (dec.DecorationType == decorationType)
                {
                    List<Pair> ranges = dec.Ranges(this.Text);
                    foreach (Pair p in ranges)
                    {
                        if (p.End > pair.Start && p.Start < pair.Start + VisibleText.Length)
                        {
                            int adjustedStart = Math.Max(p.Start - pair.Start, 0); 
                            int adjustedLength = Math.Min(p.Length + Math.Min(p.Start - pair.Start, 0), pair.Length  - adjustedStart);
                            Geometry geom = gMaker(visibleFormattedText ,new Pair(adjustedStart , adjustedLength ));
                            geomList.Add(geom);
                        }
                    }
                }
                geometryDictionary.Add(dec, geomList);
            }
            return geometryDictionary;
        }

     /// <summary>
     ///Delegate used with the PrepareGeomeries method.
     /// </summary>
     /// <param name="text">The FormattedText used for the decoration</param>
     /// <param name="p">The pair defining the begining character and the length of the character range</param>
     /// <returns></returns>
     private delegate Geometry GeometryMaker(FormattedText text, Pair p);
     
     /// <summary>
     /// Creates the Geometry for the Hilight decoration, used with the GeometryMakerDelegate.
     /// </summary>
     /// <param name="text">The FormattedText used for the decoration</param>
     /// <param name="p">The pair defining the begining character and the length of the character range</param>
     /// <returns></returns>
     private Geometry HilightGeometryMaker(FormattedText text, Pair p)
      {
        return text.BuildHighlightGeometry(new Point(0, 0), p.Start, p.Length);
      }

     /// <summary>
     /// Creates the Geometry for the Underline decoration, used with the GeometryMakerDelegate.
     /// </summary>
     /// <param name="text">The FormattedText used for the decoration</param>
     /// <param name="p">The pair defining the begining character and the length of the character range</param>
     /// <returns></returns>
      private Geometry UnderlineGeometryMaker(FormattedText text, Pair p)
        {
            Geometry geom = text.BuildHighlightGeometry(new Point(0, 0), p.Start, p.Length); 
            if (geom != null)
            {
                StackedRectangleGeometryHelper srgh = new StackedRectangleGeometryHelper(geom);
                return srgh.BottomEdgeRectangleGeometry();
            }
            else
            {
                return null;
            }
        }

     /// <summary>
      /// Creates the Geometry for the Strikethrough decoration, used with the GeometryMakerDelegate.
     /// </summary>
     /// <param name="text">The FormattedText used for the decoration</param>
     /// <param name="p">The pair defining the begining character and the length of the character range</param>
     /// <returns></returns>
      private Geometry StrikethroughGeometryMaker(FormattedText text, Pair p)
        {
            Geometry geom = text.BuildHighlightGeometry(new Point(0, 0), p.Start, p.Length);
            if (geom != null)
            {
                StackedRectangleGeometryHelper srgh = new StackedRectangleGeometryHelper(geom);
                return srgh.CenterLineRectangleGeometry();
            }
            else
            {
                return null;
            }
        }



     /// <summary>
     /// Makes sure that the scrolling event is being listended to.
     /// </summary>
     private void EnsureScrolling()
        {
            if (!mScrollingEventEnabled)
            {
                try
                {
                    DependencyObject dp = VisualTreeHelper.GetChild(this, 0);
                    dp = VisualTreeHelper.GetChild(dp, 0);
                    ScrollViewer sv = VisualTreeHelper.GetChild(dp, 0) as ScrollViewer;
                    sv.ScrollChanged += new ScrollChangedEventHandler(ScrollChanged);
                    mScrollingEventEnabled = true;
                }
                catch { }
            }
        }

     private void ScrollChanged(object sender, ScrollChangedEventArgs e)
     {
        this.InvalidateVisual();
     }

        /// <summary>
        /// Gets the Text that is visible in the textbox. Please note that it depends on
        ///  GetFirstVisibleLineIndex and 
        /// </summary>
        private  string VisibleText
        {
            get
            {
                if (this.Text == "") { return ""; }
                string visibleText = "";
                try
                {
                    int textLength = Text.Length;
                    int firstLine = GetFirstVisibleLineIndex();
                    int lastLine = GetLastVisibleLineIndex();

                    int lineCount = this.LineCount;
                    int firstChar = (firstLine == 0) ? 0 : GetCharacterIndexFromLineIndex(firstLine);

                    int lastChar = GetCharacterIndexFromLineIndex(lastLine) + GetLineLength(lastLine) - 1;
                    int length = lastChar - firstChar + 1;
                    int maxlenght = textLength - firstChar;
                    string text =  Text.Substring(firstChar, Math.Min(maxlenght, length));
                    if (text != null)
                    {
                        visibleText = text;
                    }
                }
                catch
                {
                    Debug.WriteLine("GetVisibleText failure");
                }
            return    visibleText;
            }
        }

     /// <summary>
     /// Returns the line widths for use with the wrap with overflow.
     /// </summary>
     /// <returns></returns>
        private Double[] VisibleLineWidthsIncludingTrailingWhitespace()
        {

            int firstLine = this.GetFirstVisibleLineIndex();
            int lastLine =Math.Max ( this.GetLastVisibleLineIndex(),firstLine) ;
            Double[] lineWidths = new Double[lastLine - firstLine + 1];
            if (lineWidths.Length == 1)
            {
                lineWidths[0] = MeasureString(this.Text);
            }
            else
            {
                for (int i = firstLine; i <= lastLine; i++)
                {
                    string lineString = this.GetLineText(i);
                    lineWidths[i - firstLine] = MeasureString(lineString);
                }
            }
            return lineWidths;
        }

    
     /// <summary>
     /// Returns the width of the string in the font and fontsize of the textbox including the trailing white space.
     /// Used for wrap with overflow.
     /// </summary>
     /// <param name="str">The string to measure</param>
     /// <returns></returns>
        private double MeasureString(string str)
        {
            FormattedText formattedText = new FormattedText(
                 str,
                   CultureInfo.GetCultureInfo("en-us"),
                   FlowDirection.LeftToRight,
                   new Typeface(this.FontFamily.Source),
                   this.FontSize,
                  new SolidColorBrush(Colors.Black));
            if (str == "")
            {
                return formattedText.WidthIncludingTrailingWhitespace;
            }
            else if (str.Substring(0, 1) == "\t")
            {
                return   formattedText.WidthIncludingTrailingWhitespace   ;
            }
            else
            {
                return formattedText.WidthIncludingTrailingWhitespace;
            }
        }



     #region line number calculations

     /// <summary>
     /// Generates the formated text used to display the line numbers. 
     /// It depends on the TextWrapping property.
     /// </summary>
     /// <returns></returns>
     private FormattedText GenerateLineNumbers(){
       
         switch (this.TextWrapping)
         {
             case TextWrapping.NoWrap:
                 return LineNumberWithoutWrap();
             case TextWrapping.Wrap:
                 return LineNumberWithWrap();
             case TextWrapping.WrapWithOverflow:
                 return LineNumberWithWrap();
         }
         return null;

     }

     /// <summary>
     /// Generates FormattedText for line numbers when TextWrapping = None
     /// </summary>
     /// <returns></returns>
     private FormattedText LineNumberWithoutWrap()
     {
         int firstLine = GetFirstVisibleLineIndex();  
         int lastLine = GetLastVisibleLineIndex(); 
         StringBuilder sb = new StringBuilder();
         for (int i = firstLine; i <= lastLine; i++)
         {
             sb.Append((i + StartingLineNumber) + "\n");
         }
         string lineNumberString = sb.ToString();
         FormattedText lineNumbers = new FormattedText(
               lineNumberString,
                 CultureInfo.GetCultureInfo("en-us"),
                 FlowDirection.LeftToRight,
                 new Typeface(this.FontFamily.Source),
                 this.FontSize,
                 LineNumberForeground);
         return lineNumbers;
     }

     /// <summary>
     /// Generates FormattedText for line numbers when TextWrapping = Wrap or WrapWithOverflow
     /// </summary>
     /// <returns></returns>
     private  FormattedText LineNumberWithWrap()
        {
            try
            {
                int[] linePos = MinLineStartCharcterPositions();
                int[] lineStart = VisibleLineStartCharcterPositions();
                if (lineStart != null)
                {
                    string lineNumberString = LineNumbers(lineStart, linePos);
                    FormattedText lineText = new FormattedText(
                          lineNumberString,
                            CultureInfo.GetCultureInfo("en-us"),
                            FlowDirection.LeftToRight,
                            new Typeface(this.FontFamily.Source),
                            this.FontSize,
                            LineNumberForeground);

                    renderinfo.LineNumbers = lineText;
                    return lineText;
                }
                else
                {
                    return renderinfo.LineNumbers;
                }
            }
            catch
            {
                return renderinfo.LineNumbers;

            }

        }

  

        /// <summary>
        /// Returns the character positions that start lines as determined only by the characters.
        /// </summary>
        /// <returns></returns>
        private int[] MinLineStartCharcterPositions()
        {
            int totalChars = this.Text.Length;
            char[] boxChars = this.Text.ToCharArray();
            char newlineChar = Convert.ToChar("\n");
            char returnChar = Convert.ToChar("\r");
            char formfeed = Convert.ToChar("\f");
            char vertQuote = Convert.ToChar("\v");

            List<int> breakChars = new List<int>() { 0 };

            //This looks a bit exotic but keep in mind that \r\n or \r or \n or \f or \v all will 
            //signify a new line to the textbox.
            if (boxChars.Length > 1)
            {
                for (int i = 2; i < boxChars.Length; i++)
                {
                    if (boxChars[i - 1] == returnChar && boxChars[i - 2] == newlineChar)
                    {
                        breakChars.Add(i);
                    }
                    if (boxChars[i - 1] == newlineChar && boxChars[i] != returnChar)
                    {
                        breakChars.Add(i);
                    }
                    if (boxChars[i - 1] == formfeed || boxChars[i - 1] == vertQuote)
                    {
                        breakChars.Add(i);
                    }
                }
            }
            int[] MinPositions = new int[breakChars.Count];
            breakChars.CopyTo(MinPositions);
            return MinPositions;
        }


     /// <summary>
     /// Returns the character positions that the textbox declares to begin the 
     /// visible lines.
     /// </summary>
     /// <returns></returns>
        private int[] VisibleLineStartCharcterPositions()
        {
            int firstLine = GetFirstVisibleLineIndex();
            int lastLine = GetLastVisibleLineIndex();
            if (lastLine != -1)
            {
                int lineCount = lastLine - firstLine + 1;
                int[] startingPositions = new int[lineCount];
                for (int i = firstLine; i <= lastLine; i++)
                {
                    int startPos = this.GetCharacterIndexFromLineIndex(i);
                    startingPositions[i - firstLine] = startPos;
                }

                 
                return startingPositions;

            }
            else
            {
                return null;
            }
        }

 
 
     /// <summary>
        /// Create the String of line numbers. Uses merge algorithm http://en.wikipedia.org/wiki/Merge_algorithm
     /// </summary>
     /// <param name="listA">The List of the first characters of the visible lines. This is affected by box width.</param>
     /// <param name="listB">The List of First Characters of the Lines determined by characters rather than  box width.</param>
     /// <returns></returns>
        private   string LineNumbers(int[] listA, int[] listB)
        {
            StringBuilder sb = new StringBuilder();
            int a = 0;
            int b = 0;
            List<int> matches = new List<int>() ;
            List<int> skipped = new List<int>();
            while (a < listA.Length && b < listB.Length)
            {
                if (listA[a] == listB[b])
                {
                    matches.Add(b);
                    a++;
                    b++;
                }
                else if (listA[a] < listB[b])
                {
                    matches.Add(-1);
                    a++;
                }
                else
                {
                    skipped.Add(b);
                    b++;
                }
            }
            while (a < listA.Length )
            {
                a++;
            }

            while (b < listB.Length )
            {
                b++;
            }

            //There will be missing lien numbers where the lines are blank.
            //The skipped lines are returned.
           
            // in reverse because ther could be more than one sequential blank line.
            for (int i = (skipped.Count - 1); i >= 0; i--) 
            {
                // index  is the position directly before the index in the matches array of
                //one greaer than the missing elements. 
               int  index = matches.IndexOf(skipped[i] + 1) - 1;
               if (index > -1)
               {
                   matches[index] = skipped[i];
               }
            }

            //Adjusts the line numbers so that line 0 has the value of StartingLineNumber
            for  (int i = 0; i < matches.Count ;i++)
            {
                if (matches[i] != -1) matches[i] += this.StartingLineNumber;
            }

            StringBuilder sb2 = new StringBuilder();
            foreach (int i in matches)
            {
                if (i == -1)
                {
                    sb2.Append("\n");
                }
                else
                {
                    sb2.Append(i + "\n");
                }
            }
            return sb2.ToString();
        }

        #endregion

     }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United States United States
Written software for what seems like forever. I'm currenly infatuated with WPF. Hopefully my affections are returned.

Comments and Discussions