新手入门 CAD 开发:用 C+++Qt 搭建基础框架(第一步:画布与基础绘图)

        上一篇文章我们理清了 CAD 开发的核心基础知识,这一篇就从实战出发 —— 用 C++ 结合 Qt 搭建 CAD 软件的基础框架,完成 “可绘图的画布” 核心功能。作为新手,这一步的目标很明确:创建一个带菜单栏的窗口,实现 “画点、画直线、画圆” 的基础操作,理解 Qt 绘图的核心逻辑,为后续扩展打下基础。

一、前期准备(新手必看)

1. 开发环境搭建

  • 工具:Qt Creator(直接下载 Qt 官方安装包,包含 Qt 库和编译器,新手推荐 Qt 5.15 版本,兼容性好);
  • 语言:C++(Qt 对 C++ 支持完善,且后续几何计算、性能优化更方便);
  • 无需额外库:本章仅用 Qt 自带的QWidget(画布)、QPainter(绘图工具)、QEvent(事件处理),不用额外安装第三方库,降低入门难度。

2. 核心思路拆解

本次搭建的基础框架,核心是实现 3 件事:

  1. 一个 “画布” 窗口:作为绘图区域,支持鼠标交互;
  2. 绘图功能:通过菜单栏选择 “画点 / 画直线 / 画圆”,用鼠标操作完成绘制;
  3. 数据存储:用简单的数据结构存储绘制的图形(比如直线的两个端点、圆的圆心和半径),确保窗口刷新后图形不消失。

二、实战开发步骤(分模块讲解,附完整代码)

第一步:创建 Qt 项目

  1. 打开 Qt Creator,新建 “Qt Widgets Application” 项目;
  2. 项目名称:SimpleCAD,选择保存路径;
  3. 基类选择QMainWindow(带菜单栏、工具栏,适合做软件主窗口);
  4. 取消 “创建界面文件(.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事件循环
}

第三步:编译运行,测试功能

  1. 点击 Qt Creator 的 “运行” 按钮(绿色三角),编译无错误后会弹出窗口;
  2. 测试核心功能:
    • 菜单栏 “绘图工具”→“画点”(或 Ctrl+P),点击画布任意位置,会出现黑色小点;
    • 选择 “画直线”(Ctrl+L),鼠标按下拖动,释放后生成直线;
    • 选择 “画圆”(Ctrl+C),鼠标按下(圆心)拖动(半径),释放后生成圆;
    • 菜单栏 “编辑”→“清空画布”(Ctrl+Del),可重置画布。

三、核心逻辑讲解(新手必懂)

1. Qt 绘图的核心:QPainterpaintEvent

  • QPainter:Qt 的绘图工具,相当于 “画笔”,可以设置颜色、线宽、抗锯齿等,提供drawLinedrawEllipse等绘图函数;
  • paintEvent:窗口刷新时(比如拖动窗口、调用update())自动触发的事件,所有绘图操作必须放在这里,否则图形会消失;
  • 抗锯齿:painter.setRenderHint(QPainter::Antialiasing),新手一定要加,否则线条边缘会有锯齿,影响视觉效果。

2. 鼠标事件的联动:按下→移动→释放

CAD 绘图的核心交互逻辑就是这三个事件的配合:

  • 按下(mousePressEvent):确定绘图起点(比如直线的起点、圆的圆心);
  • 移动(mouseMoveEvent):实时更新图形(比如直线的终点、圆的半径),通过update()刷新画布,实现 “拖动实时显示”;
  • 释放(mouseReleaseEvent):完成绘图,将临时图形保存到正式列表中。

3. 图形数据的存储:QVector与继承

  • QVector<BaseShape*>存储所有图形,QVector是 Qt 的动态数组,支持动态添加、遍历,适合存储不确定数量的图形;
  • 所有图形继承自BaseShape,统一存储在一个列表中,绘图时通过dynamic_cast转换类型,这种设计便于后续扩展(比如添加矩形、多边形)。

四、新手扩展建议(下一步优化方向)

完成这个基础框架后,新手可以按以下方向逐步扩展,加深理解:

  1. 增加图形样式设置:比如菜单栏添加 “颜色选择”“线宽调整”,修改BaseShapecolorpenWidth属性;
  2. 实现图形选中功能:在mousePressEvent中判断鼠标坐标是否落在图形上(比如直线的附近、圆的内部),选中后高亮显示;
  3. 添加文件保存功能:用 Qt 的QFileshapes列表中的图形坐标、类型保存为 JSON 文件,支持 “打开文件” 重新绘制;
  4. 优化清空画布逻辑:不用重新创建画布,而是遍历shapes列表删除所有图形对象,避免内存泄漏(新手可先了解delete的使用)。

五、常见问题排查(新手避坑)

  1. 编译报错 “未定义标识符”:检查头文件是否包含完整,QVectorQPainter等类是否添加了对应的头文件(比如#include <QVector>);
  2. 图形拖动时闪烁:Qt 默认开启双缓冲绘图,一般不会闪烁,如果出现闪烁,可在CanvasWidget的构造函数中添加setAttribute(Qt::WA_NoSystemBackground, true);
  3. 鼠标坐标不准确:event->pos()获取的是相对于当前部件(画布)的坐标,event->globalPos()是屏幕坐标,新手务必用pos()
  4. 内存泄漏:每次删除shapes中的图形时,要调用delete释放内存(比如清空画布时,遍历shapesdelete每个元素,再clear()列表)。

总结

用 C+++Qt 搭建 CAD 基础框架,核心是掌握 “Qt 绘图机制 + 鼠标事件处理 + 图形数据存储” 这三个关键点。本章实现的框架虽然简单,但已经包含了 CAD 软件的核心逻辑:通过交互事件获取用户操作,将操作转换为几何数据,再通过绘图工具显示在画布上。

作为新手,不用急于添加复杂功能,先把这个基础框架吃透,理解每个函数、每个类的作用,再逐步扩展。下一篇文章我们会实现 “图形选中、移动、编辑” 功能,进一步完善 CAD 的核心交互逻辑。如果在实践中遇到问题,欢迎在评论区交流~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值