如何为不同的类提供 glfwSetKeyCallback

How to glfwSetKeyCallback for different classes?

本文关键字:glfwSetKeyCallback      更新时间:2023-10-16

我使用GLFW,我有不同的类代表我的应用程序中的不同状态和一个状态管理。 我想使用 GLFW 接收键输入并将其传递给当前状态(因此传递给另一个类)。

我能想到的唯一方法是给每个类一个自己的keycallback函数并使用 glfwSetKeyCallback(window, keyCallback);

keycallback(GLFWwindow *window, int key, int scancode, int action, int mods)

它不是那样工作的

cannot convert 'IntroState::handleKeyEvent' from type 'void (IntroState::)(GLFWwindow*, int, int, int, int)' to type 'GLFWkeyfun {aka void (*)(GLFWwindow*, int, int, int, int)}'
glfwSetKeyCallback(m_manager->window, handleKeyEvent);

人们推荐了这样的东西:

void GLFWCALL functionname( int key, int action );

但是 GLFWCALL-macro 已从 GLFW(官方说明)中删除。


我读过您可以使用静态函数。

static void keyCallback(GLFWwindow*, int, int, int);

但是我需要访问非静态成员函数,并在回调函数是静态时遇到一些问题。


实际问题:

如何使当前"状态"类从 GLFW 获取密钥输入? 喜欢

states.back()->handlekeyEvent(int, int, int);

当你退后一步,根本不在课堂上思考时,事情会变得容易得多。此外,类是一种类型,类型并不真正描述特定状态。我认为您实际上的意思是具有不同内部状态的类的实例

因此,您的问题就变成了将回调与您的状态相关联。在您的帖子中,您编写了以下不完整的回调签名:

keycallback(GLFWwindow *window, int key, int scancode, int action, int mods);

缺少一些东西,即它返回的类型。这是void,所以完整的签名是

void GLFWCALL keycallback(
GLFWwindow *window,
int key,
int scancode,
int action,
int mods);

其中GLFWCALL是一个未进一步指定的宏,它扩展到其他类型签名限定符。您不必再关心它,因为这是您可以作为回调有效传递的唯一类型签名。现在想象一下你会写一些类似的东西

class FooState
{
void GLFWCALL keycallback(
GLFWwindow *window,
int key,
int scancode,
int action,
int mods);  
};

这将是什么类型签名?好吧,当你实际实现它时,你把它写下来

void GLFWCALL FooState::keycallback(
GLFWwindow *window,
int key,
int scancode,
int action,
int mods);  

看看这个额外的FooState::.这不仅仅是对名称或命名空间的一些补充。它实际上是一个类型修饰符。这就像你向函数添加了另一个参数(这就是实际发生的情况),并且该参数是指向类实例的引用或指针(在 C++ 的情况下是指针)。此指针通常称为this。因此,如果你通过 C 编译器(它不知道类)的眼睛来看待这个函数,并且 GLFW 是用 C 编写的,那么签名实际上看起来像

void GLFWCALL keycallback(
FooState *this,
GLFWwindow *window,
int key,
int scancode,
int action,
int mods);  

很明显,这不符合GLFW的需求。所以你需要的是某种包装器,在那里得到这个额外的参数。您从哪里获得该附加参数?好吧,这将是您必须管理的程序中的另一个变量。

现在我们可以为此使用静态类成员。在类的范围内static是指由类的所有实例和类命名空间内的函数共享的全局变量,但不适用于它的实例(因此从 C 编译器的角度来看,它们只是常规函数,没有额外的参数)。

首先是一个基类,它为我们提供了一个统一的接口

// statebase.h
class StateBase
{
virtual void keycallback(
GLFWwindow *window,
int key,
int scancode,
int action,
int mods) = 0; /* purely abstract function */
static StateBase *event_handling_instance;
// technically setEventHandling should be finalized so that it doesn't
// get overwritten by a descendant class.
virtual void setEventHandling() { event_handling_instance = this; }
static void GLFWCALL keycallback_dispatch(
GLFWwindow *window,
int key,
int scancode,
int action,
int mods)
{
if(event_handling_instance)
event_handling_instance->keycallback(window,key,scancode,action,mods);
}    
};
// statebase.cpp
#include "statebase.h"
// funny thing that you have to omit `static` here. Learn about global scope
// type qualifiers to understand why.
StateBase * StateBase::event_handling_instance;

