事件调度和处理

这个 pyglet.event 模块提供了统一调度和处理事件的框架。就我们的目的而言,“事件分派器”是具有需要通知其他对象的事件的对象,而“事件处理程序”是可以注册(或“附加”)以接收这些事件的代码(函数或方法)。

事件调度程序是通过将 EventDispatcher 基类。Pyglet的许多内置模块,例如 pyglet.windowpyglet.mediapyglet.apppyglet.textpyglet.inputpyglet.gui 而其他一些人则利用了这种模式。您也可以很容易地在自己的类中重用它。

即使处理程序也只是编写为接受与调度的事件相同的参数的函数或方法。事件处理程序可以在运行时注册或取消注册。可以注册多个处理程序以接收相同的事件,这将在以下各节中介绍。事件调度器可以 _optionally_ 为它们的某些事件设置默认处理程序。您自己的处理程序可以完全替换它们,也可以直接添加。

设置事件处理程序

例如,让我们看一下 Window 班级。 Window 子类 EventDispatcher 而且,作为一个窗口,它有各种不同的事件可供调度。例如, pyglet.window.Window.on_resize() 事件。每次调整可调整大小的窗口(以及第一次创建窗口时)时,都会使用两个参数调度此事件: (width, height) 。因此,应该编写此事件的事件处理程序以接受这两个值。例如::

def on_resize(width, height):
    pass

可以通过几种不同的方式附加事件处理程序来接收它们。最简单的方法是将事件处理程序直接附加到对象上的相应属性。这将完全替换默认的事件处理程序::

window = pyglet.window.Window()

def on_resize(width, height):
    # Set some custom projection

window.on_resize = on_resize

有时需要替换默认处理程序,但并非在所有情况下都是如此。例如,默认情况下 Window.on_resize 处理程序负责设置用于绘制图形的正交2D投影。如果您完全更换它,您还必须自己处理设置投影。

替换默认事件处理程序的另一种方法是当子类化pyglet对象时。这是窗口类的常见用法,如 子类化窗口 。如果您的方法与默认事件具有相同的名称,则它们将被替换::

class MyWindow(pyglet.window.Window):
    def on_resize(self, width, height):
        # set a custom projection there

当然,您仍然可以使用以下命令调用默认处理程序 super() ,然后在此之前/之后添加自定义代码::

class MyWindow(pyglet.window.Window):

    def on_resize(self, width, height):
        super().on_resize(width, height)
        # do something else

活动装饰者

您还可以只添加一个额外的处理程序,而不是替换默认处理程序。Piglet提供了一个使用 event 装饰师。您的自定义事件处理程序将运行,后跟默认事件处理程序::

window = window.Window()

@window.event
def on_resize(width, height):
    print(f"Window was resized to: {width}x{height}")

或者,如果您的处理程序具有不同的名称,请将事件名称传递给修饰符::

@window.event('on_resize')
def my_resize_handler(width, height):
    pass

在大多数简单的情况下, event 装修是最方便的。然而,使用修饰符的一个限制是您只能添加一个额外的事件处理程序。如果您想要添加多个额外的事件处理程序,下一节将介绍如何完成该操作。

堆叠事件处理程序

为一个事件附加多个事件处理程序通常比较方便。 EventDispatcher 允许您将事件处理程序堆叠在一起,而不是直接替换它们。该事件将从堆栈的顶部传播到底部,但可以由沿途的任何处理程序通过返回 pyglet.event.EVENT_HANDLED

若要将事件处理程序推送到堆栈上,请使用 push_handlers() 方法:

def on_key_press(symbol, modifiers):
    if symbol == key.SPACE:
        fire_laser()

window.push_handlers(on_key_press)

推送处理程序而不是设置处理程序的一个用途是处理不同函数中的事件的不同参数化。在上面的例子中,如果按下空格键,激光将被发射。事件处理程序返回后,控制权将传递给堆栈上的下一个处理程序,该处理程序在 Window 是一个检查Esc键并将 has_exit 属性(如果按下)。通过推送事件处理程序而不是设置它,应用程序在添加附加功能的同时保留默认行为。

您可以通过返回True值来阻止堆栈中的其余事件处理程序接收事件。将以下事件处理程序按到窗口上时,将阻止退出键退出程序:

def on_key_press(symbol, modifiers):
    if symbol == key.ESCAPE:
        return True

window.push_handlers(on_key_press)

