Qt 界面优化 --- 绘图

绘图

Qt 虽已内置诸多控件,但无法保证适配所有场景。很多时候,我们需要更灵活的 "自定义" 能力,在窗口上绘制任意图形形状,以实现复杂界面设计。Qt 提供了相关绘图 API,支持在窗口绘制任意图形。

基本概念

所谓 "控件" 本质上也是通过绘图方式生成的。画图是对控件的一种封装 ,可以类比成机器语言和高级语言的关系;控件是对画图 API 的进一步封装,能让画图 API 更简单实现。

绘图 API 核心类
说明
QPoint "绘图者" 或 "画笔" 位置,用来绘图时,提供了点(x 和 y 坐标),可在坐标系(多种相关图形类)里描述点。
QPointF 同 QPoint,精度更高(用到浮点数),QPainter 绘制时,QPoint 会被隐式转换为 QPointF。
QPainter 描述了 "绘图者" 要画到哪个对象上,绘制用的 QWidget 也属于 QPaintDevice(QWidget 是 QPaintDevice 的子类)。
QPen 描述了 QPainter 画出来的线是什么样的。(线的属性)
QBrush 描述了 QPainter 填充一个区域是什么样的。(区域,边界的属性)

注意:绘图 API 的使用,一般不会在 QWidget 的构造函数中使用,而是要放到 paintEvent 事件处理函数中

关于 paintEvent

paintEvent 会在以下情况被触发:

  • 控件首次创建;(比如往 QWidget 上画画,QWidget 创建之前,画的东西当然不生效,首次创建 QWidget 就能显示出来画的东西)
  • 控件被遮挡后,再恢复显示/接触遮挡;(这个时机,我们进行绘制也是很重要的,否则就会在被遮挡之后就没了)
  • 窗口大小变化;
  • 窗口最小化、再恢复;
  • 主动调用 repaint() 或者 update() 方法(这两个方法都是 QWidget 的方法);
  • ......

因此,若把绘图 API 放到构造函数中调用,一旦出现上述情况,界面绘制效果无法确保符合预期。

绘制各种形状

绘制线段

示例 1

cpp 复制代码
void drawLine(const QPoint &p1, const QPoint &p2);
// p1:绘制起点坐标
// p2:绘制终点坐标

在 "widget.h" 头文件中声明事件:

cpp 复制代码
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>

class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = nullptr);
    ~Widget();

    // 声明绘画事件
    void paintEvent(QPaintEvent *event);
};

#endif // WIDGET_H

在 "widget.cpp" 文件中编写 paintEvent() 方法:

cpp 复制代码
void Widget::paintEvent(QPaintEvent *event)
{
    (void)event;
    //绘图工作就会放在这里被执行
    QPainter painter(this);//此处指定的this,不是父对象,而是指定绘制的设备(往啥东西上画)

    //画一个线段
    painter.drawLine(20, 20, 200, 20);//水平线段
    painter.drawLine(QPoint(20, 100), QPoint(200, 100));
    painter.drawLine(20, 20, 20, 300);//垂直线段
    painter.drawLine(20, 20, 100, 300);//斜画
}

实现效果

绘制矩形
cpp 复制代码
void QPainter::drawRect(int x, int y, int width, int height);
// 参数:
// x,y:窗口横坐标;
// width:所绘制矩形的宽度;
// height:所绘制矩形的高度。

示例

cpp 复制代码
void Widget::paintEvent(QPaintEvent *event)
{
    // 实例化画家对象 this:表示的是在当前窗口中绘图,即绘图设备
    QPainter painter(this);
    // 绘制矩形
    painter.drawRect(20, 20, 100, 50);
}
绘制圆形
cpp 复制代码
void QPainter::drawEllipse(const QPoint &center, int rx, int ry);
// 参数:
// center:中心坐标;
// rx:横坐标;
// ry:纵坐标。

示例

cpp 复制代码
void Widget::paintEvent(QPaintEvent *event)
{
    // 实例化画家对象 this:表示的是在当前窗口中绘图,即绘图设备
    QPainter painter(this);
    // 绘制圆
    painter.drawEllipse(QPoint(100, 100), 50, 50);
}

实现效果 :在窗口中绘制出一个以 (100, 100) 为中心,横纵半径均为 50 的圆形。

