Qt4_IconEditor窗口部件

子类化QWidget

许多自定义窗口部件都是对现有窗口部件的简单组合,不论它们是内置的Qt窗口部件,还是其他一些像HexSpinBox这样的自定义窗口部件。

通过对现有窗口部件的组合构建而成的自定义窗口部件通常都可以在Qt设计师中开发出来:
● 使用"Widget" 模板创建一个新窗体。
● 把一些必需的窗口部件添加到这个窗体上,并且对它们进行摆放。
● 设置一些信号和槽的连接。
● 如果通过信号和槽不能获得所需的行为,则只需在类中添加一些必要的代码即可——这个类需要同时从QWidget类和uic生成的类中派生出来。

当然,要对这些现有窗口部件进行组合,也完全可以通过手写代码方式来加以实现。但无论使用的是哪种方式,最终生成的类都会是QWidget类的一个子类。

如果窗口部件本身没有任何信号和槽,并且它也没有重新实现任何虛函数,那么我们甚至还是有可能通过对现有窗口部件的组合而不是通过子类化的方式来生成这样的窗口部件。这就是在第1章创建Age应用程序时所使用的方法,其中用到了一个QWidget、一个QSpinBox以及一个
QSlider。虽然如此,也还是可以很容易地通过子类化QWidget,并且在它的子类构造函数中创建QSpinBox和QSlider的方式来做到这一点。

当手里没有任何一个Qt窗口部件能够满足任务要求,并且也没有办法通过组合现有窗口部件来满足所需的期望结果时,仍旧可以创建出我们想要的窗口部件来。要实现这一点,只需通过子类化QWidget,并且通过重新实现一些用来绘制窗口部件和响应鼠标点击的事件处理器即可。这一方法给了我们定义并且控制自己的窗口部件的外观和行为的完全自由。Qt 的一些内置窗口部件,像QLabel、QPushButton和QTableWidget,都是通过这种方法得以重新实现的。如果它们没有在Qt中存在,那么还是完全有可能以与平台无关的方式使用QWidget所提供的公有函数来创建它们。

为了说明如何使用这种方法编写一个自定义窗口部件,我们将会创建一个IconEditor 窗口部件。这个IconEditor本来是一个用于图标编辑器程序中的窗口部件。实际上,在我们开始潜心研究和创建一个自定义窗口部件之前,还是很有必要先去检查一下是否已经有了可用的相关窗口部件,无论是在Qt Solution ( htp://www. tolltech. com/products/qt/ addon/solutions/ catalog/4/)中还是在商业或者非商业第三方( ht:/www. toltech. comn/ products/qt/3rdparty/)那里都
行,因为这样将很有可能会节省许多时间和精力。在本例中,假设没有可用的合适窗口部件,因而需要创建我们自己的窗口部件。

IconEditor.h

#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;
    
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 // ICONEDITOR_H

类IconEditor使用Q_PROPERTY()宏声明了三个自定义属性: penColor、iconImage和zoomFactor。每一个属性都有一个数据类型、一个"读"函数和一个作为可选项的"写"函数。例如,penColor属性的类型是QColor,并且可以使用penColor()和setPenColor()函数对它进行读写。

当我们在Qt设计师中使用这个窗口部件时,在Qt设计师属性编辑器里,那些继承于QWidget的属性下面,将会显示这些自定义属性。这些属性可以是由QVariant所支持的任何类型。对于定义属性的类,Q_OBJECT宏是必需的。

IconEditor.cpp

#include <QtWidgets>

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

QSize IconEditor::sizeHint() const
{
    QSize size = zoom * image.size();
    if (zoom >= 3)
        size += QSize(1, 1);
    return size;
}

void IconEditor::setPenColor(const QColor &newColor)
{
    curColor = newColor;
}

void IconEditor::setIconImage(const QImage &newImage)
{
    if (newImage != image) {
        image = newImage.convertToFormat(QImage::Format_ARGB32);
        update();
        updateGeometry();
    }
}

void IconEditor::setZoomFactor(int newZoom)
{
    if (newZoom < 1)
        newZoom = 1;

    if (newZoom != zoom) {
        zoom = newZoom;
        update();
        updateGeometry();
    }
}

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);
            /* error: C2039: “intersect”: 不是“QRegion”的成员
             * QRegion QRegion::intersect(const QRect &rect) const 此函数是在Qt4.4中引入的,在Qt的高版本中已经放弃。
             * 解决办法:使用QRegion QRegion::intersected(const QRegion &r) const代替。
             * event->region().intersect(rect).isEmpty()改成event->region().intersected(rect).isEmpty()
             */
            if (!event->region().intersected(rect).isEmpty()) {
                QColor color = QColor::fromRgba(image.pixel(i, j));
                if (color.alpha() < 255)
                    painter.fillRect(rect, Qt::white);
                painter.fillRect(rect, color);
            }
        }
    }
}

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

