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


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

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM