扩展共同基础:Diamond继承与QObject

时间:2014-12-12 18:05:48

标签: c++ qt inheritance qwidget qt-signals

我想我在这里遇到了一种钻石继承问题。

Qt提供了几个旋转框,用于整数值,用于双精度数以及日期/时间。它们都来自QAbstractSpinBox

#include <QtWidgets/QSpinBox>
class QSpinBox:
    public QAbstractSpinBox {

};

#include <QtWidgets/QDoubleSpinBox>
class QDoubleSpinBox:
    public QAbstractSpinBox {

};

现在我想添加一些常用于所有旋转框的功能,在这个具体示例中,一个按钮可以将旋转框恢复到最小(因此是specialValueText)。 所以我也从QAbstractSpinBox派生出来并想出了类似的东西:

class AbstractRevertibleSpinBox:
    public QAbstractSpinBox {

    public:
        RevertibleSpinBox() {
            /* Use abstract base: */
            QAction *revertAction = new QAction(this);
            QAbstractSpinBox::lineEdit()->addAction(
                revertAction, QLineEdit::TrailingAction);
            /* ... */
        }

    public slots:
        virtual void revert()  = 0;
}

这包含应该实现如何恢复不同旋转框的纯revert()。例如,对setValue(double)使用QDoubleSpinBoxsetDate(QDate)使用QDateEdit。 然后我以明显的方式为我需要的所有旋转框派生了相应的类,如下所示:

class RevertibleSpinBox:
    public QSpinBox,
    public AbstractRevertibleSpinBox {

    protected:
        void revert() {
            /* Revert 'int' */
            setValue(0);
        }
};

class RevertibleDoubleSpinBox:
    public QDoubleSpinBox,
    public AbstractRevertibleSpinBox {

    protected:
        void revert() {
            /* Revert 'double' */
            setValue(0.0);
        }
};

这显然不起作用,因为QAbstractSpinBox中的任何内容现在都不明确。我以为我可以使用虚拟继承解决它,如果,例如, QDoubleSpinBox 虚拟从其自己的QAbstractSpinBox派生。但它并没有。此外,它会因QObject而失败,因为Qt似乎在那里做了很多static_cast个upwarts,这也不适用于虚拟继承。 我还考虑通过将AbstractRevertibleSpinBox模板类作为模板类参数传递给不同的旋转框类型来解决它。然后施工将如下所示:

template<class Base>
class AbstractRevertibleSpinBox:
    public Base {};

class RevertibleSpinBox:
    public AbstractRevertibleSpinBox<SpinBox> { };

这可行,但Qt​​的moc对模板类非常不满意。因此,例如,我无法连接模板类中的任何信号和插槽。至少不使用传统的基于字符串的SIGNAL()/SLOT()语法。

还有其他合理优雅的方法来克服这个问题吗?

4 个答案:

答案 0 :(得分:7)

正如我在评论中所提到的那样,如果你想要一个易于扩展的特征系统,我认为这是Decorator Pattern的一个明显的例子,否则只是继承QObject而不是从基础&#34;接口&# 34;几乎相同的代码。

我将从IMHO更糟糕的方法开始,在其他答案中提供:

  • 对每个旋转框进行子类化

这显然很无聊,甚至更重要的是,你将无法支持任何QSpinBox子类,因为你总是需要为每次添加创建一个新的子类。这只是一种不灵活的方法。

  • 拥有包含按钮和旋转框的父窗口小部件

这看起来像两个不同东西的不必要的耦合,因此如果您通过按钮之后以任何其他方式触发它们,您将无法轻松地重复使用旋转框。我认为这两个概念应该保持不同并单独管理。

此外,dynamic_cast错误的建议是错误的,因为您应该使用qobject_cast

让我们仔细看看装饰者的方法:

enter image description here

