Qt图形视图框架实战:用QGraphicsScene和QGraphicsView打造简易绘图工具
在Qt生态中,图形视图框架一直是最具生产力的模块之一。想象一下,当你需要开发一个矢量绘图工具、流程图编辑器甚至简单的2D游戏时,这套框架能让你摆脱底层绘制的繁琐,专注于业务逻辑的实现。不同于直接使用QPainter进行绘制,图形视图框架通过场景(Scene)-视图(View)-图形项(Item)的三层架构,提供了更高级别的抽象和更完善的交互支持。
本文将带你从零构建一个功能完整的绘图工具,涵盖图形项的创建、场景管理、视图交互等核心功能。不同于理论讲解,我们会采用"代码先行"的方式,每个功能点都配有可运行的代码示例。你只需要基本的Qt开发环境(Qt 5.15+或Qt 6.x)和C++基础就能跟上节奏。
1. 环境准备与基础架构
1.1 项目初始化
首先创建一个标准的Qt Widgets Application项目。在.pro文件中确保包含以下模块:
QT += core gui widgets
对于主窗口,我们继承QMainWindow并添加核心组件:
#include <QMainWindow>
#include <QGraphicsScene>
#include <QGraphicsView>
class MainWindow : public QMainWindow {
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
private:
QGraphicsScene *scene;
QGraphicsView *view;
};
在构造函数中初始化场景和视图:
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
// 创建场景(虚拟画布)
scene = new QGraphicsScene(this);
scene->setSceneRect(0, 0, 800, 600);
// 创建视图(显示窗口)
view = new QGraphicsView(scene);
setCentralWidget(view);
// 视图优化设置
view->setRenderHint(QPainter::Antialiasing);
view->setDragMode(QGraphicsView::RubberBandDrag);
view->setViewportUpdateMode(QGraphicsView::FullViewportUpdate);
}
1.2 场景坐标系理解
Qt图形视图框架使用三层坐标系系统:
- 场景坐标:虚拟的无限大画布,原点默认为(0,0)
- 视图坐标:实际显示窗口的像素坐标
- 图形项坐标:每个图形项自身的局部坐标系
关键转换方法:
| 方法 | 作用 |
|---|---|
mapToScene() | 视图坐标→场景坐标 |
mapFromScene() | 场景坐标→视图坐标 |
mapToItem() | 场景坐标→图形项坐标 |
提示:在实现交互功能时,务必注意坐标系的转换,否则会出现点击位置不准等问题
2. 实现基本绘图功能
2.1 图形项创建与管理
我们首先实现矩形、椭圆和直线三种基本图形。创建自定义图形项基类:
class DrawItem : public QGraphicsItem {
public:
enum { Type = UserType + 1 };
int type() const override { return Type; }
// 公共接口
virtual void updateShape(const QPointF &start, const QPointF &end) = 0;
};
具体实现矩形图形项:
class RectItem : public DrawItem {
public:
QRectF boundingRect() const override {
return QRectF(startPoint, endPoint).normalized();
}
void paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) override {
painter->setPen(pen());
painter->setBrush(brush());
painter->drawRect(QRectF(startPoint, endPoint));
}
void updateShape(const QPointF &start, const QPointF &end) override {
prepareGeometryChange();
startPoint = start;
endPoint = end;
update();
}
private:
QPointF startPoint, endPoint;
};
2.2 绘图交互实现
在MainWindow中添加绘图状态管理:
enum DrawMode {
None,
DrawRect,
DrawEllipse,
DrawLine
};
class MainWindow {
// ...
void setupToolBar();
DrawMode currentMode = None;
DrawItem *currentItem = nullptr;
QPointF startPos;
};
实现鼠标事件处理:
void MainWindow::mousePressEvent(QMouseEvent *event) {
if (currentMode == None) return;
startPos = view->mapToScene(event->pos());
switch (currentMode) {
case DrawRect:
currentItem = new RectItem;
break;
case DrawEllipse:
currentItem = new EllipseItem;
break;
case DrawLine:
currentItem = new LineItem;
break;
}
scene->addItem(currentItem);
}
void MainWindow::mouseMoveEvent(QMouseEvent *event) {
if (!currentItem) return;
QPointF endPos = view->mapToScene(event->pos());
currentItem->updateShape(startPos, endPos);
}
void MainWindow::mouseReleaseEvent(QMouseEvent *) {
currentItem = nullptr;
}
3. 高级功能实现
3.1 图形项选择与变换
Qt内置了对图形项变换的支持,我们可以轻松实现移动、旋转和缩放:
// 在工具栏添加变换按钮
QAction *moveAction = toolbar->addAction("移动");
QAction *rotateAction = toolbar->addAction("旋转");
QAction *scaleAction = toolbar->addAction("缩放");
connect(moveAction, &QAction::triggered, [this]() {
view->setDragMode(QGraphicsView::ScrollHandDrag);
});
connect(rotateAction, &QAction::triggered, [this]() {
if (scene->selectedItems().isEmpty()) return;
QGraphicsItem *item = scene->selectedItems().first();
item->setRotation(item->rotation() + 15);
});
connect(scaleAction, &QAction::triggered, [this]() {
if (scene->selectedItems().isEmpty()) return;
QGraphicsItem *item = scene->selectedItems().first();
item->setScale(item->scale() * 1.2);
});
3.2 撤销/重做功能实现
利用Qt的Undo框架,我们可以轻松实现历史记录:
#include <QUndoStack>
#include <QUndoCommand>
class AddCommand : public QUndoCommand {
public:
AddCommand(QGraphicsScene *scene, DrawItem *item, QUndoCommand *parent = nullptr)
: QUndoCommand(parent), scene(scene), item(item) {
item->setFlag(QGraphicsItem::ItemIsSelectable);
item->setFlag(QGraphicsItem::ItemIsMovable);
}
void undo() override { scene->removeItem(item); }
void redo() override { scene->addItem(item); }
private:
QGraphicsScene *scene;
DrawItem *item;
};
// 在MainWindow中
QUndoStack *undoStack;
// 修改鼠标释放事件
void MainWindow::mouseReleaseEvent(QMouseEvent *) {
if (currentItem) {
undoStack->push(new AddCommand(scene, currentItem));
currentItem = nullptr;
}
}
4. 性能优化与高级技巧
4.1 大场景优化策略
当场景中包含大量图形项时,性能优化至关重要:
-
设置适当的场景矩形:
scene->setSceneRect(-10000, -10000, 20000, 20000); -
使用ItemIndexMethod:
scene->setItemIndexMethod(QGraphicsScene::BspTreeIndex); -
批量操作优化:
scene->blockSignals(true); // 执行大量添加/删除操作 scene->blockSignals(false);
4.2 自定义图形项进阶
实现一个可编辑的文本项展示高级技巧:
class EditableTextItem : public QGraphicsTextItem {
Q_OBJECT
public:
EditableTextItem(QGraphicsItem *parent = nullptr)
: QGraphicsTextItem(parent) {
setTextInteractionFlags(Qt::TextEditorInteraction);
setFlag(QGraphicsItem::ItemIsFocusable);
}
protected:
void focusOutEvent(QFocusEvent *event) override {
setTextInteractionFlags(Qt::NoTextInteraction);
QGraphicsTextItem::focusOutEvent(event);
emit textChanged(toPlainText());
}
void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *) override {
setTextInteractionFlags(Qt::TextEditorInteraction);
setFocus();
}
signals:
void textChanged(const QString &text);
};
5. 项目扩展与实用功能
5.1 文件保存与加载
实现场景的序列化功能:
void MainWindow::saveScene(const QString &filename) {
QFile file(filename);
if (!file.open(QIODevice::WriteOnly)) return;
QDataStream out(&file);
out << scene->items().size();
foreach (QGraphicsItem *item, scene->items()) {
if (DrawItem *drawItem = dynamic_cast<DrawItem*>(item)) {
out << drawItem->type();
drawItem->save(out);
}
}
}
void MainWindow::loadScene(const QString &filename) {
QFile file(filename);
if (!file.open(QIODevice::ReadOnly)) return;
scene->clear();
QDataStream in(&file);
int count;
in >> count;
for (int i = 0; i < count; ++i) {
int type;
in >> type;
DrawItem *item = nullptr;
switch (type) {
case RectItem::Type: item = new RectItem; break;
case EllipseItem::Type: item = new EllipseItem; break;
case LineItem::Type: item = new LineItem; break;
}
if (item) {
item->load(in);
scene->addItem(item);
}
}
}
5.2 插件系统设计
通过插件机制支持更多图形类型:
class ShapePlugin {
public:
virtual ~ShapePlugin() {}
virtual QString name() const = 0;
virtual QIcon icon() const = 0;
virtual DrawItem *createItem() = 0;
};
// 在主窗口中管理插件
QList<ShapePlugin*> plugins;
void MainWindow::loadPlugins() {
QDir pluginsDir(qApp->applicationDirPath() + "/plugins");
foreach (QString fileName, pluginsDir.entryList(QDir::Files)) {
QPluginLoader loader(pluginsDir.absoluteFilePath(fileName));
if (ShapePlugin *plugin = qobject_cast<ShapePlugin*>(loader.instance())) {
plugins.append(plugin);
QAction *action = toolbar->addAction(plugin->icon(), plugin->name());
connect(action, &QAction::triggered, [this, plugin]() {
currentMode = CustomMode;
currentPlugin = plugin;
});
}
}
}
在实现过程中,我发现图形项的坐标转换是最容易出错的地方。特别是在处理复合图形项时,一定要清楚当前操作是在哪个坐标系下进行的。另一个实用技巧是为所有自定义图形项实现save()和load()方法,这样序列化功能就能统一处理所有图形类型。
1万+



被折叠的 条评论
为什么被折叠?



