C#开发高亮语法编辑器(一)——TextBox ,RichTextBox

C#简单实现高亮语法编辑器(一)
         ——TextBox ,RichTextBox的局限性


一、RichTextBox基本设置
二、实现语法高亮
三、关键字提示
四、实现行号

就简单快速得开发文本编辑器TextBox 最为简单,大家用得也多,缺点是无法实现复杂的操作。RichTextBox虽然是则功能比它强大很多。

TextBox.gif
图 1.1  输入框控件关系



这里要实现以下功能的编辑器:
1、实现语法高亮;
2、关键字提示;
3、行号。

显然TextBox 无法完成我们的任务,虽然都派生自TextBoxBase,但就控制力而言RichTextBox比它优秀很多。这里选用RichTextBox尝试开发。

注:以下只讨论简单开发,不考虑复杂的关键字查找机制。

一、RichTextBox基本设置

这里先建立一个工程,建立窗体Form1。
可以简单添加RichTextBox控件,可以在Form1_Load中建立。代码如下:
 1             this.WindowState = System.Windows.Forms.FormWindowState.Maximized;
 2 
 3             RichTextBox rich = new RichTextBox();
 4             rich.Multiline = true;
 5             rich.Height = this.Height - 100;
 6             rich.Width = this.Width - 100;
 7             rich.Left = 40;
 8             rich.Top = 40;
 9             rich.WordWrap = true;
10             rich.Text = "12345678";
11             rich.ScrollBars = RichTextBoxScrollBars.ForcedVertical;
12             this.Controls.Add(rich);

这样就建立了简单的RichTextBox,宽度和高度都设置了。没有做Form1窗体缩放的处理。

二、实现语法高亮

在RichTextBox里实现语法高亮还是非常简单的。可以使用
1             rich.Select(0,1);
2             rich.SelectionFont = new Font("宋体"12, (FontStyle.Regular));
3             rich.SelectionColor = Color.Blue;
意思是,先选择第一个字母,按上面的设置,选择到了数字‘1’,然后设置这个字的字体大小,再设置字的颜色。

如果对关键字进行处理(这里只处理光标向后流动的情况)
首先添加输入事件
1        rich.KeyDown += new KeyEventHandler(rich_KeyDown);   //这一行添加到Form1_Load中
2 
3         void rich_KeyDown(object sender, KeyEventArgs e)
4         {
5             //throw new Exception("The method or operation is not implemented.");
6         }

建立关键字
 1         public static List<string> AllClass()
 2         {
 3             List<string> list = new List<string>();
 4             list.Add("function");
 5             list.Add("return");
 6             list.Add("class");
 7             list.Add("new");
 8             list.Add("extends");
 9             list.Add("var");
10             return list;
11         }

当KeyDown事件发生时,向前查找
 1         //返回搜索字符
 2         public static string GetLastWord(string str,int i)
 3         {
 4             string x = str;
 5             Regex reg= new Regex(@"\s+[a-z]+\s*",RegexOptions.RightToLeft);
 6             x = reg.Match(x).Value;
 7 
 8             Regex reg2 = new Regex(@"\s");
 9             x = reg2.Replace(x, "");
10             return s;
11         }

 1         void rich_KeyDown(object sender, KeyEventArgs e)
 2         {
 3             RichTextBox rich = (RichTextBox)sender;
 4             //throw new Exception("The method or operation is not implemented.");
 5             string s = GetLastWord(rich.Text, rich.SelectionStart);
 6 
 7             if (AllClass().IndexOf(s) > -1)
 8             {
 9                 MySelect(rich, rich.SelectionStart, s, Color.CadetBlue, true);
10             }
11         }

 1         //设定颜色
 2         public static void MySelect(System.Windows.Forms.RichTextBox tb, int i, string s, Color c,bool font)
 3         {
 4             tb.Select(i - s.Length, s.Length);
 5             tb.SelectionColor = c;
               //是否改变字体
 6             if(font)
 7                 tb.SelectionFont = new Font("宋体"12, (FontStyle.Bold));
 8             else
 9                 tb.SelectionFont = new Font("宋体"12, (FontStyle.Regular));
                 //以下是把光标放到原来位置,并把光标后输入的文字重置
10             tb.Select(i,0);
11             tb.SelectionFont = new Font("宋体"12, (FontStyle.Regular));
12             tb.SelectionColor = Color.Black;
13         }

这样就完成了高亮工作。

三、关键字提示