绘制文字

QPainter 类中不仅提供了绘制图形的功能,还可以用 QPainter::drawText() 函数来绘制文字,也可以用 QPainter::setFont() 设置字体等信息。示例

cpp 复制代码
#include "widget.h"
#include <QPainter>
#include <QFont>

Widget::Widget(QWidget *parent)
    : QWidget(parent)
{
    resize(400, 300);
}

void Widget::paintEvent(QPaintEvent *event)
{
    QPainter painter(this); // 实例化画家
    // 设置字体
    QFont font;
    font.setFamily("华文行楷");
    font.setPointSize(20);
    painter.setFont(font);
    // 设置文字颜色
    painter.setPen(Qt::red);
    // 绘制文字
    painter.drawText(QRect(100, 200, 400, 150), "天行健,君子以自强不息");
}

实现效果:在窗口中用红色、华文行楷字体(字号 20)绘制出 "天行健,君子以自强不息" 文字。

注意:

有时候在使用 painter.drawText(0, 100, "hello") 绘制文字时,要留意坐标的含义:

  • 横坐标 0,代表文字最左侧的位置,文字会从这个水平位置开始绘制。
  • 纵坐标 100,表示的是文字的 ** 基线(baseline)** 位置,基线是文字排版中用于对齐的一条假想线,像字母 "h""l" 等有升部的字符,会在基线之上延伸,而 "g""p" 等有降部的字符,会在基线之下延伸。
设置画笔

QPainter 在绘制时,要有一个形状、线条粗细的 "画笔"。在绘图时,QPen 可定义 "画笔"。在 Qt 中,QPen 类定义了 QPainter 绘制的 "笔" 的形状、线条粗细等。在使用时也可以自定义 "画笔"。QPen 的 "画笔" 的风格、颜色以及宽度等属性进行设置,"设置画笔" 需要通过 setPen() 方法进行设置,"画笔" 的颜色是 QColor 类实现方法进行设置,"设置画笔" 的宽度通过 setWidth() 方法进行设置。

  • 设置画笔颜色:QPen::setBrush(const QColor &color),画笔的颜色主要通过 QColor 类设置;
  • 设置画笔宽度:void QPen::setWidth(int width)
  • 设置画笔风格:void QPen::setStyle(Qt::PenStyle style)

画笔的风格有:

风格 说明
Qt::NoPen 没有线。例如,QPainter::drawRect() 填充但不绘制任何线条。
Qt::SolidLine 一条简单的线。
Qt::DashLine 由一些像素分隔的短线。
Qt::DotLine 由一些像素分隔的点。
Qt::DashDotLine 交替的点和短线。
Qt::DashDotDotLine 一个点,一个短线,一个点。
Qt::CustomDashLine 使用 QPainterPath 定义的自定义模式。
Qt::BDiagPattern 从右上到左下的对角线。
Qt::FDiagPattern 从左上到右下的对角线。
Qt::CrossPattern 交叉对角线。
Qt::DiagCrossPattern 双交叉对角线。
Qt::LinearGradientPattern 线性渐变填充。
Qt::ConicalGradientPattern 锥形渐变填充。
Qt::RadialGradientPattern 径向渐变填充。
Qt::TexturePattern 纹理填充。

示例:画笔的使用

cpp 复制代码
#include "widget.h"
#include <QPainter>
#include <QPen>
#include <QColor>

Widget::Widget(QWidget *parent)
    : QWidget(parent)
{
    resize(400, 300);
}

void Widget::paintEvent(QPaintEvent *event)
{
    // 实例化画家对象 this:表示的是在当前窗口中绘图,即绘图设备
    QPainter painter(this);
    // 设置画笔
    QPen pen(QColor(255, 0, 0));
    // 设置画笔宽度
    pen.setWidth(5);
    // 设置画笔风格
    pen.setStyle(Qt::DashLine);
    // 让画家使用画笔
    painter.setPen(pen);
    // 绘制圆
    painter.drawEllipse(QPoint(100, 100), 50, 50);
}

实现效果:在窗口中绘制出一个红色、虚线(线宽 5)的圆形。

设置画刷

