双缓冲(double bffering) 是一种图形用户界面编程技术,它包括把一个窗口部件渲染到一个脱屏像素映射(off screen pixmap)中以及把这个像素映射复制到显示器上。在Qt的早期版本中,这种技术通常用于消除屏幕的闪烁以及为用户提供一个漂亮的用户界面。
在Qt 4中,QWidget会自动处理这些情况,所以我们很少需要考虑窗口部件的闪烁问题。尽管如此,但如果窗口部件的绘制非常复杂并且需要连续不断地重复绘制时,明确指定使用双缓冲则是非常有用的事情。于是就可以把这个窗口部件固定不变地存储成一个像素映射,这样就总可以为下一个绘制事件做好准备,并且一旦收到绘制事件,就可以把这个像素映射复制到窗口部件上。当我们想做一些小的修改时,比如一个橡皮筋选择框的绘制,此时并不需要对整个窗口部件进行重复绘制和计算,从而就会显得特别有用。
所谓缓冲,就是先在内存中画好一张图,最后把这张图一次性画到屏幕上。
双缓冲(double-buffers)绘图,就是在进行绘制时,先将所有内容都绘制到一个绘图设备(如QPixmap
)上,然后再将整个图像绘制到部件上显示出来。使用双缓冲绘图可以避免显示时的闪烁现象。从Qt 4.0开始,QWidget
部件的所有绘制都自动使用了双缓冲,所以一般没有必要在paintEvent()
函数中使用双缓冲代码来避免闪烁。
虽然在一般的绘图中无需手动使用双缓冲绘图,不过要想实现一些绘图效果,还是要借助于双缓冲的概念。比如这个程序里,我们要实现使用鼠标在界面上绘制一个任意大小的矩形。这里需要两张画布,它们都是QPixmap
实例,其中一个tempPix
用来作为临时缓冲区,当鼠标正在拖动矩形进行绘制时,将内容先绘制到tempPix
上,然后将tempPix
绘制到界面上;而另一个pix
作为缓冲区,用来保存已经完成的绘制。当松开鼠标完成矩形的绘制后,则将tempPix
的内容复制到pix上。为了绘制时不显示拖影,而且保证以前绘制的内容不消失,那么在移动鼠标过程中,每绘制一次,都要在绘制这个矩形的原来的图像上进行绘制,所以需要在每次绘制tempPix
之前,先将pix的内容复制到tempPix
上。因为这里有两个QPixmap
对象,也可以说有两个缓冲区,所以称之为双缓冲绘图。
实现过程:
在缓冲区里创建一个画布,将缓冲区(pix)的内容复制给临时缓冲区(tempPix);
在临时缓冲区上绘画一个矩形,然后在主部件绘画临时缓冲区的内容;
判断是否完成绘制,如果已经完成绘制,那么更新缓冲区。将临时缓冲区的内容复制给缓冲区(为了第二次绘制时保存上一次绘制的图);
当鼠标按下并移动时(这时进行第二次绘制),将缓冲区的内容复制到临时缓冲区去;
反锯齿(反走样):就是对图像的边缘进行平滑处理,使其看起来更加柔和流畅的一种技术。
QPainter 进行绘制时可以使用QPainter::RenderHint 渲染提示来指定是否要使用抗锯齿功能
QPainter::Antialiasing 告诉绘图引擎应该在可能的情况下进行边的反锯齿绘制
QPainter::TextAntialiasing 尽可能的情况下文字的反锯齿绘制
QPainter::SmoothPixmapTransform 使用平滑的pixmap变换算法(双线性插值算法),而不是近邻插值算法
painter.setRenderHint(QPainter::Antialiasing, true);
我们通过这条语句,将Antialiasing属性(也就是反走样)设置为 true。经过这句设置,我们就打开了QPainter的反走样功能。
本章将通过回顾在图5.7和图5.9中显示的Plotter自定义窗口部件来结束。这个窗口部件使用了双缓冲技术,并且也对Qt编程中的一些其他方面的知识进行了示范,包括像键盘事件处理、手动布局和坐标系统等。
对于需要具有一个图形处理或者图形测绘窗口部件的真正应用程序来说,最好还是使用那些可以获取的第三方窗口部件,而不是像这里所做的那样,去创建一个自定义窗口部件。例如,我们或许应当使用来自http://www.ics.com/的GraphPak,来自http://www.kdab.net/的KD Chart,或者是来自http://qwt.sourceforge.net/的Qwt。
Plotter窗口部件可以按照给定的矢量坐标绘制一条或者多条曲线。用户可以在图像中拖拽一条橡皮筋选择框,并且Plotter将会对由这个橡皮筋选择框选定的区域进行放大。用户通过单击图上的一个点按住鼠标左键并且把鼠标拖动到另外-一个位置,就可以拖拽出一个橡皮筋选择框,然后就可以松开鼠标按键。Qt为绘制橡皮筋选择框提供了类,但这里通过我们自己来绘制它,以提供更好的视觉控制效果,并且藉此说明双缓冲技术。
通过多次拖拽出橡皮筋选择框,可以重复进行放大操作。使用Zoom Out按钮可以进行缩小操作,而使用ZoomIn按钮又可以再放大回来。ZoomIn和ZoomOut按钮在第一次出现的时候就处于启用状态。这样的话,如果用户没有对这个曲线图进行缩放,它们也不会弄乱图形的显示效果。
Potter窗口部件可以保存任意条曲线的数据。它还维护着一个PlotSettings堆栈对象,而这每一个堆栈对象都对应一个特定的缩放级别。
//plotter.h
#ifndef PLOTTER_H
#define PLOTTER_H
#include
#include
#include
#include
class QToolButton;
class PlotSettings;
class Plotter : public QWidget
{
Q_OBJECT
public:
Plotter(QWidget *parent = 0);
void setPlotSettings(const PlotSettings &settings);
void setCurveData(int id, const QVector<QPointF> &data);
void clearCurve(int id);
QSize minimumSizeHint() const;
QSize sizeHint() const;
public slots:
void zoomIn();
void zoomOut();
/* 首先关注pltter头文件中包含的相应的Qt类头文件。在文件的开始部分,还前置声明了一些带指针或引用的类。
* 在Plotter类中,提供三个公有函数来用于创建绘图区(plot),并且用两个公有槽来响应图形的放大和缩小操作。
* 还重新实现了QWidget中的minimumSizeHint()和 sizeHint()两个函数。
* 我们把曲线的顶点存储为QVector ,这里的QPointF是一个具有浮点数形式的QRoint。
*/
protected:
void paintEvent(QPaintEvent *event);
void resizeEvent(QResizeEvent *event);
void mousePressEvent(QMouseEvent *event);
void mouseMoveEvent(QMouseEvent *event);
void mouseReleaseEvent(QMouseEvent *event);
void keyPressEvent(QKeyEvent *event);
void wheelEvent(QWheelEvent *event);
//在这个类的protected 段中,声明了所有需要重新实现的QWidget事件处理器。
private:
void updateRubberBandRegion();
void refreshPixmap();
void drawGrid(QPainter *painter);
void drawCurves(QPainter *painter);
enum {
Margin = 50 };
QToolButton *zoomInButton;
QToolButton *zoomOutButton;
QMap<int, QVector<QPointF> > curveMap;
QVector<PlotSettings> zoomStack;
int curZoom;
bool rubberBandIsShown;
QRect rubberBandRect;
QPixmap pixmap;
/* 在这个类的private段,声明了一些用于绘制这个窗口部件的函数,一个常量和几个成员变量。
* Margin常量可以为图形区域的周围提供一些空间。
* 在这些成员变量中,pixmap变量的类型为QPixmap。
* 这个变量对整个窗口部件的绘制数据的进行了复制保存,这和屏幕上显示的图形是相同的。
* 绘图区总是先在脱屏像素映射上绘制图形。然后,才把这一像素映射复制到窗口部件中。
*/
};
class PlotSettings
{
public:
PlotSettings();
void scroll(int dx, int dy);
void adjust();
double spanX() const {
return maxX - minX; }
double spanY() const {
return maxY - minY; }
double minX;
double maxX;
int numXTicks;
double minY;
double maxY;
int numYTicks;
private:
static void adjustAxis(double &min, double &max, int &numTicks);
/* PloSettings类给定了x轴和y轴的范围,以及在这些轴上刻度标记符的数量。
* 图5.8给出了一个PlotSettings对象和一个Plotter窗口部件之间的对应关系。
* 依照惯例,把riumXTieks和numYTicks都会减去1。
* 如果numXTicks是5,那么Plotter实际会在x轴上绘制6个刻度标记符。
* 这样可以简化后续运算。
*/
};
#endif
//plotter.cpp
#include
#include
#include "plotter.h"
Plotter::Plotter(QWidget *parent)
: QWidget(parent)
{
setBackgroundRole(QPalette::Dark);
setAutoFillBackground(true);
/* setBackgroundRole()调用告诉QWidget使用调色板中的“暗”分量作为重绘窗口部件的颜色,而不是使用“背景色"分量。
* 这样就可以为Qt设置一种默认颜色,当把这个窗口部件重新改变成一个更大的尺寸时,甚至有可能是在paintEvent()事件绘制那些新近显示的任意像素之前,就可以使用这种默认颜色来填充这些新的像素。
* 我们也需要调用setAutoFillBackground(true)来启用这一机制。(默认情况下,子窗口部件会从它们的父窗口部件那里继承相应的背景色。)
*/
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
/* setSizePolicy( )调用可以把这个窗口部件的大小策略设置为在两个方向上都是QSizePolicy::Expanding。
* 这样就会告诉负责这个窗口布局的任意布局管理器,这个窗口部件可以放大,也可以缩小。
* 这对于那些要占用很多屏幕空间的窗口部件来说,通常是一种比较典型的设置方法。
* 在两个方向上,默认设置都是QSizePolicy::Preferred,它的意思是指这个窗口部件的合适大小就是大小提示所给出的大小,
* 但是如果有必要,也可以无限放大它,或者也可以一直把它缩小到大小提示给出的最小大小。
*/
setFocusPolicy(Qt::StrongFocus);
/* setFoucusPolicy(Qt::StrongFocus)调用可以让窗口部件通过单击或者通过按下Tab键而输入焦点。
* 当Plotter获得焦点时,它将会接收由按键而产生的事件。
* Plotter窗口部件可以处理一些按键:
* “+”用来放大图形,“-”用来缩小图形,以及还可以使用四个方向键来上、下、左、右地滚动图形。
*/
rubberBandIsShown = false;
zoomInButton = new QToolButton(this);
zoomInButton->setIcon(QIcon(":/images/zoomin.png"));
zoomInButton->adjustSize();
connect(zoomInButton, SIGNAL(clicked()), this, SLOT(zoomIn()));
zoomOutButton = new QToolButton(this);
zoomOutButton->setIcon(QIcon(":/images/zoomout.png"));
zoomOutButton->adjustSize();
connect(zoomOutButton, SIGNAL(clicked()), this, SLOT(zoomOut()));
/* 同样还是在构造函数中创建了两个QToolButton, 二者都有一个图标。
* 允许用户通过这些按钮进行缩小和放大操作。
* 这些按钮的图标存储在一个资源文件中,因此任何使用Plotter窗口部件的应用程序都需要在它们的.pro文件中加人这一条代码:
* RESOURCES = plotter.qrc
*/
setPlotSettings(PlotSettings());
//最后,通过对setPlotSettings()的调用就完成了初始化工作。
}
void Plotter::setPlotSettings(const PlotSettings &settings)
{
zoomStack.clear();
zoomStack.append(settings);
curZoom = 0;
zoomInButton->hide();
zoomOutButton->hide();
refreshPixmap();
/* setPlotettings()函数用于指定显示绘图区时所用到的PlotSettings。
* 它可以被Plotter的构造函数调用,也可以被使用这个类的用户调用。
* 绘图区开始时使用它的默认缩放级。用户每放大一次,都会创建一个新的PlotSettings实例,并且会将其放进缩放堆栈中。
* 这个缩放堆栈由两个成员变量来表示:
* ● 类型为QVector的zoomStack保存不同的缩放级设置值。
* ● curZoom在这个zoomStack中保存PlotSettings的当前索引值。
* 对setPlotSettings()进行调用之后,该缩放堆栈会只包含一项,并且Zoom In和Zoom Out按钮也是隐藏的。
* 只有当我们在zoomIn()和zoomOut()槽中对它们调用show()的时候,才会把这些按钮显示出来。
* (一般情况下,在顶层窗口部件中调用show()足以显示所有的子窗口部件。但是,当明确地对子窗口部件调用hide()时,除非再次对它调用show(),否则它就一直会处于隐藏状态。)
* 为了更新显示,refreshPixmap()调用是很有必要的。通常情况下,本可以调用update(),但是这里的做法将会稍微有些不同,因为我们想让QPixmap在任意时刻都处于最新状态。
* 在重新生成像素映射之后,refreshPixmap()会调用update(),会把像素映射复制到窗口部件中。
*/
}
void Plotter::zoomOut()
{
if (curZoom > 0) {
zoomOutButton->setEnabled(curZoom > 0);
zoomInButton->setEnabled