讓我們寫一個 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
。
如果像針對本文留言,關注微信公眾號程式設計之路漫漫
,碼途求知己,天涯覓一心。