窗口工具库GLFW使用

使用OpenGL或者Vulkan的图形库时经常使用一些窗口工具库搭配,常见的就是GLFW: Introduction了,类似的有SDL,SFML和win32库,这里也会简单说一下. 这篇文章相当于GLFW的api介绍

SDL与SFML都是跨平台的多媒体库,除了创建窗口之外,还包括音视频库甚至网络库sfmlSDL_net,它们在不同平台上使用对应的库.比如SDL在windows上使用win32,使用direct3D作为图形库,在Linux上使用x11作为窗口系统,openGL作为图形库.

image-20241006121306560

SFML可以将其视为面向对象的SDL,在windows上使用了gdi32+opengl32.而GLFW相对来说更纯粹,官网直接说是OpenGL的上下文管理器.

image-20241006121554268

类似SFML的还有Fast Light Toolkit - Fast Light Toolkit (FLTK)

此外也有更上层imgui和raylib库,相当于同时包含了窗口管理和绘图等功能,raylib在windows和Linux上均使用GLFW和OpenGLraylib platforms and graphics · raysan5/raylib Wiki (github.com),而imgui可以相对更自由地选择搭配.

此外我也看到有人提到Skia,Blend2D以及cairographics.org,Yue (libyue.com)等等,它们是2D绘图库,相对使用opengl,d3d等更节省资源,本身可以创建上下文,也能与窗口结合

GLFW

GLFW目前GLFW版本到了3.0,下面是一个GLFW的经典流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <GLFW/glfw3.h>

int main(void)
{
GLFWwindow* window;

/* Initialize the library */
if (!glfwInit())
return -1;

/* Create a windowed mode window and its OpenGL context */
window = glfwCreateWindow(640, 480, "Hello World", NULL, NULL);
if (!window)
{
glfwTerminate();
return -1;
}

/* Make the window's context current */
glfwMakeContextCurrent(window);

/* Loop until the user closes the window */
while (!glfwWindowShouldClose(window))
{
/* Render here */
glClear(GL_COLOR_BUFFER_BIT);

/* Swap front and back buffers */
glfwSwapBuffers(window);

/* Poll for and process events */
glfwPollEvents();
}

glfwTerminate();
return 0;
}

包含头文件

默认GLFW的头文件包括OpenGL头文件,但是版本可能比较老,一般使用一个loader library获取版本,比如glad. 如果包括了glad可以检测到从而不使用开发环境中的gl头文件.

为了确保没有头冲突,您可以在GLFW头之前定义GLFW_INCLUDE_NONE,以显式禁用开发环境头的包含。这也允许以任何顺序包含两个头

1
2
3
#define GLFW_INCLUDE_NONE
#include <GLFW/glfw3.h>
#include <glad/gl.h>

初始化和停止GLFW

在使用大多数GLFW函数之前,必须初始化库。初始化成功时,返回GLFW_TRUE。如果发生错误,则返回GLFW_FALSE

1
2
3
4
if (!glfwInit())
{
// Initialization failed
}

当使用完GLFW后,通常在应用程序退出之前,需要终止GLFW

这将终止所有剩余的窗口并释放由GLFW分配的任何其他资源。在此调用之后,在使用任何需要它的GLFW函数之前,必须再次初始化GLFW

1
glfwTerminate();

设置错误处理

大多数事件都是通过回调的,无论是按下的键、移动的GLFW窗口还是发生的错误。回调是由GLFW调用的带有描述事件的参数的C函数(或c++静态方法)

如果GLFW函数失败,则会向GLFW错误回调函数报告一个错误。您可以通过错误回调接收这些报告。此函数必须具有下面的签名,但可以执行其他回调中允许的任何操作

1
2
3
4
void error_callback(int error, const char* description)
{
fprintf(stderr, "Error: %s\n", description);
}

必须设置回调函数,这样GLFW才知道调用它们。设置错误回调的函数是少数几个可以在初始化之前调用的GLFW函数之一,它可以在初始化期间和之后通知错误

1
glfwSetErrorCallback(error_callback);

创建与销毁窗口

1
2
3
4
5
GLFWwindow* window = glfwCreateWindow(640, 480, "My Title", NULL, NULL);
if (!window)
{
// Window or OpenGL context creation failed
}

