中级篇
第6章 2D绘图
Qt4中的2D绘图部分称为Arthur绘图系统,它由三个主要的类支撑起整个框架:QPainter,QPaintDevice和QPainterEngine。QPainter用来执行具体的绘图相关操作,如画点、画线、填充、变换、alpha通道等。QPaintDevice是QPainter用来绘图的绘图设备,Qt中有几种预定义的绘图设备,如QWidget,QPixmap,QPrinter等,它们都是从QPaintDevice继承。QPaintEngine类提供了不同类型设备的接口,QPaintEngine对程序员不透明,由QPainter,QPaintDevice类与其进行交互。
6.1 Arthur绘图基础
在Arthur绘图框架中的基本绘图元素是画笔、画刷,本章将首先从这些最基本的绘图对象开始讲解。在讲解过程中,将贯穿一个基本的绘图程序。
6.1.1 绘图
QPainter类具有GUI程序需要的绝大多数函数,能够绘制基本的图形(点、线、矩形、多边形等)以及复杂的图形(如绘图路径)。使用绘图路径(QPaintPath)的优点是复杂形状的图形只用生成一次,以后再绘制时只需调用QPainter::drawPath()就可以了。QPainterPath对象可以用来填充、绘制轮廓。
线和轮廓使用画笔(QPen)进行绘制,画刷(QBrush)进行填充。画笔定义了风格(线形)、宽度、笔尖画刷以及端点是如何绘制的(cap-style)、端点的连接方式(join-style)。画刷用来填充画笔绘制的图形,可以定义不同填充模式和颜色的画刷。
当绘制文字时,字体使用QFont类定义。Qt使用指定字体的属性,如果没有匹配的字体,Qt将使用最接近的字体。字体的属性可以使用QFontInfo类获取。字体的度量(measurement)使用QFontMetrics类来获取。QFontDatabase类可以得到底层窗口系统的所有可用字体的信息。
通常情况下,QPainter以默认的坐标系统进行绘制,也可以使用QMatrix类对坐标进行变换。
当绘制时,可以使用QPainter::RenderHint来告诉绘图引擎是否启用反锯齿功能使绘图更为平滑。QPainter::RenderHint的可取如表6-1中的值。
表6-1 绘制提示
扩展阅读
最近邻插值算法与双线性插值算法
最近邻插值算法(nearest neighbor)通过对目标像素进行反向变换得到一个浮点坐标,然后对其进行取整得到整数坐标,这个整数坐标对应的像素值就是目的像素的像素值,也就是浮点坐标最邻近的左上角点对应的像素值。因此,最近邻插值算法简单,运算量小,但得到的图像质量不高。
双线性插值算法(bilinear)中,对于一个目的像素,设坐标通过反向变换得到的浮点坐标为(i+x,j+y),其中i、j均为非负整数,x、y为 [0,1)区间的浮点数,则这个像素的值 f(i+x,j+y) 可由源图像中坐标为 (i,j)、(i+1,j)、(i,j+1)、(i+1,j+1)所对应的周围四个像素的值决定,即:
f(i+x,j+y) = (1-x)(1-y)f(i,j) + (1-x)yf(i,j+1) + x(1-y)f(i+1,j) + xyf(i+1,j+1)
双线性内插值法计算量大,但插值后图像质量较高,不会出现像素值不连续的情况。
在Qt中,绘制各种图形的函数属于QPainter类。QPainter提供的绘图函数如表6-2所示。
表6-2 QPainter的绘图函数
上面的大多数绘图函数从函数名就能够知道其功能,可能只有drawPicture()函数需要解释一下。drawPicture()函数负责绘制QPicture中存储的QPainter指令,QPicture是可以记录QPainter绘图指令的类,它将QPainter的绘图指令串行化为平台无关的格式存储。下面的代码将记录的绘图指令重绘:
QPicture picture; picture.load("mypicture.pic"); QPainter painter(this); painter.drawPicture(0, 0, picture);
上面的代码片段装入绘图文件mypicture.pic,并在点(0,0)处重放绘图指令,也可以用QPicture::play()函数来完成相同的功能。
1.使用画笔
Qt使用QPen类定义画笔。画笔的属性包括线型、线宽、颜色等。画笔的属性可以在构造函数中指定,也可以使用setStyle(),setWidth(),setBrush(),setCapStyle(),setJoinStyle()等函数逐项设定画笔的各项属性。
线宽为0的画笔称为装饰笔。不论QPainter如何进行变换,装饰笔总是1个像素。
(1)画笔风格(Pen Style)
在Qt中使用Qt::PenStyle定义了6 种画笔风格,分别是Qt::SolidLine,Qt::DashLine,Qt::DashDotLine,Qt::DashDotDotLine,Qt::CutsomDashLine。如图6-1所示。
图6-1 画笔风格
默认的画笔风格是Qt::SolidLine。实际上还有一种风格是Qt::NoPen,使用Qt::NoPen时QPainter不绘制线。如果要使用自定义的线风格(Qt::CustomDashLine),则需要使用QPen的setDashPattern()函数来设置自定义风格。
下面的代码设置了一个自定义的QPen。
QPen pen; QVector<qreal> customDashes; qreal blank = 4; dashes << 2 << blank << 4 << blank << 8 << blank << 16 << blank << 32; pen.setDashPattern(customDashes);
在上面的代码中使用了QVector定义自定义风格的画笔,在QVector中奇数位置的数是实线的长度,偶数位置的数是空白的长度。
(2)端点风格(Cap Style)
端点风格决定了线的端点样式,它只对线宽大于等于1 的线有效,对装饰笔绘制的线无效。Qt中定义了三种端点风格,用枚举类型Qt::PenCapStyle表示,分别为Qt::SquareCap, Qt::FlatCap, Qt::RoundCap。Qt::SquareCap类型的端点是方形的并将端点延长至线宽的一半,Qt::FlatCap也是方形端点但没有将线延长,Qt::RoundCap是圆形的端点。不同的端点风格如图6-2所示。
图6-2 端点风格
(3)连接风格(Join Style)
连接风格是指两条线如何连接,连接风格仅对线宽大于等于1的线有效,对装饰笔绘制的线无效。Qt定义了四种连接方式,用枚举类型Qt::PenStyle表示,分别是Qt::MiterJoin,Qt::BevelJoin,Qt::RoundJoin和Qt::SvgMiterJoin。Qt::BevelJoin风格削去了连接中心线前段的三角形,Qt::MiterJoin填充了这个三角形,Qt::RoundJoin在Qt::BevelJoin的基础上绘制了弧形使得连接平滑。Qt::SvgMiterJoin是SVG 1.2 Tiny中定义的连接风格。默认的连接风格是Qt::BevelJoin。各种连接风格如图6-3所示。
图6-3 连接风格
下面通过一个绘图程序来讲解如何绘制简单图形,工程名命名为basicdraw.pro。示例使用调色板来设定当前画笔和接下来要讲的画刷等各项绘图元素。调色板在一个QDockWidget上生成,其文件定义如下:
#ifndef PALETTE_H #define PALETTE_H #include <QtGui> #include "previewlabel.h" class Palette : public QWidget { Q_OBJECT public: Palette(QWidget *parent = 0); signals: void penChanged(QPen& pen); void brushChanged(QBrush& brush); private slots: void penChanged(); void brushChanged (); private: QLabel *penColorLabel; QLabel *penWidthLabel; QLabel *penStyleLabel; QLabel *brushColorLabel; QLabel *brushStyleLabel; PreviewLabel *preLabel; QSpinBox *penWidthSpinBox; QComboBox *penColorComboBox; QComboBox *penStyleComboBox; QComboBox *brushColorComboBox; QComboBox *brushStyleComboBox; void createColorComboBox(QComboBox *comboBox); void createStyleComboBox(); }; #endif
在该类中定义了两个信号,当画笔发生改变时发出penChanged()信号,画刷发生改变时发出brushChanged()信号。调色板中包括用来改变线宽、画笔颜色、风格等窗口部件。
调色板类构造函数的实现如下:
Palette::Palette(QWidget *parent) : QWidget(parent) { penColorComboBox = new QComboBox; createColorComboBox(penColorComboBox); penColorLabel = new QLabel(tr("Pen Color:")); penColorLabel->setBuddy(penColorComboBox); penWidthSpinBox = new QSpinBox; penWidthSpinBox->setRange(0, 20); penWidthSpinBox->setSpecialValueText(tr("0 (cosmetic pen)")); penWidthLabel = new QLabel(tr("Pen &Width:")); penWidthLabel->setBuddy(penWidthSpinBox); createStyleComboBox(); penStyleLabel = new QLabel(tr("&Pen Style:")); penStyleLabel->setBuddy(penStyleComboBox); brushColorComboBox = new QComboBox(); createColorComboBox(brushColorComboBox); brushColorLabel = new QLabel(tr("Brush Color:")); brushColorLabel->setBuddy(brushColorComboBox); brushStyleComboBox = new QComboBox; brushStyleComboBox->addItem(tr("None"), Qt::NoBrush); …… brushStyleComboBox->addItem(tr("Linear Gradient"), Qt::LinearGradientPattern); brushStyleComboBox->addItem(tr("Radial Gradient"), Qt::RadialGradientPattern); brushStyleComboBox->addItem(tr("Conical Gradient"), Qt::ConicalGradientPattern); brushStyleComboBox->addItem(tr("Texture"), Qt::TexturePattern); brushStyleLabel = new QLabel(tr("&Brush Style:")); brushStyleLabel->setBuddy(brushStyleComboBox); preLabel = new PreviewLabel(this); connect(penColorComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(penChanged())); connect(penWidthSpinBox, SIGNAL(valueChanged(int)), this, SLOT(penChanged())); connect(penStyleComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(penChanged())); connect(brushColorComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(brushChanged())); connect(brushStyleComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(brushChanged())); connect(this, SIGNAL(penChanged(QPen&)), preLabel, SLOT(penChanged(QPen&))); connect(this, SIGNAL(brushChanged(QBrush&)), preLabel, SLOT(brushChanged(QBrush&))); QGridLayout *mainLayout = new QGridLayout; mainLayout->addWidget(penColorLabel, 0, 0, Qt::AlignRight); mainLayout->addWidget(penColorComboBox, 0, 1); mainLayout->addWidget(penWidthLabel, 1, 0, Qt::AlignRight); mainLayout->addWidget(penWidthSpinBox, 1, 1); mainLayout->addWidget(penStyleLabel, 2, 0, Qt::AlignRight); mainLayout->addWidget(penStyleComboBox, 2, 1); mainLayout->addWidget(brushColorLabel, 3, 0, Qt::AlignRight); mainLayout->addWidget(brushColorComboBox, 3, 1); mainLayout->addWidget(brushStyleLabel, 4, 0, Qt::AlignRight); mainLayout->addWidget(brushStyleComboBox, 4, 1); mainLayout->addWidget(preLabel, 5, 0, 6, 2); setLayout(mainLayout); penChanged(); brushChanged(); setWindowTitle(tr("Basic Drawing")); }
在构造函数中初始化了每个选择项的条目。注意设置线宽的初始值用了
penWidthSpinBox->setSpecialValueText(tr("0 (cosmetic pen)"));
setSpecialValue()函数是QSpinBox类的特殊用法,用来在QSpinBox中显示文字而不是默认的数值。
画笔线型组合框中的每个条目是使用不同风格绘制的直线,这需要一个单独的函数createStyleComboBox()来创建,后面将会讲到。
接下来是画笔各属性改变时发出信号通知相应对象的函数:
void Palette::penChanged() { QPen pen; int width = penWidthSpinBox->value(); pen.setWidth(width); QColor color = penColorComboBox->itemData( penColorComboBox->currentIndex(), Qt::UserRole).value<QColor>(); pen.setColor(color); Qt::PenStyle penStyle = (Qt::PenStyle)penStyleComboBox->itemData( penStyleComboBox->currentIndex(), Qt::UserRole).toInt(); pen.setStyle(penStyle); emit penChanged(pen); }
由于要在改变画笔颜色组合框的条目上加上相应的颜色的标识,所以在创建画笔颜色组合框时进行一些特殊处理。
void Palette::createColorComboBox(QComboBox *comboBox) { QPixmap pix(16, 16); QPainter pt(&pix); pt.fillRect(0, 0, 16, 16, Qt::black); comboBox->addItem(QIcon(pix), tr("black"), Qt::black); pt.fillRect(0, 0, 16, 16, Qt::red); comboBox->addItem(QIcon(pix), tr("red"), Qt::red); pt.fillRect(0, 0, 16, 16, Qt::green); comboBox->addItem(QIcon(pix), tr("green"), Qt::green); pt.fillRect(0, 0, 16, 16, Qt::blue); comboBox->addItem(QIcon(pix), tr("blue"), Qt::blue); pt.fillRect(0, 0, 16, 16, Qt::yellow); comboBox->addItem(QIcon(pix), tr("yellow"), Qt::yellow); pt.fillRect(0, 0, 16, 16, Qt::cyan); comboBox->addItem(QIcon(pix), tr("cyan"), Qt::cyan); pt.fillRect(0, 0, 16, 16, Qt::magenta); comboBox->addItem(QIcon(pix), tr("magenta"), Qt::magenta); }
在上面的函数中,分别为7种颜色生成了1幅16×16的QPixmap图像,图像就是相应颜色填充的矩形。该图像直接显示在组合框的条目中。
由于要在组合框的每个条目上显示画笔线型的实际效果,所以要实现组合框条目的代理类,让代理类进行实际的条目绘制。创建线型组合框的函数实现如下:
void Palette::createStyleComboBox() { penStyleComboBox = new QComboBox; penStyleComboBox->setItemDelegate( new QPenStyleDelegate((QObject *)penStyleComboBox)); penStyleComboBox->addItem(tr("Solid"), Qt::SolidLine); penStyleComboBox->addItem(tr("Dash"), Qt::DashLine); penStyleComboBox->addItem(tr("Dot"), Qt::DotLine); penStyleComboBox->addItem(tr("Dash Dot"), Qt::DashDotLine); penStyleComboBox->addItem(tr("Dash Dot Dot"), Qt::DashDotDotLine); penStyleComboBox->addItem(tr("None"), Qt::NoPen); }
组合框条目的线型代理类QpenStyleDelegate用来完成条目的绘制,以实现特殊效果。线型代理类定义如下所示:
#ifndef QPENSTYLEDELEGATE_H #define QPENSTYLEDELEGATE_H #include <QtGui> class QPenStyleDelegate : public QAbstractItemDelegate { Q_OBJECT public: QPenStyleDelegate(QObject *parent = 0); void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const; QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const; }; #endif
代理类中的paint()函数负责条目的绘制,sizeHint()确定条目的大小。代理类的绘图函数实现如下:
void QPenStyleDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { QString text = index.data(Qt::DisplayRole).toString(); Qt::PenStyle penStyle = (Qt::PenStyle)index.data(Qt::UserRole).toInt(); QRect r = option.rect; if (option.state & QStyle::State_Selected) { painter->save(); painter->setBrush(option.palette.highlight()); painter->setPen(Qt::NoPen); painter->drawRect(option.rect); painter->setPen(QPen(option.palette.highlightedText(),2, penStyle)); } else painter->setPen(penStyle); painter->drawLine(0, r.y() + r.height()/2, r.right(), r.y() + r.height()/2); if (option.state & QStyle::State_Selected) painter->restore(); }
绘制函数中要对选项是否被选中进行判断,如果选中,则需要高亮显示。对于条目的大小,简单地返回宽100高30的矩形作为一个条目所占的空间,代码如下:
QSize QPenStyleDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { return QSize(100,30); }
这里生成了画笔的调色板,具体绘制在后面将会逐步展开。
2.画刷
在Qt中图形使用QBrush填充。画刷包括填充颜色和风格(填充模式)。在Qt中,颜色使用QColor类表示。QColor支持RGB、HSV、CMYK颜色模型。QColor还支持alpha混合的轮廓和填充(可以实现透明效果),QColor类与平台、设备无关(通过QColormap类和硬件进行映射)。
填充模式由Qt::BrushStyle枚举变量定义,包括基本模式填充、渐变填充和纹理填充。基本模式填充包括由各种点、线组合的模式。下面的例子将展示Qt中预定义的填充模式,在这个例子中,通过在窗口部件上用各种模式填充不同的矩形来观察填充效果。
扩展阅读
QColor使用的颜色模型
Qt支持RGB、HSV和CMYK颜色模型。RGB是面向硬件的模型,颜色由红、绿、蓝三种基色混合而成。HSV模型则比较符合人对颜色的感觉,由色调(0~359)、饱和度(0~255)、亮度(0~255)组成。CMYK由青、洋红、黄、黑四种基色组成,主要用在打印机等硬拷贝设备上,每个颜色分量的取值为0~255。另外QColor还可以使用SVG 1.0中定义的任何颜色名为参数初始化。
该工程名为paintertest.pro,演示了画笔、画刷等种绘图元素,其中画刷填充模式的绘制函数如下:
void TestWidget::paintBrushStyle(QPainter& painter) { list.clear(); list << "Qt::NoBrush" << "Qt::SolidPattern"<< "Qt::Dense1Pattern" << "Qt::Dense2Pattern" << "Qt::Dense3Pattern" << "Qt::Dense4Pattern" << "Qt::Dense5Pattern" << "Qt::Dense6Pattern" << "Qt::Dense7Pattern" << "Qt::HorPattern" << "Qt::VerPattern" << "Qt::CrossPattern" << "Qt::BDiagPattern" << "Qt::FDiagPattern" << "Qt::DiagCrossPattern"; QBrush brush; QRect rect; int x = 10; int y = 10; for(int i=Qt::NoBrush; i <= Qt::DiagCrossPattern; i++) { brush.setStyle((Qt::BrushStyle)i); painter.setBrush(brush); rect.setRect(x, y, w, h); painter.drawRect(rect); painter.drawText(x, y + h + 15, list.at(i)); if((i+1)%4 == 0) { x = 10; y += h + h/2; } else x += w + w/2; } }
函数的paintEvent通过变换矩形的位置和填充模式,绘出了各种填充模式。运行效果如图6-4所示。
图6-4 Qt基本填充模式
Qt4提供了渐变填充的画刷。渐变填充包括两个要素:颜色的变化和路径的变化。颜色变化可以指定从一种颜色渐变到另外一种颜色,也可以在变化的路径上指定一些点的颜色进行分段渐变。在Qt4中,提供了三种渐变填充:线性(QLinearGradient)、圆形(QRadicalGradient)和圆锥渐变(QConicalGradient)。所有的类均从QGradient类继承。
线性渐变填充指定两个控制点,画刷在两个控制点之间进行颜色插值。通过创建QLinearGradient对象来设置画刷。如下例所示:
QLinearGradient linearGradient(0, 0, 200, 100); linearGradient.setColorAt(0, Qt::red); linearGradient.setColorAt(0.5, Qt::green); linearGradient.setColorAt(1, Qt::blue); painter.setBrush(linearGradient); painter.drawRect(0, 0, 200, 100);
这段代码生成的填充如图6-5所示。
图6-5 线性渐变填充
在QGradient构造函数中指定线形填充的两点分别为(0,0),(100,100)。setColorAt()函数在0~1的范围内设置指定位置的颜色。
圆形渐变填充需要指定圆心,半径和焦点,画刷在焦点和圆上的所有点之间进行颜色插值。创建QRadialGradient对象来设置画刷,如下例所示。
QRadialGradient radialGradient(50, 50, 50, 30, 30); radialGradient.setColorAt(0.2, Qt::cyan); radialGradient.setColorAt(0.8, Qt::yellow); radialGradient.setColorAt(1, Qt::magenta); painter.setBrush(radialGradient); painter.drawEllipse(0, 0, 100, 100);
上面的代码填充效果如图6-6所示。
图6-6 圆形渐变填充
圆锥渐变填充指定圆心和开始角,画刷沿圆心逆时针对颜色进行插值。创建QConicalGradient对象并设置画刷。下面的代码展示了圆锥渐变填充:
QConicalGradient conicalGradient(60, 40, 30); conicalGradient.setColorAt(0, Qt::gray); conicalGradient.setColorAt(0.4, Qt::darkGreen); conicalGradient.setColorAt(0.6, Qt::darkMagenta); conicalGradient.setColorAt(1, Qt::darkBlue); painter.setBrush(conicalGradient); painter.drawEllipse(0, 0, 100, 100);
这段代码填充效果如图6-7所示。
图6-7 圆锥渐变填充
为了实现自定义填充,还可以使用使用QPixmap或QImage对象进行纹理填充。两种图像分别使用setTexture()和setTextureImage()函数加载纹理。
下面通过将上一节的基本图形绘制程序完善来说明绘图过程。现在实现当调色板中用户改变当前画刷的响应槽函数如下:
void Palette::brushChanged() { QBrush brush; QColor color = brushColorComboBox->itemData( brushColorComboBox->currentIndex(), Qt::UserRole).value<QColor>(); Qt::BrushStyle style = Qt::BrushStyle(brushStyleComboBox->itemData( brushStyleComboBox->currentIndex(), Qt::UserRole).toInt()); if (style == Qt::LinearGradientPattern) { QLinearGradient linearGradient(0, 0, 100, 100); linearGradient.setColorAt(0.0, Qt::white); linearGradient.setColorAt(0.2, Qt::green); linearGradient.setColorAt(1.0, Qt::black); brush = linearGradient; } else if (style == Qt::RadialGradientPattern) { QRadialGradient radialGradient(50, 50, 50, 70, 70); radialGradient.setColorAt(0.0, Qt::white); radialGradient.setColorAt(0.2, Qt::green); radialGradient.setColorAt(1.0, Qt::black); brush = radialGradient; } else if (style == Qt::ConicalGradientPattern) { QConicalGradient conicalGradient(50, 50, 150); conicalGradient.setColorAt(0.0, Qt::white); conicalGradient.setColorAt(0.2, Qt::green); conicalGradient.setColorAt(1.0, Qt::black); brush = conicalGradient; } else if (style == Qt::TexturePattern) { brush = QBrush(QPixmap(":/images/ellipse.png")); } else { brush.setColor(color); brush.setStyle(style); } emit brushChanged(brush); }
函数根据当前的画刷模式和画刷颜色生成了相应的画刷,对于渐变画刷和纹理画刷,则生成了一个固定的渐变模式和纹理。读者可自行将其修改为用户可改变的渐变模式和纹理。
为了能够实时地看到选择调色板的效果,使用了一个QLabel对象作为预览区域,定义如下:
#ifndef PREVIEWLABEL_H #define previewlabel #include <QtGui> class PreviewLabel : public QLabel { Q_OBJECT public: PreviewLabel(QWidget *parent = 0); public slots: void penChanged(QPen& pen); void brushChanged(QBrush& brush); protected: void paintEvent(QPaintEvent *event); private: QPen curPen; QBrush curBrush; };
#endif
当画笔和画刷改变时,预览标签将显示实际的效果。预览类的实现文件如下:
#include "previewlabel.h" PreviewLabel::PreviewLabel(QWidget *parent) : QLabel(parent) { } void PreviewLabel::penChanged(QPen& pen) { curPen = pen; update(); } void PreviewLabel::brushChanged(QBrush& brush) { curBrush = brush; update(); } void PreviewLabel::paintEvent(QPaintEvent *event) { QPainter painter(this); painter.setPen(curPen); painter.setBrush(curBrush); painter.drawRect(rect().x() + 10, rect().y() + 10, rect().width() -20, rect().height() -20); }
在预览类中,接收画笔和画刷改变的信号,并更新预览区域。程序的运行效果如图6-8所示。
图6-8 2D绘图
3.双缓冲绘图
在Qt4中,所有的窗口部件默认都使用双缓冲进行绘图。使用双缓冲,可以减轻绘制的闪烁感。在有些情况下,用户需要关闭双缓冲,自己管理绘图。下面的语句设置了窗口部件的Qt::WA_PaintOnScreen属性,就关闭了窗口部件的双缓冲。
myWidget->setAttribute(Qt::WA_PaintOnScreen);
由于Qt4不再提供异或笔,组合模式QPainter::CompositionMode_Xor(6.6节介绍)并不是异或笔,Qt4只提供了QRubberBand实现矩形和直线的绘图反馈。因此要实现在绘制过程中动态反馈必须采用其他方法。程序中使用双缓冲来解决这个问题。在绘图过程中,一个缓冲区绘制临时内容,一个缓冲区保存绘制好的内容,最后进行合并。
在交互绘制过程中,程序将图像缓冲区复制到临时缓冲区,并在临时缓冲区上绘制,绘制完毕再将结果复制到图像缓冲区。如果没有交互绘制,则直接将图像缓冲区显示到屏幕上。
双缓冲绘图的流程如图6-9所示。
图6-9 双缓冲绘图的流程
用于绘图的窗口部件定义如下:
#ifndef FORM_H #define FORM_H #include <QtGui> class Form : public QWidget { Q_OBJECT public: Form(QWidget * parent = 0); enum ShapeType { Line, Polyline, Rectangle, Polygon, Arc, Pie, Chord, Ellipse, Text }; void setShape(ShapeType shape); public slots: void fontChanged(const QFont& font); void penChanged(QPen& pen); void brushChanged(QBrush& brush); protected: bool bDrawing; int x,y,w,h; void mousePressEvent(QMouseEvent *event); void mouseMoveEvent(QMouseEvent *event); void mouseReleaseEvent(QMouseEvent *event); void paintEvent(QPaintEvent *event); private: QImage bufferImage; QImage tempImage; ShapeType curShape; QPen curPen; QBrush curBrush; QFont textFont; int thickness; void paint(QImage& image); }; #endif
在头文件中定义了枚举类型ShapeType,用来记录当前绘制的图形。
用来绘图的窗口部件构造函数如下:
Form::Form(QWidget * parent) : QWidget(parent) { setAttribute(Qt::WA_NoBackground); bDrawing = false; curShape = Ellipse; resize(800,600); bufferImage = QImage(width(), height(), QImage::Format_ARGB32_Premultiplied); bufferImage.fill(qRgb(255, 255, 255)); tempImage = QImage(width(), height(), QImage::Format_ARGB32_Premultiplied); }
在构造函数中初始化了两个缓冲区:bufferImage用来存储最终的图形,tempImage是临时缓冲区。setShape()函数设置当前绘制的图形种类。
void Form::setShape(ShapeType shape) { curShape = shape; }
在mousePressEvent()函数中,当鼠标按下时,设置bDrawing值为“true”,表示进入交互绘图状态。
void Form::mousePressEvent( QMouseEvent * event ) { bDrawing = true; x = event->x(); y = event->y(); }
mouseMoveEvent()函数处理鼠标移动时将图形缓冲区中的内容复制到临时缓冲区,并绘制临时的反馈图形。
void Form::mouseMoveEvent(QMouseEvent *event) { w = event->x() - x; h = event->y() - y; tempImage = bufferImage; paint(tempImage); }
mouseReleaseEvent()函数处理鼠标按钮释放时,在绘图缓冲区上绘制图形。
void Form::mouseReleaseEvent(QMouseEvent *event) { bDrawing = false; paint(bufferImage); }
paintEvent()函数根据当前是否在绘图,在不同的缓冲区上绘制图形。
void Form::paintEvent(QPaintEvent *event) { QPainter painter(this); if(bDrawing) painter.drawImage(QPoint(0, 0), tempImage); else painter.drawImage(QPoint(0, 0), bufferImage); }
实际的绘制函数paint()只实现了矩形、椭圆和文字的绘制,读者可自行完成其他图形的绘制。
void Form::paint(QImage& image) { QPainter painter(&image); painter.setPen(curPen); painter.setBrush(curBrush); switch(curShape) { case Rectangle: painter.drawRect(x, y, w, h); break; case Ellipse: painter.drawEllipse(x, y, w, h); break; case Text: QFontMetrics metrics(textFont); QRect rect = metrics.boundingRect(textFont.family()); painter.setFont(textFont); painter.translate(x, y); painter.scale(w/rect.width(), h/rect.height()); painter.drawText(0, rect.height(), textFont.family()); break; } update(); }
其他一些绘图要素变化的槽函数如下:
void Form::fontChanged(const QFont& font) { textFont = font; } void Form::penChanged(QPen& pen) { curPen = pen; } void Form::brushChanged(QBrush& brush) { curBrush = brush; }
主窗口负责建立菜单、工具条及绘图区,主窗口的定义如下:
#ifndef MAINWINDOW_H #define MAINWINDOW_H #include <QMainWindow> #include <QActionGroup> #include <QFontComboBox> #include "form.h" #include "palette.h" class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(); private slots: void draw(QAction* action); private: void createActions(); void createMenus(); void createToolBars(); void createStatusBar(); void createDockWindows(); Palette *paletteWidget; Form *form; QMenu *drawMenu; QToolBar *drawToolBar; QFontComboBox *fontCmb; QAction *rectangleAct; QAction *ellipseAct; QAction *textAct; …… QActionGroup *drawActGroup; }; #endif
主窗口的实现程序如下:
#include <QtGui> #include "mainwindow.h" // 构造函数创建菜单、工具条等元素 MainWindow::MainWindow() { resize(800,600); form = new Form(this); setCentralWidget(form); createActions(); createMenus(); createToolBars(); createStatusBar(); createDockWindows(); } // 设置当前绘制图形的种类 void MainWindow::draw(QAction* action) { if(action == rectangleAct) form->setShape(Form::Rectangle); form->setShape(Form::Ellipse); else if((action == textAct)) form->setShape(Form::Text); …… } // 创建不同图形的QAction,并将所有绘图的QAction归到一个QActionGroup中 void MainWindow::createActions() { rectangleAct = new QAction(QIcon(":/images/rectangle.png"), tr("&rectangle"), this); rectangleAct->setCheckable(true); ellipseAct = new QAction(QIcon(":/images/ellipse.png"), tr("&Ellipse"), this); ellipseAct->setCheckable(true); ellipseAct->setChecked(true); textAct = new QAction(QIcon(":/images/text.png"), tr("&Text"), this); textAct->setCheckable(true); …… drawActGroup = new QActionGroup(this); drawActGroup->addAction(rectangleAct); drawActGroup->addAction(ellipseAct); drawActGroup->addAction(textAct); …… connect(drawActGroup, SIGNAL(triggered(QAction*)), this, SLOT(draw(QAction*))); } // 创建菜单 void MainWindow::createMenus() { drawMenu = menuBar()->addMenu(tr("&Draw")); drawMenu->addAction(rectangleAct); drawMenu->addAction(ellipseAct); drawMenu->addAction(textAct); …… } // 创建工具条 void MainWindow::createToolBars() { drawToolBar = addToolBar(tr("Draw")); drawToolBar->addAction(rectangleAct); drawToolBar->addAction(ellipseAct); …… drawToolBar->addSeparator(); drawToolBar->addAction(textAct); fontCmb = new QFontComboBox(drawToolBar); drawToolBar->addWidget(fontCmb); connect(fontCmb, SIGNAL(currentFontChanged(const QFont&)), form, SLOT( fontChanged(const QFont&))); fontCmb->setCurrentFont(font()); } // 创建状态条 void MainWindow::createStatusBar() { statusBar()->showMessage(tr("Ready")); } // 创建调色板 void MainWindow::createDockWindows() { QDockWidget *dock = new QDockWidget(tr("Palette"), this); dock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); paletteWidget = new Palette(dock); dock->setWidget(paletteWidget); addDockWidget(Qt::LeftDockWidgetArea, dock); connect(paletteWidget, SIGNAL(penChanged(QPen&)), form, SLOT(penChanged(QPen&))); connect(paletteWidget, SIGNAL(brushChanged(QBrush&)), form, SLOT(brushChanged(QBrush&))); }
4.使用Alpha通道
在Windows、Mac OS X和有X Render扩展的X11系统上,Qt4能够支持Alpha通道。通过使用Alpha通道,可以实现半透明效果。QColor类中定义了Alpha通道的透明度,0表示完全透明,255表示完全不透明,默认情况下是完全不透明。
下面实现一个小程序,该程序显示一个随鼠标移动的半透明提示框。程序代码如下:
#include <QApplication> #include <QtGui> #include <QTextCodec> class MyWidget : public QWidget { public: MyWidget(QWidget *parent = 0); protected: void mouseMoveEvent(QMouseEvent *event); void paintEvent(QPaintEvent *event); private: int x,y; QPixmap pixmap; QPixmap background; }; // 在构造函数中装入背景图 MyWidget::MyWidget(QWidget *parent) : QWidget(parent) { resize(800,600); pixmap = QPixmap(100,50); background = QPixmap("background.bmp"); x = -1; y = -1; } // 鼠标移动时形成一个pixmap void MyWidget::mouseMoveEvent(QMouseEvent *event) { x = event->x(); y = event->y(); pixmap.fill(QColor(255,255,255,127)); QPainter painter(&pixmap); painter.setPen(QColor(255,0,0)); painter.drawText(20, 40, QString("%1").arg(x) + "," + QString("%1").arg(y)); update(); } // 绘制背景图和透明的pixmap void MyWidget::paintEvent(QPaintEvent *event) { QPainter painter(this); painter.drawPixmap(0, 0, background); painter.drawPixmap(x, y, pixmap); } int main(int argc, char *argv[]) { QApplication app(argc, argv); QTextCodec::setCodecForTr(QTextCodec::codecForLocale()); MyWidget widget; widget.setMouseTracking(true); widget.show(); return app.exec(); }
在程序中,定义了一个背景是半透明的QPixmap对象pixmap,随着鼠标的移动显示鼠标指针的(x,y)坐标值。主程序中的“setMouseTracking(true)”是打开鼠标移动跟踪,默认情况下只有在鼠标按下后才发送QMouseMoveEvent()事件,打开鼠标移动跟踪后就能够随时发送了。图6-10窗口中的半透明矩形显示了鼠标位置。
注意QWidget类有一个属性windowOpacity,通过setWindowOpacity(qreal level)可以设置窗口的透明度。但该属性和这里讨论的Aplha通道实现原理并不相同,Qt4在Windows和Mac OS X平台上直接支持该属性,
图6-10 Alpha效果提示框
但在X11平台上却需要Composite扩展才能工作(Alpha通道使用的是X11的XRender扩展)。
6.1.2 绘图设备
QPaintDevice类是实际绘制设备的基类。QPainter能够在QPaintDeive子类上进行绘制,如QWidget、QImage、QPixmap、QGLWidget、QGLPixelBuffer、QPicture、QPrinter和QSvgGenerator。
要实现自己的绘图设备,必须从QPaintDevice类继承并实现虚函数QPaintDevice::paintEngine(),以告知QPainter能够在这个特定的设备上绘制图形。同时还需要从QPaintEngine类继承自定义的图形绘制引擎。
几种常用绘图设备说明如下:
1.Widget
QWidget是所有用户界面元素的基类。窗口部件是用户界面的原子元素,它接收鼠标、键盘和窗口系统的其他事件并在屏幕上绘制自己。
2.Image
QImage类提供了与硬件无关的图像表示,它为直接操作像素提供优化。QImage支持单色、8-bit、32-bit和alpha混合图象。使用QImage的优点是可以获得与平台无关的绘制操作,另外还有一个好处是图像可以不必在GUI线程中处理。
3.Pixmap
QPixmap是后台显示的图像,它为在屏幕上显示图像提供优化。不同于QImage,pixmap的图像数据是用户不可见,而且由底层窗口系统管理。为了优化QPixmap绘图,Qt提供了QPixmapCache类来存储临时的pixmap。
Qt还提供了从QPixmap继承的QBitmap类。QBitmap表示单色的pixmap,主要用来创建自定义的QCursor和QBrush对象、构造QRegion对象、设置pixmap和窗口部件的掩码。
4.OpenGL Widget
Qt提供了QtOpenGL模块来实现OpenGL操作。QGLWidget允许使用OpenGL API进行绘制。同时QGLWidget是QWidget的子类,因此QPainter也可以在上面绘制。这样可以使Qt能够利用OpenGL完成绘制操作,如变换和绘制pixmap。
5.Pixel Buffer
QGLPixelBuffer从QPaintDevice继承,封装了OpenGL pbuffer。使用pbuffer绘制通常是全硬件加速,这比使用QPixmap绘制更迅速。
6.FrameBuffer
QGLFrameBufferObject从QPaintDevice继承,QGLFramebufferObject封装了OpenGL framebuffer对象。Framebuffer对象用来实现后台屏幕绘制,比pixel buffer更好一些。
7.Picture
QPicture类是能够记录和重演QPainter命令的绘图设备。Picture串行化painter的命令为平台无关的格式。QPicture同时也与分辨率无关,如QPicture能够在不同的设备上(如svg、pdf、ps、打印机和屏幕)有一致的显示。QPicture::load()和QPicture::save()函数分别完成载入和存储图像。
8.Printer
QPrinter类是在打印机上绘制的绘图设备。在Windows和Mac OS X上,QPrinter使用内建的打印机驱动程序。在X11上,QPrinter产生PostScript代码并发送给lpr、lp或其他打印程序。QPrinter可以在任意其他的QPrintEngine对象上打印,也可以直接生成PDF文件。
QPrintEngine类定义了QPrinter如何和其他打印机系统交互的接口。当要创建自己的打印引擎时,可以从QPaintEngine和QPrintEngine上继承。