void IconEditor::mousePressEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton) {
        setImagePixel(event->pos(), true);
    } else if (event->button() == Qt::RightButton) {
        setImagePixel(event->pos(), false);
    }
}

void IconEditor::mouseMoveEvent(QMouseEvent *event)
{
    if (event->buttons() & Qt::LeftButton) {
        setImagePixel(event->pos(), true);
    } else if (event->buttons() & Qt::RightButton) {
        setImagePixel(event->pos(), false);
    }
}

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

构造函数
构造函数有一些巧妙的地方,比如这里的Qt::WA_StaticContents属性和setSizePolicy()调用。我们将简单地讨论一下它们。

画笔的颜色被设置为黑色。缩放因子(zoom factor)被设置为8,也就是说,图标中的每一个像素都将会显示成一个8x8的正方形。

图标数据会保存在image成员变量中,并且可以通过setIconImage()和iconImage()函数对它们进行访问。当用户打开一个图标文件时,图标编辑器程序通常会调用setIconlmage()函数;当用户想保存这个图标时,它就会调用iconImage()来重新得到这个图标。Image变量的类型是QImage。我们把它初始化为16 x 16的像素大小和32位的ARGB颜色格式,这种颜色格式可以支持半透明效果。通过填充一种透明的颜色,就可以清空image中的数据。

Qlmage类使用一种与硬件无关的方式来存储图像。可以把它设置成使用1位、8位或者32位色深。一个具有32位色深的图像分别对每一个像素各使用8位来存储它的红、绿、蓝分量。剩余的8位存储这个像素的alpha分量(即不透明度)。例如,一个纯红颜色的红、绿、蓝和alpha分量的值分别是255、0、0和255。

在Qt中,这种颜色可以通过如下形式给定:

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

或者,由于该颜色是不透明的,所以可以表示为:

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

QRgb只是unsignied int类型的一个 typede(类型别名),并且qRgb()和qRgba()都是用来把它们的参数组合成一个32位ARGB整数值的内联函数。

也可能写成这样的形式:

QRgb red = 0xFFFF0000;

这里的第1个FF对应于alpha分量,第2个FF对应于红色分量。在IconEditor的构造函数中,我们通过使用0作为alpha分量而构成的透明色来填充这个QImage。

Qt提供了两种存储颜色的类型:QRgb和QColor。虽然QRgb仅仅是一个用在QImage中存储32位像素数据的类型别名,但QColor则是一个具有许多有用函数并且在Qt中广泛用于存储颜色的类。在QIconEditor窗口部件中,只有在处理QImage时,我们才使用QRgb,而对于其他任意东西,包括这里的penColor属性,我们都只使用QColor。

sizeHint
sizeHint()函数是从QWidget中重新实现的,并且可以返回一个窗口部件的理想大小。在这里,我们用缩放因子乘以图像的尺寸大小作为这个窗口部件的理想大小,但如果缩放因子是3或者更大,那么在每一个方向上需要再额外增加一个像素,以便可以容纳一个网格线。(如果缩放因子是2或者1,就不必再显示网格线,因为这些网格线将几乎不能再给图标的像素留下任何空间。)

在和布局联合使用时,窗口部件的大小提示非常有用。当Qt的布局管理器摆放一个窗体的子窗口部件时,它会尽可能多地考虑这些窗口部件的大小提示。为了能够让IconEditor成为一个具有良好布局的窗口部件,它必须报告一个可靠的大小提示。

除了大小提示,窗口部件还有一个大小策略,它会告诉布局系统是否可以对这个窗口部件进行拉长或者缩短。通过在构造函数中调用以QSizePolicy::Minimum为水平和垂直大小策略的setSizePolicy(),会告诉负责管理这个窗口部件的任意布局管理器,这个窗口部件的大小提示就是它的最小尺寸大小。换句话说,如果需要的话,可以拉长这个窗口部件,但是决不允许把它缩短到比它的大小提示还要小的尺寸。在Qt设计师中,通过设置这个窗口部件的sizePolicy属性也可以实现这一特性。

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