这将创建一个带有OpenGL上下文的640 * 480窗口模式窗口。如果窗口或OpenGL上下文创建失败,将返回NULL。您应该始终检查返回值。虽然窗口创建很少失败,但上下文创建取决于正确安装的驱动程序,甚至在具有必要硬件的机器上也可能失败。

默认情况下,GLFW创建的OpenGL上下文可以有任何版本。可以通过在创建之前设置GLFW_CONTEXT_VERSION_MAJOR和GLFW_CONTEXT_VERSION_MINOR提示来要求最小OpenGL版本。如果机器上不支持所需的最低版本,则上下文(和窗口)创建失败

通过设置GLFW_OPENGL_PROFILE提示,可以选择OpenGL profile.

1
2
3
4
5
6
7
8
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
GLFWwindow* window = glfwCreateWindow(640, 480, "My Title", NULL, NULL);
if (!window)
{
// Window or context creation failed
}

我们经常提到OpenGL上下文, glfwCreateWindow会返回窗口,这个窗口就相当于一个OpenGL上下文

1
glfwDestroyWindow(window);

一旦调用这个函数,就不会再为该窗口传递事件,并且它的句柄无效

设置当前OpenGL上下文

1
glfwMakeContextCurrent(window);

该上下文将保持当前状态,直到将另一个上下文设置为当前状态,或者直到拥有当前上下文的窗口被销毁。

检查窗口是否关闭

每个窗口都有一个标志,指示该窗口是否应该关闭。

1
2
3
4
while (!glfwWindowShouldClose(window))
{
// Keep running
}

当用户试图关闭窗口时,通过按标题栏中的关闭小部件或使用像Alt+F4这样的组合键,该标志被设置为1。注意该窗口实际上并没有关闭,因此您应该监视该标志,并销毁该窗口或向用户提供某种反馈

当用户试图关闭窗口时,你可以通过使用glfwSetWindowCloseCallback设置一个关闭回调来得到通知。在关闭标志被设置后,回调函数将被立即调用。

接受用户输入

每个窗口都有大量的回调函数,可以将其设置为接收所有不同类型的事件。要接收按键按下和释放事件,需要创建按键回调函数

1
2
3
4
5
static void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods)
{
if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
glfwSetWindowShouldClose(window, GLFW_TRUE);
}

设置键盘回调

1
glfwSetKeyCallback(window, key_callback);

使用OpenGL渲染

当有一个当前的OpenGL上下文,可以正常使用OpenGL

使用glfwGetFramebufferSize后去帧缓冲区大小,并为glViewport设置

1
2
3
int width, height;
glfwGetFramebufferSize(window, &width, &height);
glViewport(0, 0, width, height);

获得timer

为了创建流畅的动画,需要一个时间源。GLFW提供了一个计时器,返回自初始化以来的秒数。所使用的时间源在每个平台上都是最精确的,通常具有微秒或纳秒分辨率

1
double time = glfwGetTime();

交换缓冲区

默认情况下,GLFW窗口使用双缓冲。这意味着每个窗口都有两个渲染缓冲区;一个前缓冲,一个后缓冲。前缓冲区是要显示的缓冲区,后缓冲区是要渲染的缓冲区。

当整个帧被渲染后,缓冲区需要相互交换,所以后缓冲区变成前缓冲区,反之亦然

1
glfwSwapBuffers(window);

交换间隔表示在交换缓冲区之前需要等待多少帧,通常称为vsync。默认情况下,交换间隔为零,这意味着缓冲区交换将立即发生。在快速的机器上,许多这些帧永远不会被看到,因为屏幕通常每秒只更新60-75次,所以这浪费了大量的CPU和GPU周期

屏幕撕裂是视频显示中的视觉伪影,显示设备在单个屏幕绘制中显示来自多个帧的信息

由于这些原因,应用程序通常希望将交换间隔设置为1。它可以设置为更高的值,但通常不建议这样做,因为它会导致输入延迟

处理事件

GLFW需要定期与窗口系统通信,以便接收事件并显示应用程序尚未锁定。事件处理必须在有可见窗口时定期执行,通常在缓冲区交换后的每一帧执行。

处理挂起事件有两种方法:轮询和等待。这个例子使用事件轮询,它只处理那些已经接收到的事件,然后立即返回。

1
glfwPollEvents();

使用GLAD

