Qt学习日志-第五章

5章 定制Qt组件(Widgets)

这一章讲解怎么用Qt开发定制化的组件。我们可以继承于一个已经存在的组件,或者直接继承于 QWidget来创建一个定制化的组件。这一章中我们两个方法都会讲到,并且还要教大家怎么把这个自己开发的定制的组件集成到Qt Designer当中,使得我们用起来就跟一个内置的Qt组件一样。在这一章中我们会讲解怎么用双缓存(double buffering)技术来创建一个定制化的组件。双缓存技术是一个很有用的技术,特别对那些需要高速绘制的情况下。

5.1 HexSpinBox组建

定制化一个组件
在有些情况下,我们发现直接在Qt Designer中设置一个组件的属性,或者调用一些函数根本达不到我们对组件的需要。一个简单的方法是创建这个已存在的组件的子类,加入一些属性和函数行为以适应我们特殊的要求。

在这一部分,我们来创建一个十六进制的旋转盒,如图 5.1. QSpinBox组件只支持十进制,但是通过我们为QSpinBox创建一个子类,我们很容易对这个组件进行扩展,使得它支持显示十六进制数。

#ifndef HEXSPINBOX_H
#define HEXSPINBOX_H

#include <QSpinBox>

class QRegExpValidator;

class HexSpinBox : public QSpinBox
{
    Q_OBJECT
public:
    HexSpinBox(QWidget *parent = 0);

protected:
    QValidator::State validate(QString &text, int &pos) const;
    int valueFromText(const QString &text) const;
    QString textFromValue(int value) const;

private:
    QRegExpValidator *validator;
};

#endif

HexSpinBox类从QSpinBox类中继承了大多数的行为函数。这个类提供一个构造函数,并重新实现了QSpinBox类的三个虚函数。

 

#include <QtGui>

#include "hexspinbox.h"

HexSpinBox::HexSpinBox(QWidget *parent)
    : QSpinBox(parent)
{
    setRange(0, 255);
    validator = new QRegExpValidator(QRegExp("[0-9A-Fa-f]{1,8}"), this);
}

在这个构造函数中,我们设置了旋转范围值为0~255 (0x0 ~ 0xFF), QSpinBox的默认值范围是0~99, 对十六进制的值范围我们定义0~255比较合适。
用户可以点击上下旋转按钮来改变显示值,也可以直接输入一个值。对于后一种情况,我们想用一个正则表达式来限制用户的输入是否复合一个合法的十六进制数。
我们使用了一个正则表达式检测类QRegExpValidator来接受1到8个字符,所有的字符必须属于下列字符集 {'0',...,'9','A',...,'F','a',...'f'}.

QValidator::State HexSpinBox::validate(QString &text, int &pos) const
{
    return validator->validate(text, pos);
}

这个函数被QSpinBox调用来判断输入的文本是否有效。有三种可能的结果: Invalid(输入的文本跟正则表达式不匹配),Intermediate(输入的文本看起来是
有效文本的一部分), Acceptable(输入的文本是有效的)。 类QRegExpValidator有一个合适的函数validate(),所以这里我们只要简单的调用它并返回之。
理论上对那些超出值范围的文本我们应该返回Invalid或者Intermediate,但是这些QSpinBox都会自动帮我们检测。

QString HexSpinBox::textFromValue(int value) const
{
    return QString::number(value, 16).toUpper();
}

textFromValue()函数把一个整数转化为字符串。当用户点击旋转盒的上下箭头按钮时,QSpinBox类会调用这个函数来更新显示部分。我们使用QString的静态
函数QString::number(),指定第二个参数为16,这样就把值转化为小写的十六进制的格式,然后调用QString.toUpper()转化为大写格式。

int HexSpinBox::valueFromText(const QString &text) const
{
    bool ok;
    return text.toInt(&ok, 16);
}

跟上一个函数相反,valueFromText()函数把一个字符串转化为一个整数。当用户在编辑区输入一个值并按Enter时,这个函数被QSpinBox调用。我们调用
QString.toInt()函数,一样的我们指定基数为16.如果字符串不是有效的十六进制格式,ok会被设置成false,并且toInt()返回0.这样我们不必考虑这种
可能性,因为正则表达式检测只允许我们输入有效的十六进制格式的字符串。这里我们也可以指定toInt()函数的第一个参数为null指针。

到这里我们已经实现了一个十六进制的旋转盒。安装同样的方法,我们可以定制其它的组件:首先挑选一个合适的已存在的Qt组件,继承它,重新实现它的一些虚函数
来改变组件的一些行为。如果我们只是想要改变一个已经存在的组件的外观,我们可以采用风格单(style sheet),或者实现一个指定的风格,而不需要继承一个组件。
这个方法我们将会在第19章中讲解。


继承QWidget

很多定制化的组件就是一些已存在的组件的组合。我们可以在Qt Designer中来开发这种定制化的组件:

  • 使用组件模板创建一个新的form。
  • 添加必要的组件到form中,并且对它们进行布局。
  • 设置好信号-槽连接。
  • 如果需要的那些行为通过信号-槽满足不了,在这个类中写一些必要的代码。一般我们这个类多继承于QWidget和uic生产的类。

当然,组合这些组件也完全可以写代码的方式来实现。不管用哪个方法,最后实现的那个类都是QWidget的子类。

如 果组件中没有任何自己的信号和槽,也没有重新实现任何虚函数,那么我们也可以简单的把那些已存在的组件类直接进行组合,而不需要创建一个子类。我们在第一 章中创建一个Age应用程序就是用这个方法,直接把QWidget,QSpinBox和QSlider组件进行组合。当然我们也可以简单的创建一个 QWidget的子类,然后在子类的构造函数中创建QSpinBox和QSlider组件。

当Qt中已存在的组件都不能满足我们手头的工 作时,这样我们就没有办法仅仅组合已经存在的组件来达到我们的结果,我们仍然有办法创建一个我们需要的组件。我们可以这样来达到我们的要求: 创建一个QWidget的子类,重新实现一些事件句柄来绘制这个组件及响应鼠标事件。使用这个方法我们就可以完全按照需求的外观和行为来定义自己组件。 Qt中的内置组件,例如QLabel,QPushButtion和QTableWidget,就是按照这个方法实现的。如果它们在Qt中不存在,我们完全 可以通过使用一些QWidget类中提供的公有函数来创建不依赖于平台的组件。

为了演示怎么使用这个方法来创建定制化的组件,我们将会创建一个IconEdit组件,如图5.2所示。这个IconEdit组件可以在图表编辑程序中使用。
在实践中,在创建我们自己的组件之前,最后看看这个组件是否已经存在了,在http://www.trolltech.com/products/qt/addon/solutions/catalog/4/ 中查看Qt方案,或者查看是否存在于商业的或非商业的第三方方案(http://qt.nokia.com/products/qt/3rdparty/ ),这样可以让你节省很多时间。在这个例子中我们假设没有存在合适的组件,这样我们自己来创建它。

