关于C ++:在QScrollArea视口上安装事件过滤器

Installing an event filter on a QScrollArea viewport

我在属于顶级QWidgetQScrollArea中有一个自定义QWidget。使用Qt Designer创建布局。我想拦截鼠标移动或悬停事件,这两个事件均未出现在自定义小部件中,这显然是由于将其放置在QScrollArea中。我知道解决方案是在QScrollArea::viewport()上安装事件过滤器。问题是关于解决方案的体系结构以及有关以下所述问题的对象之间的连接。

当发生鼠标事件并被安装在QScrollArea视口上的事件过滤器拦截时,我需要调用QScrollArea::mapFromGlobal()以获得相对于自定义窗口小部件的事件坐标。但是,自定义窗口小部件对滚动区域或事件过滤器一无所知。因此,以下架构正确:

  • 顶层窗口小部件实例化自定义窗口小部件和滚动区域(让我们忘记现在是通过Qt Designer完成的,我们需要Aggregation-composition-etc等生命周期管理方面),将窗口小部件添加到滚动区域的布局中,然后实例化事件过滤器并将其设置到自定义窗口小部件上。

  • 每当自定义小部件中拦截到鼠标事件时,事件过滤器就会发出带有全局鼠标事件位置的信号。

  • 顶层窗口小部件对信号做出反应并调用QScrollArea::mapFromGlobal()

  • 然后,顶层小部件在自定义小部件中调用适当的方法,即handleMouseHOver()

  • 这样,顶级窗口小部件就是实体之间的中介者。另一种方法如下:

  • 与上面的1相同。

  • 对事件过滤器进行编程以了解滚动区域。

  • 每当事件过滤器在自定义窗口小部件中拦截鼠标事件时,它就会调用QScrollArea::mapFromGlobal()并发出带有全局鼠标事件位置的信号。

  • 定制窗口小部件订阅该信号并做出相应的反应。

  • 这样,顶级窗口小部件仅实例化实体,并让它们自己处理业务。

    编辑:现在,我已经了解了另一种方法,其中顶级小部件重新实现QObject::eventFilter(),然后将自身作为事件过滤器安装到目标小部件:someWidget->installEventFilter(this);。从架构的角度来看,这有多正确?这样,顶层窗口小部件至少要承担两个责任。将事件过滤器代码分解为一个单独的类是否更好?

    我注意到,很难谈论Qt的体系结构,因为信号和插槽违反了接口的概念,因此"针对接口编程"规则几乎无效。任何东西都可以连接到它想要的地方。仍然,以上问题至少具有可能的实体布局,甚至可能还有更多。

    我的方法是否正确,并且在任何方面都与使用QWidgets和C ++在Qt5中应该采取的方法类似吗?


    鼠标追踪救援

    好消息是:不需要显式的事件管理。在子窗口小部件上启用鼠标跟踪后,即使存在中间QScrollArea,Qt也会将相关事件传递给它。以机智:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    // https://github.com/KubaO/stackoverflown/tree/master/questions/scrollarea-filter-40605540
    #include <QtWidgets>

    class Tracker : public QFrame {
       QPoint pos;
       void invalidatePos() { pos.setX(-1); }
       bool isPosInvalid() const { return pos.x() < 0; }
       void mouseMoveEvent(QMouseEvent *event) override {
          pos = event->pos();
          update();
       }
       void paintEvent(QPaintEvent *event) override {
          QFrame::paintEvent(event);
          if (isPosInvalid()) return;
          QPainter p{this};
          p.setPen(Qt::red);
          p.setBrush(Qt::red);
          p.drawEllipse(pos, 4, 4);
       }
       void leaveEvent(QEvent *event) {
          invalidatePos();
          update();
          QFrame::leaveEvent(event);
       }
    public:
       Tracker(QWidget * parent = nullptr) : QFrame{parent} {
          setFrameStyle(QFrame::Panel);
          setLineWidth(2);
          setMouseTracking(true);
       }
    };

    class TopWidget : public QWidget {
       QVBoxLayout m_layout{this};
       QScrollArea m_area;
       QWidget m_child;
       Tracker m_tracker{&m_child};
    public:
       TopWidget(QWidget * parent = nullptr) : QWidget{parent} {
          m_layout.addWidget(&m_area);
          m_area.setWidget(&m_child);
          m_child.setMinimumSize(1024, 1024);
          m_tracker.setGeometry(150, 150, 300, 300);
       }
    };

    int main(int argc, char ** argv) {
       QApplication app{argc, argv};
       TopWidget ui;
       ui.show();
       return app.exec();
    }

    除了信号和插槽

    首先,信号和插槽肯定提供接口:它们是接口的本质,因为它们提供了减少代码耦合的一种方法。"任何事物都可以连接到所需的位置"的观察仅部分正确:只有在信号或插槽是接口的一部分时,它才是正确的。

    例如,假设您有一个用户界面小部件显示坐标。确实可以随意连接各个子控件的接口,但是这些控件是封装的,您当然不能以CoordinateDialog的用户身份直接连接到它们-除非您使用自省绕过封装,否则:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    class CoordinateDialog : public QDialog {
       Q_OBJECT
       Q_PROPERTY(QVector3D value READ value WRITE setValue NOTIFY coordinatesChanged)
       QVector3D m_value;
       QFormLayout m_layout{this};
       QDoubleSpinBox m_x, m_y, m_z;
       QDialogButtonBox m_buttons;
    public:
       CoordinateDialog(QWidget *parent = nullptr) : CoordinateDialog(QVector3D(), parent) {}
       CoordinateDialog(const QVector3D &value, QWidget *parent = nullptr) :
          QDialog{parent}, m_value(value)
       {
          m_layout.addRow("X", &m_x);
          m_layout.addRow("Y", &m_y);
          m_layout.addRow("Z", &m_z);
          m_layout.addRow(&m_buttons);
          m_buttons.addButton(QDialogButtonBox::Ok);
          m_buttons.addButton(QDialogButtonBox::Cancel);
          connect(&m_buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
          connect(&m_buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
          connect(&m_x, static_cast<void (QDoubleSpinBox::*)(double)>(&QDoubleSpinBox::valueChanged),
                  [=](double x){ auto v = m_value; v.setX(x); setValue(v); });
          connect(&m_y, static_cast<void (QDoubleSpinBox::*)(double)>(&QDoubleSpinBox::valueChanged),
                  [=](double y){ auto v = m_value; v.setY(y); setValue(v); });
          connect(&m_z, static_cast<void (QDoubleSpinBox::*)(double)>(&QDoubleSpinBox::valueChanged),
                  [=](double z){ auto v = m_value; v.setZ(z); setValue(v); });
       }
       Q_SIGNAL void coordinatesChanged(const QVector3D &);
       Q_SIGNAL void coordinatesAccepted(const QVector3D &);
       void accept() override {
          emit coordinatesAccepted(m_value);
          QDialog::accept();
       }
       QVector3D value() const { return m_value; }
       Q_SLOT void setValue(const QVector3D &value) {
          if (m_value == value) return;
          m_value = value;
          m_x.setValue(m_value.x());
          m_y.setValue(m_value.y());
          m_z.setValue(m_value.z());
          emit coordinatesChanged(m_value);
       }
    };

    作为此类的用户,您的接口是QDialog以及CoordinateDialog添加的方法(包括信号和插槽)的接口。即使对话框上有按钮,&QPushButton::clicked信号也不在界面中,并且它们肯定会发出这种信号。