做完接到个任务,要将Linux嵌入式的手持设备的TP测试界面,由原来的画线,改为消除网格的方式。
就是将这样的:
换成这样的:
其实绘制比较简单。就是想借此说一下Qt的绘图系统以及paintEvent()事件的工作机制。
一、Qt绘图系统核心原理
1.1 绘图系统架构
Qt的绘图系统基于三要素构建:
- QPainter:执行绘制操作的"画笔",提供超过200个图形绘制方法;
- QPaintDevice:绘制载体(如QWidget、QPixmap),屏幕分辨率480x800的适配可通过设置单元格尺寸实现;
QPaintEngine:设备抽象层,开发者通常无需直接操作;
三者关系如下:
QPainter → QPaintEngine → QPaintDevice
1.2 paintEvent工作机制
当发生以下事件时触发重绘:
- 窗口首次显示
- 窗口尺寸调整
- 被遮挡区域重新可见
- 主动调用update()/repaint()
update(); // 请求异步重绘(推荐)
repaint(); // 立即同步重绘(慎用)
1.3 双缓冲技术
Qt在paintEvent中默认启用双缓冲机制,通过以下方式优化绘制:
- 先在内存绘制完整图像;
- 单次写入显存避免闪烁;
- 自动处理多线程绘制同步;
二、核心类与函数详解
2.1 QPainter关键方法举例
实际QPainter的方法很多,之前文章中也列举过,各种跟图形绘制的方法。这里只侧重列一下跟本次绘制和消除网格相关的。
2.2 QWidget绘图函数
// 必须重写的核心函数
void paintEvent(QPaintEvent *event) override {QPainter painter(this);// 绘制操作 ,开始你绘制你想要的图画
}// 鼠标事件处理函数 (在嵌入式里边,没有鼠标,其实就是手触点击)
void mousePressEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
三、网格颜色交互实现方案
3.1 设计思路
所谓整个界面的网格,可以看作时一个个独立的小格子纵横排列的矩阵形成;
界面对象构造,界面初始化时,默认调用paintEvent()事件,先绘制一个默认颜色的网格;
当鼠标按下(实际嵌入式里边就是手指触摸按下),触发mousePressEvent()事件,记录点触的坐标位置;
将点触的坐标位置转换成网格里的格子;
对格子进行颜色变换的重绘;
鼠标继续滑动(手指滑动),触发mouseMoveEvent()事件,事件响应中使用轨迹插值算法保证连续绘制;
3.2 完整代码实现
// ColorGridWidget.h
#include <QWidget>
#include <QVector>class ColorGridWidget : public QWidget {Q_OBJECT
public:explicit ColorGridWidget(QWidget *parent = nullptr);protected:void paintEvent(QPaintEvent *) override;void mousePressEvent(QMouseEvent *) override;void mouseMoveEvent(QMouseEvent *) override;private:const int CELL_SIZE = 32; // 适配480x800,我的设备就是480*800的竖屏const int ROWS = 25; // 800/32=25 const int COLS = 15; // 480/32=15QVector<QVector<QColor>> gridColors; // 颜色状态矩阵 QPoint lastPos; // 轨迹记录 void initGrid();void updateCell(const QPoint &pos);QPoint posToGrid(const QPoint &pos) const;
};
// ColorGridWidget.cpp
#include "ColorGridWidget.h"
#include <QPainter>
#include <QMouseEvent>ColorGridWidget::ColorGridWidget(QWidget *parent) : QWidget(parent) {setAttribute(Qt::WA_StaticContents); // 优化静态内容绘制 initGrid();//固定界面尺寸setFixedSize(480, 800);
}void ColorGridWidget::initGrid() {gridColors.resize(ROWS); for (auto &row : gridColors) {row.resize(COLS); row.fill(Qt::red); // 初始化红色网格 }
}void ColorGridWidget::paintEvent(QPaintEvent *) {QPainter painter(this);painter.setRenderHint(QPainter::Antialiasing); // 绘制单元格颜色 for (int row=0; row<ROWS; ++row){for (int col=0; col<COLS; ++col){//整个页面的网格其实就是一个个小矩形作为单个格子拼起来的QRect cellRect(col*CELL_SIZE, row*CELL_SIZE, CELL_SIZE, CELL_SIZE); // 填充颜色 painter.fillRect(cellRect, gridColors[row][col]);// 绘制2px宽的黑色网格线painter.setPen(QPen(Qt::black, 2));//绘制小格子painter.drawRect(cellRect); }}
}QPoint ColorGridWidget::posToGrid(const QPoint &pos) const {
//这里其实是将像素点换成网格位置,类似一种坐标转换return QPoint(qBound(0, pos.x() / CELL_SIZE, COLS-1),qBound(0, pos.y() / CELL_SIZE, ROWS-1));
}void ColorGridWidget::updateCell(const QPoint &pos)
{QPoint gridPos = posToGrid(pos);if (gridColors[gridPos.y()][gridPos.x()] != Qt::green) {gridColors[gridPos.y()][gridPos.x()] = Qt::green;// 局部更新优化 update(QRect(gridPos.x()*CELL_SIZE, gridPos.y()*CELL_SIZE,CELL_SIZE, CELL_SIZE));}
}void ColorGridWidget::mousePressEvent(QMouseEvent *event) {if (event->button() == Qt::LeftButton) {lastPos = event->pos();updateCell(lastPos);}
}void ColorGridWidget::mouseMoveEvent(QMouseEvent *event) {if (event->buttons() & Qt::LeftButton) {// 轨迹插值算法保证连续绘制 QLineF line(lastPos, event->pos());qreal length = line.length(); const qreal STEP = CELL_SIZE * 0.6; // 优化轨迹密度 for (qreal i=0; i<length; i+=STEP) {QPoint curPos = line.pointAt(i/length).toPoint(); updateCell(curPos);}lastPos = event->pos();}
}
3.3 技术要点解析
(1). 坐标转换算法
posToGrid()通过整除计算将物理坐标转换为网格索引,qBound确保索引不越界.
(2). 轨迹插值优化
使用QLineF进行线性插值,STEP参数控制采样密度,平衡绘制精度与性能.
(3). 局部更新机制
update(QRect(…))仅重绘受影响区域,相比全局重绘性能提升.
(4). 渲染优化技巧
- 开启WA_StaticContents属性减少重绘区域;
- 设置Antialiasing实现抗锯齿;
- 预计算ROWS/COLS避免运行时重复计算;
至此,项目要求的网格形状绘制完成,滑动消除格子。
四、扩展功能
在已经实现的基础上,如果想做的更通用些,更完美写,可以试试:
4.1 功能增强方向
- 多色笔刷选择
// 在类中添加颜色选择成员
QColor currentColor = Qt::green;// 右键菜单设置颜色
void contextMenuEvent(QContextMenuEvent *)
{QColor color = QColorDialog::getColor();if (color.isValid()) currentColor = color;
}
- 撤销/重做功能
// 使用QStack记录状态变化
QStack<GridState> undoStack;
// 每次修改时压栈
void updateCell()
{undoStack.push(currentState); // ...修改操作
}
4.2 需要注意的问题
- 绘图残留的问题
如果由绘制的残留显示,可以在paintEvent开始时先清除背景
painter.eraseRect(rect());
- 跨设备绘制异常的情况
如果是外部设备调用,可以把QPainter 单列出来,遵循"begin-end"配对原则:
QPainter painter;
if (painter.begin(this)) {// 绘制操作 painter.end();
}