For a while now I've been toying with the idea of writing a C# editor (almost an IDE, but with only the features that I need). Clearly the hardest part of this task would be getting some form of edit control working.. the obvious choice for which is the RichTextBox. Anyone who has tried using RichTextBox in this situation will know that it has a number of flaws, the most serious being that it's rather slow when trying to colourise the text.
I've long since given up on using RichTextBox for a code editor, but while I was investigating its use I came across a number of ways to improve it's functionality. I'll present these here in a series of posts on the subject.. first up is how to speed up the control's updating, as well as adding one little extra formatting option.
Faster Updating
There are articles on this subject on the internet. The standard method seems to be to send a WM_SETREDRAW message to prevent the control from being redrawn while updating. I had this implemented and running nicely when I came across another article showing that an extra speed increase can be gained by sending EM_SETEVENTMASK to prevent the control from handling events. Here's the code:
+ Code to help speed up RichTextBox updating.
/// <summary>
/// Maintains performance while updating.
/// </summary>
/// <remarks>
/// <para>
/// It is recommended to call this method before doing
/// any major updates that you do not wish the user to
/// see. Remember to call EndUpdate when you are finished
/// with the update. Nested calls are supported.
/// </para>
/// <para>
/// Calling this method will prevent redrawing. It will
/// also setup the event mask of the underlying richedit
/// control so that no events are sent.
/// </para>
/// </remarks>
public void BeginUpdate()
{
// Deal with nested calls.
++updating;
if ( updating > 1 )
return;
// Prevent the control from raising any events.
oldEventMask = SendMessage( new HandleRef( this, Handle ),
EM_SETEVENTMASK, 0, 0 );
// Prevent the control from redrawing itself.
SendMessage( new HandleRef( this, Handle ),
WM_SETREDRAW, 0, 0 );
}
/// <summary>
/// Resumes drawing and event handling.
/// </summary>
/// <remarks>
/// This method should be called every time a call is made
/// made to BeginUpdate. It resets the event mask to it's
/// original value and enables redrawing of the control.
/// </remarks>
public void EndUpdate()
{
// Deal with nested calls.
--updating;
if ( updating > 0 )
return;
// Allow the control to redraw itself.
SendMessage( new HandleRef( this, Handle ),
WM_SETREDRAW, 1, 0 );
// Allow the control to raise event messages.
SendMessage( new HandleRef( this, Handle ),
EM_SETEVENTMASK, 0, oldEventMask );
}
Although I'm using HandleRef here, I have absolutely no idea if this is the correct usage (or even if it's needed). Full code will be at the end of this posting. I have not fully tested these methods so I have no idea what the performance gains might be (if there are any). If anyone wants to try benchmarking, please let me know the results.
Justification
It always bothered me that the SelectionAlignment property of RichTextBox does not allow fully justified text. Especially since I know it's possible with the richedit control that RichTextBox wraps. Here's some code that will replace SelectionAlignment with something a little nicer for those that are writing word processors in C#:
+ Code to allow justified text in the RichTextBox.
/// <summary>
/// Gets or sets the alignment to apply to the current
/// selection or insertion point.
/// </summary>
/// <remarks>
/// Replaces the SelectionAlignment from
/// <see cref="RichTextBox"/>.
/// </remarks>
public new TextAlign SelectionAlignment
{
get
{
PARAFORMAT fmt = new PARAFORMAT();
fmt.cbSize = Marshal.SizeOf( fmt );
// Get the alignment.
SendMessage( new HandleRef( this, Handle ),
EM_GETPARAFORMAT,
SCF_SELECTION, ref fmt );
// Default to Left align.
if ( ( fmt.dwMask & PFM_ALIGNMENT ) == 0 )
return TextAlign.Left;
return ( TextAlign )fmt.wAlignment;
}
set
{
PARAFORMAT fmt = new PARAFORMAT();
fmt.cbSize = Marshal.SizeOf( fmt );
fmt.dwMask = PFM_ALIGNMENT;
fmt.wAlignment = ( short )value;
// Set the alignment.
SendMessage( new HandleRef( this, Handle ),
EM_SETPARAFORMAT,
SCF_SELECTION, ref fmt );
}
}
The TextAlign enumeration contains Left, Right, Center and Justify. Again, this is mostly untested -- it should work on any machine with the richedit 2.0 common control (I believe), but I offer no guarantees.
Code
Here's the full code for the AdvRichTextBox control (plus the TextAlign enumeration):
+ Full AdvRichTextBox code.
using System;
using System.Windows.Forms;
using System.Runtime.InteropServices;
/// <summary>
/// Represents a standard <see cref="RichTextBox"/> with some
/// minor added functionality.
/// </summary>
/// <remarks>
/// AdvRichTextBox provides methods to maintain performance
/// while it is being updated. Additional formatting features
/// have also been added.
/// </remarks>
public class AdvRichTextBox : RichTextBox
{
/// <summary>
/// Maintains performance while updating.
/// </summary>
/// <remarks>
/// <para>
/// It is recommended to call this method before doing
/// any major updates that you do not wish the user to
/// see. Remember to call EndUpdate when you are finished
/// with the update. Nested calls are supported.
/// </para>
/// <para>
/// Calling this method will prevent redrawing. It will
/// also setup the event mask of the underlying richedit
/// control so that no events are sent.
/// </para>
/// </remarks>
public void BeginUpdate()
{
// Deal with nested calls.
++updating;
if ( updating > 1 )
return;
// Prevent the control from raising any events.
oldEventMask = SendMessage( new HandleRef( this, Handle ),
EM_SETEVENTMASK, 0, 0 );
// Prevent the control from redrawing itself.
SendMessage( new HandleRef( this, Handle ),
WM_SETREDRAW, 0, 0 );
}
/// <summary>
/// Resumes drawing and event handling.
/// </summary>
/// <remarks>
/// This method should be called every time a call is made
/// made to BeginUpdate. It resets the event mask to it's
/// original value and enables redrawing of the control.
/// </remarks>
public void EndUpdate()
{
// Deal with nested calls.
--updating;
if ( updating > 0 )
return;
// Allow the control to redraw itself.
SendMessage( new HandleRef( this, Handle ),
WM_SETREDRAW, 1, 0 );
// Allow the control to raise event messages.
SendMessage( new HandleRef( this, Handle ),
EM_SETEVENTMASK, 0, oldEventMask );
}
/// <summary>
/// Gets or sets the alignment to apply to the current
/// selection or insertion point.
/// </summary>
/// <remarks>
/// Replaces the SelectionAlignment from
/// <see cref="RichTextBox"/>.
/// </remarks>
public new TextAlign SelectionAlignment
{
get
{
PARAFORMAT fmt = new PARAFORMAT();
fmt.cbSize = Marshal.SizeOf( fmt );
// Get the alignment.
SendMessage( new HandleRef( this, Handle ),
EM_GETPARAFORMAT,
SCF_SELECTION, ref fmt );
// Default to Left align.
if ( ( fmt.dwMask & PFM_ALIGNMENT ) == 0 )
return TextAlign.Left;
return ( TextAlign )fmt.wAlignment;
}
set
{
PARAFORMAT fmt = new PARAFORMAT();
fmt.cbSize = Marshal.SizeOf( fmt );
fmt.dwMask = PFM_ALIGNMENT;
fmt.wAlignment = ( short )value;
// Set the alignment.
SendMessage( new HandleRef( this, Handle ),
EM_SETPARAFORMAT,
SCF_SELECTION, ref fmt );
}
}
/// <summary>
/// This member overrides
/// <see cref="Control"/>.OnHandleCreated.
/// </summary>
protected override void OnHandleCreated( EventArgs e )
{
base.OnHandleCreated( e );
// Enable support for justification.
SendMessage( new HandleRef( this, Handle ),
EM_SETTYPOGRAPHYOPTIONS,
TO_ADVANCEDTYPOGRAPHY,
TO_ADVANCEDTYPOGRAPHY );
}
private int updating = 0;
private int oldEventMask = 0;
// Constants from the Platform SDK.
private const int EM_SETEVENTMASK = 1073;
private const int EM_GETPARAFORMAT = 1085;
private const int EM_SETPARAFORMAT = 1095;
private const int EM_SETTYPOGRAPHYOPTIONS = 1226;
private const int WM_SETREDRAW = 11;
private const int TO_ADVANCEDTYPOGRAPHY = 1;
private const int PFM_ALIGNMENT = 8;
private const int SCF_SELECTION = 1;
// It makes no difference if we use PARAFORMAT or
// PARAFORMAT2 here, so I have opted for PARAFORMAT2.
[StructLayout( LayoutKind.Sequential )]
private struct PARAFORMAT
{
public int cbSize;
public uint dwMask;
public short wNumbering;
public short wReserved;
public int dxStartIndent;
public int dxRightIndent;
public int dxOffset;
public short wAlignment;
public short cTabCount;
[MarshalAs( UnmanagedType.ByValArray, SizeConst = 32 )]
public int[] rgxTabs;
// PARAFORMAT2 from here onwards.
public int dySpaceBefore;
public int dySpaceAfter;
public int dyLineSpacing;
public short sStyle;
public byte bLineSpacingRule;
public byte bOutlineLevel;
public short wShadingWeight;
public short wShadingStyle;
public short wNumberingStart;
public short wNumberingStyle;
public short wNumberingTab;
public short wBorderSpace;
public short wBorderWidth;
public short wBorders;
}
[DllImport( "user32", CharSet = CharSet.Auto )]
private static extern int SendMessage( HandleRef hWnd,
int msg,
int wParam,
int lParam );
[DllImport( "user32", CharSet = CharSet.Auto )]
private static extern int SendMessage( HandleRef hWnd,
int msg,
int wParam,
ref PARAFORMAT lp );
}
/// <summary>
/// Specifies how text in a <see cref="AdvRichTextBox"/> is
/// horizontally aligned.
/// </summary>
public enum TextAlign
{
/// <summary>
/// The text is aligned to the left.
/// </summary>
Left = 1,
/// <summary>
/// The text is aligned to the right.
/// </summary>
Right = 2,
/// <summary>
/// The text is aligned in the center.
/// </summary>
Center = 3,
/// <summary>
/// The text is justified.
/// </summary>
Justify = 4
}
Conclusion
I'll add to this code in future posts and if anyone has any comments or suggestions (or even requests), feel free to ask them. Hope you found this useful.