3

让我们写一个 Win32 文本编辑器吧 - 2. 计划和显示 - plle

 2 years ago
source link: https://www.cnblogs.com/lee2014/p/16103315.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

让我们写一个 Win32 文本编辑器吧 - 2. 计划和显示

如果你已经阅读了简介,相信你已经对我们接下来要做的事情有所了解。

本文,将会把简介中基础程序修改为一个窗体应用程序。并对编辑器接下来的编辑计划进行说明。

1. 程序改造

阅读过曾经我认为C语言就是个弟弟这篇文章的读者应该知道,编辑器(包括所有Win32应用程序控件),本质上都是一个窗口(WNDCLASSA(已被WNDCLASSEX取代)结构体描述)。

在本节,我们将对上一篇文章所建立的项目进行改造,使其弹出一个主窗体,并附加一个编辑器窗体。

  1. 设置项目子系统

在之前,我们为了简便,没有修改 vicapp 项目的子系统,其默认值为控制台应用程序,所以我们可以用如下代码调用 vitality-controls 给出的函数 vic_prints

#include "../../shared-include/vitality-controls.h"

int main(int argc, char** argv) {
	vic_prints("hello vic.");
	return 0;
}

但是,对于一个编辑器来说,应该是一个窗体应用程序。所以,我们要对 vicapp 进行子系统设置,打开 vicapp 项目属性(参考上一篇文章),最终设置如下:

  1. 修改主程序代码

修改之系统为窗口后,编译程序,会发现如下错误:

这是因为,链接程序会根据项目设置,去查找不同的主函数名称,而对于窗体应用程序,其主函数名应为WinMain,所以这里会报找不到符号 WinMain,因为我们没有定义它。

对于不同项目类型的启动函数定义,参考文件VS安装目录\VC\Tools\MSVC\14.31.31103\crt\src\vcruntime\exe_common.inl, 现在将相关代码列出如下:

#if defined _SCRT_STARTUP_MAIN

    using main_policy = __scrt_main_policy;
    using file_policy = __scrt_file_policy;
    using argv_policy = __scrt_narrow_argv_policy;
    using environment_policy = __scrt_narrow_environment_policy;

    static int __cdecl invoke_main()
    {
        return main(__argc, __argv, _get_initial_narrow_environment());
    }

#elif defined _SCRT_STARTUP_WMAIN

    using main_policy = __scrt_main_policy;
    using file_policy = __scrt_file_policy;
    using argv_policy = __scrt_wide_argv_policy;
    using environment_policy = __scrt_wide_environment_policy;

    static int __cdecl invoke_main()
    {
        return wmain(__argc, __wargv, _get_initial_wide_environment());
    }

#elif defined _SCRT_STARTUP_WINMAIN

    using main_policy = __scrt_winmain_policy;
    using file_policy = __scrt_file_policy;
    using argv_policy = __scrt_narrow_argv_policy;
    using environment_policy = __scrt_narrow_environment_policy;

    static int __cdecl invoke_main()
    {
        return WinMain(
            reinterpret_cast<HINSTANCE>(&__ImageBase),
            nullptr,
            _get_narrow_winmain_command_line(),
            __scrt_get_show_window_mode());
    }

#elif defined _SCRT_STARTUP_WWINMAIN

    using main_policy = __scrt_winmain_policy;
    using file_policy = __scrt_file_policy;
    using argv_policy = __scrt_wide_argv_policy;
    using environment_policy = __scrt_wide_environment_policy;

    static int __cdecl invoke_main()
    {
        return wWinMain(
            reinterpret_cast<HINSTANCE>(&__ImageBase),
            nullptr,
            _get_wide_winmain_command_line(),
            __scrt_get_show_window_mode());
    }

#elif defined _SCRT_STARTUP_ENCLAVE || defined _SCRT_STARTUP_WENCLAVE

    using main_policy = __scrt_enclavemain_policy;
    using file_policy = __scrt_nofile_policy;
    using argv_policy = __scrt_no_argv_policy;
    using environment_policy = __scrt_no_environment_policy;

#if defined _SCRT_STARTUP_ENCLAVE
    static int __cdecl invoke_main()
    {
        return main(0, nullptr, nullptr);
    }
#else
    static int __cdecl invoke_main()
    {
        return wmain(0, nullptr, nullptr);
    }
#endif

#endif

可以看到,根据不同的宏定义,函数 invoke_main() 函数的定义也不相同,由于我们的编辑器应该支持Unicode字符,并且我们是一个窗体应用程序。所以,我们主函数应该参考 _SCRT_STARTUP_WWINMAIN 宏定义内的主函数定义。

