绘制一个窗口
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>
// 窗口大小改变时的回调函数
void framebuffer_size_callback(GLFWwindow* window, int width, int height) {
glViewport(0, 0, width, height);
}
int main() {
// 初始化GLFW
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
// 创建窗口
GLFWwindow* window = glfwCreateWindow(800, 600, "OpenGL Window", NULL, NULL);
if (window == NULL) {
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
// 初始化GLAD
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
// 渲染循环
while (!glfwWindowShouldClose(window)) {
// 处理输入
glfwPollEvents();
// 渲染指令
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// 交换缓冲区和检查IO事件
glfwSwapBuffers(window);
}
// 清理资源
glfwTerminate();
return 0;
}
framebuffer_size_callback: 这是一个回调函数,当用户改变窗口大小时,GLFW 会自动调用它。
glViewport(0, 0, width, height):这是一个关键的 OpenGL 函数。它告诉 OpenGL 渲染窗口(Framebuffer)中哪个区域应该被渲染。在这里,它将渲染区域设置为与新的窗口尺寸完全匹配,确保内容不会被拉伸或裁剪。
初始化 GLFW 和创建窗口
glfwInit(): 初始化 GLFW 库。在使用任何 GLFW 函数之前必须调用它。
glfwWindowHint(...): 这是设置 OpenGL 上下文版本和模式 的重要函数:
GLFW_CONTEXT_VERSION_MAJOR, 3和GLFW_CONTEXT_VERSION_MINOR, 3:要求 GLFW 创建一个 OpenGL 3.3 版本的上下文。GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE:要求使用 核心模式 (Core Profile),这意味着你将无法使用旧版(已弃用)的固定功能管线函数,强制使用现代的 Shader 编程。
glfwCreateWindow(...): 创建一个 800x600 像素、标题为 "OpenGL Window" 的窗口对象。
glfwMakeContextCurrent(window): 告诉 GLFW/OpenGL,接下来的所有 OpenGL 调用都将作用于这个窗口上下文。它执行的是 激活 动作,而不是 绑定指针 动作。
在 OpenGL 中,所有的状态(比如设置的颜色、启用的特性、创建的缓冲区 ID 等)都存储在一个被称为 渲染上下文(Rendering Context) 的巨大状态机中。创建上下文: 当你调用 glfwCreateWindow() 时,GLFW 不仅创建了操作系统窗口,它还秘密地完成了两个关键步骤:
- 调用操作系统底层 API(在 Windows 上是 WGL)创建一个 OpenGL 渲染上下文。
- 将这个上下文与新创建的 窗口对象 关联起来。
glfwMakeContextCurrent() 的实际工作
在一个应用程序中,你可能同时创建多个窗口,每个窗口都有自己的渲染上下文。
glfwMakeContextCurrent(window) 的作用就是:
- 激活: 它告诉当前的 CPU 线程,从现在开始,任何 OpenGL 调用(如
glClearColor、glDrawArrays等)都必须作用于与传入的window对象关联的那个 OpenGL 渲染上下文。 - 切换: 如果你之前激活了另一个窗口的上下文,这个函数会将其替换为新的上下文。
glfwSetFramebufferSizeCallback(...): 注册前面定义的回调函数,以处理窗口大小改变事件。 这个函数的作用是作为 回调(Callback) 被调用。当用户拖动窗口边框、最大化或最小化窗口时,窗口的帧缓冲(Framebuffer) 尺寸会发生变化。每当这个尺寸发生变化时,GLFW 就会自动调用你注册的这个函数。
初始化 GLAD
// 初始化GLAD
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
gladLoadGLLoader(...): 这是 GLAD 的核心功能。它负责加载所有现代 OpenGL 3.3 函数的指针。
- 它需要一个函数指针作为参数,用于查找函数地址。
glfwGetProcAddress是 GLFW 提供的一个函数,它能够正确地在当前操作系统中查询到 OpenGL 函数的地址。GLAD 使用这个函数来完成它的加载任务。
渲染循环
// 渲染循环
while (!glfwWindowShouldClose(window)) {
// 处理输入
glfwPollEvents();
// 渲染指令
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// 交换缓冲区和检查IO事件
glfwSwapBuffers(window);
}
这是一个典型的游戏/渲染循环,它会一直运行,直到用户关闭窗口 (glfwWindowShouldClose(window) 返回 true)。
glfwPollEvents(): 检查是否有触发的事件(如键盘输入、鼠标移动、窗口关闭),并调用相应的回调函数(如framebuffer_size_callback)。glClearColor(R, G, B, A): 设置清除颜色。这里设置为深蓝色/蓝绿色 (RGB: 0.2, 0.3, 0.3)。glClear(GL_COLOR_BUFFER_BIT): 使用上面设置的颜色来清空颜色缓冲区(即清除屏幕)。这是每帧绘制的起点。glfwSwapBuffers(window): 交换前后缓冲区。 大多数渲染系统使用双缓冲机制,在后台缓冲区进行所有绘制操作,然后与显示在屏幕上的前台缓冲区进行交换,以避免画面闪烁。
GLFW 的默认行为
当你在 main() 函数中调用 glfwCreateWindow(...) 时,GLFW 会调用操作系统的底层 API(在 Windows 上是 Win32 API)来创建窗口。默认情况下,它请求操作系统创建一个 带有标准边框和标题栏 的窗口。
这个标题栏(你所说的“菜单栏”)不是由 GLFW 或 OpenGL 绘制的,而是由 操作系统(Windows 的桌面窗口管理器 DWM) 绘制和管理的。它负责处理:
- 窗口的拖动和移动。
- 窗口的最小化、最大化和关闭按钮。
- 窗口的边框调整大小。
OpenGL 仅渲染客户区 (Client Area)
你的所有 OpenGL 渲染指令(包括 glClearColor 和 glClear)都只作用于窗口的 客户区(Client Area),也就是标题栏和边框内部的区域。标题栏和边框是非客户区(Non-Client Area),由操作系统控制。
Win32 API 创建窗口的原理
调用 Win32 API 创建窗口的原理,不是简单地利用 Windows API 的画笔(GDI)来绘制,而是一个涉及对象注册、资源分配和消息循环的复杂过程。
核心机制:窗口类(Window Class)与对象句柄(HWND)
创建窗口的第一步是定义窗口的行为和外观。
| 步骤 | Win32 API 调用 | 作用 |
|---|---|---|
| A. 注册 | RegisterClassEx() | 你的程序必须先定义一个窗口类(Window Class)。这个类是窗口的模板,定义了窗口的默认行为(如窗口过程函数地址、图标、背景色、鼠标样式等)。这个类在 OS 内部注册,所有基于此模板创建的窗口都共享这些属性。 |
| B. 创建 | CreateWindowEx() | OS 根据你注册的窗口类创建一个具体的窗口实例。这个函数返回一个 HWND (Window Handle,窗口句柄)。HWND 是一个不透明的指针,是 OS 内部用来唯一标识和管理这个窗口对象的 ID。 |
| C. 显示 | ShowWindow() | 窗口对象创建后并不会立刻显示,你必须调用 ShowWindow() 来让 OS 调整其状态,使其可见。 |
窗口行为的核心:窗口过程(Window Procedure)和消息循环
窗口本身不包含任何绘制或逻辑代码,它的行为完全由其关联的 窗口过程函数(Window Proc/WndProc) 决定。
- 消息机制: 你的程序通过一个**消息循环(Message Loop)**不断从 OS 接收消息。任何用户或系统事件(鼠标点击、按键、窗口移动、窗口需要重绘)都被 OS 封装成一个 消息(Message) 并放入你的程序的消息队列中。
- 窗口过程: 当收到消息时,OS 调用窗口的
WndProc函数,并将消息(例如WM_LBUTTONDOWN或WM_PAINT)作为参数传递进去。你的代码在WndProc中通过巨大的switch语句来响应这些消息。
窗口绘制:GDI、DirectX/OpenGL 与 DWM
关于“画笔绘制”的问题,我们需要区分 非客户区 和 客户区。
A. 非客户区绘制(Title Bar & Border)
你之前在 GLFW 窗口中看到的 标题栏、边框、控制按钮 属于非客户区(Non-Client Area)。
- 绘制者: 这个区域主要由 Windows 桌面窗口管理器 (DWM, Desktop Window Manager) 负责绘制和管理。DWM 使用 DirectX 来进行硬件加速渲染,以确保标题栏的 Aero 特效、动画和透明度等效果流畅。
- 你的程序无法直接控制 除非你指示 OS 移除这些装饰 (
GLFW_DECORATED, GLFW_FALSE)。
B. 客户区绘制(Client Area)
窗口的内部区域(即你运行 OpenGL 或 GDI 绘制图形的区域)被称为客户区(Client Area)。
| 渲染技术 | 描述 |
|---|---|
| GDI (画笔/画刷) | GDI(Graphics Device Interface)是传统的 Windows 2D 绘图 API。如果你收到 WM_PAINT 消息,OS 会要求你重绘窗口的客户区。在这个函数中,你通常使用 GDI 的 画笔(HPEN) 和 画刷(HBRUSH) 来绘制 2D 图形和文本。 |
| OpenGL/DirectX | 对于高性能 3D/2D 渲染,你通常会忽略 WM_PAINT 消息,而是直接使用 WGL(Windows Graphics Library) 或 DXGI 创建一个硬件加速的渲染上下文,并使用 OpenGL 或 DirectX 在客户区上进行渲染。这些现代 API 绕过了 GDI,直接将指令发送给 GPU。 |