问题

如何用 C++ 从零编写 GUI?

回答
从零开始,用 C++ 打造属于你的图形用户界面

很多时候,我们希望程序能够以更加直观、易用的方式与用户交互,而不是仅仅停留在命令行界面。这时候,图形用户界面(GUI)就显得尤为重要了。很多人可能觉得 C++ 编写 GUI 是一件非常复杂的事情,需要依赖各种庞大的框架。但事实上,我们可以从最基础的概念入手,一步一步地构建一个属于自己的 GUI。这其中需要理解一些核心的原理,并且动手去实现它们。

这篇文章,咱们就抛开那些现成的 GUI 库,尝试着从最底层的 Windows API 或 X Window System(Linux/macOS 等平台)的角度出发,去“从零开始”理解 GUI 是如何诞生的。我不会深入到操作系统内核的细节,但会尽可能详细地讲解每个步骤,让你对 GUI 的运作有一个清晰的认识。

首先,我们需要明确一个目标: 我们要构建的是一个能够显示一个窗口,并且在这个窗口中能够绘制一些基本图形(比如文本和线条)的程序。这虽然简单,但包含了 GUI 的核心要素。

核心概念:窗口、消息和事件

理解 GUI,首先要理解三个核心概念:

1. 窗口 (Window): 在操作系统层面,一个 GUI 应用程序最基本的就是一个或多个窗口。窗口是用户与程序交互的基本载体,它拥有一个独立的显示区域,可以接收用户的输入,并且可以显示程序的内容。每个窗口都有一个独特的标识符,称为窗口句柄 (Window Handle)。

2. 消息 (Message): 操作系统负责管理所有的窗口和用户的输入。当用户进行操作时(比如点击鼠标、按下键盘),操作系统会将这些操作转换成“消息”,并发送给相应的窗口。消息包含了很多信息,比如事件的类型、发生的坐标、按下的键等。

3. 事件循环 (Event Loop): 应用程序需要一个机制来不断地接收和处理来自操作系统发送的消息。这个机制就叫做“事件循环”或“消息循环”。应用程序会持续不断地从消息队列中取出消息,然后根据消息的类型,执行相应的处理代码。这就是我们常说的“事件驱动编程”。

搭建基础框架 (以 Windows 为例)

由于不同的操作系统有不同的 GUI 编程接口,这里我们以 Windows 平台为例进行讲解,因为它的 API 相对来说更直接一些,也更容易理解底层的运作。Linux/macOS 上的 X Window System 也有类似的原理,只是具体的 API 函数名称和结构不同。

我们需要一个入口函数,就像 `main()` 函数一样,但对于 GUI 应用程序,Windows 需要一个特定的入口函数:`WinMain()`。

```cpp
include // 引入 Windows API 头文件

// 窗口过程函数 (Window Procedure)
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
// 这是处理窗口消息的核心函数
// hWnd: 接收消息的窗口句柄
// message: 消息的类型 (例如 WM_PAINT, WM_KEYDOWN)
// wParam, lParam: 消息的附加参数

switch (message) {
case WM_PAINT: { // 当窗口需要重绘时发送的消息
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps); // 开始绘制,获取设备上下文句柄 (HDC)

// 在这里编写绘制代码
// 例如:在窗口中央绘制一行文字
RECT rect;
GetClientRect(hWnd, ▭); // 获取窗口的客户区矩形
DrawText(hdc, TEXT("Hello, ZeroBase GUI!"), 1, ▭, DT_CENTER | DT_VCENTER | DT_SINGLELINE);

EndPaint(hWnd, &ps); // 结束绘制
break;
}
case WM_DESTROY: { // 当窗口被销毁时发送的消息
PostQuitMessage(0); // 向应用程序发送退出消息
break;
}
default:
// 对于其他未处理的消息,交给系统默认处理
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0; // 表示消息已被处理
}

// 程序入口函数
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
// 1. 定义窗口类 (Window Class)
WNDCLASSEX wc = {0}; // 初始化结构体为零
wc.cbSize = sizeof(WNDCLASSEX); // 结构体大小
wc.style = CS_HREDRAW | CS_VREDRAW; // 窗口风格:重绘时水平或垂直刷新
wc.lpfnWndProc = WndProc; // 指定窗口过程函数
wc.hInstance = hInstance; // 程序实例句柄
wc.hCursor = LoadCursor(NULL, IDC_ARROW); // 加载默认箭头光标
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); // 设置窗口背景颜色
wc.lpszClassName = TEXT("MyZeroBaseGUI"); // 窗口类的名字

// 2. 注册窗口类
if (!RegisterClassEx(&wc)) {
MessageBox(NULL, TEXT("窗口类注册失败!"), TEXT("错误"), MB_ICONERROR);
return 1; // 注册失败,退出程序
}

// 3. 创建窗口
HWND hWnd = CreateWindowEx(
0, // 扩展风格
TEXT("MyZeroBaseGUI"), // 窗口类的名字
TEXT("我的零基础 GUI"), // 窗口标题
WS_OVERLAPPEDWINDOW, // 窗口风格 (标准窗口)
CW_USEDEFAULT, CW_USEDEFAULT, // 初始位置 (由系统决定)
CW_USEDEFAULT, CW_USEDEFAULT, // 初始大小 (由系统决定)
NULL, // 父窗口句柄 (NULL 表示没有父窗口)
NULL, // 菜单句柄
hInstance, // 程序实例句柄
NULL // 创建窗口时的附加参数
);

if (!hWnd) {
MessageBox(NULL, TEXT("窗口创建失败!"), TEXT("错误"), MB_ICONERROR);
return 1; // 创建失败,退出程序
}

// 4. 显示窗口
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd); // 强制重绘窗口,触发 WM_PAINT 消息

// 5. 消息循环 (Event Loop)
MSG msg;
while (GetMessage(&msg, NULL, 0, 0)) { // 从消息队列中获取消息
TranslateMessage(&msg); // 翻译虚拟键消息为字符消息
DispatchMessage(&msg); // 将消息分派给相应的窗口过程函数
}

return (int)msg.wParam; // 返回退出码
}
```

逐行解析:理解背后的逻辑

让我们仔细看看这段代码,理解每个部分的作用:

1. `include `

这是最基础的一步,包含了所有 Windows API 的函数和类型定义。没有它,我们就无法调用任何 Windows 的功能。

2. `LRESULT CALLBACK WndProc(...)` 窗口过程函数

这是整个 GUI 系统的“大脑”。每当操作系统检测到与窗口相关的事件时,都会调用这个函数来处理。

