Qt绘图软件核心开发:架构、交互与性能优化指南
简介一套基于Qt 2D绘图功能的教程与配套源码适合希望掌握QPainter绘图及图形交互的C/Qt初学者。包内含完整的paintExample示例工程覆盖主窗口构建、绘图区域定制、鼠标按下/移动/释放事件处理、颜色与画笔工具选择、绘图保存与加载等关键模块通过源码可直观理解QPainter丰富的绘图方法、QPaintEvent重绘机制、QGraphicsView/QGraphicsScene高级图形交互以及Qt Creator中项目的组织与调试方式。通过阅读与调试可掌握QPen/QBrush属性调节、图像格式保存等技巧并快速搭建可交互的绘图界面。压缩包共计114个文件包括15个cpp、18个h、7个ui、52个png以及1个pdf文档分别对应工程逻辑、界面设计、运行截图和辅导说明整体大小3.54MB便于快速下载并在Qt Creator中直接打开运行。该资源由yafeilinux整理已有704人学习浏览内容精炼且贴近实战适合作为Qt绘图应用开发的入门指引与项目参考。1. Qt 编写绘图软件从一条线到完整工具链接到做一个绘图软件的需求时很多人第一反应是找个现成的画图控件凑合可一旦涉及自定义画笔、图层、撤销重做Qt 编写绘图软件就变成了一个真正需要认真设计的工程方向。这篇文章要拆的不是用 QPainter 画一张静态图的 Demo而是从架构到源码把画布交互、图元存储、命令回退这些核心模块逐层讲透。你会发现一个反直觉的事实QPainter 本身是没有状态的但配合事件系统和命令模式它能支撑起一个不像玩具的绘图工具。适合正在做白板、流程图编辑器、简易 CAD 或者任何需要自绘控件的开发者也适合想弄明白如何把一条线变成一个软件的人。2. 动手前先定架构用 QWidget 自绘还是 Graphics View 框架2.1 三种常见绘图实现路线与选型对照Qt 里做绘图软件绕不开三条路线。第一条是 QLabel 加 QPixmap 涂鸦鼠标移动时直接在像素图上画线实现最简单但图元无法独立选中和编辑连画个矩形要擦掉重画。第二条是 QWidget 子类重写 paintEvent 和鼠标事件用 QPainter 直接绘制这是最灵活也最可控的方式适合中小型画布代码可以按业务逻辑分层。第三条是 QGraphicsView/QGraphicsScene 框架它内置了图元 item、碰撞检测、框选和视图缩放适合大型场景但学习曲线明显更陡很多概念初次接触容易绕进去。我一般会按这个表格快速选型路线核心机制适用场景性能上限复杂度QLabel QPixmap直接改像素随手涂鸦、批注很低图元不可管理低QWidget 自绘paintEvent QPainter通用绘图软件、白板、简单编辑器几千个图元以内流畅中QGraphicsView 框架scene item大型流程图、CAD、拖拽频繁高适合海量图元高如果目标是做一个通用绘图软件比如能画线、矩形、文字能拖动、能撤销我会首选 QWidget 自绘。原因很直接逻辑可控序列化简单不会因为框架自带的行为和业务冲突。只有当图元数量可能超过几千个或者需要频繁做命中测试和框选时再切换到 Graphics View。2.2 建立工程目录把源码、资源和文档分开选型定了工程结构也要跟上。很多刚入门的人会把所有类都塞进 main.cpp画布逻辑、工具逻辑、主窗口全揉在一起改需求时痛不欲生。常见做法是按下述结构组织源码特别是当你想把教程和源码真正落到团队协作时工程目录就是地基。paint_app/ ├── CMakeLists.txt ├── src/ │ ├── canvas/ # 画布控件、绘制引擎封装 │ ├── items/ # 图元类线条、矩形、椭圆、路径 │ ├── commands/ # 撤销重做命令 │ ├── tools/ # 工具状态管理 │ └── mainwindow.cpp ├── resources/ │ └── icons/ # 工具栏图标 └── docs/ # 接口说明、决策记录每个目录的职责要清晰canvas 里只放与绘制显示相关的类items 里是图形数据对象commands 放 QUndoCommand 子类。资源文件单独放避免后续打包时资源路径丢失docs 里记录关键选型理由比如为什么选 QWidget 自绘。工程构建推荐用 CMake 而不是 qmakeQt Creator 新建项目时选择 CMake后续跨平台和接入 CI 都更方便。一个最简 CMakeLists 可以这样写cmake_minimum_required(VERSION 3.16) project(PaintApp VERSION 0.1 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(Qt6 COMPONENTS Widgets REQUIRED) add_executable(paint_app src/main.cpp src/canvas/drawing_canvas.cpp src/items/draw_item.cpp src/commands/add_item_command.cpp ) target_link_libraries(paint_app PRIVATE Qt6::Widgets)这里把 Qt6 作为默认依赖如果你还在用 Qt5把 find_package 改成 Qt5 即可。源码目录和资源目录的分开核心目的是让加一个新图元这个操作只改 items 目录而不碰画布和主窗口。2.3 绘制引擎的核心抽象画布、图层、命令很多自绘项目失败是因为把所有状态都堆在 paintEvent 里一个 QList 存点一堆临时变量存颜色结果一旦要撤销或保存无从下手。正确的抽象是把数据和渲染分开。绘图软件的核心数据结构是一份文档它保存的是图元列表而不是已经画好的像素。// draw_item.h 图元基类 class DrawItem { public: virtual ~DrawItem() default; virtual void draw(QPainter painter) const 0; virtual QJsonObject serialize() const 0; virtual void setPenColor(const QColor color) { m_penColor color; } QColor penColor() const { return m_penColor; } protected: QColor m_penColor{Qt::black}; qreal m_penWidth{1.0}; };// canvas_document.h 文档模型 class CanvasDocument : public QObject { Q_OBJECT public: void addItem(std::shared_ptrDrawItem item); void removeItem(const std::shared_ptrDrawItem item); int itemCount() const { return m_items.size(); } signals: void contentChanged(); private: std::vectorstd::shared_ptrDrawItem m_items; };这里有几个关键设计用 shared_ptr 管理图元生命周期不需要手写 delete而且分开保存和渲染很安全DrawItem 是纯虚基类所有具体图元都实现 draw() 和 serialize()这样新增矩形、椭圆时不需要改动文档结构。CanvasDocument 通过 contentChanged 信号通知画布刷新画布只负责遍历图元并调用 draw()。这样做的代价是多写一层接口但收益非常大撤销重做命令可以以添加/删除图元为操作粒度保存文件时只需遍历序列化加载文件时按类型反序列化。后续要加图层只需要在文档里增加图层列表每个图层持有自己的图元列表。可以说绘图软件的功能扩展全靠这个数据模型撑着。3. 跑通最小绘图程序画布、鼠标事件和一条线3.1 最小可用代码用 QPainter 实现自由绘制架构再漂亮也要先从一条线跑通。下面这份代码是一个最小可用的自由绘制画布它继承 QWidget用 QPixmap 作为临时缓冲区鼠标按下、移动、抬起时把线画到缓冲区上再通过 update() 触发重绘。你可以直接把这两段代码放到 Qt 工程里编译。// drawing_canvas.h class DrawingCanvas : public QWidget { Q_OBJECT public: explicit DrawingCanvas(QWidget* parent nullptr); protected: void paintEvent(QPaintEvent* event) override; void mousePressEvent(QMouseEvent* event) override; void mouseMoveEvent(QMouseEvent* event) override; void mouseReleaseEvent(QMouseEvent* event) override; void resizeEvent(QResizeEvent* event) override; private: QPixmap m_pixmap; QPoint m_lastPoint; QColor m_penColor{Qt::black}; qreal m_penWidth{2.0}; bool m_drawing{false}; };// drawing_canvas.cpp #include drawing_canvas.h #include QPainter #include QMouseEvent DrawingCanvas::DrawingCanvas(QWidget* parent) : QWidget(parent) { setAttribute(Qt::WA_OpaquePaintEvent, true); setMouseTracking(true); } void DrawingCanvas::resizeEvent(QResizeEvent* event) { QPixmap newPixmap(event-size()); newPixmap.fill(Qt::white); QPainter painter(newPixmap); painter.drawPixmap(0, 0, m_pixmap); m_pixmap newPixmap; QWidget::resizeEvent(event); } void DrawingCanvas::mousePressEvent(QMouseEvent* event) { if (event-button() Qt::LeftButton) { m_lastPoint event-pos(); m_drawing true; event-accept(); } } void DrawingCanvas::mouseMoveEvent(QMouseEvent* event) { if (m_drawing (event-buttons() Qt::LeftButton)) { QPainter painter(m_pixmap); painter.setRenderHint(QPainter::Antialiasing, true); QPen pen(m_penColor, m_penWidth, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin); painter.setPen(pen); painter.drawLine(m_lastPoint, event-pos()); m_lastPoint event-pos(); update(); } } void DrawingCanvas::mouseReleaseEvent(QMouseEvent* event) { if (event-button() Qt::LeftButton) { m_drawing false; event-accept(); } } void DrawingCanvas::paintEvent(QPaintEvent*) { QPainter painter(this); painter.drawPixmap(0, 0, m_pixmap); }这段代码的逻辑很直白按下左键时记录起始点并进入绘制状态移动时判断是否处于绘制状态且左键仍按住然后从上一个点画线到当前点抬起左键时结束绘制。注意 mouseMoveEvent 里用的是event-buttons() Qt::LeftButton而不是m_drawing一个条件这样能避免悬停时误画线。paintEvent 只负责把缓冲区像素整体贴到窗口不做任何额外计算所以重绘性能很高。这里有几个参数要说清楚m_penWidth我选 qreal 类型因为 QPen::setWidthF 支持浮点线宽比如 0.5、2.5可以实现更细腻的笔触QPen 构造第三个参数Qt::SolidLine是线型改成 Qt::DashLine 就能画出虚线后面两个参数分别是笔帽样式和连接样式设为 RoundCap 和 RoundJoin 是为了让线条端点和拐角平滑不会出现刺眼的直角。3.2 鼠标事件的三个关键点按下、移动、抬起鼠标事件是绘图交互的核心新手最常踩坑的是为什么鼠标动的时候没反应。第一个关键点是除了重新实现 mousePressEvent如果你要在不按下按钮时也收到移动事件必须调用setMouseTracking(true)。第二个关键点是区分event-pos()和event-globalPos()pos() 返回的是相对于当前窗口的坐标适合画布绘制globalPos() 返回屏幕坐标适合实现跨画布工具。第三个关键点按下事件需要 accept但大多数情况下 QWidget 对鼠标事件默认是 accept 的真正会吞事件的是父控件或覆盖在画布上的其他 QWidget 子控件。当画布上还有其他控件时比如属性面板叠加在画布上需要用事件过滤器来拦截。常见做法是在画布外层的容器里安装 eventFilter但这会让你陷入到底谁在处理事件的泥潭。我的经验是绘图画布一定要保持在窗口层级的最上层或者独占地使用整个区域这样三个鼠标事件方法就能简单可靠地工作。3.3 用 QPen 控制线宽、颜色和抗锯齿的参数说明QPen 是绘图软件里最常用的画笔对象它决定了每根线条的视觉表现。除了线宽、颜色还需要理解它的样式和渲染参数。QPen 参数常用取值作用colorQColor线条颜色支持透明度widthF1.0 ~ 20.0浮点线宽避免整数缩放锯齿styleSolidLine / DashLine / NoPen线型NoPen 是不绘制边框capStyleRoundCap / FlatCap / SquareCap线端形状RoundCap 适合手绘joinStyleRoundJoin / BevelJoin / MiterJoin折线拐角形状代码里这样设置一支画笔QPen pen; pen.setColor(QColor(30, 80, 160, 200)); pen.setWidthF(2.5); pen.setStyle(Qt::DashLine); pen.setCapStyle(Qt::RoundCap); pen.setJoinStyle(Qt::RoundJoin); painter.setPen(pen);需要特别注意的是抗锯齿QPainter::Antialiasing必须在绘制开始前通过 setRenderHint 开启它影响后续所有绘制操作。缺点是抗锯齿会引入轻微模糊如果画的是要求像素级精确的缩略图可以临时关闭。还有一个容易被忽略的细节QPen 默认样式是 SolidLine但如果你 setWidthF(0.0)QPainter 会绘制一条极细的 cosmetic 线宽度始终是 1 物理像素适合做光标十字线。这个特性在实现对齐辅助线时非常好用。4. 把功能做全形状绘制、属性面板、撤销与重做4.1 用 QPainterPath 画矩形、椭圆和箭头坐标计算自由绘制只是起点真正的绘图软件要支持规则形状。用 QPainterPath 可以把矩形、椭圆、多边形统一成一条可绘制的路径并且方便存储到 DrawItem 里。常见做法是让用户拖拽时实时构造一个新图元并用 QPainterPath 保存它。QRectF rect QRectF(startPoint, currentPoint).normalized(); QPainterPath path; path.addRect(rect); path.addEllipse(rect);这里normalized()很关键它保证无论用户从左上拖到右下还是右下拖到左上得到的矩形都是正的。如果不调用 normalized负宽度会让部分绘制逻辑出错。椭圆和矩形共享同一个 QRectF所以拖拽的预览逻辑可以复用。箭头是另一个高频需求画箭头不能只是画一条线需要在线的终点处画出箭头三角。核心是用 atan2 算出直线的角度然后按角度偏移生成箭头的两个侧翼点。QPointF start{10, 10}, end{200, 120}; double angle std::atan2(end.y() - start.y(), end.x() - start.x()); double arrowLen 12.0; double arrowAngle M_PI / 6; QPointF arrowP1 end - QPointF(arrowLen * std::cos(angle - arrowAngle), arrowLen * std::sin(angle - arrowAngle)); QPointF arrowP2 end - QPointF(arrowLen * std::cos(angle arrowAngle), arrowLen * std::sin(angle arrowAngle)); QPainterPath arrowPath; arrowPath.moveTo(end); arrowPath.lineTo(arrowP1); arrowPath.moveTo(end); arrowPath.lineTo(arrowP2);参数说明arrowLen 是箭头侧翼长度12 像素在屏幕上是比较合适的值arrowAngle 是侧翼与主线的夹角30 度看起比较协调角度太大显得笨重太小看不清楚。箭头的填充方式通常是把 arrowP1 和 arrowP2 连起来形成一个闭合三角形再用painter.setBrush(Qt::white)填充这样箭头中间不会被线条覆盖。4.2 用命令模式实现撤销/重做QUndoStack 的使用绘图软件的撤销/重做用 Qt 自带的 QUndoStack 比自己维护历史列表稳妥得多。QUndoStack 基于命令模式每个操作封装成一个 QUndoCommand 子类push 进栈时执行 redo撤销时执行 undo。下面给一个添加图元的命令class AddItemCommand : public QUndoCommand { public: AddItemCommand(CanvasDocument* doc, std::shared_ptrDrawItem item, QUndoCommand* parent nullptr) : QUndoCommand(parent), m_doc(doc), m_item(item) { setText(QStringLiteral(Add %1).arg(item-typeName())); } void undo() override { m_doc-removeItem(m_item); } void redo() override { m_doc-addItem(m_item); } private: CanvasDocument* m_doc; std::shared_ptrDrawItem m_item; };使用方式是在画布完成一次图元创建后QUndoStack* undoStack new QUndoStack(this); undoStack-push(new AddItemCommand(document, item));这里有个关键点push 之后不能再手动调用 document-addItem否则会重复添加因为 push 内部已经执行了一次 redo。很多初学者在这里吃了亏在 push 外面又调了一遍 addItem导致图元出现两个副本。QUndoStack 还有几个实用设置setUndoLimit(1000)限制历史步数防止内存随操作无限膨胀setActive(true)放在窗口获取焦点时确保 CtrlZ 生效。命令对象里保存的是 shared_ptr而不是裸指针或索引这样即使文档里的图元被删除命令中持有的对象仍然存活不会出现悬空指针。4.3 属性面板与画布联动信号槽传递链有了命令系统和文档模型属性面板就变简单了。核心链路是画布发射当前选中图元变化信号主窗口把这个变化传递给属性面板属性面板修改颜色、线宽后再直接修改图元对象并通知画布刷新整个过程通过信号槽连接。// 画布中 class DrawingCanvas : public QWidget { Q_OBJECT signals: void itemSelected(std::shared_ptrDrawItem item); }; // 主窗口 connect(canvas, DrawingCanvas::itemSelected, this, MainWindow::onItemSelected);void MainWindow::onItemSelected(std::shared_ptrDrawItem item) { propertyPanel-loadItem(item); } // 属性面板 void PropertyPanel::onColorChanged(const QColor color) { if (m_item) { m_item-setPenColor(color); emit itemModified(); } }属性面板里不需要知道画布的任何细节它只操作 DrawItem 的 setter然后通过 itemModified 信号让画布 update。如果图元的属性修改涉及命令历史比如颜色修改也要能撤销那就不应该直接 set而是创建 UpdatePropertyCommand。我的做法是高频操作画线、添加形状走命令栈低频操作选中后改颜色先用标签页的应用按钮提交一次命令这样既能撤销又不会每个颜色微调都堆积历史。4.4 文件保存与读取QJsonDocument 序列化图纸图纸保存是绘图软件的基本功能。用 QJsonDocument 比自定义二进制格式更稳健也方便调试。每个 DrawItem 子类实现 serialize() 和静态的 deserialize()文档负责汇总。QJsonObject LineItem::serialize() const { QJsonObject obj; obj[type] line; obj[start] QJsonArray{ m_start.x(), m_start.y() }; obj[end] QJsonArray{ m_end.x(), m_end.y() }; obj[color] m_color.name(QColor::HexArgb); obj[width] m_width; return obj; }std::shared_ptrDrawItem DrawItem::fromJson(const QJsonObject obj) { const QString type obj[type].toString(); if (type line) { auto item std::make_sharedLineItem(); auto start obj[start].toArray(); item-setStart(QPointF(start[0].toDouble(), start[1].toDouble())); item-setPenColor(QColor(obj[color].toString())); item-setPenWidth(obj[width].toDouble()); return item; } // 其他类型类似 return nullptr; }序列化时一定要保存版本号字段比如obj[version] 1;以后格式升级时可以兼容旧文件。颜色字段我建议用QColor::HexArgb格式也就是#AARRGGBB这样透明度信息不丢如果用color.name()透明度会被丢掉导出的文件再打开颜色就变了。保存整个文档时遍历所有图元的 serialize 放进一个 QJsonArray再包一层文档元数据。这种实现的好处是你可以在没有任何 Qt 界面参与的情况下单元测试序列化结果也方便以后做自动备份。5. Qt 绘图软件避坑指南这几个坑我踩了不止一次5.1 鼠标事件失效到底该重写 mousePressEvent 还是 eventFilter现象重写了 mousePressEvent但点击画布完全没有反应或者事件被无端吞掉。原因最常见的是画布上叠了其他透明子控件比如一个覆盖全屏的 QRubberBand 或空标签导致事件被拦截另一个常见原因是你在非 QWidget 类上想通过 eventFilter 处理鼠标但过滤对象或安装位置不对。还有一部分情况是画布设置了Qt::WA_TransparentForMouseEvents属性整个控件对鼠标透明。解决先确认你的画布是 QWidget 的直接子类并且没有其他子控件覆盖。如果只是想在主窗口级别统一拦截再考虑 eventFilter。给画布设置setFocusPolicy(Qt::StrongFocus)能避免鼠标点击后焦点跑掉导致键盘快捷键失效。最可靠的做法是把画布放在窗口中心并raise()到最顶层让三个 mouseEvent 方法成为事件处理的唯一入口。5.2 绘制闪烁与重影开启 QPainter 双缓冲的正确姿势现象快速拖动鼠标画线时窗口闪烁或者出现红色的重影残像。原因Qt 的 QWidget 默认有后台缓冲但如果你在 paintEvent 里没有完全覆盖整个画布区域旧画面就会暴露出来另外很多人习惯直接在 paintEvent 里绘制所有历史轨迹意味着每次鼠标移动都要重放几百条线段性能不够就闪烁。解决把绘制结果缓存在 QPixmap 中paintEvent 里只做一次painter.drawPixmap(0, 0, m_pixmap)。鼠标移动时只把新线段画进 QPixmap然后update()。同时设置setAttribute(Qt::WA_OpaquePaintEvent, true)告诉 Qt我的绘制会覆盖整个窗口这样 Qt 就不必先填充背景能减少闪烁。void DrawingCanvas::paintEvent(QPaintEvent* event) { QPainter painter(this); painter.drawPixmap(0, 0, m_pixmap); }注意当窗口 resize 时QPixmap 要重新创建并把旧内容拷贝过去否则窗口拉大后新区域是黑的。这段逻辑我在 3.1 的 resizeEvent 中已经给出它是双缓冲方案里必不可少的一部分。5.3 高分辨率屏幕下坐标错位devicePixelRatio 处理现象在 2K/4K 屏幕或 Windows 150% 缩放下画出的线比鼠标位置偏移导出图片尺寸不对。原因Qt 5.6 以后默认启用高 DPI 缩放逻辑坐标和物理坐标不再一一对应。QPixmap 默认的 devicePixelRatio 是 1而窗口可能是 1.5 或 2.0。如果不做处理鼠标的 pos() 是逻辑坐标而画到 pixmap 上却按物理像素位置两者错位。解决创建缓冲区 QPixmap 时把尺寸乘以当前窗口的 devicePixelRatio 值并为 pixmap 设置相同的 ratio然后绘制时继续使用逻辑坐标。qreal ratio devicePixelRatio(); QPixmap newPixmap(size() * ratio); newPixmap.setDevicePixelRatio(ratio); newPixmap.fill(Qt::white); QPainter painter(newPixmap);这样 QPainter 内部会自动把逻辑坐标转换到物理像素。如果你的导出功能要输出固定尺寸的图片也要显式设置目标 QImage 的分辨率不要直接凭感觉缩放。这个问题的隐蔽性在于低分屏上永远测不出来只有换到高分屏才暴露。5.4 源码编译不过Qt 5 与 Qt 6 的 API 差异现象同一份源码在 Qt 5.15 上编译通过换到 Qt 6.4 就报错常见的报错是QString::SkipEmptyParts is not a member或QMouseEvent::pos() is deprecated。原因Qt 6 移除了大量过时 API字符串枚举改为作用域枚举Qt::SkipEmptyParts鼠标事件的坐标访问也改为position()返回 QPointF。另外qAsConst改成了std::as_const。解决在新代码里直接用 Qt 6 写法需要兼容两个大版本时用宏做版本屏蔽。例如#if QT_VERSION QT_VERSION_CHECK(6, 0, 0) const QPoint pos event-position().toPoint(); #else const QPoint pos event-pos(); #endif我自己的习惯是新建工程直接锁定 Qt 6不再兼容 Qt 5 的过时写法。如果你维护的是老工程至少要保证所有 API 都在 Qt 5 和 Qt 6 的公共子集内。编译错误是这类问题最友好的提示真正麻烦的是那些在 Qt 5.15 标记 deprecated、在 Qt 6 才删除的接口最好在工程里开启QT_DEPRECATED_WARNINGS提前解决。6. 让绘图软件更专业性能优化和自定义图元6.1 用 QGraphicsScene 重写大规模图形变流畅当图元数量上升到几千个QWidget 自绘的逐个遍历会开始掉帧。这时候值得迁移到 QGraphicsScene/QGraphicsView。核心是把 DrawItem 改成继承 QGraphicsItem 的类重写boundingRect()和paint()然后把 item 加入 sceneview 负责显示。迁移时第一件事是定义准确的 boundingRect它决定 Qt 的绘制裁剪区域太大浪费性能太小会出现图形画不完整的现象还会和 item 的选中高亮框打架。QRectF MyItem::boundingRect() const { return QRectF(-10, -10, 20, 20); // 以 item 本地坐标原点为中心 }性能优化还要注意不要在 item::paint() 里做复杂计算painter 的样式设置也尽量缓存如果 item 数量多且形状简单可以开启 view 的setViewportUpdateMode(QGraphicsView::BoundingRectViewportUpdate)减少重绘面积。6.2 把图纸导出成图片或 PDF两个必须设置的参数导出功能是最容易被验收的部分却常常在最后时刻翻车。常见做法是用 QPainter 把文档渲染到 QImage 或 QPdfWriter。下面这一步经常被忽视输出图片前一定要填充背景色否则有透明通道的图元会在导出后变成黑底或透明底。QImage image(targetSize, QImage::Format_ARGB32); image.fill(Qt::white); // 必须否则导出透明 image.setDevicePixelRatio(ratio); // 保证缩放正确 QPainter painter(image); document-render(painter); // 按文档模型绘制另一个必须设置的参数是 PDF 分辨率QPrinter或QPdfWriter的 setResolution() 不设置默认值会导出一个画面模糊的文件。我一般设为 300 DPI打印清晰文件大小也可接受。导出的尺寸计算要统一基于逻辑坐标不要在渲染过程中混用物理像素和逻辑像素。这么多年下来我最大的感悟是绘图软件的复杂度不在某个单点而在数据模型、交互事件、渲染三者的解耦。早期我把所有逻辑堆在 paintEvent 里每加一个功能都要把整个绘制流程翻一遍。后来坚持用文档、图元、命令三层结构加新工具只需要写一个新 DrawItem 子类加一个新命令画布几乎不动。希望你也能从最小可运行版本开始一步一步把架构搭稳踩过的坑都变成代码里的防御逻辑。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

