RichEdit显示行号

 


大家好啊,好久没有出原创了,最近在学习汇编语言,学到RichEdit控件时想起很久以前
自己就想实现像很多文本编辑器那样能显示行号的功能,当时是想用VB来实现的,可惜程序半途
夭折了,也许是当时菜吧 :)
其实大家别误会了我说的多种语言实现有多少(嘻嘻,为了点击率),这里只用了3种语言而已,
用Win32Asm、C语言方式的SDK和VB6。我并没有用C++实现,其实C++也学过一下,只是又因为没有
恒心而改行汇编了=)。我之所以用3种语言我就是想对一些朋友证明语言只是工具,创新才是灵魂。

各种环境介绍:
开发平台: windows2000+sp4
C: VC6.0英文版+sp5
VB: VB6.0中文企业版+sp5
Win32Asm: RadAsm2.2汉化版+MASMV8.2

我们还是废话少说,现在来详细探讨一下原理,VB6的实现不同,我们稍候再说。我们将会探讨
有关Windows消息系统、窗口的子类技术、Gdi函数的使用,如果您对这些部分闻所未闻的话就需要
补充这方面的知识了,《Windows 程序设计》里都有这些内容介绍。首先讲解一下程序流程:
1:创建主窗口,加载RichEdit的动态链接库。
2:在WM_CREATE消息中创建RichEdit窗口。
3:设置RichEdit的左编辑宽度,为显示行号留出空间,
向RichEdit发送EM_SETMARGINS即可实现。
4:在WM_CREATE消息中设置新的RichEdit的窗口地址,并保存原有的窗口地址。
5:此时程序已正常地显示在屏幕中了。
6:每当windows向RichEdit窗口发送WM_PAINT消息时我们就先用自己的窗口程序
处理此消息。在这里显示行号。
7:把其它消息返回给RichEdit的默认窗口程序进行处理。
8:当再有WM_PAINT消息时重复第六步。直到程序结束。

程序主要用到了窗口的子类技术,现在就来讲解一下,Windows程序界面是由一个个窗口组合而成的。
按钮是一个窗口,文本框是一个窗口,列表框也是一个窗口。而每一个窗口都有一些基本的功能,
实现这些功能和管理这些窗口的程序就叫做窗口处理程序,例如当你按下一个按钮的时候,内置的
按钮窗口程序就会通知程序有人按下了按钮,这时程序就有事要做了。这个通知程序有消息到的功能
就是按钮的窗口程序做的。但平时我们编程的时候是感觉不到这些程序在幕后为我们做了这些事情。
因为我们只要收到由按钮发送过来的WM_COMMAND消息就知道用户按下了按钮,我们实现这个按钮逻辑就行了。
有时我们需要加强某控件的功能,例如做一个文本框,它只能输入16进制的数,我们知道一个文本框
只要求输入数字是很容易的,只要在风格上加上ES_NUMBER就可以,但输入16进制数值就不行了,因为
指定了ES_NUMBER后就无法输入A--F,不指定的话用户就可以输入0--9和A--F之外的东西。
我们知道每当在文本框中键入一个字符时,Windows会向文本框发送一个WM_CHAR消息,我们平时不用理
这个消息,因为文本框的内置窗口程序会自动处理这种情况。如果我们可以自己处理WM_CHAR消息的话
就可以自己设定用户只能输入那些字符了,为了扩展文本框中的这个功能,我们可以先用自己的窗口程序
检查WM_CHAR消息来进行处理,其它不关心的消息可以交回给默认的窗口程序处理,以达到我们的目的。
这个技术就是窗口的子类技术。我们可以做一个形象的比喻:小明每天上学都会带一个苹果和一个雪梨
回学校,带水果回学校的过程是一样的。可是小强突然有一天想吃小明的苹果,于是它就在小明上学的
路上等着小明经过,小强说“打劫!要命还是要苹果?”,小明毫无疑问地留下了苹果给小强,只带
着一个雪梨回学校了。子类技术的行为有点像小强,不过这时打劫的是Windows发给文本框的
WM_CHAR消息(我们要的苹果)至于其它我们不关心的消息(雪梨)我们直接给回文本框默认的窗口程序处理。
事实上Windows的任何窗口都可以实现子类化。但对于自己创建窗口类的来说,我们本身就有100%的控制权。

