Chinaunix首页 | 论坛 | 博客
  • 博客访问: 3427455
  • 博文数量: 864
  • 博客积分: 14125
  • 博客等级: 上将
  • 技术积分: 10634
  • 用 户 组: 普通用户
  • 注册时间: 2007-07-27 16:53
个人简介

https://github.com/zytc2009/BigTeam_learning

文章分类

全部博文(864)

文章存档

2023年(1)

2021年(1)

2019年(3)

2018年(1)

2017年(10)

2015年(3)

2014年(8)

2013年(3)

2012年(69)

2011年(103)

2010年(357)

2009年(283)

2008年(22)

分类: C/C++

2010-03-16 10:37:49

双缓冲技术(Double Buffering)(1、简介和源代码部分)
这一节实在是有些长,翻译完后统计了一下,快到2w字了。考虑到阅读的方便和网络的速度,打算把这节分为5个部分,第一部分为双缓冲技术的一个简介和所有的代码,如果能够看懂代码,不用看译文也就可以了。第二部分为Plotter控件的公有函数的实现,第三部分为Plotter的事件处理函数的实现,第四部分为Plotter控件的私有函数实现,第五部分为辅助类PlotSettings的实现。
这里给出一些常用的中英文对照(不一定准确,我这样用的):
Rubber band(橡皮筋线,或者橡皮线), pixmap(图像,双缓冲中用到的图像,有时也直呼pixmap),off-screen pixmap(离线图像)
Plot(plot,这一节实现的就是一个绘制曲线的控件Plotter,有时原文也叫plot,有点小名的意思,没有翻译,直接呼之)
废话少说,以下是译文:
 
双缓冲技术是GUI编程中常用的技术。所谓的双缓冲就是把需要绘制的控件保存到一个图像中,然后在把图像拷贝到需要绘制的控件上。在Qt的早期版本中,为了用户界面更加清爽,经常用这个技术来消除闪烁。
在Qt4中,QWidget能够自动处理闪烁,因此我们不用再担心这个问题。尽管如此,如果控件绘制复杂且需要经常刷新,双缓冲技术还是很有用的。我们可以把控件永久保存在一个图像中,随时准备下一次绘制事件的到来,一旦接到一个控件的绘制事件,就把图片拷贝到控件上。如果我们要做的只是小范围的修改,这个技术更是尤为有用,如要绘制一条橡皮筋线,就不必刷新整个控件了。
在本章的最后一节,我们实现的是一个叫做Plotter的自定义控件。这个控件使用了双缓冲技术,也涉及到了Qt编程的其他方面:如键盘的事件处理,布局和坐标系统。
Plotter控件用来显示一条或者多条曲线,这些曲线由一组向量坐标表示。用户可以在显示的曲线上画一个橡皮筋线,Plotter控件对橡皮筋线包围的区域进行放大。用户用鼠标左键在控件上选择一个点,然后拖动鼠标走到另一点,然后释放鼠标,就在控件上绘制一条橡皮筋线。
Figure 5.7 Zooming in on the Plotter Widget

 
用户可以多次用橡皮筋线进行放大,也可以用ZoomOut按钮缩小,然后用ZoomIn按钮再放大。ZoomOut和ZoomIn按钮只是在控件第一次放大或者缩小操作后变得可见,如果用户不缩放图形,则这两个按钮会一直不可见,这样可以使绘图区域不那么混乱。
Plotter控件可以存储任何数量的曲线的数据。同时它还维护一个PlotSettings对象的堆栈区域,每一个PlotSettings对象都是对应一个特定的放缩值。
首先看一下头文件的代码(对头文件的解析在代码中用注释的形式给出):
#ifndef PLOTTER_H
#define PLOTTER_H
#include //包含的Qt的头文件
#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 &data);
    void clearCurve(int id);
    QSize minimumSizeHint() const; //重写QWidget::minimumSizeHint()
    QSize sizeHint() const;        //重写QWidget::sizeHint()
public slots:
    void zoomIn();  //放大曲线
void zoomOut();  //缩小显示曲线
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);
private:
    void updateRubberBandRegion();
    void refreshPixmap();
    void drawGrid(QPainter *painter);
    void drawCurves(QPainter *painter);
    enum { Margin = 50 };
    QToolButton *zoomInButton;
    QToolButton *zoomOutButton;
    QMap > curveMap;  //曲线数据
    QVector zoomStack;  //PlotSettings堆栈区域
    int curZoom;
    bool rubberBandIsShown;
    QRect rubberBandRect;
    QPixmap pixmap; //显示在屏幕的控件的一个拷贝,任何绘制总是先在pixmap进行,然//后拷贝到控件上
};
//PlotSettings确定x,y轴的范围,和刻度的个数
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);
};
#endif
 
图5-8表示了Plotter控件和PlotSettings的关系。
通常,numXTicks和numYTicks是有一个的误差,如果numXTicks为5,实际上Plotter会在x轴上绘制6个刻度。这样可以简化以后的计算(至于怎么样简化的,就看程序和后文吧吧)。
Figure 5-8 PlotSettings's member variables

现在来看源文件(代码有些长,先用代码格式给出完整源文件代码):
#include
#include

#include "plotter.h"

Plotter::Plotter(QWidget *parent)
    : QWidget(parent)
{
    setBackgroundRole(QPalette::Dark);
    setAutoFillBackground(true);
    setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
    setFocusPolicy(Qt::StrongFocus);
    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()));

    setPlotSettings(PlotSettings());
}