除了在 exe_common.inl 中定义了主函数的调用函数,另外,窗体应用程序的主函数还在 WinBase.h(该文件可以通过 Windows.h 查找到 #include "WinBase.h" 一行,然后打开,或者可以直接引用) 文件中做了定义,如下:

#if WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP)

int
#if !defined(_MAC)
#if defined(_M_CEE_PURE)
__clrcall
#else
WINAPI
#endif
#else
CALLBACK
#endif
WinMain (
    _In_ HINSTANCE hInstance,
    _In_opt_ HINSTANCE hPrevInstance,
    _In_ LPSTR lpCmdLine,
    _In_ int nShowCmd
    );

int
#if defined(_M_CEE_PURE)
__clrcall
#else
WINAPI
#endif
wWinMain(
    _In_ HINSTANCE hInstance,
    _In_opt_ HINSTANCE hPrevInstance,
    _In_ LPWSTR lpCmdLine,
    _In_ int nShowCmd
    );

#endif /* WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP) */

根据之前的描述,我们把之前的 vitality-controls.h 修改为如下代码:

#pragma once

#ifdef VITALITY_CONTROLS_EXPORTS
#define VIC_API __declspec(dllexport)
#else
#define VIC_API __declspec(dllimport)
#endif // VITALITY_CONTROLS_EXPORTS

#include <Windows.h>

/**
* 函数描述:
*	初始化编辑器环境,需要在调用任何本程序集的函数之前,
*	调用本函数。
* 
* 返回值:
*	如果初始化成功,返回 TRUE,否则返回 FALSE,并设置错误码,
*	错误码可以通过 GetLastError() 获取。
*/
VIC_API BOOL Vic_Init();

/**
* 函数描述:
*	创建并初始化一个编辑器。
* 
* 参数:
*	parent: 新创建的编辑器的父窗体。
* 
* 返回值:
*	如果创建控件成功,返回该控件的句柄,否则返回 -1 并设置错误码。
*	错误码可以通过 GetLastError() 获取。
*/
VIC_API HWND Vic_CreateEditor(
	HWND parent
);

首先,我们将 stdio.h 的引用,换成了 Windows.h,这允许我们使用 Windows 关于桌面应用程序的 API

其次,我们去除了 vic_print 函数的定义。因为该函数主要是上一篇文章测试跨 DLL 调用函数的测试函数。现在,我们不再需要它。

同时,我们添加了两个函数:

  • Vic_Init
    用于初始化环境,主要是注册我们编辑器的窗体类。至于要特别添加一个初始化函数,主要是由于微软官方文档中明确指出,在 DllMain 中调用复杂的函数,可能会造成死锁。
  • Vic_CreateEditor
    用于创建一个编辑器,这里暂时不需要指定编辑器的信息,只是指定一个父窗体的句柄,以便将编辑器添加到窗体。参考曾经我认为C语言就是个弟弟中创建编辑器控件的代码。

接下来,我们还要实现这两个函数。
在项目 vitality-controlssrc\include\ 目录,建立一个 common.h 文件,输入如下内容:

#pragma once

#include "../../../shared-include/vitality-controls.h"

#define EDITOR_CLASS_NAME L"VicEditor"

 
LRESULT CALLBACK TextEditorWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);


其中,该文件引入了外部 API 文件定义,同时,声明了一个宏 EDITOR_CLASS_NAME,该宏定义了我们要创建的目标编辑器的类名。

在项目 vitality-controlssrc\controls\ 文件夹下,建立一个 init.c 文件,并编辑如下代码:

#include "../include/common.h"

/**
* 函数描述:
*	初始化编辑器环境,需要在调用任何本程序集的函数之前,
*	调用本函数。
*/
VIC_API BOOL Vic_Init() {
	WNDCLASSEX wnd = { 0 };

	wnd.cbSize = sizeof(wnd);
	wnd.hInstance = GetModuleHandle(NULL);
	wnd.lpszClassName = EDITOR_CLASS_NAME;
	wnd.hbrBackground = CreateSolidBrush(RGB(255, 0, 0));
	wnd.hCursor = LoadCursor(NULL, IDC_IBEAM);
	wnd.style = CS_GLOBALCLASS | CS_PARENTDC | CS_DBLCLKS;
	wnd.lpfnWndProc = TextEditorWindowProc;

	return RegisterClassEx(&wnd) != 0;
}

