0

在 C++ 中的 Qt 项目中,我正在使用 QtPlugin 为动态加载的插件编写接口。这个界面应该允许插件注册它们的不同参数,并且在加载插件时主程序应该显示代表每个参数的适当的 GUI 控件。例如,参数可以是 0 到 20 a 之间的 int,由 QLabel 和 QSlider 表示,或者是由 QColorDialog 表示的颜色值。

这里有一个问题:我尝试了一种标准的 OOP 方法(?),让每个参数类型继承一个抽象类并通过实现一个虚函数来创建 GUI 表示。这导致大量 Qt GUI 头文件链接到每个插件文件,将其大小从 ~20 KB 增加到 ~50 KB。

这不是为了节省这些千字节,而是为了更好地理解 OOP。我考虑了这一点并试图找到合适的设计模式,然后我用谷歌搜索了“解耦多态”、“外部多态”等,发现一个页面说这是可能的,但通常你不想去那里,因为它坏了哎呀
是这样吗?要么我从插件界面隐藏 GUI 代码,并用枚举或其他东西识别每种类型,然后“打破 OOP”,还是这个类完全对自己负责,但在内部也完全耦合?

如果每种参数类型都由数据模型、持久性和带有信号的 GUI 控件组成,您会推荐哪些解决方案?什么去哪里?

编辑:换句话说,我想知道插件是否可以是纯数据和算法,不知道如何在 Qt 中创建数据控件并且独立于 Qt GUI 标头。它可能会使用 Q_OBJECT 作为信号。

4

3 回答 3

3

我建议让插件担心其参数的类型,并有一个单独的组件知道如何将每种类型映射到 GUI 控件上。

这几乎是一个直接的模型/视图分解,所以似乎是一个很好理解的习语。

现在,您的类型模型可以被枚举,或者您可以使用可以说是更多的 OO 访问者模式,但是您基本上仍然提前提出了一个固定且不可扩展的类型系统。这够吗?

您最终可能会得到某种类型,该类型既知道给定参数的特定派生类型,又知道如何在 Qt 中呈现它的详细信息。这将处理 Qt 信号,并将值传递回参数。


...通过尝试 dynamic_cast 或阅读某种识别代码(例如枚举),我在想。我仍然不明白如何使用访客 DP 来代替这些......

访问者模式专门用于避免dynamic_cast,所以我不确定这里有什么混淆。诚然,有一个事后版本确实使用dynamic_cast了 ,但它隐藏在实现中,无论如何都不是常见的情况。

因此,举一个具体的例子,让我们创建一个具有几个参数类型的模型:

struct ArgumentHandler; // visitor
class Argument { // base class for visitable concrete types
public:
    virtual void visit(ArgumentHandler&) = 0;
};
// sample concrete types
class IntegerArgument: public Argument {
    int value_;
public:
    IntegerArgument(int value = 0) : value_(value) {}

    void set(int v) { value_ = v; }
    int get() const { return value_; }

    virtual void visit(ArgumentHandler&);
};
class BoundedIntegerArgument: public IntegerArgument
{
    int min_, max_;
public:
    virtual void visit(ArgumentHandler&);
    // etc...
};

现在我们有了一些具体的类型供它访问,我们可以写抽象的访问者

struct ArgumentHandler {
    virtual ~ArgumentHandler() {}

    virtual void handleInteger(IntegerArgument&);
    virtual void handleBoundedInteger(BoundedIntegerArgument&);
    // ...
};

我们的具体类型像这样实现访问:

void IntegerArgument::visit(ArgumentHandler& handler) {
    hander.handleInteger(*this);
}

void BoundedIntegerArgument::visit(ArgumentHandler& handler) {
    hander.handleBoundedInteger(*this);
}

现在,我们可以只根据数据模型类型编写一个抽象插件——它不需要了解有关 GUI 工具包的任何信息。假设我们现在只提供一种查询其参数的方法(注意每个具体的子类型都应该有 set/get 方法)

class PluginBase
{
public:
    virtual int arg_count() const =  0;
    virtual Argument& arg(int n) =  0;
};

最后,我们可以绘制一个视图,它知道如何查询抽象插件的参数、如何显示每个具体的参数类型以及如何处理输入:

// concrete renderer
class QtView: public ArgumentHandler
{
    struct Control {};
    struct IntegerSpinBox: public Control {
        QSpinBox control_;
        IntegerArgument &model_;
    };
    struct IntegerSlider: public Control {
        QSlider control_;
        BoundedIntegerArgument &model_;
    };
    std::vector<std::unique_ptr<Control>> controls_;
public:
    // these overloads know how to render each argument type
    virtual void handleInteger(IntegerArgument &arg) {
        controls_.push_back(new IntegerSpinBox(arg));
    }
    virtual void handleBoundedInteger(BoundedIntegerArgument &arg) {
        controls_.push_back(new IntegerSlider(arg));
    }
    // and this is how we invoke them:
    explicit QtView(PluginBase &plugin) {
        for (int i=0; i < plugin.arg_count(); ++i) {
            plugin.arg(i).visit(*this);
        }
    }
};

我省略了所有的虚拟析构函数、Qt 信号处理等等。但是,希望您能看到一个QtView::IntegerSpinBox对象如何处理valueChanged来自其捕获的 spinbox 小部件的信号,并调用model_.set()将其推回插件。

于 2013-03-30T20:12:41.747 回答
1

您可以将任何类型的消息发送到任何地方,并在另一端使用任何带有模板的虚拟包来捕获它,这些包完全是为了与任何东西松散耦合而制作的。

于 2015-06-12T07:28:08.727 回答
0

如果我对您的理解正确,您应该重新考虑这种行为。您可以为模块特定的渲染器创建一个基类,并在每个模块中创建一个工厂,以实例化模块的具体渲染器,而不是让模块在主应用程序中注册所有内容(实际上可能很多)。然后,您可以要求模块呈现您提供给模块的信息。

于 2013-03-30T19:01:31.753 回答