实现关键字提示也是在KeyDown中实现,在提示字种搜索GetLastWord返回的文字,如果前半部分匹配。那么就建立ListBox控件。
 1       void tb_KeyDown(object sender, KeyEventArgs e)
 2         {
 3             RichTextBox tb = (RichTextBox)sender;
 4             if (//条件搜索到匹配字符)
 5             {
 6                 //搜索ListBox是否已经被创建
 7                 Control[] c = tb.Controls.Find("mylb"false);
 8                 if (c.Length > 0)
 9                     ((ListBox)c[0]).Dispose();  //如果被创建则释放
10 
11                 ListBox lb = new ListBox();
12                 lb.Name = "mylb";
13                 lb.Items.Add("asdasdasd");
14                 lb.Items.Add("asdasdasd");
15                 lb.Items.Add("asdasdasd");
16                 lb.Items.Add("asdasdasd");
17                 lb.Items.Add("asdasdasd");
18                 lb.Items.Add("asdasdasd");
19                 lb.Items.Add("asdasdasd");
20                 lb.Show();
21                 lb.TabIndex = 100;
22                 lb.Location = tb.GetPositionFromCharIndex(tb.SelectionStart);
23                 lb.Left += 10;
24                 tb.Controls.Add(lb);
25             }
26         }

当然,另外一面,如果创建ListBox,而又在RichTextBox 点击了鼠标也去释放。
1         void rich_MouseClick(object sender, MouseEventArgs e)
2         {
3             RichTextBox tb = (RichTextBox)sender;
4             Control[] c = tb.Controls.Find("mylb"false);
5             if (c.Length > 0)
6                 ((ListBox)c[0]).Dispose();
7         }

当然还得在Form1_Load里注册事件
1 rich.MouseClick += new MouseEventHandler(rich_MouseClick);

然后设置ListBox 被选择后用被选择的关键字替换前文搜索到的字符。

下面我们来看看实现行号。

四、实现行号

这个是RichTextBox 唯一令我遗憾的地方,居然无法实现行号问题。为什么呢?我首先想到的是自己画。用rich.CreateGraphics()来画。但是,由于画的时候发生在窗体被创建时,所以画不成功,而被RichTextBox 本身的绘制给覆盖了。

然后我选择了在里面添加Label控件

 1            Label l = new Label();
 2             l.Name = "l";
 3             l.Top = 0;
 4             l.TextAlign = ContentAlignment.TopRight;
 5             l.Width = 40;
 6             l.Text = "1";
 7             l.Font = new Font("宋体"12, (FontStyle.Regular));
 8             l.Height = this.Height;
 9             l.BackColor = Color.Gray;
10             l.BorderStyle = BorderStyle.None;
11             rich.Controls.Add(l);
12 
13             rich.SelectionIndent = 40;

 rich.SelectionIndent = 40;是把光标对齐到左边距40的位置,防止光标被Label覆盖。

实现编号还不是太难。麻烦出在如何让Lable能跟随RichTextBox 的滚动条滚动。不说实现的 细节,我就假设,如果滚动条向上滚,那么Lable的Top属性增加,反之则减少。但是,RichTextBox 居然无法对ScollBar进行监测。

根本每办法知道滚动条滚动了多少位置,甚至都没办法知道滚动条滚动的方向。

尝试去除滚动条,然后之间添加新的滚动条
 1             VScrollBar vs = new VScrollBar();
 2             //vs.Dock = DockStyle.Right;
 3             vs.Name = "vs";
 4             vs.Maximum = 0;
 5             vs.Minimum = 0;
 6             vs.MaximumSize = new Size(0,0);
 7             vs.Top = 0;
 8             vs.Left = rich.Parent.Width - 100 -22;
 9             vs.Height = rich.Parent.Height - 100 -1;
10             vs.Value = 0;
11             vs.Scroll += new ScrollEventHandler(vs_Scroll);
12             
13             rich.Controls.Add(vs);

但是非常难于实现同步滚动,位置很难控制。这个就是目前遇到的RichTextBox 的最大局限性了,非常遗憾,无法开发出这个功能。

birdshover
http://www.cnblogs.com/birdshover/
2007年1月30日
posted @ 2007-01-30 23:48 Birdshover 阅读(6668) 评论(26)  编辑 收藏 所属分类: WinForm

  回复  引用  查看    
#1楼 2007-01-31 00:14 | yunhuasheng      
挺好的,值得学习!!
  回复  引用  查看    
#2楼 2007-01-31 00:20 | Jeffrey Zhao      
学习一下:)
  回复  引用  查看    
#3楼 2007-01-31 01:13 | xzwplus      
如果我没有记错的话,OnPaint事件可以画吧?
ps:你这样简单的设置SelectionLength,SelectionColor 会存在一个问题,就是在文本内容较多的时候,会看到关键字被选中的过程。
  回复  引用  查看    
#4楼 2007-01-31 01:18 | xzwplus      
最好的做法是继承RichTextBox,重载新类的Paint方法。

并且在设置SelectionLength的时候,禁止控件的重绘过程,这样才不会出现被语法高亮的文本有一个突然选中的过程。