画刷,顾名思义,是用 QBrush 类来描述,画刷大多用于填充。QBrush 定义了 QPainter 的填充模式,具有样式、颜色、渐变以及纹理等属性。QBrushstyle 枚举中,默认值是 Qt::NoBrush,也就是不进行任何填充。可以通过 Qt 的样式,使用 QBrush

设置画刷主要通过 void QPainter::setBrush(const QBrush &brush) 方法,其参数为画刷格式。

示例

cpp 复制代码
#include "widget.h"
#include <QPainter>
#include <QBrush>
#include <QColor>

Widget::Widget(QWidget *parent)
    : QWidget(parent)
{
    resize(400, 300);
}

void Widget::paintEvent(QPaintEvent *event)
{
    // 实例化画家对象 this:表示的是在当前窗口中绘图,即绘图设备
    QPainter painter(this);
    // 设置画刷
    QBrush brush(QColor(0, 255, 255));
    // 设置画刷风格
    brush.setStyle(Qt::Dense4Pattern);
    // 让画家使用画刷
    painter.setBrush(brush);
    // 设置画笔
    QPen pen(QColor(255, 0, 0));
    pen.setWidth(5);
    painter.setPen(pen);
    // 绘制椭圆
    painter.drawEllipse(QPoint(100, 100), 50, 50);
    // 绘制矩形
    painter.drawRect(200, 20, 100, 80);
}

绘制图片

Qt 提供了四个类来处理图像:QImageQPixmapQBitmapQPicture,它们都是常用的绘图设备。其中 QImage 主要用来进行 I/O 处理,它对 I/O 处理操作进行了优化,而且可以用来直接访问和操作像素;QPixmap 主要用来在屏幕上显示图像,它对在屏幕上显示图像进行了优化;QBitmapQPixmap 的子类,用来处理颜色深度为 1 的图像,即只能显示黑白两种颜色;QPicture 用来记录并重放 QPainter 命令。这一节只讲解 QPixmap

绘制简单图片

新建 Qt 项目 :基类选择 QWidget,项目名称为 QPainter。在 "widget.h" 头文件中声明绘图事件,如下图示:

cpp 复制代码
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>

class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = nullptr);
    ~Widget();

    // 声明绘画事件
    void paintEvent(QPaintEvent *event);
};

#endif // WIDGET_H

添加资源文件:首先准备一些图片资源文件,并将这些图片资源文件放在同一个文件夹中,将该文件夹复制到项目中。

选中项目文件,鼠标右键 -----> add new...

点击 "add new..." 之后,出现如下界面:在弹出的窗口中,选择 "Qt" 分类下的 "Qt Resource File",点击 "Choose..."。

选择 "Choose..." 之后,给资源文件重命名:输入资源文件名称,点击 "下一步"。

点击 "下一步",出现如下界面,点击 "完成"

给资源文件添加前缀,并将资源文件添加至项目中

将所有的资源文件添加到项目中,方便后续使用

点击 "构建并运行" 按钮,将资源文件添加到项目中

在 "widget.cpp" 文件中实现图片功能

cpp 复制代码
void Widget::paintEvent(QPaintEvent *event)
{
    // 实例化画家对象
    QPainter painter(this);
    // 画图片
    painter.drawPixmap(0, 0, QPixmap(":/mei.webp"));
}

实现效果:在窗口中显示指定的图片。

平移图片

平移图片通过改变坐标来实现,QPainter 类中提供了 translate() 函数来实现坐标原点的改变。示例

cpp 复制代码
void Widget::paintEvent(QPaintEvent *event)
{
    // 实例化画家对象
    QPainter painter(this);
    // 平移
    painter.translate(100, 100);
    // 画图片
    painter.drawPixmap(0, 0, QPixmap(":mei.jpg"));
}

实现效果:图片相对于原来的位置发生了平移。

缩放图片

在 Qt 中,图片的放大和缩小可以使用 QPainter 类中的 drawPixmap 函数来实现。示例

cpp 复制代码
void Widget::paintEvent(QPaintEvent *event)
{
    (void)event;
    QPainter painter(this);
    QPixmap pixmap(":/mei.jpg");
    painter.drawPixmap(0, 0, pixmap);
    painter.drawPixmap(100, 100, 500, 300, pixmap);
}

实现效果:窗口中显示了原图和缩放后的图片。

旋转图片