void Plotter::setPlotSettings(const PlotSettings &settings)
{
    zoomStack.clear();
    zoomStack.append(settings);
    curZoom = 0;
    zoomInButton->hide();
    zoomOutButton->hide();
    refreshPixmap();
}

void Plotter::zoomOut()
{
    if (curZoom > 0) {
        --curZoom;
        zoomOutButton->setEnabled(curZoom > 0);
        zoomInButton->setEnabled(true);
        zoomInButton->show();
        refreshPixmap();
    }
}

void Plotter::zoomIn()
{
    if (curZoom < zoomStack.count() - 1) {
        ++curZoom;
        zoomInButton->setEnabled(curZoom < zoomStack.count() - 1);
        zoomOutButton->setEnabled(true);
        zoomOutButton->show();
        refreshPixmap();
    }
}

void Plotter::setCurveData(int id, const QVector &data)
{
    curveMap[id] = data;
    refreshPixmap();
}

void Plotter::clearCurve(int id)
{
    curveMap.remove(id);
    refreshPixmap();
}

QSize Plotter::minimumSizeHint() const
{
    return QSize(6 * Margin, 4 * Margin);
}

QSize Plotter::sizeHint() const
{
    return QSize(12 * Margin, 8 * Margin);
}

void Plotter::paintEvent(QPaintEvent * /* event */)
{
    QStylePainter painter(this);
    painter.drawPixmap(0, 0, pixmap);
    if (rubberBandIsShown) {
        painter.setPen(palette().light().color());
        painter.drawRect(rubberBandRect.normalized()
                                      .adjusted(0, 0, -1, -1));
    }
    if (hasFocus()) {
        QStyleOptionFocusRect option;
        option.initFrom(this);
        option.backgroundColor = palette().dark().color();
        painter.drawPrimitive(QStyle::PE_FrameFocusRect, option);
    }
}

void Plotter::resizeEvent(QResizeEvent * /* event */)
{
    int x = width() - (zoomInButton->width()
                      + zoomOutButton->width() + 10);
    zoomInButton->move(x, 5);
    zoomOutButton->move(x + zoomInButton->width() + 5, 5);
    refreshPixmap();
}
void Plotter::resizeEvent(QResizeEvent * /* event */)
{
    int x = width() - (zoomInButton->width()
                      + zoomOutButton->width() + 10);
    zoomInButton->move(x, 5);
    zoomOutButton->move(x + zoomInButton->width() + 5, 5);
    refreshPixmap();
}
void Plotter::resizeEvent(QResizeEvent * /* event */)
{
    int x = width() - (zoomInButton->width()
                      + zoomOutButton->width() + 10);
    zoomInButton->move(x, 5);
    zoomOutButton->move(x + zoomInButton->width() + 5, 5);
    refreshPixmap();
}

void Plotter::mousePressEvent(QMouseEvent *event)
{
    QRect rect(Margin, Margin,
              width() - 2 * Margin, height() - 2 * Margin);
    if (event->button() == Qt::LeftButton) {
        if (rect.contains(event->pos())) {
            rubberBandIsShown = true;
            rubberBandRect.setTopLeft(event->pos());
            rubberBandRect.setBottomRight(event->pos());
            updateRubberBandRegion();
            setCursor(Qt::CrossCursor);
        }
    }
}
void Plotter::mouseMoveEvent(QMouseEvent *event)
{
    if (rubberBandIsShown) {
        updateRubberBandRegion();
        rubberBandRect.setBottomRight(event->pos());
        updateRubberBandRegion();
    }
}
void Plotter::mouseReleaseEvent(QMouseEvent *event)
{
    if ((event->button() == Qt::LeftButton) && rubberBandIsShown) {
        rubberBandIsShown = false;
        updateRubberBandRegion();
        unsetCursor();
        QRect rect = rubberBandRect.normalized();
        if (rect.width() < 4 || rect.height() < 4)
            return;
        rect.translate(-Margin, -Margin);
        PlotSettings prevSettings = zoomStack[curZoom];
        PlotSettings settings;
        double dx = prevSettings.spanX() / (width() - 2 * Margin);
        double dy = prevSettings.spanY() / (height() - 2 * Margin);
        settings.minX = prevSettings.minX + dx * rect.left();
        settings.maxX = prevSettings.minX + dx * rect.right();
        settings.minY = prevSettings.maxY - dy * rect.bottom();
        settings.maxY = prevSettings.maxY - dy * rect.top();
        settings.adjust();
        zoomStack.resize(curZoom + 1);
        zoomStack.append(settings);
        zoomIn();
    }
}

void Plotter::keyPressEvent(QKeyEvent *event)
{
    switch (event->key()) {
    case Qt::Key_Plus:
        zoomIn();
        break;
    case Qt::Key_Minus:
        zoomOut();
        break;
    case Qt::Key_Left:
        zoomStack[curZoom].scroll(-1, 0);
        refreshPixmap();
        break;
    case Qt::Key_Right:
        zoomStack[curZoom].scroll(+1, 0);
        refreshPixmap();
        break;
    case Qt::Key_Down:
        zoomStack[curZoom].scroll(0, -1);
        refreshPixmap();
        break;
    case Qt::Key_Up:
        zoomStack[curZoom].scroll(0, +1);
        refreshPixmap();
        break;
    default:
        QWidget::keyPressEvent(event);
    }
}