Django+Vue全栈电商平台架构与AI代理集成实践

Django+Vue全栈电商平台架构与AI代理集成实践

1. 项目概述:全栈电商平台的技术架构解析这个基于DjangoVue的在线购物平台设计,本质上是一个融合了前后端分离架构与数据分析能力的全栈电商解决方案。作为计算机专业毕业设计的优质选题,它完整覆盖了现代Web开发的三大核心层:后端…

2026/10/14 12:24:33 阅读更多 →
用友U8 CO组件开发采购到货单增删改审接口

用友U8 CO组件开发采购到货单增删改审接口

简介:本资源是面向用友U8二次开发工程师与ERP系统集成开发者的技术实践包,聚焦CO(Component Object)方式下采购到货单的增删改审接口开发,解决企业供应链业务与外部系统(如WMS、MES)实时对接中数…

2026/10/14 12:24:33 阅读更多 →
Artcraft项目从零到一:数字生成与实体手作全流程指南

Artcraft项目从零到一:数字生成与实体手作全流程指南

1. 从“artcraft”这个词说起:它到底指什么 第一次看到“artcraft”这个标题,我脑子里蹦出来的不是某个具体工具,而是一类东西的统称。拆开看,art 是艺术、美术、审美表达,craft 是手艺、工艺、动手制作。合在一起&…

