创建QWidget的子类

发布: 2008-06-03 00:03

许多自定义物件只是现有物件的简单组合,不管它们是内建的Qt物件或者其他像hexSpinBox的自定义物件。可通过现有物件构建的自定义物件通过可用Qt设计师进行开发。
使用”Widget”模板创建一个新的表单。
向该表单上添加必要的物件,并排列它们。
建立信号和槽的连接。
如果还有通过信号和槽未能实现的行为需求,就要在QWidget的继承类和相关的由uic生成的类中实现一些必要的代码。
当然了,可以完全通过写代码组合现有的物件。不管是用的哪哪种方法,结果类都是直接继承自QWidget的。
如果物件没有自己的信号和槽并且没有重新实现任何虑函数,甚至可以通过简单的组装现有的物件而不需要继承。这是我们在第1章中使用的创建那个Age程序的方法,它包括一个QWidget,一个QSpinBox,和一个Qslider。虽然如此,我们还可以简单的继承QWidget并在它的构造函数中创建QSpinBox和Qslider对象。
当没有Qt物件能适用于手头上的需要时,当没有办法组合或者修改现有的物件获得需要的结果时,我们照样能创建我们所需的物件。这可以通过继承QWidget并重新实现几绘制物件和响应鼠标事件处理函数。Qt的内建物件,如Qlabel,QpushButton和QTableWidget,都是以这种方式实现的。即使它们在Qt中不存在,我们依然能以完全平台无关的方式使用QWidget提供的公有函数创建他们。
为了演示如何使用这种编写一个自定义的物件,我们将创建一个图5.2中展示的IconEditor物件。IconEditor是一个能被用在图标编辑器的程序中的物件。
图5.2 IconEditor 物件


让我们从头文件看起吧:
[code type="cpp-qt"]
#ifndef ICONEDITOR_H
#define ICONEDITOR_H
#include
#include
#include
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;
[/code]
IconEditor类使用Q_PROPERTY()宏声明了三个自定义属性:penColor, iconImage, 和 zoomFactor。每个属性都有一个数据类型,一个“读取”函数,一个可选的“写入”函数。例如,penColor属性是Qcolor类型的,并可用penColor() 和 setPenColor()函数进行读写。
当我们在Qt设计师中使用物件的时候,自定义物件的属性显示在Qt设计师的属性编辑框中从QWidget继承来的属性下面。属性可以是QVariant支持的任何类型。Q_OBJECT宏对定义了属性的类是必须的。
[code type="cpp-qt"]
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
[/code]
IconEditor重新实现了来自QWidget的3个保护函数,还有几个私有函数和成员变量。这3个私有成员变量持有这3个属性的值。
实现文件以IconEditor的构造函数开始:
[code type="cpp-qt"]
#include
#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));
}
[/code]

构造函数中有一些微妙的表现,如Qt::WA_StaticContents属性和setSizePolicy()调用。我们马上就会讨论他们。
画笔的颜色被设为黑色。缩放因子被设为8,意味着图标中的每个像素将被绘制为一个8x8的正方形。
图标数据被存储在image成员变量中,并可通过setIconImage() 和 iconImage() 函数访问他们。图标编辑器程序一般在用户打开一个图标文件的时候调用setIconImage(),并在用户想保存它的时候调用iconImage()获取图标数据。Image变量是QImage类型的。我们把它初始化为16x16像素和32位ARGB格式,这是一种支持半透明的格式。我们通过使用透明颜色填充图像来清空图像数据。
Qimage类以硬件无关的方式存储一张图像。它可被设为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是unsigned int的简单的typedef,qRgb() 和 qRgba()是一个将他们的参数组合成一个32位整数值的内联函数。它还可以写为
QRgb red = 0xFFFF0000;