void Plotter::wheelEvent(QWheelEvent *event)
{
    int numDegrees = event->delta() / 8;
    int numTicks = numDegrees / 15;
    if (event->orientation() == Qt::Horizontal) {
        zoomStack[curZoom].scroll(numTicks, 0);
    } else {
        zoomStack[curZoom].scroll(0, numTicks);
    }
    refreshPixmap();
}
void Plotter::updateRubberBandRegion()
{
    QRect rect = rubberBandRect.normalized();
    update(rect.left(), rect.top(), rect.width(), 1);
    update(rect.left(), rect.top(), 1, rect.height());
    update(rect.left(), rect.bottom(), rect.width(), 1);
    update(rect.right(), rect.top(), 1, rect.height());
}
void Plotter::refreshPixmap()
{
    pixmap = QPixmap(size());
    pixmap.fill(this, 0, 0);
    QPainter painter(&pixmap);
    painter.initFrom(this);
    drawGrid(&painter);
    drawCurves(&painter);
    update();
}

void Plotter::drawGrid(QPainter *painter)
{
    QRect rect(Margin, Margin,
              width() - 2 * Margin, height() - 2 * Margin);
    if (!rect.isValid())
        return;
    PlotSettings settings = zoomStack[curZoom];
    QPen quiteDark = palette().dark().color().light();
    QPen light = palette().light().color();
    for (int i = 0; i <= settings.numXTicks; ++i) {
        int x = rect.left() + (i * (rect.width() - 1)
                                / settings.numXTicks);
        double label = settings.minX + (i * settings.spanX()
                                          / settings.numXTicks);
        painter->setPen(quiteDark);
        painter->drawLine(x, rect.top(), x, rect.bottom());
        painter->setPen(light);
        painter->drawLine(x, rect.bottom(), x, rect.bottom() + 5);
        painter->drawText(x - 50, rect.bottom() + 5, 100, 15,
                          Qt::AlignHCenter | Qt::AlignTop,
                          QString::number(label));
    }
    for (int j = 0; j <= settings.numYTicks; ++j) {
        int y = rect.bottom() - (j * (rect.height() - 1)
                                  / settings.numYTicks);
        double label = settings.minY + (j * settings.spanY()
                                          / settings.numYTicks);
        painter->setPen(quiteDark);
        painter->drawLine(rect.left(), y, rect.right(), y);
        painter->setPen(light);
        painter->drawLine(rect.left() - 5, y, rect.left(), y);
        painter->drawText(rect.left() - Margin, y - 10, Margin - 5, 20,
                          Qt::AlignRight | Qt::AlignVCenter,
                          QString::number(label));
    }
    painter->drawRect(rect.adjusted(0, 0, -1, -1));
}

void Plotter::drawCurves(QPainter *painter)
{
    static const QColor colorForIds[6] = {
        Qt::red, Qt::green, Qt::blue, Qt::cyan, Qt::magenta, Qt::yellow
    };
    PlotSettings settings = zoomStack[curZoom];
    QRect rect(Margin, Margin,
              width() - 2 * Margin, height() - 2 * Margin);
    if (!rect.isValid())
        return;
    painter->setClipRect(rect.adjusted(+1, +1, -1, -1));
    QMapIterator > i(curveMap);
    while (i.hasNext()) {
        i.next();
        int id = i.key();
        const QVector &data = i.value();
        QPolygonF polyline(data.count());
        for (int j = 0; j < data.count(); ++j) {
            double dx = data[j].x() - settings.minX;
            double dy = data[j].y() - settings.minY;
            double x = rect.left() + (dx * (rect.width() - 1)
                                        / settings.spanX());
            double y = rect.bottom() - (dy * (rect.height() - 1)
                                          / settings.spanY());
            polyline[j] = QPointF(x, y);
        }
        painter->setPen(colorForIds[uint(id) % 6]);
        painter->drawPolyline(polyline);
    }
}

////////////////////////////////////////////////////////////
PlotSettings::PlotSettings()
{
    minX = 0.0;
    maxX = 10.0;
    numXTicks = 5;
    minY = 0.0;
    maxY = 10.0;
    numYTicks = 5;
}

void PlotSettings::scroll(int dx, int dy)
{
    double stepX = spanX() / numXTicks;
    minX += dx * stepX;
    maxX += dx * stepX;
    double stepY = spanY() / numYTicks;
    minY += dy * stepY;
    maxY += dy * stepY;
}

void PlotSettings::adjust()
{
    adjustAxis(minX, maxX, numXTicks);
    adjustAxis(minY, maxY, numYTicks);
}

void PlotSettings::adjustAxis(double &min, double &max,
                              int &numTicks)
{
    const int MinTicks = 4;
    double grossStep = (max - min) / MinTicks;
    double step = pow(10.0, floor(log10(grossStep)));
    if (5 * step < grossStep) {
        step *= 5;
    } else if (2 * step < grossStep) {
        step *= 2;
    }
    numTicks = int(ceil(max / step) - floor(min / step));
    if (numTicks < MinTicks)
        numTicks = MinTicks;
    min = floor(min / step) * step;
    max = ceil(max / step) * step;
}
            双缓冲技术(Double Buffering)(2、公有函数实现)
#include
#include
using namespace std;
#include "plotter.h"
以上代码为文件的开头,在这里把std的名空间加入到当前的全局命名空间。这样在使用里的函数时,就不用前缀std::了,如可以直接使用函数floor(),而不用写成std::floor()。
 