2026/10/14 12:23:32 阅读更多 →

最新新闻

汽车行业技术动态与供应链挑战解析

汽车行业技术动态与供应链挑战解析

1. 汽车行业技术动态与供应链挑战全景观察最近汽车行业有三件大事值得关注:奇瑞部分车型被曝存在跑偏问题、特斯拉Terafab芯片项目即将投产、美国ICTS规则豁免期结束对网联汽车供应链的影响。这三个看似独立的事件,实际上反映了当前汽车产业在产品质量控…

2026/10/14 13:10:09 阅读更多 →
AI论文降重工具成本与效果实测分析

AI论文降重工具成本与效果实测分析

1. 论文降重成本核算背景在学术写作领域,AI降重服务正逐渐成为刚需。去年某高校抽检中,38%的硕士论文被查出AI辅助痕迹过重,直接催生了专业降重市场。面对动辄上万字的学位论文,手动改写耗时费力,而AI工具能在保证语义…

2026/10/14 13:10:09 阅读更多 →
卧室游戏厅搭建指南:投影仪玩PS5的完整方案与避坑清单

卧室游戏厅搭建指南:投影仪玩PS5的完整方案与避坑清单

把 PS5 从客厅搬进卧室,这事儿我琢磨了很久。电视被家里人占着看剧,游戏机放在那儿吃灰,换谁心里都痒。后来我花了两千不到,添了一台哈趣 Q1 Pro Max,直接在卧室白墙上投出一百多寸的大画面,PS5 接上去打了…