它的第一个FF对应于alpha通过,第二个FF是红色成分。在IconEditor构造函数中,我们使用一个alpha通过为0的透明色来填充QImage。
Qt提供了存储颜色的两种类型:QRgb 和 Qcolor。QRgb只是一个用于QImage中存储32位象素的typedef值,而 QColor是一个有许多有用函数的类,它被广泛地在Qt中使用来存储颜色。在IconEditor物件中,我们仅在处理QImage的时候使用了QRgb。其他地方都使用了QColor,包括penColor属性。
[code type="cpp-qt"]
QSize IconEditor::sizeHint() const
{
QSize size = zoom * image.size();
if (zoom >= 3)
size += QSize(1, 1);
return size;
}
[/code]
继承自sizeHint()函数被重新实现,它会返回物件的理想尺寸。这里我们把图象的尺寸乘以缩放因子,如果缩放因子大于等于3的时候,一个额外的象素将被加入到每个方向以适应网格。(如果缩放因子为1或者2我们不显示网格,因为这个网格几乎没有留给图标象素一点空间了)
一个物件的尺寸暗示在使用布局结合的时候非常有用。Qt的布局管理器在排列一个窗格上的子物件时会试着尽可能地考虑物件的尺寸暗示。为了让IconEditor能有个很好布局,它必须报告一个可靠的尺寸暗示。
除了尺寸暗示之外,物件还有一个尺寸策略,它告诉布局系统他们是否乐意被拉伸和收缩。通过在构造函数中以QSizePolicy::Minimum为垂直和水平策略调用setSizePolicy(),我们告诉所有的布局管理器对这个物件的尺寸暗示负责,该尺寸暗示实际就是它的最小尺寸。或者说,如果需要的话这个物件可以被拉伸,但不应该收到到尺寸暗示以下。这可通过在Qt设计师中调用物件的sizePolicy属性覆盖掉。所有尺寸策略的解释在第6章中(布局管理)。
[code type="cpp-qt"]
void IconEditor::setPenColor(const QColor &newColor)
{
curColor = newColor;
}
[/code]
setPenColor()函数设置当前画笔的颜色。该颜色将用于新画的象素。
[code type="cpp-qt"]
void IconEditor::setIconImage(const QImage &newImage)
{
if (newImage != image) {
image = newImage.convertToFormat(QImage::Format_ARGB32);
update();
updateGeometry();
}
}
[/code]
setIconImage()函数设置要编辑的图象。我们调用convertToFormat()创建带alpha通道的32位图象的缓冲区,如果它还不正在的话。在代码的其他地方,我们假设此图象数据被存储为32位ARGB值。
在设置image变量以后,我们调用QWidget::update()强制使用新图象重画此物件。下一步,我们调用QWidget::updateGeometry()告诉所有包含些物件的布局管理器它的尺寸暗示已经改变了。布局管理器会自动适应新的尺寸暗示。
[code type="cpp-qt"]
void IconEditor::setZoomFactor(int newZoom)
{
if (newZoom < 1)
newZoom = 1;
if (newZoom != zoom) {
zoom = newZoom;
update();
updateGeometry();
}
}
[/code]
setZoomFactor()函数设置图象的缩放因子。为避免其他任何地方除0问题,我们将纠正任何小于1的值。我们再次调用update()和updateGeometry()以重绘该物件并告诉所有布局管理器它的尺寸暗示的改变。
penColor(), iconImage(), 和 zoomFactor()函数都在头文件中被实现为内联函数。
现在我们回顾一下paintEvent()函数的代码。这个函数是IconEditor最重要的函数。它在任何需要重绘物件的时候调用。在QWidget中的默认实现不做任何事,保护物件的空白状态。
像我们在第3章中见到的closeEvent()一样,paintEvent()也是一个事件处理函数。Qt还有许多其他的事件处理函数,他们中的每一个对应于一种不同类型的事件。第7章将深入阐述事件处理。
产生绘图事件和paintEvent()被调用的时候有下面几种情况:
当物件第一次显示的时候,系统自动生成一个绘图事件强制物件重绘它自己。
当物件被调整尺寸的时候,系统会生成一个绘图事件。
如果物件被其他窗口覆盖并且其后重新显示,系统将会为隐藏区域产生一个绘图事件(除非窗口系统存储了该区域)。