图片的旋转使用 QPainter 类中的 rotate 函数,它默认以原点为中心进行旋转。如果要改变旋转的中心,可以用 QPaintertranslate 函数。示例

cpp 复制代码
void Widget::paintEvent(QPaintEvent *event)
{
    // 实例化画家对象
    QPainter painter(this);
    // 让旋转的中心为(100,100)
    painter.translate(100, 100);
    painter.rotate(90); // 顺时针旋转90度
    painter.translate(-100, -100); // 恢复坐标
    // 画.drawPixmap(图片
    QPixmap pixmap(":/mei.jpg");
    painter.drawPixmap(0, 0, pixmap);
}

实现效果:图片发生了旋转。

其他设置

移动画家位置

有时在绘制多个图形时,若使用同一坐标位置,绘制出的图形可能会重合。此时,可通过移动画家位置来避免图形重合。

示例 1:未移动画家位置

cpp 复制代码
#include "widget.h"
#include <QPainter>

Widget::Widget(QWidget *parent)
    : QWidget(parent)
{
}

void Widget::paintEvent(QPaintEvent *event)
{
    QPainter painter(this); // 实例化画家对象
    // 画圆
    painter.drawEllipse(QPoint(100, 200), 50, 50); 
    // 画圆,使用同一坐标位置,两个圆会重合,保证就只有一个圆
    painter.drawEllipse(QPoint(100, 200), 50, 50); 
}

实现效果:窗口中只有一个圆。

示例 2:移动画家位置 使用 translate 移动画家所在位置。

cpp 复制代码
#include "widget.h"
#include <QPainter>

Widget::Widget(QWidget *parent)
    : QWidget(parent)
{
}

void Widget::paintEvent(QPaintEvent *event)
{
    QPainter painter(this); // 实例化画家对象
    // 画圆
    painter.drawEllipse(QPoint(100, 200), 50, 50); 
    // 移动画家
    painter.translate(200, 0);
    // 画圆,此时有两个圆
    painter.drawEllipse(QPoint(100, 200), 50, 50); 
}

实现效果:窗口中有两个圆。

保存 / 加载画家的状态

在绘制的过程中,可以通过 save() 函数来保存画家的状态,使用 restore() 函数还原画家状态。

save() 函数原型如下:

cpp 复制代码
void QPainter::save();

restore() 函数原型如下:

cpp 复制代码
void QPainter::restore();

示例

cpp 复制代码
void Widget::paintEvent(QPaintEvent *event)
{
    QPainter painter(this); // 实例化画家对象
    // 画圆
    painter.drawEllipse(QPoint(100, 200), 50, 50); 
    // 往画家移动
    painter.translate(200, 0);
    painter.save(); // 保存画家状态
    // 画圆
    painter.drawEllipse(QPoint(100, 200), 50, 50); 
    painter.translate(200, 0); // 往画家移动
    painter.restore(); // 恢复画家状态
    // 画圆,第二个和第三个重合为一个
    painter.drawEllipse(QPoint(100, 200), 50, 50); 
}

实现效果:第二个和第三个圆重合为一个。

说明:上述示例中,在画第三个圆之前,由于还原了画家的状态,所以此时画家的位置坐标会移动到画家状态保存的地方,所以在绘制第三个圆的位置时其实是和第二个圆发生了重叠。

特殊的绘图设备

前面的代码中我们是使用 QWidget 作为绘图设备,在 Qt 中还存在下列三个比较特殊的绘图设备,此处我们只做个简单的介绍。

  • QPixmap 用于在显示器上显示图片。
  • QImage 用于对图片进行像素级操作。
  • QPicture 用于对 QPainter 的一系列操作进行存档。
QPixmap

QPixmap 特点:

  • 使用 QPainter 直接在上面进行绘制图形。
  • 通过文件路径加载并显示图片。
  • 插上 QPainterdrawPixmap() 函数,可以把这个图片绘制到一个 QLabelQPushButton 等控件上。
  • 和系统 / 显示设备相关,不同系统 / 显示设备下,QPixmap 的显示可能会有所差别。

示例

cpp 复制代码
#include "widget.h"
#include <QPainter>
#include <QPixmap>

Widget::Widget(QWidget *parent)
    : QWidget(parent)
{
    this->resize(600, 500);
}