`LRESULT`: 函数的返回值类型,通常用于表示消息处理的结果。
`CALLBACK`: 这是一个调用约定,告诉编译器这个函数是由操作系统来调用的,而不是我们自己主动调用。
`HWND hWnd`: 窗口句柄,可以理解为窗口的“身份证号码”,用来唯一标识一个窗口。
`UINT message`: 消息的类型,这是一个整数常量,比如 `WM_PAINT` 表示需要重绘窗口,`WM_KEYDOWN` 表示有键被按下等等。
`WPARAM wParam`, `LPARAM lParam`: 这两个参数是消息的附加信息,具体内容取决于 `message` 的类型。
`switch (message)`: 这是消息处理的核心逻辑。我们根据接收到的消息类型,执行不同的操作。
`WM_PAINT`: 这是非常重要的一个消息。当窗口需要更新显示内容时(比如刚创建、被遮挡后又显现出来),操作系统就会发送 `WM_PAINT` 消息。
`BeginPaint()`: 开始绘制前需要调用的函数,它会返回一个 `HDC`(Handle to Device Context,设备上下文句柄)。你可以把 `HDC` 理解为一个“画板”或者“画笔”,通过它才能在窗口上进行绘制。
`GetClientRect()`: 获取窗口的“客户区”矩形区域,也就是窗口中真正可以用来绘制内容的部分(不包括标题栏、边框等)。
`DrawText()`: 一个非常常用的绘制文本的函数。它接受 `HDC`、要绘制的文本、文本长度、要绘制到的矩形以及一些绘制标志。
`EndPaint()`: 结束绘制后必须调用的函数,它会告知系统绘制工作已经完成。
`WM_DESTROY`: 当用户关闭窗口时,会发送这个消息。
`PostQuitMessage(0)`: 这个函数很重要。它会在消息队列中添加一个 `WM_QUIT` 消息,从而导致消息循环终止,应用程序退出。
`default: return DefWindowProc(...)`: 如果当前消息没有被我们处理,就交给系统默认的窗口过程函数去处理,这样可以确保窗口的基本功能(比如拖动、最小化、最大化)正常工作。

3. `int WINAPI WinMain(...)` 程序入口

这是 Windows GUI 应用程序的标准入口点。

`HINSTANCE hInstance`: 当前应用程序实例的句柄。
`HINSTANCE hPrevInstance`: 上一个应用程序实例的句柄,在 32 位和 64 位 Windows 中,这个参数总是 NULL。
`LPSTR lpCmdLine`: 命令行参数字符串。
`int nCmdShow`: 指定窗口的显示状态(例如,最大化、最小化或正常显示)。

在 `WinMain` 函数中,我们主要做了以下几件事:

定义窗口类 (`WNDCLASSEX wc`): 在创建窗口之前,我们必须先定义一个“窗口类”。你可以把它理解为模板,它描述了这个窗口的许多属性,比如它的外观(图标、光标、背景颜色)、行为(由哪个函数处理消息)以及它的名字。
`wc.cbSize = sizeof(WNDCLASSEX)`: 指定结构体的大小,这是必须的。
`wc.style = CS_HREDRAW | CS_VREDRAW`: 这些是窗口的风格。`CS_HREDRAW` 意味着当窗口的水平尺寸改变时,窗口内容会被重绘;`CS_VREDRAW` 意味着当窗口的垂直尺寸改变时,窗口内容也会被重绘。
`wc.lpfnWndProc = WndProc`: 将我们之前定义的 `WndProc` 函数与这个窗口类关联起来。
`wc.hInstance = hInstance`: 告诉系统这个窗口类属于哪个应用程序实例。
`wc.hCursor = LoadCursor(NULL, IDC_ARROW)`: 加载一个标准的箭头光标,当鼠标悬停在窗口上时会显示。
`wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1)`: 设置窗口的背景颜色。这里的 `COLOR_WINDOW + 1` 是一个预定义的系统颜色常量。
`wc.lpszClassName = TEXT("MyZeroBaseGUI")`: 给这个窗口类起一个名字,这个名字会在创建窗口时用到。
注册窗口类 (`RegisterClassEx(&wc)`): 调用 `RegisterClassEx` 函数将我们定义的窗口类注册到操作系统中。只有注册过的窗口类才能用来创建窗口。
创建窗口 (`CreateWindowEx(...)`): 这是实际创建窗口的函数。
`TEXT("MyZeroBaseGUI")`: 这里我们使用了之前注册的窗口类的名字。
`TEXT("我的零基础 GUI")`: 这是窗口的标题栏文本。
`WS_OVERLAPPEDWINDOW`: 这是一个组合风格,表示一个具有标题栏、边框、最小化/最大化/关闭按钮的标准窗口。
`CW_USEDEFAULT, CW_USEDEFAULT`: 这些参数告诉操作系统使用默认的窗口位置和大小。
`hInstance`: 应用程序实例句柄。
显示窗口 (`ShowWindow(hWnd, nCmdShow)`): 将创建的窗口显示在屏幕上。`nCmdShow` 参数决定了窗口最初的显示方式。
强制重绘 (`UpdateWindow(hWnd)`): 这一步很重要,它会发送一个 `WM_PAINT` 消息给窗口,从而触发 `WndProc` 函数中的绘制代码,将窗口内容显示出来。如果没有这一步,窗口可能只会显示一个空白的背景。
消息循环 (`while (GetMessage(&msg, NULL, 0, 0))`):
`MSG msg`: 一个结构体,用来存储从消息队列中取出的消息。
`GetMessage()`: 这个函数会阻塞(等待),直到有消息发送给应用程序。它会从当前线程的消息队列中取出消息,并将其放入 `msg` 结构体中。如果取出的消息是 `WM_QUIT`,则返回 0,循环终止。
`TranslateMessage()`: 负责将一些低级的键盘消息(如 `WM_KEYDOWN`, `WM_KEYUP`)翻译成更高级的字符消息(如 `WM_CHAR`)。
`DispatchMessage()`: 将消息分派给拥有该消息的窗口的窗口过程函数。

编译与运行

要编译这段代码,你需要一个 C++ 编译器(比如 MinGW 或 Visual Studio),并且需要链接 Windows 系统库。

使用 MinGW (命令行):

```bash
g++ o ZeroBaseGUI.exe your_code_file.cpp mwindows lgdi32
```

`o ZeroBaseGUI.exe`: 指定输出的可执行文件名。
`your_code_file.cpp`: 你的 C++ 源文件名称。
`mwindows`: 告诉编译器这是一个 Windows GUI 应用程序,而不是控制台应用程序。
`lgdi32`: 链接 GDI (Graphics Device Interface) 库,这是进行图形绘制所必需的。