这还不是您的案例的解决方案,但它很好地展示了如何将功能添加到现有层次结构中(即#34;装饰&#34;)。为了更加具体地了解您的用例,让我们看看在您的特定场景中会出现什么:

  • 组件:QAbstractSpinBox

  • 具体组件

    • QSpinBox
    • QDoubleSpinBox
    • QDateTimeEdit
      • QDateEdit
      • QTimeEdit
  • 装饰者:AbstractSpinBoxDecorator(在你的情况下可以省略这一步)

  • Concrete Decorator:RevertibleSpinBoxDecorator

让我们亲自动手实施这个设计:

的main.cpp

#include <QAbstractSpinBox>
#include <QSpinBox>
#include <QDoubleSpinBox>
#include <QDateTimeEdit>
#include <QDateEdit>
#include <QTimeEdit>
#include <QPushButton>
#include <QApplication>
#include <QMainWindow>
#include <QHBoxLayout>
#include <QWidget>
#include <QShowEvent>

class RevertibleSpinBoxDecorator : public QAbstractSpinBox
{
    Q_OBJECT
public:
    explicit RevertibleSpinBoxDecorator(QAbstractSpinBox *abstractSpinBox, QAbstractSpinBox *parent = Q_NULLPTR)
        : QAbstractSpinBox(parent)
        , m_abstractSpinBox(abstractSpinBox)
    {
    }

public slots:
    void revert(bool)
    {
        QSpinBox *spinBox = qobject_cast<QSpinBox*>(m_abstractSpinBox);
        if (spinBox) {
            spinBox->setValue(spinBox->minimum());
            return;
        }

        QDoubleSpinBox *doubleSpinBox = qobject_cast<QDoubleSpinBox*>(m_abstractSpinBox);
        if (doubleSpinBox) {
            doubleSpinBox->setValue(doubleSpinBox->minimum());
            return;
        }

        QDateEdit *dateEdit = qobject_cast<QDateEdit*>(m_abstractSpinBox);
        if (dateEdit) {
            dateEdit->setDate(dateEdit->minimumDate());
            return;
        }

        QTimeEdit *timeEdit = qobject_cast<QTimeEdit*>(m_abstractSpinBox);
        if (timeEdit) {
            timeEdit->setTime(timeEdit->minimumTime());
            return;
        }

        QDateTimeEdit *dateTimeEdit = qobject_cast<QDateTimeEdit*>(m_abstractSpinBox);
        if (dateTimeEdit) {
            dateTimeEdit->setDateTime(dateTimeEdit->minimumDateTime());
            return;
        }

        Q_ASSERT_X(false, "decorator", "concrete component unimplemented");
    }

protected:
    void showEvent(QShowEvent *event) Q_DECL_OVERRIDE
    {
        m_abstractSpinBox->show();
        event->ignore();
        hide();
    }

private:
     QAbstractSpinBox *m_abstractSpinBox;
};

class MainWindow : public QMainWindow
{
    Q_OBJECT
    public:
        explicit MainWindow(QWidget *parent = Q_NULLPTR) : QMainWindow(parent)
        {
            connect(pushButton, &QPushButton::clicked, revertibleSpinBoxDecorator, &RevertibleSpinBoxDecorator::revert);
            QHBoxLayout *layout = new QHBoxLayout(centralWidget);
            layout->addWidget(revertibleSpinBoxDecorator);
            layout->addWidget(pushButton);
            setCentralWidget(centralWidget);
        }

    private:
        QWidget *centralWidget{new QWidget(this)};
        QDoubleSpinBox *doubleSpinBox{new QDoubleSpinBox(this)};
        RevertibleSpinBoxDecorator *revertibleSpinBoxDecorator{new RevertibleSpinBoxDecorator(doubleSpinBox)};
        QPushButton *pushButton{new QPushButton(this)};
};

#include "main.moc"

int main(int argc, char **argv)
{
    QApplication application(argc, argv);
    MainWindow mainWindow;
    mainWindow.show();
    return application.exec();
}

如果你想摆脱QAbstractSpinBox继承,你将需要使用更多的胶水和恕我直言,以获得不多的收益,同时失去灵活性。你会从这样的事情开始:

非装饰

#include <QAbstractSpinBox>
#include <QSpinBox>
#include <QDoubleSpinBox>
#include <QDateTimeEdit>
#include <QDateEdit>
#include <QTimeEdit>
#include <QPushButton>
#include <QApplication>
#include <QMainWindow>
#include <QHBoxLayout>
#include <QWidget>
#include <QShowEvent>

class RevertibleSpinBoxDecorator : public QObject
{
    Q_OBJECT
public:
    explicit RevertibleSpinBoxDecorator(QAbstractSpinBox *abstractSpinBox, QObject *parent = Q_NULLPTR)
        : QObject(parent)
        , m_abstractSpinBox(abstractSpinBox)
    {
    }

public slots:
    void revert(bool)
    {
        QSpinBox *spinBox = qobject_cast<QSpinBox*>(m_abstractSpinBox);
        if (spinBox) {
            spinBox->setValue(spinBox->minimum());
            return;
        }

        QDoubleSpinBox *doubleSpinBox = qobject_cast<QDoubleSpinBox*>(m_abstractSpinBox);
        if (doubleSpinBox) {
            doubleSpinBox->setValue(doubleSpinBox->minimum());
            return;
        }

        QDateEdit *dateEdit = qobject_cast<QDateEdit*>(m_abstractSpinBox);
        if (dateEdit) {
            dateEdit->setDate(dateEdit->minimumDate());
            return;
        }

        QTimeEdit *timeEdit = qobject_cast<QTimeEdit*>(m_abstractSpinBox);
        if (timeEdit) {
            timeEdit->setTime(timeEdit->minimumTime());
            return;
        }

        QDateTimeEdit *dateTimeEdit = qobject_cast<QDateTimeEdit*>(m_abstractSpinBox);
        if (dateTimeEdit) {
            dateTimeEdit->setDateTime(dateTimeEdit->minimumDateTime());
            return;
        }

        Q_ASSERT_X(false, "strategy", "strategy not implemented");
    }

private:
     QAbstractSpinBox *m_abstractSpinBox;
};

class MainWindow : public QMainWindow
{
    Q_OBJECT
    public:
        explicit MainWindow(QWidget *parent = Q_NULLPTR) : QMainWindow(parent)
        {
            connect(pushButton, &QPushButton::clicked, revertibleSpinBoxDecorator, &RevertibleSpinBoxDecorator::revert);
            QHBoxLayout *layout = new QHBoxLayout(centralWidget);
            layout->addWidget(doubleSpinBox);
            layout->addWidget(pushButton);
            setCentralWidget(centralWidget);
        }

    private:
        QWidget *centralWidget{new QWidget(this)};
        QDoubleSpinBox *doubleSpinBox{new QDoubleSpinBox(this)};
        RevertibleSpinBoxDecorator *revertibleSpinBoxDecorator{new RevertibleSpinBoxDecorator(doubleSpinBox)};
        QPushButton *pushButton{new QPushButton(this)};
};

#include "main.moc"

int main(int argc, char **argv)
{
    QApplication application(argc, argv);
    MainWindow mainWindow;
    mainWindow.show();
    return application.exec();
}

main.pro

TEMPLATE = app
TARGET = main
QT += widgets
CONIG += c++11
SOURCES += main.cpp

构建并运行

qmake && make && ./main

答案 1 :(得分:1)

我认为我根本不会使用继承;相反,我会使用合成:创建一个不相关的类(例如从QWidget派生),它使用QBoxLayout(或类似的)将旋转框和按钮排列为子窗口小部件。可以将适当的spinbox对象指针传递给类的构造函数,该构造函数也会执行必要的connect()命令来来回转发各种信号。当然,你可能想重新声明类中QAbstractSpinBox类的插槽/信号/方法的等价物,但好处是它适用于任何QAbstractSpinBox子类。

(至于让revert()做正确的事情,最简单的方法可能只是使用dynamic_cast&lt;&gt;的一些丑陋的特殊情况逻辑 - 因为你只需要支持三个QAbstractSpinBox子类,那将是可管理的,至少那种丑陋隐藏在私有方法体内,而不是暴露给类的用户)

答案 2 :(得分:1)

要考虑的一个选项可能是简单地创建从QSpinBoxQDoubleSpinBoxQDateEdit中派生的类,然后将公共代码提取到函数中。

QSpinBox为例:

class RevertibleSpinBox : public QSpinBox
{
public:
    RevertibleSpinBox(QWidget* parent) : QSpinBox(parent)
    {
        RevertibleSpinBoxHelpers::installRevertAction(lineEdit(), this);
    }

public slots:
    void revert()
    {
        setValue(0);
    }

// etc.
};

namespace RevertibleSpinBoxHelpers
{
    void installRevertAction(QLineEdit* target, QObject* handler)
    {
        QAction* revertAction = new QAction(handler);
        target->addAction(revertAction, QLineEdit::TrailingAction);
        QObject::connect(revertAction, SIGNAL(triggered()), handler, SLOT(revert()));
    }
}

免责声明:如果您的需求比您在问题中提到的要复杂得多,那么这可能不是最佳方法。

答案 3 :(得分:0)

IMO你不必要地使你的生活变得复杂。甚至装饰器模式都是矫枉过正,更不用说那些不必要的继承和设计问题了。您所需要的只是一个漂亮的辅助功能:

void revert(QAbstractSpinBox * box) {
    QSpinBox * spin = qobject_cast<QSpinBox *>(box);
    if (spin) { spin->setValue(spin->minimum()); return; }
    QDateTimeEdit * dt = qobject_cast<QDateTimeEdit *>(box);
    if (dt) { dt->setDateTime(dt->minimumDateTime()); return; }
    // and so on...
    qDebug() << "you should not be seeing this";
    return;
}

如果你想要在spinbox旁边实例化一些东西,那么将它包装为Reverter对象的静态方法。

如果你把它放在QObject派生类中,你可以把它作为一个插槽,但这真的不是必需的,因为在Qt中你可以连接到非自由函数和方法{{1派生类。或者,如果您愿意,可以直接从插槽中调用它:

QObject

这类似于装饰器模式,但它确实带来了一些优点:

  • 它只为继承层次结构的所有对象提供额外的功能, 这意味着你可以省去重新实现现有功能的工作,你可以直接使用传递的对象
  • 你只需要一个public slots: void revertSpinBoxA() { revert(spinBoxA); } void revertSpinBoxB() { revert(spinBoxB); } 来恢复任意数量的旋转框,装饰器需要n个装饰器来容纳n个旋转框,它会耗尽更多的内存,这可能不是致命的但是不必要开销,因为revert方法中的类型转换使得实际的装饰器完全冗余。与装饰器的不同之处在于,您将使用原始对象+每个装饰器作为对象的控制器,而在此解决方案中,您仍然使用实际对象,并仅将其传递给{{1}为了额外的功能。减少工作量,减少代码量,减少复杂性,减少使用的内存。