Syntax highlighting klíčových slov pomocí RichTextBoxu (C#)
Úvod
V tomto článku si předvedeme, jak je možné upravit .NET komponentu
RichTextBox za účelem provádění syntax highlightingu, neboli
zvýrazňování syntaxe (například na zvýrazňování kódu C/C++, C#, PHP,
...).
Pozn.: Implementace je spíše než ukázkovým řešením jen
zjednodušení, určitě nebude vždy fungovat správně... je to tedy jen
jakýsi "proof of concept".
Cílem je udělat to celé tak, že:
- řešení je co nejvíce obecné, aby některé algoritmy
(třeba už tokenizace, viz níže) mohly být v případě potřeby
pozměněny
- řešení je co nejvíce čitelné (protože o to jde ve
webových tutoriálech především, optimalizaci za daným účelem si pak
schopnější programátor zajisté provede sám)
- zvýraznění syntaxe musí probíhat v "rozumném čase"
(třeba při otevření souboru je potřeba obarvit celkem velký kus
kódu)
- text při zvýrazňování nemá blikat (to je častý
nedostatek na webu nalezitelných řešení)
- bude to umět operace Undo/Redo (taktéž často není
diskutováno) - není totiž možné použít vestavěné operace Undo/Redo,
neboť například operace Undo by pouze znovu odbarvila text, apod. (za tímto
účelem si napíšeme nejprimitivnější implementaci těchto funkcí)
Soubory ke stažení
Algoritmus
Obarvování textu bude založeno na tokenizaci kódu
pomocí regulárního výrazu (.NET třída Regexp, metoda
split()). K tomuto kompromisu - kompromis spočívá v "líném"
použití třídy Regexp - jsem se uchýlil proto, aby kód, který zde budu
prezentovat, zůstal rozumně dlouhý a co nejvíce srozumitelný. Na druhou
stranu tak celý kód běží o něco pomaleji, než by mohl, kdyby byla
tokenizace šitá na míru. Navíc toto řešení neumí obarvit například
řetězce nebo komentáře (tokeny které mohou obsahovat mezeru, čárku, ...
nebo jiný znak indikující hranici dvou tokenů). Jedná se tedy spíše o
zvýrazňování klíčových slov (nicméně to se dá upravit
právě napsáním vlastní tokenizace a určením "typu tokenu" spíše než
konkrétního klíčového slova, jak to dělám zde)... Pro samotné obarvení
textu se bude používat metod RichTextBoxu spojených s úpravami vybraného
textu.
Implementace
Jendotlivé části implementace jsem kvůli přehlednosti rozdělil
do následujících částí:
Kostra programu
Na začátku (poté, co přidáme do projektu novou třídu) upravíme kód
do této podoby:
» Rozbalit/sbalit kód «
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Collections;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows.Forms;
using System.ComponentModel;
using System.Runtime.InteropServices;
namespace SyntaxHighlighter
{
/*Slot A*/
public class extendedRichText : RichTextBox
{
/* Slot B */
}
}
Máme zde tedy uvedené všechny potřebné jmenné prostory a deklaraci
třídy extendedRichText, odvozené od RichTextBoxu. Povšimněte si také
komenářů - ty nic neříkají o kódu samotném, používám je zde jako
výukovou pomůcku pro to, abych vám mohl říct kam co psát.
Reprezentace tokenu
Začneme tedy psát třídu token na místo komentáře "Slot A":
» Rozbalit/sbalit kód «
public class token {
private String tokenValue;
private Color tokenColor;
private Font tokenFont;
public token(String tValue, Color tColor, Font tFont) {
this.tokenValue = tValue;
this.tokenColor = tColor;
this.tokenFont = tFont;
}
public String getValue() {
return this.tokenValue;
}
public Color getColor()
{
return this.tokenColor;
}
public Font getFont()
{
return this.tokenFont;
}
}
Token je tedy v našem (jednoduchém, výukovém) pojetí slovo, které má k
sobě asociovanou barvu a typ písma...
Datové položky, konstruktor třídy
Budeme pokračovat definicí těla třídy extendedRichText - všechno odteď
dolů tedy píšeme do Slotu B (!!!).
Nejprve samozřejmně specifikujeme datové položky, o kterých už víme,
že je budeme potřebovat. Po nich následuje definice třídy pro zpracování
operací Undo/Redo, dále pak konstruktor třídy -
extendedRichText():
» Rozbalit/sbalit kód «
// Klicova slova
private token[] keywords;
// Tridu UndoClass definujeme nize
private List<UndoClass> History;
// UndoPointer ukazuje na konkretni pozici v seznamu historie
private int UndoPointer = 0;
// Promenne spojene s updatovanim platna (prekreslenim)
private int updating = 0;
private int oldEventMask = 0;
// Jednoducha trida pro ulozeni historie uprav
private class UndoClass
{
public string rtfText;
public int posText;
public UndoClass(int pos, string str)
{
this.rtfText = str;
this.posText = pos;
}
}
// Konstruktor tridy
public extendedRichText(token[] Tokens, int UndoSize)
{
this.SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer, true);
this.Multiline = true;
this.WordWrap = false;
this.AcceptsTab = true;
this.DetectUrls = false;
this.ScrollBars = RichTextBoxScrollBars.ForcedBoth;
this.Dock = DockStyle.Fill;
// Velikost historie uprav se definuje v konstruktoru
History = new List<UndoClass>(UndoSize);
// Uloz jedno undo, aby ses mohl vratit az na zacatek...
// metoda StoreNewUndo je uvedena nize...
this.StoreNewUndo();
// Seznam klicovych slov se definuje v konstruktoru
this.keywords = Tokens;
// Handlery pro zmenu textu a pro zachytavani klaves
this.TextChanged += new EventHandler(TextChangedEvent);
this.KeyDown += new KeyEventHandler(KeyPressEvent);
}
Historie úprav (Undo/Redo)
Již z definice třídy UndoClass je vidět, že operace Undo a Redo budou
prováděny velice jednoduše - prostě si jen uložíme do seznamu aktuální
text a pozici kurzoru v textu. Aby toto bylo únosné, neukládáme si historii
po stisku každé klávesy, nýbrž po stisku "význačných kláves" - handler
KeyPressEvent(...) vypadá tedy takto:
» Rozbalit/sbalit kód «
private void KeyPressEvent(object sender, System.Windows.Forms.KeyEventArgs e)
{
if (e.Control && (e.Shift || e.Alt) && (e.KeyValue == 'Z' || e.KeyValue == 'Y'))
return;
if (e.KeyCode == Keys.Enter ||
e.KeyCode == Keys.Delete ||
e.KeyCode == Keys.Back ||
e.KeyValue == ' ' ||
e.KeyValue == ',' ||
e.KeyValue == '.' ||
e.KeyValue == ';' ||
e.KeyValue == '}' ||
e.KeyValue == '{' ||
e.KeyValue == '(' ||
e.KeyValue == ')')
{
StoreNewUndo();
}
}
Následují metody pro samotné provádění operací Undo a Redo. Metoda
StoreNewUndo() slouží k uložení aktuálního bodu historie
úprav (je privátní, není asi potřeba ji explicitně volat z venku),
RedoEvent() je metoda pro operaci Redo, UndoEvent() je
metoda pro operaci Undo (obě metody jsou veřejné).
» Rozbalit/sbalit kód «
private int StoreNewUndo()
{
if (History.Count > UndoPointer)
{
History.RemoveRange(UndoPointer, History.Count - UndoPointer);
}
History.Add(new UndoClass(this.SelectionStart, this.Rtf));
if (History.Count > 80)
{
History.RemoveAt(0);
UndoPointer = History.Count;
}
else
{
UndoPointer++;
}
return UndoPointer;
}
public void RedoEvent()
{
if (UndoPointer < 50 && UndoPointer < History.Count - 1)
{
this.BeginUpdate();
UndoPointer++;
this.Rtf = History[UndoPointer].rtfText;
this.SelectionStart = History[UndoPointer].posText;
this.EndUpdate();
this.Refresh();
}
}
public void UndoEvent()
{
if (UndoPointer > 0)
{
if (UndoPointer == History.Count)
{
StoreNewUndo();
if (UndoPointer > 0) UndoPointer--;
}
this.BeginUpdate();
UndoPointer--;
this.Rtf = History[UndoPointer].rtfText;
this.SelectionStart = History[UndoPointer].posText;
this.EndUpdate();
this.Refresh();
}
}
Vyvolání metod UndoEvent() a RedoEvent()
ponecháme na položce menu a k ní asociované klávesové zkratce, řešeni
subclassingem vynecháme.
Metody pro vypnutí překreslování RichTextBoxu
Dále si rozšíříme třídu o metody BeginUpdate() a
EndUpdate(), které zabrání (resp. povolí) překreslování
RichTextBoxu (tím zabráníme problikávání).
Za tímto účelem nejprve jednorázově importujeme WinAPI funkci
SendMessage(...) a definujeme podstatné konstanty... Je pěkné to
uzavřít do "regionu", ať nám to nestraší...
» Rozbalit/sbalit kód «
#region Windows API Balast
private const int WM_SETREDRAW = 0x0B;
private const int EM_SETEVENTMASK = 0x0431;
[DllImport("user32", CharSet = CharSet.Auto)]
private static extern int SendMessage(HandleRef hWnd, int msg, int wParam, int lParam);
#endregion
Nyní už můžeme napsat přímo metody BeginUpdate() a EndUpdate():
» Rozbalit/sbalit kód «
public void BeginUpdate()
{
this.TextChanged -= this.TextChangedEvent;
++updating;
if (updating > 1)
return;
oldEventMask = SendMessage(new HandleRef(this, Handle), EM_SETEVENTMASK, 0, 0);
SendMessage(new HandleRef(this, Handle), WM_SETREDRAW, 0, 0);
}
public void EndUpdate()
{
this.TextChanged += this.TextChangedEvent;
--updating;
if (updating > 0)
return;
SendMessage(new HandleRef(this, Handle), WM_SETREDRAW, 1, 0);
SendMessage(new HandleRef(this, Handle), EM_SETEVENTMASK, 0, oldEventMask);
// this.Refresh();
}
Povšimněte si zakomentovaného řádku v metodě EndUpdate().
Obnovení (překreslení) RichTextBoxu ve chvíli, kdy opětovně povolíme
překreslování totiž z principu do metody EndUpdate() nepatří,
nicméně volání metody Refresh() po zavolání
EndUpdate() je tak časté, že já jsem ve svém řešení tento
řádek odkomentoval, abych to nepsal pořád dokola (ale zde si to předvedeme
"pěkně", proto ho nechám zakomentovaný).
Dále je na začátku BeginUpdate() potřeba odebrat (a
následně na konci EndUpdate() přidat) handler pro událost
TextChanged - na to nezapomenout, jinak se nám budou lavinovitě
valit události!
Tokenizace, obarvování textu
Teď se dostáváme k samotnému jádru naší třídy. Napíšeme si funkci
pro tokenizaci textu (využijeme třídu Regexp), funkci na obarvování textu a
handleru události TextChanged.
» Rozbalit/sbalit kód «
// Napred si napiseme metodu pro tokenizaci textu dle znaku
private string[] tokenize(string Line)
{
// Do regularniho vyrazu zahrneme vsechny znaky, ktere mohou
// potencialne oddelovat jednotlive tokeny (znaky " ", ",", ...) ...
Regex r = new Regex("([ \\t{}():;,\"'])");
return r.Split(Line);
}
public void ParseLine(string Line)
{
// ...a roztrhneme text podle techto znaku na jednotlive tokeny
String[] tokens = this.tokenize(Line);
string token;
// Kazdy z techto tokenu potom porovname se seznamem tokenu, ktere
// specifikujeme v konstruktoru tridy...
foreach (string token2 in tokens)
{
// Osetreni v pripade, ze text "Line" obsahuje newline
// v takovem pripade je nutne ho odstranit, nebot:
// "\r\nif" != "if" apod.
if (token2.StartsWith("\r"))
{
if (token2 == "\r\n" || token2 == "\n" || token2 == " " || token2 == "\t")
{
this.SelectedText = token2;
continue;
}
token = token2.Replace("\r\n", "");
}
else
{
token = token2;
}
// Nastaveni vychozi barvy
this.SelectionColor = Color.Black;
this.SelectionFont = new Font("Courier New", 10, FontStyle.Regular);
// Porovnani aktualniho tokenu s tokeny jazyka, nastaveni barvy
// a typu pisma v pripade shody...
for (int i = 0; i < keywords.Length; i++)
{
if (keywords[i].getValue() == token)
{
this.SelectionColor = keywords[i].getColor();
this.SelectionFont = keywords[i].getFont();
break;
}
}
// A prepiseme vybrany text...
this.SelectedText = token2;
}
}
public void TextChangedEvent(object sender, EventArgs e)
{
if (this.SelectionStart < 0) return;
int start, end;
// zabraneni tomu aby se prekreslil vice nez jeden radek
for (start = this.SelectionStart - 1; start > 0; start--)
{
if (this.Text[start] == '\n')
{
start++;
break;
}
}
// zabraneni tomu aby se prekreslil vice nez jeden radek
for (end = this.SelectionStart; end < this.Text.Length; end++)
{
if (this.Text[end] == '\n')
{
break;
}
}
// osetreni podteceni pro prvni radek
if (start < 0) start = 0;
// ulozime si text ktery se ma updatovat...
String line = this.Text.Substring(start, end - start);
// vypneme prekreslovani RichTextBoxu
this.BeginUpdate();
// Ulozime si stav vyberu
int selectionStart = this.SelectionStart;
int selectionLength = this.SelectionLength;
// ... a vybereme text, ktery se ma prekreslit
this.SelectionStart = start;
this.SelectionLength = end - start;
// Zavolame "magickou cernou skrinku" ktera
// nam ten text prekresli
this.ParseLine(line);
// Vratime stav vyberu tak, jak byl, vybereme okno do "fokusu" a obnovime prekreslovani (prekreslime)...
this.SelectionStart = selectionStart;
this.SelectionLength = selectionLength;
this.Focus();
this.EndUpdate();
this.Refresh();
}
Kopírování a vkládání textu
Abychom mohli rychleji provádět vložení textu, budeme rozlišovat dva
případy. Jeden z nich je, že schránka obsahuje prostý text. V tom
případě si nepomůžeme a musíme celý text ve schránce obarvit (při
vkládání velkého kusu kódu je to pomalejší). V případě, že schránka
obsahuje RichText, je pravděpodobné, že se jedná o zkopírováný již
jednou zvýrazněný text - v takovém případě text prostě vložíme.
Samozřejmě to je hodně zjednodušený přístup - když si někdo zkopíruje
RichText z jiného editoru a prostě ho vloží, obarvení nebude správné. Pro
jednoduchost nám to ale bude stačit.
» Rozbalit/sbalit kód «
public new void Paste()
{
this.BeginUpdate();
if (Clipboard.ContainsText(TextDataFormat.Rtf))
{
base.Paste();
}
else
{
this.ParseLine(Clipboard.GetText());
}
this.EndUpdate();
this.Refresh();
}
Metody pro kopírování (Copy) a vyjmutí (Cut) můžeme ponechat tak jak
jsou...
Jak nakonec třídu používat
Příklad jednoduchého použití naší třídy:
public mainForm()
{
InitializeComponent();
token T1 = new token("if", Color.Blue, new Font("Courier New", 10, FontStyle.Bold));
token T2 = new token("using", Color.Purple, new Font("Courier New", 10, FontStyle.Bold));
token T3 = new token("namespace", Color.Purple, new Font("Courier New", 10, FontStyle.Bold));
token T4 = new token("class", Color.Blue, new Font("Courier New", 10, FontStyle.Bold));
token T5 = new token("public", Color.Blue, new Font("Courier New", 10, FontStyle.Bold));
token T6 = new token("private", Color.Blue, new Font("Courier New", 10, FontStyle.Bold));
token T7 = new token("new", Color.Blue, new Font("Courier New", 10, FontStyle.Bold));
token T8 = new token("void", Color.Blue, new Font("Courier New", 10, FontStyle.Bold));
token T9 = new token("(", Color.Orange, new Font("Courier New", 10, FontStyle.Bold));
token T10 = new token(")", Color.Orange, new Font("Courier New", 10, FontStyle.Bold));
token T11 = new token("{", Color.Orange, new Font("Courier New", 10, FontStyle.Bold));
token T12 = new token("}", Color.Orange, new Font("Courier New", 10, FontStyle.Bold));
token T13 = new token("==", Color.Red, new Font("Courier New", 10, FontStyle.Bold));
token[] tokenArray = { T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13 };
ExtRichText = new extendedRichText(tokenArray , 50);
this.Controls.Add(ExtRichText);
}
Implementace - celý kód
Celý "necenzurovaný" a nerozstříhaný kód třídy vypadá
následovně:
» Rozbalit/sbalit kód «
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Collections;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows.Forms;
using System.ComponentModel;
using System.Runtime.InteropServices;
namespace SyntaxHighlighter
{
public class token
{
private String tokenValue;
private Color tokenColor;
private Font tokenFont;
public token(String tValue, Color tColor, Font tFont)
{
this.tokenValue = tValue;
this.tokenColor = tColor;
this.tokenFont = tFont;
}
public String getValue()
{
return this.tokenValue;
}
public Color getColor()
{
return this.tokenColor;
}
public Font getFont()
{
return this.tokenFont;
}
}
public class extendedRichText : RichTextBox
{
// Klicova slova
private token[] keywords;
// Tridu UndoClass definujeme nize
private List<UndoClass> History;
// UndoPointer ukazuje na konkretni pozici v seznamu historie
private int UndoPointer = 0;
// Promenne spojene s updatovanim platna (prekreslenim)
private int updating = 0;
private int oldEventMask = 0;
// Jednoducha trida pro ulozeni historie uprav
private class UndoClass
{
public string rtfText;
public int posText;
public UndoClass(int pos, string str)
{
this.rtfText = str;
this.posText = pos;
}
}
// Konstruktor tridy
public extendedRichText(token[] Tokens, int UndoSize)
{
this.SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer, true);
this.Multiline = true;
this.WordWrap = false;
this.AcceptsTab = true;
this.DetectUrls = false;
this.ScrollBars = RichTextBoxScrollBars.ForcedBoth;
this.Dock = DockStyle.Fill;
// Velikost historie uprav se definuje v konstruktoru
History = new List<UndoClass>(UndoSize);
// Seznam klicovych slov se definuje v konstruktoru
this.keywords = Tokens;
// Handlery pro zmenu textu a pro zachytavani klaves
this.TextChanged += new EventHandler(TextChangedEvent);
this.KeyDown += new KeyEventHandler(KeyPressEvent);
this.StoreNewUndo();
}
private void KeyPressEvent(object sender, System.Windows.Forms.KeyEventArgs e)
{
if (e.Control && (e.Shift || e.Alt) && (e.KeyValue == 'Z' || e.KeyValue == 'Y'))
return;
if (e.KeyCode == Keys.Enter ||
e.KeyCode == Keys.Delete ||
e.KeyCode == Keys.Back ||
e.KeyValue == ' ' ||
e.KeyValue == ',' ||
e.KeyValue == '.' ||
e.KeyValue == ';' ||
e.KeyValue == '}' ||
e.KeyValue == '{' ||
e.KeyValue == '(' ||
e.KeyValue == ')')
{
StoreNewUndo();
}
}
private int StoreNewUndo()
{
if (History.Count > UndoPointer)
{
History.RemoveRange(UndoPointer, History.Count - UndoPointer);
}
History.Add(new UndoClass(this.SelectionStart, this.Rtf));
if (History.Count > 80)
{
History.RemoveAt(0);
UndoPointer = History.Count;
}
else
{
UndoPointer++;
}
return UndoPointer;
}
public void RedoEvent()
{
if (UndoPointer < 50 && UndoPointer < History.Count - 1)
{
this.BeginUpdate();
UndoPointer++;
this.Rtf = History[UndoPointer].rtfText;
this.SelectionStart = History[UndoPointer].posText;
this.EndUpdate();
this.Refresh();
}
}
public void UndoEvent()
{
if (UndoPointer > 0)
{
if (UndoPointer == History.Count)
{
StoreNewUndo();
if (UndoPointer > 0) UndoPointer--;
}
this.BeginUpdate();
UndoPointer--;
this.Rtf = History[UndoPointer].rtfText;
this.SelectionStart = History[UndoPointer].posText;
this.EndUpdate();
this.Refresh();
}
}
#region Windows API Balast
private const int WM_SETREDRAW = 0x0B;
private const int EM_SETEVENTMASK = 0x0431;
[DllImport("user32", CharSet = CharSet.Auto)]
private static extern int SendMessage(HandleRef hWnd, int msg, int wParam, int lParam);
#endregion
public void BeginUpdate()
{
this.TextChanged -= this.TextChangedEvent;
++updating;
if (updating > 1)
return;
oldEventMask = SendMessage(new HandleRef(this, Handle), EM_SETEVENTMASK, 0, 0);
SendMessage(new HandleRef(this, Handle), WM_SETREDRAW, 0, 0);
}
public void EndUpdate()
{
this.TextChanged += this.TextChangedEvent;
--updating;
if (updating > 0)
return;
SendMessage(new HandleRef(this, Handle), WM_SETREDRAW, 1, 0);
SendMessage(new HandleRef(this, Handle), EM_SETEVENTMASK, 0, oldEventMask);
// this.Refresh();
}
// Napred si napiseme metodu pro tokenizaci textu dle znaku
private string[] tokenize(string Line)
{
// Do regularniho vyrazu zahrneme vsechny znaky, ktere mohou
// potencialne oddelovat jednotlive tokeny (znaky " ", ",", ...) ...
Regex r = new Regex("([ \\t{}():;,\"'])");
return r.Split(Line);
}
public void ParseLine(string Line)
{
// ...a roztrhneme text podle techto znaku na jednotlive tokeny
String[] tokens = this.tokenize(Line);
string token;
// Kazdy z techto tokenu potom porovname se seznamem tokenu, ktere
// specifikujeme v konstruktoru tridy...
foreach (string token2 in tokens)
{
// Osetreni v pripade, ze text "Line" obsahuje newline
// v takovem pripade je nutne ho odstranit, nebot:
// "\r\nif" != "if" apod.
if (token2.StartsWith("\r"))
{
if (token2 == "\r\n" || token2 == "\n" || token2 == " " || token2 == "\t")
{
this.SelectedText = token2;
continue;
}
token = token2.Replace("\r\n", "");
}
else
{
token = token2;
}
// Nastaveni vychozi barvy
this.SelectionColor = Color.Black;
this.SelectionFont = new Font("Courier New", 10, FontStyle.Regular);
// Porovnani aktualniho tokenu s tokeny jazyka, nastaveni barvy
// a typu pisma v pripade shody...
for (int i = 0; i < keywords.Length; i++)
{
if (keywords[i].getValue() == token)
{
this.SelectionColor = keywords[i].getColor();
this.SelectionFont = keywords[i].getFont();
break;
}
}
// A prepiseme vybrany text...
this.SelectedText = token2;
}
}
public void TextChangedEvent(object sender, EventArgs e)
{
if (this.SelectionStart < 0) return;
int start, end;
// zabraneni tomu aby se prekreslil vice nez jeden radek
for (start = this.SelectionStart - 1; start > 0; start--)
{
if (this.Text[start] == '\n')
{
start++;
break;
}
}
// zabraneni tomu aby se prekreslil vice nez jeden radek
for (end = this.SelectionStart; end < this.Text.Length; end++)
{
if (this.Text[end] == '\n')
{
break;
}
}
// osetreni podteceni pro prvni radek
if (start < 0) start = 0;
// ulozime si text ktery se ma updatovat...
String line = this.Text.Substring(start, end - start);
// vypneme prekreslovani RichTextBoxu
this.BeginUpdate();
// Ulozime si stav vyberu
int selectionStart = this.SelectionStart;
int selectionLength = this.SelectionLength;
// ... a vybereme text, ktery se ma prekreslit
this.SelectionStart = start;
this.SelectionLength = end - start;
// Zavolame "magickou cernou skrinku" ktera
// nam ten text prekresli
this.ParseLine(line);
// Vratime stav vyberu tak, jak byl, vybereme okno do "fokusu" a obnovime prekreslovani (prekreslime)...
this.SelectionStart = selectionStart;
this.SelectionLength = selectionLength;
this.Focus();
this.EndUpdate();
this.Refresh();
}
public new void Paste()
{
this.BeginUpdate();
if (Clipboard.ContainsText(TextDataFormat.Rtf))
{
base.Paste();
}
else
{
this.ParseLine(Clipboard.GetText());
}
this.EndUpdate();
this.Refresh();
}
}
}