亚洲精品久久国产精品37p,亚洲av无码av制服另类专区,午夜直播免费看,玩弄人妻少妇500系列视频,无码人妻久久久一区二区三区

曾經(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)該有如下功能:

  1. 能夠打開(kāi)一個(gè)文本文件(通過(guò)打開(kāi)文件對(duì)話框);
  2. 能夠?qū)ξ谋具M(jìn)行編輯;
  3. 能夠?qū)⑽募4妫?/li>
  4. 文件保存時(shí),如果當(dāng)前沒(méi)有已打開(kāi)任何文件,則顯示文件保存對(duì)話框。
  5. 能夠?qū)⑽募泶鏋榱硗饴窂?,保存后打開(kāi)內(nèi)容為另存為路徑;
  6. 在主窗體顯示當(dāng)前打開(kāi)文件的文件名;
  7. 如果文件已編輯,并且未保存,主窗體標(biāo)題前加'*';
  8. 如果文件保存,則去除主窗體標(biāo)題前的'*';

為了能夠?qū)ξ覀兘酉聛?lái)要做的事情有一個(gè)整體印象,讓我們?cè)谶@里對(duì)本文要實(shí)現(xiàn)一個(gè)簡(jiǎn)單記事本功能的計(jì)劃說(shuō)明,我們的簡(jiǎn)單步驟如下:

  1. 說(shuō)說(shuō)如何對(duì)一個(gè)C語(yǔ)言項(xiàng)目進(jìn)行設(shè)置,以創(chuàng)建一個(gè)GUI應(yīng)用程序;
  2. 聊聊入口函數(shù);
  3. 使用C語(yǔ)言創(chuàng)建一個(gè)窗體;
  4. 為我們的窗體添加一個(gè)菜單,并添加菜單命令;
  5. 添加編輯器;
  6. 響應(yīng)菜單命令;
  7. 實(shí)現(xiàn)退出命令;
  8. 實(shí)現(xiàn)打開(kāi)文件命令;
  9. 響應(yīng)編輯器內(nèi)容變化事件;
  10. 實(shí)現(xiàn)保存命令;
  11. 實(shí)現(xiàn)另存為命令;
  12. 整理我們的代碼,按照功能進(jìn)行分離;
  13. 最后,我們聊聊整個(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)目了。主要步驟如下:

  1. 啟動(dòng) Visual Studio,并點(diǎn)擊“創(chuàng)建新項(xiàng)目”按鈕
  2. 選擇項(xiàng)目類型
  3. 設(shè)置項(xiàng)目源代碼目錄以及項(xiàng)目名稱
  4. 設(shè)置項(xiàng)目類型
  5. 新建一個(gè)主程序文件
  6. 編輯開(kāi)始代碼
  7. 編譯運(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)文件功能,我們可以將其分成如下步驟:

  1. 彈出打開(kāi)文件對(duì)話框;
  2. 獲取文件大小;
  3. 分配文件大小相等的內(nèi)存;
  4. 將文件內(nèi)容讀取到分配的內(nèi)存;
  5. 設(shè)置主窗體標(biāo)題為文件名;
  6. 設(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)制編輯器。

碼途求知己,天涯覓一心。

評(píng)論(0條)