讓我們寫一個 Win32 文本編輯器吧 - 2. 計划和顯示
如果你已經閱讀了
簡介,相信你已經對我們接下來要做的事情有所了解。本文,將會把
簡介中基礎程序修改為一個窗體應用程序。並對編輯器接下來的編輯計划進行說明。
1. 程序改造
閱讀過曾經我認為C語言就是個弟弟這篇文章的讀者應該知道,編輯器(包括所有Win32應用程序控件),本質上都是一個窗口(WNDCLASSA(已被WNDCLASSEX取代)結構體描述)。
在本節,我們將對上一篇文章所建立的項目進行改造,使其彈出一個主窗體,並附加一個編輯器窗體。
- 設置項目子系統
在之前,我們為了簡便,沒有修改 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 項目屬性(參考上一篇文章),最終設置如下:

- 修改主程序代碼
修改之系統為窗口后,編譯程序,會發現如下錯誤:

這是因為,鏈接程序會根據項目設置,去查找不同的主函數名稱,而對於窗體應用程序,其主函數名應為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-controls 的 src\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-controls 的 src\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-controls 的 src\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. 之后的計划
由於代碼編輯的過程中,想法可能發生改變,所以未來的計划並不是固定死的,有可能發生變更。
通常情況下,變更的可能有:
- 發現了某個功能的更好的實現方式。
- 某個功能過於復雜,導致一篇文章寫不完。
雖然計划可能會變更,但是大致的思路如下:
-
背景設置
在這里,你將看到,如何設置背景色,或者將我們的編輯器背景設置為一張圖片。
這個過程可能要耗費一節。 -
文本繪制
主要目的是將當前使用
GDI的文本繪制轉換為DirectWrite繪制。
這個過程可能要耗費一節。 -
光標
在此小節,我們將會看到如何將光標顯示在編輯器的指定位置。
這個過程可能要耗費一節。 -
鼠標選擇和高亮
在此主題下,我們將會為我們的編輯器添加鼠標選擇,以及選擇區域高亮顯示的支持。
這個過程可能要耗費 2~3 個小結。 -
文本內存結構
這將是一個比較大的主題,因為文件內容在內存中的保存,根據不同的考慮,將會采用不同的內存結構。
這個過程可能要耗費 2~3 個小結。 -
滾動條實現
由於我們計划讓我們的編輯器可編輯的文件盡可能的大,並且
Windows自帶的滾動條的取值范圍有限,所以我們打算實現一個滾動條,其最大取值為UINT64的最大取值,這樣我們可以處理總行數就大大增加。
這個過程可能要耗費一節。 -
Unicode支持這個主題下,我們會對
Unicode編碼格式做一個簡單的介紹,並實現對Unicode字符的顯示。
這個過程可能要耗費 2~3 個小結。 -
文本透明度設置
由於我們的編輯器允許我們設置背景顏色,甚至背景圖片,考慮到文本顏色可能和背景色相近,導致不容易區分,那么文本的透明渲染就很有必要了。如果我們的文本是透明的,那就可以和背景色相結合,生成更豐富的顏色搭配,起到更好的閱讀體驗的目的。
這個過程可能要耗費 1~2 個小結。 -
添加注解
到此為止,我們的編輯器已經可以顯示內容,選擇內容,上下左右滾動,是時候添加注解功能了。
這個過程可能要耗費 1~2 個小結。 -
添加樣式支持
這里所謂的樣式,是根據配置,識別出文件的不同組成部分,然后將給定識別部分顯示為固定顏色。如下方代碼:
int main(int argc, char** argv) { return 0; }根據配置,將會分別以不同的顏色/字體顯示不同的元素,如類型
int將會被顯示為藍色等等。
這意味着,過了本節,你將至少可以實現一種編程語言的高亮功能。
當前,我們考慮實現C語言的高亮顯示。
好了,到此為止,我們已經能夠將我們的控件顯示出來了,計划也已經說明。如果你有什么建議,或者發現程序中有 BUG,歡迎到本文檔所在項目lets-write-a-edit-control 下留言,或者到源代碼項目 vitality-controls 下提交 issue。
如果像針對本文留言,關注微信公眾號編程之路漫漫,碼途求知己,天涯覓一心。