我们可以通过调用QWidget::update()或者QWidget::repaint()强制产生重绘事件。这两个函数的不同之处是repaint()强制产生一个即时重绘,而update()只简单地调度一个绘图事作为Qt的下一个处理事件。(如果该物件在屏幕上不可见,两个函数都不做任何事)。如果update()被多次调用,Qt把几个连续的绘图事件压缩到单个绘图事件以避免闪烁。在IconEditor,我们总是调用update()。
下面是程序代码:
[code type="cpp-qt"]
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));
painter.fillRect(rect, color);
}
}
}
}
[/code]
我们以在此物件上构造一个QPainter对象为开始。如果缩放因子大于等于3,我们使用Qpainter::drawLine()画出一个水平的和垂直的直线来构成一个网格。
Qpainter::drawLine()的语法如下:

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

(x1,y1)是直线的一端的位置,(x2,y2)是直线的另一端。还有一个该函数的重载版本,它有两个Qpoint类型的参数而不是4个int参数。
Qt物件的左上角上的象素的益在(0,0),右下角上的象素的位置为(width()-1,height()-1)。这与传统的笛卡尔坐标系统类似,但是与之上下巅倒。我们可以使用坐标变换来改变Qpainter的坐标系统,如平移,按比例缩放,旋转和裁剪。这在第8章中说明(2D和3D图象)。
图5.3 使用Qpainter画一条直线


在我们调用Qpainter的drawLine()之前,我们用setPen()设置直线的颜色。我们可以硬编码一种颜色,像黑色或者灰色,但一个更好的方法是使用物件的设色板。
每个物件都有一个调色板,它指定此物件将使用哪种颜色。例如,一个设色板入口用于物件的背景(通过是浅灰色),一个调色板用于背景之上的文本(通过是黑色的)。默认情况下,物件的调色板自适应当前的窗口系统的颜色方案。通过使用调色板中的颜色,我们可以保证IonEditor并重尊重用户的参数选择。
物件调色板邮三组颜色组成:活动颜色,非活动颜色,无效时的颜色。使用哪组颜色取决于物件的当前状态:
活动组的颜色用于物件是当前的活动窗口时。
非活动级颜色用于物件在其他窗口中时。
无效时的颜色用于任何窗口中的无效物件。

QWidget::palette()函数返回物件的调色板Qpainter对象。颜色组由枚举变量类QPalette::ColorGroup指定。
当我们想为绘图获取一个适当的刷子或者颜色的时候,正确的方法是使用当前的调色板,从QWidget::palette()获得,和需求角色,例如,Qpalette::foreground()。每个角色函数返回一个刷子,它一般是我们所需要的,但如果我们只需要颜色,我们可以从画刷中抽取出来,正如我们在paintEvent()中所做的一样。默认情况下,返回的画刷正是适用于物件状态的画刷,因此我们不需要特别指定一个颜色组。
paintEvent()函数以绘出它自己的图象结束。IconEditor::pixelRect()返回一个定义了重绘区域的Qrect。作为一个简单的优化,我们不重画在这区域之外的任何象素。
图5.4 使用Qpainter画一条直线


