WhatsCanvas教程-第九章:窗口呈现与交互

WhatsCanvas教程-第九章:窗口呈现与交互

仓库地址:https://github.com/ClarkWain/WhatsCanvas

.

本章目标:学会使用 OpenGL/Vulkan/Metal/Software 后端将渲染结果显示到窗口,建立帧循环,处理窗口缩放和用户输入。


9.1 从离屏到窗口

前面的章节都使用 Software 后端进行离屏渲染。本章进入"实时渲染"领域------将每一帧绘制到屏幕窗口上。

窗口呈现的关键概念:

复制代码
创建窗口 → 创建 Canvas → 设置 OutputTarget → 帧循环:
    beginFrame() → 绘制 → endFrame() → present()

9.2 OpenGL + GLFW 窗口(最常见方案)

GLFW 是一个跨平台的窗口和 OpenGL 上下文管理库,WhatsCanvas 的所有桌面示例都使用它。

完整示例

cpp 复制代码
#define GLFW_INCLUDE_NONE
#include <GLFW/glfw3.h>
#include <wsc/wsc.h>
#include <cmath>

#if defined(_WIN32)
#define GLFW_EXPOSE_NATIVE_WIN32
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <GLFW/glfw3native.h>
#endif

using namespace wsc;

int main()
{
    // 1. 初始化 GLFW
    if (!glfwInit()) return 1;

    // 2. 设置 OpenGL 版本要求
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    glfwWindowHint(GLFW_SAMPLES, 4);  // MSAA

    // 3. 创建窗口
    int width = 800, height = 600;
    GLFWwindow* window = glfwCreateWindow(width, height,
        "WhatsCanvas Window", nullptr, nullptr);
    if (!window) { glfwTerminate(); return 1; }

    // 4. 设置 OpenGL 上下文
    glfwMakeContextCurrent(window);
    glfwSwapInterval(1);  // VSync

    // 5. 加载 OpenGL 函数指针
    if (!Canvas::loadOpenGL(
            reinterpret_cast<Canvas::OpenGLProcAddress>(glfwGetProcAddress))) {
        glfwTerminate();
        return 1;
    }

    // 6. 创建 Canvas
    auto canvas = Canvas::create(Canvas::Backend::OpenGL, width, height);
    canvas->initializeContext();

    // 7. 设置窗口呈现目标(可选,用于 Canvas 自管理 swap)
    NativeSurface surface;
#if defined(_WIN32)
    surface.platform = NativeSurface::Platform::Win32;
    surface.window = glfwGetWin32Window(window);
#endif
    bool useCanvasPresent = canvas->setOutputTarget(
        OutputTarget::ToWindow(surface));

    // 8. 帧循环
    double startTime = glfwGetTime();
    while (!glfwWindowShouldClose(window)) {
        glfwPollEvents();

        // 处理窗口大小变化
        int fbw, fbh;
        glfwGetFramebufferSize(window, &fbw, &fbh);
        if (fbw != canvas->getWidth() || fbh != canvas->getHeight()) {
            canvas->setSize(fbw, fbh);
            if (useCanvasPresent) canvas->resizeOutput(fbw, fbh);
        }

        float t = static_cast<float>(glfwGetTime() - startTime);
        float w = static_cast<float>(canvas->getWidth());
        float h = static_cast<float>(canvas->getHeight());

        // 开始帧
        canvas->beginFrame();

        // 绘制背景
        Paint bg;
        bg.setLinearGradient(0, 0, w, h,
            Color(24, 26, 34), Color(40, 44, 60));
        canvas->drawRect(RectF(0, 0, w, h), bg);

        // 动画圆形
        float cx = w * 0.5f + std::cos(t) * 150.0f;
        float cy = h * 0.5f + std::sin(t * 1.3f) * 100.0f;
        Paint circle;
        circle.setAntiAlias(true);
        circle.setRadialGradient(cx, cy, 80.0f,
            Color(120, 200, 255), Color(40, 90, 160));
        circle.setShadowLayer(20, 0, 8, Color(0, 0, 0, 120));
        canvas->drawCircle(cx, cy, 80, circle);

        // 旋转矩形
        canvas->save();
        canvas->translate(w * 0.5f, h * 0.5f);
        canvas->rotate(t * 0.7f);
        Paint box;
        box.setAntiAlias(true);
        box.setColor(Color(255, 180, 80));
        canvas->drawRoundRect(RectF(-60, -40, 120, 80), 16, box);
        canvas->restore();

        // 结束帧
        canvas->endFrame();

        // 呈现
        if (useCanvasPresent) {
            canvas->present();
        } else {
            glfwSwapBuffers(window);
        }
    }

    glfwDestroyWindow(window);
    glfwTerminate();
    return 0;
}