我们具体来看看怎样实现,首先我们应该先获取原有的窗口地址。可以用GetWindowLong函数来实现。
原有的窗口地址 = GetWindowLong(窗口句柄, GWL_WNDPROC)。
保留了原有的窗口地址后我们就可以设置新的窗口地址,可以用SetWindowLong来实现,有趣的是这个函数
的返回值正是原窗口程序的地址,所以我们可以用一个函数实现,程序中这样使用:

//设置新的窗口地址,lpEditProc为RichEdit的内置窗口处理地址。
(long)lpEditProc = SetWindowLong( hWinEdit, GWL_WNDPROC, (long)&SubEditProc );

SubEditProc正是我们设置的新的窗口程序,程序中用这个函数预先处理WM_PAINT消息。
SubEditProc函数必须遵守一定的参数约定,也就是必须把Windows发送给文本框的消息参数原样放回给
原有的窗口程序处理。程序中SubEditProc的函数是这样的:

long SubEditProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam )
{
//功能:截获RichEdit的WM_PAINT消息,用于当RichEdit刷新时显示行号。
//参数:
// hWnd, uMsg, wParam, lParam 都为原有Windows发给RichEdit的消息和参数
//返回值:没有
//说明:当消息处理完毕后必须把消息回送给默认的RichEdit的窗口处理过程,
// lpEditProc为RichEdit的旧窗口地址。

PAINTSTRUCT ps;

if( uMsg == WM_PAINT )
{
CallWindowProc( lpEditProc, hWnd, uMsg, wParam, lParam);
BeginPaint( hWnd, &ps);
//下列函数完成显示行号的功能
ShowLineNum( hWinEdit );

return EndPaint( hWnd, &ps);

}
return CallWindowProc( lpEditProc, hWnd, uMsg, wParam, lParam);
}

程序中只处理WM_PAINT消息,其它的消息还是给回原有的窗口程序进行处理,即调用CallWindowProc函数。
其中SubEditProc的参数必须是上面的形式。其实我们可以预先处理任何Windows发给文本框中的消息。
程序中处理WM_PAINT消息是因为每当在文本框中键入一个字符,Windows都会发送该消息给文本框来进行
更新。我们真需要在更新前显示文本的行号,所以只处理这个消息。
总的来说实现子类技术有下列几个步骤:
1:先编写自己的用于处理消息的窗口程序。
2:当程序初始化时(即WM_CREATE消息到来)保留想要截获消息的窗口的窗口程序地址
3:为窗口设置新的窗口地址,即第一步我们编写的函数的地址。
4:在自己的窗口程序中处理我们感兴趣的消息,其它消息返回给默认的窗口程序处理。

好了,子类技术就介绍到这里了,如果你还是不明白的话就看看源代码,代码可以说明一切。我们
现在来探讨如何实现显示行号,这里涉及到比较常用的GDI函数,但要求读者有Windows图形编程的
基本功。显示行号当然要知道当前文本有多少行了,直接向文本框发送EM_GETLINECOUNT即可获取
文本的总行数。接下来就是在每一行的最左边显示行号,程序首先建立一个与文本框兼容的Dc,然后
在这个Dc上用TextOut函数输出行号。就这样简单?不我们还有一个问题,如果文本很多,窗口容纳不下
的话在窗口中自然有滚动条,当滚动文本的时候问题就来了,我们显示的行号不会跟着滚动而造成明明当前
是在第三行,而行号还时死死地显示为第一行。当文本滚动时候行号应该跟着滚动,怎样实现呢?这一点
微软帮我们想到了,可以向文本框发送EM_GETFIRSTVISIBLELINE 来获取文本框中第一个可见的行号。这样
显示行号就不会老是从第一行开始,但文本滚到第三行时,显示行号就显示为第三行。大家可以在程序去掉

FirstLine = SendMessage( hEdit, EM_GETFIRSTVISIBLELINE, 0, 0 );

这一行来试试,没有这个FirstLine行号是不会跟着文本滚动而动态显示的。显示行号的函数中涉及到的GDI
函数操作我就不一一介绍了,函数中已有详细的注释,相信有基础的朋友都能看懂。这里贴出实现的C代码:

