讓我們寫一個 Win32 文字編輯器吧 - 2. 計劃和顯示

plle發表於2022-04-05

讓我們寫一個 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

如果像針對本文留言,關注微信公眾號程式設計之路漫漫,碼途求知己,天涯覓一心。

相關文章