9.3 Vulkan 窗口呈现

Vulkan 后端完全自管理渲染资源,不需要外部 GL 上下文:

cpp 复制代码
// Vulkan 窗口:不使用 GL
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);

GLFWwindow* window = glfwCreateWindow(800, 600,
    "WhatsCanvas - Vulkan", nullptr, nullptr);

// 检查 Vulkan 可用性
if (!Canvas::isBackendAvailable(Canvas::Backend::Vulkan)) {
    // fallback 到其他后端
}

// 创建 Vulkan Canvas
auto canvas = Canvas::create(Canvas::Backend::Vulkan, 800, 600);
canvas->initializeContext();

// 设置窗口呈现
NativeSurface surface;
#if defined(_WIN32)
surface.platform = NativeSurface::Platform::Win32;
surface.window = glfwGetWin32Window(window);
#endif
canvas->setOutputTarget(OutputTarget::ToWindow(surface));

// 帧循环与 OpenGL 完全一致
while (!glfwWindowShouldClose(window)) {
    glfwPollEvents();
    canvas->beginFrame();
    // ... 绘制 ...
    canvas->endFrame();
    canvas->present();
}

9.4 Metal 窗口呈现 (macOS)

objcpp 复制代码
// macOS: 使用 Metal + CAMetalLayer
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);

GLFWwindow* window = glfwCreateWindow(800, 600,
    "WhatsCanvas - Metal", nullptr, nullptr);

auto canvas = Canvas::create(Canvas::Backend::Metal, 800, 600);
canvas->initializeContext();

// 获取 NSWindow 的 contentView
NSWindow* nsWindow = glfwGetCocoaWindow(window);
NSView* contentView = nsWindow.contentView;
contentView.wantsLayer = YES;

NativeSurface surface;
surface.platform = NativeSurface::Platform::Cocoa;
surface.window = (__bridge void*)contentView;
canvas->setOutputTarget(OutputTarget::ToWindow(surface));

// 帧循环同上

9.5 Software 窗口呈现 (Windows GDI)

纯 CPU 渲染也可以显示到窗口(通过 GDI blit):

cpp 复制代码
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);  // 无需 GL 上下文

auto canvas = Canvas::create(Canvas::Backend::Software, 800, 600);
canvas->initializeContext();

NativeSurface surface;
surface.platform = NativeSurface::Platform::Win32;
surface.window = glfwGetWin32Window(window);
canvas->setOutputTarget(OutputTarget::ToWindow(surface));

// 帧循环
while (!glfwWindowShouldClose(window)) {
    glfwPollEvents();
    canvas->beginFrame();
    // ... 绘制 ...
    canvas->endFrame();
    canvas->present();  // GDI blit
}

注意:Software 窗口呈现目前仅支持 Windows。


9.6 OutputTarget 类型汇总

cpp 复制代码
// 离屏渲染(默认)--- 使用 readPixelsRGBA 获取结果
OutputTarget::Offscreen();

// 离屏纹理 --- 结果存在 GPU 纹理中
OutputTarget::OffscreenTexture();

// 窗口呈现 --- 渲染到窗口
OutputTarget::ToWindow(surface, swapchainConfig);

// 包装现有 GL Framebuffer
OutputTarget::GLFramebuffer(fbo, width, height, opaque);

// 包装 Vulkan Image
OutputTarget::VulkanImageTarget(vkImage, vkFormat, width, height);

9.7 处理窗口大小变化

cpp 复制代码
// 在帧循环中检测大小变化
int fbw, fbh;
glfwGetFramebufferSize(window, &fbw, &fbh);
float scaleX, scaleY;
glfwGetWindowContentScale(window, &scaleX, &scaleY);
const float dpr = scaleX > 0.0f ? scaleX : 1.0f;

if (fbw != canvas->getWidth() || fbh != canvas->getHeight()
    || std::abs(dpr - canvas->devicePixelRatio()) > 0.001f) {
    canvas->setSize(fbw, fbh);
    canvas->resizeOutput(fbw, fbh);  // 通知 swapchain 更新
    canvas->setDevicePixelRatio(dpr);

    const float logicalWidth = fbw / dpr;
    const float logicalHeight = fbh / dpr;
    updateLayout(logicalWidth, logicalHeight);
}

