绘图 ######################################## 坐标系统 **************************************** 对于给定的绘图设备,在绘制时具有两个坐标系:逻辑坐标系、物理坐标系 - 物理坐标系:对应于Qt的 ``QPainter::viewPort``,是建立在绘图设备上的坐标系 - 逻辑坐标系对应于Qt的 ``QPainter::window``,在绘制时将会绘制到此坐标系 在默认情况下,物理坐标和逻辑坐标是重合的(原点相同,大小相同) .. note:: 这里说坐标系 ``大小相同`` 是指坐标系单位长度代表的刻度,比如一单位长度代表两个像素长。大小相同意味着两个坐标系单位长度代表的距离是相同的 window - viewport 坐标转换 ======================================== window 代表的是逻辑坐标,QPainter 在此坐标上进行绘制操作 viewport 代表的是物理坐标,代表了 QWidget,显示时遵循此坐标 世界坐标转换,QPainter绘制在 window 上的图形将被映射到 viewport 上 首先看看 window 和 viewport 的函数签名: - ``setViewport(int x, int y, int width, int height)`` - ``setWindow(int x, int y, int width, int height)`` 此处,``(x, y)`` 代表 window/viewport 左上角的坐标,``(width, height)`` 代表 window/viewport 的大小。 **window 和 viewport 的相对坐标:** 而 viewport 代表物理坐标,window 代表逻辑坐标。所谓物理坐标,以 ``(x, y)`` 原点,x 轴向右增长,y 轴向下增长。所谓逻辑坐标,以 ``(x, y)`` 和 ``(width, height)`` 围成的矩形的中心为原点,x 轴向右增长,y 轴向下增长。 如图: .. image:: assets/viewport-window.png :scale: 50 要实现上述效果,使用的代码如下: .. code-block:: cpp qreal w = this->width(); qreal h = this->height(); painter.setViewport(0, 0, w, h); // ① painter.setWindow(w/(-2),h/(-2),w,h); 需要注意的是 ① ,由于默认情况下 物理坐标与部件的窗口是重叠的,所以此处代码可以省略。 **window 和 viewport 的大小转换:** viewport 和 window 的第二种签名如下: .. code-block:: cpp QPainter::setViewport(const QRect &rectangle) QPainter::setWindow(const QRect &rectangle) 两者对应的 ``rectangle`` 将会通过映射使两者大小相同: 如: .. image:: assets/windows-viewport转换.png 设代码如下: .. code-block:: cpp qreal w = width(); qreal h = height(); paint.setViewport(QRect(0, 0, w, h)); paint.setWindow(QRect(0, 0, w/2, h/2)); 此时由于 viewport 代表的矩形比 window 代表的矩形大两倍(按长宽来看,而不是按面积),因此,window 的一个单位长度 = viewport 的两个单位长度。 又由于实际上绘图是绘制在 window 上的,因此,看到的图形将会比绘制的图形大两倍(按长宽) 例如: .. code-block:: cpp // window 和 viewport 为 1 : 1 QPainter painter(this); painter.setPen(Qt::red); painter.drawRect(20, 20, 40, 40); // 设置 window 和 viewport 为 2 : 1 painter.setWindow(0, 0, width()*2, height()*2); painter.setPen(Qt::blue); painter.drawRect(20, 20, 40, 40); 效果如下: .. image:: assets/window-viewport.png 双缓冲绘图 **************************************** 现在假设要实现下面功能: - 使用鼠标在主窗口上画矩形 - 画的矩形在每次都保留 实现方案1.0: .. code-block:: cpp class MainWindow : public QMainWindow { private: Ui::MainWindow *ui; protected: void paintEvent(QPaintEvent *event) override{ QPainter painter(this); painter.drawRect(QRectF(startPoint,endPoint)); }; void mousePressEvent(QMouseEvent *event) override{ this->startPoint = event->pos(); update(); }; void mouseMoveEvent(QMouseEvent *event) override{ this->endPoint = event->pos(); update(); }; void mouseReleaseEvent(QMouseEvent *event) override{ this->endPoint = event->pos(); update(); }; private: QPoint startPoint; QPoint endPoint; }; 但是实际运行后发现每次绘图后上一次绘制的矩形都会丢失。其根本问题在于每次重新绘制后没有保留上一次绘制的结果 实现方案2.0 现在我们先在 QPixmap 进行绘制,然后再把 QPixmap 绘制到 MainWindow,这样就可以保留上一次绘制的结果: .. code-block:: cpp class MainWindow : public QMainWindow { private: Ui::MainWindow *ui; protected: void paintEvent(QPaintEvent *event) override{ QPainter painter(&mainPix); painter.drawRect(QRectF(startPoint,endPoint)); QPainter painter2(this); painter2.drawPixmap(QRectF(0,0,this->width(),this->height()),mainPix,QRectF(0,0,mainPix.width(),mainPix.height())); } }; void mousePressEvent(QMouseEvent *event) override{ this->startPoint = event->pos(); update(); }; void mouseMoveEvent(QMouseEvent *event) override{ this->endPoint = event->pos(); update(); }; void mouseReleaseEvent(QMouseEvent *event) override{ this->endPoint = event->pos(); update(); }; private: QPoint startPoint; QPoint endPoint; QPixmap mainPix; }; 实际运行结果如下: .. image:: assets/双缓冲绘制2.0.png 其原因在于在鼠标移动过程中不断更新了 endPoint ,而且不断触发了 paintEvent , 这导致在鼠标移动过程中的矩形也被保存在 mainPix 中,但是我们并不需要鼠标移动过程中绘制的矩形。 实现方案3.0 现在我们使用两个 QPixmap , 一个 mainPix 用于保存每次绘制的结果,而 tmpPix 则用于绘制鼠标移动过程中的矩形,同时为了消除鼠标移动过程中绘制的矩形,在每次 paintEvent 中都将 mainPix 复制到 tmpPix。这样,中间绘制的矩形就被清除了: .. code-block:: cpp class MainWindow : public QMainWindow { private: Ui::MainWindow *ui; protected: void paintEvent(QPaintEvent *event) override{ QPainter painter; if(!isDone){ // 若绘制未完成,则将矩形绘制到 tmpPix 上 tmpPix = mainPix; // 消除残影 painter.begin(&tmpPix); painter.drawRect(QRectF(startPoint,endPoint)); painter.end(); painter.begin(this); painter.drawPixmap(QRectF(0,0,this->width(),this->height()),tmpPix,QRectF(0,0,tmpPix.width(),tmpPix.height())); painter.end(); }else{ // 如绘制已完成,则将矩形绘制到 mainPix 上 painter.begin(&mainPix); painter.drawRect(QRectF(startPoint,endPoint)); painter.end(); painter.begin(this); painter.drawPixmap(QRectF(0,0,this->width(),this->height()),mainPix,QRectF(0,0,mainPix.width(),mainPix.height())); painter.end(); } }; void mousePressEvent(QMouseEvent *event) override{ this->startPoint = event->pos(); this->isDone = false; update(); }; void mouseMoveEvent(QMouseEvent *event) override{ this->endPoint = event->pos(); update(); }; void mouseReleaseEvent(QMouseEvent *event) override{ this->endPoint = event->pos(); this->isDone = true; update(); }; private: bool isDone = false; QPoint startPoint; QPoint endPoint; QPixmap mainPix; QPixmap tmpPix; }; 运行结果如下: .. image:: assets/双缓冲绘图3.0.png 在实现方案3.0中,我们使用了两个 QPixmap 用来绘制矩形,因此又被称为 ``双缓冲绘图`` .. important:: 在上面代码初始化 QPixmap 时,需要将 QPixmap 的初始化代码放置在 ``ui->setupUi(this)`` 之后,否则将会导致 ``this->size() ≠ QPixmap::size()`` .. seealso:: - `Qt中坐标:窗口坐标,视口坐标 `_ 图片压缩 **************************************** 常见的 PNG 是一种压缩格式,在内存中会进行一次展开,展开大小为:水平像素×垂直像素×每个像素所需位数/8 (字节)。对于常见的真彩色一共是 24 位,还有 32 位色(新增了 8 位用于代表灰度)。 rgba 就是 32 位色 颜色越单一,画面约简单的图片越容易压缩。但是这些在内存中都是会被展开的。尺寸 500MB 的 PNG 照片在内存中展开大小约为 6G 优化方式有两种: 一种方式是对图片的颜色进行减少,例如将 32 位色降低为 24 位色 一种是对图片进行裁剪。对图片进行裁剪会减少占用内存,放大则会增加占用内存。为了快速得到高质量图片,可以使用 Qt 先快速裁剪到目标尺寸的 1.5 倍,再精确裁剪到目标尺寸,这样处理比较快,而且画质比较好 .. seealso:: - `png图片压缩原理解析 `_ - `Qt实现图片的简单压缩 `_ - `图片是如何在内存中存储的 `_ - `深度解析——图片加载到内存中的大小计算 & 内存优化 `_ - `数据压缩算法---LZ77算法 的分析与实现 - DreamGo - 博客园 `_