跳转到内容
新建笔记

Qt 拖动子控件:事件目标、布局与坐标系

拖动一个 QChartView 需要同时解决三件事:谁接收鼠标事件、谁控制这个子控件的位置,以及移动时使用哪个坐标系。只在父窗口里重写 mousePressEvent(),不能保证收到已被图表视口处理的鼠标事件。

先决定移动的是控件还是图表内容

跳转到“先决定移动的是控件还是图表内容”

fontChartView->move(x, y) 移动整个图表视图在父窗口中的位置;图表平移、坐标轴缩放和曲线数据更新则是图表内部操作。原需求是前一种,所以例子让图表视图作为宿主的普通子控件,采用手工几何布局。

如果控件仍由 QVBoxLayout 等布局管理,下一次布局计算可能覆盖手动位置。可以让整个画布区域参与外层布局,而让画布中的可拖动子控件由画布手工摆放;不必取消整个窗口的一切布局。移除已有布局管理时,注意 removeWidget() 不会删除或自动隐藏对象,详见 [[10-knowledge/computing/software-development/application-development/desktop/qt/gui/layout-ownership-and-removal|布局与控件移除]]。

QChartView 继承自图形视图体系,鼠标输入来自内部 viewport();继承它并重写专门的鼠标处理函数,是一种明确的接收路径。若采用外部过滤器,通常应装到实际输入视口,而不只是父窗口。Qt:视口事件处理

拖动过程中使用稳定的全局坐标

跳转到“拖动过程中使用稳定的全局坐标”

按下时记住鼠标相对于控件左上角的偏移。移动时,用鼠标全局位置减去这个偏移,得到控件左上角的全局位置,再通过父控件的 mapFromGlobal() 转回父坐标。

如果一边移动接收事件的控件,一边直接把该控件局部的 position() 当成稳定坐标,会形成反馈并可能抖动。Qt 6 使用 position()、globalPosition();移动事件的 button() 通常是 Qt::NoButton,当前是否仍按住左键要查 buttons()。仅实现按住拖动不需要启用鼠标跟踪;setMouseTracking(true) 用于没有按键时也接收移动。Qt:QMouseEvent

完整的 Qt Charts 拖动例子

跳转到“完整的 Qt Charts 拖动例子”

这一示例用于维护原有 Qt Charts 工程。当前官方在线文档已将 QChartView 标记为弃用;这里保留原工程的组件并验证其拖动逻辑,不把它作为新项目图表模块的选型结论。Qt:QChartView 状态

初始宿主尺寸仍为 660×280,图表为 (10, 10, 640, 260),保留隐藏图例、背景不绘制、圆角 0 和抗锯齿设置。这里让窗口可以继续放大,并在每次拖动时约束控件位置;只有宿主宽高均不小于图表时,才能完整容纳图表。在初始尺寸下,每个方向的可用移动范围只有 20 像素,这是几何约束的结果。

左键被明确用于拖动整个控件,因此这条输入路径不再执行图表的默认左键行为;其他按键仍交给 QChartView。若还需要橡皮筋缩放或曲线点击,应增加拖动手柄、修饰键或独立模式,而不是让两个行为抢同一组鼠标事件。