如果不用glad,opengl版本就与装的动态库与使用的gl头文件相关,如果使用glad,就能根据版本选择对应版本的库

1
gladLoadGL(glfwGetProcAddress);

一些老代码使用glfwGetProcAddress((GLADloadproc)glfwGetProcAddress)

可以使用gladLoadGL,首先加载dll库,获取gl动态库中的wglGetProcAddress,得到这个函数方便获的gl的地址,调用gladLoadGLLoader传递get_proc,get_proc中调用类似gladGetProcAddressPtr,GetProcAddress操作获取对应的函数指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
int gladLoadGL(void) {
int status = 0;

if(open_gl()) {
status = gladLoadGLLoader(&get_proc);
close_gl();
}

return status;
}
int open_gl(void) {
#ifndef IS_UWP
libGL = LoadLibraryW(L"opengl32.dll");
if(libGL != NULL) {
void (* tmp)(void);
tmp = (void(*)(void)) GetProcAddress(libGL, "wglGetProcAddress");
gladGetProcAddressPtr = (PFNWGLGETPROCADDRESSPROC_PRIVATE) tmp;
return gladGetProcAddressPtr != NULL;
}
#endif

return 0;
}

void* get_proc(const char *namez) {
void* result = NULL;
if(libGL == NULL) return NULL;

#if !defined(__APPLE__) && !defined(__HAIKU__)
if(gladGetProcAddressPtr != NULL) {
result = gladGetProcAddressPtr(namez);
}
#endif
if(result == NULL) {
#if defined(_WIN32) || defined(__CYGWIN__)
result = (void*)GetProcAddress((HMODULE) libGL, namez);
#else
result = dlsym(libGL, namez);
#endif
}

return result;
}

gladLoadGLLoader中将利用刚才的get_proc函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

int gladLoadGLLoader(GLADloadproc load) {
GLVersion.major = 0; GLVersion.minor = 0;
glGetString = (PFNGLGETSTRINGPROC)load("glGetString");
if(glGetString == NULL) return 0;
if(glGetString(GL_VERSION) == NULL) return 0;
find_coreGL();
load_GL_VERSION_1_0(load);
load_GL_VERSION_1_1(load);
load_GL_VERSION_1_2(load);
load_GL_VERSION_1_3(load);
load_GL_VERSION_1_4(load);
load_GL_VERSION_1_5(load);
load_GL_VERSION_2_0(load);
load_GL_VERSION_2_1(load);
load_GL_VERSION_3_0(load);
load_GL_VERSION_3_1(load);
load_GL_VERSION_3_2(load);
load_GL_VERSION_3_3(load);
load_GL_VERSION_4_0(load);
load_GL_VERSION_4_1(load);
load_GL_VERSION_4_2(load);
load_GL_VERSION_4_3(load);
load_GL_VERSION_4_4(load);
load_GL_VERSION_4_5(load);
load_GL_VERSION_4_6(load);

if (!find_extensionsGL()) return 0;
return GLVersion.major != 0 || GLVersion.minor != 0;
}
1
2
PFNGLGETSTRINGPROC glad_glGetString = NULL;
#define glGetString glad_glGetString