setIconImage
函数setIconImage()会设置需要编辑的图像。如果这个图像还不是我们正在编辑的图像,则会调用convertToFormat()把它变成一个带alpha缓冲的32位图像。在其他代码中,将假设图像数据是存储在32位的ARGB值中的。

在设置完image变量后,调用QWidget::update(),它会使用新的图像强制重绘这个窗口部件。接下来,调用QWidget::updateGeometry(),告诉包含这个窗口部件的任意布局,这个窗口部件的大小提示已经发生改变了。于是,该布局将会自动适应这个新的大小提示。

setZoomFactor
setZoomFactor()函数设置图像的缩放因子。为了避免在其他地方被0除,应纠正任何小于1的值。之后,会再次调用update()和updateGeometry()来重新绘制该窗口部件,以便可以把大小提示的变化通知给其他任何一个负责管理它的布局。

在头文件中,我们把penColor()、iconImage()和zoomFactor()函数都实现成了内联函数。任何在类内部定义的函数自动成为内联函数。

paintEvent
现在查看paintEvent()函数的代码。这个函数是IconEditor最为重要的函数。只要需要重新绘制窗口部件,就会调用它。它在QWidget中的默认实现什么都不做,这样就留下了一个空白的窗口部件。

就像在之前中碰到的closeEvent( )函数一样,paintEvent()也是一个事件处理器。Qt还有很多其他的事件处理器,每一个都对应一种不同类型的事件。

当产生一个绘制事件并且调用paintEvent() 函数的时候,会出现如下几种情况:
● 在窗口部件第一次显示时,系统会自动产生一个绘制事件,从而强制绘制这个窗口部件本身。
● 当重新调整窗口部件大小的时候,系统也会产生一个绘制事件。
● 当窗口部件被其他窗口部件遮挡,然后又再次显示出来的时候,就会对那些隐藏的区域产生一个绘制事件(除非这个窗口系统存储了整个区域)。

也可以通过调用QWidget::update()或者QWidget::repaint()来强制产生一个绘制事件。这两个函数之间的区别是:repaint()函数会强制产生一个即时的重绘事件,而update()函数则只是在Qt下一次处理事件时才简单地调用一个绘制事件。(如果窗口部件在屏幕上是不可见的,那么这两个函数会什么也不做。)如果多次调用update(),Qt就会把连续多次的绘制事件压缩成一个单一的绘制事件,这样就可以避免闪烁现象。在IconEditor中,我们总是使用update()函数。

首先,在窗口部件上构建了一个QPainter对象。如果缩放因子是3或者比3还要大,就使用QPainter::drawLine()函数绘制构成网格的水平线段和垂直线段。

对于QPainter::drawLine()的调用遵循这样的语法:

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

Qt窗口部件的左上角处的位置坐标是(0,0),右下角的位置坐标是(width()-1,height()-1)。
在这里插入图片描述
在对QPainter调用drawLine()之前,使用了setPen()设置线段的颜色。本来也可以直接通过代码来指定像黑色或者灰色这样的颜色,但是使用窗口部件的调色板(palette)会更好些。

每一个窗口部件都会配备一个调色板,由它来确定做什么事应该使用什么颜色。例如,对于窗口部件的背景色会有一个对应的调色板条目(通常是亮灰色) ,并且对于文本的背景色也会对应一个调色板条目(通常是黑色)。默认情况下,一个窗口部件的调色板会采用窗口系统的颜色主题。通过使用调色板中的这些颜色,可以确保IconEditor能够尊重用户的选择。

一个窗口部件的调色板由三个颜色组构成:激活组(Active)、非激活组(Inactive)和不可用组(Disabled)。

应该使用哪一个颜色组取决于该窗口部件的当前状态:
● Active颜色组可用于当前激活窗口中的那些窗口部件。
● Inactive颜色组可用于其他窗口中的那些窗口部件。
● Disabled颜色组可用于任意窗口中的那些不可用窗口部件。

QWidget::palette()函数可以返回窗口部件的调色板,它是一个QPalette型对象。颜色组给定为QPalette::ColorGroup型枚举变量值。

如果我们希望获得一个用于绘制的适当的画笔或者颜色,正确的方法就是使用当前调色板。它可以通过QWidget::palette()而获得,并且也可以使用所需的角色,例如,QPalette::foreground()。每个角色函数都可以返回一个画笔,它通常就正是我们所想要的东西,但如果我们只需要颜色的话,则可以将其从画笔中提取出来,就像paintEvent()中所做的那样。默认情况下,返回的那些画笔都是能够适用于窗口部件的状态的,因而就没有必要再去给定颜色组。