Plotter::Plotter(QWidget *parent) : QWidget(parent)
{
    setBackgroundRole(QPalette::Dark);
    setAutoFillBackground(true);
    setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
    setFocusPolicy(Qt::StrongFocus);
    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()));

    setPlotSettings(PlotSettings());
}
在构造函数中,调用setBackGroundRole(QPalette::Dark),当对控件进行放大需要重新绘制时,提供给Qt一个缺省的颜色填充新的区域,为了能够使用这个机制,还调用了setAutoFillBackground(true)。
函数setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding)让控件在水平和垂直两个方向上都可以进行伸缩。如果控件需要占据屏幕上很大的控件,经常设置这个属性。缺省的设置是两个方向都是QSizePolicy::Preferred,意思是控件的实际尺寸和它的sizeHint一致,控件最小只能缩小到它的最小的sizeHint,并能够无限放大。
调用setFocusPolicy(Qt::StrongFocus)可以使控件通过鼠标点击或者Tab得到焦点。当Plotter控件得到焦点时,它可以接受键盘敲击事件。Plotter控件能够理解一些键盘事件,如+放大,-为缩小,可以向上下左右平移。
Figure 5.9. Scrolling the Plotter widget

 
在构造函数中,我们还创建了两个QToolButton,每一个按钮都有一个图标。点击这些图标可以放大或者缩小显示的图像。图标保存在资源文件中,为了任何程序都可以使用Plotter控件,需要在.pro添加资源条目:
RESOURCES    = plotter.qrc
资源文件是一个XML格式的文本文件,和在Spreadsheet中使用的很像:


    images/zoomin.png
    images/zoomout.png


调用QToolButton::adjustSize()调整按钮的大小为它们的sizeHint。在这里按钮不在布局中,在控件大小改变的时候,又程序计算它们的位置。由于没有了布局管理,因为我们必须在按钮的构造函数中确定按钮的父控件。
 
调用setPlotSettings()函数用来完成控件的初始化。函数代码如下:
void Plotter::setPlotSettings(const PlotSettings &settings)
{
    zoomStack.clear();
    zoomStack.append(settings);
    curZoom = 0;
    zoomInButton->hide();
    zoomOutButton->hide();
    refreshPixmap();
}
函数setPlotSettings()确定显示控件时的PlotSettings。它在Plotter构造函数中调用,也可以被Plotter的用户调用。开始的时候,Plotter使用的是缺省的放缩值。用户进行放大一次,就有一个新的PlotSettings对象加入到堆栈中。这个堆栈中有两个变量:
      zoomStack是保存PlotSettings对象的一个数组;
      curZoom是当前使用的PlotSettings的一个索引值。
调用setPlotSettings()后,zoomStack中只有一项,zoomIn和zoomOut按钮隐藏。如果我们调用函数zoomIn()和zoomOut(),这两个函数中调用了按钮的show()函数,它们才能显示出来。(通常,调用父控件的show()函数就显示所有的子控件。但是如果我们显式调用了子控件的hide(),必须要显示调用其show()函数显示它,否则就会一直隐藏)
调用refreshPixmap()来更新显示。通常,我们调用update()就可以,这里有些不一样,因为我们要保持QPixmap一直最新的状态。更新了图片后,refreshPixmap()再调用update()把图片显示到控件上。
void Plotter::zoomOut()
{
    if (curZoom > 0) {
        --curZoom;
        zoomOutButton->setEnabled(curZoom > 0);
        zoomInButton->setEnabled(true);
        zoomInButton->show();
        refreshPixmap();
    }
}
如果图片放大了,调用zoomOut()缩小它。它缩小比例系数,如果还能进一步缩小,zoomOut按钮一直有效。显示zoomIn按钮使之按钮有效,调用refreshPixmap()刷新控件。
void Plotter::zoomIn()
{
    if (curZoom < zoomStack.count() - 1) {
        ++curZoom;
        zoomInButton->setEnabled(curZoom < zoomStack.count() - 1);
        zoomOutButton->setEnabled(true);
        zoomOutButton->show();
        refreshPixmap();
    }
}
如果用户放大后又缩小控件,下一个放缩系数的PlotSettings就进入zoomStack。我们就可以再放大控件。
函数zoomIn增加放缩系数,zoomIn按钮显示出来,只要能够放大,按钮会一直有效。同事显示zoomOut按钮使之有效状态。
void Plotter::setCurveData(int id, const QVector &data)
{
    curveMap[id] = data;
  refreshPixmap();
}
函数setCurveData()设置一个指定id的曲线数据。如果曲线中有一个同样的id,那么就用新的数据替代旧数据。如果没有指定的id,则增加一个新的曲线。曲线的数据类型为QMap >
void Plotter::clearCurve(int id)
{
    curveMap.remove(id);
    refreshPixmap();
}
函数clearCurve()删除一个指定id的曲线。
QSize Plotter::minimumSizeHint() const
{
    return QSize(6 * Margin, 4 * Margin);
}
函数minimumSizeHint()和sizeHint()很像,确定控件的理想的尺寸。minimumSizeHint()确定控件的最大尺寸。布局管理器排列控件时不会超过控件的最大尺寸。
由于Margin值为50,所以我们返回的值为300×200,包括四个边界的宽度和Plot本身。如果再小,尺寸太小Plot就不能正常显示了。
QSize Plotter::sizeHint() const
{
    return QSize(12 * Margin, 8 * Margin);
}
在sizeHint()中,我们返回控件的理想尺寸,用Margin常数作为倍数,长宽的比例为3:2,与minimumSizeHint()中比例一致。
以上是Plotter的公有函数和槽函数。
            双缓冲技术(Double Buffering)(3、事件处理函数)