然后,在命令行中运行 `ZeroBaseGUI.exe` 即可看到你的第一个“零基础” GUI 窗口。

使用 Visual Studio:

创建一个新的“Windows 桌面应用程序”项目,然后将上面的代码粘贴到主源文件中,并确保项目类型是 GUI 应用程序(通常在项目属性 > 链接器 > 系统 > 子系统 中设置为 Windows (/SUBSYSTEM:WINDOWS))。然后直接编译运行即可。

扩展你的 GUI

我们已经构建了一个基础的窗口和绘制能力。接下来,你可以尝试扩展它:

绘制更多图形: 在 `WM_PAINT` 的 `BeginPaint`/`EndPaint` 之间,你可以使用 `HDC` 调用更多的 GDI 函数,例如:
`MoveToEx()`, `LineTo()`: 绘制直线。
`Rectangle()`, `Ellipse()`: 绘制矩形和椭圆。
`SelectObject()`: 选择画刷、画笔、字体等来改变绘制的样式。
响应更多消息: 在 `WndProc` 的 `switch` 语句中添加对其他消息的处理,例如:
`WM_KEYDOWN`, `WM_KEYUP`, `WM_CHAR`: 处理键盘输入。
`WM_LBUTTONDOWN`, `WM_MOUSEMOVE`, `WM_LBUTTONUP`: 处理鼠标输入。
`WM_SIZE`: 当窗口大小改变时会发送。
创建子窗口 (控件): 你可以使用 `CreateWindowEx()` 函数创建其他更小的窗口,并将它们放置在主窗口内部,这些就是我们常说的控件(按钮、文本框、复选框等)。你需要为这些子窗口指定一个父窗口句柄。
事件处理机制: 为了更清晰地组织代码,你可以为不同的事件(如按钮点击)编写专门的函数,并在 `WndProc` 中根据消息类型调用这些函数。

总结

通过从零开始编写这样一个简单的 GUI 程序,我们得以窥探 GUI 编程的底层逻辑:窗口的创建、消息的传递、事件的响应以及 GDI 的绘图机制。这远比直接使用高级 GUI 库要复杂,但也让我们更深刻地理解了 GUI 的运作原理。

尽管现代 GUI 开发往往会依赖于 Qt, wxWidgets, MFC 等成熟的框架,它们提供了丰富的控件和更高级的抽象,大大简化了开发过程。但理解这些底层概念,对于深入学习 GUI 开发,甚至在某些特殊场景下进行低级别控制,都非常有益。

希望这篇文章能够帮助你打开 C++ GUI 编程的“零基础”大门,让你对如何用代码构建出图形化的交互界面有一个初步但清晰的认识。这只是一个开始,GUI 的世界广阔而精彩,等待你去探索和创造!

网友意见

user avatar

楼主你其实是在找这本书:《精通嵌入式Linux编程:构建自己的GUI环境

user avatar

谢邀。看了很多答案,都是教新手如何利用各种图形库,我个人觉得这是一种误导。所有不推荐新手学习原理,而上来就是各种库的,是走不远的。目前大多数使用的有界面的程序还是Windows系统上的各种软件,所以我们就说说Windows上如何利用C/C++写出有界面的程序。

下面是我的答案,个人成长经历,不喜勿喷。

写在前面的话

很多年以前,我正如现在那些初学C或者C++的同学,一直心存这样的迷惑:人们都说现在我在电脑上看到了的大多数软件,比如QQ、迅雷、千千静听等等,都是用C/C++编写出来的,可是我现在都把C或者C++整本教材都学完了,我还是整天只能面对那个黑洞洞的命令行窗口,就算把书翻到最后一页也找不到如何用C/C++像QQ这样有界面的程序,这是怎么回事?
的确,这样的迷惑,困扰大多数的开始学C和C++的学生,他们这样的迷惑也很难有人给予解答。因为,大学里面教C/C++的老师们往往也是理论脱离实际,没有多少实际经验,只能照本宣科了。
于是很多同学因为学习的苦闷和难以获得喜悦或者成就感开始放弃C/C++的学习,开始转向其他的方面,比如Web设计,通俗地说也就是网页制作,他们惊喜地发现用一个个简单的<div>或者alert()函数就可以作出一个有界面的框框或者显示一个对话框,这比C/C++要容易的多,而且成果也明显;又有一些同学发现了C#或者Java这样的编程语言,他们只要在编程工具中将一个个各式各样的按钮拖进一个被称为“窗体”的对象中,然后运行程序就能生成一个个有界面的程序,而且这些编程语言学起来容易。
这个时候C/C++赫然已经被同学们抛弃了。


根据我个人经历和经验,个人始终认为C/C++是世界上最实用的编程语言,因为我是一个由原先的Web设计者兼Flash程序开发者转变成今天的一个C/C++程序员。我之所以放弃Web设计和Flash编程,是在我遇到了一个叫Charles Petzold写的一本叫《windows程序设计》书以后,以前写的那些东西,虽然好玩,但是我始终都耿耿于怀,因为,那些你所能制作出来的东西,只是别人不感兴趣罢了,只要别人有天对它们有了兴趣,同样能学会,而且这些东西必须借助一定的运行环境,而不是像QQ这样的软件直接运行于操作系统之上。我们来看几个我大学时代写的几个“软件”,注意里的“软件”一词我加了引号:






上面的图片是我为我大学毕业时花了四个月时间为我们班做的毕业纪念册,它是一个可直接运行于桌面上的程序,但是除了制作比较精美一点,实际并没有多少技术含量,Flash编程是我的强项,我先用flash做成动画形式,也就是一个个swf文件,然后用flash打包工具生成exe程序,这样它就能运行于桌面了。我虽然实现了这个程序的核心部分,但是用的那个打包工具程序如何编写的,工作原理是什么,我那个时候一无所知。这个程序的网络版,可以访问如下网址:hootina.org/jnc/byjnc.h


我们再来看看我曾经写过的一个三维立体的音乐播放器,如下图:



这个播放器软件是一个能在桌面上根据鼠标位置自动旋转的软件,并且可以拖动位置,也能通过右键菜单控制歌曲。但,现在看来同样没有什么技术含量。这是2011年五月在上海九维上班有天晚上在公司花了一宿时间做的,核心程序是一个flash,flash可以在flash播放器中转动,而且可以从网络加载歌曲,做好后通过一个flash打包工具让它能直接运行在桌面上。当时写这个程序是为了练习自己的面向对象编程技能。它通过一个配置文件从网络或者本地加载歌曲,配置文件是个xml格式的文本文件:


如果你需要这个软件,可从这个网址下载:hootina.org/res-for-dow


再来看一个C#版的程序:



我来介绍下这个程序吧,这个程序是许多年以前武汉大学一个同学发给我帮着修改的,程序的目的是为了演示一个森林火灾预报。启动程序以后,先点击Random按钮随机生成一片绿地,然后鼠标在绿地某个地方单击,表示从这里产生火源,然后点击start按钮火势开始向四周蔓延。
这个同学的程序存在一个问题,大家看第二个图,当点击start让火势蔓延,结果整个原来随机覆盖的绿地变成百分百覆盖了。我当时花了一下午时间帮她改好。由于隐私问题,改过后的程序我就不截图了。
这个程序是用C#写的,想到当时何军军同学因为不知道需要一个.net框架环境才能运行而抓耳挠腮的样子,我就觉得好笑。你看用C#写出来的程序还是不能直接运行在系统上。哈哈~
正是这些借助他物的原因,我一直对这些徒有其表的花哨技术心存芥蒂。因为我想去了解下计算机底层,去夯实一些基础知识,以期将来有番作为。好在,我在中国地大读研的这两个年头里,我的老师没有给我安排过多的任务,加上史超同学的照顾,让我有时间和经历去学习去研究这些底层的东西。而Windows下的C/C++编程就是我的那把合适的钥匙。

现在我来回答文章开头提到的同学的困惑,为什么用学习了C/C++那么久还是无法写出有界面的软件。讲授C/C++的书,一般只会去讲解C/C++语法层次上的东西,而不会去介绍相关的平台的API函数。如果我们要用C/C++去写出有界面的程序,还得调用所在平台提供的接口函数,而这个平台我们现在大多数人见到的正是Windows操作系统,所以这些接口函数也就是Windows API了。说的更通用点,个人认为,编程语言和语言之间的差别多数是语法层次上的差别,一种编程语言本身不能做任何软件,你必须借助或者调用所在平台或者运行环境的API函数。拿C#来说,你之所以能作出一些界面或者窗口出来,不是C#本身就有这个功能,而是.net环境提供了这些东西,具体点就是CLR(C#运行时,或C#运行环境)。
我们再深入一点,你调用CLR提供的API接口,最后在底层CLR还是去调用操作系统的API函数,既然如此,我为什么隔一层CLR,不直接去调用系统API了,就这一点来说,这就是在操作系统之上用C/C++编程最大的优势,正如Charles Petzold所说的,我这里把书中原话摘录如下:
“显而易见,究竟用哪种方式编写Windows应用程序最好,其实并无一定之规。应用程序本身的特性应该是决定采用何种编程工具的最主要因素,但是无论将来你采用什么样的编程工具,通过了解Windows API从而深入理解Windows的工作原理,这本身就有很重要的意义。Windows是一个非常复杂的系统,在API之上加一层编程语言并不能消除其复杂性,最多不过是把复杂性隐藏起来而已。说不定什么时候,Windows复杂的那一面迟早会蹦出来拖你的后腿,懂得API能让你到时候更快地挣脱困境。

在基本Windows API之上的任何软件层或多或少都会限制你使用Windows的全部功能。比如,你或许发现采用Visual Basic来编写你的应用程序非常理想,但是就有那么一两项非常基本的功能Visual Basic无法支持。往往这个时候你得非要调用基本API。作为Windows程序员,我们的活动空间完全由API来规范,再没有什么其他方式比直接调用API更有效、更灵活多样了。
这段文字选自Charles Petzold的《Windows程序设计》第一章第十页。
我上面展示了很多图片,也是为了印证这个道理。C# 高级用户肯定写过这样的代码:



有时候Windows提供的一些功能C#没有,这个时候我们只能通过这种用中括号括起来的元标签的方法调用系统dll中的一些函数来为自己服务。这也用侧面说明了,操作系统API上层的任何软件层都没有系统原生API来的高效与直接。

但是如今的世界,懂这个道理的人甚多,最后他们还是离C/C++远去了。为什么呢?
就其主要原因,还是从学生时代来说吧,Windows编程实在太难了,不仅难而且成效不明显,你写了一堆代码恐怕都很难显示出一个按钮,相比较其他语言所写即所得,能坚持下来而不跟风的同学实在太少了。等到工作的时候,若不是从事这个,也很难有心境和时间去学了。
我们还是来举个例子,我现在分别用ActionScript和C语言为一个程序制作一个右键菜单(老外叫上下文菜单,Context Menu) 。
下面是ActionScript代码:


仅仅 就两行就自定义了一个右键菜单项,而且代码也很容易看懂。

现在,我们再来看看,如何用C语言实现同样的功能:


这段代码来自我三月份帮所里一同学写的一个批量生成Excel表格的项目。效果如下:


当我告诉你上面那段程序确实是C程序你可能不信,怎么会是C程序呢,上面出现的那些诸如HMENU这样的数据类型,C语言中可没有啊。Windows系统最初诞生的时候,C++语言还不存在,所以整个Windows系统都是用C加上少许的汇编写出来了。现在,就请跟随我,我将用C语言来一行行教你如何写出一个Windows程序出来,注意这里说的Windows程序是那种有窗口有按钮有菜单的界面程序,而不是你之前写的那些黑洞洞的控制台程序。当然,这两种程序本质上是一样的,请你记住这一点。同时,我们从教科书或者老师那里学到的C/C++知识并不完整,工作以后接触Windows编程,我也发现了这一点,很多很常用的C/C++语法,我们在教科书上从来就没学到,所以,我也将根据经验教你一些你所不知道的C/C++小技巧。



如何用C/C+编写窗口程序(非命令行程序)

这是我大学时代的一个困惑,今天我将原原本本详详细细地给予解答。我们在Windows平台下,要想写出有界面的程序,必须调用Windows提供的API函数


你所不知道的C语言细节1  
C语言中有一个关键字叫typedef,这个关键字是用来利用已有类型来定义新类型用的。这点多数C教材中都没有讲到过它,很多同学很容易把它与#define关键字搞混淆,下面我们来做个对比:


程序含义很简单,我们分别用define和typedef这两个关键字定义了两个新类型I1和I2,而测试结果发现,用typedef定义的新类型的确生效了,当然这种新类型本质还是int*(int型指针),而define关键却没有工作的那么好。
我们来分析下原因,define一般用来定义宏的,而这个宏在预编译(编译的前一个步骤)时被完全替换成其定义的东西,也就是纯粹的文本替换,而typedef却不是这样。也就是上述代码相当于:


总结起来,typedef关键字的确是新类型的定义方法,一旦用它定义了一个类型,到处可用,而不是什么简单的替换。为什么我要说到typedef这个关键字呢?第一,我当年在教材中也没学到这个关键字,所以开始接触Windows编程时甚是迷糊,总以为它和define替换类似,第二,整个windows系统中的数据类型就是通过typedef和define这两个关键字一步步地设计出来的。


你所不知道的C语言细节2
关于C语言的宏与宏定义大家一般对define关键字很熟悉,但是所谓的熟悉也只是知道些简单的应用,其实define定义宏可以写的很复杂。下面列举几个很常用但是却在教科书中基本不提的用法:


函数宏


看到了没,这种宏调用起来类似函数调用,所以叫做“函数宏”。这个知识点,我相信很多同学也知道这个。好下面,我们继续深入一步,看下面代码:


我的问题是表达式t(1)是什么?答案是L1。表达式t("Hello")的结果为L"Hello"。"##"在C语言中被称为合并操作符或者叫“令牌粘贴”,言下之意就是将##两边的东西连接成在一起,L和1连接在一起便成了L1,L与字符串"Hello"在一起便成了L"Hello"(注意:包含引号,原封不动地连接!)。 这是一个有用的C语言知识,请大家记住。


宏其实可以写的很复杂,甚至一行写不下可以写成多行,mfc中的消息宏就是一个例子:


注意由于这三行代码并没有以分号结尾,所以它们是三个宏,我们现在来看看这个三个宏如何定义的,试着看看哦,看看你能否看懂:




这三个宏就比较复杂了,我相信你在C或者C++教材中从来没见过这么复杂的宏定义吧,甚至这些宏定义一行写不下写成了多行,那些反斜杠就是续行符号。对于这么复杂的宏定义也请不要害怕,因为宏的本质还是原封不动地替换。我们一步步地替换下就看出来了,替换之后是这样的:



其中PTM_WARNING_DISABLE和PTM_WARNING_RESTORE又是定义的两个宏,__pragma是C/C++中用来设置编译器参数的指令,如果我不说,你在教科书上恐怕也学不到这个指令吧,这个指令很有用,你可以去搜索它,好好学习下。这两个宏展开之后,是用来不让编译器显示4867号警告。等这段代码执行完以后,取消这个不显示这个警告的设置。


这两个宏对理解上面的代码无帮助。我们来分析下上面展开的的代码,上面无法是一个叫做CMFCControlsApp这个C++类的两个成员函数GetMessageMap()GetThisMessageMap()的具体实现代码,两者返回类型是AFX_M指针类型。AFX_MSGMAP是什么类型?我们再看看:


哈,AFX_MSGMAP原来这是一个自定义的结构的类型。你可能觉得不尽兴,因为这个结构体里面定义的两个东西还不是你熟悉的C语言中的基本类型。好,那让我们继续展开,结构体第一个类型是一个返回值为AFX_MSGMAP常指针类型(const AFX_MSGMAP*)。你可能也不熟悉什么是函数指针,那么我们插播个广告来讲解下什么是函数指针以及如何定义一个函数指针。


函数指针
我们以C语言函数库中的signal()函数来说明吧,signal()函数的签名如下:



首先这个函数名为signal,其次它返回void*类型,这里对这个类型说明下,我们熟悉void类型,一般表示无返回类型,而void*类型 是一个指向内存某个区域的指针,这个块内存大小根据具体情况而定,比如可以将这个不确定的类型指针转换成合适的类型,如int*,float*,struct*等等,它不再含有空的意思,而只是那块内存数据类型不确定而已,但是在合适的时候还是会转换成实际的数据指针类型的。
然后是这个函数含有两个参数,第一个参数sig是一个int型,第二个参数就是一个函数指针类型,通俗地说就是调用这个signal函数时第一个参数必须传递一个int型数据,第二个参数必须传入一个函数名,C教材上告诉我们函数名本质上就是函数在内存中的入口地址。那么是不是什么样的函数都行呢?当然不是,必须是一个返回值为void类型(注意不是void*)含有一个参数为int类型的函数。


这里的的void (*func)(int)就是声明一个函数指针,你可以把*func看作是一个整体的函数名,那么func就是一个函数指针了。
这里的第二个形参名就是func,只是写的那么复杂,只是为了说明func是一个返回值为void含有一个int型参数的函数指针类型的参数。有点绕口,希望你能转变观念。还有一种用typedef定义函数指针的方法:


如上图, 可以将f1的地址(&f1)赋给pfn,而不能将&f2,&f3赋给pfn,f2是由于函数参数类型不符合,f3是由于函数返回类型不符合。
函数指针在Windows程序中广泛使用着,尤其是一些回调函数(后面会讲到)。

好了,广告完毕,回到AFX_MSGMAP结构上来:


这个结构体第一个参数类型是一个函数指针,这个函数返回值类型为AFX_MSGMAP*,函数无参数,其中PASCAL为函数的调用方式,函数的调用方式有很多种,不同的调用方式主要区别在于调用函数时函数的参数传递顺序(是由左向右还是由右向左),函数的堆栈由调用方还是被调用方来清理。常见的函数调用方式有__cdecl 、__stdcall 、fastcall等调用方式,我们写C/C++控制台程序时,函数调用方式为__cdecl方式,而windows API函数的调用方式为__stdcall方式,请记住这一点。到底调用方式具体是怎么回事,下文中详细道来。因为我们默认的调用方式是__cdecl,所以,我们自己编写函数的时候,我们总是将这个函数调用方式修饰方式省略。下面两种写法等价:


上面的PASCAL也是一个宏,我们来展开看看:


看到了没,它本质上的调用方式也是__stdcall


回到AFX_MSGMAP结构上来:


这个结构第一个参数名为pfnGetBaseMap,第二参数名为 lpEntries,这是一个AFX_MSGMAP_ENTRY指针类型(AFX_MSGMAP_ENTRY*),我们继续展开AFX_MSGMAP_ENTRY看看:


其中UINT类型和UINT_PTR两种类型定义如下:



可以发现这两种类型本质上就是C中的unsigned int类型。AFX_MSGMAP_ENTRY结构最后一个字段类型是AFX_PMSG,其定义如下:


其中AFX_MSG_CALL仍然是函数调用方式CCmdTarget是微软mfc提供的一个类,可见AFX_PMSG是一个函数指针类型,这种函数指针是一个类成员函数指针,有点特殊哦,其返回类型为void,参数也是void。

至此,我们发现即使再复杂的Windows程序也是通过C/C++语法和基础数据类型一步步组装起来的。所以只要认真分析,没什么好怕的。说了这么多,这是我想给大家传达的意思。

