在QScrollArea视口上安装事件过滤器

时间:2016-11-15 08:53:20

标签: c++ qt oop architecture qt5

我在QWidget中有一个属于顶级QScrollArea的自定义QWidget。使用Qt Designer创建布局。我想拦截鼠标移动或悬停事件,这些事件都不会出现在自定义窗口小部件中,显然是因为它被放入QScrollArea。我知道解决方案是在QScrollArea::viewport()上安装一个事件过滤器。问题是关于解决方案的体系结构以及关于下述问题的对象之间的联系。

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

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

  2. 只要在自定义小部件中拦截了鼠标事件,事件过滤器就会发出一个带有全局鼠标事件位置的信号。

  3. 顶级窗口小部件对信号作出反应并调用QScrollArea::mapFromGlobal()

  4. 然后,顶级窗口小部件会在自定义窗口小部件中调用适当的方法,即handleMouseHOver()

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

    1. 与上述1相同。

    2. 事件过滤器被编程为了解滚动区域。

    3. 事件过滤器调用QScrollArea::mapFromGlobal()并在自定义小部件中拦截鼠标事件时发出带有全局鼠标事件位置的信号。

    4. 自定义窗口小部件订阅该信号并做出相应的反应。

    5. 这样,顶级窗口小部件只实例化实体,并让他们自己处理业务。

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

      我注意到用Qt谈论架构很难,因为信​​号和插槽违背了接口的概念,因此“针对接口的程序”规则几乎无效。任何东西都可以连接到它想要的地方。尽管如此,上述问题至少有可能的实体布局,甚至可能更多。

      我的方法是否正确,是否与使用QWidgets和C ++在Qt5中应该如何完成相似?

1 个答案:

答案 0 :(得分:1)

鼠标跟踪到救援

好消息是:不需要明确的事件管理。在子窗口小部件上启用鼠标跟踪后,Qt会将相关事件传递给它,即使存在干预QScrollArea。即:

// 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的用户那样直接连接到它们 - 除非你使用内省绕过封装:

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信号不在界面中,即使对话框上有按钮,它们肯定会发出这样的信号。