让我们先从头文件开始看起。

#ifndef ICONEDITOR_H
#define ICONEDITOR_H

#include <QColor>
#include <QImage>
#include <QWidget>

class IconEditor : public QWidget
{
    Q_OBJECT
    Q_PROPERTY(QColor penColor READ penColor WRITE setPenColor)
    Q_PROPERTY(QImage iconImage READ iconImage WRITE setIconImage)
    Q_PROPERTY(int zoomFactor READ zoomFactor WRITE setZoomFactor)

public:
    IconEditor(QWidget *parent = 0);

    void setPenColor(const QColor &newColor);
    QColor penColor() const { return curColor; }
    void setZoomFactor(int newZoom);
    int zoomFactor() const { return zoom; }
    void setIconImage(const QImage &newImage);
    QImage iconImage() const { return image; }
    QSize sizeHint() const;

 

IconEdit 类使用Q_PROPERTY()宏来声明三个定制的属性:penColor, iconImage和zoomFactor,没个属性都有一个数据类型,一个”读“函数,和一个可选的“写“函数。例如,penColor属性是 QColor类型,可以用penColor()函数来读取这个属性,可以用setPenColor()函数来写(设置)这个属性。

当我们在Qt Designer中使用这个组件时,这些定制化的属性会出现在Qt Designer中属性编辑区域中,这些属性位于那些从QWidget继承下来的属性的下面。属性可以是任何QVariant支持的类型。宏Q_OBJECT对那些需要定义属性的类是必须要添加的。

 

protected:

    

void mousePressEvent(QMouseEvent *event);

    

void mouseMoveEvent(QMouseEvent *event);

    

void paintEvent(QPaintEvent *event);

 

private:

    

void setImagePixel(const QPoint &pos, bool opaque);

    

QRect pixelRect(int i, int j) const;

 

    

QColor curColor;

    

QImage image;

    

int zoom;

};

 

#endif

 

IconEdit 对父类 QWidget 重新实现了三个保护函数,并添加了自己的一些私有函数和变量。三个私有变量保存了三个属性值。

 

实现的文件如下:

 

#include <QtGui>

 

#include "iconeditor.h"

 

IconEditor::IconEditor(QWidget *parent)

    

: QWidget(parent)

{

    

setAttribute(Qt::WA_StaticContents);

    

setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);

 

    

curColor = Qt::black;

    

zoom = 8;

 

    

image = QImage(16, 16, QImage::Format_ARGB32);

    

image.fill(qRgba(0, 0, 0, 0));

}

这个构造函数有点微妙,比如 Qt::WA_StaticContents 属性,还有对 setSizePolicy() 函数的调用。我们很快会对这个进行讨论。

 

画笔颜色被设置成黑色。缩放因子设置为 8 ,意味着图标中的每个像素占据 8*8 个小格子的空间。

 

图标的图像数据保存在 image 成员变量中,可以用 setIconImage() iconImage() 函数对这个变量进行存储操作。当用户打开一个图标文件时,图标编辑程序就调用 setIconImage() 函数;当用户想保存这个图标时,则调用 iconImage() 函数来得到这个图标数据。 Image 变量是 QImage 类型。我们初始化一个 16*16 像素,并且是 32 ARGB 格式(这个格式支持半透明)的 QImage 类型变量。我们先设置为透明的颜色。

 

QImage 类保存了一个不依赖于硬件的图像数据。它可以被设置成 1-bit,8-bit 或者 32-bit 深度。一个 32-bit 深度的图像为每一个像素的红,蓝,绿分量各由 8 位来表示。剩下的那个 8 位保存了像素的 alpha 通道值,表示透明度。例如,一个纯红颜色的红,蓝,绿和 alpha 分量的值分别为 255, 0, 0 255 。在 Qt 中,可以这样来表示:

QRgb red = qRgba(255, 0, 0, 255);

或者,因为这个颜色是不透明的,也可以这样来表示:

QRgb red = qRgb(255, 0, 0);

 

QRgb 实际上就是 unsigned int 类型, qRgb() qRgba() 是内敛函数,用来把传给它们的参数组合成一个 32 位的 ARGB 整数值。当然我们可以直接写出这样:

QRgb red = 0xFFFF0000;

这里第一个 FF 对应于 alpha 因子值,第二个 FF 对于着红色分量。在 IconEditor 构造函数中,我们设置 QImage 为透明的颜色, alpha 因子值为 0.

 

Qt 提供两种类型来保存颜色值: QRgb QColor. QRgb 其实是 unsigned int 类型,只是用于 QImage 中用来保存 32 位的像素值,而 QColor 是一个类,它有好多有用的成员函数,这个类广泛用于 Qt 中,用来保存颜色。在 IconEditor 组建中,我们使用 QRgb 只是用来处理 QImage ;在其它地方,我们都是用 QColor ,包括 penColor 属性。

 

QSize IconEditor::sizeHint() const

{

    

QSize size = zoom * image.size();

    

if (zoom >= 3)

        

size += QSize(1, 1);

    

return size;

}

sizeHint() 函数是对于从 QWidget 中继承而来的函数的重新实现,这个函数返回组建的理想的大小尺寸。这里,我们把 image 的大小乘以缩放因子来得到组建的大小,如果放大倍数大于等于 3 ,则在每个方向上增加一个像素的大小来容纳网格。如果缩放因子是 2 1 ,我们没有空间来显示网格。

 

组建的默认大小在布局时特别有用。 Qt 的布局管理器会根据这个 sizeHint 值来排列子组建。如果要想布局管理器来很好的管理这个 IconEditor 组建,我们必须告诉它一个默认的尺寸。

 

除了这个默认提示的尺寸大小,组建也有一个尺寸的规则来告诉布局管理器这个组建是否可以被拉伸或者缩小。通过在构造函数中调用 setSizePolicy() 来设置水平和垂直规则为 QSizePolicy::Minimum ,我们告诉布局管理器这个默认的尺寸即是它的最小尺寸。换句话说,组建需要的时候可以被拉伸,但是绝对不可以缩小到比这个默认的尺寸还小。这个属性可以在 Qt Designer 当中进行设置。我们会在第 6 章中对这些尺寸的规则进行解释。

 

void IconEditor::setPenColor(const QColor &newColor)

{

    

curColor = newColor;

}

setPenColor() 函数设置当前的画笔的颜色。这个颜色将会被用于新绘制的像素。

 

void IconEditor::setIconImage(const QImage &newImage)

{

    

if (newImage != image) {

        

image = newImage.convertToFormat(QImage::Format_ARGB32);

        

update();

        

updateGeometry();

    

}

}

 

setIconImage() 函数设置要被编辑的图片。我们调用 convertToFormat() 函数把图转换为 32 位带 alpha 通道格式。其它代码中,我们就认为图像数据是保存为 32 ARGB 值的。

 

设置 image 变量以后,我们调用 QWidget::update() 函数来对组建进行重绘。接下来我们调用 QWidget::updateGeometry() 来告诉布局管理器组建的 sizeHint 已经被修改。布局管理器就会自动使用新的默认尺寸来。

 

void IconEditor::setZoomFactor(int newZoom)

{

    

if (newZoom < 1)

        

newZoom = 1;

 

    

if (newZoom != zoom) {

        

zoom = newZoom;

        

update();

        

updateGeometry();

    

}

}

 

setZoomFactor() 函数用来设置图片的缩放因子。为了避免除数为 0 的情况,我们把所有小于 1 的值都设置为 1. 同样,我们调用 update() updateGeometry() 来重绘组件并且告诉布局管理器默认尺寸已经修改。

 

penColor() iconImage() zoomFactor() 函数作为内敛函数实现放在头文件中。

 

现在我们来看看 paintEvent() 函数的代码。这个函数是 IconEditor 组件中最重要的函数。无论什么时候组件需要重绘时这个函数就会被调用。 QWidget 中默认的实现没有做任何事情,所以 QWidget 是一个空的组件。

 

就如我们第 3 章中碰到的 closeEvent() 函数, paintEvent() 也是一个事件句柄。 Qt 有许多其他的事件句柄,每个句柄都对应一个不同的事件。第 7 章中会详细讲解所有的事件处理。

 

许多情况下会产生一个绘画事件, paintEvent() 被调用。比如:

1.       当一个组件第一次显示的时候,系统自动产生一个绘画事件来强制组件绘制自己。

2.       当一个组件被改变大小时,系统会产生一个绘画事件。

3.       如果组件被其它窗口挡住,然后被再次激活时,也会产生一个绘画挡住部分的事件(除非这部分区域被锁住了)。

 

我们也可以通过调用 QWidget:update() QWidget::repaint() 强制产生一个绘画事件。这两个函数的不同点在于 repaint() 强制立刻进行重绘,而 update() 只是调度一个绘画事件, Qt 在下一次事件处理时会对这个事件进行响应。(如果组件在屏幕上不可见,那么这两个函数将不做任何事情。)如果 update() 函数被多次调用, Qt 把多次连续调用的绘画事件压缩成一次,避免屏幕抖动。在 IconEditor ,我们总是使用 update()

这里是代码:

void IconEditor::paintEvent(QPaintEvent *event)

{

    

QPainter painter(this);

    

if (zoom >= 3) {

        

painter.setPen(palette().foreground().color());

        

for (int i = 0; i <= image.width(); ++i)

            

painter.drawLine(zoom * i, 0,

                             

zoom * i, zoom * image.height());

        

for (int j = 0; j <= image.height(); ++j)

            

painter.drawLine(0, zoom * j,

                             

zoom * image.width(), zoom * j);

    

}

    

for (int i = 0; i < image.width(); ++i) {

        

for (int j = 0; j < image.height(); ++j) {

            

QRect rect = pixelRect(i, j);

            

if (!event->region().intersect(rect).isEmpty()) {

                

QColor color = QColor::fromRgba(image.pixel(i, j));

                

if (color.alpha() < 255)

                    

painter.fillRect(rect, Qt::white);

                

painter.fillRect(rect, color);

            

}

        

}

    

}

}

 

首先我们创建一个 QPainter 对象。如果缩放因子大于等于 3 ,我们使用 QPainter::drawLine() 函数绘制水平线和垂直线来形成网格。

 

函数 QPainter::drawLine() 的调用形式:

painter.drawLine(x1, y1, x2, y2);

(x1, y1) 是直线的起点,( x2, y2 )是直线的终点。也有一个重载函数,带有两个 QPoint 类型而不是四个 int 类型。

 

一个 Qt 组件的左上角的像素位于 (0, 0) ,而右下角像素位于 (width() -1, height() – 1) 。这个跟传统的笛卡尔坐标类似,只是 y 坐标向下,如图 5.3 所示。我们可以利用坐标变换改变 QPainter 的坐标系统,如平移,缩放,旋转,剪切。我们会在第 8 章中讲解这部分内容。

       5.3 使用 QPainter 绘制直线

 

我们调用 drawLine() 绘制直线以前,先调用 setPen() 来设置直线的颜色。我们可以直接用代码设置颜色,如灰色或黑色,但是用组件的调色板是一个更好的办法。

 

每一个组件都配备了一个调色板来设置不同部位的颜色。如,组件的背景颜色(一般是亮灰色),文本颜色(通常是黑色)。默认情况下,一个组件的调色板采用和所使用系统的窗口颜色机制。通过使用调色板的颜色,我们确保 IconEditor 中使用的颜色跟用户的喜好一直。

 

一个组件的调色板由三个颜色组组成:激活的,未激活的和不可用的。使用哪个颜色组需要根据组件当前的状态而定:

1.       激活组被用于当前激活窗口中的组件的颜色。

2.       未激活颜色组则被用于其它窗口中的组建的颜色。

3.       不可用的颜色组则被用于任何窗口中那些不可用的组件的颜色。

 

QWidget::palette() 函数返回一个组件的调色板对象 QPalette 的实例。颜色组则被指定为枚举类型 QPalette::ColorGroup

 

当我们想要得到一个合适的刷子或颜色来绘制时,正确的方法是使用当前的调色板,由函数 QWidget::palette() 得到,以及需要的角色,比如 , QPalette::foreground() 。每个角色函数返回一个刷子,如果我们只是需要颜色,则我们只是根据这个刷子得到一个颜色值,如在 paintEvent() 中。默认情况下,角色函数得到的刷子跟组件的状态是一致的,所以我们不需要制定一个颜色组。

 

paintEvent() 函数最后绘制了图片。调用 IconEditor::pixelRect() 函数返回 QRect 对象实例,这个实例定义了需要重绘的区域。图 5.4 显示了一个矩形是怎么被绘制的。为简单起见,我们不重绘这个区域外面的像素。

                     5.4 使用 QPainter 绘制一个矩形

 

我们调用 QPainter::fillRect() 函数来画一个放大的像素。 QPainter::fillRect() 需要两个参数 QRect QBrush 。我们把 QColor 作为刷子传递给这个函数,这样说明实填充模式。如果颜色不是完全的不透明(即 alpha 通道值小于 255 ),我们则先绘制白色背景。

 

QRect IconEditor::pixelRect(int i, int j) const