一次可以推送多个事件处理程序,这在与 pop_handlers() 功能。在下面的示例中,当游戏开始时,一些额外的事件处理程序被推送到堆栈上。当游戏结束时(可能返回到某个菜单屏幕),一下子弹出处理程序::

def start_game():
    def on_key_press(symbol, modifiers):
        print('Key pressed in game')
        return True

    def on_mouse_press(x, y, button, modifiers):
        print('Mouse button pressed in game')
        return True

    window.push_handlers(on_key_press, on_mouse_press)

def end_game():
    window.pop_handlers()

请注意,您没有指定从堆栈中弹出哪些处理程序--整个顶级“级别”(由在单个调用中指定的所有处理程序组成 push_handlers() )被弹出。

您可以通过将相关的事件处理程序分组到单个类中,以面向对象的方式应用相同的模式。在下面的示例中, GameEventHandler 类已定义。可以将该类的实例推入窗口或从窗口弹出::

class GameEventHandler:
    def on_key_press(self, symbol, modifiers):
        print('Key pressed in game')
        return True

    def on_mouse_press(self, x, y, button, modifiers):
        print('Mouse button pressed in game')
        return True

game_handlers = GameEventHandler()

def start_game()
    window.push_handlers(game_handlers)

def stop_game()
    window.pop_handlers()

备注

为了防止垃圾回收问题, EventDispatcher 类只包含对推送事件处理程序的弱引用。这意味着下面的示例将不起作用,因为推送的对象将落入作用域并被收集:

dispatcher.push_handlers(MyHandlerClass())

相反,您必须确保在推送对象之前保留对该对象的引用。例如::

my_handler_instance = MyHandlerClass()
dispatcher.push_handlers(my_handler_instance)

创建您自己的事件分派器

pyglet提供了 WindowPlayer ,以及其他事件调度器,但公开了用于创建和调度您自己的事件的公共接口。

创建事件调度程序的步骤如下:

  1. 子类 EventDispatcher

  2. 调用 register_event_type() 类方法,用于子类将识别的每个事件。

  3. 打电话 dispatch_event() 根据需要创建和调度事件。

在下面的示例中,一个假想的图形用户界面小部件提供了几个事件:

class ClankingWidget(pyglet.event.EventDispatcher):
    def clank(self):
        self.dispatch_event('on_clank')

    def click(self, clicks):
        self.dispatch_event('on_clicked', clicks)

    def on_clank(self):
        print('Default clank handler.')

ClankingWidget.register_event_type('on_clank')
ClankingWidget.register_event_type('on_clicked')

然后可以按照前面几节中的说明附加事件处理程序:

widget = ClankingWidget()

@widget.event
def on_clank():
    pass

@widget.event
def on_clicked(clicks):
    pass

def override_on_clicked(clicks):
    pass

widget.push_handlers(on_clicked=override_on_clicked)

这个 EventDispatcher 负责将事件传播到所有附加的处理程序,如果没有该事件的处理程序,则忽略该事件。

没有附加事件处理程序的对象的实例开销为零(仅在需要时创建事件堆栈)。这使得 EventDispatcher 即使在可能不总是有手柄的轻便物体上也适用。例如, Player 是一种 EventDispatcher 即使可能每秒创建和销毁数百个这样的对象,并且大多数不需要事件处理程序。

实现观察者模式

观察者设计模式,也称为发布者/订阅者,是一种分离软件组件的简单方法。它在许多大型软件项目中被广泛使用;例如,Java的AWT和Swing图形用户界面工具包以及 logging 模块;并且是任何模型-视图-控制器体系结构的基础。

EventDispatcher 可用于轻松地将可观察的组件添加到应用程序。下面的示例重新创建 ClockTimer 示例来自 Design Patterns (300-301页),但不需要笨重的 AttachDetachNotify 方法:

# The subject
class ClockTimer(pyglet.event.EventDispatcher):
    def tick(self):
        self.dispatch_event('on_update')

ClockTimer.register_event_type('on_update')

# Abstract observer class
class Observer:
    def __init__(self, subject):
        subject.push_handlers(self)

# Concrete observer
class DigitalClock(Observer):
    def on_update(self):
        pass

# Concrete observer
class AnalogClock(Observer):
    def on_update(self):
        pass

timer = ClockTimer()
digital_clock = DigitalClock(timer)
analog_clock = AnalogClock(timer)

每当计时器被“滴答”时,这两个时钟对象将被通知,尽管计时器和时钟都不需要事先知道对方。在对象构造过程中,可以创建主体和观察者之间的任何关系。