以下2个方法将会对你解决这一问题有很大的帮助.
[DllImport("user32")]
private static extern int SendMessage(IntPtr hwnd, int wMsg, int wParam, IntPtr lParam);
private const int WM_SETREDRAW = 0xB;

//停止控件的重绘
private void BeginPaint()
{
SendMessage(yourRichTextBox.Handle, WM_SETREDRAW, 0, IntPtr.Zero);
}

//允许控件重绘.
private void EndPaint()
{
SendMessage(yourRichTextBox.Handle, WM_SETREDRAW, 1, IntPtr.Zero);
yourRichTextBox.Refresh();
}

  回复  引用  查看    
#5楼 2007-01-31 01:23 | xzwplus      
ps:关于/* */,//等注释标记,还有字符串匹配("")的着色,难度会上升的。
例如,如何界定在字符串中出现的/*...*/, //等字符?如何在注释中出现的"..."识别为注释而不是字符串?
  回复  引用  查看    
#6楼 2007-01-31 08:39 | Jeffers Yuan      
很高兴看到你的这篇文章,帮助很大,谢谢!
下面能否考虑自动完成功能?
  回复  引用  查看    
#7楼 [楼主]2007-01-31 10:31 | BirdsHover      
@xzwplus
如何查找关键字复杂一点也该能办到。

我也尝试了重绘
但是发现要在Form1 OnPaint事件的时候才能重画
而我本意是要把控件重写的,所以没有使用

我会试下API的效果,非常感谢
  回复  引用  查看    
#8楼 2007-01-31 14:25 | 笨小苏      
我的编译器,做语法加亮用的也是RichTextBox
不过我是解析后再生成代码
你可以考虑一下,用正则表达式作key 比如匹配一个字符串 可以
Add(" \"[^\"*]\"",Color.Blue);

行号我用双richtextbox做,不过也没解决同步问题
我准备得空用画布重写一个

  回复  引用  查看    
#9楼 2007-01-31 14:39 | AlleNny      
这样写法恐怕performance不会太好,特别是语法复杂的时候。
  回复  引用  查看    
#10楼 [楼主]2007-01-31 15:00 | BirdsHover      
@AlleNny
性能要看业务怎么设计了,每次回溯前文不多的话肯定是没有问题的。算法在里面将担任很重要的角色了。
  回复  引用  查看    
#11楼 2007-01-31 15:46 | rex,xiang      
在粘贴,剪切,覆盖.插入,删除等操作的时候,你应该知道是哪些文本改变了,这个时候,你只需要重新解析改变的部分+之前/之后的一个词就可以了,不必再全文解析.

ps:俺还是上面的xzwplus,改名咯!
  回复  引用  查看    
#12楼 2007-01-31 19:48 | Anders.Zhao      
参考一下SharpDevelop是怎么做的...
  回复  引用  查看    
#13楼 [楼主]2007-01-31 23:10 | BirdsHover      
@Anders.Zhao
你不说我还忘了有个那玩意了,呵呵
  回复  引用  查看    
#14楼 2007-02-01 03:53 | lin-zhang      
正好我最近在读 SharpDevelop 源码,希望博主多与我交流 :)