以下是Plotter控件的事件处理函数部分
void Plotter::paintEvent(QPaintEvent * /* event */)
{
    QStylePainter painter(this);
    painter.drawPixmap(0, 0, pixmap);
    if (rubberBandIsShown) {
        painter.setPen(palette().light().color());
        painter.drawRect(rubberBandRect.normalized()
                                      .adjusted(0, 0, -1, -1));
    }
    if (hasFocus()) {
        QStyleOptionFocusRect option;
        option.initFrom(this);
        option.backgroundColor = palette().dark().color();
        painter.drawPrimitive(QStyle::PE_FrameFocusRect, option);
    }
}
通常情况下,paintEvent()是我们处理控件的所有绘制的地方。这里Plotter控件的绘制是在refreshPixmap()中完成的,因此在paintEvent()函数中只是把图片显示在控件的(0,0)位置上。
如果能看到橡皮线,我们把它画到控件的上面。使用控件当前颜色组的“轻”的颜色橡皮线的颜色,和“黑”的背景形成对比。需要注意的是这个线是直接画在控件上的,对图片没有任何影响。使用QRect::normalized()确保橡皮线的矩形有着正数的宽和高,adjusted()减掉一个象素宽的矩形,显示它的轮廓。
如果Plotter有焦点,用控件样式的drawPrimitive()绘制一个焦点矩形,第一个参数为QStyle::PE_FrameFocusRect,第二个参数为一个QStyleOptionFocusRect对象。焦点矩形的绘制选项用initFrom()函数设置,继承自Plotter,但是背景颜色必须明确设置。
如果我们想使用当前的样式,我们可以直接调用QStyle的函数,比如:
style()->drawPrimitive(QStyle::PE_FrameFocusRect, &option, &painter, this);
或者我们使用QStylePainter,能绘制更加方便。
QWidget::Style()函数返回绘制控件使用的样式。在Qt中,一个控件的样式是QStyle的基类。Qt提供的样式有QWindowStyle,QWindowXpStyle,QMotifStyle,QCDEStyle,QMacStyle和QPlastiqueStyle。这些样式类都是重新实现了QStyle的虚函数来模拟特定平台的样式。QStylePainter::drawPrimitive()函数调用QStyle的同名函数,绘制控件的一些基本原色,如面板,按钮,焦点矩形等。在一个应用程序中,所有控件的样式都是一样的,可用通过QApplication::style()得到,也可以用QWidget::setStyle()设置某一个控件的样式。
把QStyle作为基类,可以定义一个用户样式。可以让一个应用程序看起来与众不同。通常的建议是使用和目标平台一致的样式。只要你有想法,Qt提供了很多的灵活性。
Qt提供的控件都是用QStyle绘制自己,所以在所有Qt支持的平台上,它们看起来都和平台的风格一致。
用户空间可以使用QStyle绘制自己或者使用Qt提供的控件作为子控件。对于Plotter,我们使用两个方式的组合,焦点矩形用QStyle样式绘制,zoomIn和zoomOut按钮为Qt提供的控件。
void Plotter::resizeEvent(QResizeEvent * /* event */)
{
    int x = width() - (zoomInButton->width()+ zoomOutButton->width() + 10);
    zoomInButton->move(x, 5);
    zoomOutButton->move(x + zoomInButton->width() + 5, 5);
    refreshPixmap();
}
控件大小改变时,Qt都会产生一个“resize”事件。这里,我们重写了resizeEvent()把zoomIn和zoomOut按钮放在Plotter控件的右上角。
我们把zoomIn和zoomOut按钮并排摆放,中间有5个象素的空隙,距离控件的上边距和右边距也为5个象素宽。
如果我们希望按钮放在控件的左上角(坐标点为(0,0))上,直接可以在Plotter构造函数中它们移动到左上角。如果我们想跟踪控件的右上角,它的坐标取决与控件的大小。因此需要重写resizeEvent()设置按钮位置。
在Plotter构造函数中,我们没有确定按钮的位置。但是不要紧,在控件第一次显示之前,Qt就会产生一个resize事件。
如果不重写resizeEvent()手工排列子控件,还可以使用布局管理器,如QGridLayout。使用一个布局会有些复杂,也会消耗更多资源。另一方面,它能够把左右排列的布局安排的更好,对Arabic和Hebrew语言尤其适用。
最后,调用refreshPixmap()绘制新的尺寸下的图片。
void Plotter::mousePressEvent(QMouseEvent *event)
{
    QRect rect(Margin, Margin,
              width() - 2 * Margin, height() - 2 * Margin);
    if (event->button() == Qt::LeftButton) {
        if (rect.contains(event->pos())) {
            rubberBandIsShown = true;
            rubberBandRect.setTopLeft(event->pos());
            rubberBandRect.setBottomRight(event->pos());
            updateRubberBandRegion();
            setCursor(Qt::CrossCursor);
        }
    }
}
当用户点击了鼠标左键,在控件上显示出一个橡皮线,显示的条件是rubberBandIsShown为true。把变量rubberBandRect的左上角和右下角都为当前的鼠标点,然后发出一个绘制事件绘制橡皮线,同时把光标改为十字型。
变量rubberBandRect为QRect类型。一个QRect可以由四个量(x,y,width,height)定义。其中(x,y)为矩形左上角的坐标,width*height为矩形的面积。或者由左上角和右下角的坐标对定义。在这里使用了坐标对定义的方法,把矩形的左上角和右下角的坐标都设置为鼠标点击的位置。然后调用updateRubberBandRegion()把橡皮线内的区域绘制出来。
Qt有两种设置光标形状的方法:
QWidget::setCursor(),当鼠标移动到一个控件上时,使用这个函数设置光标的形状。如果子控件上没有设置光标形状,则使用父控件的光标。通常使用的光标是一个箭头式光标。
QApplication::setOverrideCursor()设置应用程序的光标形状,取代控件中设定的光标,调用restoreOverrideCursor()后光标回到原来的状态。
在第四章中,我们调用了QApplication::setOverrideCursor()把光标设置为Qt::WaitCursor,把应用程序光标设置为等待式光标。
void Plotter::mouseMoveEvent(QMouseEvent *event)
{
    if (rubberBandIsShown) {
        updateRubberBandRegion();
        rubberBandRect.setBottomRight(event->pos());
        updateRubberBandRegion();
    }
}
当用户点中鼠标左键移动鼠标时,调用updateRubberBandRegion()重新绘制橡皮线所在区域。然后根据鼠标移动的位置重新计算橡皮线区域的大小,最后在调用updateRubberBandRegion()绘制新的橡皮线区域。这样就可以删除原来的橡皮线,在新的位置绘制新的橡皮线。
如果用户向上或者向下移动鼠标,rubberBandRect的右下角可能会到达它的左上角的上面或者左面,QRect的width和height会出现负值,在paintEvent()函数中调用了QRect::normalized()函数,它可以重新计算矩形的左上角和右下角的坐标值,保证得到一个非负的宽和高。
void Plotter::mouseReleaseEvent(QMouseEvent *event)
{
    if ((event->button() == Qt::LeftButton) && rubberBandIsShown) {
        rubberBandIsShown = false;
        updateRubberBandRegion();
        unsetCursor();
        QRect rect = rubberBandRect.normalized();
        if (rect.width() < 4 || rect.height() < 4)
            return;
        rect.translate(-Margin, -Margin);
        PlotSettings prevSettings = zoomStack[curZoom];
        PlotSettings settings;
        double dx = prevSettings.spanX() / (width() - 2 * Margin);
        double dy = prevSettings.spanY() / (height() - 2 * Margin);
        settings.minX = prevSettings.minX + dx * rect.left();
        settings.maxX = prevSettings.minX + dx * rect.right();
        settings.minY = prevSettings.maxY - dy * rect.bottom();
        settings.maxY = prevSettings.maxY - dy * rect.top();
        settings.adjust();
        zoomStack.resize(curZoom + 1);
        zoomStack.append(settings);
        zoomIn();
    }
}
用户释放鼠标左键时,我们删除橡皮线,恢复到正常的箭头式光标。如果橡皮线区域大于4*4,则把这个区域放大。如果小于这个值,则很可能是用户的一个误操作,也许只是想给控件一个焦点罢了,程序返回,什么都不做了。
进行放大的这部分代码有点复杂,因为我们需要同时处理控件坐标和plotter的坐标。大部分代码都是把rubberBandRect从控件坐标转到plotter坐标。完成转换以后,调用PlotSettings::adjust()进行四舍五入,找到一个合理的坐标刻度。图5-10和图5-11示意了这个坐标的转换:
Figure 5.10. Converting the rubber band from widget to plotter coordinates