现在,纯抽象的虚函数是这里的诀窍:无论从StateBase派生的类是什么,event_handling_instance通过使用虚拟调度来引用它,它最终都会被该实例的类类型的函数实现调用。

所以我们现在可以声明FooState是从StateBase派生的,并像往常一样实现虚拟回调函数(但省略 GLFWCALL 宏)

class FooState : BaseState
{
virtual void keycallback(
GLFWwindow *window,
int key,
int scancode,
int action,
int mods);  
};
void FooState::keycallback(
GLFWwindow *window,
int key,
int scancode,
int action,
int mods)
{
/* ... */
}

要将其与 GLFW 一起使用,请将静态调度程序注册为回调

glfwSetKeyCallback(window, StateBase::keycallback_dispatcher);

要选择特定实例作为活动回调处理程序,您可以调用其继承的void StateBase::setEventHandling()函数。

FooState state;
state.setEventHandling();

几句临别忠告:

如果您不熟悉 OOP 的这些概念C++以及类型和实例交互的复杂方式,则还不应该进行任何 OOP 编程。OOP曾经声称可以让事情更容易理解,而事实上,它在C++中的精明实现方式实际上使人大脑受伤并扩散非常糟糕的风格。

这里有一个练习:尝试在不使用 OOP 的情况下实现您尝试实现的目标。只需使用没有成员函数的structs(这样它们就不会变相成为类),尝试思考平面数据结构以及如何使用它们。这是一项非常重要的技能,直接跳入OOP的人不会接受很多训练。

你需要这样做的东西被称为闭包(提供必要的上下文来引用非本地存储)。您无法在传统的 C 回调中访问类成员,因为无法满足 C++ 类函数的调用约定(即this指针的附加要求)。

大多数框架通过允许您提供一些直接传递给回调或可以在触发回调时查询的void *用户数据来解决此问题。您可以将此void *强制转换为所需的任何内容(在本例中为指向类对象的指针),然后调用属于该对象的成员函数。

顺便说一句,GLFWCALL真的不应该为这个讨论添加任何东西。这是一个宏,用于定义平台的默认调用约定。它永远不会帮助您在C++中调用成员函数,它的主要目的是简化 DLL 导入/导出之类的事情。

@andon-m-Coleman说的话

auto keyCallback = []( GLFWwindow* window, int key, int scancode, int action, int mods ) {
auto me = (Window*)glfwGetWindowUserPointer( window );
me->KeyCallback( window, key, scancode, action, mods );
};
glfwSetWindowUserPointer( window, this );
glfwSetKeyCallback( window, keyCallback );

为 GLFW 创建一个单独的控制器例程; 让它接收事件并将它们传递给当前位于状态堆栈之上的任何状态。(我通常直接在应用程序主循环中使用它。

我想出了一个将类粘附到 glfw 回调的解决方案。它相当灵活,但只允许您注册一次课程,而无法重新注册。

将对类的静态引用包装在函数中,作为静态函数变量。将引用作为参数,对其进行设置,然后返回 lambda 的指针。请注意,lambda 不需要捕获任何内容,因为我们的类引用是静态的,但只能在函数中访问:

void  (*build_framebuffer_callback(rendering::renderer& r))(GLFWwindow*, int, int)
{
static rendering::renderer& renderer = r;
void (*callback)(GLFWwindow*, int, int) = ([](GLFWwindow* window, int width, int height) {
renderer.resize(width, height);
});
return callback;
}

签名与以下 glfw 回调标头匹配:

void framebuffer_size_callback(GLFWwindow* window, int width, int height);

然后,您可以执行以下操作:

glfwSetFramebufferSizeCallback(window, build_framebuffer_callback(renderer));

以上是针对我的渲染器类的,但我认为这也适用于输入。然后,您将在输入类上创建一个函数,该函数允许 lambda 设置它需要设置的任何内容。