非模态对话框窗口的良好设计是什么?

What is a good design for modeless dialog windows?

本文关键字:是什么 模态 对话框 窗口      更新时间:2023-10-16

我目前正在制作一款c++游戏。我有一个主循环,在此期间计算逻辑,然后绘制精灵等我想实现"对话窗口":当你在NPC面前按下按键时,屏幕底部会弹出一个对话窗口,你被冻结,直到你按下按键,但游戏仍在继续运行(其他角色正在移动等),所以主循环仍在运行。我设法做得很好:当一个对象被激活时,它会向一个对话管理器发送一条消息,在游戏继续运行时显示文本窗口,所以它看起来像:

object::OnActivated() {
GameManager::doWindow("some text");
//the game is not blocked here, the game continues to run normally and will display a window on the next frame
}

现在问题来了,当我想在对话结束后发生一些关联动作,或者例如,如果你对一个问题选择"是"。我想有一个这样的实现:

object::OnActivated() {
GameManager::doWindow("some text");
if(GameManager::Accepted()) addGold(100);
}

问题是这个检查&操作将在创建窗口事件时立即执行,而不是在窗口关闭/接受时执行。是否有任何方法可以做到这一点,同时保持在OnActivated()函数中的相关操作?我不知道如何在不使用函数指针的情况下正确地做到这一点,这将迫使我对每个可以使用的方法都有特定的签名。由于

我发了一个赏金,因为我想知道这个问题最"规范"的答案是什么。我想这是一个非常普遍的问题(对于许多应用程序和所有现代游戏来说),我希望有一个尽可能灵活的解决方案,因为我今天无法列出对话可能引发的所有可能的"后果"。更多信息:-每个对话框将由一个公共"实体"类派生的对象触发来自同一类的不同对象几乎总是会有不同的对话/动作(例如,所有npc对象不会有相同的对话)-我不关心将"对话逻辑"移出OnActivated方法,甚至移出Entity类。因为我希望能够为每个NPC添加"随机"对话场景,所以对话等将被存储在其他地方-但我想保持对话框逻辑本身尽可能接近一个单一的对话框。理想情况下,我希望能够做这样的事情:"结果= dialogWindow("问题?");If (result){…}"。我不确定这是否可行

很难给出一个具体的答案,因为您没有指定(或标记)这是针对的平台,所以我将写一个通用的答案。

你问题的答案:

"是否有办法做到这一点,同时保持在OnActivated()函数的相关操作?"

最可能是"No"

对于你所描述的问题,有一系列行之有效的模式可以解决。这一系列模式是各种模型-视图- xxx模式(MVC、MVP、文档-视图等)。这些模式的基本前提是有一个构造,通常是一个对象图,它封装了系统的当前状态(模型)和一组向用户显示此状态的用户界面元素(视图)。当模型改变时,视图也随之改变以匹配新的状态。模型如何更改和视图如何更新的细节将家族中的不同模式区分开来,使用哪一种模式取决于如何处理特定系统的输入的细节。MVC非常适合互联网应用程序和许多基于循环的游戏,因为用户输入只有一个进入系统的入口点。MVP, DV和MVVM(有些人说与MVP相同)更适合桌面应用程序,在桌面应用程序中,输入将进入GUI中的活动控件。

使用这些模式的缺点是,用于创建视图的代码很少后跟用于相关操作的代码,但是它的好处远远大于这个缺点。

在您的例子中,您的模型应该有一个对话框文本的属性和一个存储当前输入处理程序的属性(状态模式)。主循环将执行以下操作:
  1. 获取当前输入处理程序以基于用户输入更新模型,如果有的话(例如改变用户精灵的位置)。
  2. 更新模型的其余部分以反映游戏中的其他元素。
  3. 基于当前模型更新UI

当用户在NPC前按压时,默认输入处理程序将改变为处理触发的特定对话框的输入,并且对话框的通用视图将文本显示给用户。

当用户选择对话框中的动作时,处理程序恢复到默认输入处理程序,并且对话框的属性返回为空。

步骤1和步骤2构成MVC模式中的控制器,步骤3是非事件驱动的视图更新;相反,您可以使用Observable-Observer模式,并让模型抛出由视图观察到的事件,这些事件会相应地发生变化。

您可以创建一个事件类,它根据您的需要执行一些预定义的操作。它将有一个实例变量,该实例变量保存枚举值,如EVENT_ADD_GOLD。它还有一个Perform函数,用于检查实例变量并执行适当的操作。可以根据需要添加其他操作。

这样做的好处是每个类型只需要一个实例变量。例如,value可能指的是金币或伤害的数量。含义由事件类型决定。

在说"我们不再需要显示对话框了!"的代码中,您可以调用Event对象的Perform方法。因为在任何时候为这个目的使用多个事件是没有意义的,所以您可以只使用一个实例变量来保存引用。

命令模式用于封装稍后执行的代码。

在您的情况下,object::OnActivated()函数将创建一个适当的命令对象并将其存储以供以后查找。当用户选择Yes/No时,命令可以在不需要代码知道特定命令对象恰好在那里的情况下运行。

下面是一个"add gold"命令对象的例子:

class DialogResponseCommand
{
public:
  virtual run() = 0;
};
class AddGoldCommand : public DialogResponseCommand
{
public:
  AddGoldCommand( int amount ) : amount(amount) {}
  virtual run()
  {
    if(GameManager::Accepted())
      addGold(amount);
  }
private:
  int amount;
};

现在,为即将到来的命令提供一些存储空间:

shared_ptr<DialogResponseCommand> dialog_command;

您可以让您的OnActivated()创建命令:

object::OnActivated() {
  GameManager::doWindow("some text");
  dialog_command = make_shared<AddGoldCommand>(100);
}

当用户最终做出选择时:

dialog_command->run();

我想你这里缺少的是回调。这个问题的解决方案是在onActivated方法中提供一个回调。当非模态对话框被接受时,对话管理器将调用这个回调函数或方法,这就是您可以执行所需行为的地方。

你没有提供足够的游戏细节,所以我不能给你一个明确的解决方法。如果对于任何给定的对象,您总是想要相同的操作,那么您可以简单地提供一个方法OnAccepted。像这样:

object::OnActivated() {
    GameManager::doWindow(this, "some text"); // note I'm passing the object to the dialog manager
}
// the dialog manager calls this when the dialog box is accepted
void object::OnAccepted() {
    addGold(100);
}

以上假设表示对象的所有类都属于同一层次结构,因此可以在基类中将OnAccepted方法声明为虚函数。

如果这是一个过于简单的方法,你可以使它更详细,但它总是有一些回调数据传递到doWindow方法,对话管理器可以使用它在适当的时间触发回调。

如果你需要一些非常复杂的东西,并且可以访问boost或具有std::functionstd::bind的c++11实现,那么你甚至可以支持任意参数的回调。传递给doWindow的参数是一个function对象。对象可以将常规函数或方法包装在某个对象中,如果有额外的参数,则可以使用std::bind将它们绑定到函数对象中。

每当您处理窗口和小部件时,使用MVC,演示器优先或任何替代方法总是好的。

现在,为了对某些"事件"做出反应,你可以使用回调,或者一种更好的观察者方式(看看boost的信号/槽)。