下面,我们通过一个简单但完整Windows UI的程序来告诉大家怎样利用现有的C/C++知识去阅读Windows程序。
按下列步骤在VS中建立一个Windows GUI程序(GUI的意思就是Graphics User Interface),平常你写的控制台程序叫Windows CUI程序(CUI, Console User Interface):






这样一个Windows建立好了。下面编译运行下:


随着你继续的学习,你可以给这个窗口添加更多的东西和功能,甚至可以自己绘制那种不规则的窗口和自定义背景。
我将代码做了点简化,贴在下面:

        1#include "stdafx.h"  2#include "FirstWindow.h"  3LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);  4// 程序入口:  5int APIENTRY _tWinMain(HINSTANCE hInstance,  6                     HINSTANCE hPrevInstance,  7                     LPTSTR    lpCmdLine,  8                     int       nCmdShow)  9{ 10 UNREFERENCED_PARAMETER(hPrevInstance); 11 UNREFERENCED_PARAMETER(lpCmdLine); 12 static TCHAR szClassName[] = _T("WindowsProgramTemplate"); 13 HWND hWnd; 14 MSG msg; 15 WNDCLASSEX wcex; 16 wcex.cbSize = sizeof(WNDCLASSEX); 17 wcex.style   = CS_HREDRAW | CS_VREDRAW; 18 wcex.lpfnWndProc = WndProc; 19 wcex.cbClsExtra  = 0; 20 wcex.cbWndExtra  = 0; 21 wcex.hInstance  = hInstance; 22 wcex.hIcon   = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_FIRSTWINDOW)); 23 wcex.hCursor  = LoadCursor(NULL, IDC_CROSS); 24 wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1); 25 //没有菜单 26 wcex.lpszMenuName = NULL; 27 wcex.lpszClassName = szClassName; 28 wcex.hIconSm  = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL)); 29 RegisterClassEx(&wcex); 30 hWnd = CreateWindow(szClassName,      //lpClassName  31      _T("Windows Program Template"),  //lpWindowName 32      WS_OVERLAPPEDWINDOW,    //dwStyle 33      CW_USEDEFAULT,      //x 34      CW_USEDEFAULT,      //y  35      CW_USEDEFAULT,      //nWidth 36      CW_USEDEFAULT,      //nHeight  37      NULL,        //hWndParent 38      NULL,        //hMenu  39      hInstance,       //hInstance  40      NULL);        //lParam 41   ShowWindow(hWnd, nCmdShow); 42   UpdateWindow(hWnd); 43 // 主消息循环: 44 while (GetMessage(&msg, NULL, 0, 0)) 45 { 46   TranslateMessage(&msg); 47   DispatchMessage(&msg); 48 } 49 return (int) msg.wParam; 50}// end  _tWinMain 51// 52//  函数: WndProc(HWND, UINT, WPARAM, LPARAM) 53// 54//  目的: 处理主窗口的消息。 55// 56//  WM_COMMAND - 处理应用程序菜单 57//  WM_PAINT - 绘制主窗口 58//  WM_DESTROY - 发送退出消息并返回 59// 60// 61LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) 62 { 63 HDC hdc; 64 PAINTSTRUCT ps; 65 switch (message) 66{  67 case WM_PAINT: 68  hdc = BeginPaint(hWnd, &ps); 69  // TODO: 在此添加任意绘图代码... 70  EndPaint(hWnd, &ps); 71  break; 72 case WM_DESTROY: 73  PostQuitMessage(0); 74  break; 75 default: 76  return DefWindowProc(hWnd, message, wParam, lParam); 77 } 78 return 0; 79}// end WndProc     

这样的代码对于新手看起来的确很难受,但是它的的确确是C程序呀。前面也说了,Windows程序通过typedef和define关键字定义了很多的新数据类型。下面我将为你一步步地分析这个程序的运行原理。

在我们写的CUI程序(控制台程序)函数的入口是main函数,但是其实这样说不贴切,因为这是在只考虑到像美国这样只使用英文的国家的情况下,传统C语言用一个字节去存储一个字符,这样最多只能表示2的8次方个字符,也就256的字符,可是像我们中国还有日本等亚洲国家光文字就不只256个,那么我们的汉字就无法编码进计算机中,可是Windows操作系统和C/C++毕竟在全世界广泛地流行着呀。好吧,让我再给你补充下C语言知识,让我们欢迎wchar_t类型,这个数据类型也是一个字符类型,和char类型一样,只不过它是占两个字节,传统的char类型占一个字节,请以一视同仁的态度对待wchar_t和char类型。

扩展下,因为本质上char类型与int类型无多大差别,毕竟在内存中都是数字。所以在C89标准中实现wchar_t时一般如下定义:


也就说wchar_t本质上是unsigned short,因为在32位机器上short型占2个字节(16位)。为了区别与char类型,我们把wchar_t称为宽字符,请从现在开始接受并使用它吧,它现在的的确确是C/C++中的基础数据类型。

所以如果是CUI程序,那么程序入口是_tmain()函数,_tmain()函数是何许人也?它本质又是一个宏:


UNICODE是编译器定义的一个宏,可以简单地理解是否需要支持宽字符,比如在我们国家肯定是要支持宽字符啦,不然汉字是不好显示的。
上面的代码表明,如果定义了UNICODE宏,那么_tmain在预编译的时候就被替换成wmain函数,反之,就被替换成main函数,但是请注意,由于替换是发生在预编译时期,所以只会有一个main函数版本。所以对于CUI程序,它的真正入口函数要么是wmain要么是main函数。同理,我们再看看GUI程序入口函数_tWinMain


所以GUI程序入口函数要么是wWinMain或者WinMain函数。我们阅读一个Windows GUI程序要从这个函数读起。
我们好好看看这个函数:

       1int APIENTRY _tWinMain(HINSTANCE hInstance, 2                     HINSTANCE hPrevInstance, 3                     LPTSTR    lpCmdLine, 4                     int       nCmdShow)     


Windows程序的入口函数
和控制台程序(CUI程序)一样,Windows GUI程序也有一个入口函数,要么是WinMain(),要么是wWinMain(),可以综合起来统一写成_tWinMain();而CUI程序的入口是main()或者wmain(),综合起来可以写成_tmain()。_tWinMain()函数原型如下:


这个函数是Windows GUI入口函数,我们编写程序都是从这个函数开始的。这个函数有四个参数,其中第四个参数的类型是我们熟悉的int型,返回值也是int型。还有两个陌生的类型HINSTANCE和LPTSTR。请不要害怕,我来帮你分析下。你会发现它们也是由C语言基础类型组装而来的。在这之前让我来说下那个APIENTRY,这是函数调用类型,它是用一个宏,对__stdcall的一个包装,在WinDef.h文件里有如下定义:





Windows程序的数据类型
我们来回顾下C语言中的数据类型,C语言中有以下常用数据类型:


就这么多,仅此而已。下面我们就利用这些基本的数据类型来组装成Windows中的数据类型:





根据以上说明,我们回过头来看_tWinMain()函数第三个参数类型: LPTSTR,我们可以将它当作PTSTR,PTSTR本质上就是char*或者wchar_t*类型。这样一分析,第三个参数是不是就明确了类型了呀。

Windows程序中的布尔类型
由于C语言中是不存在布尔类型的,所以微软就做了自己的布尔实现,如下文:


我个人建议,除非你使用Windows函数需要使用这种BOOL类型,否则,请使用C++中原生的bool类型,尤其是在一些逻辑判断时,这点我是从电驴的源代码中借鉴的:



电驴的源代码可以在verycd.com这个网站上下载到,打开这个网站搜索下就可以了,用的是mfc框架,如果你下载不了或者找不到可以向我索要。

说完这些简单数据类型以后,剩下的我们来说说Windows程序中最多的一个数据类型:句柄(HANDLE),而且句柄演化成各种具体的句柄类型。所谓句柄就是指向某块内存的一个指针,其外在表现形式可以简单地认为是某块内存的首地址,句柄用来对某种资源进行标识:


然后呢,这个HANDLE类型摇身一变成了各种具体的句柄类型。读者请看:


好吧,现在再回过头来看_tWinMain()函数的四个参数类型,第一二两个是HINSTANCE(void *),第三个是LPTSTR(char*或者wchar_t*), 最后一个是int型。

授人以鱼不如授人以渔,我们阅读或者刚刚开始学习编写Windows程序的时候,会遇到各种数据类型和Windows函数。我们不必害怕,可以通过下列方法学习它的用法,比如这个显示一个对话框的函数MessageBox():



当你看到这个MessageBox()函数时,如何去学习它的用法呢?
第一步:请打开微软官网上的MSDN(微软开发者社区),msdn.microsoft.com/或者microsoft.com/msdn/,默认打开的显示的语言是中文网页,建议换成英文的,英文资源更丰富一点,将地址栏中的网址msdn.microsoft.com/zh_c中的zh_cn替换成en_US就可以了。



看上面第二个图,你可以在左边设置搜索范围和搜索主题,微软站点引用了两个有名的国外技术站点的资源,一个是Stack Overflow(stackoverflow.org/),另外一个是Code Project(codeproject.com/)。

这里补充一点,这种搜索方法同样适合学习C#(当然我已经不建议学C#了,如果不是特别需要的话),仔细观察第二个图,你有没有发现搜索结果的第二项显示的是.net环境中的MessageBox类,这是用于C#中的的类,我们先点开第二搜索结果项看看:



在这个页面可以很清楚地看到C#中的MessageBox类的继承树、成员属性和成员函数。我对C#语言并不熟悉,但是通过阅读这个页面上的MessageBox.Show()方法,我可以断定,在C#中可以用这个方法显示一个对话框?对么?
我们学习C#中的某个类或者API函数,在百度或者Google上面搜索的结果也没有在这个地方搜索的结果权威和准确。建议学习C#的同学注意这一点。

回到正题上来,我们打开第一个搜索项,出现如下界面:



这个界面详细地介绍了Windows函数MessageBox的用法,甚至还说明了这个函数的注意事项和头文件、在哪个dll文件中实现、使用这个函数要求的最低系统版本要求:


比如根据这个页面的说明,我们可以将上面的消息框改成带三个按钮“取消”“重试”“继续”,图标样式为问号,第二个按钮是默认按钮的样式:


其代码是:



这篇文章到此也快结束了。我已经为你在C/C++和Windows GUI程序之间架设了一道桥梁。你也看到了如何利用C基础数据类型组建成Windows中的数据类型。如果你已经有了一定的C/C++基础,那么找一点简单的Windows程序来试着读一读改一改,或者找本Windows程序的入门书来看看。当然,Charles Petzold的《Windows程序设计》这本书我是强烈推荐的。虽然这本书出版年数已经久远了,而且Windows操作系统已经发展到Win10了,但是书中所讲述的Windows程序设计的原理和机制永远不过时。中国的老一代Windows程序员就是阅读这本书成长起来的,而他们此刻或在腾讯或在金山或在迅雷等大大小小的公司,编写着你正在使用的一个个软件呢。当然,遇到不认识或者不懂的函数按照上面的方法搜索学习吧。只要坚持,要不了多久,你就可以用C/C++写出很好看的带界面的程序。慢着,至于_tWinMain()函数的四个参数具体的用法,我这篇文章也不介绍了。其实,你也可以去msdn上搜索到,自己学习嘛。不过好的英语基础还是很重要的,用曹鹏老师的一句话:“英语是编程的霓裳”。 所以请学好英语。

题外话:

我上学的时候也研究过一些优质软件的界面库,现在把这些软件的源码整理出来分享给大家。

金山卫士源码

源码地址:链接: pan.baidu.com/s/1R48X4O 密码: 9jre

电驴

链接: pan.baidu.com/s/1xH-Wb3 密码: n5i0

开源 FTP 软件 —— filezilla

链接: pan.baidu.com/s/1YGe4a8 密码: 675q