Figure 5.11. Adjusting plotter coordinates and zooming in on the rubber band

 
坐标转换以后,我们进行放大。同时把放大系数等设置形成一个新的PlotSettings对象,然后把它放到zoomStack的最上面。
void Plotter::keyPressEvent(QKeyEvent *event)
{
    switch (event->key()) {
    case Qt::Key_Plus:
        zoomIn();
        break;
    case Qt::Key_Minus:
        zoomOut();
        break;
    case Qt::Key_Left:
        zoomStack[curZoom].scroll(-1, 0);
        refreshPixmap();
      break;
    case Qt::Key_Right:
        zoomStack[curZoom].scroll(+1, 0);
        refreshPixmap();
        break;
    case Qt::Key_Down:
        zoomStack[curZoom].scroll(0, -1);
        refreshPixmap();
        break;
    case Qt::Key_Up:
        zoomStack[curZoom].scroll(0, +1);
        refreshPixmap();
        break;
    default:
        QWidget::keyPressEvent(event);
    }
}
当当前的焦点在Plotter控件上时,用户敲击了键盘的某一个键值,keyPressEvent()就会调用。这里我们重写了这个函数,相应用户对6个键的相应:+,-,Up,Down,Left和Right。如果用户敲击的键不在这六个之中,则调用基类的函数进行处理。为了简便,我们这里忽略了Shift,Ctrl,和Alt键,这些键可以通过QKeyEvent::modifiers()得到。
void Plotter::wheelEvent(QWheelEvent *event)
{
    int numDegrees = event->delta() / 8;
    int numTicks = numDegrees / 15;
    if (event->orientation() == Qt::Horizontal) {
        zoomStack[curZoom].scroll(numTicks, 0);
    } else {
        zoomStack[curZoom].scroll(0, numTicks);
    }
    refreshPixmap();
}
鼠标滚轮转动时,Qt产生一个滚轮事件(Wheel event)。很多鼠标只有一个垂直的滚轮,但是考虑到一些鼠标也有水平滚轮,Qt对这两种方式的滚轮都支持。滚轮事件只是发生在有焦点的控件上。函数delta()返回的是滚轮滚动了8°时移动的距离。一般鼠标都是以15°事件发生后,我们修改zoomStack最上面的设置,然后刷新图片。
滚轮鼠标一般用来处理滚动条。如果我们使用了QScrollArea提供一个可以滚动的区域,QScrollBar自动处理滚轮事件,我们不用自己重写wheelEvent()函数。
            双缓冲技术(Double Buffering)(4、私有函数的实现)
  以下是私有函数的实现:
  void Plotter::updateRubberBandRegion()
{
    QRect rect = rubberBandRect.normalized();
    update(rect.left(), rect.top(), rect.width(), 1);
    update(rect.left(), rect.top(), 1, rect.height());
    update(rect.left(), rect.bottom(), rect.width(), 1);
    update(rect.right(), rect.top(), 1, rect.height());
}
函数updateRubberBand()在mousePressEvent(),mouseMoveEvent()和mouseReleaseEvent()中被调用,用来删除或者从新绘制橡皮线。函数中调用了四次update(),用四个绘制事件完成由橡皮线组成的四个小矩形的绘制。Qt也提供了一个类QRubberBand用来绘制橡皮线,但是控件自己提供的绘制函数会更好
void Plotter::refreshPixmap()
{
    pixmap = QPixmap(size());
    pixmap.fill(this, 0, 0);
    QPainter painter(&pixmap);
    painter.initFrom(this);
    drawGrid(&painter);
    drawCurves(&painter);
    update();
}
函数refreshPixmap()把plot绘制到图片上,并且更新控件。首先我们把图片的大小调整为和当前控件大小相同,用控件的背景颜色填充整个图片。这个颜色是当前调色版的“dark”部分。如果背景用的刷子不是固体的(solid brush,刷子的样式,只有颜色,没有花纹的那种最简单的),QPixmap::fill()需要知道控件中刷子的偏移量,以便图片和控件保持一致。因为我们保存的是整个控件,那么因此偏移位置为(0,0)。
在这个函数中,我们使用了一个QPainter绘制图片,QPainter::initFrom()设置绘制图片所需画笔,背景和字体,参数this表示这些设置和Plotter控件的相应设置是一致的。然后我们调用drawGrid(),drawCurves()绘制网格和曲线。最后,update()函数更新全部控件,在painteEvent()函数中把图片拷贝到控件上。
void Plotter::drawGrid(QPainter *painter)
{
  QRect rect(Margin, Margin,
              width() - 2 * Margin, height() - 2 * Margin);
    if (!rect.isValid())
        return;
    PlotSettings settings = zoomStack[curZoom];
    QPen quiteDark = palette().dark().color().light();
    QPen light = palette().light().color();
    for (int i = 0; i <= settings.numXTicks; ++i) {
        int x = rect.left() + (i * (rect.width() - 1)
                                / settings.numXTicks);
        double label = settings.minX + (i * settings.spanX()
                                          / settings.numXTicks);
        painter->setPen(quiteDark);
        painter->drawLine(x, rect.top(), x, rect.bottom());
        painter->setPen(light);
        painter->drawLine(x, rect.bottom(), x, rect.bottom() + 5);
        painter->drawText(x - 50, rect.bottom() + 5, 100, 15,
                          Qt::AlignHCenter | Qt::AlignTop,
                          QString::number(label));
    }
    for (int j = 0; j <= settings.numYTicks; ++j) {
        int y = rect.bottom() - (j * (rect.height() - 1)
                                  / settings.numYTicks);
        double label = settings.minY + (j * settings.spanY()
                                          / settings.numYTicks);
        painter->setPen(quiteDark);
        painter->drawLine(rect.left(), y, rect.right(), y);
        painter->setPen(light);
        painter->drawLine(rect.left() - 5, y, rect.left(), y);
        painter->drawText(rect.left() - Margin, y - 10, Margin - 5, 20,
                          Qt::AlignRight | Qt::AlignVCenter,
                          QString::number(label));
    }
    painter->drawRect(rect.adjusted(0, 0, -1, -1));
}
函数drawGrid()在坐标轴和曲线的下面绘制网格。这个区域由一个矩形确定,如果控件太小,则不绘制。第一个循环绘制网格的垂直线,个数为x坐标轴的刻度个数。第二个循环绘制网格的水平线,共y坐标轴的刻度个数。最后,沿边界绘制一个矩形。drawText()绘制两个坐标轴上刻度的个数。
函数painter->drawText()语法如下:
painter->drawText(x, y, width, height, alignment, text);
其中(x,y,width,height)所确定的矩形确定文字的大小和位置alignment为文字的对其方式。
void Plotter::drawCurves(QPainter *painter)
{
    static const QColor colorForIds[6] = {
        Qt::red, Qt::green, Qt::blue, Qt::cyan, Qt::magenta, Qt::yellow
    };
    PlotSettings settings = zoomStack[curZoom];
    QRect rect(Margin, Margin,
              width() - 2 * Margin, height() - 2 * Margin);
    if (!rect.isValid())
        return;
    painter->setClipRect(rect.adjusted(+1, +1, -1, -1));
    QMapIterator > i(curveMap);
    while (i.hasNext()) {
        i.next();
        int id = i.key();
        const QVector &data = i.value();
        QPolygonF polyline(data.count());
        for (int j = 0; j < data.count(); ++j) {
            double dx = data[j].x() - settings.minX;
            double dy = data[j].y() - settings.minY;
            double x = rect.left() + (dx * (rect.width() - 1)
                                        / settings.spanX());
            double y = rect.bottom() - (dy * (rect.height() - 1)
                                          / settings.spanY());
            polyline[j] = QPointF(x, y);
        }
        painter->setPen(colorForIds[uint(id) % 6]);
        painter->drawPolyline(polyline);
    }
}
函数drawCurves()在网格上绘制出曲线。调用了QPainter::setClipRect()函数设置绘制曲线的矩形区域(不包括四周的间隙和框架)。QPainter会忽略画到这个区域外的象素。
然后我们遍历所有的曲线,在每一条曲线,遍历它所有的QPointF点。函数key()得到曲线的id,value()函数得到曲线的QVector类型的数据。内层循环把QPointF记录的plotter坐标转换为控件坐标,把它们保存在多段线变量中。
转换坐标后,我们设置画笔的颜色(使用函数前面预定义的颜色),调用drawPolyline()绘制出所有的曲线的点。
        双缓冲技术(Double Buffering)(5、类PlotSettings实现)
