Qt 5.12 画图程序源码解析:QPainter 与鼠标事件处理从零到实战
1. 为什么我要自己维护一份“类似画图软件”的Qt源码先说点背景。我手头这份源码项目开发环境是Qt Creator 4.8.0 与 Qt 5.12.0目标很朴素做一个功能类似 Windows 画图的桌面小工具。源码里包含了一个完整的可编译工程支持鼠标自由绘制、直线/矩形/椭圆工具、橡皮擦、调色板、撤销、清空画布、打开图片继续编辑、保存 PNG/JPG。它不会像 PhotoShop 那么复杂但它把画图软件最核心的那几条数据流都走通了。也许你会问网上随便搜一下“Qt 画图源码”就能找到一堆为什么还要自己维护一份我实际用下来的感受是网上的源码要么绑定了过于复杂的框架要么直接把绘制逻辑全塞在paintEvent里想加一个撤销功能就得重构。自己维护一份的好处是你清楚地知道每个类干什么、每个信号和事件在什么时候触发、以后加什么功能该动哪几个文件。如果你正准备做课程设计、毕业设计或者只是想在 Qt 里练一下QPainter和鼠标事件处理这份源码的思路可以直接抄。我下面讲的不是逐个文件的注释而是整个项目的实现逻辑和我在调试过程中踩过的坑这些才是真正有价值的部分。2. 先把源码的模块结构理清楚这个画图程序由哪几个类组成2.1 核心类划分MainWindow、PaintWidget 与 ToolManager整个项目没有用复杂的 MVC 框架就是三个主要层次MainWindow负责菜单栏、工具栏、画布区域的布局以及新建/打开/保存/撤销/清空这些动作的调度。PaintWidget一个继承QWidget的画布控件。它是整个程序的心脏负责接收鼠标事件、把绘制结果写到离屏缓冲里、在paintEvent中把缓冲显示出来。ToolManager一个轻量级的工具枚举管理保存当前选中的工具类型、画笔颜色、画笔宽度。早期版本我把这些变量直接放在PaintWidget里后来工具多了发现每加一个按钮就要改一次PaintWidget所以拆了一个ToolManager出来。如果你拿到这份源码建议先看PaintWidget因为它集中了画图软件 70% 的逻辑。不要一上来就看MainWindow那里面有大量菜单动作初学者容易看乱。2.2 源码目录与 .pro 工程文件的组织工程文件是 qmake 管理的用 Qt Creator 4.8.0 打开.pro文件就可以直接构建。目录结构大概是这样PaintApp/ ├── PaintApp.pro ├── main.cpp ├── mainwindow.h ├── mainwindow.cpp ├── paintwidget.h ├── paintwidget.cpp ├── toolmanager.h ├── toolmanager.cppPaintApp.pro里最关键的是这几行QT core gui greaterThan(QT_MAJOR_VERSION, 4): QT widgets TARGET PaintApp TEMPLATE app SOURCES main.cpp \ mainwindow.cpp \ paintwidget.cpp \ toolmanager.cpp HEADERS mainwindow.h \ paintwidget.h \ toolmanager.h注意我这里没有加QT printsupport也没有加webenginewidgets因为画图这个需求用不到。后面我会提到很多编译错误其实就是因为.pro里多写了不需要的模块。2.3 为什么一定要用离屏缓冲机制很多新手写画图程序时会犯一个典型错误在paintEvent里每次鼠标移动时的坐标都直接画到窗口上。这样看起来好像能画但窗口一旦被遮挡、最小化再恢复所有笔迹就没了。原因很简单paintEvent只负责“画面”不负责“存画面”窗口重绘时引擎不会帮你把之前画过的东西保存下来。所以我在PaintWidget里维护了一个QImage m_buffer所有绘制操作都是先画到这个QImage上然后paintEvent只做一件事把m_buffer整体拷贝到窗口中。void PaintWidget::paintEvent(QPaintEvent *) { QPainter painter(this); painter.drawImage(0, 0, m_buffer); }整个绘制链就是鼠标事件触发在m_buffer上执行绘制。调用update()Qt 会在合适时机触发paintEvent。paintEvent将m_buffer显示到屏幕上。这个机制一旦建立后续的撤销、保存、导出都变得非常容易因为它们都只和m_buffer这个QImage交互和界面解耦了。我第一版改的时候也犹豫过要不要直接用QPixmap。最后选了QImage原因是QImage对像素级操作和文件保存更直观底层不依赖窗口系统的显示优化跨平台时行为更一致。如果你的软件在屏幕上滚动非常频繁再考虑换成QPixmap也不迟。3. 鼠标事件到画笔轨迹的转换详细拆解绘制流程3.1 三种鼠标事件的分工画图的本质就是把鼠标的连续移动转换成一段段线段。这里有个容易忽略的细节鼠标事件要区分“按下”、“移动”和“松开”三种状态。我在PaintWidget里用三个事件处理函数处理void PaintWidget::mousePressEvent(QMouseEvent *event) { if (event-button() Qt::LeftButton) { m_isDrawing true; m_startPoint event-pos(); m_lastPoint event-pos(); // 自动绘制一个像素点解决点击一下没反应的问题 if (m_currentTool ToolManager::FreeDraw) { drawLineSegment(m_lastPoint, m_lastPoint); update(); } } } void PaintWidget::mouseMoveEvent(QMouseEvent *event) { if (!m_isDrawing) return; if (m_currentTool ToolManager::FreeDraw) { drawLineSegment(m_lastPoint, event-pos()); m_lastPoint event-pos(); update(); } else { // 形状工具先恢复快照再画预览避免拖出残影 m_buffer m_shapeBaseSnapshot.copy(); drawShapePreview(m_startPoint, event-pos()); update(); } } void PaintWidget::mouseReleaseEvent(QMouseEvent *event) { if (event-button() Qt::LeftButton m_isDrawing) { // 对形状工具松开时把最终形状画入 m_buffer if (m_currentTool ! ToolManager::FreeDraw) { m_buffer m_shapeBaseSnapshot.copy(); drawShapePreview(m_startPoint, event-pos()); update(); } m_isDrawing false; } }注意m_lastPoint的用法。自由画笔不是每个鼠标移动事件都从m_startPoint画到当前点而是从m_lastPoint画到当前点否则每次移动都会把前面所有线段重新描一遍整根线条会变得异常粗重。3.2 用 QPainter 画线段的具体实现QPainter的使用要遵循“begin 绘制end 收尾”的规则。我封装了一个drawLineSegment函数每次绘制都建立一个独立的QPainter实例避免状态泄露void PaintWidget::drawLineSegment(const QPoint from, const QPoint to) { QPainter painter(m_buffer); painter.setRenderHint(QPainter::Antialiasing, true); QPen pen(m_currentColor); pen.setWidth(m_penWidth); pen.setCapStyle(Qt::RoundCap); painter.setPen(pen); painter.drawLine(from, to); }这里有两个细节值得说。第一是Qt::RoundCap。如果不设置这个画出来的线段头尾是平头快速画线时转折处会有明显棱角看起来断断续续。设置成圆头之后每相邻两段线段的首尾会圆滑连上视觉效果接近真正的毛笔笔触。第二是抗锯齿。Antialiasing能让斜线看起来平滑一些代价是性能略降。对于单机画图软件完全值得开启。如果你的程序后续要处理超大画布且频繁绘制可以做成一个开关只有缩放时才关闭抗锯齿。3.3 形状工具的统一预览机制直线、矩形、椭圆这三种工具有一个共同点鼠标按下到松开之间需要一个实时预览。最简单也最容易出错的做法是每一次鼠标移动都直接在m_buffer上画一个新形状。这样做的直接后果是拖动过程中上一次画的形状没有被擦掉画面会积累一层层残影。我用了一个很老的技巧在鼠标按下时保存一份基础快照m_shapeBaseSnapshot m_buffer.copy()然后每次鼠标移动都先恢复快照再画新的形状。void PaintWidget::drawShapePreview(const QPoint from, const QPoint to) { QPainter painter(m_buffer); QPen pen(m_currentColor); pen.setWidth(m_penWidth); pen.setCapStyle(Qt::RoundCap); painter.setPen(pen); painter.setBrush(Qt::NoBrush); switch (m_currentTool) { case ToolManager::Line: painter.drawLine(from, to); break; case ToolManager::Rect: painter.drawRect(QRect(from, to).normalized()); break; case ToolManager::Ellipse: painter.drawEllipse(QRect(from, to).normalized()); break; default: break; } }QRect(from, to).normalized()是一个很实用的写法。鼠标从右下往左上拖动时QPainter::drawRect也能自动把矩形归一化避免绘制出反向矩形。这个方案虽然简单但每次鼠标移动都涉及一次copy()和一次形状绘制。对于 1920×1080 画布实测在普通电脑上依然流畅。QImage::copy()虽然是深度拷贝但现代内存带宽足够应付这个量级。如果你不放心可以后面再改成双缓冲临时图层方案但那是优化问题不是功能问题。4. 工具面板和操作指令的设计颜色、橡皮擦、撤销到底怎么选型4.1 调色板实现预设色块和自选颜色的关系调色板我做了两档底部一排预设色块以及一个“自定义颜色”按钮。色块就是个QPushButton点击后发出颜色改变信号MainWindow再调用ToolManager::setColor。自定义颜色用QColorDialog。但这里有一个注意点QColorDialog默认允许用户选择透明的 Alpha 通道颜色而画图程序如果直接画透明画笔会出现“看起来画了但保存后背景全透”的怪现象。我建议在getColor之后强制校验QColor color QColorDialog::getColor(Qt::black, this, QStringLiteral(选择颜色)); if (color.isValid()) { color.setAlpha(255); m_toolManager-setColor(color); }补上setAlpha(255)这行是因为我踩过一次坑在某个 Linux 发行版上QColorDialog返回的颜色 Alpha 值是 0导致画笔写上去完全透明排查了很久才发现是这个问题。4.2 橡皮擦的两种实现方式白色绘制和 CompositionMode_Clear橡皮擦有两种常见实现这里提前帮后人避坑。第一种直接用背景色白色画线。实现极其简单把画笔颜色设为Qt::white就好。缺点也很明显如果将来你用 PNG 图片作为背景或者想保留透明画布这种“橡皮擦”擦过的地方会变成白色而不是变回原图。第二种使用QPainter::CompositionMode_Clear把对应区域的像素清成透明色void PaintWidget::eraseLineSegment(const QPoint from, const QPoint to) { QPainter painter(m_buffer); painter.setCompositionMode(QPainter::CompositionMode_Clear); QPen pen(QColor(0, 0, 0, 0)); pen.setWidth(m_penWidth); pen.setCapStyle(Qt::RoundCap); painter.setPen(pen); painter.drawLine(from, to); }CompositionMode_Clear的意思其实是把源像素和背景像素统统丢弃直接输出全透明黑色。这样擦除后的区域是全透明的。如果背景画布本身是白色显示效果还是白色如果导入了一张图片作为底图橡皮擦擦过的地方就能露出底图这才是大家直觉理解的橡皮擦。我最终的源码里默认使用第二种方式理由很明确它让“橡皮擦”和“背景色”解耦后面增加图层功能也更容易。4.3 撤销功能的轻量级实现方案画图软件里撤销的思路大体有两种命令栈模式和快照模式。命令栈模式要求每种绘制动作记录操作前后差异适合超大尺寸画布但实现复杂度很高。我这个画图程序走的是快照模式每次鼠标按下时保存一份m_buffer.copy()到撤销栈里一旦用户点撤销就直接从栈里弹出一张快照恢复。void PaintWidget::saveUndoSnapshot() { m_undoStack.push(m_buffer.copy()); if (m_undoStack.size() m_undoLimit) { m_undoStack.removeFirst(); } m_redoStack.clear(); }需要注意的是撤销栈里面的QImage会占用大量内存。一张 1280×720 的画布单张快照大约 3~4 MB取决于显卡和内存布局如果每笔都保存50 次撤销就接近 200 MB。我在PaintWidget构造函数里设了m_undoLimit 30而且每次保存前都会移除最旧的一个快照while (m_undoStack.size() m_undoLimit) { m_undoStack.removeFirst(); }这样内存占用可控用户体验也够用。如果以后要支持任意大小的撤销就得换“命令记录坐标重放”的模式不适合新手实现。4.4 清空画布和新建画面的边界处理“新建”和“清空”操作在很多源码里被当成一回事但我两者做了区分清空是m_buffer.fill(Qt::white)并保存撤销快照新建是会弹出对话框让用户选择画布尺寸然后重新生成对应尺寸的QImage。新建之前的一行代码必须注意m_undoStack.clear(); m_redoStack.clear(); update();否则你新建画布之后点撤销会把一张旧尺寸的图片恢复到新画布里这属于典型的僵尸状态 bug。类似问题在所有“重置了画布但没重置历史记录”的程序里都会出现一旦出现用户会被折磨得摔鼠标。5. 保存、打开与文件格式的坑不只是调一个 save 方法那么简单5.1 直接导出 PNG/JPG 和自定义工程文件的区别画图程序的保存功能需要分两层看待导出位图PNG、JPG、BMP 这些使用QImage::save()一行代码即可。保存工程文件如果后续想支持图层、笔刷参数、撤销历史就必须定义自己的文件格式。先看最简单的导出位图void MainWindow::onSaveAction() { QString fileName QFileDialog::getSaveFileName(this, QStringLiteral(保存图片), QString(), QStringLiteral(PNG 图片 (*.png);;JPEG 图片 (*.jpg);;BMP 图片 (*.bmp))); if (fileName.isEmpty()) return; if (!m_paintWidget-bufferImage().save(fileName)) { QMessageBox::warning(this, QStringLiteral(保存失败), QStringLiteral(无法写入文件%1).arg(fileName)); } }这里有个隐藏坑保存 JPG 时QImage::save是能成功的但 JPG 是一种有损格式如果原图包含透明的橡皮擦像素保存后会被黑色或白色填充。我在测试中画了一幅透明底图另存为 JPG 后整块橡皮擦区域变成黑色。解决方案是导出 JPG 前先把透明像素铺到白色背景上QImage exportImage m_paintWidget-bufferImage().convertToFormat( QImage::Format_RGB32); QPainter painter(exportImage); painter.fillRect(exportImage.rect(), Qt::white); QPainter::CompositionMode_SourceOver;上面的fillRect写法其实不完整正确的做法是QImage exportImage(m_paintWidget-bufferImage().size(), QImage::Format_RGB32); exportImage.fill(Qt::white); QPainter painter(exportImage); painter.drawImage(0, 0, m_paintWidget-bufferImage()); painter.end(); exportImage.save(fileName);这样透明区域就成了白色底符合大多数用户对 JPG 的预期。5.2 自定义工程文件QDataStream 的版本标记我自己的工程文件使用.pbrush扩展名格式非常简单一个魔数、一个版本号、图片宽度和高度然后是QImage的原始字节数据。void PaintWidget::writeToStream(QDataStream out) const { out quint32(0x505452); // 魔数 PTR out quint32(1); // 格式版本号 out m_buffer.width(); out m_buffer.height(); out m_buffer; }读取时先检验魔数和版本号避免把任意文件当成画图文件解析。这里有一个很妙的点QDataStream对QImage有内建序列化支持也就是说out m_buffer;一行就能把整张图片写进文件。具体细节我对齐了字节长度实测稳定性足够。也许有人觉得做自定义格式纯属多余但如果你的画图源码后续要加入“图层可见性”“笔刷透明度”等参数位图格式完全承载不了。建议从早期就留一个自定义格式的读写通道。5.3 打开图片继续编辑的坑“打开”功能本质上是把一个磁盘图片加载到缓冲区里void PaintWidget::loadImage(const QString path) { QImage image; if (!image.load(path)) { setStatus(QStringLiteral(图片加载失败)); return; } m_buffer image.convertToFormat(QImage::Format_ARGB32); m_undoStack.clear(); m_redoStack.clear(); update(); }这里要注意QImage::load是按扩展名判断编码格式的如果文件扩展名和真实格式不符偶尔会加载失败。更可靠的方式是用QImageReader并强制读取QImageReader reader(path); reader.setAutoTransform(true); QImage image reader.read();setAutoTransform是个容易被忽略但很实用的 API它会根据 JPEG 的 EXIF 方向信息自动旋转图片手机或相机拍的照片打开后方向就正确了。6. 在 Qt Creator 4.8.0 与 Qt 5.12.0 环境下跑通项目的关键细节和填坑记录6.1 构建套件、版本符号和 fatal: cannot mix incompatible Qt library我看热搜词里有人提到fatal: cannot mix incompatible qt library (version ex50601) with this library。这个报错基本可以确定是编译器头文件路径和实际链接的 DLL 或 so 文件版本对不上。比如你明明在 Qt Creator 里选的 5.12.0 套件但工程文件里的LIBS手动写老了其他版本的 Qt 库路径或者系统 PATH 里加载了另一个 Qt 版本就会出现这种诡异问题。解决办法是不要手动添加 Qt 库路径。在 Qt Creator 中选择“构建套件”时它已经自动配好了INCLUDEPATH和LIBS。你只要在.pro里保留QT core gui widgets画图程序用不到第三方库所以也不应该出现在你的.pro里。一旦出现版本不匹配先检查Qt Creator - 工具 - 选项 - 构建套件 - 选择当前套件中的 qmake 路径是否指向 Qt 5.12.0然后重新执行清除 - qmake - 重新构建。6.2 编译报错 unknown module(s) in qt: webenginewidgets这个报错在很多 Qt 新手工程中高频出现。原因通常是.pro文件里写了QT webenginewidgets但安装 Qt 5.12.0 时没勾选 Qt WebEngine 模块。画图软件完全不需要网页渲染引擎我在源码的.pro里已经把这一行删掉了。如果你是从某份旧源码里拷过来的.pro搜索一下里面是否有QT webenginewidgets有就删掉。如果有的机器需要这个模块也应该在安装 Qt 时单独勾选但这已经超出画图源码的范畴了。6.3 事件坐标在高分屏下的缩放问题Qt 5.12 在 Windows 高分屏下默认启用缩放。设备像素比例是 1.5 或 2.0 时event-pos()返回逻辑坐标而QImage的尺寸是物理像素两者如果混着用画出来的内容会偏移。解决办法是让画布图片始终和缩放后的窗口保持一致。通常paintEvent里用painter.drawImage(0, 0, m_buffer)时Qt 会自动按照当前的缩放率绘制但如果你的画布尺寸是手动设置的常量比如QImage(1920, 1080)在缩放比例 2 的显示器上可能显示不全。一个从实际工程里验证过的稳妥方案是在初始化时读取设备像素比并设置到画布上qreal dpr devicePixelRatioF(); m_buffer QImage(width() * dpr, height() * dpr, QImage::Format_ARGB32); m_buffer.setDevicePixelRatio(dpr); m_buffer.fill(Qt::white);这样m_buffer的逻辑尺寸等于控件尺寸实际存储的物理分辨率更高绘制内容不会出现边缘撕裂。6.4 Qt Creator 4.8.0 自动补全按钮无反应我在 Qt Creator 里写QPainter相关代码时偶尔会碰到自动补全失效。这不是源码问题是 Qt Creator 的代码模型崩溃。清理方式项目目录下删除*.pro.user文件重启 Qt Creator让它重新解析工程。*.pro.user文件是用户级别的配置删掉不影响源码只影响本地界面布局。7. 我后期扩展这个画图源码的经验和几个新方向我这份源码目前已经稳定跑在 Qt 5.12.0 环境里作为基础画图工具完全没有问题。但如果你想让它的复杂度向上走一个台阶我最推荐先加这四个方向第一箭头和标记工具。很多用户画图不只是画画还要在截图上标注重点。代码中多一个Arrow枚举值然后在drawShapePreview里画一条直线并在终点画两个三角箭头。核心代码在 30 行左右收益却很明显它能覆盖大量办公标注场景。第二画布缩放和滚动。目前源码是固定画布后续如果支持超大尺寸图像就要把PaintWidget放进QScrollArea并在paintEvent里做局部重绘。这里会有复杂的坐标换算但也是一个很有价值的进阶练习。第三按像素取色。增加一个吸管工具点击画布时读取m_buffer.pixelColor(pos)并把颜色设置到ToolManager。这个功能实现起来非常简单但实战中极其好用特别是想复现图片中某个颜色的时候。第四粒子笔刷。在drawLineSegment里使用setClip和随机数生成散布的点让线条笔触更像粉笔或喷枪。这个属于趣味性扩展适合用来熟悉QPainter的绘制状态。最后再说一句源码如果说只有一个值得记住的设计那就是“先画到 QImage再一次性显示到窗口”的离屏缓冲机制。搞懂这一点后面的所有功能都是在给这张 QImage 写不同的操作而已。希望你拿到这份源码后改出属于你自己的画图工具。

相关新闻

RAG混合召回实战:为什么纯向量检索在生产中必须被替代

RAG混合召回实战:为什么纯向量检索在生产中必须被替代

1. 项目概述:RAG系统里那个被悄悄绕开的“纯向量检索”陷阱你有没有在某次技术分享会上,听到大厂工程师轻描淡写地说一句:“我们线上RAG服务不用纯向量检索,早就切到混合召回了”?当时你可能心里一咯噔——不是说向量检…

2026/10/13 5:28:31 阅读更多 →
OFDM频偏估计MATLAB仿真:从子载波正交性到星座图验证的完整实现

OFDM频偏估计MATLAB仿真:从子载波正交性到星座图验证的完整实现

/* 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 5:27:31 阅读更多 →
Blensor TOF雷达仿真:生成高保真PCD点云的完整实践指南

Blensor TOF雷达仿真:生成高保真PCD点云的完整实践指南

/* 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 5:27:31 阅读更多 →

最新新闻

关系模型实战指南:从二维表到业务稳定性的底层逻辑

关系模型实战指南:从二维表到业务稳定性的底层逻辑

1. 这不是教科书里的“关系”,而是你每天都在用的数据组织逻辑“关系模型的基本概念”——看到这八个字,很多人第一反应是大学数据库课上那个被粉笔灰笼罩的下午,黑板上写着“实体、属性、域、键”,讲台下有人偷偷刷手机&#xff…

2026/10/13 12:22:17 阅读更多 →
Qt国际化翻译完整实践:从tr()到动态语言切换

Qt国际化翻译完整实践:从tr()到动态语言切换

做Qt开发这么多年,遇到最多的问题不是功能写不出来,而是功能做完以后客户说看不懂。于是QT国际化翻译就成了绕不开的环节。表面上看,这事只是把界面文字换成英文、日文,真正做起来却涉及源码扫描、翻译文件管理、运行时加载、动态…

2026/10/13 12:22:17 阅读更多 →
华为+红帽双赛道:2026网工转型的破局之道

华为+红帽双赛道:2026网工转型的破局之道

1. 为什么2026年网工转型,我建议你押注"华为红帽"双赛道最近不少朋友私信问我关于2026年转行网络工程师(网工)的事,大家的焦虑点很一致:AI工具越来越强,初中级网工的活是不是要被替代&#xff1f…

2026/10/13 12:22:17 阅读更多 →
Modern.js 服务端编译核心 @modern-js/server-utils:TypeScript 编译、别名解析与原生 ESM 支持的源码级解析

Modern.js 服务端编译核心 @modern-js/server-utils:TypeScript 编译、别名解析与原生 ESM 支持的源码级解析

前端后端Web框架 【免费下载链接】modern.js A progressive web framework based on React and Rsbuild. 项目地址: https://gitcode.com/gh_mirrors/mo/modern.js 点击查看 免费下载 本文围绕 packages/server/utils/CHANGELOG.md 记录的能力演进,结合…

2026/10/13 12:22:17 阅读更多 →
基于SpringBoot+Vue的派出所健康档案管理系统设计与实现

基于SpringBoot+Vue的派出所健康档案管理系统设计与实现

选题背景 社会对公共安全的重视,派出所民警的工作压力日益增大,其健康状况备受关注。早期的健康档案管理方式效率低下,难以实现信息的实时共享和动态更新。本系统采用 Spring Boot 框架,结合前端技术和数据库技术,构建…

2026/10/13 12:22:17 阅读更多 →
EmbeddingGemma 2本地多模态嵌入模型部署与向量检索实战

EmbeddingGemma 2本地多模态嵌入模型部署与向量检索实战

1. 这个模型到底是什么:先把它的定位弄清楚拿到"EmbeddingGemma 2"这个名字,第一反应就是:这是个嵌入模型,740M参数规模,多模态,而且主打本地推理。这几个关键词放在一起,意味着它并不…

2026/10/13 12:21:16 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

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

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

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

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

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

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

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

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

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

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 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/12 21:05:09 阅读更多 →
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/12 14:48:49 阅读更多 →
黑夜航拍船只数据集训练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/12 18:11:17 阅读更多 →