首先获取gl获取版本的函数,然后根据版本加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
static void find_coreGL(void) {

/* Thank you @elmindreda
* https://github.com/elmindreda/greg/blob/master/templates/greg.c.in#L176
* https://github.com/glfw/glfw/blob/master/src/context.c#L36
*/
int i, major, minor;

const char* version;
const char* prefixes[] = {
"OpenGL ES-CM ",
"OpenGL ES-CL ",
"OpenGL ES ",
NULL
};

version = (const char*) glGetString(GL_VERSION);
if (!version) return;
// 根据获取到的版本,匹配对应的前缀,再加上
for (i = 0; prefixes[i]; i++) {
const size_t length = strlen(prefixes[i]);
if (strncmp(version, prefixes[i], length) == 0) {
version += length;
break;
}
}

/* PR #18 */
#ifdef _MSC_VER
sscanf_s(version, "%d.%d", &major, &minor);
#else
sscanf(version, "%d.%d", &major, &minor);
#endif

GLVersion.major = major; GLVersion.minor = minor;
max_loaded_major = major; max_loaded_minor = minor;
GLAD_GL_VERSION_1_0 = (major == 1 && minor >= 0) || major > 1;
GLAD_GL_VERSION_1_1 = (major == 1 && minor >= 1) || major > 1;
GLAD_GL_VERSION_1_2 = (major == 1 && minor >= 2) || major > 1;
GLAD_GL_VERSION_1_3 = (major == 1 && minor >= 3) || major > 1;
GLAD_GL_VERSION_1_4 = (major == 1 && minor >= 4) || major > 1;
GLAD_GL_VERSION_1_5 = (major == 1 && minor >= 5) || major > 1;
GLAD_GL_VERSION_2_0 = (major == 2 && minor >= 0) || major > 2;
GLAD_GL_VERSION_2_1 = (major == 2 && minor >= 1) || major > 2;
GLAD_GL_VERSION_3_0 = (major == 3 && minor >= 0) || major > 3;
GLAD_GL_VERSION_3_1 = (major == 3 && minor >= 1) || major > 3;
GLAD_GL_VERSION_3_2 = (major == 3 && minor >= 2) || major > 3;
GLAD_GL_VERSION_3_3 = (major == 3 && minor >= 3) || major > 3;
GLAD_GL_VERSION_4_0 = (major == 4 && minor >= 0) || major > 4;
GLAD_GL_VERSION_4_1 = (major == 4 && minor >= 1) || major > 4;
GLAD_GL_VERSION_4_2 = (major == 4 && minor >= 2) || major > 4;
GLAD_GL_VERSION_4_3 = (major == 4 && minor >= 3) || major > 4;
GLAD_GL_VERSION_4_4 = (major == 4 && minor >= 4) || major > 4;
GLAD_GL_VERSION_4_5 = (major == 4 && minor >= 5) || major > 4;
GLAD_GL_VERSION_4_6 = (major == 4 && minor >= 6) || major > 4;
if (GLVersion.major > 4 || (GLVersion.major >= 4 && GLVersion.minor >= 6)) {
max_loaded_major = 4;
max_loaded_minor = 6;
}
}

可以看到如果版本大于某个大版本,会加载对应的函数,也就是说1.0加载的东西,在1.1的函数中就不会有了,文件中定义了许多函数指针

1
2
3
4
5
6
7
8
9
10
11
typedef void (APIENTRYP PFNGLGENSAMPLERSPROC)(GLsizei count, GLuint *samplers);
typedef void (APIENTRYP PFNGLGENTRANSFORMFEEDBACKSPROC)(GLsizei n, GLuint *ids);
typedef void (APIENTRYP PFNGLGENVERTEXARRAYSPROC)(GLsizei n, GLuint *arrays);
PFNGLGENTEXTURESPROC glad_glGenTextures = NULL;
PFNGLGENTRANSFORMFEEDBACKSPROC glad_glGenTransformFeedbacks = NULL;
PFNGLGENVERTEXARRAYSPROC glad_glGenVertexArrays = NULL;
PFNGLGENERATEMIPMAPPROC glad_glGenerateMipmap = NULL;
PFNGLGENERATETEXTUREMIPMAPPROC glad_glGenerateTextureMipmap = NULL;
PFNGLGETACTIVEATOMICCOUNTERBUFFERIVPROC glad_glGetActiveAtomicCounterBufferiv = NULL;
PFNGLGETACTIVEATTRIBPROC glad_glGetActiveAttrib = NULL;
PFNGLGETACTIVESUBROUTINENAMEPROC glad_glGetActiveSubroutineName = NULL;

当加载完毕后即可直接使用glad_xx调用glxx库.

glad相当于在知道使用的gl.dll版本之后加载相应版本所有的头文件,加载后即可使用gladxx,而原本的gl头文件实际上就不需要了(除非你还需要在代码中直接使用gl头文件)

image-20241005230313466

glad.h中如果之前定义了__gl_h_宏,那就会报错,否则自己会定义一个__gl_h

1
2
3
4
5
6
7
8
// glad.h
#ifndef __glad_h_
#define __glad_h_

#ifdef __gl_h_
#error OpenGL header already included, remove this include, glad already provides it
#endif
#define __gl_h_

而在gl.h中,定义了这个宏,表明使用glad并不需要引入gl头文件

1
2
3
4
5
6
7
8
9
10
11
12
// gl.h
#ifndef __gl_h_
#ifndef __GL_H__