下面是PlotSettings的实现:
PlotSettings::PlotSettings()
{
    minX = 0.0;
    maxX = 10.0;
    numXTicks = 5;
    minY = 0.0;
    maxY = 10.0;
    numYTicks = 5;
}
在构造函数中,把两个坐标轴的初始化为从0到10,分为5个刻度。
void PlotSettings::scroll(int dx, int dy)
{
    double stepX = spanX() / numXTicks;
    minX += dx * stepX;
    maxX += dx * stepX;
    double stepY = spanY() / numYTicks;
    minY += dy * stepY;
    maxY += dy * stepY;
}
函数scroll()增加或者减少minX,maxX,minY,maxY的值,放大或缩小控件的尺寸为给定的偏移值乘以坐标刻度的两倍。这个函数在Plotter::keyPressEvent()函数中调用。
void PlotSettings::adjust()
{
    adjustAxis(minX, maxX, numXTicks);
    adjustAxis(minY, maxY, numYTicks);
}
函数adjust()在Plotter::mouseReleaseEvent()中调用。重新计算minX,maxX,minY,maxY的值,重新得到坐标轴刻度的个数。私有函数adjustAxis()一次计算一个坐标轴。
void PlotSettings::adjustAxis(double &min, double &max,
                              int &numTicks)
{
    const int MinTicks = 4;
    double grossStep = (max - min) / MinTicks;
    double step = pow(10.0, floor(log10(grossStep)));
    if (5 * step < grossStep) {
        step *= 5;
    } else if (2 * step < grossStep) {
        step *= 2;
    }
    numTicks = int(ceil(max / step) - floor(min / step));
    if (numTicks < MinTicks)
        numTicks = MinTicks;
    min = floor(min / step) * step;
    max = ceil(max / step) * step;
}
函数adjustAxis()修正minX,maxX,minY,maxY的值,根据给定的最大最小范围值计算刻度的个数。函数修改了参数的值(成员变量的值),所以没有使用常引用。
前部分代码主要是确定坐标轴上单位刻度的值(step)。为了得到合理的刻度数,必须得到准确的步长值。例如,一个坐标轴步长为3.8,坐标轴上其他的刻度值都是3.8的倍数,在用户很不习惯,对于一个整数坐标值,合理的步长应给为10n, 2•10n, 或者5•10n。
首先我们计算最大步长(gross step),然后计算小于或者等于这个步长的10n,通过计算这个步长的以十为底的对数,然后计算这个值的10次方。例如,如果最大步长为236,log (236)为2.37291…,四舍五入为2,得到102 = 100作为候选的步长值。
有了第一个值以后,我们再继续计算其他的候选值2•10n 和 5•10n。如上例中,另外两个可能的值为200和500。500大于最大的步长值不能使用,200小于236,使用200作为步长的值。
接着计算刻度数,min和max就很容易了。新的min值为原来的min值和步长乘积的较小整数值,新的max为原来的max值和步长乘积的较大整数值。新的numTicks为新的min和max的间隔数。例如,输入的min值为240,max为1184,新的值就会变成200,1200,200为步长,就有numTicks值为5;
有时这个算法并不是最优的。一个更加复杂的算法是Paul S. Heckbert在Graphics Gem上发表的一篇名为“Nice Numbers for Graph Labels”(ISBN 0-12-286166-3)
这一章是第一部分的最后一章。介绍了怎样从现有的Qt控件基础上得到一个新的控件,和以QWidget作为基类得到一个新的控件。在第二章我们看到了怎么在一个控件中对其他控件进行组合,在第六章中我们将会继续介绍。
到此为止,我们已经介绍了很多Qt GUI编程方面的知识。在第二部分和第三部分中,我们将会深入介绍Qt编程的其他方面。
阅读(4857) | 评论(0) | 转发(0) |
0

上一篇:qt图片显示4

下一篇:工作日记1(原创)

给主人留下些什么吧!~~