在项目 vitality-controlssrc\controls\ 文件夹下,建立一个 common.c 文件,并输入如下代码:

#include "../include/common.h"

/**
* 函数描述:
*	创建并初始化一个编辑器。
*
* 参数:
*	parent: 新创建的编辑器的父窗体。
*
* 返回值:
*	如果创建控件成功,返回该控件的句柄,否则返回 NULL 并设置错误码。
*	错误码可以通过 GetLastError() 获取。
*/
VIC_API HWND Vic_CreateEditor(
	HWND parent
) {
	RECT rect = { 0 };

	if (!GetClientRect(parent, &rect)) {
		return NULL;
	}

	return CreateWindowEx(
		0,
		EDITOR_CLASS_NAME,
		L"",
		WS_CHILD | WS_VISIBLE | ES_MULTILINE |
		WS_VSCROLL | WS_HSCROLL |
		ES_AUTOHSCROLL | ES_AUTOVSCROLL,
		0, 0,
		rect.right,
		rect.bottom,
		parent,
		NULL,
		GetModuleHandle(NULL),
		NULL
	);
}

LRESULT CALLBACK TextEditorWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
	switch (uMsg) {
	case WM_PAINT: {
		PAINTSTRUCT ps;
		HDC hdc = BeginPaint(hwnd, &ps);

		TextOut(hdc, 0, 0, L"HELLO", 5);

		EndPaint(hwnd, &ps);
		return 0;
	}
	default:
		break;
	}
	return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

其中,新增了一个 TextEditorWindowProc 函数,该函数是我们编辑器的回调函数,参考 init.c 文件中,对 wnd.lpfnWndProc 字段的赋值。关于回调函数,参考文档

最后,让我们修改我们应用程序的主函数,修改项目 vicapp 的主程序文件 vicapp-main.c 如下所示:


#include <Windows.h>
#include "../../shared-include/vitality-controls.h"

LRESULT CALLBACK MainWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

PCWSTR MAIN_CLASS_NAME = L"VIC-APP-MAIN";

HWND editorHwnd = NULL;

LRESULT CALLBACK MainWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	switch (uMsg)
	{
	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;
	case WM_CREATE: {
		editorHwnd = Vic_CreateEditor(hwnd);
		if (editorHwnd == 0) {
			int lastError = GetLastError();
			ShowWindow(hwnd, 0);
		}
		return 0;
	}
	case WM_SIZE: {
		RECT rect = { 0 };
		if (!GetWindowRect(hwnd, &rect)) {
			break;
		}
		MoveWindow(
			editorHwnd,
			0,
			0,
			rect.right,
			rect.bottom,
			TRUE
		);
		return 0;
	}
	default:
		break;
	}
	return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

BOOL InitApplication(HINSTANCE hinstance)
{
	WNDCLASSEX wcx = { 0 };

	wcx.cbSize = sizeof(wcx);
	wcx.style = CS_HREDRAW | CS_VREDRAW;
	wcx.lpfnWndProc = MainWindowProc;
	wcx.cbClsExtra = 0;
	wcx.cbWndExtra = 0;
	wcx.hInstance = hinstance;
	wcx.hIcon = LoadIcon(NULL, IDI_APPLICATION);
	wcx.hCursor = LoadCursor(NULL, IDC_ARROW);
	wcx.hbrBackground = GetStockObject(WHITE_BRUSH);
	wcx.lpszClassName = MAIN_CLASS_NAME;
	wcx.hIconSm = LoadImage(
		hinstance,
		MAKEINTRESOURCE(5),
		IMAGE_ICON,
		GetSystemMetrics(SM_CXSMICON),
		GetSystemMetrics(SM_CYSMICON),
		LR_DEFAULTCOLOR
	);

	return RegisterClassEx(&wcx);
}

BOOL InitInstance(HINSTANCE hinstance, int nCmdShow)
{
	HWND hwnd = CreateWindowEx(
		0,
		MAIN_CLASS_NAME,
		L"VicApp",
		WS_OVERLAPPEDWINDOW,
		CW_USEDEFAULT,
		CW_USEDEFAULT,
		CW_USEDEFAULT,
		CW_USEDEFAULT,
		(HWND)NULL,
		(HMENU)NULL,
		hinstance,
		(LPVOID)NULL
	);

	if (!hwnd) {
		return FALSE;
	}

	ShowWindow(hwnd, nCmdShow);
	UpdateWindow(hwnd);
	return TRUE;
}

