【C++实战(68)】从0到1:C++跨平台开发之Windows API深度实战

目录

  • [一、Windows API 的基础概念](#一、Windows API 的基础概念)
    • [1.1 Windows API 的定义](#1.1 Windows API 的定义)
    • [1.2 Windows 开发环境搭建](#1.2 Windows 开发环境搭建)
    • [1.3 Windows 消息机制原理](#1.3 Windows 消息机制原理)
  • [二、Windows 窗口与控件实战](#二、Windows 窗口与控件实战)
    • [2.1 窗口的创建](#2.1 窗口的创建)
    • [2.2 常用控件的创建与消息处理](#2.2 常用控件的创建与消息处理)
    • [2.3 菜单与工具栏的创建与使用](#2.3 菜单与工具栏的创建与使用)
  • [三、Windows 文件与注册表操作](#三、Windows 文件与注册表操作)
    • [3.1 文件操作 API 的使用](#3.1 文件操作 API 的使用)
    • [3.2 注册表操作 API 的应用](#3.2 注册表操作 API 的应用)
    • [3.3 对话框的创建与模态 / 非模态处理](#3.3 对话框的创建与模态 / 非模态处理)
  • [四、实战项目:简易记事本(Windows API 版)](#四、实战项目:简易记事本(Windows API 版))
    • [4.1 项目需求](#4.1 项目需求)
    • [4.2 窗口创建与文件 API 实现核心功能](#4.2 窗口创建与文件 API 实现核心功能)
    • [4.3 消息处理与用户交互优化](#4.3 消息处理与用户交互优化)

一、Windows API 的基础概念

1.1 Windows API 的定义

Windows API(Application Programming Interface)即 Windows 应用程序编程接口,是微软 Windows 操作系统提供给应用程序开发者的一套函数接口,用于访问操作系统的核心功能和服务。它以 C 风格函数的形式呈现,为开发者提供了调用系统功能的途径,使得开发者能够利用操作系统的各种资源,如窗口管理、图形绘制、文件操作、设备控制等。通过 Windows API,开发者可以创建具有丰富功能和良好用户体验的 Windows 应用程序,而无需深入了解操作系统内核的实现细节。例如,当我们需要在 Windows 系统中创建一个窗口时,就可以调用 Windows API 中的相关函数来完成这一操作,极大地简化了开发过程,提高了开发效率。

1.2 Windows 开发环境搭建

  1. Visual Studio 配置
    • 首先,从微软官方网站下载并安装最新版本的 Visual Studio。在安装过程中,确保选择 "使用 C++ 的桌面开发" 组件,这个组件包含了 Windows 开发所需的基本工具和库。
    • 安装完成后,打开 Visual Studio。选择 "工具" 菜单中的 "选项",在弹出的对话框中,找到 "项目和解决方案" -> "VC++ 目录"。在这里,你可以设置包含目录、库目录等路径,以确保编译器能够找到所需的头文件和库文件。例如,Windows API 的头文件通常位于系统安装目录下的 "\Program Files (x86)\Windows Kits\10\Include" 路径下,库文件位于 "\Program Files (x86)\Windows Kits\10\Lib" 路径下。
  2. Win32 项目创建
    • 打开 Visual Studio 后,选择 "文件" -> "新建" -> "项目"。
    • 在 "新建项目" 对话框中,左侧选择 "Visual C++",中间选择 "Win32",然后在右侧选择 "Win32 项目"。输入项目名称并选择项目保存路径,点击 "确定"。
    • 在弹出的 "Win32 应用程序向导" 中,点击 "下一步"。在 "应用程序设置" 页面,选择 "应用程序类型" 为 "Windows 应用程序","附加选项" 中勾选 "空项目",然后点击 "完成"。这样,一个空的 Win32 项目就创建好了。接下来,你可以在项目中添加源文件、头文件等,开始编写 Windows API 代码。

1.3 Windows 消息机制原理

  1. 消息队列:Windows 操作系统采用消息驱动机制来管理应用程序的交互。消息队列是一个先进先出(FIFO)的数据结构,用于存放应用程序接收到的各种消息。这些消息可以是用户输入(如鼠标点击、键盘按键)、系统事件(如窗口创建、销毁)、定时器事件等。当一个事件发生时,操作系统会将相应的消息放入应用程序的消息队列中。例如,当用户点击鼠标左键时,操作系统会生成一个 WM_LBUTTONDOWN 消息,并将其放入应用程序的消息队列。
  2. 消息循环:应用程序通过消息循环来处理消息队列中的消息。在 WinMain 函数中,通常会有一个消息循环,其基本结构如下:
cpp 复制代码
MSG msg;
while (GetMessage(&msg, NULL, 0, 0))
{
    TranslateMessage(&msg);
    DispatchMessage(&msg);
}
  • GetMessage:该函数从应用程序的消息队列中获取一条消息,并将消息的相关信息填充到 MSG 结构体中。如果消息队列中没有消息,GetMessage 函数会使线程进入等待状态,直到有新消息到达。当获取到 WM_QUIT 消息时,GetMessage 函数返回 0,消息循环结束,应用程序退出。
  • TranslateMessage:主要用于处理键盘消息,将虚拟键消息转换为字符消息。例如,当用户按下并释放一个按键时,TranslateMessage 会将按键的扫描码转换为对应的字符消息,并将其放入消息队列中,以便后续处理。
  • DispatchMessage:将消息发送到相应的窗口过程函数(WndProc)进行处理。它会根据消息中的窗口句柄,找到对应的窗口过程函数,并将消息传递给它。
  1. 窗口过程:窗口过程函数是一个回调函数,用于处理特定窗口接收到的消息。每个窗口都有一个与之关联的窗口过程函数,其原型如下:
cpp 复制代码
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);
  • hwnd:窗口句柄,标识接收消息的窗口。
  • message:消息标识符,用于区分不同类型的消息,如 WM_PAINT 表示窗口需要重绘,WM_COMMAND 表示菜单或控件的命令消息等。
  • wParam和lParam:消息的附加参数,其含义取决于消息的类型。例如,在 WM_LBUTTONDOWN 消息中,wParam 包含了鼠标按键的状态信息,lParam 包含了鼠标点击的坐标信息。在窗口过程函数中,通过 switch 语句根据不同的消息类型进行相应的处理。例如,当接收到 WM_PAINT 消息时,通常会调用绘图函数来绘制窗口的内容;当接收到 WM_DESTROY 消息时,会进行一些清理工作,并发送 WM_QUIT 消息来结束应用程序的消息循环。

二、Windows 窗口与控件实战

2.1 窗口的创建

在 Windows API 中,创建窗口需要经过以下几个关键步骤:

  1. 注册窗口类(RegisterClassEx):在创建窗口之前,首先要注册一个窗口类,以便操作系统了解窗口的基本属性和行为。RegisterClassEx 函数用于完成这一操作,其函数原型如下:
cpp 复制代码
ATOM RegisterClassEx(const WNDCLASSEX *lpwcx);
  • 参数说明
    • lpwcx:指向一个 WNDCLASSEX 结构体的指针,该结构体包含了窗口类的详细信息,如窗口过程函数指针、图标、光标、背景画刷、菜单名、类名等。
cpp 复制代码
typedef struct _WNDCLASSEX {
    UINT        cbSize;         // 结构体大小,通常设置为 sizeof(WNDCLASSEX)
    UINT        style;          // 窗口类的样式,如CS_HREDRAW(水平大小改变时重绘)、CS_VREDRAW(垂直大小改变时重绘)等
    WNDPROC     lpfnWndProc;    // 窗口过程函数指针,用于处理窗口收到的消息
    int         cbClsExtra;     // 窗口类额外的字节数,一般设为0
    int         cbWndExtra;     // 窗口实例额外的字节数,一般设为0
    HINSTANCE   hInstance;      // 应用程序实例句柄
    HICON       hIcon;          // 窗口类的图标句柄
    HCURSOR     hCursor;        // 窗口类的光标句柄
    HBRUSH      hbrBackground;  // 窗口类的背景画刷句柄
    LPCTSTR     lpszMenuName;   // 窗口类的菜单名,若没有菜单则设为NULL
    LPCTSTR     lpszClassName;  // 窗口类的名称,必须唯一
    HICON       hIconSm;        // 窗口类的小图标句柄,通常与hIcon相同
} WNDCLASSEX, *PWNDCLASSEX;

例如,注册一个简单的窗口类:

cpp 复制代码
WNDCLASSEX wc = { 0 };
wc.cbSize = sizeof(WNDCLASSEX);
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = WndProc; // WndProc是自定义的窗口过程函数
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInstance; // hInstance为应用程序实例句柄,通常在WinMain函数中获取
wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wc.lpszMenuName = NULL;
wc.lpszClassName = L"MainWindowClass";
wc.hIconSm = LoadIcon(NULL, IDI_APPLICATION);

RegisterClassEx(&wc);
  1. 创建窗口(CreateWindowEx):注册窗口类后,使用 CreateWindowEx 函数在内存中创建窗口,其函数原型如下:
cpp 复制代码
HWND CreateWindowEx(
    DWORD dwExStyle,        // 扩展窗口样式,如WS_EX_CLIENTEDGE(带客户区边框)
    LPCTSTR lpClassName,    // 窗口类名,必须是已注册的类名
    LPCTSTR lpWindowName,   // 窗口标题
    DWORD dwStyle,          // 窗口样式,如WS_OVERLAPPEDWINDOW(标准重叠窗口样式)
    int x,                  // 窗口左上角的x坐标
    int y,                  // 窗口左上角的y坐标
    int nWidth,             // 窗口宽度
    int nHeight,            // 窗口高度
    HWND hWndParent,        // 父窗口句柄,若为顶级窗口则设为NULL
    HMENU hMenu,            // 菜单句柄,若没有菜单则设为NULL
    HINSTANCE hInstance,    // 应用程序实例句柄
    LPVOID lpParam          // 创建窗口时传递的参数,一般设为NULL
);

例如,创建一个窗口:

cpp 复制代码
HWND hWnd = CreateWindowEx(
    0,
    L"MainWindowClass",
    L"我的窗口",
    WS_OVERLAPPEDWINDOW,
    CW_USEDEFAULT, CW_USEDEFAULT,
    800, 600,
    NULL,
    NULL,
    hInstance,
    NULL
);
  1. 显示窗口(ShowWindow):窗口创建后,使用 ShowWindow 函数将窗口显示在屏幕上,其函数原型如下:
cpp 复制代码
BOOL ShowWindow(
    HWND hWnd,      // 窗口句柄
    int nCmdShow    // 窗口显示状态,如SW_SHOW(显示并激活窗口)、SW_SHOWMINIMIZED(显示并最小化窗口)等
);

例如,显示窗口:

cpp 复制代码
ShowWindow(hWnd, SW_SHOW);
UpdateWindow(hWnd); // 更新窗口,确保窗口内容正确显示

通过以上步骤,就可以在 Windows 系统中成功创建并显示一个窗口。

2.2 常用控件的创建与消息处理

  1. 按钮(Button):按钮是 Windows 应用程序中常用的交互控件,用于触发特定的操作。创建按钮使用 CreateWindowEx 函数,按钮的窗口类名为 "BUTTON"。
cpp 复制代码
HWND hButton = CreateWindowEx(
    0,
    L"BUTTON",
    L"点击我",
    WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
    100, 100,
    80, 30,
    hWnd,       // 父窗口句柄,hWnd为创建的主窗口句柄
    (HMENU)IDC_BUTTON1, // 按钮的ID,用于标识按钮,IDC_BUTTON1为自定义的ID
    hInstance,
    NULL
);

处理按钮点击消息,在窗口过程函数 WndProc 中添加以下代码:

cpp 复制代码
case WM_COMMAND:
    switch (LOWORD(wParam))
    {
    case IDC_BUTTON1:
        MessageBox(hWnd, L"按钮被点击了", L"提示", MB_OK);
        break;
    }
    break;
  1. 编辑框(Edit Box):编辑框用于用户输入文本。创建编辑框同样使用 CreateWindowEx 函数,窗口类名为 "EDIT"。
cpp 复制代码
HWND hEdit = CreateWindowEx(
    0,
    L"EDIT",
    L"",
    WS_CHILD | WS_VISIBLE | WS_BORDER | ES_MULTILINE | ES_AUTOVSCROLL,
    100, 150,
    200, 100,
    hWnd,
    (HMENU)IDC_EDIT1,
    hInstance,
    NULL
);

处理编辑框的输入消息,例如获取编辑框中的文本:

cpp 复制代码
case WM_COMMAND:
    switch (LOWORD(wParam))
    {
    case IDC_EDIT1:
        if (HIWORD(wParam) == EN_CHANGE)
        {
            TCHAR buffer[1024];
            GetWindowText(hEdit, buffer, sizeof(buffer) / sizeof(TCHAR));
            MessageBox(hWnd, buffer, L"编辑框内容", MB_OK);
        }
        break;
    }
    break;
  1. 列表框(List Box):列表框用于显示一系列选项供用户选择。创建列表框使用 CreateWindowEx 函数,窗口类名为 "LISTBOX"。
cpp 复制代码
HWND hListBox = CreateWindowEx(
    0,
    L"LISTBOX",
    L"",
    WS_CHILD | WS_VISIBLE | LBS_STANDARD,
    350, 100,
    150, 200,
    hWnd,
    (HMENU)IDC_LISTBOX1,
    hInstance,
    NULL
);

// 向列表框中添加选项
SendMessage(hListBox, LB_ADDSTRING, 0, (LPARAM)L"选项1");
SendMessage(hListBox, LB_ADDSTRING, 0, (LPARAM)L"选项2");
SendMessage(hListBox, LB_ADDSTRING, 0, (LPARAM)L"选项3");

处理列表框的选择消息:

cpp 复制代码
case WM_COMMAND:
    switch (LOWORD(wParam))
    {
    case IDC_LISTBOX1:
        if (HIWORD(wParam) == LBN_SELCHANGE)
        {
            int index = SendMessage(hListBox, LB_GETCURSEL, 0, 0);
            TCHAR buffer[1024];
            SendMessage(hListBox, LB_GETTEXT, index, (LPARAM)buffer);
            MessageBox(hWnd, buffer, L"选择的选项", MB_OK);
        }
        break;
    }
    break;

通过以上方式,可以创建常用的控件并处理它们的消息,实现与用户的交互。

2.3 菜单与工具栏的创建与使用

  1. 菜单的创建与使用
    • 创建菜单资源文件:在 Visual Studio 中,可以通过资源编辑器创建菜单资源文件(.rc)。在资源视图中右键点击项目,选择 "添加" -> "资源",然后选择 "Menu",点击 "新建"。在菜单编辑器中,可以添加菜单项、子菜单等。例如,创建一个简单的文件菜单,包含 "新建" 和 "退出" 两个菜单项:
cpp 复制代码
IDR_MAINMENU MENU
BEGIN
    POPUP "&文件"
    BEGIN
        MENUITEM "&新建", ID_FILE_NEW
        MENUITEM SEPARATOR
        MENUITEM "&退出", ID_APP_EXIT
    END
END
  • 加载菜单:在窗口创建后,使用 SetMenu 函数将菜单加载到窗口中:
cpp 复制代码
HMENU hMenu = LoadMenu(hInstance, MAKEINTRESOURCE(IDR_MAINMENU));
SetMenu(hWnd, hMenu);
  • 处理菜单命令消息:在窗口过程函数 WndProc 中处理菜单命令消息:
cpp 复制代码
case WM_COMMAND:
    switch (LOWORD(wParam))
    {
    case ID_FILE_NEW:
        // 处理新建文件操作
        MessageBox(hWnd, L"新建文件", L"提示", MB_OK);
        break;
    case ID_APP_EXIT:
        // 处理退出应用程序操作
        DestroyWindow(hWnd);
        break;
    }
    break;
  1. 工具栏的创建与使用
    • 创建工具栏:首先,需要创建一个工具栏的图像列表(Image List),用于存储工具栏按钮的图标。然后使用 CreateToolbarEx 函数创建工具栏。
cpp 复制代码
// 创建图像列表
HIMAGELIST hImageList = ImageList_Create(16, 16, ILC_COLOR32, 2, 0);
HBITMAP hBitmap = (HBITMAP)LoadImage(hInstance, MAKEINTRESOURCE(IDB_BITMAP1), IMAGE_BITMAP, 0, 0, LR_DEFAULTCOLOR);
ImageList_Add(hImageList, hBitmap, NULL);

// 创建工具栏
HWND hToolBar = CreateToolbarEx(
    hWnd,
    WS_CHILD | WS_VISIBLE | TBSTYLE_FLAT | TBSTYLE_LIST,
    IDR_TOOLBAR,
    2,
    hInstance,
    0,
    hImageList,
    NULL,
    0,
    0,
    0,
    0,
    sizeof(TBBUTTON)
);
  • 添加按钮到工具栏:使用 SendMessage 函数向工具栏添加按钮:
cpp 复制代码
TBBUTTON buttons[2];
buttons[0].iBitmap = 0; // 图像列表中的图标索引
buttons[0].idCommand = ID_FILE_NEW; // 按钮对应的命令ID
buttons[0].fsState = TBSTATE_ENABLED; // 按钮状态
buttons[0].fsStyle = TBSTYLE_BUTTON; // 按钮样式
buttons[0].dwData = 0;
buttons[0].iString = 0;

buttons[1].iBitmap = 1;
buttons[1].idCommand = ID_APP_EXIT;
buttons[1].fsState = TBSTATE_ENABLED;
buttons[1].fsStyle = TBSTYLE_BUTTON;
buttons[1].dwData = 0;
buttons[1].iString = 0;

SendMessage(hToolBar, TB_BUTTONSTRUCTSIZE, (WPARAM)sizeof(TBBUTTON), 0);
SendMessage(hToolBar, TB_ADDBUTTONS, 2, (LPARAM)&buttons);
  • 响应工具栏按钮点击事件:在窗口过程函数 WndProc 中处理工具栏按钮的点击消息,与处理菜单命令消息类似:
cpp 复制代码
case WM_COMMAND:
    switch (LOWORD(wParam))
    {
    case ID_FILE_NEW:
        // 处理新建文件操作
        MessageBox(hWnd, L"新建文件", L"提示", MB_OK);
        break;
    case ID_APP_EXIT:
        // 处理退出应用程序操作
        DestroyWindow(hWnd);
        break;
    }
    break;

通过以上步骤,可以在 Windows 应用程序中创建并使用菜单和工具栏,提升用户界面的交互性和便捷性。

三、Windows 文件与注册表操作

3.1 文件操作 API 的使用

在 Windows API 中,文件操作是通过一系列函数来实现的,其中CreateFile、ReadFile、WriteFile和CloseHandle是最常用的几个函数。

  1. CreateFile:CreateFile函数用于打开或创建一个文件、设备、管道等对象,其函数原型如下:
cpp 复制代码
HANDLE CreateFile(
    LPCTSTR lpFileName,         // 文件名或设备名
    DWORD dwDesiredAccess,      // 访问模式,如GENERIC_READ、GENERIC_WRITE等
    DWORD dwShareMode,          // 共享模式,如FILE_SHARE_READ、FILE_SHARE_WRITE等
    LPSECURITY_ATTRIBUTES lpSecurityAttributes, // 安全属性,通常设为NULL
    DWORD dwCreationDisposition, // 创建方式,如CREATE_NEW、CREATE_ALWAYS等
    DWORD dwFlagsAndAttributes, // 文件属性和标志,如FILE_ATTRIBUTE_NORMAL等
    HANDLE hTemplateFile        // 模板文件句柄,通常设为NULL
);

例如,打开一个文件用于读取:

cpp 复制代码
HANDLE hFile = CreateFile(
    L"test.txt",
    GENERIC_READ,
    FILE_SHARE_READ,
    NULL,
    OPEN_EXISTING,
    FILE_ATTRIBUTE_NORMAL,
    NULL
);
if (hFile == INVALID_HANDLE_VALUE)
{
    // 处理打开文件失败的情况
    DWORD dwError = GetLastError();
    // 可以根据dwError的值进行相应的错误处理,如输出错误信息
    wprintf(L"打开文件失败,错误代码: %d\n", dwError);
}
  1. ReadFile:ReadFile函数用于从文件中读取数据,其函数原型如下:
cpp 复制代码
BOOL ReadFile(
    HANDLE hFile,              // 文件句柄
    LPVOID lpBuffer,           // 用于存储读取数据的缓冲区
    DWORD nNumberOfBytesToRead, // 要读取的字节数
    LPDWORD lpNumberOfBytesRead, // 实际读取的字节数
    LPOVERLAPPED lpOverlapped  // 用于异步I/O,通常设为NULL
);

例如,从文件中读取数据:

cpp 复制代码
TCHAR buffer[1024];
DWORD dwBytesRead;
BOOL bResult = ReadFile(
    hFile,
    buffer,
    sizeof(buffer) - 1,
    &dwBytesRead,
    NULL
);
if (bResult)
{
    buffer[dwBytesRead] = '\0'; // 确保字符串以null结尾
    // 处理读取到的数据
    wprintf(L"读取到的数据: %s\n", buffer);
}
else
{
    // 处理读取失败的情况
    DWORD dwError = GetLastError();
    wprintf(L"读取文件失败,错误代码: %d\n", dwError);
}
  1. WriteFile:WriteFile函数用于向文件中写入数据,其函数原型如下:
cpp 复制代码
BOOL WriteFile(
    HANDLE hFile,              // 文件句柄
    LPCVOID lpBuffer,          // 要写入的数据缓冲区
    DWORD nNumberOfBytesToWrite, // 要写入的字节数
    LPDWORD lpNumberOfBytesWritten, // 实际写入的字节数
    LPOVERLAPPED lpOverlapped  // 用于异步I/O,通常设为NULL
);

例如,向文件中写入数据:

cpp 复制代码
TCHAR data[] = L"Hello, Windows API!";
DWORD dwBytesWritten;
BOOL bWriteResult = WriteFile(
    hFile,
    data,
    wcslen(data) * sizeof(TCHAR),
    &dwBytesWritten,
    NULL
);
if (bWriteResult)
{
    // 处理写入成功的情况
    wprintf(L"写入数据成功,写入字节数: %d\n", dwBytesWritten);
}
else
{
    // 处理写入失败的情况
    DWORD dwError = GetLastError();
    wprintf(L"写入文件失败,错误代码: %d\n", dwError);
}
  1. CloseHandle:CloseHandle函数用于关闭文件句柄,释放系统资源,其函数原型如下:
cpp 复制代码
BOOL CloseHandle(
    HANDLE hObject // 文件句柄或其他对象句柄
);

在文件操作完成后,务必调用CloseHandle关闭文件句柄:

cpp 复制代码
CloseHandle(hFile);

如果不关闭文件句柄,可能会导致文件资源泄漏,影响系统性能,甚至可能导致其他程序无法访问该文件。例如,在一个长时间运行的程序中,如果频繁打开文件而不关闭句柄,系统的句柄资源会逐渐耗尽,最终导致程序或系统出现异常。

3.2 注册表操作 API 的应用

注册表是 Windows 操作系统中用于存储系统和应用程序配置信息的数据库,通过注册表操作 API,我们可以对注册表进行读取、写入和修改等操作。

  1. RegOpenKeyEx:RegOpenKeyEx函数用于打开一个已有的注册表项,其函数原型如下:
cpp 复制代码
LONG RegOpenKeyEx(
    HKEY hKey,                // 父键句柄,如HKEY_CURRENT_USER、HKEY_LOCAL_MACHINE等
    LPCTSTR lpSubKey,         // 子键名称
    DWORD ulOptions,          // 保留参数,设为0
    REGSAM samDesired,        // 访问权限,如KEY_READ、KEY_WRITE等
    PHKEY phkResult           // 用于接收打开的子键句柄
);

例如,打开当前用户的软件项:

cpp 复制代码
HKEY hKey;
LONG lResult = RegOpenKeyEx(
    HKEY_CURRENT_USER,
    L"Software",
    0,
    KEY_READ,
    &hKey
);
if (lResult == ERROR_SUCCESS)
{
    // 成功打开注册表项,可以进行后续操作
}
else
{
    // 处理打开失败的情况
    wprintf(L"打开注册表项失败,错误代码: %d\n", lResult);
}
  1. RegQueryValueEx:RegQueryValueEx函数用于查询指定注册表项的值,其函数原型如下:
cpp 复制代码
LONG RegQueryValueEx(
    HKEY hKey,                // 已打开的注册表项句柄
    LPCTSTR lpValueName,      // 值名称,若为NULL则查询默认值
    LPDWORD lpReserved,       // 保留参数,设为NULL
    LPDWORD lpType,           // 用于接收值类型
    LPBYTE lpData,            // 用于存储值数据的缓冲区
    LPDWORD lpcbData          // 缓冲区大小,返回时为实际数据大小
);

例如,查询注册表项的值:

cpp 复制代码
TCHAR valueData[1024];
DWORD dwValueSize = sizeof(valueData);
DWORD dwValueType;
LONG lQueryResult = RegQueryValueEx(
    hKey,
    L"SomeValueName",
    NULL,
    &dwValueType,
    (LPBYTE)valueData,
    &dwValueSize
);
if (lQueryResult == ERROR_SUCCESS)
{
    // 成功查询到值,可以处理valueData中的数据
    valueData[dwValueSize / sizeof(TCHAR)] = '\0'; // 确保字符串以null结尾
    wprintf(L"查询到的值: %s\n", valueData);
}
else
{
    // 处理查询失败的情况
    wprintf(L"查询注册表值失败,错误代码: %d\n", lQueryResult);
}
  1. RegSetValueEx:RegSetValueEx函数用于设置指定注册表项的值,其函数原型如下:
cpp 复制代码
LONG RegSetValueEx(
    HKEY hKey,                // 已打开的注册表项句柄
    LPCTSTR lpValueName,      // 值名称,若为NULL则设置默认值
    DWORD Reserved,           // 保留参数,设为0
    DWORD dwType,             // 值类型,如REG_SZ、REG_DWORD等
    const BYTE *lpData,       // 要设置的值数据
    DWORD cbData              // 值数据的大小
);

例如,设置注册表项的值:

cpp 复制代码
TCHAR newValue[] = L"NewValueData";
LONG lSetResult = RegSetValueEx(
    hKey,
    L"SomeValueName",
    0,
    REG_SZ,
    (const BYTE *)newValue,
    (wcslen(newValue) + 1) * sizeof(TCHAR)
);
if (lSetResult == ERROR_SUCCESS)
{
    // 成功设置值
    wprintf(L"设置注册表值成功\n");
}
else
{
    // 处理设置失败的情况
    wprintf(L"设置注册表值失败,错误代码: %d\n", lSetResult);
}

在完成注册表操作后,应使用RegCloseKey函数关闭打开的注册表项句柄,以释放资源。

cpp 复制代码
RegCloseKey(hKey);

3.3 对话框的创建与模态 / 非模态处理

对话框是 Windows 应用程序中常用的用户界面元素,用于与用户进行交互。在 Windows API 中,对话框分为模态对话框和非模态对话框,它们的创建和使用方式有所不同。

  1. 模态对话框:模态对话框在显示时,会阻止用户与对话框所在应用程序的其他窗口进行交互,直到对话框被关闭。使用DialogBox函数创建模态对话框,其函数原型如下:
cpp 复制代码
INT_PTR DialogBox(
    HINSTANCE hInstance,      // 应用程序实例句柄
    LPCTSTR lpTemplateName,   // 对话框模板资源名或ID
    HWND hWndParent,          // 父窗口句柄,若为NULL则为桌面窗口
    DLGPROC lpDialogFunc      // 对话框过程函数指针
);

例如,创建一个模态对话框:

首先,在资源文件(.rc)中定义对话框模板:

cpp 复制代码
IDD_MYDIALOG DIALOGEX 0, 0, 200, 100
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "我的模态对话框"
FONT 9, "宋体"
BEGIN
    // 添加对话框控件,如按钮、编辑框等
    DEFPUSHBUTTON "确定",IDOK,75,70,50,14
    PUSHBUTTON "取消",IDCANCEL,130,70,50,14
END

然后,在代码中调用DialogBox函数创建并显示对话框:

cpp 复制代码
INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    case WM_INITDIALOG:
        // 对话框初始化代码,如设置控件初始值等
        return TRUE;
    case WM_COMMAND:
        switch (LOWORD(wParam))
        {
        case IDOK:
            // 处理确定按钮点击事件
            EndDialog(hwndDlg, IDOK);
            return TRUE;
        case IDCANCEL:
            // 处理取消按钮点击事件
            EndDialog(hwndDlg, IDCANCEL);
            return TRUE;
        }
        break;
    case WM_CLOSE:
        // 处理关闭对话框事件
        EndDialog(hwndDlg, 0);
        return TRUE;
    }
    return FALSE;
}

// 在主函数或其他地方调用DialogBox创建模态对话框
HINSTANCE hInstance = GetModuleHandle(NULL);
DialogBox(hInstance, MAKEINTRESOURCE(IDD_MYDIALOG), NULL, DialogProc);
  1. 非模态对话框:非模态对话框在显示时,用户可以继续与应用程序的其他窗口进行交互。创建非模态对话框需要使用CreateDialog函数,其函数原型如下:
cpp 复制代码
HWND CreateDialog(
    HINSTANCE hInstance,      // 应用程序实例句柄
    LPCTSTR lpTemplateName,   // 对话框模板资源名或ID
    HWND hWndParent,          // 父窗口句柄
    DLGPROC lpDialogFunc      // 对话框过程函数指针
);

例如,创建一个非模态对话框:

在资源文件中定义对话框模板(与模态对话框类似),然后在代码中创建并显示非模态对话框:

cpp 复制代码
HWND hDlg = CreateDialog(
    hInstance,
    MAKEINTRESOURCE(IDD_MYDIALOG),
    hWnd, // hWnd为父窗口句柄,通常为主窗口句柄
    DialogProc
);
if (hDlg)
{
    ShowWindow(hDlg, SW_SHOW);
}

在使用非模态对话框时,需要注意对话框的生命周期管理。通常,需要在不再使用对话框时调用DestroyWindow函数销毁对话框,并在适当的时候释放相关资源。例如,可以在主窗口的WM_DESTROY消息处理中添加销毁非模态对话框的代码:

cpp 复制代码
case WM_DESTROY:
    if (hDlg)
    {
        DestroyWindow(hDlg);
        hDlg = NULL;
    }
    PostQuitMessage(0);
    break;
  1. 对比
    • 使用场景:模态对话框适用于需要用户专注于当前对话框操作,获取用户关键输入或确认信息的场景,如文件保存时的 "另存为" 对话框、软件的设置对话框等。非模态对话框适用于需要用户在进行其他操作的同时,随时可以访问和操作对话框的场景,如文本编辑器中的查找替换对话框、绘图软件中的颜色选择对话框等。
    • 消息处理:模态对话框在显示期间,应用程序的消息循环会被阻塞,直到对话框关闭。对话框过程函数负责处理对话框自身的消息,消息处理完成后通过EndDialog函数关闭对话框并返回结果。非模态对话框显示后,应用程序的消息循环继续运行,主窗口和非模态对话框可以同时接收和处理消息。非模态对话框的消息处理与普通窗口类似,通过对话框过程函数处理消息,但关闭对话框时需要调用DestroyWindow函数,而不是EndDialog函数。

四、实战项目:简易记事本(Windows API 版)

4.1 项目需求

  • 新建文件:点击 "新建" 菜单项或相应工具栏按钮,能够创建一个空白的文本编辑区域,清空当前已编辑的内容,以便用户输入新的文本信息。这就好比在现实中打开一本崭新的笔记本,准备记录新的内容。
  • 打开文件:支持用户通过 "打开" 菜单项或按钮,弹出文件选择对话框,选择本地磁盘上已存在的文本文件(.txt 等常见文本格式),并将文件内容读取显示在文本编辑区域,方便用户查看和继续编辑文件内容。例如,当用户需要查看之前记录的会议纪要时,就可以通过打开功能找到对应的文件。
  • 保存文件:当用户点击 "保存" 菜单项或按钮时,如果当前编辑的内容是新建文件,弹出 "另存为" 对话框,让用户选择保存路径并输入文件名;如果是已打开的文件,则直接将编辑后的内容覆盖原文件保存。保存功能就像是将笔记本中的内容妥善保存起来,防止丢失。
  • 文本编辑:提供基本的文本编辑功能,如输入文本、删除文本、复制、粘贴、剪切等。用户可以像在普通文本编辑器中一样,自由地对文本进行各种操作,以满足不同的编辑需求。例如,在撰写文章时,可能需要复制一段精彩的语句,或者删除一些错误的内容。
  • 格式设置:支持简单的文本格式设置,如设置字体(字体类型、字号大小、加粗、倾斜、下划线等),改变文本颜色等,使文本内容在显示上更加丰富多样,满足用户对文本样式的个性化需求。比如,为了突出重要内容,可以将其设置为加粗、加大字号或者改变颜色。

4.2 窗口创建与文件 API 实现核心功能

  1. 窗口布局和界面元素创建
    • 首先,按照前文所述的窗口创建方法,注册窗口类并创建主窗口。在主窗口创建后,添加菜单栏、工具栏和文本编辑框等界面元素。
    • 菜单栏:通过资源编辑器创建包含 "文件""编辑""格式" 等菜单项的菜单资源文件。在 "文件" 菜单下添加 "新建""打开""保存""另存为""退出" 等子菜单项;"编辑" 菜单包含 "复制""粘贴""剪切" 等;"格式" 菜单用于设置字体和颜色等格式选项。然后加载菜单资源并将其设置到主窗口上。
    • 工具栏:创建工具栏图像列表,添加对应 "新建""打开""保存" 等功能的图标。使用 CreateToolbarEx 函数创建工具栏,并向工具栏中添加按钮,每个按钮对应相应的功能命令 ID。
    • 文本编辑框:使用 CreateWindowEx 函数创建一个多行编辑框,用于显示和编辑文本内容。设置编辑框的样式为 WS_CHILD | WS_VISIBLE | WS_BORDER | ES_MULTILINE | ES_AUTOVSCROLL,使其具有子窗口、可见、带边框、多行显示和自动垂直滚动的功能。编辑框的位置和大小根据主窗口的布局进行合理设置。
  2. 文件操作 API 实现文件功能的代码逻辑
    • 新建文件:当用户点击 "新建" 菜单项或工具栏按钮时,在窗口过程函数中处理相应的 WM_COMMAND 消息,通过 ID 判断是新建操作。此时,清空文本编辑框的内容,可以使用 SetWindowText 函数将编辑框的文本设置为空字符串。
cpp 复制代码
case WM_COMMAND:
    switch (LOWORD(wParam))
    {
    case ID_FILE_NEW:
        SetWindowText(hEdit, L""); // hEdit为文本编辑框句柄
        break;
    // 其他菜单项处理代码...
    }
    break;
  • 打开文件:在处理 "打开" 菜单项的 WM_COMMAND 消息时,使用 OpenFileDialog 函数弹出文件选择对话框,让用户选择要打开的文件。获取用户选择的文件名后,使用 CreateFile 函数以 GENERIC_READ 模式打开文件,再使用 ReadFile 函数读取文件内容到缓冲区,最后将缓冲区中的内容设置到文本编辑框中显示。
cpp 复制代码
case WM_COMMAND:
    switch (LOWORD(wParam))
    {
    case ID_FILE_OPEN:
    {
        OPENFILENAME ofn;
        TCHAR szFileName[MAX_PATH] = { 0 };
        ZeroMemory(&ofn, sizeof(ofn));
        ofn.lStructSize = sizeof(ofn);
        ofn.hwndOwner = hWnd;
        ofn.lpstrFile = szFileName;
        ofn.nMaxFile = MAX_PATH;
        ofn.lpstrFilter = L"文本文件(*.txt)\0*.txt\0所有文件(*.*)\0*.*\0";
        ofn.nFilterIndex = 1;
        ofn.lpstrFileTitle = NULL;
        ofn.nMaxFileTitle = 0;
        ofn.lpstrInitialDir = NULL;
        ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST;

        if (GetOpenFileName(&ofn))
        {
            HANDLE hFile = CreateFile(
                ofn.lpstrFile,
                GENERIC_READ,
                FILE_SHARE_READ,
                NULL,
                OPEN_EXISTING,
                FILE_ATTRIBUTE_NORMAL,
                NULL
            );
            if (hFile != INVALID_HANDLE_VALUE)
            {
                TCHAR buffer[1024];
                DWORD dwBytesRead;
                SetWindowText(hEdit, L"");
                while (ReadFile(hFile, buffer, sizeof(buffer) - 1, &dwBytesRead, NULL) && dwBytesRead > 0)
                {
                    buffer[dwBytesRead] = '\0';
                    SendMessage(hEdit, EM_REPLACESEL, FALSE, (LPARAM)buffer);
                }
                CloseHandle(hFile);
            }
        }
        break;
    }
    // 其他菜单项处理代码...
    }
    break;
  • 保存文件:处理 "保存" 菜单项时,如果当前编辑的是新建文件(可以通过判断文件名是否为空来确定),调用 SaveFileDialog 函数弹出 "另存为" 对话框获取文件名和路径,然后使用 CreateFile 函数以 GENERIC_WRITE 模式创建文件(如果文件已存在则覆盖),再通过 GetWindowText 函数获取文本编辑框中的内容,使用 WriteFile 函数将内容写入文件;如果是已打开的文件,直接获取编辑框内容并覆盖写入原文件。
cpp 复制代码
case WM_COMMAND:
    switch (LOWORD(wParam))
    {
    case ID_FILE_SAVE:
    {
        if (wcslen(szFileName) == 0) // szFileName为存储文件名的变量,初始为空
        {
            OPENFILENAME ofn;
            ZeroMemory(&ofn, sizeof(ofn));
            ofn.lStructSize = sizeof(ofn);
            ofn.hwndOwner = hWnd;
            ofn.lpstrFile = szFileName;
            ofn.nMaxFile = MAX_PATH;
            ofn.lpstrFilter = L"文本文件(*.txt)\0*.txt\0所有文件(*.*)\0*.*\0";
            ofn.nFilterIndex = 1;
            ofn.lpstrFileTitle = NULL;
            ofn.nMaxFileTitle = 0;
            ofn.lpstrInitialDir = NULL;
            ofn.Flags = OFN_PATHMUSTEXIST | OFN_OVERWRITEPROMPT;

            if (GetSaveFileName(&ofn))
            {
                HANDLE hFile = CreateFile(
                    ofn.lpstrFile,
                    GENERIC_WRITE,
                    0,
                    NULL,
                    CREATE_ALWAYS,
                    FILE_ATTRIBUTE_NORMAL,
                    NULL
                );
                if (hFile != INVALID_HANDLE_VALUE)
                {
                    TCHAR buffer[1024];
                    DWORD dwBytesWritten;
                    int length = GetWindowTextLength(hEdit);
                    for (int i = 0; i < length; i += sizeof(buffer) - 1)
                    {
                        GetWindowText(hEdit, buffer, sizeof(buffer));
                        WriteFile(hFile, buffer, wcslen(buffer) * sizeof(TCHAR), &dwBytesWritten, NULL);
                    }
                    CloseHandle(hFile);
                }
            }
        }
        else
        {
            HANDLE hFile = CreateFile(
                szFileName,
                GENERIC_WRITE,
                0,
                NULL,
                CREATE_ALWAYS,
                FILE_ATTRIBUTE_NORMAL,
                NULL
            );
            if (hFile != INVALID_HANDLE_VALUE)
            {
                TCHAR buffer[1024];
                DWORD dwBytesWritten;
                int length = GetWindowTextLength(hEdit);
                for (int i = 0; i < length; i += sizeof(buffer) - 1)
                {
                    GetWindowText(hEdit, buffer, sizeof(buffer));
                    WriteFile(hFile, buffer, wcslen(buffer) * sizeof(TCHAR), &dwBytesWritten, NULL);
                }
                CloseHandle(hFile);
            }
        }
        break;
    }
    // 其他菜单项处理代码...
    }
    break;

4.3 消息处理与用户交互优化

  1. 需要处理的消息类型及处理方式
    • WM_COMMAND 消息:处理菜单和工具栏按钮的点击事件。通过 LOWORD (wParam) 获取菜单项或按钮的 ID,根据不同的 ID 执行相应的功能,如新建文件、打开文件、保存文件、复制、粘贴等操作。例如,当点击 "复制" 菜单项时,在 WM_COMMAND 消息处理中判断 ID 为 ID_EDIT_COPY,然后获取文本编辑框中选中的内容,将其复制到剪贴板。
cpp 复制代码
case WM_COMMAND:
    switch (LOWORD(wParam))
    {
    case ID_EDIT_COPY:
    {
        SendMessage(hEdit, WM_COPY, 0, 0);
        break;
    }
    // 其他菜单项和按钮ID处理代码...
    }
    break;
  • WM_KEYDOWN 消息:处理键盘按键事件,实现快捷键功能。例如,当按下 Ctrl + N 组合键时,触发新建文件操作;按下 Ctrl + S 组合键时,触发保存文件操作。可以通过判断 wParam 的值和 GetAsyncKeyState 函数来检测组合键的按下情况。
cpp 复制代码
case WM_KEYDOWN:
    if (wParam == 'N' && (GetAsyncKeyState(VK_CONTROL) & 0x8000))
    {
        // 执行新建文件操作,调用处理新建文件的函数或代码
        SendMessage(hWnd, WM_COMMAND, ID_FILE_NEW, 0);
    }
    else if (wParam == 'S' && (GetAsyncKeyState(VK_CONTROL) & 0x8000))
    {
        // 执行保存文件操作,调用处理保存文件的函数或代码
        SendMessage(hWnd, WM_COMMAND, ID_FILE_SAVE, 0);
    }
    break;
  • WM_SIZE 消息:当窗口大小改变时,调整文本编辑框、菜单栏、工具栏等界面元素的大小和位置,使其适应窗口的变化,保持良好的用户界面布局。例如,在 WM_SIZE 消息处理中,根据窗口的新大小重新计算编辑框的位置和大小,并使用 MoveWindow 函数调整编辑框的位置和大小。
cpp 复制代码
case WM_SIZE:
    int width = LOWORD(lParam);
    int height = HIWORD(lParam);
    MoveWindow(hEdit, 0, 0, width, height, TRUE);
    break;
  1. 优化用户交互体验的措施
    • 实时保存提示:在每次成功保存文件后,使用 MessageBox 函数弹出一个提示框,告知用户文件已成功保存,让用户清楚知道保存操作的结果,增强用户对操作的感知和信心。
cpp 复制代码
case WM_COMMAND:
    switch (LOWORD(wParam))
    {
    case ID_FILE_SAVE:
    // 保存文件代码...
    MessageBox(hWnd, L"文件已成功保存", L"提示", MB_OK);
    break;
    // 其他菜单项处理代码...
    }
    break;
  • 快捷键设置:除了在 WM_KEYDOWN 消息中实现常见的快捷键功能外,还可以在菜单栏的菜单项中添加快捷键提示,如 "新建 (&N)",括号内的字母表示快捷键,用户可以通过 Alt + N 快速打开新建文件功能,提高操作效率,使用户能够更便捷地执行常用操作。
  • 自动换行:在创建文本编辑框时,设置 ES_AUTOHSCROLL 和 ES_AUTOVSCROLL 样式,使编辑框在输入文本时能够自动换行,避免水平滚动条的频繁出现,提升用户编辑文本的体验,让用户在输入长文本时更加方便和舒适。
相关推荐
nnerddboy3 小时前
QT(c++)开发自学笔记:1.串口
c++·笔记·qt
范特西_3 小时前
两个无重叠子数组的最大和
c++·算法
dot to one4 小时前
应用层:Http、Https
linux·c++·网络协议
名誉寒冰5 小时前
# 深入理解Linux内核与用户态通信:Netlink机制实战
linux·服务器·windows
2401_841495645 小时前
【数据结构】链栈的基本操作
java·数据结构·c++·python·算法·链表·链栈
huangyuchi.5 小时前
【Linux实战 】Linux 线程池的设计、实现与单例模式应用
linux·c++·单例模式·线程池·懒汉模式·项目·linux系统
Archie_IT6 小时前
「深入浅出」嵌入式八股文—P2 内存篇
c语言·开发语言·数据结构·数据库·c++·算法
是那盏灯塔6 小时前
【算法】——动态规划算法及实践应用
数据结构·c++·算法·动态规划
Wadli6 小时前
C++面经|小林coding|(1)
开发语言·c++