类似的话题

  • 回答
    从零开始,用 C++ 打造属于你的图形用户界面很多时候,我们希望程序能够以更加直观、易用的方式与用户交互,而不是仅仅停留在命令行界面。这时候,图形用户界面(GUI)就显得尤为重要了。很多人可能觉得 C++ 编写 GUI 是一件非常复杂的事情,需要依赖各种庞大的框架。但事实上,我们可以从最基础的概念入.............
  • 回答
    当然!在 C++ 中优雅地实现从 1 乘到 20,我们可以有多种方法,每种方法都有其独特的“优雅”之处。这里我将为你详细解释几种常见且优雅的实现方式,并分析它们的优缺点。核心目标: 计算 1 2 3 ... 20 的值。“优雅”的定义: 在编程中,“优雅”通常意味着代码具有以下特点: 清.............
  • 回答
    从只会 C++ 语法到能够独立完成软件项目,这是一个漫长但充满回报的旅程。这不仅仅是掌握更多的 C++ 特性,更重要的是理解软件工程的原理,学习解决问题的思路,以及掌握开发工具和流程。下面我将详细阐述这个过程,并提供具体的建议: 第一阶段:巩固基础,理解 C++ 的核心概念(语法进阶与初步实践)在掌.............
  • 回答
    从心理学角度解析C罗觉得自己强于梅西,这是一个非常有趣且多层面的话题。这背后涉及到多种心理机制,包括自我认知、社会比较、动机驱动、情绪管理,甚至是对身份和价值的塑造。以下将尽可能详细地阐述:一、 自我认知与自我效能感 (SelfPerception & SelfEfficacy) 核心信念与建构.............
  • 回答
    好的,咱们就聊聊C++这玩意儿怎么从一堆字符变成能在屏幕上蹦跶的游戏,这事儿说起来也挺有意思的,不是什么神秘魔法,就是一层层剥洋葱,一层层解锁。你想想,你手里拿着一本菜谱,里面写着各种步骤、配料,但它本身并不能变成一道菜。C++代码也是一样,它只是你对电脑下达的指令。那怎么才能变成一场让你沉浸其中的.............
  • 回答
    要从突破能力、个人能力和个人对球队的贡献这三个维度来比较小罗、卡卡、C 罗、梅西这四位巨星的巅峰时期,需要非常详细的分析,因为他们都是各自时代最具影响力的球员。以下将逐一展开,并尽可能详细地描述: 一、 突破能力突破能力是指球员通过带球摆脱对手防守球员的能力,这通常涉及到速度、盘带技巧、重心变化、爆.............
  • 回答
    在 C 面试中被问到代码优化,这确实是一个很能体现你技术深度的问题。回答的时候,你需要展现出你对性能的敏感度,以及解决问题的思路和方法,而不是简单地罗列几个技术名词。首先,我会从理解性能瓶颈这个源头说起。代码优化不是无的放矢,首先要明白“优化”是为了解决什么问题。是启动慢?是响应迟钝?还是内存占用过.............
  • 回答
    好的,非常乐意为您详细讲解如何使用 C 语言和 Windows API 实现一个基本的 SSL/TLS 协议。您提到参考资料已备齐,这非常好,因为 SSL/TLS 是一个相当复杂的协议,没有参考资料很难深入理解。我们将从一个高层次的概述开始,然后逐步深入到具体的 Windows API 函数和 C .............
  • 回答
    在 C 语言中绘制心形有多种方法,最常见和易于理解的方法是使用字符输出,也就是在控制台上用特定的字符(如 `` 或 ``)组合成心形的形状。另一种更高级的方法是使用图形库(如 SDL、Allegro 或 Windows GDI)来绘制真正的图形心形,但这需要更多的设置和知识。这里我们主要讲解 字符输.............
  • 回答
    当然,我们来聊聊如何在 C 中实现一个避免装箱的通用容器。这实际上是一个挺有意思的话题,因为它触及了 C 类型系统和性能优化的核心。你提到的“装箱”(boxing)是指当一个值类型(比如 `int`, `float`, `struct`)被当作引用类型(比如 `object`)来处理时,会在托管堆上.............
  • 回答
    好嘞,咱们这就来聊聊怎么用 C 语言搭一个简易计算器。别担心,不讲那些晦涩难懂的理论,咱们一步一步来,就像搭积木一样,让它一点点变得能用起来。1. 目标:我们想做什么?首先,得明确我们要造个什么样的计算器。最基本的,就是能做加、减、乘、除这四种运算。所以,咱们的用户需要输入: 第一个数字 运.............
  • 回答
    好的,不使用列表,我来详细说说如何用C语言生成一个范围在 (0, 1) 之间的随机浮点数。在C语言中,我们通常依赖标准库中的函数来处理随机数。最核心的函数是 `rand()`。1. `rand()` 函数的初步认识`rand()` 函数位于 `` 头文件中。它会返回一个介于 0 和 `RAND_MA.............
  • 回答
    要用 C++ 从头开始构建一个光栅化渲染器,这绝对是一个令人兴奋且富有挑战性的项目。它能让你深入理解图形学的底层原理,从像素的绘制到复杂的三维场景的呈现,每一步都充满了探索的乐趣。我将尽量为你详细梳理这个过程,让你感受到构建一个渲染器的“手动”乐趣。第一步:准备你的战场——基础知识与工具在真正动手写.............
  • 回答
    作为一名C开发者,想要打造一款令人眼前一亮的桌面应用界面,绝非一日之功。这需要我们从多个维度去思考和实践,结合美学原则、用户体验设计以及技术手段,才能最终呈现出既实用又赏心悦目的作品。本文就来深入探讨一下,如何在C桌面应用开发中做出漂亮的界面。一、 理解“漂亮”的内涵:超越视觉的极致体验首先,我们要.............
  • 回答
    好的,我们来聊聊怎么用 C 语言的 `for` 循环来计算 1 + 11 + 111 + 1111 这个特定的累加和。这实际上是一个很有趣的小问题,因为它涉及到了数字模式的生成和累加。理解问题:我们要加的是什么?首先,我们要清楚我们要计算的式子是:1 + 11 + 111 + 1111我们可以发现,.............
  • 回答
    在 Linux 系统中,使用 C 语言判断 `yum` 源是否配置妥当,并不是直接调用一个 C 函数就能完成的事情,因为 `yum` 的配置和操作是一个相对复杂的系统级任务,涉及到文件系统、网络通信、进程管理等多个层面。更准确地说,我们通常是通过 模拟 `yum` 的一些基本行为 或者 检查 `yu.............
  • 回答
    在C++中,当一个函数接收到一个 `T ptr` 类型的指针,而没有任何额外的上下文信息时,要准确判断应该使用 `delete ptr` 还是 `delete[] ptr`,原则上是无法绝对确定的。这是C++内存管理的一个核心设计点,也是一个潜在的陷阱。这篇文章就来深入剖析一下这个问题,并解释其中的.............
  • 回答
    .......
  • 回答
    这确实是一个有趣的挑战,很多时候我们被框架和高级技术的光环所吸引,却忽略了 C 本身作为一门语言的深度和广度。如果你的工作环境仅仅需要 C 的基础语法,那么提升的方向其实非常多,而且往往能让你对这门语言有更扎实的理解。首先,抛开对“高级技术”的执念,专注于将 C 的基础打磨到极致,这本身就是一条非常.............
  • 回答
    听到你同学这么说,我完全理解你的感受。这种说法其实挺常见的,尤其是在接触过一些“更方便”的编程语言之后。不过,要反驳他“C语言太低级,不如易语言强大好用”的说法,咱们得把事情说透了。这不是一句两句话就能解决的,需要咱们好好掰扯掰扯。首先,我们得明确一点,“低级”和“强大好用”这两件事,其实是两个维度.............

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有