目录
初学QT,本节坐标部分包含大量个人理解(先说服自己),仅供参考
1、动态随机绘图
Qt图形系统中的关键角色
- QPainter :Qt中的画家,能够绘制各种基础图形 ,拥有绘图所需的画笔(QPen) , 画刷(QBrush) , 字体(QFont)
- QPaintDevice :Qt中的画布,画家(QPainter)的绘图板 。所有的QWidget类都继承自QPaintDevice (即可以在任意QWidget对象上绘制)
Qt图形系统中的关键角色
QPainter中的所有绘制参数都可以自定义,任意的QWidget对象都能够作为画布绘制图形
画家(QPainter)所使用的工具角色
- QPen :用于绘制几何图形的边缘,由颜色,宽度,线风格等参数组成
- QBrush :用于填充几何图形的调色板,由颜色和填充风格组成
- QFont :用于文本绘制,由字体属性组成
QPainter的基本绘图能力
重要规则 : 只能在 QWidget::paintEvent 中绘制图形
动态绘制图形 (模型视图思想)
1. 根据需要确定参数对象(绘图类型,点坐标,角度,等) - 数据
2. 将参数对象存入数据集合中(如:链表) - 模型
3. 在paintEvent函数中遍历数据集合 - 视图
4. 根据参数对象绘制图形 ( update() ) - 数据发生改变,通过模型改变视图显示
Widget.h
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QPushButton>
#include <QPoint>
#include <QList>
class Widget : public QWidget
{
Q_OBJECT
enum
{
LINE,
RECT,
ELLIPSE
};
// 绘制参数
struct DrawParam
{
int type; // 图形类型
Qt::PenStyle pen; // 画笔类型
QPoint begin;
QPoint end;
};
QPushButton m_testBtn;
QList<DrawParam> m_list; // 用链表保存绘图参数对象
protected slots:
void onTestBtnClicked();
protected:
void paintEvent(QPaintEvent *);
public:
Widget(QWidget *parent = 0);
~Widget();
};
#endif // WIDGET_H
Widget.cpp
#include "Widget.h"
#include <QPainter>
#include <QPoint>
Widget::Widget(QWidget *parent)
: QWidget(parent)
{
m_testBtn.setParent(this);
m_testBtn.move(400, 300);
m_testBtn.resize(70, 30);
m_testBtn.setText("Test");
resize(500, 350);
connect(&m_testBtn, SIGNAL(clicked()), this, SLOT(onTestBtnClicked()));
}
// 工程中通过改变绘图参数进行动态绘图
void Widget::onTestBtnClicked()
{
DrawParam dp =
{
qrand() % 3, //0,1,2
static_cast<Qt::PenStyle>(qrand() % 5 + 1),//1-5
QPoint(qrand() % 400, qrand() % 300),
QPoint(qrand() % 400, qrand() % 300)
}; // 定义绘图参数对象
if( m_list.count() == 5 )
{
m_list.clear(); // 最多只能画5个图案
}
m_list.append(dp); // 放到链表 (模型数据改变,需要通知视图更新数据)
update(); // 强制更新主窗口内容,于是paintEvent函数被调用
}
void Widget::paintEvent(QPaintEvent *)
{
QPainter painter;
painter.begin(this); // 在当前Widget对象上开始绘图,可以手工指定开始结束绘图
for(int i=0; i<m_list.count(); i++)
{
int x = (m_list[i].begin.x() < m_list[i].end.x()) ? m_list[i].begin.x() : m_list[i].end.x();
int y = (m_list[i].begin.y() < m_list[i].end.y()) ? m_list[i].begin.y() : m_list[i].end.y();
int w = qAbs(m_list[i].begin.x() - m_list[i].end.x()) + 1;
int h = qAbs(m_list[i].begin.y() - m_list[i].end.y()) + 1;
painter.setPen(m_list[i].pen);
switch(m_list[i].type)
{
case LINE:
painter.drawLine(m_list[i].begin, m_list[i].end);
break;
case RECT:
painter.drawRect(x, y, w, h);
break;
case ELLIPSE:
painter.drawEllipse(x, y, w, h);
break;
default:
break;
}
}
painter.end();
}
Widget::~Widget()
{
}
2、正弦波形绘图实例
Qt图形系统中的坐标系
-物理坐标系(设备坐标系): 原点(0, 0)在左上角的位置,单位:像素(点), x坐标向右增长,y坐标向下增长
-逻辑坐标系 :数学模型中的抽象坐标系,单位由具体问题决定,坐标轴的增长方向由具体问题决定
视口与窗口
-视口(view port) :物理坐标系中一个任意指定的矩形 , 视口是与设备相关的一个矩形区域,坐标单位是与设备相关的“像素”
-窗口(window) :逻辑坐标系下对应到物理坐标系中的相同矩形 。窗口的坐标是逻辑坐标,与设备无关,可能是像素、毫米或者英寸
-视口与窗口是不同坐标系中的同一个图形,用窗口坐标表达的图形长和宽与视口的坐标系统表达的长和宽是不同的。二者就定义了这两个坐标系统的比例关系。程序作图时,使用的坐标总是是窗口坐标。而实际的显示或输出设备却各有自己的坐标
-QPainter使用逻辑坐标系绘制图形 ,逻辑坐标系中图形的大小和位置经由转换后绘制于具体设备
-默认情况下的逻辑坐标系与物理坐标系完全一致 (逻辑坐标生成的图形显示输出在物理坐标的(0,0)位置)
-窗口和视口可以互相转换(线性)
Widget.cpp
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
class Widget : public QWidget
{
Q_OBJECT
protected:
void paintEvent(QPaintEvent *);
public:
Widget(QWidget *parent = 0);
~Widget();
};
#endif // WIDGET_H
Widget.cpp
#include "Widget.h"
#include <QPainter>
#include <QPointF>
#include <QPen>
#include <qmath.h>
Widget::Widget(QWidget *parent)
: QWidget(parent)
{
setFixedSize(600, 600);
}
void Widget::paintEvent(QPaintEvent *)
{
QPainter painter(this);
QPen pen;
pen.setColor(Qt::green);
pen.setStyle(Qt::SolidLine);
pen.setWidthF(0.1); // 使用浮点精度将笔宽设置为给定的宽度
painter.setPen(pen);
painter.setViewport(40, 40, width()-80, height()-80); // 将QPainter的视口矩形设置为给定的矩形,并启用视图转换。
painter.setWindow(-10, -10, 20, 20); // 将QPainter的窗口设置为给定的矩形,并启用视图转换。
painter.fillRect(-10, -10, 20, 20, Qt::black); // QPainter使用逻辑坐标系
painter.drawLine(QPointF(-10, 0), QPointF(10, 0)); // x
painter.drawLine(QPointF(0, 10), QPointF(0, -10)); // y
for(float x=-10; x<10; x+=0.01)
{
float y = qSin(x);
painter.drawPoint(QPointF(x, y));
}
}
Widget::~Widget()
{
}
通过最后一个样例可以发现这样来平移图形(通过改变视口)很不直观且不方便。视口使得不必关心输出设备的大小与分辨率,这应该是其主要功能吧,所以不应随便改变,有没有其他方式平移图像呢?
3、简易绘图程序
Widget.h
#ifndef WIDGET_H
#define WIDGET_H
#include <QtGui/QWidget>
#include <QRadioButton>
#include <QComboBox>
#include <QGroupBox>
#include <QList>
#include <QPoint>
class Widget : public QWidget
{
Q_OBJECT
// 绘图类型
enum DrawType
{
NONE,
FREE,
LINE,
RECT,
ELLIPSE
};
// 绘图绘制参数: 绘图类型,颜色,坐标值
struct DrawParam
{
DrawType type;
Qt::GlobalColor color;
QList<QPoint> points;
};
QGroupBox m_group;
QRadioButton m_freeBtn;
QRadioButton m_lineBtn;
QRadioButton m_rectBtn;
QRadioButton m_ellipseBtn;
QComboBox m_colorBox;
QList<DrawParam> m_drawList; // 已经绘制结束的图形参数
DrawParam m_current; // 当前正在绘制的图形参数
DrawType drawType();
Qt::GlobalColor drawColor();
void draw(QPainter& painter, DrawParam& param);
void append(QPoint p);
protected:
void mousePressEvent(QMouseEvent *evt);
void mouseMoveEvent(QMouseEvent *evt);
void mouseReleaseEvent(QMouseEvent *evt);
void paintEvent(QPaintEvent *);
public:
Widget(QWidget *parent = 0);
~Widget();
};
#endif // WIDGET_H
Widget.cpp
#include "Widget.h"
#include <QMouseEvent>
#include <QPainter>
#include <QPen>
#include <QBrush>
Widget::Widget(QWidget *parent)
: QWidget(parent)
{
m_group.setParent(this);
m_group.setTitle("Setting");
m_group.resize(600, 65);
m_group.move(20, 20);
m_freeBtn.setParent(&m_group);
m_freeBtn.setText("Free");
m_freeBtn.resize(70, 30);
m_freeBtn.move(35, 20);
m_freeBtn.setChecked(true);
m_lineBtn.setParent(&m_group);
m_lineBtn.setText("Line");
m_lineBtn.resize(70, 30);
m_lineBtn.move(140, 20);
m_rectBtn.setParent(&m_group);
m_rectBtn.setText("Rect");
m_rectBtn.resize(70, 30);
m_rectBtn.move(245, 20);
m_ellipseBtn.setParent(&m_group);
m_ellipseBtn.setText("Ellipse");
m_ellipseBtn.resize(70, 30);
m_ellipseBtn.move(350, 20);
m_colorBox.setParent(&m_group);
m_colorBox.resize(80, 25);
m_colorBox.move(480, 23);
m_colorBox.addItem("Black");
m_colorBox.addItem("Blue");
m_colorBox.addItem("Green");
m_colorBox.addItem("Red");
m_colorBox.addItem("Yellow");
setFixedSize(width(), 600);
m_current.type = NONE;
m_current.color = Qt::white;
m_current.points.clear();
}
// 返回用户选择的绘图类型
Widget::DrawType Widget::drawType()
{
DrawType ret = NONE;
if( m_freeBtn.isChecked() ) ret = FREE;
if( m_lineBtn.isChecked() ) ret = LINE;
if( m_rectBtn.isChecked() ) ret = RECT;
if( m_ellipseBtn.isChecked() ) ret = ELLIPSE;
return ret;
}
// 返回用户选择的颜色
Qt::GlobalColor Widget::drawColor()
{
Qt::GlobalColor ret = Qt::black;
if( m_colorBox.currentText() == "Black") ret = Qt::black;
if( m_colorBox.currentText() == "Blue") ret = Qt::blue;
if( m_colorBox.currentText() == "Green") ret = Qt::green;
if( m_colorBox.currentText() == "Red") ret = Qt::red;
if( m_colorBox.currentText() == "Yellow") ret = Qt::yellow;
return ret;
}
// 以鼠标按下为开始,记录开始坐标
void Widget::mousePressEvent(QMouseEvent *evt)
{
m_current.type = drawType(); // 确定当前要绘制的图形与颜色
m_current.color = drawColor();
m_current.points.append(evt->pos()); // 保存鼠标按下坐标点
}
// 将鼠标移动时经过的每个坐标作为临时结束坐标
void Widget::mouseMoveEvent(QMouseEvent *evt)
{
append(evt->pos()); // 保存按下鼠标移动时的坐标点
update(); // 实时绘制图案
}
// 以鼠标释放为结束,确定最终结束坐标
void Widget::mouseReleaseEvent(QMouseEvent *evt)
{
append(evt->pos());
m_drawList.append(m_current); // 放到已绘制参数链表
m_current.type = NONE; // 当前绘制参数清空
m_current.color = Qt::white;
m_current.points.clear();
update();
}
void Widget::append(QPoint p)
{
if( m_current.type != NONE )
{
if( m_current.type == FREE )
{
m_current.points.append(p);
}
else
{
if( m_current.points.count() == 2 )
{
m_current.points.removeLast(); // 绘制直线,矩形和椭圆只需要两个点
}
m_current.points.append(p);
}
}
}
void Widget::paintEvent(QPaintEvent *)
{
QPainter painter(this);
for(int i=0; i<m_drawList.count(); i++)
{
draw(painter, m_drawList[i]); // 绘制已经完成的图形
}
draw(painter, m_current); // 正在绘制的图形
}
// 在开始坐标和结束坐标之间绘制目标图形
void Widget::draw(QPainter& painter, DrawParam& param)
{
if( (param.type != NONE) && (param.points.count() >= 2) )
{
// 获得图形左上角坐标和宽高
int x = (param.points[0].x() < param.points[1].x()) ? param.points[0].x() : param.points[1].x();
int y = (param.points[0].y() < param.points[1].y()) ? param.points[0].y() : param.points[1].y();
int w = qAbs(param.points[0].x() - param.points[1].x()) + 1;
int h = qAbs(param.points[0].y() - param.points[1].y()) + 1;
painter.setPen(QPen(param.color));
painter.setBrush(QBrush(param.color));
switch(param.type)
{
case FREE:
for(int i=0; i<param.points.count()-1; i++)
{
painter.drawLine(param.points[i], param.points[i+1]);
}
break;
case LINE:
painter.drawLine(param.points[0], param.points[1]);
break;
case RECT:
painter.drawRect(x, y, w, h);
break;
case ELLIPSE:
painter.drawEllipse(x, y, w, h);
break;
default:
break;
}
}
}
Widget::~Widget()
{
}
4、文本绘制
QPainter拥有绘制文本的能力
-drawText (拥有多个重载形式)
★ p.drawText(10, 10, "D.T.Software"); // 在坐标(10, 10)处绘制文本
★ p.drawText(0, 0,100,30,Qt::AlignCenter, "D.T.Software"); // 在矩形范围(0, 0, 100, 30)中以居中对齐的方式绘制文本
-QPainter能够将文本绘制于图片(图片水印)
文本绘制参数
-字体(QFont) , 颜色(QColor) :控制文本大小,风格,颜色,等
-坐标(QPoint) , 角度(rotate) :文本绘制的位置(对齐该坐标) ,rotate以绘制坐标为圆心顺时针旋转
实例分析
Widget.h
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QTimer>
class Widget : public QWidget
{
Q_OBJECT
int m_sizeFactor;
QTimer m_timer;
protected slots:
void onTimeout();
protected:
void paintEvent(QPaintEvent *);
public:
Widget(QWidget *parent = 0);
~Widget();
};
#endif // WIDGET_H
Widget.cpp
#include "Widget.h"
#include <QPainter>
#include <QFontMetrics>
#include <QFont>
#include <QRect>
Widget::Widget(QWidget *parent)
: QWidget(parent)
{
m_sizeFactor = 0;
m_timer.setParent(this);
connect(&m_timer, SIGNAL(timeout()), this, SLOT(onTimeout())); // 可以直接使用 update 作为槽函数
m_timer.start(50);
}
void Widget::onTimeout()
{
update(); // 每隔50ms更新一次界面
}
void Widget::paintEvent(QPaintEvent *)
{
QPainter painter;
const QString text = "Hello";
QFont font("Comic Sans MS", 5 + (m_sizeFactor++) % 100);
QFontMetrics metrics(font);
const int w = metrics.width(text); // 获取font字体下text宽度
const int h = metrics.height();
QRect rect((width()-w)/2, (height()-h)/2, w, h);
painter.begin(this);
painter.setPen(Qt::blue);
painter.setFont(font);
painter.drawText(rect, Qt::AlignCenter, text);
painter.end();
}
Widget::~Widget()
{
}
5、世界转换
QPainter使用逻辑坐标系统绘制图形,使用设备自己的坐标系统绘制于具体设备显示。但QPainter也支持仿射坐标变换(affine coordinate transformations)( QT文档查 Coordinate System )
窗口用于逻辑坐标系下的图形绘制 ,视口用于实际物理设备上的图形显示 。窗口与视口无法实现图形的平移(translate),旋转(rotate),缩放(scale),扭曲(shear),因此需要新的坐标转换机制支持
QT中的QTransform与QMatrix指定了坐标系统的2D转换,QTransform是QMatrix的进化,使用方式几乎一致,更推荐使用QTransform
QTransform与QMatrix的不同之处在于,它是一个真正的3x3矩阵,QTransform允许 投影变换(perspective transformations)
QMatrix和QTransform都可以使用公式将平面上的一个点(x, y)变换为另一个点(x', y')。具体公式参见QT文档
QMatrix和QTransform的成员变量
-dx和dy元素表示水平和垂直的平移(translate函数的参数)
-m11和m22元素指定了水平和垂直缩放(scale函数参数)
-m21和m12元件指定水平和垂直扭曲(shear函数参数)
-m13和m23元素指定了水平和垂直投影,m33作为一个额外的投影因子
注:后文提到的 世界逻辑坐标 与 窗口逻辑坐标 是我为帮助自己理解造的词...
代码理解
Widget::Widget(QWidget *parent)
: QWidget(parent)
{
setFixedSize(600, 600);
}
void Widget::paintEvent(QPaintEvent*)
{
QPainter painter(this);
// 请先忽视这条注释
// painter.setViewport(10, 10, width()-10, height()-10);
// 绘制蓝色矩形
painter.setPen(QPen(Qt::blue, 1, Qt::DashLine));
painter.drawRect(0, 0, 200, 200); // 过程:采用窗口逻辑坐标绘制了矩形,采用物理坐标绘制到了默认的(0, 0)
painter.drawRect(0, 0, 150, 150);
// 如果世界转换被启用,返回true;否则返回false。输出false
qDebug() << painter.matrixEnabled() << endl; // 输出false,说明还没有启用世界转换
// 绘制黄色矩形
painter.setPen(QPen(Qt::yellow,1));
QMatrix matrix;
matrix.translate(200, 200); // 发生世界转换
matrix.scale(1.5, 1.5); // 绘制的图形宽高都放大1.5倍
painter.setMatrix(matrix);
painter.drawRect(0, 0, 100, 100); // 这些世界逻辑坐标被映射到窗口逻辑坐标.如矩形右上角坐标从(100,0)转换为窗口的(350,200)。实际编写时只需关注这些局部的逻辑坐标
qDebug() << painter.matrixEnabled() << endl; // 输出true
// 绘制红色坐标系
painter.setPen(QPen(Qt::red, 1));
painter.drawLine(-30, 0, 30, 0); // 此时再又会发生世界逻辑坐标到窗口逻辑坐标的转换
painter.drawLine(0, 30, 0, -30);
matrix.rotate(30);
painter.setMatrix(matrix);
// 绘制绿色坐标系
painter.setPen(QPen(Qt::green, 1));
painter.drawLine(-60, 0, 60, 0); // 此时再又会发生世界逻辑坐标到窗口逻辑坐标的转换
painter.drawLine(0, 60, 0, -60);
matrix.rotate(-30);
painter.setMatrix(matrix);
painter.setPen(QPen(Qt::black, 1));
// 绘制简单时钟
for (int i = 1; i <= 60 ; ++i)
{
if (i % 5 == 1)
{
painter.drawLine(0, -90, 0, -100); // 世界逻辑坐标上方为y轴的负方向
painter.drawText(-4, -75, tr("%1").arg((i == 1) ? 12 : i/5)); // 为了文字的处于线的中间,偏移-4
}
else
{
painter.drawLine(0, -95, 0, -100);
}
matrix.rotate(6);
painter.setMatrix(matrix);
}
}
代码中的 QMatrix matrix; painter.setMatrix(matrix); 可替换为 QTransform ts; painter.setWorldTransform(ts); 效果一致
总结
没有使用图形的平移(translate),旋转(rotate),缩放(scale),扭曲(shear)操作时,只使用窗口逻辑坐标,然后视口显示
当使用了这些操作会启用世界转换,将世界逻辑坐标转换为窗口逻辑坐标,然后视口显示
实际完全不需要显式使用QMatrix或QTransform ,因为QPainter可以直接使用translate、rotate等操作,应该是当使用这些操作时默认使用QTransform
Widget::Widget(QWidget *parent)
: QWidget(parent)
{
setFixedSize(600, 600);
}
void Widget::paintEvent(QPaintEvent*)
{
QPainter painter(this);
// 请先忽视这条注释
//painter.setViewport(10, 10, width()-10, height()-10);
// 绘制蓝色矩形
painter.setPen(QPen(Qt::blue, 1, Qt::DashLine));
painter.drawRect(0, 0, 200, 200); // 过程:采用逻辑坐标绘制了矩形,采用物理坐标绘制到了默认的(0, 0)
painter.drawRect(0, 0, 150, 150);
// 如果世界转换被启用,返回true;否则返回false。输出false
qDebug() << painter.matrixEnabled() << endl; // 输出false,说明还没有启用世界转换
// 绘制黄色矩形
painter.setPen(QPen(Qt::yellow,1));
painter.translate(200, 200); // 发生世界转换
painter.scale(1.5, 1.5); // 绘制的图形宽高都放大1.5倍
painter.drawRect(0, 0, 100, 100); // 这些世界逻辑坐标被映射到窗口逻辑坐标.如矩形右上角坐标从(0,100)转换为窗口的(350,200)。实际编写时只需关注这些局部的逻辑坐标
qDebug() << painter.matrixEnabled() << endl; // 输出true
// 绘制红色坐标系
painter.setPen(QPen(Qt::red, 1));
painter.drawLine(-30, 0, 30, 0); // 此时再又会发生世界逻辑坐标到窗口逻辑坐标的转换
painter.drawLine(0, 30, 0, -30);
painter.rotate(30);
// 绘制绿色坐标系
painter.setPen(QPen(Qt::green, 1));
painter.drawLine(-60, 0, 60, 0); // 此时再又会发生世界逻辑坐标到窗口逻辑坐标的转换
painter.drawLine(0, 60, 0, -60);
painter.rotate(-30);
painter.setPen(QPen(Qt::black, 1));
// 绘制简单时钟
for (int i = 1; i <= 60 ; ++i)
{
if (i % 5 == 1)
{
painter.drawLine(0, -90, 0, -100);
painter.drawText(-4, -75, tr("%1").arg((i == 1) ? 12 : i/5)); // 由于为了文字的处于线的中间,所有偏移-4
}
else
{
painter.drawLine(0, -95, 0, -100);
}
painter.rotate(6);
}
}
绘制图片
Widget::Widget(QWidget *parent)
: QWidget(parent)
{
setFixedSize(600, 600);
}
void Widget::paintEvent(QPaintEvent*)
{
QPainter painter(this);
QFont font(painter.font());
font.setBold(true);
painter.setFont(font);
painter.setPen(Qt::green);
painter.setRenderHint(QPainter::Antialiasing);//尽可能消除边缘锯齿
painter.drawPixmap(QRect(0, 0, 400, 225), QPixmap(":/res/pic.png").scaled(400, 225));
painter.translate(200, 200);
painter.drawLine(0, -20, 0, 20);
painter.drawLine(20, 0, -20, 0);
painter.rotate(30);
painter.scale(0.6, 0.6); // 实际生成会被缩放为0.6倍
//painter.shear(0.4, 0);
painter.drawPixmap(QRect(0, 0, 400, 225), QPixmap(":/res/pic.png").scaled(400, 225));
}
扩展:自己可以尝试参考QT文档中的样例,完成动态时钟的绘制,效果如下