SharpDevelop浅析_3_Internationalization & TextEditor 国际化、文档编辑器、语法高亮显示…… (http://www.cnblogs.com/michael-zhang/articles/636381.html)

SharpDevelop浅析_序 (http://www.cnblogs.com/michael-zhang/articles/621144.html)
  回复  引用    
#15楼 2007-02-01 06:02 | EnHuang [未注册用户]
Line number can be done by create a non-client area. You need to call native windows functions to do that. I've just finished a project with that.
  回复  引用    
#16楼 2007-02-01 06:04 | EnHuang [未注册用户]
@rex,xiang
在粘贴,剪切,覆盖.插入,删除等操作的时候,你应该知道是哪些文本改变了,这个时候,你只需要重新解析改变的部分+之前/之后的一个词就可以了,不必再全文解析.

How? Can you come up with a high performance algorithm plz. I am trying to improve my performance.
  回复  引用  查看    
#17楼 2007-02-01 09:04 | anchky      
mark
  回复  引用  查看    
#18楼 2007-05-02 20:37 | 桂圆      
期待后续文章
支持
  回复  引用    
#19楼 2007-07-13 16:01 | Holinz [未注册用户]
回EnHuang :Line number can be done by create a non-client area. You need to call native windows functions to do that. I've just finished a project with that.
是使用API函数吧,具体哪个呢?哦还有介绍下怎么做哦。谢谢!
  回复  引用    
#20楼 2007-07-13 16:02 | Holinz [未注册用户]
哦。可以回复到:lhq545@yahoo.com.cn
  回复  引用  查看    
#21楼 2007-08-21 17:35 | jeffersyuan      
如何实现匹配字符?
(//条件搜索到匹配字符)
  回复  引用    
#22楼 2007-11-30 13:02 | chanhlr2 [未注册用户]
四、实现行号
Private Sub runIndexScroll()
Dim firstIndex As Integer
Dim lastIndex As Integer
Dim firstLine As Integer
Dim lastLine As Integer
Dim intPageLineCnt As Integer

intPageLineCnt = CInt((rtbFirst.Height - 6) / (rtbFirst.Font.Height + 4))

firstIndex = rtbFirst.GetCharIndexFromPosition(New Point(0, 0))
firstLine = rtbFirst.GetLineFromCharIndex(firstIndex)
lastIndex = rtbFirst.GetCharIndexFromPosition(New Point(ClientRectangle.Width, ClientRectangle.Height))
lastLine = rtbFirst.GetLineFromCharIndex(lastIndex)

intPoint = 0
If firstLine = 0 Then
If lastLine - firstLine = intPageLineCnt + 1 Then
firstLine = -1
End If
End If
For i = 1 To firstLine + 1
intPoint = intPoint + Len(i.ToString) + 1
Next

rtbIndex.SelectionStart = intPoint
rtbIndex.ScrollToCaret()
End Sub
  回复  引用    
#23楼 2008-01-15 16:26 | 666 [未注册用户]
int word_position = 0;
List<string> keyword = new List<string>();

private void richTextBox1_KeyDown(object sender, KeyEventArgs e)
{
if (e.KeyValue == 32||e.KeyValue==13)
{
RichTextBox rich = (RichTextBox)sender;
//throw new Exception("The method or operation is not implemented.");
if (word_position <= rich.Text.Length)
{
string s = GetLastWord(rich.Text, word_position);

if (keyword.IndexOf(s) > -1)
{
MySelect(rich, rich.SelectionStart, s, Color.CadetBlue);
}
}
word_position = rich.SelectionStart;

}

}
public static string GetLastWord(string str,int i)
{
string x = str.Substring(i);

Regex reg = new Regex(@"[\s:.]+[a-zA-Z]+[\s:.]*", RegexOptions.RightToLeft);
x = reg.Match(x).Value;

Regex reg2 = new Regex(@"[\s:.]");
x = reg2.Replace(x, "");
return x;
}

public static void MySelect(System.Windows.Forms.RichTextBox tb, int i, string s, Color c)
{
tb.Select(i - s.Length, s.Length);
tb.SelectionColor = c;
//以下是把光标放到原来位置,并把光标后输入的文字重置
tb.Select(i,0);
tb.SelectionColor = Color.Black;
}



public void Read_text()
{
int current_position = 0; string current_word;
while (current_position != -1)
{
current_word= rich.Text.Substring(current_position);
Regex reg = new Regex(@"[\s:.]+[a-zA-Z]+[\s:.]*");
current_word = reg.Match(current_word).Value;
Regex word = new Regex(@"[\s:.]");
current_word = word.Replace(current_word, "");
current_position = rich.Find(current_word, current_position, RichTextBoxFinds.MatchCase)+current_word.Length;
if (keyword.IndexOf(current_word) > -1)
{
MySelect(rich,current_position, current_word, Color.Blue);
}
if (current_position == rich.Text.Length - current_word.Length)
{
break;
}


}

}

private void button1_Click(object sender, EventArgs e)
{
Read_text();
}

private void Form1_Load(object sender, EventArgs e)
{

keyword.Add("function");
keyword.Add("Return");
keyword.Add("GoTo");
keyword.Add("new");
keyword.Add("Dim");
keyword.Add("Sub");
keyword.Add("Class");
keyword.Add("End");
keyword.Add("As");
keyword.Add("Public");
keyword.Add("Private");
keyword.Add("For");
keyword.Add("Integer");
keyword.Add("If");
keyword.Add("Try");


}

上述代码是我根据楼主的思想建的,不仅可以在编写时实现高亮,也可对已有text高亮化
  回复  引用    
#24楼 2008-01-15 16:27 | 666 [未注册用户]
楼主的程序不能识别大写
  回复  引用    
#25楼 2008-01-29 09:59 | 高飛 [未注册用户]
取得text的資料後再計算行號,但是當內容過多時效能可能會被影響

private int getTextLine()
{
string str = this.EditProg.Text;
int start = 0;
int at = 0;
int end = this.EditProg.SelectionStart;
int count = 0;
int lines = 1;


while ((start <= end) && (at > -1))
{

count = end - start;
at = str.IndexOf("\r\n", start, count);

if (at == -1 )
{
break;
}

start = at + 1;
lines++;
}
return lines;
}


标题  
姓名  
主页
Email (博主才能看到) 
验证码 *  看不清,换一张 [登录][注册]
内容(请不要发表任何与政治相关的内容)  
  登录  使用高级评论  新用户注册  返回页首  恢复上次提交      
该文被作者在 2007-02-01 11:22 编辑过


相关链接: