如何构建一个简单的文本编辑器?教程






4.26/5 (16投票s)
本文通过构建一个基于WTL对象的简单文本编辑器,来说明ATL/WTL的应用。

引言
文本编辑器是一个常用的应用程序,可以处理文本文件。文本编辑器主要功能包括“打开/保存文件”、“编辑/查看内容”等。在本教程中,我们将介绍如何基于WTL对象构建一个简单的文本编辑器。
如何使用代码?
我们假设读者已经准备好了支持ATL/WTL的Visual C++。如果已经准备好,源代码可以直接作为VC++项目使用。如果没有,读者需要做一些额外的工作来准备开发环境。
- 在您的系统上获取并安装Visual C++程序。Visual C++的版本应高于6.0,因为Visual C++在6.0版本之后提供了内置的完整STL支持。如果您必须使用VC6.0,则需要在系统中安装STL库,例如
STLPort
,并配置您的VC6.0使用STL库。 - 获取并安装与您的Visual C++程序兼容的Windows驱动程序工具包(WDK)。WDK包含一个独立的ATL/MFC实现,这是WTL所必需的。确保WDK的头文件、库文件和可执行文件的路径已包含在Visual C++的环境设置中。
- 获取并安装一个开源的WTL实现。最新的WTL版本是8.1。在WTL安装包中,有几个用于相应VC++版本的安装脚本。
创建一个 WTL 项目
通过WTL项目向导创建C++项目时,我们可以选择文档视图的基类。CEdit
类和CRichEditCtrl
类都可以作为简单文本编辑器的基类。CRichEditCtrl
类是Windows ActiveX控件“richedt20.dll”的WTL封装,而CEdit
是基于Windows API的WTL实现。作为一个简单的文本编辑器程序,CEdit
类已经足够作为起点。我们需要做的是丰富CEdit
的功能,使其成为一个简单的文本编辑器。
打开和保存文本
通过WTL项目向导创建的初始项目提供了一个基本程序框架。作为一个完整的可执行程序,它提供了基本GUI,包括窗口、菜单、工具栏、工作区和状态栏,以及用户友好的自定义程序行为入口。
我们可以在工作区输入文本,但输入结果无法保存,程序也无法打开现有文本文件。原因是默认的文件打开/保存行为为空。我们首先为程序添加文件打开和保存功能,如下所示。
读取文件并将内容显示在CEdit视图中
HANDLE hFile = ::CreateFile(sPath, GENERIC_READ, 0, 0,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN, 0);
if(INVALID_HANDLE_VALUE == hFile) return false;
DWORD dwSizeLow = GetFileSize(hFile, 0);
char* pbBuff = (char*)malloc(dwSizeLow+1);
DWORD pcb = 0;
DWORD dwRet = ::ReadFile(hFile, pbBuff, dwSizeLow, (LPDWORD)&pcb, NULL);
pbBuff[dwSizeLow] = '\0';
::CloseHandle(hFile);
ATLASSERT(::IsWindow(m_hWnd));
::SendMessage(m_hWnd, WM_SETTEXT, 0, (LPARAM)pbBuff);
delete pbBuff;
将CEdit视图中的内容保存到文件中
HANDLE hFile = ::CreateFile(sPath, GENERIC_WRITE,
0, 0, CREATE_ALWAYS, FILE_FLAG_WRITE_THROUGH, 0);
if(INVALID_HANDLE_VALUE == hFile) return false;
ATLASSERT(::IsWindow(m_hWnd));
DWORD dwSizeLow = ::GetWindowTextLength(m_hWnd);
char* pbBuff = (char*)malloc(dwSizeLow+1);
::SendMessage(m_hWnd, WM_GETTEXT, (WPARAM)dwSizeLow, (LPARAM)pbBuff);
pbBuff[dwSizeLow] = '\0';
DWORD pcb = 0;
DWORD dwRet = ::WriteFile(hFile, pbBuff, dwSizeLow, (LPDWORD)&pcb, NULL);
::CloseHandle(hFile);
基本的文本编辑功能
CEdit
类支持基本的文本编辑操作,我们可以在工作区输入文本并复制/粘贴/剪切文本。但是,CEdit
的编辑操作会操作其内部数据结构。由于我们的文本编辑器程序需要进行一些自定义操作,因此我们基于自己的数据结构实现了基本的编辑操作。
定义数据结构
我们使用一个STL string
列表来存储内容。
#include <vector>
#include <string>
vector<string> m_contents;
不同数据结构之间的映射
每个string
包含一行文本,默认情况下,我们使用两个字符'\r' '\n'来结束一行并开始新的一行。众所周知,与我们的方法不同,CEdit
类将内容视为一个长字符序列。如果我们选择工作区中的文本,CEdit
函数GetSel()
将返回当前选择的起始位置。我们必须将GetSel()
函数获取的位置映射到我们m_contents
中的位置。例如,内容...
[
Hello world!
foo
]
...默认被视为一个序列“Hello world!\r\n\r\n foo”,而我们将它视为一个字符串列表:“Hello world!\r\n”、“\r\n”、“foo”。序列中的位置17是'f',而它对应的位置是[2, 0],即第三个string
的第一个char
。映射函数如下所示。
GetRowColFromPosition(int& nRow, int &nCol, int nPosition)
{
int len = 0;
int size = m_rawText.size();
nCol = nPosition;
for(nRow=0;nRow<size;nRow++) {
len = m_rawText[nRow].size();
if(nCol > len)
nCol -= len;
else {
if(nCol == len && size - nRow > 1) {
nCol = 0; nRow++;
}
break;
}
}
}
在位置映射之后,仍然需要处理文本编辑的消息。消息包括WM_CHAR
、WM_KEYDOWN
等。
处理WM_CHAR消息
当编辑器通过WM_CHAR
消息捕获用户输入的char
时,CEdit
类会将更改保存到其内部数据结构并反映在工作区中。但是,我们可以通过重叠或扩展“OnChar
”函数来处理WM_CHAR
消息,从而自定义程序行为。
BEGIN_MSG_MAP(CEditExView)
MESSAGE_HANDLER(WM_KEYDOWN, OnKeyDown)
MESSAGE_HANDLER(WM_CHAR, OnChar)
END_MSG_MAP()
OnChar
函数的主要过程首先定位工作区中当前选定的内容,然后用输入的char
替换选定的内容。重要的是,编辑器应识别控制字符并妥善处理它们。例如,当输入是“return
” char VK_RETURN
时,程序应该将一个string
对象拆分为两个string
对象,而不是简单地替换选定的内容。
int CEditExView::InsertAt(int nSelectionStart, TCHAR chNewChar)
{
int nInsertionPoint=nSelectionStart;
int nRow = 0;
int nCol = 0;
GetRowColFromPosition(nRow, nCol, nInsertionPoint);
if(chNewChar==VK_RETURN)
{
string line1 = m_rawText[nRow].substr(0, nCol);
line1.append(1, '\r').append(1, '\n');
string line2 = m_rawText[nRow].substr(nCol, m_rawText[nRow].size());
m_rawText[nRow] = line2;
m_rawText.insert(m_rawText.begin() + nRow, line1);
nInsertionPoint++;
}
else if (chNewChar == VK_TAB)
{
for(int i=0;i<m_TabCount;i++) {
nInsertionPoint = nCol++;
m_rawText[nRow].insert(nInsertionPoint, 1, _T(' '));
}
}
else
m_rawText[nRow].insert(nCol, &chNewChar);
return nInsertionPoint;
}
处理WM_KEYDOWN消息
这里的WM_KEYDOWN
消息用于处理“insert
” char VK_INSERT
。每次按下VK_INSERT
键时,编辑模式应在INSERT
模式和REPLACE
模式之间切换。
LRESULT CEditExView::OnKeyDown(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
UINT nChar = (TCHAR)wParam;
BOOL bIsShiftKeyDown=::GetAsyncKeyState(VK_SHIFT)<0;
BOOL bIsCtrlKeyDown=::GetAsyncKeyState(VK_CONTROL)<0;
if(nChar == VK_INSERT) {
if (!bIsShiftKeyDown && !bIsCtrlKeyDown)
{
// The standard CEdit control does not support over-typing.
// This flag is used to manage over-typing internally.
SetInsertMode(!GetInsertMode());
}
}
return 0;
}
关注点
CEdit
和CRichEditCtrl
都提供选择功能,可以响应鼠标操作在工作区中选择文本。我想知道如果我们在这片工作区上绘制一些奇怪的东西,比如图像,选择功能还会起作用吗?
历史
- 2011年7月31日 - 初始版本