{

    

if (zoom >= 3) {

        

return QRect(zoom * i + 1, zoom * j + 1, zoom - 1, zoom - 1);

    

} else {

        

return QRect(zoom * i, zoom * j, zoom, zoom);

    

}

}

 

pixelRect() 函数返回一个 QRect ,传递给 QPainter::fillRect() 。参数 i j QImage 中的像素坐标,注意:不是组建中的坐标。如果缩放因子是 1 ,则这两个坐标系统刚好完全一样。

 

QRect 的构造函数格式 QRect(x, y, width, height) ,( x, y )是矩形区域的左上角, width*height 是矩形区域的大小。如果缩放因子大于等于 3 ,我们在水平方向上和垂直方向上各减少一个像素,这样我们不会把网格线给覆盖率。

 

void IconEditor::mousePressEvent(QMouseEvent *event)

{

    

if (event->button() == Qt::LeftButton) {

        

setImagePixel(event->pos(), true);

    

} else if (event->button() == Qt::RightButton) {

        

setImagePixel(event->pos(), false);

    

}

}

 

当用户按下鼠标按钮时,系统产生一个“鼠标按下”事件。这里重新实现了 QWidget::mousePressEvent() 函数,我们可以响应这个事件,设置或清楚光标下的图像像素。

 

如果用户按钮来鼠标左键,我们调用私有函数 setImagePixel() ,并设置第二个参数为 true ,即设置当前像素为当前的 penColor 画笔颜色。如果用户按下右键,我们清楚当前像素。

 

void IconEditor::mouseMoveEvent(QMouseEvent *event)

{

    

if (event->buttons() & Qt::LeftButton) {

        

setImagePixel(event->pos(), true);

    

} else if (event->buttons() & Qt::RightButton) {

        

setImagePixel(event->pos(), false);

    

}

}

 

mouseMoveEvent() 函数处理“鼠标移动”事件。默认情况下,只有当用户按住鼠标键不放时才会产生这些事件。可以调用 QWidget::setMouseTracking() 函数改变这个行为,在这个例子中我们不需要这么做。

 

就像鼠标的按下事件那样,保持按住并在像素上面移动同样可以设置或清除像素。因为存在着两个鼠标键同时被按下的可能,所以 QMouseEvent::buttons() 函数返回的值是一个位或。我们使用与操作 & 来测试是否某个按键被按下,如果是则调用 SetImagePixel() 函数。

 

void IconEditor::setImagePixel(const QPoint &pos, bool opaque)

{

    

int i = pos.x() / zoom;

    

int j = pos.y() / zoom;

 

    

if (image.rect().contains(i, j)) {

        

if (opaque) {

            

image.setPixel(i, j, penColor().rgba());

        

} else {

            

image.setPixel(i, j, qRgba(0, 0, 0, 0));

        

}

        

update(pixelRect(i, j));

    

}

}

setImagePixel() 函数被 mousePressEvent() mouseMoveEvent() 函数调用来设置或清除一个像素。参数 pos 是鼠标在组件中的位置。

 

第一步是把鼠标的位置从组件坐标转换为图标坐标。通过把鼠标位置的 x y 坐标除以缩放因子来得到鼠标在图像中的位子。接下来,我们判断这个点是否在正确的范围内。判断很容易, QImage::rect() QRect::contains() 得到结果了;这里会检测 i 是否属于 0 image.width()-1 范围内, j 是否属于 0 image.height()-1 范围内。

 

根据 opaque 参数,我们设置或者清除图片中的像素。清除一个像素就是把这个像素设置成完全透明。我们必须把绘画笔的 QColor 转化为 32 ARGB 值,传给 QImage::setPixel() 。最后,我们调用 update() ,参数 QRect 就是需要重绘的区域,由 pixelRect() 得到。

 

现在我们已经浏览了所有的成员函数,我们再来看看构造函数中使用的 Qt::WA_StaticContents 属性。这个属性告诉 Qt 当组件大小改变时这个组件的内容不会改变,即里面的图片不会跟着缩放,内容仍旧固定在组件的左上角。当组件改变大小时, Qt 使用这个属性不对那些已经显示的区域进行重绘。如图 5.5 所示。

              5.5 改变一个 Qt::WA_StaticContents 属性的组件

 

通常当一个组件改变大小时, Qt 会产生一个绘画事件来绘制整个可见的区域。但是如果组件创建时被指定属性 Qt::WA_StaticContents, 则绘画事件的区域被限制在以前没有显示的那一部分像素。这意味着如果组件被缩小了,则不会有绘画事件产生。

 

我们已经完成了 IconEditor 组件。使用以前章节中的例子和知识,我们可以写一些代码来使用这个组件,可以作为一个在 QMainWindow 中的中央组件,或者作为一个布局中的子组件,或者放到 QScrollArea 中。下一部分,我们会看到怎么把这个组件集成到 Qt Designer 中。

 

集成定制的组件到 Qt Designer 当中

 

Qt Designer 中使用定制的组件之前,我们必须让 Qt Designer 知道它们。有两种技术可以实现:“升级” (promotion) 法和插件法。

 

升级法是最简单和方便的。选择一个 Qt 内置的组件,这个组件跟我们定制的组件具有类似的 API ,在 Qt Designer 的定制组件对话框中添加一些这个定制组件的信息就大功告成了。如图 5.6 。这个定制的组件然后就可以在 Qt Designer 中使用了,尽管在编辑和预览的时候,呈现的跟内置的 Qt 组件一样。

              5.6. Qt Designer 的定制组件对话框

 

这里讲解一下怎么用这种方法把 HexSpinBox 组件插入到窗体中:

1.       Qt Designer 中的组件箱中拖一个 QSpinBox 到窗体中。

2.       右键旋转盒,从右键菜单中选择 Promote to Custom Widget

3.       填入对话框,类名为“ HexSpinBox ”,头文件为“ hexspinbox.h ”。

 

好了。 Uic 产生的代码将会包含 hexspinbox.h 而不是 <QSpinBox> ,实例化一个 HexSpinBox 。在 Qt Designer 中, HexSpinBox 组件会按 QSpinBox 呈现,允许我们设置 QSpinBox 所有的属性。(比如,值的范围和当前值)。

 

升级法的缺点是不能在 Qt Designer 中设置那些自定义组件的特有的属性,也不能绘制自己。这些问题可以用插件法来解决。

 

插件法要求创建一个插件库, Qt Designer 会在运行时加载这个库并创建一个组件的实例。这样在编辑和预览时, Qt Designer 就会使用这个真正的组件,多亏了 Qt 的元 - 对象机制, Qt Designer 能够得到一系列组件的属性。为了演示这个方法是怎么工作的,我们会把上一节中实现的 IconEditor 作为插件集成到 Qt Designer 中。

 