#define __gl_h_
#define __GL_H__

#include <winapifamily.h>

#ifdef __cplusplus
extern "C" {
#endif

而在glfw3.h中也说明了如果定义了__gl_h_,就不需要再引入头文件了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#elif !defined(GLFW_INCLUDE_NONE) && \
!defined(__gl_h_) && \
!defined(__gles1_gl_h_) && \
!defined(__gles2_gl2_h_) && \
!defined(__gles2_gl3_h_) && \
!defined(__gles2_gl31_h_) && \
!defined(__gles2_gl32_h_) && \
!defined(__gl_glcorearb_h_) && \
!defined(__gl2_h_) /*legacy*/ && \
!defined(__gl3_h_) /*legacy*/ && \
!defined(__gl31_h_) /*legacy*/ && \
!defined(__gl32_h_) /*legacy*/ && \
!defined(__glcorearb_h_) /*legacy*/ && \
!defined(__GL_H__) /*non-standard*/ && \
!defined(__gltypes_h_) /*non-standard*/ && \
!defined(__glee_h_) /*non-standard*/

#if defined(__APPLE__)

#if !defined(GLFW_INCLUDE_GLEXT)
#define GL_GLEXT_LEGACY
#endif
#include <OpenGL/gl.h>

#else /*__APPLE__*/

#include <GL/gl.h>
#if defined(GLFW_INCLUDE_GLEXT)
#include <GL/glext.h>
#endif

#endif /*__APPLE__*/

#endif /* OpenGL and OpenGL ES headers */

初始化

在调用大多数GLFW函数之前,必须初始化库。这个初始化检查机器上可用的特性、显示器初始化计时器并执行任何所需的特定于平台的初始化

在调用glfwInit之前可以调用一些函数,比如获取版本,平台支持,设置错误处理回调等.

hints

hints用于设置GLFW,包括共享/通用的hints以及platform-specific的hints.

1
2
glfwInitHint(GLFW_JOYSTICK_HAT_BUTTONS, GLFW_FALSE);
glfwInitHint(GLFW_PLATFORM, GLFW_PLATFORM_X11);

自定义内存分配器

1
2
3
4
5
6
7
GLFWallocator allocator;
allocator.allocate = my_malloc;
allocator.reallocate = my_realloc;
allocator.deallocate = my_free;
allocator.user = NULL;

glfwInitAllocator(&allocator);

设置错误处理

1
2
3
4
int code = glfwGetError(NULL);

if (code != GLFW_NO_ERROR)
handle_error(code);
1
2
3
4
5
6
glfwSetErrorCallback(error_callback);

void error_callback(int code, const char* description)
{
display_error_message(code, description);
}

只要GLFW成功初始化,无论发生多少错误,它都将保持初始化并处于安全状态,直到终止。如果在初始化过程中发生错误导致glfwInit失败,则初始化库的任何部分都将被安全终止

坐标系统

GLFW有两个主要的坐标系统,虚拟屏幕和窗口内容区域.

image-20241008191903556

​ 虚拟屏幕和内容区域坐标系统的x轴指向右,y轴指向下.

​ 窗口和显示器的位置指定为其内容区域的左上角相对于虚拟屏幕的位置,而光标的位置指定为相对于窗口的内容区域的位置

​ 由于窗口的内容区域坐标系统的原点也是指定窗口位置的点,因此可以通过添加窗口位置将内容区域坐标转换为虚拟屏幕。当窗口frame出现时,它从内容区域向外延伸,但不影响窗口位置

​ GLFW中几乎所有的位置和大小都是以相对于上述两个原点之一的屏幕坐标来测量的。这包括光标位置、窗口位置和大小、窗口frame大小、显示器位置和视频分辨率.

​ 显示器的物理大小以毫米为单位和帧缓冲区大小(以像素为单位)。

​ 像素和屏幕坐标在一些机器上可能是1:1的映射,但在其他机器上就不一定了,比如在带有Retina显示屏的Mac上dpr是2:1。屏幕坐标和像素之间的比率也可能在运行时改变,这取决于窗口当前被认为在哪个显示器上

窗口

窗口对象封装了顶级窗口和OpenGL或OpenGL ES上下文。它是用glfwCreateWindow创建的,用glfwDestroyWindow或glfwTerminate销毁

由于窗口和上下文是不可分割地联系在一起的,窗口对象也充当上下文句柄。

设置窗口的一些属性

想像一下,一个窗口能有哪些东西? 标题,显示器,位置,大小,是否透明,最小化,最大化,焦点,图标. 这些属性有些可以使用hints设置,有些有单独的函数设置,还可以使用glfwSetWindowAttrib设置.

需要注意的是Framebuffer size和window size,window size是虚拟屏幕坐标,而framebuffer size是pixel,适合使用glviewport

1
2
3
int width, height;
glfwGetFramebufferSize(window, &width, &height);
glViewport(0, 0, width, height);

窗口属性

Windows有许多属性可以使用glfwGetWindowAttrib返回。一些反映了可能由于用户交互而改变的状态(例如是否有输入焦点),而另一些反映了窗口的固有属性(例如它有什么样的边界)。一些与窗口相关,另一些与OpenGL或OpenGL ES上下文相关

1
2
3
4
5
6
if (glfwGetWindowAttrib(window, GLFW_FOCUSED))
{
// window has input focus
}

glfwSetWindowAttrib(window, GLFW_RESIZABLE, GLFW_FALSE);

buffer交换

默认情况下,GLFW窗口是双缓冲的。这意味着有两个渲染缓冲区;一个前缓冲,一个后缓冲。前缓冲区是要显示的缓冲区,后缓冲区是要渲染的缓冲区

1
glfwSwapBuffers(window);

选择何时进行缓冲区交换是很有用的。使用函数glfwSwapInterval,可以选择驱动程序在交换缓冲区之前从调用glfwSwapBuffers开始应该等待的监视器刷新的最小次数

1
glfwSwapInterval(1);

如果间隔为零,则在调用glfwSwapBuffers时立即进行交换,而无需等待刷新。否则,每次缓冲区交换之间至少会传递间隔回溯。当不希望测量等待垂直回溯所需的时间时,使用零交换间隔对基准测试很有用。但是,交换间隔为1可以避免撕裂

上下文

当使用glfwCreateWindow创建一个窗口和它的OpenGL或OpenGL ES上下文时,可以指定另一个窗口,它的上下文应该与新窗口共享它的对象(纹理,顶点和元素缓冲区等)

1
GLFWwindow* second_window = glfwCreateWindow(640, 480, "Second Window", NULL, first_window);

在你进行OpenGL或OpenGL ES调用之前,需要有一个正确类型的当前上下文。一个上下文一次只能对一个线程是当前的,而一个线程一次只能有一个当前的上下文

1
2
glfwMakeContextCurrent(window);
GLFWwindow* window = glfwGetCurrentContext();

显示器

显示器对象表示当前连接的显示器,并表示为指向不透明类型GLFWmonitor的指针。显示器对象不能由应用程序创建或销毁,并保留其地址,直到它们所代表的显示器断开连接或直到库终止

每个显示器都有一个当前视频模式,一个支持的视频模式列表,一个虚拟位置,一个人类可读的名称,一个估计的物理尺寸和一个gamma ramp。其中一个监控器是主监控器.显示器的虚拟位置以屏幕坐标表示,并与当前视频模式一起描述了连接的显示器提供给跨越它们的虚拟桌面的视口

1
2
3
GLFWmonitor* primary = glfwPrimaryMonitor();
int count;
GLFWmonitor** monitors = glfwGetMonitors(&count);

显示器配置改变

1
2
3
4
5
6
7
8
9
10
11
12
glfwSetMonitorCallback(monitor_callback);
void monitor_callback(GLFWmonitor* monitor, int event)
{
if (event == GLFW_CONNECTED)
{
// The monitor was connected
}
else if (event == GLFW_DISCONNECTED)
{
// The monitor was disconnected
}
}

显示器属性

当创建一个全屏窗口时,改变它的视频模式或使窗口成为一个全屏,GLFW通常会很好地选择一个合适的视频模式,但有时确切地知道支持哪些视频模式是有用的。

视频模式表示为GLFWvidmode结构。您可以使用glfwGetVideoModes获得监视器支持的视频模式数组。有关返回数组的生命周期

1
2
3
int count;
GLFWvidmode* modes = glfwGetVideoModes(monitor, &count);
const GLFWvidmode* mode = glfwGetVideoMode(monitor);

监视器的物理尺寸(以毫米为单位)或其估计值可以使用glfwGetMonitorPhysicalSize来检索。这与当前分辨率无关

1
2
int width_mm, height_mm;
glfwGetMonitorPhysicalSize(monitor, &width_mm, &height_mm);

可以使用glfwGetMonitorPos获取监视器在虚拟桌面上的位置(以屏幕坐标表示)。

1
2
int xpos, ypos;
glfwGetMonitorPos(monitor, &xpos, &ypos);

未被全局任务栏或菜单栏占用的监视器区域是工作区域。这是在屏幕坐标中指定的,可以使用glfwGetMonitorWorkarea进行检索

1
2
int xpos, ypos, width, height;
glfwGetMonitorWorkarea(monitor, &xpos, &ypos, &width, &height);

输入

GLFW提供多种输入。虽然有些只能轮询,如时间,或只能通过回调接收,如滚动,但许多同时提供回调和轮询。回调要比轮询做更多的工作,但CPU密集程度较低,并保证不会错过状态更改

所有输入回调都接收一个窗口句柄。通过使用窗口用户指针,您可以从回调中访问非全局结构或对象。

事件处理

GLFW需要轮询窗口系统的事件,以便向应用程序提供输入,并向窗口系统证明应用程序没有锁定。事件处理通常在缓冲区交换后的每一帧完成。即使没有窗口,也需要进行事件轮询,以便接收监视器和操纵杆连接事件

有三个函数用于处理挂起事件

glfwPollEvents,只处理那些已经接收到的事件,然后立即返回。

如果只需要在接收新输入时更新窗口的内容,那么glfwWaitEvents是更好的选择

它将线程置于睡眠状态,直到至少接收到一个事件,然后处理所有接收到的事件。这节省了大量的CPU周期,并且对于编辑工具等非常有用。

如果你想等待事件,但有UI元素或其他需要定期更新的任务,glfwWaitEventsTimeout允许你指定一个超时。

1
glfwWaitEventsTimeout(0.7);

如果主线程在glfwWaitEvents中休眠,可以通过使用glfwPostEmptyEvent向事件队列发送一个空事件来从另一个线程唤醒它

不要假设回调只会在响应上述函数时被调用。虽然有必要以上述一种或多种方式处理事件,但需要GLFW注册其自身回调的窗口系统可以将事件传递给GLFW以响应许多窗口系统函数调用。GLFW将在返回之前将这些事件传递给应用程序回调

键盘输入

GLFW将键盘输入分为两类;关键事件和角色事件。键事件与实际的物理键盘键有关,而字符事件与按下其中一些键产生的文本有关

1
2
3
4
5
6
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods)
{
if (key == GLFW_KEY_E && action == GLFW_PRESS)
activate_airship();
}
glfwSetKeyCallback(window, key_callback);

