《QT Creator快速入门》第七章:Qt对象模型与容器类
1、属性
QObject的setProperty()/property()方法用来设置和获得属性(动态属性)值,如下所示:
class Foo: public QObject { public: explicit Foo(QObject* parent = nullptr): QObject(parent){} }; MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui->setupUi(this); Foo* foo = new Foo(this); foo->setProperty("userAge", 20); //添加userAge属性并设置其值 int age = foo->property("userAge").toInt(); //获得属性值,20 }
还可以给一个从QObject派生的类添加默认属性(静态属性),通过添加Q_PROPERTY()宏,宏里面前两项是必须有的,分别用来指定属性名称,READ指定读取属性值方法,其它可选的有WRITE用来指定设置属性值方法、NOTIFY用来指定属性值改变时的通知信号、RESET用来指定恢复属性值方法、CONSTANT用来指定属性值为常量(常量属性不能有WRITE和NOTIFY)、FINAL表明属性不能被派生类重写等。需要注意的是READ、WRITE、RESET指定的方法在多继承时,他们必须继承自第一个父类。以下的MyClass类包含了默认的属性userName:
class MyClass: public QObject { Q_OBJECT Q_PROPERTY(QString userName READ getUserName WRITE setUserName NOTIFY userNameChanged); //注册默认属性userName public: explicit MyClass(QObject* parent = nullptr): QObject(parent){} QString getUserName()const{ return m_userName; } //实现READ读函数 void setUserName(QString userName) //实现WRITE写函数 { m_userName = userName; emit userNameChanged(userName); //当属性值改变时发射该信号 } signals: void userNameChanged(QString); //声明NOTIFY通知消息 private: QString m_userName; //存放userName属性的值 }; MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui->setupUi(this); MyClass* my = new MyClass(this); connect(my, SIGNAL(userNameChanged(QString)), SLOT(userChanged(QString))); my->setUserName("leon"); //设置属性的值,会调用userChanged()方法 QString userName = my->getUserName(); //获得属性的值,"leon" my->setProperty("userName", "json"); //通过Object::setProperty()设置属性值,会调用userChanged()方法 QVariant var = my->property("userName"); //通过Object::property()获得属性值 userName = var.toString(); //"json" userName = my->getUserName(); //"json" }
2、元对象系统
元对象系统提供了对象间通信的信号和槽机制,以及下面代码里的三个特性,元对象系统是基于三个条件的:
1、该类必须继承自QObject类。
2、该类的私有声明区域必须声明Q_OBJECT宏。
3、元对象编译器,它为QObject的子类实现元对象特性提供必要的代码。
//1、运行时类型信息 const QMetaObject* metaObj = ui->pushButton->metaObject(); //获得一个类的元对象 const char* className = metaObj->className(); //运行时获得类名, 不需要C++中RTTI支持(比如typeid需要类中包含虚函数?),"QPushButton" QObject* obj = metaObj->newInstance(); //构造该类的一个新实例 bool b = ui->pushButton->inherits("QPushButton"); //判断对象是否是Object继承树上一个类的实例,true b = ui->pushButton->inherits("QWidget"); //true //2、tr()、trUtf8()进行字符串翻译来实现国际化 QString str = trUtf8("test测试"); ui->pushButton->setText(str); QTextCodec::setCodecForTr(QTextCodec::codecForName("UTF-8")); str = QObject::tr("test测试"); ui->pushButton->setText(str); //3、动态属性系统 setProperty("name", "leon"); QVariant var = property("name"); QString name = var.toString();
除了上面的特性,还有可以使用qobject_cast()方法来对QObject类进行动态类型转换,与dynamic_cast()不同的是它不再需要RTTI的支持,如下所示。
QObject* _obj = new MyWidget; QWidget* widget = qobject_cast<QWidget*>(obj);
RTTI(Run-Time Type Identification,运行时类型识别),通过运行时类型信息程序能够使用基类的指针或引用来检查该指针或引用所指的对象的实际派生类型。RTTI提供了以下两个非常有用的操作符:
(1)typeid操作符,返回指针和引用所指的实际类型。
(2)dynamic_cast操作符,将基类类型的指针或引用安全地转换为派生类型的指针或引用。
3、容器
Qt中容器有QVector、QList、QLinkedList、QStack、QQueue、QMap、QMultiMap、QSet、QHash(对应STL中的unordered_map)、QMultiHash。
QList是最常用的容器,它与STL中list不同的是可以使用索引,QStringList就继承自QList。
当对一个很大的列表(超过1000个元素)进行中间插入操作时QLinkList性能比QList更好,QLinkList的另一个特点是对容器进行插入或删除操作后只要原迭代器指向的项还存在则这个迭代器就不会失效,缺点是只能使用迭代器来访问项。
容器中存储的元素类型可以值任何内置类型,或者是包含默认构造函数、拷贝构造函数、赋值操作元素符的类对象,QObject和其子类对象不能使用容器存储,只能存储其指针类型。
下面为对容器的操作:
QList<int> l; l.append(2); //表尾插入 l.prepend(0); //表头插入 l.insert(1, 1); //表内指定位置插入 l << 3 << 4; //表尾插入 l.replace(0, -1); //替换指定位置的元素 l[0] = -1; //数组下标操作 int n = l.at(0); //对于只读操作的话at()方法会比使用下标[]来获取元素值要快。 l.removeAt(0); //删除指定位置元素 int num = l.takeAt(0); //删除并获得指定位置元素 l.swap(0, 1); //交换0和1处元素 l.move(1, 3); //移动1处元素到3处 l.indexOf(0); //获取元素值的索引 l.count(0); //获取元素出现的次数 l.contains(1); //是否包含某值
Java风格迭代器:
QList<int> l; l << 1 << 2 << 3; /*********只读迭代器***********/ QListIterator<int> itList(l); //指向首位元素之前 //正向遍历容器 while(itList.hasNext()) //是否包含下一个元素 { int n = itList.next(); //next()获得下一元素的引用 } //反向遍历容器 while(itList.hasPrevious()) //是否包含上一个元素 { int n = itList.previous(); //previous()获得上一元素的引用 } itList.toFront(); //移动迭代器到首位元素之前 int value = itList.peekNext(); //获得下一个元素但不移动迭代器 bool b = itList.findNext(-1); //从当前往后查找某个元素 b = itList.findPrevious(-1); //从当前往前查找某个元素 QMap<int, int> m; m[0] = 2; m[1] = 1; m[2] = 0; int v = m.value(3, -1); //获取指定key的值,如果key不存在则返回指定值 QMapIterator<int, int> itMap(m); while(itMap.hasNext()) { itMap.next(); qDebug() << itMap.key() << ":" << itMap.value(); } /*********读写迭代器***********/ QMutableListIterator<int> itWriteList(l);//指向首位元素之前 itWriteList.insert(10); //在当前位置插入元素,然后迭代器向后移动一位 itWriteList.previous(); itWriteList.setValue(-1); //设置当前位置元素值 itWriteList.toBack(); //转到列表最后一个元素之后 while(itWriteList.hasPrevious()) { int n = itWriteList.previous(); if(n == -1) itWriteList.remove(); //删除值为-1的元素 }
STL风格的迭代器也分为只读和读写两种类型,而且只读迭代器性能比读写迭代器要快。
QList<QString> l; l << "A" << "B" << "C"; //只读迭代器 QList<QString>::const_iterator iter; for(iter = l.constBegin(); iter != l.constEnd(); ++iter) { qDebug() << *iter; } //读写迭代器 QList<QString>::iterator it; for(it = l.begin(); it != l.end(); ++it) { *it = ""; } QMap<int, int> m; m.insert(1, 1); m.insert(2, 2); QMap<int, int>::iterator itMap; for(itMap = m.begin(); itMap != m.end(); ++itMap) { int key = itMap.key(); int value = itMap.value(); }
也可以使用foreach和for来遍历容器:
QList<QString> l; l << "A" << "B" << "C"; foreach(QString str, l) { qDebug() << str; } for(auto item: l) { if(item == "A") break; qDebug() << item; }
4、隐式共享
“隐式共享“又称写时复制,所有的容器类、QString、QVariant、QByteArray、QFont、QCursor、QPixmap都是隐式共享类,只有向其写入时才会复制数据。比如下面的示例,当隐式共享类对象使用”=“操作符时使用的是浅拷贝,当对象被修改时就需要对数据进行深拷贝。深拷贝是复制一个对象,浅拷贝只是复制一个引用。
QPixmap p1, p2; p1.load("image.bmp"); p2 = p1; //p1与p2共享数据 QPainter paint; paint.begin(&p2); //p2被修改,p1与p2不再共享数据 paint.drawText(0, 50, "Hi"); paint.end();
5、QString、QByteArray
QString存储了一串QChar,QChar提供了一个16位的Unicode字符。QByteArray拥有和QString类似的接口方法(除了arg()),QByteArray可以用来存储原始字节,它比char*更加方便,而且在幕后,它始终确保数据后面带有“\0”终止符:
QString str("hi,,my,qt"); str = str.simplified(); //去除字符串内的空白字符 QStringList listStr = str.split(",", QString::SkipEmptyParts); //以","分割字符串,跳过空值 str = listStr.join(","); //str为"hi,my,qt" str = "100"; int n = str.toInt(); //字符串转整型 QString s16 = QString::number(255, 16) //整形转字符串,以16进制显示 str = QString::number(3.1416, 'f'/*不使用科学计数法*/, 3/*小数位数*/); //浮点转字符串,str为"3.142" int age = 25; QString name = "leon"; str = QString("my name is %1, age is %2.").arg(name).arg(age); //格式化参数,name替换%1,age替换%2 str = QString("my name is %2, age is %1.").arg(age).arg(name); //age替换%1, name替换%2 str = "%1, %2"; QString s = str.arg("hello", "world"); // "hello world" s = str.arg("hello").arg("world"); // "hello world" s = str.arg("%1hello", "world"); // "%1hello, world" s = str.arg("%1hello").arg("world"); // "worldhello, %2" s = QString("/%1").arg("/", 3, '*');// %1替换的字符串字段宽度为3, 不足3位前面补*(),"/**/" s = QString("/%1").arg("/", -3, '*');// %1替换的字符串字段宽度为3, 不足3位后面补*(),"//**" double value = 123.456; s = QString("%1").arg(value, 0/*不设置字段宽度*/, 'f'/*不使用科学计数法*/, 2/*小数位数*/); //数值转字符串,"123.46" QByteArray ary = s.toUtf8(); //转换为UTF8格式的字符串 QByteArray ar = "abc"; int l = ar.length(); //3
6、QVariant
//使用to...()方法转换 QVariant v1(15), v2("nihao"); int n1 = v1.toInt(); QString s2 = v2.toString(); //使用value()转换 QColor clr = QColor(Qt::red); QVariant v4 = clr; QVariant::Type type = v4.type(); qDebug() << type; // QVariant::QColor QColor c = v4.value<QColor>(); qDebug() << c; // QColor(ARGB 1, 1, 0, 0) //使用convert QString s3 = "hello"; QVariant v3 = s3; if(v3.canConvert(QVariant::Int)) //字符串类型的数字可以转换为int { bool b = v3.convert(QVariant::Int); //false,s3虽然是字符串类型但不是数字字符串 }
7、正则表达式
正则表达式可以用在以下几个方面:
1、验证,测试一个字符串是否符合一些条件,如是一个整数、不包含任何空格等。
2、搜索,子字符串匹配,如匹配单词mail而不匹配email。
3、查找和替换,如使用Mail来替换所有的M字符,但如果M字符后面有ail时则不进行替换。
4、字符串分割,如分割制表符隔离的字符串。
QRegExp类可以实现正则表达式的功能,它支持unicode,可以使用setPatternSyntax()函数来更改QRegExp的语法规则。
正则表达式由表达式、量词、断言组成。
表达式:如x、5、[ABC],5[A-C]等,其中[ABC]表示匹配一个A或者B或者C,其与[A-C]意义相同,5[1-9]表示匹配5加上一个1到9之间的数([1-9]可以使用\d来代替)。表达式中一些常用字符集的缩写有:
量词:要匹配的表达式出现的次数,如x{1,1}表示匹配包含一个x(可以省去{1,1}直接用x来表示),x{0,1}表示匹配不存在x或一个x(可以使用x?来替换),x{0,3}表示匹配不存在x或者一个x或者两个x或者三个x(等价于x{,3}),x{2,2}表示匹配两个x(也可以使用x{2}),x{1,3}表示匹配一个x或两个x或三个x,x{1,}表示匹配一个x或两个x或三个x...(也可以使用x+),[0-9]表示匹配0到9之间的数,[0-9][0-9]{0,1}或[0-9]{1,2}表示匹配包含0到99之间的数,^[0-9]{1,2}表示匹配开头是0到99之间,^[0-9]{1,2}$表示匹配整个字符串是0到99之间的数。^在方括号的开始表示相反的意思,如[^abc]表示匹配除'a'、'b'、'c'之外的任意字符。+表示匹配一次或多次,如0+表示匹配0或00或000...。
量词一般是贪婪的,如0+会连续匹配0、00、 000...,而对于"20005"进行0+匹配时它匹配的是000,setMinimal(true)可以关闭贪婪模式:
QRegExp rx("(0+)"); //使用括号才可以捕获被匹配的文本 int idx = rx.indexIn("20005"); //1 QString str = rx.cap(1); //捕获匹配的文本,"000" rx.setMinimal(true); idx = rx.indexIn("20005"); //1 str = rx.cap(1); //"0"
断言:断言表示为一些布尔表达式,如\bmail\b表示匹配单词mail,\b(mail|book)\b表示匹配单词mail或book,其中\b是单词边界断言。m(?!ail)表示匹配字符m,但其后不能跟ail,这里使用了(?!E)断言。^0表示匹配开头是数字0的,[a-b]0$表示匹配结尾是0并且开头是a或b,^0[a-c]0$"表示匹配整个文本以0为首尾,中间一个字符为a-c的。
一些表达式和量词可以进行简写,如x{1, 1}表示匹配包含一个x,可以简写成x,{0,1}可以使用?来替换,[0-9]可以使用\d来替代。需要注意的是C++字符串中\是转义字符,所以应该使用\\d,而如果想要使用字符'\'本身的话应该使用\\\\。
QRegExp rx("x"); int idx = rx.indexIn("axe"); //1 rx.setPattern("[ABC]"); idx = rx.indexIn("TXBC"); //2 rx.setPattern("5[ABC]"); idx = rx.indexIn("1345CE"); //3 rx.setPattern("[0-9]"); idx = rx.indexIn("ab197cd"); //2 rx.setPattern("[0-9][0-9]{0,1}"/*"[0-9]{1, 2}"*/); idx = rx.indexIn("ab8cd"); //2 idx = rx.indexIn("ab89cd"); //2 idx = rx.indexIn("ab892cd"); //2 rx.setPattern("^[0-9]{1,2}"); idx = rx.indexIn("89ab"); //0 idx = rx.indexIn("ab89"); //-1 rx.setPattern("^[0-9]{1,2}$"); idx = rx.indexIn("89"); //0 idx = rx.indexIn("a89"); //-1 idx = rx.indexIn("89a"); //-1 rx.setPattern("x{2,2}"); idx = rx.indexIn("axf"); //-1 idx = rx.indexIn("axxf"); //1 rx.setPattern("x{1,3}"); idx = rx.indexIn("abxxf"); //2 idx = rx.indexIn("abxxxxf"); //2 rx.setPattern("x{1,3}(?!x)"); idx = rx.indexIn("abxxxxf"); //3 rx.setPattern("(?!x)x{1,3}(?!x)"); idx = rx.indexIn("abxxxxf"); //-1 rx.setPattern("\\bmail\\b"); idx = rx.indexIn("AmailB"); //-1 idx = rx.indexIn("a mail"); //2 rx.setPattern("\\b(mail|book)\\b"); idx = rx.indexIn("a book"); //2 rx.setPattern("m(?!ail)"); QString str = "this is my mail"; str.replace(rx, "the"); //"this is they mail" rx.setPattern("\\bei?ri[ck]\\b"/*"\\b(eric|eirik)\\b"*/); //匹配单词单词eric或eirik idx = rx.indexIn("a eric"); //2 idx = rx.indexIn("a eirik"); //2 //统计单词eric和eirik出现的次数 QString str2 = "one eric another eirik, and an ericsson." "how many eiriks eric"; int pos = 0, count = 0; while(pos >= 0) { pos = rx.indexIn(str2, pos); if(pos >=0) { ++pos; ++count; } } qDebug() << count; //3
setPatternSyntax()用来设置正则表达式的语法规则,一些语法如下所示:
比如Wildcard是通配符匹配模式,bash、cmd.exe等shell都支持这种文件通配符,通配符匹配比默认的regexp要简单,它只有四个特点:
QRegExp rx("*.txt"); //使用括号才可以捕获被匹配的文本 rx.setPatternSyntax(QRegExp::Wildcard); qDebug() << rx.exactMatch("readme.txt"); //true qDebug() << rx.exactMatch("readme.txt.bak"); //false
要捕获匹配的文本需要使用括号将其包起来,然后可以使用cap()或capturedTexts()方法来提取匹配的文本:
QRegExp rx("(\\d+)"); //匹配一个数字或多个数字 QString str = "ab 12,14 99-231"; QStringList list; int pos = 0; while((pos = rx.indexIn(str, pos)) != -1) { list << rx.cap(1); //第一个捕获到的文本 pos += rx.matchedLength(); //匹配的字符串的长度 } qDebug() << list; //12, 14, 99, 231
如果使用括号是为了组合元素而不是为了捕获文本那么可以在括号中加?:,如(?:green|blue),这样更高效:
QRegExp rx("(\\d+)(?:\\s*)(cm|inch)");//匹配一个或多个数字 + 0个或1个或多个空白字符 + cm或inch int pos = rx.indexIn("Length: 189cm"); if(pos > -1) { QString value = rx.cap(1); //189,第一个匹配的文本 QString unit = rx.cap(2); //cm,第二个匹配的文本 QString str = rx.cap(0); //189cm,所有匹配的文本 int a = 0; }
还可以在正则中引用捕获到的文本,通过使用反向引用\n,如\1表示前面第一个捕获的文本,例如:
QRegExp rx("\\b(\\w+)\\W+\\1\\b"); //匹配一个单词的边界 + 一个或多个单词字符 + 一个或多个非单词字符 + 前面第一个捕获的文本(即(\\w+)匹配的文本) + 一个单词边界 rx.setCaseSensitivity(Qt::CaseInsensitive); //设置不区分大小写 qDebug() << rx.indexIn("Hello- -hello"); //0 qDebug() << rx.cap(1); //Hello
正则的一个常用场景就是对于用户输入密码的判断:
QRegExp rx("^\\d+$"); //匹配整个文本全是数字 qDebug() << rx.exactMatch("123"); //true rx.setPattern("^(?![0-9]+$).+$"); //匹配整个文本不全是数字 qDebug() << rx.exactMatch("123"); //false rx.setPattern("^[a-zA-Z]+$"); //匹配整个文本全是字母 qDebug() << rx.exactMatch("abc"); //true rx.setPattern("^(?![a-zA-Z]+$).+$"); //匹配整个文本不全是字母 qDebug() << rx.exactMatch("abc"); //false //密码输入验证:6-20位的数字或字母或*#!,但不能全是数字或英文字母 rx.setPattern("^(?![0-9]+$)(?![a-zA-Z]+$)([0-9A-Za-z]|[*#!]){6,20}$"); qDebug() << rx.exactMatch("abcd1234*"); //true