首先,我们必须创建一个 QDesignerCustomWidgetInterface 的子类,并重新实现一些徐函数。我们假设插件的源代码位于一个叫做 iconeditorplugin 的目录中,而 IconEditor 的源代码位于一个并行的目录 iconeditor 中。

这里是类的定义:

 

#include <QDesignerCustomWidgetInterface>

 

class IconEditorPlugin : public QObject,

                         

public QDesignerCustomWidgetInterface

{

    

Q_OBJECT

    

Q_INTERFACES(QDesignerCustomWidgetInterface)

 

public:

    

IconEditorPlugin(QObject *parent = 0);

 

    

QString name() const;

    

QString includeFile() const;

    

QString group() const;

    

QIcon icon() const;

    

QString toolTip() const;

    

QString whatsThis() const;

    

bool isContainer() const;

    

QWidget *createWidget(QWidget *parent);

};

IconEditorPlugin 子类是一个工厂类,这个类封装了 IconEditor 组件。这个类多继承于 QObject QDesignerCustomWidgetInterface ,类定义中使用了宏 Q_INTERFACES() 来告诉 moc 第二个基类是一个插件类。 Qt Designer 使用一些函数来创建类的实例并获得关于这个实例的一些信息。

 

IconEditorPlugin::IconEditorPlugin(QObject *parent)

    

: QObject(parent)

{

}

这个构造函数是个空函数。

 

QString IconEditorPlugin::name() const

{

    

return "IconEditor";

}

name() 函数返回由插件提供的组件的名字。

 

QString IconEditorPlugin::includeFile() const

{

    

return "iconeditor.h";

}

includeFile() 函数返回这个插件封装的组件的头文件的名字。这个头文件会被包含在 uic 工具生成的代码中。

 

QString IconEditorPlugin::group() const

{

    

return tr("Image Manipulation Widgets");

}

group() 函数返回组件所属的工具箱的名字。如果这个名字当前还不存在, Qt Designer 就会为这个组件创建新组。

 

QIcon IconEditorPlugin::icon() const

{

    

return QIcon(":/images/iconeditor.png");

}

icon() 函数返回一个图标,这个图标显示在 Qt Designer 的工具箱中。这里,我们假设 IconEditorPlugin 有一个关联的资源文件,里面有一个图标编辑器的图标。

 

QString IconEditorPlugin::toolTip() const

{

    

return tr("An icon editor widget");

}

toolTip() 函数返回提示信息,当鼠标停留在 Qt Designer 中组件箱中的这个组件时,这个提示信息就会显示。

 

QString IconEditorPlugin::whatsThis() const

{

    

return tr("This widget is presented in Chapter 5 of <i>C++ GUI "

              

"Programming with Qt 4</i> as an example of a custom Qt "

              

"widget.");

}

whatsThis() 函数返回 Qt Designer 显示的“ What’s This? ”提问。

 

bool IconEditorPlugin::isContainer() const

{

    

return false;

}

如果这个组件可以包含其他组件,则 isContainer() 函数返回 true ;否则返回 false 。例如, QFrame 是一个可以包含其他组件的组件。通常情况下,任何 Qt 组件都可以包含其它的组件,但是当 isContainer() 返回 false 时, Qt Designer 就不允许这个功能来。

 

QWidget *IconEditorPlugin::createWidget(QWidget *parent)

{

    

return new IconEditor(parent);

}

Qt Designer 调用 createWidget() 函数来创建一个组件类的实例,父组件作为参数传递。

 

Q_EXPORT_PLUGIN2(iconeditorplugin, IconEditorPlugin)

 

插件类源文件的最后,我们必须使用 Q_EXPORT_PLUGIN2() 宏,这个宏可以让 Qt Designer 使用这个插件。这个宏的第一个参数是我们想要给这个插件的名字;第二个参数是插件类的类名。

 

.pro 文件如下:

TEMPLATE     

= lib

CONFIG      

+= designer plugin release

HEADERS      

= ../iconeditor/iconeditor.h /

               

iconeditorplugin.h

SOURCES      

= ../iconeditor/iconeditor.cpp /

               

iconeditorplugin.cpp

RESOURCES    

= iconeditorplugin.qrc

DESTDIR      

= $$[QT_INSTALL_PLUGINS]/designer

 

Qmake 编译工具有一些预定义的变量。其中一个是 $$[QT_INSTALL_PLUGINS] ,这个变量保存着按照 Qt 的插件目录。当你输入 make 或者 nmake 编译插件时,它会自动安装到 Qt plugins/designer 目录中。一旦插件被编译安装, IconEditor 组件就可以跟其它内置的组件一样在 Qt Designer 中使用了。

 

如果你想集成多个定制的组件到 Qt Designer 中,你可以为每一个定制组件创建一个插件,也可以通过继承于 QDesignerCustomWidgetCollectionInterface 类来为它们一次性创建。

 

双缓存技术( Double Buffering

双缓冲技术是 GUI 编程中的一项技术,它是把组件绘制到一个离线的像素映射表中,然后把这个像素映射表显示出来。早期的 Qt 版本中,这个技术经常用来消除抖动,使界面显示更加迅速。

 

Qt 4 中, QWidget 会自动处理这种情况,所以我们很少需要担心组件会闪烁。然而,如果需要绘制的组件很复杂并且不断的需要绘制,那么我显式的使用双缓冲技术将会得到更好的效果。我们可以把一个组件永久的保存在一个像素映射表中,随时准备下一次绘制事件的到来,一旦我们接受到一个绘制事件就把像素映射表拷贝到组件中。这个方法很有用,特别当我们只需要做一个很小的修改,比如画一个橡皮筋,不需要一遍一遍的刷新整个组件。

 

我们来实现一个定制组件 Plotter 来结束本章,如图 5.7 5.8 。这个组件使用双缓冲技术,并演示了另外一些 Qt 编程技术,包括键盘事件出来,手动布局,以及坐标系统。

              5.7 Zooming in on the Plotter widget

 

对于一个真正的绘图组件,通常我们会直接使用那些已经存在的第三方组件,不会自己去创建一个。比如,我们可以使用 GraphPak http://www.ics.com ,KD Chart(http://www.kdab.net/ ), 或者 Qwt(http://qwt.sourceforge.net/ )

 

Plotter 组件显示一个或多个曲线,这组曲线由坐标向量表示。用户可以在图像上绘制一条橡皮筋线,然后 Plotter 程序会把选中的区域进行放大。用户可以这样来绘制一个选中框,首先在图像上点击一个点,按住左键不放,拖动鼠标到另一个位置,然后释放鼠标。 Qt 提供类 QRubberBand 来方便我们绘制橡皮筋线,但是这里我们自己来画,为了有一个更好看的外观,并且演示双缓冲技术。

 

用户可以通过多次绘制选中框来不断的放大,使用放大按钮则进行放大,缩小按钮则进行缩小。放大跟缩小按钮只有在它们第一次可使用的情况下才显示,这样图片不会显得凌乱如果用户不想缩放图片。

 

Plotter 组件可保持任何数量的曲线数据。并且一个 PlotSettings 对象的栈,每个 PlotSetting 对象实例对应着一个缩放的程度。

 

让我们来看看代码, plotter.h

 

#ifndef PLOTTER_H
#define PLOTTER_H

#include <QMap>
#include <QPixmap>
#include <QVector>
#include <QWidget>

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();

我 们包含了一些头文件,因为这些数据结构会在这里用到,以及前向申明了一些会在下面用到的指针或引用的类。在Plotter类中,我们提供了三个公有函数, 两个公共槽用来放大缩小,并且我们重新实现了从QWidget继承过来的minimumSizeHint()和sizeHint()函数。我们把曲线的点 都保存在QVector<QPointF>向量中,这里QPointF是QPoint类的浮点数版本。

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);