9.8 处理用户输入

GLFW 通过回调函数处理输入事件。配合 WhatsCanvas 的 hit-test 可以实现交互:

键盘输入

cpp 复制代码
void keyCallback(GLFWwindow* window, int key, int scancode,
                 int action, int mods)
{
    if (action == GLFW_PRESS) {
        switch (key) {
            case GLFW_KEY_ESCAPE:
                glfwSetWindowShouldClose(window, true);
                break;
            case GLFW_KEY_SPACE:
                // 触发某个动作
                break;
        }
    }
}

// 注册回调
glfwSetKeyCallback(window, keyCallback);

鼠标点击 + Path Hit-Testing

cpp 复制代码
struct AppState {
    Canvas* canvas;
    Path buttonPath;
    bool buttonHovered = false;
};

void mouseCallback(GLFWwindow* window, int button, int action, int mods)
{
    if (button == GLFW_MOUSE_BUTTON_LEFT && action == GLFW_PRESS) {
        double mx, my;
        glfwGetCursorPos(window, &mx, &my);

        auto* state = static_cast<AppState*>(glfwGetWindowUserPointer(window));

        // 使用 Path 的 hit-testing
        if (state->buttonPath.contains(mx, my)) {
            // 按钮被点击!
        }

        // 或使用 Canvas 的 hit-test(考虑变换)
        if (state->canvas->hitTestPathFill(
                state->buttonPath, PointF(mx, my))) {
            // 在变换后的坐标系中命中
        }
    }
}

void cursorCallback(GLFWwindow* window, double xpos, double ypos)
{
    auto* state = static_cast<AppState*>(glfwGetWindowUserPointer(window));
    state->buttonHovered = state->buttonPath.contains(xpos, ypos);
}

glfwSetMouseButtonCallback(window, mouseCallback);
glfwSetCursorPosCallback(window, cursorCallback);
glfwSetWindowUserPointer(window, &appState);

9.9 HiDPI 支持

cpp 复制代码
// 获取内容缩放比
float scaleX, scaleY;
glfwGetWindowContentScale(window, &scaleX, &scaleY);
const float dpr = scaleX > 0.0f ? scaleX : 1.0f;

// 帧缓冲尺寸 vs 逻辑尺寸
int fbW, fbH;       // 物理像素
glfwGetFramebufferSize(window, &fbW, &fbH);

// 逻辑尺寸 = 物理尺寸 / DPR
float logicalW = fbW / dpr;
float logicalH = fbH / dpr;

// Canvas 使用物理尺寸
canvas->setSize(fbW, fbH);
canvas->setDevicePixelRatio(dpr);
// 绘制代码使用逻辑坐标(Canvas 内部根据 DPR 缩放)

getWidth() / getHeight() 返回 framebuffer 的物理尺寸。页面布局应保存 logicalW / logicalH,不能在设置 DPR 后继续用物理尺寸计算居中位置。GLFW 光标回调给出窗口内容坐标,通常已经处于逻辑坐标系;如果输入来自物理像素坐标,则先除以 DPR 再做 hit-testing。


9.10 帧率控制与性能统计

VSync

cpp 复制代码
glfwSwapInterval(1);  // 启用 VSync(限制到显示器刷新率)
glfwSwapInterval(0);  // 关闭 VSync(尽可能快)

SwapchainConfig

cpp 复制代码
SwapchainConfig config;
config.vsync = true;
config.imageCount = 3;  // Triple buffering

canvas->setOutputTarget(OutputTarget::ToWindow(surface, config));

渲染统计

cpp 复制代码
canvas->setGpuTimingEnabled(true);

// 每帧获取统计数据
auto stats = canvas->getRenderStats();
// stats 包含帧时间、draw call 数量、纹理内存等信息

9.11 完整交互示例:可拖动的圆

cpp 复制代码
#define GLFW_INCLUDE_NONE
#include <GLFW/glfw3.h>
#include <wsc/wsc.h>
#include <cmath>

using namespace wsc;

struct Ball {
    float x = 200, y = 200, radius = 40;
    bool dragging = false;
    float dragOffX = 0, dragOffY = 0;
};

Ball ball;