我们调用QPainter::fillRect()画一个缩放过的象素。QPainter::fillRect()有一个Qrect和一个Qbrush参数。通过传递过去一个QColor作为画刷,我们得到一个solid填充模式。
[code type="cpp-qt"]
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);
}
}
[/code]
pixelRect()函数返回一个适用于QPainter::fillRect()的Qrect。i 和 j 参数是象素在QImage 上的的坐标而不是此物件上的。如果缩放因子是1,这两个坐标系统就是完全一致的了。
Qrect的构造函数的语法为QRect(x, y, width, height),(x,y)是此矩形左上解的位置,width和height是此矩形的尺寸。如果缩放因子大于等于3,我们在矩形的水平和垂直方向都减少一个象素的大小,这样填充的时候就不会画到网格线上了。
[code type="cpp-qt"]
void IconEditor::mousePressEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton) {
setImagePixel(event->pos(), true);
} else if (event->button() == Qt::RightButton) {
setImagePixel(event->pos(), false);
}
}
[/code]
当用户按下一个鼠标键时,系统会产生一个“mouse press”事件。通过重新QWidget::mousePressEvent(),我们可以这个事件并且设置或者清除鼠标渡假光标下面的象素。
如果用户按下鼠标左键,我们用true作为第二个参数调用私有函数setImagePixel(),告诉它设置此处的象素为当前的画笔颜色。如果用户按下鼠标右键,我们还是调用setImagePixel(),但传递过去false来清除此处的象素。
[code type="cpp-qt"]
void IconEditor::mouseMoveEvent(QMouseEvent *event)
{
if (event->buttons() & Qt::LeftButton) {
setImagePixel(event->pos(), true);
} else if (event->buttons() & Qt::RightButton) {
setImagePixel(event->pos(), false);
}
}
[/code]

mouseMoveEvent()处理”mouse move”事件。默认情况下,仅当用户按下一个按键的时候才产生这些事件。可以通过调用QWidget::setMouseTracking()改变这一行为,但就现在的例子来说并不需要。
正如按下左右键能设置或者清除象素一样,保持它的按下状态并在象素上盘旋也能设置或者清除一个象素。因为很可能同时按下多个鼠标键,QMouseEvent::buttons()的返回值是这些鼠标键的位级的OR结果。我们使用&操作符检测一个特定的键是否按下了,如果是符合的情况我们就调用setImagePixel()。
[code type="cpp-qt"]
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));
}
}
[/code]

setImagePixel()函数被mousePressEvent() 和 mouseMoveEvent()调用来设置或者清除一个象素。Pos 参数是物件上鼠标的位置。
第一步是把鼠标的位置从物件坐标转换成图象坐标。这通过用鼠标的x()和y()除以缩放因子来实现的。下步,我们检测这一点是否在正确的区域中。检测过程通过使用QImage::rect() 和 QRect::contains()非常简单的就做到了。这可有效的检测i是否在0到image.width()-1之间,j是否在0到image.height()-1之间。
依赖于opaque参数,我们设置或者清除图象中的象素。清除一个象素实际上就是把它设置为透明的。为了使用QImage::setPixel()调用我们必须把画笔的QColor转换为一个32位ARGB值。最后,我们使用需要重绘的Qrect 作为参数调用update()。
既然我们已经回顾了成员函数,我们返回到我们在构造函数中使用的Qt:: WA_StaticContents属性。该属性告诉Qt物件在调整尺寸的时候物件的内容并没有改变,并且内容还停留在物件的左上角。Qt使用这一信息避免调整物件尺寸的时候重绘已经显示的不需要重绘的区域。
正常情况,当物件被调整大小的时候,Qt对物件的整个可见区域产生一个绘图事件。但如果物件使用Qt::WA_StaticContents属性创建的,绘图事件的区别被严格限制在以前没有显示过的象素。这暗示出如果物件被调整为一个更小的尺寸,并不会有任何绘图事件产生。
图5.5 缩放一个Qt::WA_StaticContents物件


IconEditor物件现在已经完成了。用了前面章节的信息和例子,我们应该能编写代码,把IconEditor作为一个有自己权利的窗口使用,作为QMainWindow中的一个中央物件,作为布局中一个子物件,或者作为QscollArea(148页)内部的一个子物件。在下一节中,我们将看一下如果把它整合到Qt设计师中。


原文: http://qtchina.tk/?q=node/173

Powered by zexport