绘图

坐标系统

对于给定的绘图设备,在绘制时具有两个坐标系:逻辑坐标系、物理坐标系

  • 物理坐标系:对应于Qt的 QPainter::viewPort,是建立在绘图设备上的坐标系

  • 逻辑坐标系对应于Qt的 QPainter::window,在绘制时将会绘制到此坐标系

在默认情况下,物理坐标和逻辑坐标是重合的(原点相同,大小相同)

这里说坐标系 大小相同 是指坐标系单位长度代表的刻度,比如一单位长度代表两个像素长。大小相同意味着两个坐标系单位长度代表的距离是相同的

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

要实现上述效果,使用的代码如下:

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 的第二种签名如下:

QPainter::setViewport(const QRect &rectangle)
QPainter::setWindow(const QRect &rectangle)

两者对应的 rectangle 将会通过映射使两者大小相同:

如:

image

设代码如下:

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 上的,因此,看到的图形将会比绘制的图形大两倍(按长宽)

例如:

// 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

双缓冲绘图

现在假设要实现下面功能:

  • 使用鼠标在主窗口上画矩形

  • 画的矩形在每次都保留

实现方案1.0:

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,这样就可以保留上一次绘制的结果:

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

其原因在于在鼠标移动过程中不断更新了 endPoint ,而且不断触发了 paintEvent , 这导致在鼠标移动过程中的矩形也被保存在 mainPix 中,但是我们并不需要鼠标移动过程中绘制的矩形。

实现方案3.0

现在我们使用两个 QPixmap , 一个 mainPix 用于保存每次绘制的结果,而 tmpPix 则用于绘制鼠标移动过程中的矩形,同时为了消除鼠标移动过程中绘制的矩形,在每次 paintEvent 中都将 mainPix 复制到 tmpPix。这样,中间绘制的矩形就被清除了:

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

在实现方案3.0中,我们使用了两个 QPixmap 用来绘制矩形,因此又被称为 双缓冲绘图

在上面代码初始化 QPixmap 时,需要将 QPixmap 的初始化代码放置在 ui→setupUi(this) 之后,否则将会导致 this→size() ≠ QPixmap::size()

图片压缩

常见的 PNG 是一种压缩格式,在内存中会进行一次展开,展开大小为:水平像素×垂直像素×每个像素所需位数/8 (字节)。对于常见的真彩色一共是 24 位,还有 32 位色(新增了 8 位用于代表灰度)。 rgba 就是 32 位色

颜色越单一,画面约简单的图片越容易压缩。但是这些在内存中都是会被展开的。尺寸 500MB 的 PNG 照片在内存中展开大小约为 6G

优化方式有两种:

一种方式是对图片的颜色进行减少,例如将 32 位色降低为 24 位色 一种是对图片进行裁剪。对图片进行裁剪会减少占用内存,放大则会增加占用内存。为了快速得到高质量图片,可以使用 Qt 先快速裁剪到目标尺寸的 1.5 倍,再精确裁剪到目标尺寸,这样处理比较快,而且画质比较好

Last moify: 2022-12-04 15:11:33
Build time:2025-07-18 09:41:42
Powered By asphinx