在类的保护区域部分,我们声明了所有从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;
};

在类的私有部分,我们声明了一些绘制组件的函数,一个常量,以及一些成员变量。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);
};

#endif

PlotSettings类指定x轴和y轴的范围,以及这些轴的刻度数。图5.8显示了PlotSettings对象和Plotter组件的对应关系。

5.8 PlotSettings的成员变量


按照约定,numXTicks和numYTicks值比实际绘制的要少1;比如,如果numXTicks值为5,Plotter实际上会在x轴上绘制6个刻度。这个会简化我们将来的计算。

现在我们来看看实现文件:

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() 函数告诉QWidget使用调色盘的黑色作为背景颜色,而不是默认的窗口(window)颜色。当组件被放大时,Qt就用这个设定的默认的颜色来填充任何 新区域,因为这个时候paintEvent()事件还没有机会来绘制这些新的区域。我们还必须调用 setAutoFillBackground(true)函数来使能这个机制。(默认情况下,子组件会从父组件中继承背景色。)

setSizePolicy() 函数设置组件的尺寸策略在两个方向上都为QSizePolicy::Expanding。这个设置告诉负责这个组件的布局管理器,这个组件可以放大或缩 小。对应那些需要在屏幕上占据很大空间的组件,通常进行这个设置。默认的设置是,两个方向上都为QSizePolicy::Preffered,意味着组 件一般大小为sizeHint值,但是如果需要也可以缩小到最近的尺寸,或者也可以无限的放大。

setFocusPolicy(Qt::StrongFocus) 函数调用使得这个组件可以通过鼠标点击或者Tab键来接受焦点。当Plotter组件得到焦点时,它才会接收按键的事件。Plotter组件会处理一些键 值,比如,+放大,-缩小,箭头方向键用来向上下左右滚动。

5.9 滚动Plotter组件


另外,我们在构造函数中创建了两个QToolButton按钮,并且没个按钮都对应一个图标。这两个按钮允许用户放大或缩小图片。按钮的图标被保存在一个资源文件中,所以任何需要使用这个Plotter组件的程序都必须在.pro 文件中添加这一行:
RESOURCES = plotter.qrc

资源文件的格式跟我们在上一章中为Spreadsheet程序所写的资源文件很类似:

<RCC>
<qresource>
    <file>images/zoomin.png</file>
    <file>images/zoomout.png</file>
</qresource>
</RCC>

对 按钮调用了adjustSize()函数用来设置按钮的大小为它们的推荐(sizeHint)值。按钮没有被放到布局管理器中;相反,我们会在 Plotter的resize事件中手动的来定位它们。因为我们没有使用任何布局,我们必须明确的指定按钮的父组件(传递this给 QToolButton的构造函数)。

构造函数的最后调用setPlotSettings()函数。

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

setPlotSettings() 函数用来指定PlotSettings,这些PlotSettings用来显示这个组件。这个函数在构造函数中被调用,也可以被使用这个类的用户调用(公 有函数)。Plotter程序启动时使用缺省的缩放值。每次用户点击放大时,一个新的PlotSettings实例会被创建,并且放入这个缩放堆栈中。缩 放堆栈由两个成员变量表示:

  • zoomStack是一个向量类型QVector<PlotSettings>,保存着不同的缩放设置。
  • curZoom保存着zoomStack堆栈中当前的缩放设置PlotSettings的索引值。

调 用setPlotSettings()函数以后,缩放堆栈中只有一个向量项,放大和缩小按钮被隐藏。在zoomIn()和zoomOut()槽中,我们调 用QToolButtion::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()槽缩小图像。这个函数减少当前的缩放值,使能放大按钮并且实现这个按钮。对应缩小按钮,则取决于减少缩放值以后,这个图像是否还可以再次缩小(判断当前的缩放值)。然后,通过调用refreshPixmap()函数显示被更新。

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

如果用户以前对图像进行了放大,然后又缩小,则下一次的缩放值对应的PlotSettings会被保存在缩放堆栈中,我们就可以对图像进行放大。(否则,也可以通过橡皮筋线的方法来进行放大。)

zoomIn()槽增加curZoom值,根据图像是否可以再次放大来设置放大按钮是否可用,使能缩小按钮,并且显示之。再一次,我们调用refreshPixmap()来使用最新的缩放设置对图像进行更新。

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

setCurveData() 函数对一个给定的id设置曲线数据。如果一个同样id的曲线已经存在于curveMap,则用新的数据进行覆盖;否则,简单的插入新的曲线。成员变量 curveMap是QMap<int, QVector<QPointF>>类型。

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

clearCurve()函数则从curveMap中清楚指定的曲线。

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

minimumSizeHint()函数则类似于sizeHint()函数。就像sizeHint()函数指定一个组件的理想尺寸,minimumSizeHint()函数指定组件的理想最小尺寸。改变组件的尺寸绝对不会小于它的最小的理想尺寸。

这个函数的返回值是300*200(因为常量Margin的值是50),包括4个边界和plot本身。小于这个值的话,plot太小了以至于不能正常使用。

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

sizeHint()函数返回组件的理想尺寸。长宽是常数Margin的倍数,并且比例是3:2,跟minimumSizeHint()中的比例一样。

我们已经浏览了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()函数处理所有的绘画操作。但是这里,所有的绘画操作已经在之前的refreshPixmap()函数中处理了,所以我们只要简单的把像素映射表拷贝到组件上的(0,0)位置上就可以了。

