我在属于顶级QWidget
的QScrollArea
中有一个自定义QWidget
。布局是使用Qt Designer创建的。我想拦截鼠标移动或悬停事件,这两个事件都没有出现在自定义小部件中,显然是因为它被放置在QScrollArea
中。我知道解决方案是在QScrollArea::viewport()
上安装一个事件过滤器。这个问题是关于解决方案的体系结构和下面描述的问题的对象之间的连接。
当鼠标事件发生并被安装在QScrollArea
的视口上的事件过滤器拦截时,我需要调用QScrollArea::mapFromGlobal()
以获得相对于自定义小部件的事件坐标。但是,自定义小部件对滚动区域或事件过滤器一无所知。因此,下面的架构是否正确:
-
顶层小部件实例化自定义小部件和滚动区域(让我们忘记它是通过Qt设计器完成的,我们需要聚合组合等生命周期管理方面),将小部件添加到滚动区域的布局,然后实例化事件过滤器并将其设置到自定义小部件上。
-
每当在自定义小部件中截获鼠标事件时,事件过滤器会发出带有全局鼠标事件位置的信号。
-
顶层小部件响应信号并调用
QScrollArea::mapFromGlobal()
。 -
顶层小部件然后调用相应的方法,即自定义小部件中的
handleMouseHOver()
。
这样,顶层小部件就是实体之间的中介。另一种方法如下:
-
同上1
-
事件过滤器被编程为知道滚动区域
-
当事件过滤器在自定义小部件中截获鼠标事件时,调用
QScrollArea::mapFromGlobal()
并发出带有全局鼠标事件位置的信号。 -
自定义小部件订阅该信号并做出相应的反应。
这样,顶层小部件只实例化实体,并让它们自己处理业务。
编辑:现在我已经了解了另一种方法,其中顶级小部件重新实现QObject::eventFilter()
,然后将自己安装到目标小部件上作为事件过滤器:someWidget->installEventFilter(this);
。从架构的角度来看,这有多正确?这样,顶级小部件至少有两个职责。将事件过滤器代码分解到一个单独的类中不是更好吗?我注意到很难用Qt讨论体系结构,因为信号和槽无视接口的概念,因此"程序反对接口"的规则几乎无效。任何东西都可以连接到它想要的地方。尽管如此,上面的问题至少有两种可能的实体布局,甚至可能更多。
我的方法是正确的,它是否以任何方式类似于如何在Qt5与QWidgets和c++中完成?
鼠标跟踪救援
好消息是:不需要显式的事件管理。一旦在子小部件上启用鼠标跟踪,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();
}
Aside About Signals And Slots
首先,信号和插槽当然提供接口:它们是接口的本质,因为它们提供了中的方法来减少代码中的耦合。"任何东西都可以连接到它想要的地方"的观察结果只是部分正确的:只有当信号或插槽是接口的一部分时,它才是正确的。
例如,假设您有一个显示坐标的用户界面小部件。虽然单个子控件的接口确实可以随意连接,但这些控件是封装的,您当然不能作为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
信号不在界面中,即使在对话框上有按钮,它们也肯定会发出这样的信号。