许多Qt的控件或者象HexSpinBox这些自定义控件都是现有的控件的一个组合。由Qt控件组合而成的用户控件可以用Qt Designer实现:
1. 用模板“Widget”新建一个控件框架
2. 在框架中加入需要的控件,并对控件进行布局
3. 进行信号和槽连接
4. 如果还需要更多的信号和槽,可以在继承QWidget和uic生成的类中添加相关代码
这种控件的组合也可以用代码实现。不管那种方法,结果都是创建一个直接从QWidget继承的类。
如果控件不需要信号和槽,也不用实现任何虚函数,也可以不用继承,只要把现有的控件组合起来就可以。在第一章的Age程序 中就是这样。当然我们也可以写一个QWidget的基类,在新类的构造函数中创建QSpinBox和QSlider。
如果所需要的用户控件,找不到合适的Qt控件可用,也没有办法通过组合或者调整现有的控件,我们也可以自己创建出来。我们可以从QWidget继承一个新类,重新实现一些事件处理函数实现绘制新的控件,相应鼠标的点击,我们可以完全控制控件的外观和行为。Qt 的许多控件如QLabel,QPushButton和QTableWidget就是这样实现的。如果他们不存在,也可以使用QWidget提供的函数创建出来,并保持平台的无关性。
我们创建一个IconEditor控件来说明这个方法。IconEditor控件如下图所示,这个控件可以在图标编辑程序中使用。
Figure 5-2 the IconEditor Widget
下面是头文件:
#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使用宏Q_PROPERTY()定义了三个自定义属性:penColor,iconImage,zoomFactor。每一个属性都有一个数据类型,一个读函数和一个写函数。例如,属性penColor类型为QColor,读写函数分别为penColor()和setPenColor()。
如果在Qt Designer中使用这个控件,自定义属性就会出现在Qt Designed的属性编辑窗口中。属性的数据类型可以是QVariant支持的各种类型。要使这些属性有效,Q_OBJECT宏是必须的。
IconEditor实现了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
));
}
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);
if
(
!
event
->
region().intersect(rect).isEmpty()) {
QColor color
=
QColor::fromRgba(image.pixel(i, j));
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的调用稍后再介绍。
画笔的颜色为黑色,放大倍数为8,意思是图标中的每一个象素占用了8×8个小格子的空间。
图标数据保存在image成员变量中可以用函数iconImage()和setIconImage()读取。图标编辑程序可以调用setIconImage()打开图标文件,调用iconImage()得到图标把它保存到磁盘上。变量image的类型为QImage,初始化为透明的图片,16×16个象素,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类型,内联函数qRgb(),qRgba()只是把分量值合成一个32为整数。QRgb red = 0xffff0000。第一个ff为alpha分量值,第二个ff为红色的分量值。在IconEditor中我们设置alpha分量为0,得到一个透明的图片。
Qt提供了两种颜色有关的类:QRgb和QColor。在QImage中使用的QRgb只是一个32位的象素数据。QColor是一个有很多功能的类,在Qt中使用的很多。在这个控件中,我们只是在处理QImage的时候使用了QRgb,其他地方都是用的QColor,penColor属性也是使用的QColor类型。
函数IconEditor::sizeHint()是QWidget的虚函数,返回控件的最适当的大小。这里进行了重写,图片的大小乘以放大倍数,如果放大倍数大于3,则在四个方向上再加上一个象素,用来显示网格。如果放大倍数小于3,根本没有位置显示网格,所以也就没有必要加一个象素。
控件的sizeHint在布局中非常有用。布局管理器根据控件的sizeHint排列子控件。IconEditor控件为了能在布局时有一个好的位置,就必须要提供一个sizeHint。
除了控件的sizeHint,控件还有一个sizePolicy属性,布局管理器根据这个属性拉伸或者缩小空间尺寸。在IconEditor构造函数中,setSizePolicy()的参数位QSizePolicy::Minimum,布局管理器就会把控件的最小尺寸做为sizeHint。即控件可以被拉伸,但是不能缩小到小于它的最小尺寸值。这个值可以在Qt Designer中的sizePolicy属性里修改。sizePolicy的各种取值的含义在第六章中介绍。
函数setPenColor()设置当前画笔的颜色。新绘制的象素显示新的画笔颜色。
函数setIconImage()重新设置编辑的图片。调用convertToFormat()构成一个32位具有alpha值的图片数据。程序的其他地方都假设编辑的图片数据保存的是32位ARGB值。调用QWidget::update()强制控件显示新的图片,QWidget::updateGeometry()通知布局管理器用新的sizeHint重新调整控件的大小。
函数setZoomFactor()设置图片的放大倍数。为了避免0位除数,不允许放大倍数小于1。放大倍数改变后,也要调用update()和updateGeometry()重新显示图片,调整控件大小。
函数penColor(),iconImage(),zoomFactor()在头文件中做为内联函数实现。
现在来看paintEvent()函数。这个函数是IconEditor最重要的一个函数,在控件需要重新绘制的时候调用。在类QWidget中这个函数不作任何事情,控件是一片空白,
和第三章的closeEvent()一样,paintEvent()函数也是一个事件处理函数。Qt有很多事件处理函数,每一个函数相应一个类型的事件,第七章将会深入介绍事件处理。
Qt中很多情况下都会产生绘制事件,调用paintEvent()函数:
1. 当控件第一次显示时,Qt自动产生绘制事件使空间绘制自身。
2. 当控件尺寸发生变化时,系统产生绘制事件
3. 如果控件被其他的窗口遮住,窗口移走时,产生绘制被遮住部分的事件。
如果我们调用了QWidget::update()和QWidget::repaint()函数时,也会产生一个绘制事件。这两个函数也有所不同。repaint()立刻产生绘制事件,重新绘制控件。而调用update()后,只是提交给Qt一个产生绘制事件的计划。如果控件在屏幕上不可见,那么这两个函数都是什么都不做。如果update()被调用了多次后,Qt就把这几个连续的绘制事件合为一个事件避免闪烁。在IconEditor中,我们总是使用update()产生绘制事件。
在代码中,首先创建一个控件的QPainter对象。如果放大倍数大于等于3,调用QPainter::drawLine()函数绘制水平垂直线形成网格。
QPainter::drawLine()语法如下:
painter.drawLine(x1, y1, x2, y2);
(x1,y1)是线的一个端点,(x2,y2)是另一个端点。函数还有一个重载形式,两个QPoints做为参数。
在Qt中,控件左上角的坐标为(0,0),右下角的坐标为(width()-1,height()-1)。这和常规笛卡儿坐标很像,只是方向向下。在第八章中,我们会介绍利用坐标变换改变QPainter的坐标系统,如平移,放缩,旋转,剪切。
5-3 drawing a line with QPainter
在drawLine()之前,用setPen()设置线的颜色。我们可以用代码设置线的颜色,如黑色或者灰色,但是使用控件的调色板是一个更好的方法。
每一个控件都有一个调色板设置控件不同位置的颜色。例如,控件的背景(一般是亮灰色),文本的颜色(一般为黑色)。缺省情况下,一个控件的调色板的颜色和所使用系统的窗口颜色设置一样。使用调色板的颜色,IconEditor 的外观和用户的喜好一致。
一个控件的调色板由三个颜色组构成:激活的,未激活的和不可用的。使用那个颜色组由控件当前的状态决定:
1. 控件所在窗口未当前激活的窗口,使用激活组的颜色;
2. 控件在非当前窗口的其他窗口,使用未激活组的颜色;
3. 控件在窗口中为不可用状态时,使用不可用组的颜色;
QWidget::palette()函数返回当前控件的调色板对象。颜色组由枚举QPalette::ColorGroup决定。
在需要一个合适的刷子颜色画图时,正确的方法时使用当前QWidget::palette()返回的调色板和一个特定的角色(role),如QPalette::foreground()。每一个角色都返回一个刷子,一般我们使用这个刷子就可以了,有时也需要使用刷子的颜色,如在paintEvent()函数就是这样。通过这种方法得到的刷子与控件的状态一致,一般不需要确定颜色组。
函数paintEvent()绘制了图像。IconEditor::pixelRect()返回的QRect定义了需要重新绘制的区域。这样我们就不用重新绘制那些不在这个区域里的象素。
Figure5-4 Darwing a line with QQainter
QPainter::fillRect()绘制一个有放大倍数的象素。需要一个QRect和QBrush类型的参数。使QColor作为刷子,我们得到一个固体填充的模式。
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)设置当前象素为当前画笔的颜色。如果用户点击鼠标右键,也调用setImagePixel(,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);
}
}
鼠标移动事件由函数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是控件上鼠标的位置。
首先坐标值除以放大倍数是把控件坐标系的鼠标位置转换为图像坐标里的位置。然后我们检查当前的点是否在有效区域内,使用的函数是QImage::rect()和QRect::contains(),判断i是否在0和iamge.width()-1之间,和j是否在0和image.height()-1之间。
根据opaque参数,我们或者设置或者清除图像象素。将象素值为透明就可以清除象素。QImage::setPixel需要把画笔的QColor转换为32位的ARGB值。最后,我们调用update()重新绘制QRect区域。
成员函数我们义已经介绍完了,现在让我们来回到构造函数中的Qt::WA_StaticContents属性。这个属性的含义是当控件大小改变时,控件的内容不会跟着放缩。从左上角开始保持不变。这样当控件尺寸改变时,不需要重新绘制已经绘制的区域。
通常情况下,控件尺寸改变时,Qt会产生一个控件全部可见区域的绘制事件。如果控件的属性设置为Qt::WA_StaticContents属性,绘制事件的区域就会限制在以前没有显示的部分。如果控件变小,那么没有绘制事件产生。
Figure 5-5 Resizing a Qt::WA_StaticContents widgets
IconEditor控件已经完成了。我们可以写代码使用IconEditor作为一个独立的窗口或者时QMainWindow的一个中央控件,或者作为一个布局里的子控件,或者是QScrollArea中的子控件。在下一节中,我们把IconEditor控件集成到Qt Designer中。