GLFW支持由操作系统文本输入系统生成的Unicode码点流形式的文本输入。与按键输入不同,文本输入受键盘布局和修改键的影响

1
2
3
4
void character_callback(GLFWwindow* window, unsigned int codepoint)
{
}
glfwSetCharCallback(window, character_callback);

鼠标输入

鼠标输入有多种形式,包括鼠标移动、按钮按压和滚动偏移。还可以更改光标的外观,将其更改为自定义图像或来自系统主题的标准光标形状

1
2
3
double xpos, ypos;
glfwGetCursorPos(window, &xpos, &ypos);
glfwSetCursorPosCallback(window, cursor_position_callback);

回调函数接收光标位置,以屏幕坐标测量,但相对于窗口内容区域的左上角。。

如果希望实现基于鼠标运动的相机控制或其他需要无限制鼠标移动的输入方案,请将光标模式设置为GLFW_CURSOR_DISABLED.

1
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);

这将隐藏光标并将其锁定到指定的窗口。然后,GLFW将处理光标重新居中和偏移计算的所有细节,并为应用程序提供虚拟光标位置。这个虚拟位置通常通过回调和轮询提供。原始鼠标运动更接近鼠标在表面上的实际运动。它不受应用于桌面光标运动的缩放和加速的影响。这种处理适合于光标,而原始运动更适合于控制,例如3D相机。因此,仅在禁用光标时才提供原始鼠标运动