void mouseButton(GLFWwindow*, int button, int action, int) {
    if (button != GLFW_MOUSE_BUTTON_LEFT) return;
    double mx, my;
    glfwGetCursorPos(glfwGetCurrentContext(), &mx, &my);

    if (action == GLFW_PRESS) {
        float dx = mx - ball.x, dy = my - ball.y;
        if (dx*dx + dy*dy <= ball.radius * ball.radius) {
            ball.dragging = true;
            ball.dragOffX = dx;
            ball.dragOffY = dy;
        }
    } else {
        ball.dragging = false;
    }
}

void cursorPos(GLFWwindow*, double x, double y) {
    if (ball.dragging) {
        ball.x = x - ball.dragOffX;
        ball.y = y - ball.dragOffY;
    }
}

int main() {
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

    auto* window = glfwCreateWindow(600, 400, "Drag Demo", nullptr, nullptr);
    glfwMakeContextCurrent(window);
    glfwSwapInterval(1);

    Canvas::loadOpenGL(reinterpret_cast<Canvas::OpenGLProcAddress>(
        glfwGetProcAddress));

    auto canvas = Canvas::create(Canvas::Backend::OpenGL, 600, 400);
    canvas->initializeContext();

    glfwSetMouseButtonCallback(window, mouseButton);
    glfwSetCursorPosCallback(window, cursorPos);

    while (!glfwWindowShouldClose(window)) {
        glfwPollEvents();

        canvas->beginFrame();
        canvas->drawColor(Color(245, 245, 245));

        // 阴影
        canvas->drawBoxShadow(
            RectF(ball.x - ball.radius, ball.y - ball.radius,
                  ball.radius * 2, ball.radius * 2),
            ball.radius, 0, 16, 0, 6, Color(0, 0, 0, 60));

        // 球
        Paint p;
        p.setAntiAlias(true);
        p.setRadialGradient(ball.x - 10, ball.y - 10, ball.radius,
            Color(100, 200, 255), Color(30, 100, 200));
        canvas->drawCircle(ball.x, ball.y, ball.radius, p);

        // 提示文字
        Paint text;
        text.setColor(Color(100, 100, 100));
        text.setTextSize(14.0f);
        text.setTextAlign(Paint::TextAlign::CENTER);
        canvas->drawText("Drag the ball!", 300, 380, text);

        canvas->endFrame();
        glfwSwapBuffers(window);
    }

    glfwTerminate();
    return 0;
}

9.12 小结

本章学习了:

  • OpenGL + GLFW 窗口创建和帧循环
  • Vulkan / Metal / Software 窗口呈现
  • OutputTarget 的各种类型
  • 窗口大小变化处理
  • 用户输入(键盘/鼠标)+ Hit-Testing
  • HiDPI 支持
  • 帧率控制与性能统计

下一章多后端与 Fallback ------ 学习多后端切换策略和运行时降级。

.

.

.

.

.

.

.

.

.

相关推荐
用户307140958481 天前
零依赖实现「形切形」:我用 6200 行原生 Canvas 写了个网页设计工具
前端·ai编程·canvas
又吹风_Bassy3 天前
WhatsCanvas教程-第三章:画笔 Paint 详解
canvas·opengl·skia·2d渲染库·nanovg·whatscanvas·2d图形库
又吹风_Bassy3 天前
WhatsCanvas教程-第二章:基础图形绘制
canvas·opengl·skia·2d渲染库·nanovg·whatscanvas·2d图形库
郝学胜-神的一滴3 天前
CMake 047:解锁安装阶段自定义操作,告别配置阶段提前执行坑
运维·服务器·c++·游戏引擎·图形渲染·opengl
我就是DaLing呀!3 天前
vue3 + 独立的数据管理 Store实现视频剪辑功能
前端·typescript·vue3·canvas·store
牧艺7 天前
登录页还在用渐变?我一口气做了 5 个能摸的背景动效:吹蒲公英、滴墨染水、点熔岩闷裂
前端·canvas·交互设计
令狐掌门7 天前
OpenGL是个状态机,如何理解
opengl
Highcharts.js7 天前
Highcharts 渲染方式解析:SVG 默认渲染与 Boost 模块的 WebGL 加速,以及它与其他可视化库的不同
svg·webgl·可视化·canvas·highcharts·渲染方式
AndrewHZ10 天前
图像处理入门018 | 简易图像浏览器开发:用 OpenCV + Tkinter 打造你的第一个图像 GUI 工具
图像处理·人工智能·opencv·canvas·tkinter·事件绑定