如 果橡皮线可见,我们直接把它画到组件的上面。我们使用组件当前调色板中颜色组中的“轻”颜色,来确保很“黑”的背景色具有好的对比度。注意,我们直接画在 组件上面的,对于离线的像素映射表没有任何影响。使用QRect::normalized()函数确保橡皮线矩形区域的宽度和高度是正数(如果必要,变换 坐标),adjusted()函数则把矩形的大小减少一个像素来绘制矩形框一个像素的外框。

如果Plotter组件有焦点,则用组件风格 的drawPrimitive()函数来绘制一个焦点矩形,第一个参数为QStyle::PE_FrameFocusRect,第二个参数为 QStyleOptionFocusRect对象。焦点矩形的绘制参数是基于Plotter组件进行初始化的(通过调用initForm(this)函 数)。背景颜色则必须显式的指定。

当我们需要使用当前的风格进行绘画时,我们可以有两种方法。第一种直接调用QStyle的函数,比如:
style()->drawPrimitive(QStyle::PE_FrameFocusRect, &option, &painter, this);

第二种方法使用QStylePainter,而不是QPainter,更加方便的来进行绘制,我们在Plotter中就是使用这种方法。

QWidget::style() 函数返回用来绘制这个组件的风格。在Qt中,组件的风格是一个QStyle的子类。内置的风格包括QWindowStyle, QWindowXPStyle, QWindowVistaStyle, QMotifStyle, QCDEStyle, QMacStyle, QPlastiqueStyle和QCleanlooksStyle。每一种风格重新实现了QStyle类中的一些虚函数来按不同平台上的显示风格来绘制 组件。QStylePainter的drawPrimitive()函数调用QStyle类中同名的函数,用来绘制那些常用的元素,比如面板,按钮,还有 焦点框。一个程序(QApplication::style())中所有组件的风格通常是一样的,但是也可以对每个组件进行使用特定的风格,使用函数 QWidget::setStyle()。

通过继承QStyle类,我们可以定制自己的显示风格。这个可以使得我们的程序或程序集具有一 定特定的外观,在第19章中我们会进行讲解。通常我们建议使用目标平台的外观,但Qt提供很多灵活的特性,如果你想有所创新。Qt中内置的组件几乎完全单 独依赖于QStyle类来绘制它们自己,这就是为什么在Qt所支持的平台上,这些组件的风格跟平台的风格是多么一致。定制化的组件可以使用QStyle来 绘制它们,或者使用内置的Qt组件作为子组件,来使它们的风格跟平台相符。对应Plotter例子,我们同时使用了这两个方法:焦点框是使用 QStyle(通过QStylePainter),而放大按钮,缩小按钮是内置的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();
}

Plotter组件被改变大小时,Qt产生一个resize事件。这里,我么重新实现resizeEvent()函数,把放大按钮和缩小按钮放到Plotter组件的右上角。

我们把放大和缩小按钮挨个放置,两个按钮中间有5个像素的间隙,并且与父组件的上边框和右边框也有5个像素的间隔。

如 果我们只是想把按钮放到组件的左上方一定固定的坐标(坐标(0, 0)),那么我们只要简单的在Plotter的构造函数中设置。但是这里我们想把按钮放到右上角,这个坐标由组件的大小决定。正因为这个,有必要重新实现 resizeEvent()函数,在这里设置按钮的位置。

在Plotter构造函数我们没有设置任何按钮的位置。这个不是问题,因为组件在第一次显示的时候会产生一个resize事件。

如果不重写resizeEvent()手工排布子组件,还可以使用布局管理器(比如,QGridLayout)。使用布局管理器会更加的复杂,并且消耗更多的资源;另一方面,它可以更好的处理至右向左的布局,对阿拉伯语和希伯来语来说更合适。

最后,我们调用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()。

在第4章中,我们调用QApplication::setOverrideCursor(),指定参数为Qt::WaitCursor,来设置光标的形状为标准的等待光标。

void Plotter::mouseMoveEvent(QMouseEvent *event)
{
    if (rubberBandIsShown) {
        updateRubberBandRegion();
        rubberBandRect.setBottomRight(event->pos());
        updateRubberBandRegion();
    }
}

当 用户按住左键移动鼠标时,我们先调用updateRubberBandRegion()调度一个绘画事件来重新绘制橡皮线包围的区域,然后我们重新计算 rubberBandRect变量来说明鼠标移动的位置,最后我们再次调用updateRubberBandRegion()来重新绘制橡皮线的区域。这 样可以删除橡皮线,并在新的坐标点重新绘制橡皮线。

如果用户向上或向左移动鼠标,则rubberBandRect的右下角有可能在它的左 上角上面或左面。这种情况下,QRect就会出现负的宽度和高度。我们在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,我们进行一个缩放操作。如果橡皮线的包围范围小于4×4,则有可能是用户误点击了这个组件,或者只是给组件一个焦点,则我们什么事也不做。

进 行缩放操作的代码有一点点复杂。这是因为我们需要同时处理组件的坐标和plotter的坐标。我们这里做的大部分工作是把rubberBandRect从 组件坐标转化为Plotter的坐标。一旦完成了转化工作,我们调用PlotSettings::adjust()进行四舍五入,在x和y轴上找到一个合 理的坐标刻度。图5.10和图5.11演示了这个过程。

5.10 Converting the rubber band from widget to plotter coordinates

 

5.11 Adjusting plotter coordinates and zooming in on the rubber band


然后我们进行缩放操作。缩放的过程是把刚刚计算的新的PlotSettings值压入缩放堆栈的栈顶,然后调用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);
    }
}

当 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();
}

鼠 标滚轮滚动时,wheelEvent()事件发生。大多数鼠标只提供一个垂直方向的滚轮,但是还有一些鼠标还多提供了一个水平方向上的滚轮。Qt对这两种 滚轮都支持。滚轮事件只发生在那些取得焦点的组件上。delta()返回滚轮滚过的距离,1度滚过8个单位距离,所以event->delta() / 8得到滚动的角度。典型的鼠标是以15度为单位工作。这样,计算得到滚动后的刻度值,按住这个值修改缩放堆栈中最上面的那个设置值,并调用 refreshPixmap()来更新显示。

鼠标滚轮事件最常用的例子是滚动一个滚动条。当我们使用QScrollArea(第6章讲解)来提供滚动条时,QScrollArea会自动处理鼠标滚轮事件,因此我们不需要自己重新实现wheelEvent()。

我们已经完成了所有的事件处理函数。现在我们来看看私有函数。

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());
}

updateRubberBandRegion() 函数在mousePressEvent(),mouseMoveEvent()和mouseReleaseEvent()事件处理函数中被调用,来擦除或 重绘橡皮线矩形区域。这个函数调用4个update()来绘制4个小矩形,2个水平的,2个垂直的,橡皮线就 画在这些小矩形里面。