1
2
if (glfwRawMouseMotionSupported())
glfwSetInputMode(window, GLFW_RAW_MOUSE_MOTION, GLFW_TRUE);

使用glfwCreateCursor创建自定义鼠标,它返回创建的鼠标对象的句柄

1
2
3
4
5
6
7
8
9
unsigned char pixels[16 * 16 * 4];
memset(pixels, 0xff, sizeof(pixels));

GLFWimage image;
image.width = 16;
image.height = 16;
image.pixels = pixels;

GLFWcursor* cursor = glfwCreateCursor(&image, 0, 0);

可以使用glfwCreateStandardCursor创建当前系统游标主题中具有标准形状的游标

1
GLFWcursor* url_cursor = glfwCreateStandardCursor(GLFW_POINTING_HAND_CURSOR);

如果希望在光标进入或离开窗口的内容区域时得到通知设置光标进入/离开回调

1
2
3
4
5
6
7
8
9
10
11
12
void cursor_enter_callback(GLFWwindow* window, int entered)
{
if (entered)
{
// The cursor entered the content area of the window
}
else
{
// The cursor left the content area of the window
}
}
glfwSetCursorEnterCallback(window, cursor_enter_callback);

可以查询鼠标当前是否在具有glfw_hoved窗口属性的窗口的内容区域内

