绘制一个窗口

#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 不仅创建了操作系统窗口,它还秘密地完成了两个关键步骤:

  1. 调用操作系统底层 API(在 Windows 上是 WGL)创建一个 OpenGL 渲染上下文。
  2. 将这个上下文与新创建的 窗口对象 关联起来。

glfwMakeContextCurrent() 的实际工作

在一个应用程序中,你可能同时创建多个窗口,每个窗口都有自己的渲染上下文。

glfwMakeContextCurrent(window) 的作用就是:

  1. 激活: 它告诉当前的 CPU 线程,从现在开始,任何 OpenGL 调用(如 glClearColor、glDrawArrays 等)都必须作用于与传入的 window 对象关联的那个 OpenGL 渲染上下文。
  2. 切换: 如果你之前激活了另一个窗口的上下文,这个函数会将其替换为新的上下文。

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。