以图像自身的绘制作为paintEvent()函数的结尾。对lconEditor::pixelRect()的调用会返回一个QRect,其中定义了需要重新绘制的区域。作为简单的优化处理方法,我们没有对落在这个区域之外的像素进行重新绘制。
在这里插入图片描述
我们调用QPainter::fillRect()来绘制一个缩放后的像素QPainter::fillRect()带一个QRect和一个QBrush。通过传递一个用作画笔的QColor,我们获得了一个实心填充图案。如果该颜色并非完全不透明(它的alpha通道小于255),就会先绘制出一个白色的背景来。

pixelRect
pixelRect()函数返回一个适用于QPainter::fillRect()的QRect。 这里的参数i和j是QImage的像素坐标,而不是窗口部件中的坐标。如果缩放因子是1,那么这两个坐标系就可以恰好一致了。

QRect构造函数具有QRect(x ,y, width , height)的语法形式,这里的(x ,y)是这个矩形左上角的位置坐标,而width和height就是矩形的尺寸大小。如果缩放因子是3或者更大,则可以在矩形的水平和竖直方向大小上都减去一个像素,以便在填充时不会覆盖那些网格线。

mousePressEvent
当用户按下鼠标按钮时,系统就会产生一个"鼠标按下"事件。通过重新实现QWidget::mousePressEvent(),就可以响应这一事件,并且可以对鼠标光标下的图像像素进行设置或者清空。

如果用户按下了鼠标左键,则使用true作为调用私有函数setlmagePixel()的第二个参数,告诉它要把这个像素设置成当前画笔的颜色。如果用户按下了鼠标右键,也会调用setImagePixel(),但这一次是通过传递false来清空这个像素。

mouseMoveEvent
mouseMoveEvent()处理"鼠标移动"事件。默认情况下,只有当用户按住一个键不放的时候,才会产生这些事件。通过调用QWidget::setMouseTracking()则有可能改变这一行为,但是在这个例子中不需要这样做。

就像按下鼠标左键或者右键可以设置或者清空一个像素一样,把按键按下不放并且悬停在另一个像素上也足可以设置或者清空一个像素。由于有可能会同时按下多个键,所以最终结果实际是QMouseEvent::buttons()的返回值与鼠标的按键按照按位"或"(OR)运算之后的结果。可以使用"&"操作符来测试某个特定键是否按下了,并且如果是这样的话,就调用setImagePixel()。

setImagePixel
setImagePixel()函数是从mousePressEvent()和mouseMoveEvent()中得到调用的,用来设置或者清空一个像素。pos 参数是鼠标在窗口部件中的位置。

第一步是把鼠标的位置从窗口部件的坐标转换到图像的坐标。这可以通过使用鼠标的x()和y()分量除以缩放因子完成。

接下来,检查该点是否位于正确的范围之内。使用QImage::rect()和QRect::contains()可以很容易地完成这一检查过程。这样就可以高效地检查出i是不是在0和image.width()-1之间,j是否位于0和image.height()-1之间。

根据opaque参数,我们可以设置或者清空图像中的像素。清空一个像素,实际就是把它设置成透明。我们必须把画笔的QColor转换为一个用于调用Qlmage::setPixe()的32位ARGB值。

最后,对需要重新绘制的区域调用带QRect的update()。

现在已经查看了各个成员函数,下面将回到构造函数中使用Qt::WA_StaticContents属性上。这个属性告诉Qt,当重新改变窗口部件的大小时,这个窗口部件的内容并没有发生变化,而且它的内容仍旧保留从窗口部件左上角开始的特性。当重新定义窗口部件的大小时,通过使用这个信息,Qt就可以避免对已经显示区域的重新绘制。
在这里插入图片描述
通常情况下,当重新定义一个窗口部件的大小时,Qt会为窗口部件的整个可见区域生成一个绘制事件。但是如果该窗口部件在创建时使用了Qt::WA_StaticContents属性,那么绘制事件的区域就会被严格限定在之前没有被显示的像素部分上。这也就意味着,如果重新把窗口部件改变为比原来还要小的尺寸,那么就根本不会产生任何绘制事件。

main.cpp

#include "iconeditor.h"

#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    IconEditor w;
    w.show();
    return a.exec();
}

在这里插入图片描述
IconEditor窗口部件现在就完成了。我们编写代码,把lconEditor作为一个独立的窗口,或者作为QMainWindow中的一个中央窗口部件、布局中的一个子窗口部件以及QScrollArea中的一个子窗口部件。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阳光开朗男孩

你的鼓励是我最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值