void Widget::paintEvent(QPaintEvent *event)
{
    // /xp/pic.png(给图片各段仅尺寸为 50*50)
    QPixmap pixmap(":/xp/pic.png");
    QPainter painter(this); // 实例化画家对象
    painter.drawEllipse(QPoint(100, 100), 50, 50); // 设置圆心和半径
    // 保存绘制的图片
    pixmap.save("C:\\Users\\lenovo\\Desktop\\Test\\pic.png");
}
QImage

QImage 特点:

  • 使用 QPainter 直接在上面进行绘制图形。
  • 能对文件路径加载并显示图片。
  • 能通过文件对图片进行像素级操作(修改某一个指定的像素)。
  • 独立于硬件的系统,能够在不同系统之上提供一致的显示。

代码示例QImage 绘图时对像素的修改

头文件中声明事件:

cpp 复制代码
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>

class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = nullptr);
    ~Widget();

    // 声明绘画事件
    void paintEvent(QPaintEvent *event);
};

#endif // WIDGET_H

源文件中实现事件,使用 QImage 对图片的像素进行修改:

cpp 复制代码
#include "widget.h"
#include <QPainter>
#include <QImage>

Widget::Widget(QWidget *parent)
    : QWidget(parent)
{
}

Widget::~Widget()
{
}

void Widget::paintEvent(QPaintEvent *event)
{
    QPainter painter(this); // 实例化画家对象
    QImage img(":/picture/3.jpg"); // 加载图片
    // 修改像素点
    for(int i = 100; i < 200; i++)
    {
        for(int j = 100; j < 200; j++)
        {
            img.setPixelColor(i, j, Qt::blue);
        }
    }
    painter.drawImage(0, 0, img);
}

效果如下

  • 没有修改像素之前:显示原始图片。
  • 修改像素之后:图片对应区域像素变为蓝色。
QPicture

QPicture 特性:

  • 使用 QPainter 直接在上面进行绘制图形。
  • 能通过文件路径加载并显示图片。
  • 独立于硬件的系统,能够在不同系统之上提供一致的显示。
  • QPicture 加载的必须是自身的存文件,而不能是任意的 jpgpng 等图片文件。
  • QPicture 类似于很多游戏的 Replay 功能。

示例

cpp 复制代码
#include "widget.h"
#include <QPainter>
#include <QPicture>

Widget::Widget(QWidget *parent)
    : QWidget(parent)
{
}

void Widget::paintEvent(QPaintEvent *event)
{
    QPicture pic;
    QPainter painter;
    pic.open("C:\\Users\\lenovo\\Desktop\\Test\\pic.pic"); // 设置要保存的路径
    painter.begin(&pic); // 把图画到 pic 上
    painter.setPen(Qt::red); // 设置画笔颜色
    painter.drawEllipse(QPoint(100, 100), 50, 50); // 画圆
    painter.end();
    QPainter painter2(this); // 实例化画家对象
    painter2.drawPicture(0, 0, pic); // 绘制图片
}

Qt 对于界面的美化,还涉及到很多其他的话题,大家未来在工作中如果涉及到了,再针对性学习即可。

  • Qt 动画
  • Qt 3D
  • Qt Quick 三控件
  • Qt 串口
  • Qt 数据库......
相关推荐
掘根3 小时前
【Qt】容器类控件——QTabWidget
开发语言·qt
hqwest3 小时前
QT肝8天07--连接数据库
开发语言·数据库·c++·qt·sqlite·上位机·qt开发
lagelangri6663 小时前
MySql的存储过程以及JDBC实战
android·数据库·mysql
ManThink Technology3 小时前
实用的LoRaWAN 应用层协议规范
开发语言·php
敲代码的嘎仔3 小时前
牛客算法基础noob59 简写单词
java·开发语言·数据结构·程序人生·算法·leetcode·学习方法
catchadmin3 小时前
如何在 PHP 升级不踩坑?学会通过阅读 RFC 提前预知版本变化
开发语言·后端·php
tpoog4 小时前
[C++项目组件]Elasticsearch简单介绍
开发语言·c++·elasticsearch
特立独行的猫a6 小时前
C 语言各种指针详解
java·c语言·开发语言
彭于晏Yan7 小时前
MyBatis-Plus使用动态表名分表查询
java·开发语言·mybatis