本文由 發(fā)布,轉(zhuǎn)載請(qǐng)注明出處,如有問(wèn)題請(qǐng)聯(lián)系我們! 發(fā)布時(shí)間: 2021-05-16曾經(jīng)我認(rèn)為C語(yǔ)言就是個(gè)弟弟
加載中曾經(jīng)我認(rèn)為C語(yǔ)言就是個(gè)弟弟
本文所有代碼,均上傳至GitHub,如果你想直接看源代碼,請(qǐng)到github下載,下載地址:https://github.com/vitalitylee/TextEditor
“C語(yǔ)言只能寫(xiě)有一個(gè)黑框的命令行程序,如果要寫(xiě)圖形界面的話,要用Java或者C#”,在2009年左右,我對(duì)同學(xué)這么說(shuō)。
都2021年了,說(shuō)這句話導(dǎo)致的羞愧感,一直在我腦海徘徊。
在這里,就讓我們一起用C寫(xiě)一個(gè)GUI應(yīng)用程序,以正視聽(tīng)。
但是,寫(xiě)什么呢?
首先,這個(gè)程序不應(yīng)該太復(fù)雜,不然的話沒(méi)有辦法在一篇文章內(nèi)實(shí)現(xiàn);
其次,這個(gè)程序又要具有一定的實(shí)用性;
考慮到這兩點(diǎn),記事本應(yīng)該是個(gè)不錯(cuò)的選擇,既不太大,也比較常用。
那么,就讓我們開(kāi)始吧。
對(duì)于我們要實(shí)現(xiàn)的記事本,應(yīng)該有如下功能:
- 能夠打開(kāi)一個(gè)文本文件(通過(guò)打開(kāi)文件對(duì)話框);
- 能夠?qū)ξ谋具M(jìn)行編輯;
- 能夠?qū)⑽募4妫?/li>
- 文件保存時(shí),如果當(dāng)前沒(méi)有已打開(kāi)任何文件,則顯示文件保存對(duì)話框。
- 能夠?qū)⑽募泶鏋榱硗饴窂?,保存后打開(kāi)內(nèi)容為另存為路徑;
- 在主窗體顯示當(dāng)前打開(kāi)文件的文件名;
- 如果文件已編輯,并且未保存,主窗體標(biāo)題前加'*';
- 如果文件保存,則去除主窗體標(biāo)題前的'*';
為了能夠?qū)ξ覀兘酉聛?lái)要做的事情有一個(gè)整體印象,讓我們?cè)谶@里對(duì)本文要實(shí)現(xiàn)一個(gè)簡(jiǎn)單記事本功能的計(jì)劃說(shuō)明,我們的簡(jiǎn)單步驟如下:
- 說(shuō)說(shuō)如何對(duì)一個(gè)C語(yǔ)言項(xiàng)目進(jìn)行設(shè)置,以創(chuàng)建一個(gè)GUI應(yīng)用程序;
- 聊聊入口函數(shù);
- 使用C語(yǔ)言創(chuàng)建一個(gè)窗體;
- 為我們的窗體添加一個(gè)菜單,并添加菜單命令;
- 添加編輯器;
- 響應(yīng)菜單命令;
- 實(shí)現(xiàn)退出命令;
- 實(shí)現(xiàn)打開(kāi)文件命令;
- 響應(yīng)編輯器內(nèi)容變化事件;
- 實(shí)現(xiàn)保存命令;
- 實(shí)現(xiàn)另存為命令;
- 整理我們的代碼,按照功能進(jìn)行分離;
- 最后,我們聊聊整個(gè)過(guò)程中可能遇到的問(wèn)題;
如果完成以上步驟,那么我們就有了一個(gè)可以簡(jiǎn)單工作的文本編輯器了,接下來(lái),讓我們開(kāi)始吧。
在開(kāi)始寫(xiě)代碼之前,開(kāi)發(fā)環(huán)境自然是少不了的。在這里,我們用Visual Studio Community 2019作為我們的開(kāi)發(fā)環(huán)境。
安裝包可以到官網(wǎng)下載,地址如下:
https://visualstudio.microsoft.com/zh-hans/thank-you-downloading-visual-studio/?sku=Community&rel=16
也可以到 Visual Studio 官網(wǎng)搜索下載,界面如下:
點(diǎn)擊圖中紅框處的按鈕下載。
待下載完成后,需要選中“使用C++的桌面開(kāi)發(fā)”選擇框,如下圖所示:
具體的安裝步驟,可參考:
https://docs.microsoft.com/en-us/visualstudio/install/install-visual-studio?view=vs-2019
一、說(shuō)說(shuō)如何對(duì)一個(gè)C語(yǔ)言項(xiàng)目進(jìn)行設(shè)置,以創(chuàng)建一個(gè)GUI應(yīng)用程序
安裝完我們的環(huán)境之后,我們就可以創(chuàng)建我們的項(xiàng)目了。主要步驟如下:
- 啟動(dòng) Visual Studio,并點(diǎn)擊“創(chuàng)建新項(xiàng)目”按鈕
- 選擇項(xiàng)目類型
- 設(shè)置項(xiàng)目源代碼目錄以及項(xiàng)目名稱
- 設(shè)置項(xiàng)目類型
- 新建一個(gè)主程序文件
- 編輯開(kāi)始代碼
- 編譯運(yùn)行
接下來(lái),我們?cè)敿?xì)看看各個(gè)步驟的操作。
1. 啟動(dòng) Visual Studio,并點(diǎn)擊“創(chuàng)建新項(xiàng)目”按鈕
2. 選擇項(xiàng)目類型
3. 設(shè)置項(xiàng)目源代碼目錄以及項(xiàng)目名稱
4. 設(shè)置項(xiàng)目類型
由于Visual Studio默認(rèn)的項(xiàng)目類型為Console類型,但是我們要?jiǎng)?chuàng)建一個(gè)GUI的文本編輯器,所以這里我們要設(shè)置項(xiàng)目類型為GUI類型。具體設(shè)置方法如下:
a. 打開(kāi)解決方案管理器,如下
b. 右鍵項(xiàng)目TextEditor,選擇屬性
c. 將“系統(tǒng)”選項(xiàng)由控制臺(tái)修改為窗口,最后點(diǎn)擊“確定”
5. 新建一個(gè)主程序文件
在設(shè)置好項(xiàng)目類型之后,我們就可以新建我們的主程序文件了,在這里,我們將主程序文件命名為 main.c
。
a. 在解決方案資源管理器中,右鍵“源文件”
b. 在彈出的菜單中依次選擇“添加”->“新建項(xiàng)”
c. 在新建項(xiàng)對(duì)話框中,按照下圖步驟添加源文件
6. 編輯代碼
我們知道,在C語(yǔ)言中,程序是從main函數(shù)開(kāi)始執(zhí)行的。但是對(duì)于一個(gè)GUI應(yīng)用程序來(lái)說(shuō),我們的程序入口變成了如下形式:
int wWinMain(
_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR lpCmdLine,
_In_ int nShowCmd
);
你可以到 winbase.h
文件中找到此函數(shù)的定義,如下:
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) */
我們可以發(fā)現(xiàn),這里定義了兩個(gè)主函數(shù),至于要用哪一個(gè),取決于我們程序運(yùn)行平臺(tái)的選擇,WinMain
主要用于ANSI
環(huán)境,wWinMain
主要用于 Unicode
環(huán)境。由于 Windows
內(nèi)核均采用 Unicode
編碼,而且非 Unicode
字符在真正調(diào)用 Windows API
時(shí),均會(huì)轉(zhuǎn)化為 Unicode
版本,所以對(duì)于我們的程序,采用 Unicode
會(huì)更快(省略了轉(zhuǎn)換步驟),所以這里我們采用 Unicode
版本的主程序。
好了,準(zhǔn)備好環(huán)境之后,讓我們把如下代碼添加到源文件中:
#include <Windows.h>
// 我們的窗體需要一個(gè)消息處理函數(shù)來(lái)處理各種動(dòng)作。
// 由于我們要將消息處理函數(shù)入口賦值給窗體對(duì)象,
// 這里需要提前聲明。
LRESULT CALLBACK mainWinProc(
HWND hWnd, UINT unit, WPARAM wParam, LPARAM lParam);
int wWinMain(
_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR lpCmdLine,
_In_ int nShowCmd
) {
return 0;
}
我們的主程序,只是返回了一個(gè)0,沒(méi)有做任何操作。
7. 編譯運(yùn)行
要編譯我們的C語(yǔ)言程序,和平時(shí)我們編譯C#應(yīng)用程序沒(méi)有區(qū)別,在這里,我們直接按下 Ctrl+F5
執(zhí)行程序,我們發(fā)現(xiàn),沒(méi)有任何反應(yīng),這個(gè)時(shí)候,我們?nèi)?Debug
目錄下去看看,我們發(fā)現(xiàn),Visual Studio
為我們生成了如下文件:
其中文件的作用如下:
- TextEditor.exe: 我們的可執(zhí)行文件;
- TextEditor.ilk: 為鏈接器提供包含對(duì)象、導(dǎo)入和標(biāo)準(zhǔn)庫(kù)、資源、模塊定義和命令輸入;
- TextEditor.pdb:保存.exe文件或者.dll文件的調(diào)試信息。
之所以在我們運(yùn)行程序之后,什么都沒(méi)有看到,是因?yàn)槲覀兊某绦驔](méi)有做任何事情。
二、 聊聊入口函數(shù)
對(duì)于入口函數(shù),在之前我們編輯代碼時(shí)已經(jīng)有了說(shuō)明,我們可以在 WinBase.h 包含文件中找到其定義。并且我們還知道了,在ANSI字符編碼和Unicode字符編碼環(huán)境下,我們要分別定義不同的入口函數(shù)名。
接下來(lái),我們來(lái)聊聊我們主函數(shù)的參數(shù)以及返回值。
參數(shù):
對(duì)于一個(gè) Win32 GUI 應(yīng)用程序主函數(shù)來(lái)說(shuō),一共有四個(gè)參數(shù),說(shuō)明如下:
hInstance
類型:HINSTANCE
說(shuō)明:
當(dāng)前應(yīng)用程序?qū)嵗木浔?
hPrevinstance
類型:HINSTANCE
說(shuō)明:
當(dāng)前應(yīng)用程序的上一個(gè)實(shí)例的句柄。這個(gè)參數(shù)始終為 NULL。如果要判斷是否有另外一個(gè)已經(jīng)運(yùn)行的當(dāng)前應(yīng)用程序的實(shí)例,需要使用 CreateMutex 函數(shù),創(chuàng)建一個(gè)具有唯一命名的互斥鎖。
如果互斥鎖已經(jīng)存在,CreateMutex 函數(shù)也會(huì)成功執(zhí)行,但是返回值為 ERROR_ALREADY_EXISTS. 這說(shuō)明你的應(yīng)用程序的另外一個(gè)實(shí)例正在運(yùn)行,因?yàn)榱硪粋€(gè)實(shí)例已經(jīng)創(chuàng)建了該互斥鎖。
然而,惡意用戶可以在你的應(yīng)用程序啟動(dòng)之前,先創(chuàng)建一個(gè)互斥鎖,從而阻止你的應(yīng)用程序啟動(dòng)。如果要防止這種情況,請(qǐng)創(chuàng)建一個(gè)隨機(jī)命名的互斥鎖,并保存該名稱,從而使得只有指定應(yīng)用可以使用該互斥鎖。
如果要限定一個(gè)用戶只能啟動(dòng)一個(gè)應(yīng)用程序?qū)嵗?,更好的方法是在用戶的配置文件中?chuàng)建一個(gè)鎖定文件。
lpCmdLine
類型:LPSTR/LPWSTR
說(shuō)明:
對(duì)于我們的 Unicode 應(yīng)用程序來(lái)說(shuō),這個(gè)參數(shù)的類型應(yīng)為 LPWSTR,對(duì)于ANSI 應(yīng)用程序來(lái)說(shuō),這個(gè)參數(shù)類型為 LPSTR。
本參數(shù)表示啟動(dòng)當(dāng)前應(yīng)用程序時(shí),傳入的命令行參數(shù),包括當(dāng)前應(yīng)用程序的名稱。如果要獲取某一個(gè)命令行參數(shù),可以通過(guò)調(diào)用 GetCommandLine 函數(shù)實(shí)現(xiàn)。
nShowCmd
類型:int
說(shuō)明:
用于控制程序啟動(dòng)之后的窗體如何顯示。
當(dāng)前參數(shù)可以是 ShowWindow 函數(shù)的 nCmdShow 參數(shù)允許的任何值。
返回值:
類型:int
說(shuō)明:
如果程序在進(jìn)入消息循環(huán)之前結(jié)束,那么主程序應(yīng)該返回0。如果程序成功,并且因?yàn)槭盏搅?WM_QUIT 消息而結(jié)束,那么主程序應(yīng)該返回消息的 wParam 字段值。
使用C語(yǔ)言創(chuàng)建一個(gè)窗體
在了解如何使用C語(yǔ)言創(chuàng)建一個(gè)窗體之前,讓我們先看一看Windows是如何組織窗體的。
在 Windows 啟動(dòng)的時(shí)候,操作系統(tǒng)會(huì)自動(dòng)創(chuàng)建一個(gè)窗體-桌面窗體(Desktop Window)。桌面窗體是一個(gè)由操作系統(tǒng)定義,用于繪制顯示器背景,并作為所有其它應(yīng)用程序窗體基礎(chǔ)窗體的窗體。
桌面窗體使用一個(gè) Bitmap 文件來(lái)繪制顯示器的背景。這個(gè)圖片,被稱為桌面壁紙。
說(shuō)完桌面窗體,接下來(lái),讓我們聊聊其它窗體。
在 Windows 下,窗體被分為三類:系統(tǒng)窗體,全局窗體和本地窗體。
-
系統(tǒng)窗體為操作系統(tǒng)注冊(cè)的窗體,大部分這類窗體可以由所有應(yīng)用程序使用,另外還有一些,供操作系統(tǒng)內(nèi)部使用。由于這些窗體由操作系統(tǒng)注冊(cè),所以我們的應(yīng)用程序不能銷毀他們。
-
全局窗體是由一個(gè)可執(zhí)行文件或者DLL文件注冊(cè),并可以被所有其它進(jìn)程使用的窗體。比如,你可以在一個(gè)DLL中注冊(cè)一個(gè)窗體,在要使用這個(gè)窗體的應(yīng)用程序中,加載該dll,然后使用該窗體。當(dāng)然,你也可以通過(guò)在如下注冊(cè)表鍵的 AppInit_DLLs 值中添加當(dāng)前dll路徑實(shí)現(xiàn):
HKEY_LOCAL_MACHINE\Software\Microsoft\WindowsNT\CurrentVersion\Windows
這樣的話,每當(dāng)一個(gè)進(jìn)程啟動(dòng),操作系統(tǒng)就會(huì)在調(diào)用應(yīng)用的主函數(shù)之前,加載指定的DLL。給定的DLL必須在其 Initialization 函數(shù)中注冊(cè)窗體,并設(shè)置窗體類型的樣式為 CS_GLOBALCLASS。
如果要銷毀全局窗體并釋放其內(nèi)存,可以通過(guò)調(diào)用 UnregisterClass 函數(shù)實(shí)現(xiàn)。
- 本地窗體是可執(zhí)行文件或者 DLL 注冊(cè)的,當(dāng)前進(jìn)程獨(dú)占使用的窗體,雖然可以注冊(cè)多個(gè),但是通常情況下,一個(gè)應(yīng)用程序只注冊(cè)一個(gè)本地窗體類。這個(gè)本地窗體類用于處理應(yīng)用程序的主窗體邏輯。
操作系統(tǒng)會(huì)在進(jìn)程結(jié)束之前,注銷本地窗體類。應(yīng)用程序也可以使用 UnregisterClass 函數(shù)注銷本地窗體類。
操作系統(tǒng)會(huì)為以上三種窗體類型分別創(chuàng)建一個(gè)結(jié)構(gòu)鏈表。當(dāng)一個(gè)應(yīng)用程序調(diào)用CreateWindow 或者 CreateWindowEx 函數(shù),以創(chuàng)建窗體時(shí),操作系統(tǒng)會(huì)先從本地窗體類鏈表中,查找給定的窗體類。
經(jīng)過(guò)以上介紹,不難發(fā)現(xiàn),如果要?jiǎng)?chuàng)建一個(gè)窗體,要么使用系統(tǒng)已經(jīng)注冊(cè)過(guò)的窗體類,要么使用一個(gè)自己注冊(cè)的窗體類。
在這里,我們需要一個(gè)自定義窗體,系統(tǒng)中不存在該窗體類型,所以需要我們自己注冊(cè)。而又由于此窗體不進(jìn)行共享,只是在我們的應(yīng)用程序中使用,所以我們需要注冊(cè)一個(gè)自定義的類型。
注冊(cè)一個(gè)窗體類型,需要使用 WNDCLASSEX 結(jié)構(gòu)體,通過(guò) RegisterClassEx 函數(shù)進(jìn)行注冊(cè)。其中 WNDCLASSEX 結(jié)構(gòu)體用于設(shè)置我們窗體的基礎(chǔ)屬性,如所屬進(jìn)程的應(yīng)用實(shí)例,類名,樣式,關(guān)聯(lián)的菜單等。
由于注冊(cè)窗體類型和其他過(guò)程沒(méi)有關(guān)系,所以這里我們將本過(guò)程抽出,寫(xiě)出如下函數(shù):
LPCWSTR mainWIndowClassName = L"TextEditorMainWindow";
/**
* 作用:
* 主窗體消息處理函數(shù)
*
* 參數(shù):
* hWnd
* 消息目標(biāo)窗體的句柄。
* msg
* 具體的消息的整型值定義,要了解系統(tǒng)
* 支持的消息列表,請(qǐng)參考 WinUser.h 中
* 以 WM_ 開(kāi)頭的宏定義。
*
* wParam
* 根據(jù)不同的消息,此參數(shù)的意義不同,
* 主要用于傳遞消息的附加信息。
*
* lParam
* 根據(jù)不同的消息,此參數(shù)的意義不同,
* 主要用于傳遞消息的附加信息。
*
* 返回值:
* 本函數(shù)返回值根據(jù)發(fā)送消息的不同而不同,
* 具體的返回值意義,請(qǐng)參考 MSDN 對(duì)應(yīng)消息
* 文檔。
*/
LRESULT CALLBACK mainWindowProc(
HWND hWnd,
UINT msg,
WPARAM wParam,
LPARAM lParam) {
return DefWindowProc(hWnd, msg, wParam, lParam);
}
/**
* 作用:
* 注冊(cè)主窗體類型。
*
* 參數(shù):
* hInstance
* 當(dāng)前應(yīng)用程序的實(shí)例句柄,通常情況下在
* 進(jìn)入主函數(shù)時(shí),由操作系統(tǒng)傳入。
*
* 返回值:
* 類型注冊(cè)成功,返回 TRUE,否則返回 FALSE。
*/
BOOL InitMainWindowClass(HINSTANCE hInstance) {
WNDCLASSEX wcx;
// 在初始化之前,我們先將結(jié)構(gòu)體的所有字段
// 均設(shè)置為 0.
ZeroMemory(&wcx, sizeof(wcx));
// 標(biāo)識(shí)此結(jié)構(gòu)體的大小,用于屬性擴(kuò)展。
wcx.cbSize = sizeof(wcx);
// 當(dāng)窗體的大小發(fā)生改變時(shí),重繪窗體。
wcx.style = CS_HREDRAW | CS_VREDRAW;
// 在注冊(cè)窗體類型時(shí),要設(shè)置一個(gè)窗體消息
// 處理函數(shù),以處理窗體消息。
// 如果此字段為 NULL,則程序運(yùn)行時(shí)會(huì)拋出
// 空指針異常。
wcx.lpfnWndProc = mainWindowProc;
// 設(shè)置窗體背景色為白色。
wcx.hbrBackground = GetStockObject(WHITE_BRUSH);
// 指定主窗體類型的名稱,之后創(chuàng)建窗體實(shí)例時(shí)
// 也需要傳入此名稱。
wcx.lpszClassName = mainWIndowClassName;
return RegisterClassEx(&wcx) != 0;
}
其中,InitMainWindowClass 函數(shù)用于注冊(cè)本應(yīng)用程序的主窗體類型,由于注冊(cè)窗體類型時(shí),需要一個(gè)窗體消息處理函數(shù),所以在這里,我們又新增了一個(gè) mainWindowProc 函數(shù),該函數(shù)調(diào)用 DefWindowProc 函數(shù),讓操作系統(tǒng)采用默認(rèn)的消息處理。
通過(guò)以上代碼,我們可以看到,雖然我們通過(guò)返回一個(gè) BOOL 類型值,判斷注冊(cè)類型是否成功,但是我們并不知道具體失敗的原因,所以在這里,我們?cè)偬砑右粋€(gè)函數(shù),以調(diào)用 GetLastError 函數(shù),獲取最后的錯(cuò)誤,并彈出對(duì)應(yīng)消息:
/**
* 作用:
* 顯示最后一次函數(shù)調(diào)用產(chǎn)生的錯(cuò)誤消息。
*
* 參數(shù):
* lpszFunction
* 最后一次調(diào)用的函數(shù)名稱。
*
* hParent
* 彈出消息窗體的父窗體,通常情況下,
* 應(yīng)該指定為我們應(yīng)用程序的主窗體,這樣
* 當(dāng)消息彈出時(shí),將禁止用戶對(duì)主窗體進(jìn)行
* 操作。
*
* 返回值:
* 無(wú)
*/
VOID DisplayError(LPWSTR lpszFunction, HWND hParent) {
LPVOID lpMsgBuff = NULL;
LPVOID lpDisplayBuff = NULL;
DWORD errCode = GetLastError();
if (!FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_IGNORE_INSERTS,
NULL,
errCode,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPTSTR)&lpMsgBuff,
0,
NULL
)) {
return;
}
lpDisplayBuff = LocalAlloc(
LMEM_ZEROINIT,
(lstrlen((LPCTSTR)lpMsgBuff)
+ lstrlenW((LPCTSTR)lpszFunction)
+ 40
) * sizeof(TCHAR)
);
if (NULL == lpDisplayBuff) {
MessageBox(
hParent,
TEXT("LocalAlloc failed."),
TEXT("ERR"),
MB_OK
);
goto RETURN;
}
if (FAILED(
StringCchPrintf(
(LPTSTR)lpDisplayBuff,
LocalSize(lpDisplayBuff) / sizeof(TCHAR),
TEXT("%s failed with error code %d as follows:\n%s"),
lpszFunction,
errCode,
(LPTSTR)lpMsgBuff
)
)) {
goto EXIT;
}
MessageBox(hParent, lpDisplayBuff, TEXT("ERROR"), MB_OK);
EXIT:
LocalFree(lpDisplayBuff);
RETURN:
LocalFree(lpMsgBuff);
}
當(dāng)我們格式化錯(cuò)誤消息失敗時(shí),由于已經(jīng)沒(méi)有了其他的補(bǔ)救措施,當(dāng)前我們直接退出程序。
經(jīng)過(guò)以上步驟,我們創(chuàng)建了一個(gè)主窗體類,接下來(lái),讓我們創(chuàng)建一個(gè)實(shí)例,并顯示窗體。要實(shí)現(xiàn)目標(biāo),我們需要使用 CreateWindow 函數(shù)創(chuàng)建一個(gè)窗體實(shí)例,并獲取到窗體句柄,然后通過(guò)調(diào)用 ShowWindow 函數(shù)顯示窗體,然后通過(guò)一個(gè)消息循環(huán),不斷地處理消息。
添加創(chuàng)建主窗體函數(shù)如下:
/**
* 作用:
* 創(chuàng)建一個(gè)主窗體的實(shí)例,并顯示。
*
* 參數(shù):
* hInstance
* 當(dāng)前應(yīng)用程序的實(shí)例句柄。
*
* cmdShow
* 控制窗體如何顯示的一個(gè)標(biāo)識(shí)。
*
* 返回值:
* 創(chuàng)建窗體成功,并成功顯示成功,返回 TRUE,
* 否則返回 FALSE。
*/
BOOL CreateMainWindow(HINSTANCE hInstance, int cmdShow) {
HWND mainWindowHwnd = NULL;
// 創(chuàng)建一個(gè)窗體對(duì)象實(shí)例。
mainWindowHwnd = CreateWindowEx(
WS_EX_APPWINDOW,
mainWIndowClassName,
TEXT("TextEditor"),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
NULL,
NULL,
hInstance,
NULL
);
if (NULL == mainWindowHwnd) {
DisplayError(TEXT("CreateWindowEx"), NULL);
return FALSE;
}
// 由于返回值只是標(biāo)識(shí)窗體是否已經(jīng)顯示,對(duì)于我們
// 來(lái)說(shuō)沒(méi)有意義,所以這里丟棄返回值。
ShowWindow(mainWindowHwnd, cmdShow);
if (!UpdateWindow(mainWindowHwnd)) {
DisplayError(TEXT("UpdateWindow"), mainWindowHwnd);
return FALSE;
}
return TRUE;
}
修改我們的主函數(shù)如下:
int WINAPI wWinMain(
_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR lpCmdLine,
_In_ int nShowCmd
) {
MSG msg;
BOOL fGotMessage = FALSE;
if (!InitMainWindowClass(hInstance)
|| !CreateMainWindow(hInstance, nShowCmd)) {
return FALSE;
}
while ((fGotMessage = GetMessage(&msg, (HWND)NULL, 0, 0)) != 0
&& fGotMessage != -1)
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return msg.wParam;
}
由于我們使用了一些Windows API,所以需要在我們的源代碼中包含API聲明,當(dāng)前,我們只需要 Windows.h 和 StrSafe.h 兩個(gè)頭文件,所以需要在我們 main.c 文件頭部添加如下兩行:
#include <Windows.h>
#include <strsafe.h>
好了,點(diǎn)擊運(yùn)行按鈕,我們發(fā)現(xiàn),程序成功啟動(dòng),并彈出了一個(gè)窗體,如下:
我們可以看到,彈出的窗體有它的默認(rèn)行為,我們可以拖動(dòng)窗體位置,可以調(diào)整大小,可以最小化,最大化和關(guān)閉按鈕,并且它有一個(gè)標(biāo)題 “TextEditor”?,F(xiàn)在,讓我們關(guān)閉窗體,這個(gè)時(shí)候,問(wèn)題出現(xiàn)了:雖然窗體關(guān)閉了,但是我們的進(jìn)程怎么沒(méi)有結(jié)束?
那是因?yàn)?,我們的消息循環(huán)沒(méi)有收到退出消息,要在關(guān)閉窗體時(shí),退出程序,我們需要處理窗體的 WM_DESTORY 事件,當(dāng)銷毀窗體時(shí),向我們的應(yīng)用程序發(fā)送一個(gè)退出消息。
這可以通過(guò)修改我們之前注冊(cè)的消息處理函數(shù)實(shí)現(xiàn),修改我們的 mainWindowProc 函數(shù)如下:
LRESULT CALLBACK mainWindowProc(
HWND hWnd,
UINT msg,
WPARAM wParam,
LPARAM lParam) {
switch (msg) {
case WM_DESTROY:
PostQuitMessage(0);
return 0;
default:
return DefWindowProc(hWnd, msg, wParam, lParam);
}
}
再次運(yùn)行我們的程序,當(dāng)關(guān)閉窗體后,程序就終止了。
通過(guò)之前的內(nèi)容,不難意識(shí)到,對(duì)于每一個(gè)消息,它的 lParam 和 wParam 分別代表的意義不同,并且消息處理函數(shù)的返回值代表的意義也不同,那么對(duì)于每一個(gè)窗體消息,是不是都要查詢文檔,并將參數(shù)進(jìn)行強(qiáng)制類型轉(zhuǎn)換后,獲取對(duì)應(yīng)信息,最后返回我們的處理結(jié)果呢?當(dāng)然,這么做是可以的,但是會(huì)增加我們程序的復(fù)雜度,并且容易出錯(cuò)。這個(gè)時(shí)候,我們就可以使用平臺(tái)提供的一個(gè)頭文件 "windowsx.h" 來(lái)解決這個(gè)問(wèn)題,這個(gè)文件定義了一系列的宏,用于消息的轉(zhuǎn)換,在頭部包含 "windowsx.h" 頭文件之后,我們的消息處理函數(shù)就可以改成如下形式:
LRESULT CALLBACK mainWindowProc(
HWND hWnd,
UINT msg,
WPARAM wParam,
LPARAM lParam) {
switch (msg) {
case WM_DESTROY:
return HANDLE_WM_DESTROY(
hWnd,
wParam,
lParam,
MainWindow_Cls_OnDestroy
);
default:
return DefWindowProc(hWnd, msg, wParam, lParam);
}
}
其中,HANDLE_WM_DESTROY 是 windowsx.h 頭文件定義的一個(gè)宏,用于處理 WM_DESTROY 消息,其中前三個(gè)函數(shù)分別為消息處理函數(shù)的三個(gè)同名參數(shù),最后一個(gè)參數(shù)是我們定義的消息處理函數(shù)名稱,消息函數(shù)的簽名可以到消息處理宏的定義處查看,對(duì)應(yīng)注釋就是我們的消息處理函數(shù)的定義形式,名稱可以不一樣,但是簽名需要一樣,比如,HANDLE_WM_DESTROY 宏的注釋如下:
/* void Cls_OnDestroy(HWND hwnd) */
那么,我們的消息處理函數(shù)就應(yīng)該定義為一個(gè) HWND 參數(shù),并且沒(méi)有返回值的函數(shù)。所以,我們的窗體銷毀函數(shù)定義如下:
void MainWindow_Cls_OnDestroy(HWND hwnd) {
PostQuitMessage(0);
}
運(yùn)行程序,我們發(fā)現(xiàn)和之前是一樣的效果。
四、添加一個(gè)菜單,并添加菜單命令
在上一節(jié),我們了解了創(chuàng)建一個(gè)窗體的方法,本節(jié),我們聊聊菜單。
在 Visual Studio 中,菜單是以資源的形式存在和編譯的,要增加菜單,其實(shí)就是添加一個(gè)菜單資源。
添加過(guò)程如下:
1. 解決方案資源管理器中,鼠標(biāo)右鍵項(xiàng)目名 -> 添加 -> 資源,彈出添加資源對(duì)話框:
2. 在彈出的添加資源對(duì)話框左側(cè),選擇 Menu,點(diǎn)擊右側(cè)”新建“按鈕,彈出菜單編輯界面
我們會(huì)發(fā)現(xiàn),有一個(gè)”請(qǐng)?jiān)诖随I入“的框,在這里,輸入我們的菜單項(xiàng),比如,輸入”文件“,界面將變成下面的樣子:
其中,在”文件“下方的輸入框輸入的項(xiàng),為”文件“菜單項(xiàng)的子項(xiàng),右側(cè)為同級(jí)菜單項(xiàng),當(dāng)我們?cè)凇蔽募安藛巫禹?xiàng)中新增項(xiàng)目之后,子項(xiàng)的下方和右方也會(huì)出現(xiàn)對(duì)應(yīng)的輸入框,這時(shí)候,下方的為統(tǒng)計(jì)項(xiàng),右側(cè)的為子項(xiàng)。
按照之前我們定義的程序功能,分別為每一個(gè)功能添加一個(gè)菜單項(xiàng),結(jié)果如下:
添加完成之后,在屬性工具欄,我們分別修改對(duì)應(yīng)的菜單項(xiàng)ID名稱,以便之后識(shí)別命令,修改過(guò)程為選擇一個(gè)菜單項(xiàng),然后在屬性工具欄中修改ID項(xiàng),我們依次修改菜單項(xiàng)的ID如下:
- 打開(kāi):ID_OPEN
- 保存:ID_SAVE
- 另存為:ID_SAVEAS
- 退出:ID_EXIT
雖然IDE為我們提供了可視化的修改方法,但是可視化修改,當(dāng)我們改ID之后,IDE就會(huì)新增一個(gè)ID,而不是將原來(lái)的ID替換,更好的辦法是直接編輯資源文件。
在我們新增菜單資源的時(shí)候,仔細(xì)觀察的話,會(huì)發(fā)現(xiàn),IDE為我們添加了兩個(gè)文件:resource.h 和 TextEditor.rc。
首先,讓我們打開(kāi) resource.h文件,發(fā)現(xiàn)文件內(nèi)容如下:
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ 生成的包含文件。
// 供 TextEditor.rc 使用
//
#define IDR_MENU1 101
#define ID_Menu 40001
#define ID_40002 40002
#define ID_40003 40003
#define ID_40004 40004
#define ID_40005 40005
#define ID_OPEN 40006
#define ID_SAVE 40007
#define ID_SAVE_AS 40008
#define ID_EXIT 40009
// Next default values for new objects
//
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 102
#define _APS_NEXT_COMMAND_VALUE 40010
#define _APS_NEXT_CONTROL_VALUE 1001
#define _APS_NEXT_SYMED_VALUE 101
#endif
#endif
這里,我們?nèi)コ裏o(wú)用聲明,將其修改如下:
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ 生成的包含文件。
// 供 TextEditor.rc 使用
//
#define IDR_MENU_MAIN 101
#define ID_OPEN 40001
#define ID_SAVE 40002
#define ID_SAVE_AS 40003
#define ID_EXIT 40004
// Next default values for new objects
//
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 102
#define _APS_NEXT_COMMAND_VALUE 40010
#define _APS_NEXT_CONTROL_VALUE 1001
#define _APS_NEXT_SYMED_VALUE 101
#endif
#endif
注意,在這里,我們不止修改了子菜單項(xiàng)的ID,而且還修改了菜單資源的ID名為 IDR_MENU_MAIN。
修改 resource.h 的同時(shí),我們還要同步修改 TextEditor.rc文件,extEditor.rc文件不能通過(guò)雙擊打開(kāi),要通過(guò)右鍵->查看代碼打開(kāi),否則會(huì)顯示文件已經(jīng)在其他編輯器打開(kāi),或者打開(kāi)資源編輯器。
打開(kāi)extEditor.rc文件,你看到的內(nèi)容可能如下:
// Microsoft Visual C++ generated resource script.
//
#include "resource.h"
#define APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 2 resource.
//
#include "winres.h"
/////////////////////////////////////////////////
#undef APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////
// 中文(簡(jiǎn)體,中國(guó)) resources
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_CHS)
LANGUAGE LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED
#pragma code_page(936)
#ifdef APSTUDIO_INVOKED
/////////////////////////////////////////////////
//
// TEXTINCLUDE
//
1 TEXTINCLUDE
BEGIN
"resource.h\0"
END
2 TEXTINCLUDE
BEGIN
"#include ""winres.h""\r\n"
"\0"
END
3 TEXTINCLUDE
BEGIN
"\r\n"
"\0"
END
#endif // APSTUDIO_INVOKED
/////////////////////////////////////////////////
//
// Menu
//
IDR_MENU1 MENU
BEGIN
POPUP "文件"
BEGIN
MENUITEM "打開(kāi)", ID_OPEN
MENUITEM "保存", ID_SAVE
MENUITEM "另存為", ID_SAVE_AS
MENUITEM "退出", ID_EXIT
END
END
#endif // 中文(簡(jiǎn)體,中國(guó)) resources
/////////////////////////////////////////////////
#ifndef APSTUDIO_INVOKED
/////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 3 resource.
//
/////////////////////////////////////////////////
#endif // not APSTUDIO_INVOKED
其中,第52行到61行定義了我們的菜單資源,這里我們要將菜單資源的ID修改為我們之前在 TextEditor.rc文件中定義的名稱,同時(shí),我們還要修改資源的編碼聲明(20行),不然編譯的時(shí)候會(huì)出現(xiàn)亂碼。
最終,我們修改該文件內(nèi)容為:
// Microsoft Visual C++ generated resource script.
//
#include "resource.h"
#define APSTUDIO_READONLY_SYMBOLS
////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 2 resource.
//
#include "winres.h"
///////////////////////////////////////////////////////
#undef APSTUDIO_READONLY_SYMBOLS
///////////////////////////////////////////////////////
// 中文(簡(jiǎn)體,中國(guó)) resources
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_CHS)
LANGUAGE LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED
#pragma code_page(65001)
#ifdef APSTUDIO_INVOKED
//////////////////////////////////////////////////////
//
// TEXTINCLUDE
//
1 TEXTINCLUDE
BEGIN
"resource.h\0"
END
2 TEXTINCLUDE
BEGIN
"#include ""winres.h""\r\n"
"\0"
END
3 TEXTINCLUDE
BEGIN
"\r\n"
"\0"
END
#endif // APSTUDIO_INVOKED
/////////////////////////////////////////////////////
//
// Menu
//
IDR_MENU_MAIN MENU
BEGIN
POPUP "文件"
BEGIN
MENUITEM "打開(kāi)", ID_OPEN
MENUITEM "保存", ID_SAVE
MENUITEM "另存為", ID_SAVE_AS
MENUITEM "退出", ID_EXIT
END
END
#endif // 中文(簡(jiǎn)體,中國(guó)) resources
//////////////////////////////////////////////////////////
#ifndef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 3 resource.
//
/////////////////////////////////////////////////////////
#endif // not APSTUDIO_INVOKED
其中,第20行聲明我們資源文件的編碼為 UTF-8。
做完以上操作之后,我們就完成了我們菜單資源的添加,接下來(lái),怎么將菜單添加到我們彈出的窗體上呢?
在之前注冊(cè)窗體類的時(shí)候,我們可以看到,在 WNDCLASSEX 結(jié)構(gòu)體中,有一個(gè) lpszMenuName 字段,我們通過(guò)設(shè)置該字段,就可以實(shí)現(xiàn)將我們新增的菜單資源和我們的主窗體綁定的操作。
在 InitMainWindowClass 函數(shù)中添加如下代碼:
// 將主窗體的菜單設(shè)置為主菜單
wcx.lpszMenuName = MAKEINTRESOURCE(IDR_MENU_MAIN);
運(yùn)行程序,就可以看到,我們的主窗體現(xiàn)在已經(jīng)有了我們要的菜單,如下:
五、添加編輯器
還記得之前我們說(shuō)過(guò),在Windows下,有一些窗體是操作系統(tǒng)注冊(cè)的嗎?其中就有一個(gè)窗體,叫做 EDIT,就是用于文本編輯的控件。沒(méi)錯(cuò),文本編輯控件,本身也是一個(gè)窗體。那么添加編輯器的操作就簡(jiǎn)單了,只需要?jiǎng)?chuàng)建一個(gè) EDIT 窗體,并將其作為我們主窗體的子窗體即可。
要實(shí)現(xiàn)這一點(diǎn),和創(chuàng)建我們的主窗體的代碼沒(méi)有什么不同。為了在創(chuàng)建主窗體的時(shí)候,同時(shí)創(chuàng)建編輯器控件,我們將編輯器的創(chuàng)建,放到主窗體的 WM_CREATE 事件處理函數(shù)中,在 mainWindowProc 函數(shù)中添加如下處理:
case WM_CREATE:
return HANDLE_WM_CREATE(
hWnd, wParam, lParam, MainWindow_Cls_OnCreate
);
然后定義主窗體的創(chuàng)建消息處理函數(shù)如下:
BOOL MainWindow_Cls_OnCreate(
HWND hwnd, LPCREATESTRUCT lpCreateStruct) {
return NULL != CreateTextEditor(GetWindowInstance(hwnd), hwnd);
}
通過(guò)查看 WM_CREATE 消息的說(shuō)明,我們可以知道,當(dāng) WM_CREATE 消息的處理結(jié)果為-1時(shí),操作系統(tǒng)將銷毀已經(jīng)創(chuàng)建的窗體對(duì)象實(shí)例,如果為 0,才會(huì)繼續(xù)執(zhí)行,所以這里當(dāng)我們創(chuàng)建文本編輯器成功之后,返回0,否則返回 -1。
接下來(lái),添加創(chuàng)建編輯器的函數(shù),以及創(chuàng)建默認(rèn)字體的函數(shù)如下:
/**
* 作用:
* 創(chuàng)建編輯器使用的字體,這里默認(rèn)為 "Courier New"
*
* 參數(shù):
* 無(wú)
*
* 返回值:
* 新建字體的句柄。
*/
HANDLE CreateDefaultFont() {
LOGFONT lf;
ZeroMemory(&lf, sizeof(lf));
// 設(shè)置字體為Courier New
lf.lfHeight = 16;
lf.lfWidth = 8;
lf.lfWeight = 400;
lf.lfOutPrecision = 3;
lf.lfClipPrecision = 2;
lf.lfQuality = 1;
lf.lfPitchAndFamily = 1;
StringCchCopy((STRSAFE_LPWSTR)&lf.lfFaceName, 32, L"Courier New");
return CreateFontIndirect(&lf);
}
/**
* 作用:
* 創(chuàng)建編輯器窗體
*
* 參數(shù):
* hInstance
* 當(dāng)前應(yīng)用程序?qū)嵗木浔?*
* hParent
* 當(dāng)前控件的所屬父窗體
*
* 返回值:
* 創(chuàng)建成功,返回新建編輯器的句柄,否則返回 NULL。
*/
HWND CreateTextEditor(
HINSTANCE hInstance, HWND hParnet) {
RECT rect;
HWND hEdit;
// 獲取窗體工作區(qū)的大小,以備調(diào)整編輯控件的大小
GetClientRect(hParnet, &rect);
hEdit = CreateWindowEx(
0,
TEXT("EDIT"),
TEXT(""),
WS_CHILDWINDOW |
WS_VISIBLE |
WS_VSCROLL |
ES_LEFT |
ES_MULTILINE |
ES_NOHIDESEL,
0,
0,
rect.right,
rect.bottom,
hParnet,
NULL,
hInstance,
NULL
);
gHFont = CreateDefaultFont();
if (NULL != gHFont) {
// 設(shè)置文本編輯器的字體。并且在設(shè)置之后立刻重繪。
SendMessage(hEdit, WM_SETFONT, (WPARAM)gHFont, TRUE);
}
return hEdit;
}
再運(yùn)行一下,我們可以看到,編輯器已經(jīng)添加到我們的窗體中了:
六、響應(yīng)菜單命令
通過(guò)之前的內(nèi)容,我們已經(jīng)可以顯示我們的主窗體、編輯文字了,接下來(lái),我們?cè)趺错憫?yīng)菜單的命令呢?
自然是通過(guò)消息處理函數(shù)!
當(dāng)我們點(diǎn)擊了一個(gè)菜單,操作系統(tǒng)就會(huì)發(fā)送向我們的主窗體發(fā)送一個(gè) WM_COMMAND 消息,所以,我們可以通過(guò)處理 WM_COMMAND 消息來(lái)響應(yīng)菜單點(diǎn)擊。
為了響應(yīng) WM_COMMAND 消息,向我們的消息處理函數(shù)添加如下分支代碼:
case WM_COMMAND:
return HANDLE_WM_COMMAND(
hWnd, wParam, lParam, MainWindow_Cls_OnCommand
);
然后添加我們的命令消息處理函數(shù)骨架,如下:
/**
* 作用:
* 處理主窗體的菜單命令
*
* 參數(shù):
* hwnd
* 主窗體的句柄
* id
* 點(diǎn)擊菜單的ID
*
* hwndCtl
* 如果消息來(lái)自一個(gè)控件,則此值為該控件的句柄,
* 否則這個(gè)值為 NULL
*
* codeNotify
* 如果消息來(lái)自一個(gè)控件,此值表示通知代碼,如果
* 此值來(lái)自一個(gè)快捷菜單,此值為1,如果消息來(lái)自菜單
* 此值為0
*
* 返回值:
* 無(wú)
*/
void MainWindow_Cls_OnCommand(
HWND hwnd, int id, HWND hwndCtl, UINT codeNotify) {
switch (id) {
case ID_OPEN:
MessageBox(
hwnd,
TEXT("ID_OPEN"),
TEXT("MainWindow_Cls_OnCommand"),
MB_OK
);
break;
case ID_SAVE:
MessageBox(
hwnd,
TEXT("ID_SAVE"),
TEXT("MainWindow_Cls_OnCommand"),
MB_OK
);
break;
case ID_SAVE_AS:
MessageBox(
hwnd,
TEXT("ID_SAVE_AS"),
TEXT("MainWindow_Cls_OnCommand"),
MB_OK
);
break;
case ID_EXIT:
MessageBox(
hwnd,
TEXT("ID_EXIT"),
TEXT("MainWindow_Cls_OnCommand"),
MB_OK
);
break;
default:
break;
}
}
在命令處理函數(shù)中,每當(dāng)我們收到要給命令時(shí),就彈出對(duì)應(yīng)命令的 ID,以確認(rèn)命令正確到達(dá),并忽略任何我們不需要處理的命令。
運(yùn)行程序,看看是不是彈出了正確消息?
七、實(shí)現(xiàn)退出命令
在我們要實(shí)現(xiàn)的功能中,最容易實(shí)現(xiàn)的應(yīng)該就是保存命令了。在收到 ID_EXIT 命令時(shí),我們只需要調(diào)用之前窗體關(guān)閉的處理邏輯即可。將命令處理函數(shù)的 ID_EXIT 分支代碼改成調(diào)用窗體關(guān)閉函數(shù),如下:
case ID_EXIT:
MainWindow_Cls_OnDestroy(hwnd);
break;
再次運(yùn)行,并點(diǎn)擊菜單 "文件" -> "退出",可以看到,我們的程序正常關(guān)閉了。
八、實(shí)現(xiàn)打開(kāi)文件命令
要實(shí)現(xiàn)打開(kāi)文件功能,我們可以將其分成如下步驟:
- 彈出打開(kāi)文件對(duì)話框;
- 獲取文件大小;
- 分配文件大小相等的內(nèi)存;
- 將文件內(nèi)容讀取到分配的內(nèi)存;
- 設(shè)置主窗體標(biāo)題為文件名;
- 設(shè)置編輯器控件的文本;
1. 彈出打開(kāi)文件對(duì)話框
在Windows中,可以通過(guò)調(diào)用 GetOpenFileName 函數(shù)彈出打開(kāi)文件對(duì)話框,并獲取到用戶選擇的文件路徑,但是根據(jù) MSDN 文檔,建議使用 COM 組件的方式彈出打開(kāi)文件對(duì)話框,這里我們采取 COM 組件的方式。
添加如下代碼:
// 支持的編輯文件類型,當(dāng)前我們只支持文本文件(*.txt).
COMDLG_FILTERSPEC SUPPORTED_FILE_TYPES[] = {
{ TEXT("text"), TEXT("*.txt") }
};
// 包含一個(gè)類型為 PWSTR 參數(shù),沒(méi)有返回值的函數(shù)指針
typedef VOID(*Func_PWSTR)(PWSTR parameter, HWND hwnd);
/**
* 作用:
* 選擇一個(gè)文件,選擇成功之后,調(diào)用傳入的回調(diào)函數(shù) pfCallback
*
* 參數(shù):
* pfCallback
* 當(dāng)用戶成功選擇一個(gè)文件,并獲取到文件路徑之后,本函數(shù)
* 將回調(diào) pfCallback 函數(shù)指針指向的函數(shù),并將獲取到的文
* 路徑作為參數(shù)傳入。
*
* hwnd
* 打開(kāi)文件對(duì)話框的父窗體句柄。
*
* 返回值:
* 無(wú)
*/
VOID EditFile(Func_PWSTR pfCallback, HWND hwnd) {
// 每次調(diào)用之前,應(yīng)該先初始化 COM 組件環(huán)境
HRESULT hr = CoInitializeEx(
NULL,
COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE
);
if (SUCCEEDED(hr))
{
IFileOpenDialog* pFileOpen = NULL;
// 創(chuàng)建一個(gè) FileOpenDialog 實(shí)例
hr = CoCreateInstance(
&CLSID_FileOpenDialog,
NULL,
CLSCTX_ALL,
&IID_IFileOpenDialog,
&pFileOpen
);
if (SUCCEEDED(hr))
{
// 設(shè)置打開(kāi)文件擴(kuò)展名
pFileOpen->lpVtbl->SetFileTypes(
pFileOpen,
_countof(SUPPORTED_FILE_TYPES),
SUPPORTED_FILE_TYPES
);
// 顯示選擇文件對(duì)話框
hr = pFileOpen->lpVtbl->Show(pFileOpen, hwnd);
// Get the file name from the dialog box.
if (SUCCEEDED(hr))
{
IShellItem* pItem;
hr = pFileOpen->lpVtbl->GetResult(pFileOpen, &pItem);
if (SUCCEEDED(hr))
{
PWSTR pszFilePath;
hr = pItem->lpVtbl->GetDisplayName(
pItem, SIGDN_FILESYSPATH, &pszFilePath);
// Display the file name to the user.
if (SUCCEEDED(hr))
{
if (pfCallback) {
pfCallback(pszFilePath, hwnd);
}
CoTaskMemFree(pszFilePath);
}
pItem->lpVtbl->Release(pItem);
}
}
pFileOpen->lpVtbl->Release(pFileOpen);
}
CoUninitialize();
}
}
在這里,需要注意的是,為了方便,我們將回調(diào)函數(shù)指針聲明和文件類型聲明與編輯文件函數(shù)定義放到了一起,在真是狀態(tài)下,我們會(huì)將聲明放到源文件開(kāi)頭。
另外,為了使用COM,我們需要引入兩個(gè)頭文件,stdlib.h 和 ShlObj.h,其中_countof 宏定義在 stdlib.h 中,其他的COM相關(guān)定義,在 ShlObj.h 文件中。
現(xiàn)在,我們已經(jīng)實(shí)現(xiàn)了彈出打開(kāi)文件對(duì)話框的功能,但是還沒(méi)有調(diào)用。接下來(lái),讓我們調(diào)用它,并試一下,是否正常彈出了打開(kāi)文件對(duì)話框。
首先,修改 ID_OPEN 命令的響應(yīng)分支如下:
case ID_OPEN:
EditFile(OpenNewFile, hwnd);
break;
然后,我們添加一個(gè)新函數(shù): OpenNewFile, 它接收一個(gè)字符串和父窗體句柄,用于讀取文件,并將文件內(nèi)容添加到編輯器控件內(nèi),其基礎(chǔ)定義如下:
/**
* 作用:
* 如果當(dāng)前已經(jīng)有了打開(kāi)的文件,并且內(nèi)容已經(jīng)被修改,
* 則彈出對(duì)話框,讓用戶確認(rèn)是否保存以打開(kāi)文件,并打開(kāi)
* 新文件。
* 如果當(dāng)前沒(méi)有已打開(kāi)文件或者當(dāng)前已打開(kāi)文件未修改,
* 則直接打開(kāi)傳入路徑指定文件。
*
* 參數(shù):
* fileName
* 要新打開(kāi)的目標(biāo)文件路徑。
*
* hwnd
* 彈出對(duì)話框時(shí),指定的父窗體,對(duì)于本應(yīng)用來(lái)說(shuō),
* 應(yīng)該為主窗體的句柄。
*
* 返回值:
* 無(wú)
*/
VOID OpenNewFile(PWSTR fileName, HWND hwnd) {
MessageBox(hwnd, fileName, TEXT("打開(kāi)新文件"), MB_OK);
}
在這里,為了演示打開(kāi)文件對(duì)話框的函數(shù)是否正常工作,我們暫時(shí)是彈出一個(gè)對(duì)話框,顯示傳入的文件路徑,沒(méi)有做任何操作。運(yùn)行代碼,點(diǎn)擊"文件" -> "打開(kāi)" 菜單,我們可以看到,程序正確彈出了打開(kāi)文件對(duì)話框,且在選擇文件之后,彈出了選中路徑:
由于在內(nèi)存中,字符串是以 UTF-16 寬字符進(jìn)行編碼,所以在讀取文件之后,我們需要將讀取到的內(nèi)容轉(zhuǎn)換為寬字符表示,另外我們將內(nèi)存分配的邏輯也抽取出來(lái),封裝成我一個(gè)函數(shù),于是,得到以下兩個(gè)輔助函數(shù):
/**
* 作用:
* 從默認(rèn)進(jìn)程堆中分配給定大小的內(nèi)存,大小的單位為 BYTE。
* 如,要分配 100 byte 的內(nèi)存,可以通過(guò)如下方式調(diào)用:
* NewMemory(100, NULL)
*
* 參數(shù):
* size
* 以 byte 為單位的內(nèi)存大小。
*
* hwnd
* 如果分配出錯(cuò),彈出消息框的父窗體句柄。
*
* 返回值:
* 如果內(nèi)存分配成功,返回分配內(nèi)存的起始指針,否則返回 NULL。
*/
PBYTE NewMemory(size_t size, HWND hwnd) {
HANDLE processHeap;
PBYTE buff = NULL;
if ((processHeap = GetProcessHeap()) == NULL) {
DisplayError(TEXT("GetProcessHeap"), hwnd);
return buff;
}
buff = (PBYTE)HeapAlloc(processHeap, HEAP_ZERO_MEMORY, size);
if (NULL == buff) {
// 由于 HeapAlloc 函數(shù)不設(shè)置錯(cuò)誤碼,所以這里
// 只能直接彈出一個(gè)錯(cuò)誤消息,但是并不知道具體
// 錯(cuò)誤原因。
MessageBox(
hwnd,
TEXT("alloc memory error."),
TEXT("Error"),
MB_OK
);
}
return buff;
}
/**
* 作用:
* 從內(nèi)存 buff 中讀取字符串,并將其轉(zhuǎn)換為 UTF16 編碼,
* 返回編碼后的寬字符字符。
*
* 參數(shù):
* buff
* 文本原始內(nèi)容。
*
* hwnd
* 操作出錯(cuò)時(shí),彈框的父窗體句柄。
*
* 返回值:
* 無(wú)論原始內(nèi)容是否為 UTF16 編碼字符串,本函數(shù)均會(huì)
* 重新分配內(nèi)存,并返回新內(nèi)存。
*/
PTSTR Normalise(PBYTE buff, HWND hwnd) {
PWSTR pwStr;
PTSTR ptText;
size_t size;
pwStr = (PWSTR)buff;
// 檢查BOM頭
if (*pwStr == 0xfffe || *pwStr == 0xfeff) {
// 如果是大端序,要轉(zhuǎn)換為小端序
if (*pwStr == 0xfffe) {
WCHAR wc;
for (; (wc = *pwStr); pwStr++) {
*pwStr = (wc >> 8) | (wc << 8);
}
// 跳過(guò) BOM 頭
pwStr = (PWSTR)(buff + 2);
}
size = (wcslen(pwStr) + 1) * sizeof(WCHAR);
ptText = (PWSTR)NewMemory(size, hwnd);
if (!ptText) {
return NULL;
}
memcpy_s(ptText, size, pwStr, size);
return ptText;
}
size =
MultiByteToWideChar(
CP_UTF8,
0,
buff,
-1,
NULL,
0
);
ptText = (PWSTR)NewMemory(size * sizeof(WCHAR), hwnd);
if (!ptText) {
return NULL;
}
MultiByteToWideChar(
CP_UTF8,
0,
buff,
-1,
ptText,
size
);
return ptText;
}
有了以上兩個(gè)輔助函數(shù),接下來(lái),我們新增兩個(gè)全局變量,如下:
LPCSTR currentFileName = NULL;
HWND hTextEditor = NULL;
其中,currentFileName 指向當(dāng)前以打開(kāi)文件的路徑,hTextEditor 為我們文本編輯器實(shí)例的句柄。
由于我們?cè)谠O(shè)置編輯器文本的時(shí)候,需要獲取到編輯器句柄,所以在創(chuàng)建編輯器窗體的時(shí)候,使用 hTextEditor 記錄句柄,修改主窗體創(chuàng)建事件處理函數(shù),添加賦值:
BOOL MainWindow_Cls_OnCreate(
HWND hwnd, LPCREATESTRUCT lpCreateStruct) {
return NULL != (
hTextEditor = CreateTextEditor(
GetWindowInstance(hwnd), hwnd)
);
}
最后,修改 OpenNewFile 函數(shù)代碼如下:
VOID OpenNewFile(PWSTR fileName, HWND hwnd) {
LARGE_INTEGER size;
PBYTE buff = NULL;
HANDLE processHeap = NULL;
DWORD readSize = 0;
HANDLE hFile = CreateFile(
fileName,
GENERIC_ALL,
0,
NULL,
OPEN_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
NULL
);
if (INVALID_HANDLE_VALUE == hFile) {
DisplayError(TEXT("CreateFile"), hwnd);
return;
}
if (!GetFileSizeEx(hFile, &size)) {
DisplayError(TEXT("GetFileSizeEx"), hwnd);
goto Exit;
}
if ((processHeap = GetProcessHeap()) == NULL) {
DisplayError(TEXT("GetProcessHeap"), hwnd);
goto Exit;
}
buff = (PBYTE)HeapAlloc(
processHeap,
HEAP_ZERO_MEMORY,
(SIZE_T)(size.QuadPart + 8));
if (NULL == buff) {
MessageBox(
hwnd,
TEXT("alloc memory error."),
TEXT("Error"),
MB_OK
);
goto Exit;
}
if (!ReadFile(
hFile, buff,
(DWORD)size.QuadPart,
&readSize,
NULL
)) {
MessageBox(
hwnd,
TEXT("ReadFile error."),
TEXT("Error"),
MB_OK
);
goto FreeBuff;
}
// 因?yàn)閷?duì)話框關(guān)閉之后,將會(huì)釋放掉文件路徑的內(nèi)存
// 所以這里,我們重新分配內(nèi)存,并拷貝一份路徑
// 在這之前,需要判斷當(dāng)前文件名是否指向了一個(gè)地址,
// 如果有指向,應(yīng)將其釋放。
if (currentFileName) {
HeapFree(GetProcessHeap(), 0, currentFileName);
}
size_t bsize = (wcslen(fileName) + 1) * sizeof(WCHAR);
currentFileName = (PWSTR)NewMemory(bsize, hwnd);
if (!currentFileName) {
goto FreeBuff;
}
StringCbCopy(currentFileName, bsize, fileName);
PTSTR str = Normalise(buff, hwnd);
SendMessage(hTextEditor, WM_SETTEXT, 0, (WPARAM)str);
SendMessage(hwnd, WM_SETTEXT, 0, (WPARAM)currentFileName);
if (str) {
HeapFree(processHeap, 0, str);
}
FreeBuff:
HeapFree(processHeap, 0, buff);
Exit:
CloseHandle(hFile);
}
運(yùn)行代碼,并打開(kāi)文件,可以看到,程序讀取了文件內(nèi)容,并將內(nèi)容顯示在編輯器內(nèi),并且主窗體的標(biāo)題變?yōu)楫?dāng)前打開(kāi)的文件路徑:
九、響應(yīng)編輯器內(nèi)容變化事件
雖然我們已經(jīng)實(shí)現(xiàn)了讀取并顯示文本文件內(nèi)容的功能,但是如果你對(duì)編輯器內(nèi)的文本進(jìn)行修改,就會(huì)發(fā)現(xiàn),我們主窗體的標(biāo)題沒(méi)有發(fā)生變化。
如果要在文本編輯器內(nèi)的文本發(fā)生變化之后,響應(yīng)該變化,應(yīng)該怎么辦呢?
還記得之前,我們?cè)谔幚砻钕⒌臅r(shí)候,有 hwndCtl 和 codeNotify參數(shù)嗎?當(dāng)編輯器控件的內(nèi)容發(fā)生變化后,該控件會(huì)向其父窗體(也就是我們的主窗體)發(fā)送一個(gè) WM_COMMAND 消息,并且傳入 EN_CHANGE 通知參數(shù),處理命令函數(shù)中,響應(yīng) EN_CHANGE 通知,修改我們的標(biāo)題即可。
由于在修改文本之后,我們需要固定在標(biāo)題之前添加一個(gè) '*',其他部分和文件名是完全一樣的,所以,我們?cè)诜峙渎窂絻?nèi)存時(shí),多分配一個(gè)字符的空間,將 currentFileName 指針指向新內(nèi)存的第一個(gè)字符,這樣,之后修改標(biāo)題文本的時(shí)候,就不選喲重新分配內(nèi)存了。
我們把打開(kāi)文件的代碼修改如下:
VOID OpenNewFile(PWSTR fileName, HWND hwnd) {
LARGE_INTEGER size;
PBYTE buff = NULL;
HANDLE processHeap = NULL;
DWORD readSize = 0;
HANDLE hFile = CreateFile(
fileName,
GENERIC_ALL,
0,
NULL,
OPEN_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
NULL
);
if (INVALID_HANDLE_VALUE == hFile) {
DisplayError(TEXT("CreateFile"), hwnd);
return;
}
if (!GetFileSizeEx(hFile, &size)) {
DisplayError(TEXT("GetFileSizeEx"), hwnd);
goto Exit;
}
if ((processHeap = GetProcessHeap()) == NULL) {
DisplayError(TEXT("GetProcessHeap"), hwnd);
goto Exit;
}
buff = (PBYTE)HeapAlloc(
processHeap,
HEAP_ZERO_MEMORY,
(SIZE_T)(size.QuadPart + 8));
if (NULL == buff) {
MessageBox(
hwnd,
TEXT("alloc memory error."),
TEXT("Error"),
MB_OK
);
goto Exit;
}
if (!ReadFile(
hFile, buff,
(DWORD)size.QuadPart,
&readSize,
NULL
)) {
MessageBox(
hwnd,
TEXT("ReadFile error."),
TEXT("Error"),
MB_OK
);
goto FreeBuff;
}
// 因?yàn)閷?duì)話框關(guān)閉之后,將會(huì)釋放掉文件路徑的內(nèi)存
// 所以這里,我們重新分配內(nèi)存,并拷貝一份路徑
// 在這之前,需要判斷當(dāng)前文件名是否指向了一個(gè)地址,
// 如果有指向,應(yīng)將其釋放。
if (currentFileName) {
HeapFree(GetProcessHeap(), 0, currentFileName - 1);
}
size_t bsize = (wcslen(fileName) + 2) * sizeof(WCHAR);
currentFileName = (PWSTR)NewMemory(bsize, hwnd);
if (!currentFileName) {
goto FreeBuff;
}
currentFileName[0] = (WCHAR)'*';
currentFileName = ((PWCHAR)currentFileName) + 1;
StringCbCopy(currentFileName, bsize, fileName);
PTSTR str = Normalise(buff, hwnd);
SendMessage(hTextEditor, WM_SETTEXT, 0, (WPARAM)str);
SendMessage(hwnd, WM_SETTEXT, 0, (WPARAM)currentFileName);
if (str) {
HeapFree(processHeap, 0, str);
}
FreeBuff:
HeapFree(processHeap, 0, buff);
Exit:
CloseHandle(hFile);
}
重點(diǎn)關(guān)注72-73行,我們多分配了一個(gè)字符;
另外,還需要關(guān)注第65行,因?yàn)?currentFileName 指向的是分配內(nèi)存起始地址之后,所以釋放內(nèi)存的時(shí)候,要傳入 currentFileName - 1。
同時(shí),我們新增一個(gè)標(biāo)識(shí)文本是否變更的變量,如下:
BOOL textChanged = FALSE;
然后,修改我們的命令處理程序的默認(rèn)分支如下:
default:
if (hwndCtl != NULL) {
switch (codeNotify)
{
case EN_CHANGE:
if (!textChanged && currentFileName != NULL) {
SendMessage(
hwnd,
WM_SETTEXT,
0,
(LPARAM)((((PWCHAR)currentFileName)) - 1)
);
}
textChanged = TRUE;
break;
default:
break;
}
}
break;
在這里,當(dāng)我們沒(méi)有打開(kāi)文件時(shí),標(biāo)題時(shí)不會(huì)發(fā)生變更的,但是變更標(biāo)識(shí)會(huì)同步變更。
接下來(lái),運(yùn)行程序,打開(kāi)一個(gè)文件,做出任何的編輯,可以看到,在編輯之后,我們主窗體的標(biāo)題均發(fā)生了變化。
補(bǔ)充一句,在調(diào)整窗體大小時(shí),發(fā)現(xiàn)編輯器的大小沒(méi)有隨主窗體的大小發(fā)生變化,這是因?yàn)椋覀儧](méi)有處理主窗體的大小變化消息,在主消息處理函數(shù)中,添加如下分支:
case WM_SIZE:
// 主窗體大小發(fā)生變化,我們要調(diào)整編輯控件大小。
return HANDLE_WM_SIZE(
hWnd, wParam, lParam, MainWindow_Cls_OnSize);
添加如下函數(shù)定義:
/**
* 作用:
* 處理主窗體的大小變更事件,這里只是調(diào)整文本編輯器
* 的大小。
*
* 參數(shù):
* hwnd
* 主窗體的句柄
*
* state
* 窗體大小發(fā)生變化的類型,如:最大化,最小化等
*
* cx
* 窗體工作區(qū)的新寬度
*
* cy
* 窗體工作區(qū)的新高度
*
* 返回值:
* 無(wú)
*/
VOID MainWindow_Cls_OnSize(
HWND hwnd, UINT state, int cx, int cy) {
MoveWindow(
hTextEditor,
0,
0,
cx,
cy,
TRUE
);
}
修改完成代碼,并保存,運(yùn)行程序,現(xiàn)在,我們的文本編輯器大小就會(huì)隨著主窗體大小的變化而變化了。
十、實(shí)現(xiàn)保存命令
類似于打開(kāi)文件的處理,我們先寫(xiě)一個(gè)獲取編輯器內(nèi)容,并將內(nèi)容寫(xiě)入文件(UTF8)的函數(shù),如下:
/**
* 作用:
* 將給定的 byte 數(shù)組中的 bSize 個(gè)子接,寫(xiě)入 file 指定
* 的文件中。
*
* 參數(shù):
* bytes
* 要寫(xiě)入目標(biāo)文件的 byte 數(shù)組。
*
* bSize
* 要寫(xiě)入目標(biāo)文件的字節(jié)數(shù)量。
*
* file
* 要寫(xiě)入內(nèi)容的目標(biāo)文件名。
*
* hwnd
* 出現(xiàn)錯(cuò)誤時(shí),本函數(shù)會(huì)彈出對(duì)話框,
* 此參數(shù)為對(duì)話框的父窗體句柄。
*
* 返回值:
* 無(wú)
*/
VOID WriteBytesToFile(
PBYTE bytes,
size_t bSize,
PWSTR file,
HWND hwnd
) {
DWORD numberOfBytesWritten = 0;
HANDLE hFile = CreateFile(
file,
GENERIC_WRITE,
0,
NULL,
CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
NULL
);
if (INVALID_HANDLE_VALUE == hFile) {
DisplayError(TEXT("CreateFile"), hwnd);
return;
}
if (!WriteFile(
hFile,
bytes,
bSize,
&numberOfBytesWritten,
NULL
)) {
DisplayError(TEXT("WriteFile"), hwnd);
goto Exit;
}
Exit:
CloseHandle(hFile);
}
/**
* 作用:
* 保存當(dāng)前已經(jīng)打開(kāi)的文件,如果當(dāng)前沒(méi)有已打開(kāi)文件,
* 則調(diào)用另存為邏輯。
*
* 參數(shù):
* hwnd
* 出現(xiàn)錯(cuò)誤時(shí),本函數(shù)會(huì)彈出對(duì)話框,
* 此參數(shù)為對(duì)話框的父窗體句柄。
*
* 返回值:
* 無(wú)
*/
VOID SaveFile(HWND hwnd) {
size_t cch = 0;
size_t bSize = 0;
PWCHAR buffWStr = NULL;
PBYTE utf8Buff = NULL;
// 如果當(dāng)前沒(méi)有打開(kāi)任何文件,當(dāng)前忽略
if (!currentFileName) {
return;
}
// 獲取文本編輯器的文本字符數(shù)量。
cch = SendMessage(
hTextEditor, WM_GETTEXTLENGTH, 0, 0);
// 獲取字符時(shí),我們是通過(guò) UTF16 格式(WCHAR)獲取,
// 我們要在最后添加一個(gè)空白結(jié)尾標(biāo)志字符
buffWStr = (PWCHAR)NewMemory(
cch * sizeof(WCHAR) + sizeof(WCHAR), hwnd);
if (buffWStr == NULL) {
return;
}
// 獲取到編輯器的文本
SendMessage(
hTextEditor,
WM_GETTEXT,
cch + 1,
(WPARAM)buffWStr
);
// 獲取將文本以 UTF8 格式編碼后所需的內(nèi)存大小(BYTE)
bSize = WideCharToMultiByte(
CP_UTF8,
0,
buffWStr,
cch,
NULL,
0,
NULL,
NULL
);
utf8Buff = NewMemory(bSize, hwnd);
if (utf8Buff == NULL) {
goto Exit;
}
// 將文本格式化到目標(biāo)緩存
WideCharToMultiByte(
CP_UTF8,
0,
buffWStr,
cch,
utf8Buff,
bSize,
NULL,
NULL
);
// 將內(nèi)容覆蓋到目標(biāo)文件。
WriteBytesToFile(
utf8Buff, bSize, currentFileName, hwnd);
// 保存完成之后,設(shè)置文本變更標(biāo)識(shí)為 FALSE,
// 并設(shè)置主窗體標(biāo)題為當(dāng)前文件路徑。
SendMessage(hwnd, WM_SETTEXT, 0, (LPARAM)currentFileName);
HeapFree(GetProcessHeap(), 0, utf8Buff);
Exit:
HeapFree(GetProcessHeap(), 0, buffWStr);
}
接下來(lái),將我們的保存命令處理分支稍作修改,調(diào)用 SaveFile 函數(shù),如下:
case ID_SAVE:
SaveFile(hwnd);
break;
運(yùn)行程序,打開(kāi)一個(gè)文件,編輯,保存,看看標(biāo)題的 * 是否按照預(yù)想顯示和消失,文件是否正常保存?
在這里,如果沒(méi)有已經(jīng)打開(kāi)的文件,我們是忽略保存命令的,這我們將在實(shí)現(xiàn)另存為命令之后,再回來(lái)解決這個(gè)問(wèn)題。
十一、實(shí)現(xiàn)另存為命令
對(duì)于另存為命令,和保存命令的主要區(qū)別,就是另存為命令需要讓用戶選擇一個(gè)保存目標(biāo)文件名,然后,其他邏輯就和保存的邏輯一樣了。
讓我們實(shí)現(xiàn)另存為函數(shù),如下:
/**
* 作用:
* 彈出另存為對(duì)話框,在用戶選擇一個(gè)文件路徑之后,
* 回調(diào) pfCallback 函數(shù)指針指向的函數(shù)。
*
* 參數(shù):
* pfCallback
* 一個(gè)函數(shù)指針,用于執(zhí)行用戶選擇一個(gè)保存路徑
* 之后的操作。
*
* hwnd
* 出錯(cuò)情況下,彈出錯(cuò)誤對(duì)話框的父窗體句柄。
*
* 返回值:
* 無(wú)
*/
VOID SaveFileAs(Func_PWSTR_HWND pfCallback, HWND hwnd) {
// 每次調(diào)用之前,應(yīng)該先初始化 COM 組件環(huán)境
HRESULT hr = CoInitializeEx(
NULL,
COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE
);
if (SUCCEEDED(hr))
{
IFileSaveDialog* pFileSave = NULL;
// 創(chuàng)建一個(gè) FileOpenDialog 實(shí)例
hr = CoCreateInstance(
&CLSID_FileSaveDialog,
NULL,
CLSCTX_ALL,
&IID_IFileSaveDialog,
&pFileSave
);
if (SUCCEEDED(hr))
{
// 設(shè)置打開(kāi)文件擴(kuò)展名
pFileSave->lpVtbl->SetFileTypes(
pFileSave,
_countof(SUPPORTED_FILE_TYPES),
SUPPORTED_FILE_TYPES
);
// 顯示選擇文件對(duì)話框
hr = pFileSave->lpVtbl->Show(pFileSave, hwnd);
// Get the file name from the dialog box.
if (SUCCEEDED(hr))
{
IShellItem* pItem;
hr = pFileSave->lpVtbl->GetResult(pFileSave, &pItem);
if (SUCCEEDED(hr))
{
PWSTR pszFilePath;
hr = pItem->lpVtbl->GetDisplayName(
pItem, SIGDN_FILESYSPATH, &pszFilePath);
// Display the file name to the user.
if (SUCCEEDED(hr))
{
if (pfCallback) {
pfCallback(pszFilePath, hwnd);
}
CoTaskMemFree(pszFilePath);
}
pItem->lpVtbl->Release(pItem);
}
}
pFileSave->lpVtbl->Release(pFileSave);
}
CoUninitialize();
}
}
以上函數(shù)只是實(shí)現(xiàn)了彈出對(duì)話框,獲取另存為路徑的功能,讓我們?cè)偬砑右粋€(gè)獲取路徑之后的處理函數(shù),如下:
/**
* 作用:
* 將當(dāng)前內(nèi)容保存到 fileName,并且設(shè)置 currentFileName
* 為 fileName。
*
* 參數(shù):
* fileName
* 要將當(dāng)前內(nèi)容保存到的目標(biāo)路徑
*
* hwnd
* 出錯(cuò)彈出消息框時(shí),消息框的父窗體句柄。
*
* 返回值:
* 無(wú)
*/
VOID SaveFileTo(PWSTR fileName, HWND hwnd) {
size_t len = lstrlen(fileName);
int bSize = len * sizeof(WCHAR);
int appendSuffix = !(
fileName[len - 4] == '.' &&
fileName[len - 3] == 't' &&
fileName[len - 2] == 'x' &&
fileName[len - 1] == 't');
if (appendSuffix) {
bSize += 5 * sizeof(WCHAR);
}
if (currentFileName) {
HeapFree(GetProcessHeap(), 0, currentFileName);
currentFileName = NULL;
}
currentFileName = (PWSTR)NewMemory(bSize, hwnd);
if (!currentFileName) {
return;
}
StringCbCopy(currentFileName, bSize, fileName);
if (appendSuffix) {
currentFileName[len + 0] = '.';
currentFileName[len + 1] = 't';
currentFileName[len + 2] = 'x';
currentFileName[len + 3] = 't';
currentFileName[len + 4] = '\0';
}
SaveFile(hwnd);
}
該函數(shù)的工作很簡(jiǎn)單,就是解析獲取到的路徑,如果路徑最后不是以 ".txt" 結(jié)尾,則添加 ".txt" 擴(kuò)展,最后調(diào)用保存文件的邏輯。
接下來(lái),讓我們修改 ID_SAVE_AS 命令分支代碼:
case ID_SAVE_AS:
SaveFileAs(SaveFileTo, hwnd);
break;
最后,還記得之前我們編輯保存邏輯時(shí),省略了當(dāng)前打開(kāi)文件名為 NULL 時(shí)的處理嗎?現(xiàn)在是時(shí)候處理這種情況了,處理方式很簡(jiǎn)單,就是掉喲個(gè)另存為邏輯。
將SaveFile 函數(shù)做如下修改:
VOID SaveFile(HWND hwnd) {
size_t cch = 0;
size_t bSize = 0;
PWCHAR buffWStr = NULL;
PBYTE utf8Buff = NULL;
// 如果當(dāng)前沒(méi)有打開(kāi)任何文件,則調(diào)用另存為邏輯,
// 讓用戶選擇一個(gè)文件名進(jìn)行保存,然后退出。
if (!currentFileName) {
SaveFileAs(SaveFileTo, hwnd);
return;
}
// 獲取文本編輯器的文本字符數(shù)量。
cch = SendMessage(
hTextEditor, WM_GETTEXTLENGTH, 0, 0);
// 獲取字符時(shí),我們是通過(guò) UTF16 格式(WCHAR)獲取,
// 我們要在最后添加一個(gè)空白結(jié)尾標(biāo)志字符
buffWStr = (PWCHAR)NewMemory(
cch * sizeof(WCHAR) + sizeof(WCHAR), hwnd);
if (buffWStr == NULL) {
return;
}
// 獲取到編輯器的文本
SendMessage(
hTextEditor,
WM_GETTEXT,
cch + 1,
(WPARAM)buffWStr
);
// 獲取將文本以 UTF8 格式編碼后所需的內(nèi)存大小(BYTE)
bSize = WideCharToMultiByte(
CP_UTF8,
0,
buffWStr,
cch,
NULL,
0,
NULL,
NULL
);
utf8Buff = NewMemory(bSize, hwnd);
if (utf8Buff == NULL) {
goto Exit;
}
// 將文本格式化到目標(biāo)緩存
WideCharToMultiByte(
CP_UTF8,
0,
buffWStr,
cch,
utf8Buff,
bSize,
NULL,
NULL
);
// 將內(nèi)容覆蓋到目標(biāo)文件。
WriteBytesToFile(
utf8Buff, bSize, currentFileName, hwnd);
// 保存完成之后,設(shè)置文本變更標(biāo)識(shí)為 FALSE,
// 并設(shè)置主窗體標(biāo)題為當(dāng)前文件路徑。
SendMessage(hwnd, WM_SETTEXT, 0, (LPARAM)currentFileName);
HeapFree(GetProcessHeap(), 0, utf8Buff);
Exit:
HeapFree(GetProcessHeap(), 0, buffWStr);
}
在第10行,我們添加了調(diào)用另存為邏輯的代碼。
另外需要說(shuō)明的是,由于 SaveFileTo函數(shù)調(diào)用了SaveFile函數(shù),SaveFile 函數(shù)也調(diào)用了 SaveFileTo 函數(shù),由于在C語(yǔ)言中,必須先聲明,才能夠使用,所以需要按照你代碼的為止,對(duì)函數(shù)進(jìn)行提前聲明。
在這里,我將SaveFileTo函數(shù)的實(shí)現(xiàn)放到了 SaveFile函數(shù)的后面,所以需要在SaveFile之前添加SaveFileTo函數(shù)的額聲明,如下:
VOID SaveFileTo(PWSTR fileName, HWND hwnd);
到此為止,運(yùn)行我們的程序,看看它是否能夠正常工作?
我們先點(diǎn)擊另存為,保存一個(gè)新文件,然后再打開(kāi)另一個(gè)文件,然后,程序報(bào)異常了。
為什么?
還記得之前我們處理打開(kāi)文件的邏輯嗎?每次分配內(nèi)存的時(shí)候,我們都多分配了一個(gè)字符的空間,currentFileName 指向的不是分配內(nèi)存的起始地址。
讓我們看看SaveFileTo 函數(shù)的邏輯,發(fā)現(xiàn)我們沒(méi)有做相同的處理,所以釋放內(nèi)存的時(shí)候,報(bào)錯(cuò)了。
讓我們將SaveFileTo的代碼改成這樣:
VOID SaveFileTo(PWSTR fileName, HWND hwnd) {
size_t len = lstrlen(fileName);
int bSize = len * sizeof(WCHAR);
int appendSuffix = !(
fileName[len - 4] == '.' &&
fileName[len - 3] == 't' &&
fileName[len - 2] == 'x' &&
fileName[len - 1] == 't');
if (appendSuffix) {
bSize += 5 * sizeof(WCHAR);
}
if (currentFileName) {
HeapFree(GetProcessHeap(), 0, currentFileName - 1);
currentFileName = NULL;
}
currentFileName = (PWSTR)NewMemory(bSize + sizeof(WCHAR), hwnd);
if (!currentFileName) {
return;
}
currentFileName = currentFileName + 1;
StringCbCopy(currentFileName, bSize, fileName);
if (appendSuffix) {
currentFileName[len + 0] = '.';
currentFileName[len + 1] = 't';
currentFileName[len + 2] = 'x';
currentFileName[len + 3] = 't';
currentFileName[len + 4] = '\0';
}
SaveFile(hwnd);
}
再試試?
為什么第一次保存之前,文本變化的反應(yīng)是正確的,一旦調(diào)用保存之后,文本變化之后,主窗體的標(biāo)題沒(méi)有變化?
原來(lái)是保存文件成功之后,沒(méi)有更新內(nèi)容變化標(biāo)識(shí)。修改SaveFile函數(shù),在保存完成后,添加如下語(yǔ)句:
textChanged = FALSE;
再試試?終于正常工作了。
十二、整理我們的代碼,按照功能進(jìn)行分離
至此,我們已經(jīng)得到了一個(gè)正常工作的基礎(chǔ)編輯器。但所有代碼合在一起,有些凌亂,讓我們整理下結(jié)構(gòu)。
首先,我們將和編輯功能,窗體顯示功能相關(guān)的代碼,都放到 WinTextEditor.c 中,然后添加一個(gè) InitEnv 函數(shù),在主程序中調(diào)用該函數(shù)以初始化ii能夠。
現(xiàn)在,main.c 中只剩下了主程序,如下:
#include "WinTextEditor.h"
int WINAPI wWinMain(
_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR lpCmdLine,
_In_ int nShowCmd
) {
MSG msg;
BOOL fGotMessage = FALSE;
if (!InitEnv(hInstance, nShowCmd)) {
return 0;
}
while ((fGotMessage = GetMessage(&msg, (HWND)NULL, 0, 0)) != 0
&& fGotMessage != -1)
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return msg.wParam;
}
在頭文件 WinTextEditor.h 中,我們對(duì)外聲明了一個(gè) InitEnv 函數(shù),其內(nèi)容如下:
#include <Windows.h>
#include <windowsx.h>
#include <strsafe.h>
#include <stdlib.h>
#include <ShlObj.h>
#include "resource.h"
BOOL InitEnv(HINSTANCE hInstance, int nShowCmd);
接下來(lái),按照相同的步驟,分別抽象出,錯(cuò)誤處理、文件操作等模塊,最終,我們的文件結(jié)構(gòu)如下:
十三、可能遇到的問(wèn)題
- 編譯器警告(等級(jí) 1)C4819
這個(gè)問(wèn)題是由于源代碼文件保存編碼不是Unicode字符集造成的,當(dāng)前Visual Studio內(nèi)沒(méi)有合適的配置能夠解決這個(gè)問(wèn)題。
但是,通過(guò)測(cè)試,可以通過(guò)記事本打開(kāi)文件,并將源代碼保存為帶BOM的UTF8編碼,解決這個(gè)問(wèn)題。
- 編輯資源文件的時(shí)候,提示錯(cuò)誤
這個(gè)問(wèn)題,在之前編輯文件的時(shí)候說(shuō)過(guò)了,可以通過(guò)在資源文件中添加字符編碼聲明解決。
最后的最后,歡迎關(guān)注公眾號(hào) [編程之路漫漫],下次,讓我們不通過(guò)使用Win32控件,實(shí)現(xiàn)一個(gè)二進(jìn)制編輯器。
碼途求知己,天涯覓一心。