1
2
3
4
if (glfwGetWindowAttrib(window, GLFW_HOVERED))
{
highlight_interface();
}

鼠标按钮输入

如果希望在鼠标按钮被按下或释放时收到通知设置鼠标按钮回调

1
glfwSetMouseButtonCallback(window, mouse_button_callback);

回调函数接收鼠标按钮、按钮动作和修饰符位

1
2
3
4
5
void mouse_button_callback(GLFWwindow* window, int button, int action, int mods)
{
if (button == GLFW_MOUSE_BUTTON_RIGHT && action == GLFW_PRESS)
popup_menu();
}

每个受支持的鼠标按钮的最后状态也保存在每个窗口状态数组中,可以使用glfwGetMouseButton轮询。

1
2
3
4
5
int state = glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_LEFT);
if (state == GLFW_PRESS)
{
upgrade_cow();
}

滚动

1
2
3
4
glfwSetScrollCallback(window, scroll_callback);
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
}

普通的鼠标滚轮是垂直的,它提供沿y轴的偏移量

参考资料

1.如何在windows上去掉启动时出现的控制台(使用vs或cmake)c++ - Removing console window for Glut/FreeGlut/GLFW? - Stack OverflowHow do I remove the console window in a C++ application in Visual Studio? - Stack Overflow 但是似乎只对cl.exe也就是MSVC管用,无法跨平台了,只能针对不同平台分别编译,gcc/clang(但测试了貌似不管用)可以使用-mwindowsEliminate shell window? - support - GLFW

在vs上在配置属性,链接器,系统上修改subsystem以及高级中的entry point

设置应用为窗口应用,由于它默认需要wmain函数而不是main函数,还需要修改入口函数为main/ENTRY(入口点符号) | Microsoft Learn.

image-20241006151255023

image-20241006151328843

在cmake上类似,但需要使用clang-cl.exe(而不是clang.exe),msvc作为generator(不能是其他的).

1
2
3
4
5
if(CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows" AND CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
target_link_options(learn_gl PRIVATE /SUBSYSTEM:WINDOWS /ENTRY:mainCRTStartup)
endif()
# 或者使用set_target_properties(exe_name PROPERTIES
LINK_FLAGS "/ENTRY:mainCRTStartup /SUBSYSTEM:WINDOWS")

或者在源文件中添加如下c++ - Replacing WinMain() with main() function in Win32 programs - Stack Overflow

1
2
3
#ifdef _MSC_VER
# pragma comment(linker, "/subsystem:windows /ENTRY:mainCRTStartup")
#endif

一个问题是使用了vs作为generator,目前无法生成clangd的compile_commands.json了CMAKE_EXPORT_COMPILE_COMMANDS — CMake 3.30.4 Documentation,那就不使用cland使用微软的c++工具用于代码搜索、跳转.

C++轻量级跨平台桌面GUI库FLTK的简单使用 - 知乎 (zhihu.com)介绍了一些现有的跨平台桌面库

-------------本文结束感谢您的阅读-------------
感谢阅读.

欢迎关注我的其它发布渠道