void ShowLineNum( HWND hEdit )
{
/*
功能:显示文本的总行数
参数:
hEdit:要显示行号的文本框,普通的Edit控件没有测试过,这里只用RichEdit

返回值:没有。
--------------------------------------------------------------------------------
*/
RECT     ClientRect;     //RichEdit的客户区大小
HDC      hdcEdit;         //RichEdit的Dc(设备环境)
HDC      hdcCpb;          //与RichEdit兼容的Dc
HBITMAP  hdcBmp;      //RichEdit兼容的位图dc
int      CharHeight;      //字符的高度
int      chHeight;        //字符的高度,常量
int      FirstLine;       //文本框中的第一个可见行的行号。
int      ClientHeight;    //RichEdit的客户区高度
int      LineCount;       //文本的总行数
char     countBuf[10];   //显示行号的缓冲区
CHARFORMAT     CharFmt;  //RichEdit中的一个结构,用于获取字符的一系列信息,这里只用它来获取字符高度

//获取RichEdit的Dc
hdcEdit = GetDC( hEdit );
GetClientRect( hEdit, &ClientRect);
//获取RichEdit的客户区高度
ClientHeight = ClientRect.bottom - ClientRect.top;
//创建与RichEdit兼容的Dc
hdcCpb = CreateCompatibleDC( hdcEdit );
//创建与RichEdit兼容的位图Dc,我们用来显示行号用的。
hdcBmp = CreateCompatibleBitmap( hdcEdit, 40, ClientHeight );
//将位图dc选入RichEdit环境中
SelectObject( hdcCpb, hdcBmp );
//填充显示行号dc的背景颜色。大家可以试试其它颜色
FillRect( hdcCpb, &ClientRect, CreateSolidBrush(0x8080ff) );
SetBkMode( hdcCpb, TRANSPARENT );
//获取当前RichEdit文本中的总行数
LineCount = SendMessage( hEdit, EM_GETLINECOUNT, 0, 0 );

RtlZeroMemory( &CharFmt, sizeof(CharFmt) );
CharFmt.cbSize = sizeof( CharFmt );
SendMessage( hEdit, EM_GETCHARFORMAT, TRUE, (long)&CharFmt );
//获取字符高度,以英寸为单位,需转化为磅,只要除以20就得到磅尺寸。
CharHeight = CharFmt.yHeight / 20;
chHeight = CharHeight;
CharHeight = 1;
//设置显示行号的前景色
SetTextColor( hdcCpb, 0x000000 );
//获取文本框中第一个可见的行的行号,没有这个行号,显示不会跟着文本的滚动而滚动。
FirstLine = SendMessage( hEdit, EM_GETFIRSTVISIBLELINE, 0, 0 );
FirstLine++;
//在位图dc中循环输出行号
while( FirstLine <= LineCount )
{
    TextOut( hdcCpb, 1, CharHeight, countBuf, wsprintf( countBuf, TEXT("%4u"), FirstLine++ ));
    CharHeight += chHeight + 4;
    if( CharHeight > ClientHeight ) break;
}
//将已"画好"的位图真正"贴"到RichEdit中
BitBlt( hdcEdit, 0, 0, 40, ClientHeight, hdcCpb, 0, 0, SRCCOPY );
DeleteDC( hdcCpb );
ReleaseDC( hEdit, hdcEdit );
DeleteObject( hdcBmp );

}

VB6中的实现:
这里主要是为了做如何显示文本行号的实例。这里并没有像Win32Asm和SDK方式那样直接在
Edit控件里画出行号,因为在VB中做这些事情实在太复杂而且不时有莫名其妙的问题发生。
动不动就会使VB整个开发环境崩溃,所以采取了直接用Label控件来显示行号,这样方便
多了。其它功能是完全一样的。有部分常数定义在VB自带的API浏览器里找不到,我是从
VC的头文件中找出来的。还有一点是当文本框中的字体大小改变时Label控件的字体也要
跟着变才不会出现行号显示不正确的现象。

如果还有哪些地方不明白的可以提出问题,我尽力而为,另外由于我本身技术的缘故,如果文中出现阐述错误
或不妥的地方请大家批评指正。

你可能感兴趣的:(职场,行号,休闲,RichEdit)