#include <QApplication>
#include <QChart>
#include <QChartView>
#include <QEvent>
#include <QLegend>
#include <QLineSeries>
#include <QMouseEvent>
#include <QPainter>
#include <QWidget>
#include <algorithm>
#include <cassert>
class DragChartView final : public QChartView {
public:
explicit DragChartView(QWidget *parent) : QChartView(new QChart, parent) {
auto *series = new QLineSeries;
series->append(0, 1);
series->append(1, 3);
series->append(2, 2);
chart()->addSeries(series);
chart()->createDefaultAxes();
chart()->legend()->hide();
chart()->setBackgroundRoundness(0);
chart()->setBackgroundVisible(false);
setRenderHint(QPainter::Antialiasing);
setGeometry(10, 10, 640, 260);
}
protected:
void mousePressEvent(QMouseEvent *event) override {
if (event->button() == Qt::LeftButton && parentWidget()) {
dragging = true;
offset = event->globalPosition().toPoint() - mapToGlobal(QPoint(0, 0));
event->accept();
return;
}
QChartView::mousePressEvent(event);
}
void mouseMoveEvent(QMouseEvent *event) override {
if (dragging && event->buttons().testFlag(Qt::LeftButton) && parentWidget()) {
QPoint position = parentWidget()->mapFromGlobal(
event->globalPosition().toPoint() - offset);
position.setX(std::clamp(position.x(), 0,
std::max(0, parentWidget()->width() - width())));
position.setY(std::clamp(position.y(), 0,
std::max(0, parentWidget()->height() - height())));
move(position);
event->accept();
return;
}
if (!event->buttons().testFlag(Qt::LeftButton)) dragging = false;
QChartView::mouseMoveEvent(event);
}
void mouseReleaseEvent(QMouseEvent *event) override {
if (dragging && event->button() == Qt::LeftButton) {
dragging = false;
event->accept();
return;
}
QChartView::mouseReleaseEvent(event);
}
bool event(QEvent *event) override {
if (event->type() == QEvent::Hide || event->type() == QEvent::WindowDeactivate
|| (event->type() == QEvent::EnabledChange && !isEnabled()))
dragging = false;
return QChartView::event(event);
}
bool viewportEvent(QEvent *event) override {
if (event->type() == QEvent::UngrabMouse) dragging = false;
return QChartView::viewportEvent(event);
}
private:
bool dragging = false;
QPoint offset;
};
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
QWidget host;
host.resize(660, 280);
auto *view = new DragChartView(&host);
host.show();
app.processEvents();
if (app.arguments().contains("--check")) {
const QPoint start = view->viewport()->mapToGlobal(QPoint(40, 40));
const auto send = [&](QEvent::Type type, const QPoint &global,
Qt::MouseButton button, Qt::MouseButtons buttons) {
const QPoint local = view->viewport()->mapFromGlobal(global);
QMouseEvent event(type, QPointF(local), QPointF(global),
button, buttons, Qt::NoModifier);
QApplication::sendEvent(view->viewport(), &event);
};
send(QEvent::MouseButtonPress, start, Qt::LeftButton, Qt::LeftButton);
send(QEvent::MouseMove, start + QPoint(35, 15), Qt::NoButton, Qt::LeftButton);
assert(view->pos() == QPoint(20, 20)); // 超出的部分被限制在宿主边界
send(QEvent::MouseButtonRelease, start + QPoint(35, 15),
Qt::LeftButton, Qt::NoButton);
send(QEvent::MouseMove, start + QPoint(-10, -10), Qt::NoButton, Qt::LeftButton);
assert(view->pos() == QPoint(20, 20)); // 释放后未重新开始拖动
assert(view->chart()->series().size() == 1);
assert(view->chart()->axes().size() == 2);
return 0;
}
return app.exec();
}

构建文件如下;示例面向 Qt 6 的 Charts 模块,类名无需 Qt 5 中的 QtCharts 命名空间写法。

cmake_minimum_required(VERSION 3.20)
project(DragChartExample LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(Qt6 REQUIRED COMPONENTS Widgets Charts)
add_executable(drag_chart main.cpp)
target_link_libraries(drag_chart PRIVATE Qt6::Widgets Qt6::Charts)

使用 Debug 构建后传入 --check,会向实际视口发送具有明确局部/全局坐标的事件,验证位移、边界和释放后的停止行为。该检查验证控件逻辑,不等价于验证所有操作系统、触摸设备和窗口管理器的真实鼠标抓取行为。

本例只在拖动时重新限制位置。宿主缩小后可能裁切图表;若要在调整窗口大小时也保持完整可见,还需在宿主的 resizeEvent() 中重新夹定子控件位置,并设置足够的最小尺寸,或另行设计图表缩放规则。宿主小于图表时,仅修改位置无法让它完整容纳图表。

图表对象也需要明确所有权

跳转到“图表对象也需要明确所有权”

QChartView 接管构造时传入的 QChart,QChart::addSeries() 接管加入的序列,createDefaultAxes() 为已有序列建立默认坐标轴。原片段另外 new QValueAxis() 却没有加入图表,这些额外轴不会因为调用 createDefaultAxes() 就自动获得归属;使用自定义轴时,应显式 addAxis()、attachAxis(),否则不要创建无用对象。Qt:QChartView、Qt:图表序列与坐标轴

原先的 backChartView 也没有设置宿主、布局或显示位置。上例只创建实际显示的前图表;若确实需要背景图层,应给后图表设置宿主、几何尺寸和层叠关系,并决定它是否接收鼠标。setBackgroundVisible(false) 只是关闭图表自身的背景绘制,不是整个窗口必然透明的保证。隐藏、禁用或失去激活时清除拖动状态,可避免取消操作之后继续沿用旧偏移。