上一篇文章我们理清了 CAD 开发的核心基础知识,这一篇就从实战出发 —— 用 C++ 结合 Qt 搭建 CAD 软件的基础框架,完成 “可绘图的画布” 核心功能。作为新手,这一步的目标很明确:创建一个带菜单栏的窗口,实现 “画点、画直线、画圆” 的基础操作,理解 Qt 绘图的核心逻辑,为后续扩展打下基础。
一、前期准备(新手必看)
1. 开发环境搭建
- 工具:Qt Creator(直接下载 Qt 官方安装包,包含 Qt 库和编译器,新手推荐 Qt 5.15 版本,兼容性好);
- 语言:C++(Qt 对 C++ 支持完善,且后续几何计算、性能优化更方便);
- 无需额外库:本章仅用 Qt 自带的
QWidget(画布)、QPainter(绘图工具)、QEvent(事件处理),不用额外安装第三方库,降低入门难度。
2. 核心思路拆解
本次搭建的基础框架,核心是实现 3 件事:
- 一个 “画布” 窗口:作为绘图区域,支持鼠标交互;
- 绘图功能:通过菜单栏选择 “画点 / 画直线 / 画圆”,用鼠标操作完成绘制;
- 数据存储:用简单的数据结构存储绘制的图形(比如直线的两个端点、圆的圆心和半径),确保窗口刷新后图形不消失。
二、实战开发步骤(分模块讲解,附完整代码)
第一步:创建 Qt 项目
- 打开 Qt Creator,新建 “Qt Widgets Application” 项目;
- 项目名称:
SimpleCAD,选择保存路径; - 基类选择
QMainWindow(带菜单栏、工具栏,适合做软件主窗口); - 取消 “创建界面文件(.ui)”(新手先手动写代码,理解界面逻辑,后续再用 UI 设计器)。
第二步:设计核心数据结构(存储图形)
CAD 软件的核心是 “图形数据”,每个图形需要存储:类型(点 / 线 / 圆)、坐标、样式(颜色 / 线型)。我们用 C++ 的 “结构体 + 枚举” 来定义,清晰又好维护。
在SimpleCAD.h中添加以下代码:
// 图形类型枚举(支持点、直线、圆)
enum ShapeType {
Point,
Line,
Circle,
None // 无选中绘图工具
};
// 基础图形结构体(存储所有图形的通用属性)
struct BaseShape {
ShapeType type; // 图形类型
QColor color; // 颜色
int penWidth; // 线宽
// 构造函数(初始化默认值)
BaseShape(ShapeType t) : type(t), color(Qt::black), penWidth(2) {}
};
// 点图形(继承基础图形,添加坐标属性)
struct PointShape : public BaseShape {
QPointF pos; // 点的坐标(用QPointF支持浮点数,精度更高)
PointShape() : BaseShape(Point) {}
};
// 直线图形(继承基础图形,添加两个端点坐标)
struct LineShape : public BaseShape {
QPointF startPos; // 起点
QPointF endPos; // 终点
LineShape() : BaseShape(Line) {}
};
// 圆图形(继承基础图形,添加圆心和半径)
struct CircleShape : public BaseShape {
QPointF center; // 圆心
qreal radius; // 半径(qreal是Qt的浮点数类型,跨平台兼容)
CircleShape() : BaseShape(Circle), radius(0) {}
};
第三步:搭建主窗口框架(菜单栏 + 画布)
主窗口包含两部分:顶部菜单栏(选择绘图工具)、中央画布(绘图区域)。我们用QMainWindow作为主窗口,自定义一个CanvasWidget类作为画布(继承QWidget)。
1. 自定义画布类(CanvasWidget)
画布是绘图的核心,需要处理:鼠标事件(点击、拖动)、绘图事件(刷新时重绘图形)。在SimpleCAD.h中添加CanvasWidget类:
#include <QMainWindow>
#include <QWidget>
#include <QPainter>
#include <QVector>
#include <QMouseEvent>
// 画布类(负责绘图和鼠标交互)
class CanvasWidget : public QWidget {
Q_OBJECT
public:
CanvasWidget(QWidget *parent = nullptr) : QWidget(parent) {
setBackgroundColor(Qt::white); // 画布默认白色
currentTool = None; // 初始无选中工具
}
// 设置当前绘图工具(从主窗口菜单栏调用)
void setCurrentTool(ShapeType tool) {
currentTool = tool;
}
private:
ShapeType currentTool; // 当前选中的绘图工具
QVector<BaseShape*> shapes; // 存储所有绘制的图形(动态数组,自动扩容)
// 临时图形(比如画直线时,拖动过程中显示的临时线)
BaseShape* tempShape = nullptr;
// 设置画布背景色
void setBackgroundColor(QColor color) {
setPalette(QPalette(color));
setAutoFillBackground(true);
}
// 绘图事件(窗口刷新时自动调用,必须重写)
void paintEvent(QPaintEvent *event) override {
Q_UNUSED(event);
QPainter painter(this); // 创建绘图工具
painter.setRenderHint(QPainter::Antialiasing); // 抗锯齿(避免线条锯齿)
// 绘制所有已保存的图形
foreach (BaseShape* shape, shapes) {
drawShape(&painter, shape);
}
// 绘制临时图形(比如拖动过程中的直线/圆)
if (tempShape != nullptr) {
drawShape(&painter, tempShape);
}
}
// 鼠标按下事件(绘图的起点)
void mousePressEvent(QMouseEvent *event) override {
if (currentTool == None) return; // 无选中工具,不处理
QPointF pos = event->pos(); // 获取鼠标点击坐标(相对于画布)
// 根据当前工具创建临时图形
switch (currentTool) {
case Point: {
// 画点:点击即完成,直接添加到图形列表
PointShape* point = new PointShape;
point->pos = pos;
shapes.append(point);
update(); // 刷新画布,显示新图形
break;
}
case Line: {
// 画直线:按下是起点,拖动是终点,先创建临时线
LineShape* line = new LineShape;
line->startPos = pos;
line->endPos = pos; // 初始终点=起点
tempShape = line;
break;
}
case Circle: {
// 画圆:按下是圆心,拖动是半径,创建临时圆
CircleShape* circle = new CircleShape;
circle->center = pos;
tempShape = circle;
break;
}
default:
break;
}
}
// 鼠标移动事件(拖动时更新临时图形)
void mouseMoveEvent(QMouseEvent *event) override {
if (tempShape == nullptr) return; // 无临时图形,不处理
QPointF pos = event->pos();
// 根据临时图形类型更新坐标
switch (tempShape->type) {
case Line: {
LineShape* line = dynamic_cast<LineShape*>(tempShape);
line->endPos = pos; // 更新直线终点
break;
}
case Circle: {
CircleShape* circle = dynamic_cast<CircleShape*>(tempShape);
// 计算圆心到当前鼠标位置的距离(半径)
circle->radius = sqrt(pow(pos.x() - circle->center.x(), 2) +
pow(pos.y() - circle->center.y(), 2));
break;
}
default:
break;
}
update(); // 实时刷新画布,显示拖动效果
}
// 鼠标释放事件(完成绘图,保存临时图形)
void mouseReleaseEvent(QMouseEvent *event) override {
Q_UNUSED(event);
if (tempShape != nullptr) {
// 过滤无效图形(比如半径为0的圆)
if (tempShape->type == Circle) {
CircleShape* circle = dynamic_cast<CircleShape*>(tempShape);
if (circle->radius < 1) { // 半径太小,视为无效
delete tempShape;
tempShape = nullptr;
return;
}
}
// 将临时图形添加到正式列表
shapes.append(tempShape);
tempShape = nullptr; // 清空临时图形
}
}
// 辅助函数:根据图形类型绘制(统一绘图逻辑)
void drawShape(QPainter* painter, BaseShape* shape) {
if (shape == nullptr) return;
// 设置画笔样式(颜色、线宽)
QPen pen(shape->color, shape->penWidth);
painter->setPen(pen);
// 根据图形类型绘制
switch (shape->type) {
case Point: {
PointShape* point = dynamic_cast<PointShape*>(shape);
// 画点:用小圆形表示(点太小看不见)
painter->drawEllipse(point->pos, 3, 3);
break;
}
case Line: {
LineShape* line = dynamic_cast<LineShape*>(shape);
painter->drawLine(line->startPos, line->endPos);
break;
}
case Circle: {
CircleShape* circle = dynamic_cast<CircleShape*>(shape);
// 画圆:QPainter的drawEllipse参数是“矩形区域”,需计算左上角坐标
QRectF rect(circle->center.x() - circle->radius,
circle->center.y() - circle->radius,
circle->radius * 2, circle->radius * 2);
painter->drawEllipse(rect);
break;
}
default:
break;
}
}
};
2. 主窗口类(SimpleCADWindow)
主窗口负责创建菜单栏,将画布作为中央部件,关联 “绘图工具” 菜单与画布的绘图功能。在SimpleCAD.h中添加主窗口类:
// 主窗口类(包含菜单栏和画布)
class SimpleCADWindow : public QMainWindow {
Q_OBJECT
public:
SimpleCADWindow(QWidget *parent = nullptr) : QMainWindow(parent) {
// 设置窗口大小和标题
setWindowTitle("SimpleCAD - 新手入门版");
resize(800, 600);
// 创建画布并设置为中央部件
canvas = new CanvasWidget(this);
setCentralWidget(canvas);
// 创建菜单栏
createMenuBar();
}
private:
CanvasWidget* canvas;
// 创建菜单栏
void createMenuBar() {
QMenuBar* menuBar = new QMenuBar(this);
setMenuBar(menuBar);
// 1. 绘图工具菜单(核心)
QMenu* drawMenu = new QMenu("绘图工具(&D)", this);
menuBar->addMenu(drawMenu);
// 菜单动作:画点
QAction* pointAction = new QAction("画点(&P)", this);
pointAction->setShortcut(QKeySequence("Ctrl+P")); // 快捷键
connect(pointAction, &QAction::triggered, [=]() {
canvas->setCurrentTool(Point);
});
// 菜单动作:画直线
QAction* lineAction = new QAction("画直线(&L)", this);
lineAction->setShortcut(QKeySequence("Ctrl+L"));
connect(lineAction, &QAction::triggered, [=]() {
canvas->setCurrentTool(Line);
});
// 菜单动作:画圆
QAction* circleAction = new QAction("画圆(&C)", this);
circleAction->setShortcut(QKeySequence("Ctrl+C"));
connect(circleAction, &QAction::triggered, [=]() {
canvas->setCurrentTool(Circle);
});
// 添加动作到菜单
drawMenu->addAction(pointAction);
drawMenu->addAction(lineAction);
drawMenu->addAction(circleAction);
// 2. 辅助菜单:清空画布
QMenu* editMenu = new QMenu("编辑(&E)", this);
menuBar->addMenu(editMenu);
QAction* clearAction = new QAction("清空画布(&Clear)", this);
clearAction->setShortcut(QKeySequence("Ctrl+Del"));
connect(clearAction, &QAction::triggered, [=]() {
// 重新创建画布(简单粗暴,新手易懂)
delete canvas;
canvas = new CanvasWidget(this);
setCentralWidget(canvas);
});
editMenu->addAction(clearAction);
}
};
3. 主函数(程序入口)
在main.cpp中编写主函数,启动应用程序:
#include <QApplication>
#include "SimpleCAD.h"
int main(int argc, char *argv[]) {
QApplication a(argc, argv);
SimpleCADWindow w;
w.show(); // 显示主窗口
return a.exec(); // 进入Qt事件循环
}
第三步:编译运行,测试功能
- 点击 Qt Creator 的 “运行” 按钮(绿色三角),编译无错误后会弹出窗口;
- 测试核心功能:
- 菜单栏 “绘图工具”→“画点”(或 Ctrl+P),点击画布任意位置,会出现黑色小点;
- 选择 “画直线”(Ctrl+L),鼠标按下拖动,释放后生成直线;
- 选择 “画圆”(Ctrl+C),鼠标按下(圆心)拖动(半径),释放后生成圆;
- 菜单栏 “编辑”→“清空画布”(Ctrl+Del),可重置画布。
三、核心逻辑讲解(新手必懂)
1. Qt 绘图的核心:QPainter与paintEvent
QPainter:Qt 的绘图工具,相当于 “画笔”,可以设置颜色、线宽、抗锯齿等,提供drawLine、drawEllipse等绘图函数;paintEvent:窗口刷新时(比如拖动窗口、调用update())自动触发的事件,所有绘图操作必须放在这里,否则图形会消失;- 抗锯齿:
painter.setRenderHint(QPainter::Antialiasing),新手一定要加,否则线条边缘会有锯齿,影响视觉效果。
2. 鼠标事件的联动:按下→移动→释放
CAD 绘图的核心交互逻辑就是这三个事件的配合:
- 按下(
mousePressEvent):确定绘图起点(比如直线的起点、圆的圆心); - 移动(
mouseMoveEvent):实时更新图形(比如直线的终点、圆的半径),通过update()刷新画布,实现 “拖动实时显示”; - 释放(
mouseReleaseEvent):完成绘图,将临时图形保存到正式列表中。
3. 图形数据的存储:QVector与继承
- 用
QVector<BaseShape*>存储所有图形,QVector是 Qt 的动态数组,支持动态添加、遍历,适合存储不确定数量的图形; - 所有图形继承自
BaseShape,统一存储在一个列表中,绘图时通过dynamic_cast转换类型,这种设计便于后续扩展(比如添加矩形、多边形)。
四、新手扩展建议(下一步优化方向)
完成这个基础框架后,新手可以按以下方向逐步扩展,加深理解:
- 增加图形样式设置:比如菜单栏添加 “颜色选择”“线宽调整”,修改
BaseShape的color和penWidth属性; - 实现图形选中功能:在
mousePressEvent中判断鼠标坐标是否落在图形上(比如直线的附近、圆的内部),选中后高亮显示; - 添加文件保存功能:用 Qt 的
QFile将shapes列表中的图形坐标、类型保存为 JSON 文件,支持 “打开文件” 重新绘制; - 优化清空画布逻辑:不用重新创建画布,而是遍历
shapes列表删除所有图形对象,避免内存泄漏(新手可先了解delete的使用)。
五、常见问题排查(新手避坑)
- 编译报错 “未定义标识符”:检查头文件是否包含完整,
QVector、QPainter等类是否添加了对应的头文件(比如#include <QVector>); - 图形拖动时闪烁:Qt 默认开启双缓冲绘图,一般不会闪烁,如果出现闪烁,可在
CanvasWidget的构造函数中添加setAttribute(Qt::WA_NoSystemBackground, true);; - 鼠标坐标不准确:
event->pos()获取的是相对于当前部件(画布)的坐标,event->globalPos()是屏幕坐标,新手务必用pos(); - 内存泄漏:每次删除
shapes中的图形时,要调用delete释放内存(比如清空画布时,遍历shapes并delete每个元素,再clear()列表)。
总结
用 C+++Qt 搭建 CAD 基础框架,核心是掌握 “Qt 绘图机制 + 鼠标事件处理 + 图形数据存储” 这三个关键点。本章实现的框架虽然简单,但已经包含了 CAD 软件的核心逻辑:通过交互事件获取用户操作,将操作转换为几何数据,再通过绘图工具显示在画布上。
作为新手,不用急于添加复杂功能,先把这个基础框架吃透,理解每个函数、每个类的作用,再逐步扩展。下一篇文章我们会实现 “图形选中、移动、编辑” 功能,进一步完善 CAD 的核心交互逻辑。如果在实践中遇到问题,欢迎在评论区交流~
2035

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