2026/10/14 13:10:09 阅读更多 →
龙旗科技港股IPO解析:募资用途与业务结构

龙旗科技港股IPO解析:募资用途与业务结构

1. 龙旗科技IPO核心信息速览龙旗科技于2024年1月正式启动港股IPO进程,计划发行3.425亿股(含超额配售),每股发行价4.55-5.85港元,预计募资15.58-20.03亿港元(约合人民币14.3-18.4亿元)。公司将于…

2026/10/14 13:10:09 阅读更多 →
基于OpenCode构建现代化书签管理工具

基于OpenCode构建现代化书签管理工具

1. 项目背景与核心价值网页书签管理是每个互联网用户都会遇到的日常需求。随着浏览的网页越来越多,传统的浏览器书签功能逐渐暴露出几个痛点:无法跨设备同步、分类管理困难、搜索效率低下、缺乏可视化展示。这正是我们需要用OpenCode构建一个现代化书签管…

2026/10/14 13:10:09 阅读更多 →
二分查找边界定位算法详解与应用

二分查找边界定位算法详解与应用

1. 问题背景与需求分析在数据处理和算法应用中,有序数组的二分查找是最基础也最经典的场景之一。但实际工程中,我们往往不仅需要判断目标值是否存在,还需要精确获取其出现的边界位置。比如在日志时间戳查询、用户行为分析等场景,统…