int WINAPI wWinMain(
	_In_ HINSTANCE hInstance,
	_In_opt_ HINSTANCE hPrevInstance,
	_In_ LPWSTR lpCmdLine,
	_In_ int nShowCmd
) {
	MSG msg = { 0 };

	if (!Vic_Init()) {
		int err = GetLastError();

		return FALSE;
	}

	if (!InitApplication(hInstance))
		return FALSE;

	if (!InitInstance(hInstance, nShowCmd))
		return FALSE;

	BOOL fGotMessage;
	while ((fGotMessage = GetMessage(&msg, (HWND)NULL, 0, 0)) != 0 && fGotMessage != -1)
	{
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}
	return msg.wParam;
}

其中,在出程序的第一句,我们调用了控件初始化函数 Vic_Init,并在创建主窗体的事件处理过程中,调用了 Vic_CreateEditor 函数,创建了一个子窗体,该子窗体就是我们的编辑器。

为了突出显示我们的编辑器,我们在 Vic_Init 函数中,设置背景颜色为红色,代码如下:

wnd.hbrBackground = CreateSolidBrush(RGB(255, 0, 0));

编译,并运行我们的程序,可以看到如下窗体:

由于我们在处理函数 TextEditorWindowProc 中,在窗体上绘制了字符串 "HELLO"。所以,可以看到界面上出现了 "HELLO" 的字样,并且背景色为红色。

2. 之后的计划

由于代码编辑的过程中,想法可能发生改变,所以未来的计划并不是固定死的,有可能发生变更。

通常情况下,变更的可能有:

  • 发现了某个功能的更好的实现方式。
  • 某个功能过于复杂,导致一篇文章写不完。

虽然计划可能会变更,但是大致的思路如下:

  1. 在这里,你将看到,如何设置背景色,或者将我们的编辑器背景设置为一张图片。
    这个过程可能要耗费一节。

  2. 主要目的是将当前使用 GDI 的文本绘制转换为 DirectWrite 绘制。
    这个过程可能要耗费一节。

  3. 在此小节,我们将会看到如何将光标显示在编辑器的指定位置。
    这个过程可能要耗费一节。

  4. 鼠标选择和高亮

    在此主题下,我们将会为我们的编辑器添加鼠标选择,以及选择区域高亮显示的支持。
    这个过程可能要耗费 2~3 个小结。

  5. 文本内存结构

    这将是一个比较大的主题,因为文件内容在内存中的保存,根据不同的考虑,将会采用不同的内存结构。
    这个过程可能要耗费 2~3 个小结。

  6. 滚动条实现

    由于我们计划让我们的编辑器可编辑的文件尽可能的大,并且 Windows 自带的滚动条的取值范围有限,所以我们打算实现一个滚动条,其最大取值为 UINT64 的最大取值,这样我们可以处理总行数就大大增加。
    这个过程可能要耗费一节。

  7. Unicode 支持

    这个主题下,我们会对 Unicode 编码格式做一个简单的介绍,并实现对 Unicode 字符的显示。
    这个过程可能要耗费 2~3 个小结。

  8. 文本透明度设置

    由于我们的编辑器允许我们设置背景颜色,甚至背景图片,考虑到文本颜色可能和背景色相近,导致不容易区分,那么文本的透明渲染就很有必要了。如果我们的文本是透明的,那就可以和背景色相结合,生成更丰富的颜色搭配,起到更好的阅读体验的目的。
    这个过程可能要耗费 1~2 个小结。

  9. 到此为止,我们的编辑器已经可以显示内容,选择内容,上下左右滚动,是时候添加注解功能了。
    这个过程可能要耗费 1~2 个小结。

  10. 添加样式支持

    这里所谓的样式,是根据配置,识别出文件的不同组成部分,然后将给定识别部分显示为固定颜色。如下方代码:

    int main(int argc, char** argv) {
        return 0;
    }
    

    根据配置,将会分别以不同的颜色/字体显示不同的元素,如类型 int 将会被显示为蓝色等等。
    这意味着,过了本节,你将至少可以实现一种编程语言的高亮功能。
    当前,我们考虑实现 C语言 的高亮显示。

好了,到此为止,我们已经能够将我们的控件显示出来了,计划也已经说明。如果你有什么建议,或者发现程序中有 BUG,欢迎到本文档所在项目lets-write-a-edit-control 下留言,或者到源代码项目 vitality-controls 下提交 issue

如果像针对本文留言,关注微信公众号编程之路漫漫,码途求知己,天涯觅一心。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK