Chapter5 创建自定义窗口部件
Qt本身已经包含了绝大部分,我们开发过程中所需要的窗体部件,比如说QPushButton,QLineEdit等等,我们也都接触过,用起来很方便.但是有的时候,我们由于应用上的需要,而内置的窗口部件又不能满足我们的功能,于是就需要我们自己动手,写一个自定义的窗口部件出来.
非常幸运,Qt中一种叫做元对象(meta object)的机制救了我们.目前我理解的也并不深刻,简单说下自己的理解:元对象,其实就是利用了C++里抽象,封装,继承以及多态这种面向对象的设计思路,通过继承,重新为子类实现功能,常规功能保持不变,因此我们可以得到自定义的子类.
这也就谈及了Qt自定义窗口部件的两种方法:
1.对一个已经存在的Qt窗口部件进行子类化
2.直接对QWidget进行子类化
上面两种方法可以看出:第一种比较简单,但是实现的功能有限,要围绕其继承的父类对象展开,第二种方法较为麻烦,但是*度更高.
1.自定义Qt窗口部件
hexspinbox.h
#ifndef HEXSPINBOX_H
#define HEXSPINBOX_H
#include <QSpinBox>
class QRegExpValidator;
class HexSpinBox : public QSpinBox{
Q_OBJECT
public:
explicit HexSpinBox(QWidget *parent = 0);
protected:
QValidator::State validate(QString &text, int &pos) const ;
virtual int valueFromText(const QString &text) const;
virtual QString textFromValue(int value) const;
private:
QRegExpValidator *validator;
};
#endif
代码并不复杂,在原有QSpinBox的基础上增加了一个正则表达式检验器,并且重新实现三个虚函数.validate()函数返回一个QValidator::State,验证用户输入的合法性,valueFromText()会在用户输入的时候触发,textFromValue()会在用户通过微调框调整值的时候触发.
注意:在继承QSpinxBox的时候,必须加上explicit关键字,禁止隐式转换
hexspinbox.cpp
#include <QtWidgets>
#include "hexspinbox.h"
HexSpinBox::HexSpinBox(QWidget *parent) : QSpinBox(parent){
setRange(0, 255);
validator = new QRegExpValidator(QRegExp("[0-9A-Fa-f]{1,8}"), this);
}
QValidator::State HexSpinBox::validate(QString &text, int &pos) const{
return validator->validate(text, pos);
}
QString HexSpinBox::textFromValue(int value) const{
return QString::number(value, 16).toUpper();
}
int HexSpinBox::valueFromText(const QString &text) const{
bool ok;
return text.toInt(&ok, 16);
}
先设定一个范围,再增加一个validator限制用户输入,这样用户输入的就一定是16进制的合法值.进制的转换就比较简单了.
这时如果我们需要在窗口中增加这个窗体空间,直接降头文件导入,即可使用该类,很方便.
注意:除非是想实现原本窗体控件不具备的功能,才需要子类化,如果只是想更改外观,那大可不必如此复杂,通过修改样式表即可实现,这个后面会接触到.
2.子类化QWidget
通常情况,我们通过组合不同的窗体控件,可以实现很复杂的功能,但是有的时候我们会发现,无法组织出一个这样的窗体出来,我们就需要子类化QWidget,重新实现绘制事件,以及鼠标事件等.
这一次,跟着书中所讲,从0开始,实现一个图标编辑器(Icon Editor).
iconeditor.h
#ifndef ICONEDOTOR_H
#define ICONEDOTOR_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:
explicit 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 obaque);
QRect pixelRect(int i, int j) const;
QColor curColor;
QImage image;
int zoom;
};
#endif
明显的复杂了很多.
先定义三个Q_PROPERTY宏,声明一个数据,再声明读写属性,有些像Java里的setter和getter.
核心的属性有三个:curColor用来描述画笔的颜色,image用来表示图像,zoom表示放大级别,共有函数即为这三个属性的读写函数,保护属性里重新实现QWidget的绘制事件,还有鼠标的点击和移动事件.
iconeditor.cpp
#include <QtWidgets>
#include <QRegion>
#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().intersects(rect)){
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 obaque){
int i = pos.x() / zoom;
int j = pos.y() / zoom;
if(image.rect().contains(i, j)){
if(obaque){
image.setPixel(i, j, penColor().rgba());
}else{
image.setPixel(i, j, qRgba(0, 0, 0, 0));
}
update(pixelRect(i, j));
}
}
不拘泥于具体的代码,主要看三个部分:
sizeHint()函数,可以返回一个窗口部件的理想大小.
update()重新绘制窗口事件,这时会调用paintEvent()函数.
paintEvent()函数是整个源文件的核心,负责窗口的绘制,也是最复杂的部分,是通过QPainter对象实现的.
3.在Qt设计师中集成自定义窗口部件
方法主要分为两种:改进法和插件法,其中,改进法简单直接,更容易利用,也便于分发.
比如HexSpinBox,现在设计师窗口中拖入一个QSpinBox,右击,选择'Promote to Custom Widget',将HexSpinBox的类名填入,即可.
插件法过于复杂,而且Qt4和5之间的方式不兼容,按下不表.
总结:这一节我们自定义了自己的窗体控件,*度还是挺高的,可以根据自己的需求,选择性的实现原本并不具备的功能,而且Qt的元对象系统为我们提供了很大的便利,我们只需重写部分函数,其余的保持不动,即可,完全没有必要将所有的功能都全部实现,没有这个必要.