我们已经完成了 CAD 基础框架搭建和性能优化,现在你可能会问:“接下来该往哪个方向走?” 新手最忌 “贪多求全”(比如直接上手 3D 建模、复杂约束),也忌 “停滞不前”(只停留在画点线圆)。这篇文章会给你一条循序渐进、可落地、能看到明确成果的开发路线,每个阶段都有具体目标、核心知识点和实操步骤,帮你稳步推进 CAD 框架的功能迭代。
一、先明确核心目标:从 “能画图” 到 “能编辑”
前两步我们实现了 “画点 / 线 / 圆 + 性能优化”,下一步的核心是让框架具备图形编辑能力—— 这是 CAD 软件区别于普通画图工具的关键,也是新手能快速掌握、且能显著提升框架实用性的方向。
整体路线分 3 个阶段(新手建议按顺序推进,每个阶段 1-2 周):
- 基础编辑:图形选中、移动、删除;
- 进阶编辑:图形缩放、旋转、复制粘贴;
- 辅助功能:图层管理、尺寸标注、文件读写。
每个阶段都基于之前的代码优化,不推翻原有逻辑,只做增量开发,降低学习成本。
二、第一阶段:实现图形选中、移动、删除(核心)
1. 核心目标
- 鼠标点击能选中图形(高亮显示);
- 选中后拖动鼠标能移动图形;
- 按 Delete 键删除选中的图形;
- 保持操作流畅(复用之前的性能优化技巧)。
2. 关键知识点
- 几何命中测试(判断鼠标是否 “点中” 图形);
- 选中状态管理(标记选中的图形,绘图时高亮);
- 事件联动(选中→拖动→释放的完整交互链)。
3. 实操步骤(附核心代码)
步骤 1:给图形添加 “选中状态” 属性
修改之前的图形结构体,增加isSelected标记(用于绘图时高亮):
// 基础图形结构体(新增选中状态) struct BaseShape { ShapeType type; // 图形类型 QColor color; // 颜色 int penWidth; // 线宽 bool isSelected; // 选中状态(新增) // 构造函数(初始化默认值) BaseShape(ShapeType t) : type(t), color(Qt::black), penWidth(2), isSelected(false) {} };步骤 2:实现 “命中测试”(判断鼠标是否选中图形)
这是编辑功能的核心 —— 新手不用搞复杂的几何算法,先实现 “简化版命中测试”(足够满足基础需求),后续再优化精度。
在CanvasWidget中新增命中测试函数(按图形类型分别实现):
// 新增:判断鼠标点是否命中图形(返回命中的图形指针) BaseShape* hitTest(const QPointF& mousePos) { // 按“圆→线→点”的顺序判断(圆的命中区域大,优先判断) // 1. 测试圆 foreach (CircleShape* circle, circleShapes) { // 计算鼠标到圆心的距离,小于半径+5像素视为命中(扩大范围,新手操作更友好) qreal dist = sqrt(pow(mousePos.x() - circle->center.x(), 2) + pow(mousePos.y() - circle->center.y(), 2)); if (dist <= circle->radius + 5) { return circle; } } // 2. 测试直线(简化版:点到直线的距离<5像素视为命中) foreach (LineShape* line, lineShapes) { if (isPointNearLine(mousePos, line->startPos, line->endPos)) { return line; } } // 3. 测试点 foreach (PointShape* point, pointShapes) { qreal dist = sqrt(pow(mousePos.x() - point->pos.x(), 2) + pow(mousePos.y() - point->pos.y(), 2)); if (dist <= 5) { // 5像素范围内视为命中 return point; } } return nullptr; // 未命中任何图形 } // 辅助函数:判断点是否靠近直线(复用之前优化的版本,避免开根号) bool isPointNearLine(const QPointF& point, const QPointF& p1, const QPointF& p2) { QVector2D v1(p2 - p1); QVector2D v2(point - p1); qreal dot = QVector2D::dotProduct(v1, v2); if (dot < 0) return false; qreal lenSq = v1.lengthSquared(); if (dot > lenSq) return false; qreal distSq = v2.lengthSquared() - dot * dot / lenSq; return distSq < pow(5, 2); // 5像素阈值 }步骤 3:处理鼠标事件,实现 “选中 + 移动”
修改CanvasWidget的鼠标事件,增加选中和移动逻辑:
// CanvasWidget新增成员变量 private: BaseShape* selectedShape = nullptr; // 当前选中的图形 QPointF lastMousePos; // 上次鼠标位置(用于移动计算) bool isDraggingShape = false; // 是否正在拖动图形 // 1. 鼠标按下事件:选中图形 void mousePressEvent(QMouseEvent *event) override { QPointF mousePos = event->pos(); // 先取消之前的选中状态 if (selectedShape != nullptr) { selectedShape->isSelected = false; selectedShape = nullptr; update(); // 刷新画布,取消高亮 } // 如果是右键/中键,走原有逻辑(平移画布) if (event->button() == Qt::MiddleButton) { // 平移画布的逻辑(之前实现的) m_isDragging = true; m_lastMousePos = mousePos; return; } // 左键:命中测试,选中图形 selectedShape = hitTest(mousePos); if (selectedShape != nullptr) { selectedShape->isSelected = true; isDraggingShape = false; // 初始未拖动 lastMousePos = mousePos; // 记录选中时的鼠标位置 update(); // 刷新画布,高亮显示选中的图形 return; } // 未选中图形:走原有绘图逻辑(画点/线/圆) if (currentTool != None) { // 原有绘图逻辑(按下创建临时图形)... } } // 2. 鼠标移动事件:移动选中的图形 void mouseMoveEvent(QMouseEvent *event) override { QPointF mousePos = event->pos(); // 优先处理图形拖动 if (selectedShape != nullptr && event->buttons() & Qt::LeftButton) { if (!isDraggingShape) { isDraggingShape = true; // 开始拖动 } // 计算鼠标移动的偏移量 QPointF delta = mousePos - lastMousePos; // 根据图形类型移动坐标 moveShape(selectedShape, delta); // 记录新的鼠标位置 lastMousePos = mousePos; // 局部重绘(复用之前的性能优化) update(getShapeBoundingRect(selectedShape).adjusted(-5, -5, 5, 5)); return; } // 未拖动图形:走原有逻辑(临时图形/平移画布) if (tempShape != nullptr) { // 原有临时图形拖动逻辑... } else if (m_isDragging) { // 原有平移画布逻辑... } } // 3. 鼠标释放事件:结束拖动 void mouseReleaseEvent(QMouseEvent *event) override { Q_UNUSED(event); isDraggingShape = false; // 结束拖动 // 原有临时图形保存逻辑... } // 新增:移动图形的核心函数(按类型处理坐标) void moveShape(BaseShape* shape, const QPointF& delta) { if (shape == nullptr) return; switch (shape->type) { case Point: { PointShape* point = dynamic_cast<PointShape*>(shape); point->pos += delta; break; } case Line: { LineShape* line = dynamic_cast<LineShape*>(shape); line->startPos += delta; line->endPos += delta; break; } case Circle: { CircleShape* circle = dynamic_cast<CircleShape*>(shape); circle->center += delta; break; } default: break; } }步骤 4:绘图时高亮选中的图形
修改drawShape(或分类型的绘图函数),选中的图形用 “红色 + 加粗” 显示:
void drawLine(QPainter* painter, LineShape* line) { QPen pen; if (line->isSelected) { // 选中状态:红色、线宽+2、虚线 pen = QPen(Qt::red, line->penWidth + 2, Qt::DashLine); } else { pen = QPen(line->color, line->penWidth); } painter->setPen(pen); painter->setRenderHint(QPainter::Antialiasing, false); painter->drawLine(line->startPos, line->endPos); } // 圆和点的绘图函数做类似修改...步骤 5:实现删除功能(按 Delete 键删除选中图形)
在CanvasWidget中重写键盘事件:
void keyPressEvent(QKeyEvent *event) override { if (event->key() == Qt::Key_Delete && selectedShape != nullptr) { // 根据类型删除图形(分桶存储的优势) switch (selectedShape->type) { case Point: { PointShape* point = dynamic_cast<PointShape*>(selectedShape); pointShapes.removeOne(point); delete point; break; } case Line: { LineShape* line = dynamic_cast<LineShape*>(selectedShape); lineShapes.removeOne(line); delete line; break; } case Circle: { CircleShape* circle = dynamic_cast<CircleShape*>(selectedShape); circleShapes.removeOne(circle); delete circle; break; } default: break; } selectedShape = nullptr; update(); // 刷新画布 } }4. 测试验证
编译运行后,测试核心功能:
- 点击画好的直线 / 圆 / 点,图形会变成红色虚线(选中高亮);
- 选中后拖动鼠标,图形会跟随移动,且只有图形区域刷新(不卡顿);
- 选中图形后按 Delete 键,图形被删除;
- 未选中图形时,仍能正常画点 / 线 / 圆。
三、第二阶段:进阶编辑(缩放、旋转、复制粘贴)
完成基础编辑后,可进一步实现 “图形变换” 功能 —— 这是 CAD 编辑的核心能力,新手重点掌握 “坐标变换” 逻辑,复用 Qt 的QTransform简化计算。
1. 核心目标
- 选中图形后,鼠标滚轮缩放图形(区别于画布缩放);
- 选中图形后,按快捷键(如 R)旋转图形;
- 支持复制(Ctrl+C)、粘贴(Ctrl+V)选中的图形。
2. 关键实操(以图形缩放为例)
// CanvasWidget新增:缩放选中的图形 void scaleSelectedShape(qreal scale) { if (selectedShape == nullptr) return; switch (selectedShape->type) { case Line: { LineShape* line = dynamic_cast<LineShape*>(selectedShape); // 以起点为中心缩放直线 QPointF center = line->startPos; line->endPos = center + (line->endPos - center) * scale; break; } case Circle: { CircleShape* circle = dynamic_cast<CircleShape*>(selectedShape); circle->radius *= scale; // 缩放半径 break; } default: break; } // 局部重绘 update(getShapeBoundingRect(selectedShape).adjusted(-5, -5, 5, 5)); } // 重写滚轮事件,区分“画布缩放”和“图形缩放” void wheelEvent(QWheelEvent *event) override { if (selectedShape != nullptr) { // 有选中图形:缩放图形 qreal delta = event->angleDelta().y() > 0 ? 1.1 : 0.9; scaleSelectedShape(delta); } else { // 无选中图形:缩放画布(原有逻辑) qreal delta = event->angleDelta().y() > 0 ? 1.1 : 0.9; scaleFactor *= delta; scaleFactor = qBound(0.1, scaleFactor, 10.0); // 更新变换矩阵... update(); } }四、第三阶段:辅助功能(图层、标注、文件读写)
当编辑功能稳定后,添加 “提升实用性” 的辅助功能 —— 这些功能不涉及复杂几何,但能让你的 CAD 框架更接近真实软件。
1. 图层管理(核心是 “分类显示 / 隐藏”)
- 新增
Layer结构体:包含图层名称、是否可见、颜色等属性; - 每个图形关联一个图层(修改
BaseShape,增加QString layerName属性); - 菜单栏添加 “图层管理” 窗口,实现 “显示 / 隐藏图层”“修改图层颜色”;
- 绘图时只绘制 “可见图层” 的图形。
2. 尺寸标注(CAD 的核心辅助功能)
- 新增
DimensionShape图形类型:存储标注的起点、终点、文字位置、标注值; - 实现 “长度标注”(直线长度)、“半径标注”(圆的半径):通过几何计算得到长度 / 半径,绘图时显示文字;
- 标注跟随图形变化:比如移动直线后,标注值自动更新。
3. 文件读写(保存 / 打开绘图成果)
- 用 Qt 的
QJsonDocument将图形数据(类型、坐标、颜色、图层)保存为 JSON 文件(新手易上手,比 DXF 简单); - 打开文件时,读取 JSON 数据,重建图形列表并绘制;
- 菜单栏添加 “新建 / 打开 / 保存” 功能,关联文件操作函数。
五、新手避坑指南(关键提醒)
- 不要同时推进多个功能:比如先做完 “选中 + 移动 + 删除”,再做缩放旋转,最后做图层标注 —— 贪多会导致代码混乱,排查问题困难;
- 复用已有代码和优化技巧:比如局部重绘、分桶存储、
QTransform等,不要重复造轮子; - 优先实现 “简化版”,再优化精度:比如命中测试先做 “扩大范围” 的简化版,能满足基本操作即可,后续再优化 “精准命中”;
- 及时测试和重构:每新增一个功能,都要测试性能(比如 100 个图形时是否卡顿),代码冗余时及时重构(比如提取通用的图形操作函数)。
六、后续进阶方向(当你完成以上所有功能后)
当你掌握了 “2D 绘图 + 编辑 + 辅助功能” 后,可根据兴趣选择进阶方向:
- 2D 进阶:添加约束功能(比如平行、垂直、等长约束)、批量绘图(阵列、镜像);
- 3D 入门:学习 OpenGL/Qt 3D 模块,实现简单的 3D 建模(拉伸、旋转 2D 图形成 3D 模型);
- 兼容性:支持读取 / 保存 DXF 格式(用 dxflib 库),兼容 AutoCAD 文件;
- 性能极致优化:引入空间索引(比如 R 树),提升大量图形的命中测试效率。
总结
作为新手,下一步的核心是 “从画图到编辑”—— 先实现图形选中、移动、删除,再逐步添加缩放、旋转、图层、标注等功能。这条路线的核心是 “增量开发、先易后难”,每个阶段都能看到明确的成果,既不会因难度过高放弃,也不会因功能单一失去动力。
记住:CAD 开发是一个 “积少成多” 的过程,哪怕每天只实现一个小功能(比如今天做选中,明天做移动),坚持下来你的框架会越来越完善。如果在开发过程中遇到具体问题(比如旋转图形的坐标计算),可以聚焦单个问题深入研究,不用急于求成