65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.26/5 (16投票s)

2011年8月7日

CPOL

4分钟阅读

viewsIcon

119719

downloadIcon

7429

本文通过构建一个基于WTL对象的简单文本编辑器,来说明ATL/WTL的应用。

Sample Image1 - a simple text editor.

引言

文本编辑器是一个常用的应用程序,可以处理文本文件。文本编辑器主要功能包括“打开/保存文件”、“编辑/查看内容”等。在本教程中,我们将介绍如何基于WTL对象构建一个简单的文本编辑器。

如何使用代码?

我们假设读者已经准备好了支持ATL/WTL的Visual C++。如果已经准备好,源代码可以直接作为VC++项目使用。如果没有,读者需要做一些额外的工作来准备开发环境。

  1. 在您的系统上获取并安装Visual C++程序。Visual C++的版本应高于6.0,因为Visual C++在6.0版本之后提供了内置的完整STL支持。如果您必须使用VC6.0,则需要在系统中安装STL库,例如STLPort,并配置您的VC6.0使用STL库。
  2. 获取并安装与您的Visual C++程序兼容的Windows驱动程序工具包(WDK)。WDK包含一个独立的ATL/MFC实现,这是WTL所必需的。确保WDK的头文件、库文件和可执行文件的路径已包含在Visual C++的环境设置中。
  3. 获取并安装一个开源的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_CHARWM_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替换选定的内容。重要的是,编辑器应识别控制字符并妥善处理它们。例如,当输入是“returnchar 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消息用于处理“insertchar 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;
}

关注点

CEditCRichEditCtrl都提供选择功能,可以响应鼠标操作在工作区中选择文本。我想知道如果我们在这片工作区上绘制一些奇怪的东西,比如图像,选择功能还会起作用吗?

历史

  • 2011年7月31日 - 初始版本
© . All rights reserved.