void Plotter::refreshPixmap()
{
    pixmap = QPixmap(size());
    pixmap.fill(this, 0, 0);

    QPainter painter(&pixmap);
    painter.initFrom(this);
    drawGrid(&painter);
    drawCurves(&painter);
    update();
}

refreshPixmap() 函数把plot重新绘制到离线的像素映射表上,并更新显示。我们重新设定像素映射表的大小,使其与组件具有同样的大小,并且用组件的清除颜色进行填充。这 个颜色是调色板中的黑色,因为我们在Plotter的构造函数中调用了setBackgroundRole()函数进行了设置。如果背景不是一个固定的刷 子,QPixmap::fill()需要知道组件中刷子的偏移量。这里,整个像素映射表对应整个组件,所以我们设置位置(0, 0).





然 后我们创建一个QPainter来绘制这个像素映射表。initFrom()函数调用设置绘制器的画笔,背景,以及字体,使得这些跟Plotter组件保 持一致。接下来,我们调用drawGrid()和drawCurves()来进行具体绘制。最后,我们调用update()来为整个组件调度一个绘画事 件。像素映射表会在paintEvent()绘画事件处理函数中被拷贝到组件上。





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, 20,


                              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()函数绘制曲线和坐标轴后面的网格。网格绘制的区域由rect指定。如果组件不够大来容纳整个图片,则直接返回。





第一个for循环绘制网格的垂直线以及x轴上的刻度。第二个for循环绘制网格线的水平线及y轴上的刻度。最后我们绘制整个边界框。drawText()是用来绘制两个坐标轴上对应刻度的数字。





drawText()的调用语法如下:


painter->drawText(x, y, width, height, alignment, text);





(x, y, width, height)定义一个矩形区域,alignment是矩形区域中文本的位置,text就是要绘制的文本。这我们这个例子中,我们已经手动计算出了文本绘制的矩形区域;一个更好的方法是用QFontMetrics来计算文本的边界矩形区域。





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<int, QVector<QPointF> > i(curveMap);


    while (i.hasNext()) {


        i.next();





        int id = i.key();


        QVector<QPointF> 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()函数用来在网格上绘制曲线。我们首先调用setClipRect()设置绘制曲线的巨型区域(不包括图片周围的边界和边框)。QPainter就会忽略这个区域以外的绘制操作。

 

接下来我们使用一个java风格的遍历器来遍历所有的曲线。对于每条曲线,我们遍历所以的QPointF点。我们调用遍历器的key()函数来得到曲线的ID,value()函数来得到对应的曲线数据QVector<QPointF>。内层for循环则把 每个QPointF标识从plotter坐标转换到组件标准,并把它保存到ployline变量中。

 

一旦我们把所有的曲线上的点转换成组件坐标,我们为曲线设置画笔的颜色(使用其中一个预定义的颜色集),然后调用drawPolyline()来绘制这些点所组成的曲线。

 

到这里我们完成了所有Plotter类的函数。还剩下一些PlotSettings类的一些函数。

 

PlotSettings::PlotSettings()

{

    

minX = 0.0;

  

  
maxX = 10.0;

    

numXTicks = 5;

 

    

minY = 0.0;

    

maxY = 10.0;

    

numYTicks = 5;

}

 

PlotSettings构造函数初始化x轴和y轴,范围为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()函数被mouseReleaseEvent()时间处理函数中调用,这个函数对minX,maxX,minY和maxY值进行四舍五入得到一个合适的值,从而为x轴和y轴得到一个合适的刻度数。私有函数adjustAxis()一次处理一个方向上的轴。

 

void PlotSettings::adjustAxis(double &min, double &max, int &numTicks)

{

    

const int MinTicks = 4;

    

double grossStep = (max - min) / MinTicks;

    

double step = std::pow(10.0, std::floor(std::log10(grossStep)));

 

    

if (5 * step < grossStep) {

        

step *= 5;

    

} else if (2 * step < grossStep) {

        

step *= 2;

  

  
}

 

    

numTicks = int(std::ceil(max / step) - std::floor(min / step));

    

if (numTicks < MinTicks)

        

numTicks = MinTicks;

    

min = std::floor(min / step) * step;

    

max = std::ceil(max / step) * step;

}

 

adjustAxis()函数和min和max参数(注意是引用类型)转化为一个合适的值,并根据给定的【min,max】范围计算得到合适的刻度数给numTicks参数。因为adjustAxis()函数需要修改它的形参值,所以这个参数为非常量(non-const)引用。

 

adjustAxis()函数中的大多数代码是在试图计算单位刻度值(两个刻度之间的间隔)。为了在轴上得到一个合适的数值,我们必须很小心的选择单位刻度值。比如,一个坐标的单位刻度值是3.8,则轴上刻度值都是3.8的倍数,这样用户会很不习惯。对于那些十进制的刻度值,好的单位刻度值是10

n


, 2·10
n



, or 5·10
n


 

我们首先粗略的计算一个步进值(单位刻度值),这个值应该是最大的步进值了,然后我们找到一个对应的最接近粗略步进值的10

n


形式的值,这个值应该比粗略步进值小或者相等。我们通过对这个粗略步进值求自然对数来得到一个值,并对这个值四舍五入,然后对这个四舍五入后的值求10的指数。比如,如果粗略的步进值是236,我们计算得到log236为2.37291…;然后四舍五入得到2,然后得到10
2


= 100, 
100就是我们需要的步进值(单位刻度值)。

 

一旦我们有了第一个候选步进值,我们可以使用它来计算其它的两个候选值:2×10

n


5×10
n。


对于前面的例子中,其它两个候选值是200和500.500比粗略步进值还有大,所以我们不能用这个值。但是200比236小,所以我们使用200作为这个例子中的步进值。
 

知道步进值以后,得到numTicks,min和max就很容易了。新的min值是对原先的min四舍五入到最接近于步进值的倍数,新的max值则是对原先值max四舍五入最接近于步进值的倍数。新的numTicks值是新的min和max之间的间隔的数目。例如,如果min是240,max是1184,则新的范围值是[200, 1200],中间有5个刻度值。

 

这个算法有时候不是最优的。一个更加复杂的算法在Paul S. Heckbert的文章有描述,这个文章的名字是“Nice Numbers for Graph Labels”,出版于Graphics Gems (Morgan Kaufmann, 1990).

 

这一章同时也是我们这本书的第一部分结束了。这一章中讲解了怎么定制一个已经存在的Qt组件,怎么使用QWidget作为基类全新的创建一个组件。我们已经在第二章中看到了怎么使用布局管理器来布局子组件,我们会在接下来的第6章详细讲解这个主题。

 

到目前为止,我们已经掌握了足够的知识来编写Qt的GUI程序。在第二和第三部分,我们会对Qt进行更加深入的探索,来掌握更多Qt的功能。

 

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值