2026/10/14 13:09:09 阅读更多 →

日新闻

AnyPS5通用适配层设计:兼容性与性能的工程取舍

AnyPS5通用适配层设计:兼容性与性能的工程取舍

1. 从"AnyPS5"这个名字说起:它到底想解决什么问题第一次看到"AnyPS5"这个标题,我脑子里冒出来的第一个念头是:这大概率是一个围绕"跨平台运行"或者"通用适配"做文章的项目。名字里的"Any"…

2026/10/14 0:00:41 阅读更多 →
Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

1. 项目概述:Flutter跨平台二手交易应用开发作为一名长期从事移动应用开发的工程师,我最近完成了一个基于Flutter框架的二手物品交易平台项目。这个项目最吸引我的地方在于它完美结合了Flutter的跨平台优势与HarmonyOS的生态特性,实现了"…

2026/10/14 0:00:41 阅读更多 →
DFS算法解决飞机降落调度问题实战

DFS算法解决飞机降落调度问题实战

1. 项目背景与核心思路飞机降落问题是一个经典的调度优化场景,也是算法竞赛和面试中的高频题目。这个题目要求我们合理安排多架飞机的降落顺序和时间,确保所有飞机都能安全着陆而不发生冲突。在实际航空管制中,这类问题直接关系到飞行安全和机…

2026/10/14 0:00:41 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/14 3:30:28 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/13 16:22:04 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/14 3:29:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 22:01:27 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 7:53:34 阅读更多 →