感觉上用C#来写这种扩充组件确实比较麻烦。一个调用系统级的API需要用DllImport来封装,比较麻烦。特别是用SendMessage时,一大堆的message id等常量需要查资料。二个就是.net组件封装得太厉害。封装厉害倒不是件什么坏事,但是总得考虑用户重写某些接口的可能,应该将接口多用virtual修饰允许重写吧?
好,牢骚到此结束,现在言归正传。从Richtextbox继承的语法高亮控件已经很多。原理基本一样。有些控件实现了显示行号的功能(本控件也是),可以算是一个亮点吧。下面从几个方面来讲述。
1. 语法高亮
当用户插入一个字符的时候,有些控件的实现会将整个Text扫描一遍,对各个关键字进行处理。这个包含了很多重复的工作,效率有点低。既然内容是控件自身管理,那么就可以断定:除了插入点临近的地方高亮模式被破坏以外,其它的地方都是不需要改变的。因此,只需要对插入点向左、向右(向前和向后太容易混淆弄糊涂)检查相邻的第一个Word是不是关键字就可以了。
Code: ProcessHighlight
/**//// <summary>
/// Process highlight the keywords.
/// This process is devided into 2 parts:
/// first, we process the left neighbor string,
/// second, we process the right neighbor string.
/// </summary>
private void ProcessHighlight()
{
if (string.IsNullOrEmpty(Text))
return;
int oldSelectionStart = this.SelectionStart;
ProcessLeftDirection();
SelectionStart = oldSelectionStart;
SelectionLength = 0;
ProcessRightDirection();
SelectionStart = oldSelectionStart;
SelectionLength = 0;
SelectionColor = this.ForeColor;
} 将关键字高亮也挺简单,就是先选中一段文字,然后设置SelectionColor=Color.Blue。
Code:DoHighlightWord
/**//// <summary>
/// Highlight the identified word.
/// </summary>
/// <param name="start"></param>
/// <param name="word"></param>
private void DoHighlightWord(int start, string word)
{
lock (this)
{
Native.SendMessage(this.Handle, WinMsgs.WM_SETREDRAW, new IntPtr(0), IntPtr.Zero);
SelectionStart = start;
SelectionLength = word.Length;
Native.SendMessage(this.Handle, WinMsgs.WM_SETREDRAW, new IntPtr(1), IntPtr.Zero);
if (Keywords.IndexOf(word) >= 0)
{
SelectionColor = Color.Blue;
}
else
{
SelectionColor = this.ForeColor;
}
}
} 但是这也带来了新的问题,就是必须处理粘贴的文本。本控件在这个版本就没有加入对它的实现。打算在以后的版本再做吧。
2. 显示行号
这个问题要完美地解决还真不简单。我在写代码的时候,想了很多的办法,结果还是没有很好地解决这个问题。
我的做法是,在主TextBox中嵌入一个独立的靠左的TextBox显示行号。为了使TextBox不遮住内容,在插入字符时,设置TextBox的SelectionIndent为一个固定值。当然普通的Textbox是不能用的,需要重写。重写的主要目的是:(1)去掉textBox的光标,让它不像一个可输入的控件。(2)在右边画一条竖的虚线。(3)加入显示行号的逻辑。
去掉textBox的光标的做法是,textBox获得Focus的时候,立即让它失去Focus。代码如下:
void RowNumTextBox_GotFocus(object sender, EventArgs e)
{
Native.SendMessage(this.Handle, WinMsgs.WM_KILLFOCUS, IntPtr.Zero, IntPtr.Zero);
} 现在的情况是,TextBox显示内容,另一个TextBox显示行号。但在滚动内容时,又想让行号跟着滚动。这就有了同步的问题。要说.net封装得太厉害,想要实现某些功能却又不能。比如监听TextBox的滚动条的滚动位置。这个事件没有办法监听,只能从引起滚动条变化的因素入手。在之前,先说明,让TextBox内容自动滚动,可以对某个TextBox发送EM_LINESCROLL事件。看代码:
Code:ScrollRowNumberTextBox
/**//// <summary>
/// When scrolled the text, update the rownumber textbox.
/// </summary>
private void ScrollRowNumberTextBox()
{
Application.DoEvents();
IntPtr topLineOfCode = Native.SendMessage(this.Handle, WinMsgs.EM_GETFIRSTVISIBLELINE, IntPtr.Zero, IntPtr.Zero);
IntPtr topLineOfRowNumber = Native.SendMessage(txtRowNum.Handle, WinMsgs.EM_GETFIRSTVISIBLELINE, IntPtr.Zero, IntPtr.Zero);
int topIndex = topLineOfCode.ToInt32();
int topIndexRowNum = topLineOfRowNumber.ToInt32();
int dx = topIndex - topIndexRowNum;
Logger.Debug(string.Format("topIndex={0},topIndexRowNum={1} dx={2}", topIndex, topIndexRowNum, dx));
if (dx != 0)
{
Native.SendMessage(txtRowNum.Handle, (uint)WinMsgs.EM_LINESCROLL, IntPtr.Zero, new IntPtr(dx));
}
然后重写WndProc方法,当以下事件发生时,调用ScrollRowNumberTextBox滚动行号方法。
WM_VSCROLL
EM_SCROLL
EM_LINESCROLL
WM_MOUSEWHEEL
消息的相关意思可以查阅MSDN。此外,当内容有改变时,也需要调用ScrollRowNumberTextBox并更新行号。
Code: WndProc
protected override void WndProc(ref Message m)
{
switch (m.Msg)
{
case WinMsgs.WM_VSCROLL:
case WinMsgs.EM_SCROLL:
Logger.Debug("WM_VSCROLL");
ScrollRowNumberTextBox();
break;
case WinMsgs.EM_LINESCROLL:
Logger.Debug("EM_LINESCROLL");
if (0 != m.LParam.ToInt32())
{
ScrollRowNumberTextBox();
}
break;
default:
break;
}
base.WndProc(ref m);
switch (m.Msg)
{
case WinMsgs.WM_MOUSEWHEEL:
ScrollRowNumberTextBox();
break;
case WinMsgs.WM_PASTE:
Logger.Debug("after paste");
break;
default:
break;
}
} 3. 代码中有许多直接用SendM种essage来操作控件。这原生的Windows API真是异常地生猛。如果要开发出一个可用的代码编辑器来,还不如用WIN32编程来得直接。最后看看截图。
下载示例:
/Files/qkhh/Editor.zip下载源码:
/Files/qkhh/Editor-Soruce.zip源码请用VS2008(SP1)打开。