资讯动态

Qt绘图软件实战教程:从QPainter自绘到交互与撤销重做

发布时间:2026/10/15 19:10:02 来源:尧图企业网站定制
简介面向Qt初学者与需要实现2D绘图功能的开发者这份教程与源码包围绕paintExample示例系统讲解QPainter绘图、鼠标事件处理、颜色与画笔选择以及图像保存与加载等核心知识点可帮助读者快速构建一个简单可用的画图程序。资源共114个文件包含C源码.cpp/.h、界面文件.ui、工程配置.pro、资源文件.qrc以及多张运行截图和说明文档压缩包大小仅3.54MB结构清晰便于对照学习。目前已有704人学习下载。通过学习这份材料读者既能掌握QPainter在QWidget上的绘制流程也能理解MainWindow与绘图区域的协作方式还能模仿示例实现自定义画板、设置画笔样式并导出图片为后续开发复杂图形应用打下扎实基础。1. 用 Qt 写绘图软件这份教程为什么值得照着重做一遍我在学 2D 绘图那会儿最先接触到的 Qt 教程大多只讲「怎么画一条线」创建一个工程、加上 paintEvent、调用 drawLine 就完事了。可真当你想做一个能拖动矩形、能撤销重做、能导出图片的绘图软件时才发现 QPainter 只是最底层的一块砖上面还有坐标变换要不要处理、鼠标事件怎么映射、图片导出怎么不模糊这些一个比一个现实的坑。这份《Qt 编写绘图软件教程及相关源码》就是把整个链路串好了才放出来的从 QWidget 上自绘开始到工具栏交互、形状对象管理、撤销重做再到最终打包发布每一步都有对应源码。它的价值不在于教你背 API而在于你能直接拿到一个能跑起来的工程照着改出自己要的形态。适合已经能写简单 Qt 界面、但没完整实现过自绘制软件的开发者也适合想用最短时间摸清 QPainter 工程化写法的同学。2. QPainter 绘图系统先搞清楚为什么这套教程选它而不是 QGraphicsView2.1 绘图设备与坐标系QPainter 的定位与选型理由拿到这份源码包你会看到它没有用 QGraphicsView而是把 QPainter 直接画在 QWidget 上面。很多人在微博或社区问过是不是 QGraphicsView 更高级这里先说结论如果要做的是画图板这类自由绘制工具QPainter 路线在事件响应和局部重绘控制上更直接也更接近传统绘图软件的实现方式。QGraphicsView 的那套 Item 体系适合管理大量既有的图形元素比如地图标注、流程图编辑器但当你需要像橡皮筋拉框、手写笔迹这种逐像素交互时QPainter 加 paintEvent 的组合反而少一层封装思维负担更小。源码里画出第一张图的路径是先创建一个 QPixmap 作为离屏缓冲区再构造 QPainter 在它上面绘制最后把这张图整体贴到控件上。这样做的第一个好处是绘图过程与屏幕显示解耦你可以随时重画而且不闪烁。// 创建一个 800x600 的离屏画布 QPixmap canvas(800, 600); canvas.fill(Qt::white); // 清空背景 QPainter painter(canvas); painter.setRenderHint(QPainter::Antialiasing); painter.drawLine(20, 20, 200, 20); painter.end(); // 结束绘制之后才可以把 canvas 贴到控件上这段代码里canvas.fill(Qt::white)相当于清空画布不写会残留上一次的内容。setRenderHint(QPainter::Antialiasing)打开抗锯齿画斜线和椭圆时边缘会柔和很多代价是绘制速度下降一点。painter.end()很重要在同一个设备上再次构造 QPainter 之前必须先结束上一次绘制否则 Qt 会警告你设备已经被占用。关于坐标系QPainter 默认的逻辑坐标原点在左上角x 向右y 向下单位是像素。这个和数学坐标系不一样很多人第一次画函数曲线时发现图形是上下颠倒的其实就是忘了 y 轴方向。如果你要画直角坐标系需要提前做一次变换painter.translate(400, 300); // 把原点移到控件中心 painter.scale(1, -1); // 翻转 y 轴translate(400, 300)表示把逻辑原点移到物理坐标 (400, 300) 处之后的绘图坐标都相对这个新原点。scale(1, -1)让 y 轴反向这样 y 值越大越往上和数学约定一致。要注意翻转后文字也会倒过来所以画完坐标轴后记得restore()或者把文字绘制放到翻转之前。2.2 基础图元绘制与画笔参数控制线条与填充的粒度源码包里把直线、矩形、椭圆、圆角矩形这几个基础图元都封装成了独立的绘制函数。每个图元对应一组 QPen 和 QBrush 参数这是 2D 绘图里最核心的两个概念。QPen 负责线的颜色、宽度、样式QBrush 负责填充的颜色和填充模式。void drawShape(QPainter p, const Shape s) { QPen pen(s.penColor); pen.setWidth(s.penWidth); pen.setStyle(Qt::SolidLine); pen.setCapStyle(Qt::RoundCap); // 让线段端点变成圆头 pen.setJoinStyle(Qt::RoundJoin); // 折线拐角处圆滑 p.setPen(pen); QBrush brush(s.fillColor); brush.setStyle(Qt::SolidPattern); p.setBrush(brush); if (s.type Shape::Rectangle) { p.drawRect(s.rect); } else if (s.type Shape::Ellipse) { p.drawEllipse(s.rect); } }这里需要注意QPen的宽度单位默认情况下setWidth(1)是 1 个逻辑像素在高 DPI 屏幕上实际物理像素可能是 2 个。如果你想画成物理像素宽需要手动乘上devicePixelRatio。另外Qt::RoundCap会让线段端点从直角变成圆头在画折线和自由笔迹时观感好很多Qt::RoundJoin解决了折线拐点处的尖角毛刺问题。绘制椭圆时drawEllipse接收的是一个矩形参数椭圆会内切于这个矩形。如果你想画正圆就得保证矩形的宽高相等源码里是用QRect对象传入所以你在拖拽生成椭圆时需要在鼠标事件里做一次「正方形约束」否则拉出来永远不是正圆。还有一个容易被忽略的坑QBrush的默认样式是Qt::NoBrush也就是不填充。很多新手画矩形发现只有边框没有填充就是因为忘了setBrush(Qt::SolidPattern)。反过来如果你只想画填充图形不想要边框就把setPen(Qt::NoPen)。2.3 绘制状态机save 与 restore 是变换的后悔药在绘图软件里经常需要先画一个图形缩放一下画另一个图形再旋转一下画一段文字。如果每次都手动把变换属性设回来代码会乱成一锅粥。QPainter 提供了一个维护状态栈的机制save()把当前的画笔、画刷、变换矩阵全部压栈restore()把之前保存的状态弹出来恢复。这个机制在源码里被大量用于坐标变换和不同图元的切换。void paintEvent(QPaintEvent*) override { QPainter p(this); p.save(); p.translate(width() / 2, height() / 2); p.rotate(45); // 画旋转 45 度的矩形此时原点在控件中心 p.setPen(QPen(Qt::red, 2)); p.drawRect(-50, -50, 100, 100); p.restore(); p.save(); p.translate(20, 20); // 画一个普通矩形不受上面的旋转影响 p.setPen(QPen(Qt::blue, 2)); p.drawRect(0, 0, 80, 40); p.restore(); }这里的save和restore成对出现把第一段的旋转限制在局部。如果不写成对状态会一直累积到下一次paintEvent导致后续绘图坐标全乱。这里给一个我自己的习惯每次进入 paintEvent 先无条件save()一次退出前再restore()中间所有变换都不成对使用也问题不大因为外层已经兜底了。不过要注意save/restore保存的是 QPainter 的状态也就是笔、刷、变换矩阵等属性它不会保存你绘制的图形数据。图形数据必须由你自己维护比如用一个QListShape存下来。这就是绘图软件里「绘制状态机」和「数据模型」的边界QPainter 只管怎么画不管画了些什么。3. 搭建可交互的绘图软件界面把 QWidget 变成画布3.1 界面框架与工具栏设计从 QMainWindow 到 QToolBar源码包的工程是一个标准的 QMainWindow 应用中间放一个自绘的 CanvasWidget顶部是工具栏左边是颜色和线宽选择。工具栏的 Action 通过QAction添加用setCheckable(true)实现互斥选择。这个设计比多窗口弹窗直接得多也是大多数桌面绘图软件的基础布局。QToolBar* toolbar addToolBar(tr(绘图工具)); QAction* drawRectAction new QAction(tr(矩形), this); drawRectAction-setCheckable(true); drawRectAction-setData(Shape::Rectangle); toolbar-addAction(drawRectAction); QAction* drawEllipseAction new QAction(tr(椭圆), this); drawEllipseAction-setCheckable(true); drawEllipseAction-setData(Shape::Ellipse); toolbar-addAction(drawEllipseAction); QActionGroup* group new QActionGroup(this); group-addAction(drawRectAction); group-addAction(drawEllipseAction); drawRectAction-setChecked(true);setData可以给每个 Action 附加一个整数类型的工具标识这样点击时只要从QAction::data()里取出类型值再传给画布组件避免了写多个 if-else 槽函数的冗余。QActionGroup保证了同类工具只能有一个被选中实现互斥切换。这里有个细节QActionGroup默认不设置独占你要显式调用setExclusive(true)或者像上面代码一样把所有同组 Action 加到同一个 groupQt 在 group 中默认就是独占的。不过为了可读性建议显式写清楚。画布组件CanvasWidget继承自 QWidget设置setAutoFillBackground(false)并且把背景绘制交给自己的paintEvent。这样才能保证后续用离屏缓冲时不会出现系统默认背景色覆盖内容的问题。3.2 鼠标事件实现自由绘制与形状拖拽橡皮筋和拖拽状态机绘图的核心交互是按下鼠标开始绘制拖动过程中显示一个临时预览图形松开鼠标后把这个图形固定到画布上。这个「临时预览」在绘图软件里叫橡皮筋Rubber Band它的实现依赖于在mouseMoveEvent里不断更新存储的临时矩形的终点并触发update()重绘。void CanvasWidget::mousePressEvent(QMouseEvent* event) { if (event-button() Qt::LeftButton) { startPoint event-pos(); currentShape Shape(startPoint, startPoint, currentTool, currentPen, currentBrush); isDrawing true; } } void CanvasWidget::mouseMoveEvent(QMouseEvent* event) { if (isDrawing) { currentShape.setEnd(event-pos()); update(); // 触发整个画布重绘 } } void CanvasWidget::mouseReleaseEvent(QMouseEvent* event) { if (event-button() Qt::LeftButton isDrawing) { currentShape.setEnd(event-pos()); shapes.append(currentShape); isDrawing false; update(); } }这里event-pos()返回的是控件坐标系下的整数坐标。注意mousePressEvent里要先判断左键因为鼠标右键可能被预留成其他功能比如弹出菜单。update()是异步请求重绘Qt 会合并同一事件循环内多次update()调用所以拖动时效果很流畅不会因为每帧都同步重绘导致卡顿。只有在非常特殊的情况——比如你需要立即刷新界面来响应用户操作——才用repaint()但那种用法默认建议加参数只重绘局部。在paintEvent里先绘制所有已经固定的shapes再绘制currentShape这样预览图形会浮在已有图形上方视觉上就像橡皮筋拉出来的一条轮廓。为了区分预览和正式图形预览状态可以画成虚线见下面的绘制代码。void CanvasWidget::paintEvent(QPaintEvent*) { QPainter p(this); p.setRenderHint(QPainter::Antialiasing); for (const Shape s : shapes) { drawShape(p, s); // 已绘制的图形实线实心 } if (isDrawing) { p.save(); QPen pen currentShape.pen; pen.setStyle(Qt::DashLine); p.setPen(pen); drawShape(p, currentShape); // 预览图形虚线 p.restore(); } }这里把预览图形画成虚线视觉上给用户一个明确暗示图形还没确定。drawShape函数在绘制前会设置笔和刷所以虚线状态需要覆盖掉currentShape里已有的实线画笔样式。保存恢复是必要的否则pen的样式改动会留在 QPainter 对象里。3.3 撤销重做命令模式在绘图软件里的落地画图软件的撤销本质上是「回到上一步操作前的状态」。最省事的实现是每次操作前存一份完整快照但 GUI 里不对齐像素的话很容易出问题。源码包的实现走的是命令模式每个操作都被封装成「记录之前图形列表和之后图形列表」的动作撤销时用之前快照覆盖重做时用之后快照覆盖。class DrawCommand { public: QListShape oldShapes; QListShape newShapes; void undo(CanvasWidget* canvas) { canvas-setShapes(oldShapes); canvas-update(); } void redo(CanvasWidget* canvas) { canvas-setShapes(newShapes); canvas-update(); } };这里用oldShapes和newShapes保存整个图形列表。当用户完成一次绘制后程序里这样操作void CanvasWidget::addShapeWithUndo(const Shape shape) { DrawCommand* cmd new DrawCommand; cmd-oldShapes undoShapes; shapes.append(shape); cmd-newShapes shapes; undoStack.push(cmd); // 清空 redo 栈因为新操作会打断重做链 redoStack.clear(); }这种快照式命令的优点是实现简单对形状数量几百以内的场景性能毫无压力缺点是如果图形数量是几万条线每次快照都会复制整个列表内存和耗时都会明显增加。针对这个源码包的教学定位快照式是最容易懂得等你想做复杂工程时再改成增量式命令只记录每个图元的变化字段也不迟。这里有个潜规则undoShapes必须保存的是深拷贝。Qt 的QListShape在赋值时如果Shape内部的成员变量没有指针会自动深拷贝没问题。可一旦你的Shape类内部有QPixmap成员赋值就会变成隐式共享导致撤销时出现诡异问题。关于这一点第四章会专门讲。4. 常见问题与避坑记录我在绘图软件里踩过的四个坑4.1 导出图片发白或模糊被忽略的设备像素比现象在 2K 或 4K 屏幕上绘制的内容导出成 PNG 后用看图软件打开感觉边缘发虚有的地方有白边。放大看图形边缘像是被切割过。原因Qt 在高 DPI 屏幕上默认启用缩放控件内逻辑坐标只是抽象像素和物理像素不是 1:1 关系。你直接按控件尺寸创建QPixmap实际像素数不够导出到图片时被拉伸自然就糊了。解决导出前判断devicePixelRatio()创建画布时乘上这个系数并把画布本身也设置同样的靠背比。void exportImage(const QString filePath) { qreal dpr devicePixelRatio(); QPixmap exportPix(this-size() * dpr); exportPix.setDevicePixelRatio(dpr); QPainter p(exportPix); // 必须先把背景清掉否则导出整张是黑色 p.fillRect(exportPix.rect(), Qt::white); for (const Shape s : shapes) { drawShape(p, s); } p.end(); exportPix.save(filePath, PNG); }this-size() * dpr是QSize对象和浮点数的乘法结果是每个维度都放大对应倍数。setDevicePixelRatio(dpr)告诉图片自己内在的物理像素密度保存时 Qt 会按这个比例写出真正的像素矩阵。注意fillRect(exportPix.rect(), Qt::white)这一句空白画布默认是透明的很多看图软件把透明显示为黑色或棋盘格看起来就是「发黑」。4.2 鼠标拖动画图时闪烁残影repaint 与 update 的用法区别现象鼠标快速拖动绘制矩形屏幕上留下上一帧的残影图形像被拖出一条条尾巴。严重的时候整个控件在闪。原因paintEvent里没有先行清除背景或者你用了repaint()刷屏。repaint()会立即同步刷新而update()是异步合并刷新。连续repaint()会导致窗口系统频繁重绘配合 QPainter 绘制较慢时就会出现闪烁。解决只调用update()并且在paintEvent第一步先画背景色。void CanvasWidget::paintEvent(QPaintEvent* event) { QPainter p(this); // 用纯白背景覆盖整个控件形成双缓冲效果 p.fillRect(event-rect(), Qt::white); // ... 之后画图形 }这里event-rect()是当前需要重绘的矩形区域比直接用this-rect()效率高。如果你发现只是局部图形在动完全可以用update(rect)只更新一小块区域这是第五章会讲到的性能优化。也是从那以后我在任何 QWidget 自绘组件里都直接禁用repaint()。4.3 窗口缩放后图形错位窗口与视口的设置现象把绘图控件从 800x600 拉大到 1200x900发现之前画的矩形位置偏了正方形变成了长方形原本居中显示的图形跑到左上角。原因QPainter默认把逻辑坐标和物理坐标 1:1 映射窗口大小变了逻辑坐标原点还是在控件左上角图的相对位置自然就变了。想让图形跟随窗口等比缩放就要手动设置窗口和视口。解决在resizeEvent里维护一个固定比例的逻辑坐标系。void CanvasWidget::resizeEvent(QResizeEvent*) { QPainter p(this); p.setWindow(0, 0, 800, 600); // 逻辑区域固定为 800x600 p.setViewport(0, 0, width(), height()); // 物理区域跟随控件大小 }这样设置后后续所有绘制逻辑都在 800x600 的逻辑区域内进行Qt 自动把逻辑区域拉伸到控件实际大小。注意setViewport的单位是物理像素setWindow的单位是逻辑坐标。如果你希望图形绝对不缩放比如绘图软件要真实尺寸那就不要设置窗口只用视口映射一部分。这个坑的核心是搞清楚「逻辑坐标」和「设备坐标」的区别然后选择适合你产品的映射策略。4.4 撤销历史恢复失败QPixmap 隐式共享是个大坑现象画几个矩形后执行撤销结果画面没有变化再撤销一次突然跳回很旧的状态。检查命令栈发现数据确实在栈里但undo()时canvas-setShapes(oldShapes)没有生效。原因Shape类如果包含QPixmap成员而你在构造函数或赋值时直接用了QPixmap的拷贝Qt 的隐式共享只会增加引用计数两个对象指向同一块缓冲。当你之后在paintEvent里用 QPainter 修改其中一个图的像素时另一个对象也悄悄变了。所以命令栈里的oldShapes和newShapes实际共享了底层的像素数据恢复也就失去了意义。解决使用QPixmap::copy()做深拷贝或者在Shape类的拷贝构造函数里显式调用copy()。Shape::Shape(const Shape other) : pixmap(other.pixmap.copy()), // 深拷贝断开共享 rect(other.rect), pen(other.pen), brush(other.brush) {}如果Shape里存的是快照形式的像素图深拷贝后每次赋值都会独立分配内存。这就是为什么第四章开头建议在设计Shape时不要轻易存QPixmap而是把绘制参数存下来到paintEvent时才实时绘制。这样不仅省内存也彻底绕开了隐式共享的坑。5. 把演示代码变成可维护的工程目录结构、性能与打包5.1 源码目录结构分离界面与绘制逻辑很多 Qt 教程倾向于把所有代码塞进main.cpp和widget.cpp前期方便但功能一多就难维护。这个源码包的目录结构是我个人比较推荐的小型工程组织方式一个形状类、一个画布控件、一个主窗口、一组绘制工具。project/ ├── main.cpp ├── mainwindow.h/.cpp # 主窗口负责菜单栏、工具栏、状态栏 ├── canvaswidget.h/.cpp # 画布控件负责鼠标事件与 paintEvent ├── shape.h/.cpp # 形状数据类描述图元的类型、位置、样式 ├── command.h/.cpp # 撤销重做命令类 └── resources/ ├── icons.qrc # 工具栏图标资源 └── images/ # 图标文件shape.h里定义的是一个纯数据类不包含任何 Qt 界面头文件这样它可以在非 GUI 测试中直接使用。canvaswidget只关心如何绘制QListShape以及如何把鼠标事件转成新的Shape。mainwindow负责把工具状态传给canvaswidget。分层的边界很明确数据、逻辑、表现各自独立。我一般会在CanvasWidget里增加一个setTool(int)方法接收来自工具栏的data()值然后在内部维护一个currentTool。这样主窗口不知道绘图细节画布也不需要知道自己被谁控制整个调用链很短后续加新工具只需要扩展Shape类型和drawShape函数。5.2 绘制性能优化区域重绘与缓存层当画布上有几百个图形时每次update()全量重绘可能会导致鼠标拖动不跟手。优化手段是只重绘包含新图形的局部区域。Qt 的QWidget::update(const QRect)允许只请求某一矩形区域重绘paintEvent里收到的event-rect()就是这些局部区域的并集。void CanvasWidget::mouseMoveEvent(QMouseEvent* event) { if (isDrawing) { // 计算之前矩形和当前矩形并集避免残留 QRect oldRect currentShape.boundingRect().adjusted(-2, -2, 2, 2); currentShape.setEnd(event-pos()); QRect newRect currentShape.boundingRect().adjusted(-2, -2, 2, 2); update(oldRect.united(newRect)); } }boundingRect()返回图形的外接矩形adjusted(-2, -2, 2, 2)给矩形四周加上 2 像素的余量防止线宽较宽时边缘被裁掉。united求两个矩形的并集这样重绘区域覆盖了图形从旧位置到新位置的所有部分不会留下残影。注意这里adjusted的余量要根据当前画笔的最大线宽来定比较保险的做法是加上pen.width()的值。如果图形数量非常多还可以引入缓存层把静态背景先绘制到一个 QPixmap每次重绘时只把这个背景像素图贴上去再把正在拖动的图形画在上面。这对于图层类绘图软件来说几乎是必备技巧。void CanvasWidget::paintEvent(QPaintEvent* event) { QPainter p(this); p.drawPixmap(0, 0, backgroundCache); // 快速贴出缓存 p.save(); p.setPen(QPen(Qt::DashLine)); if (isDrawing) drawShape(p, currentShape); p.restore(); }后台维护backgroundCache的方式是当图形列表发生变化时用离屏QPainter把所有shapes画一遍。你也可以把这个思路理解成「双缓冲」的升级版——双缓冲解决闪烁缓存层解决重复绘制耗时。5.3 打包发布把绘图软件变成可独立运行的程序源码包的文档里提到了 Qt 程序的部署方法这一步是很多初学者拿到代码后卡住的地方。在 Windows 下用windeployqt把动态库和插件拷贝到可执行文件目录mkdir release cd release cmake .. -DCMAKE_BUILD_TYPERelease cmake --build . --config Release # 将可执行文件复制到 release 目录后执行 windeployqt drawingapp.exewindeployqt会扫描可执行文件的导入表自动拷贝 QWidget 平台插件qwindows.dll、字体引擎、图片格式插件等。常见的问题是漏掉platforms目录导致双击 exe 报「Could not find or load the Qt platform plugin “windows”」。解决办法是重新执行windeployqt或者手动检查release/platforms里是否存在qwindows.dll。另外源码包里的图标是通过resources/icons.qrc打包进可执行文件的。如果你要在自己的工程里引用这些图标注意.qrc里的路径用的是资源前缀方式比如:/icons/rectangle.png不要写成相对路径否则窗口运行时找不到文件。6. 进阶给绘图软件加上自定义图元和 SVG 导出当基本绘图功能跑通后下一步自然是加入自己的图元类型。源码包里预留了扩展点Shape类里有一个type字段drawShape函数里通过switch分发绘制逻辑。我建议不要再去扩展这个 switch而是把Shape改成继承结构基类提供纯虚函数draw()和boundingRect()每种图元一个子类。这样新加箭头、星形、文字都不需要改动既有代码。class ShapeBase { public: virtual ~ShapeBase() {} virtual void draw(QPainter p) const 0; virtual QRect boundingRect() const 0; virtual ShapeBase* clone() const 0; }; class ArrowShape : public ShapeBase { public: QLineF line; void draw(QPainter p) const override { p.drawLine(line); // 根据 line 角度计算箭头端点画两条线段作为箭头 QLineF n1 line.normalVector(); qreal angle 30 * 3.1415926 / 180; // 具体箭头端点计算略这是留给你的练习 } QRect boundingRect() const override { return QRect(line.p1().toPoint(), line.p2().toPoint()).normalized(); } };这里clone()函数用来实现撤销快照时的深拷贝比重写拷贝构造函数更灵活。boundingRect()用于局部重绘和碰撞检测。完成新图元后只需在工具栏加一个 Action并把对应的ShapeBase子类对象传入到CanvasWidget就能复用鼠标绘制和撤销重做的全部逻辑。导出 SVG 是另一个提升作品可用性的功能。Qt 自带的QSvgGenerator可以在不引入额外库的情况下输出矢量图非常适合把绘制的图形存成可无限缩放的格式。void exportSvg(const QString filePath) { QSvgGenerator generator; generator.setFileName(filePath); generator.setSize(QSize(800, 600)); generator.setViewBox(QRect(0, 0, 800, 600)); QPainter p(generator); for (const ShapeBase* s : shapes) { s-draw(p); } p.end(); }QSvgGenerator构造出的 QPainter 会有一些限制不支持抗锯齿之外的某些混合模式不支持复杂的渐变但基础线段、矩形、椭圆都没问题。导出 SVG 后用浏览器打开就能缩小放大不糊。注意 QSvgGenerator 默认的坐标原点在左上角如果你之前的坐标系经过翻转导出前要在绘制函数里做同样的变换。从那以后我每次做 Qt 自绘项目都强制把「形状数据、绘制函数、交互事件」三件事分开写新功能只加数据类和绘制分支不动事件逻辑。这套源码包的价值也就在这里它不只是教你怎么画图而是教你怎么把一个画板需求合理地拆成代码。希望这种风格能帮你少走一点我当时走过的弯路。记住拿到源码后自己动手敲一遍把矩形改成圆角矩形、把虚线改成点线、把普通导出改成 SVG 导出——每改一处你才算真正接住了这份教程里的知识点。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