1996年进入商业领域,它也是目前流行的Linux桌面环境KDE的基础
2008年奇趣科技被诺基亚公司收购,Qt称为诺基亚旗下的编程语言
2012年Qt又被Digia公司收购
2014年4月跨平台的集成开发环境QtCreator3.1.0发布,同年5月20日配发了Qt5.3正式版,至此Qt实现了对iOS、Android、WP等各平台的全面支持。
当前Qt最新版本为5.5.0
lWindows–XP、Vista、Win7、Win8、Win2008、Win10
lUinux/X11–Linux、SunSolaris、HP-UX、CompaqTru64UNIX、IBMAIX、SGIIRIX、FreeBSD、BSD/OS、和其他很多X11平台
lMacintosh–MacOSX
lEmbedded–有帧缓冲支持的嵌入式Linux平台,WindowsCE
Qt按照不同的版本发行,分为商业版和开源版
l商业版
为商业软件提供开发,他们提供传统商业软件发行版,并且提供在商业有效期内的免费升级和技术支持服务。
l开源的LGPL版本:
为了开发自有而设计的开放源码软件,它提供了和商业版本同样的功能,在GNU通用公共许可下,它是免费的。
MinGW32-->MinimalistGNUforWindows32
l跨平台,几乎支持所有的平台
l接口简单,容易上手,学习QT框架对学习其他框架有参考意义。
l一定程度上简化了内存回收机制
l开发效率高,能够快速的构建应用程序。
l有很好的社区氛围,市场份额在缓慢上升。
l可以进行嵌入式开发。
打开QtCreator界面选择NewProject或者选择菜单栏【文件】-【新建文件或项目】菜单项
弹出NewProject对话框,选择QtWidgetsApplication,
选择【Choose】按钮,弹出如下对话框
设置项目名称和路径,按照向导进行下一步,
选择编译套件
向导会默认添加一个继承自CMainWindow的类,可以在此修改类的名字和基类。继续下一步
即可创建出一个Qt桌面程序。
添加一个空项目
选择【choose】进行下一步。设置项目名称和路径—>选择编译套件-->修改类信息-->完成(步骤同上),生成一个空项目。在空项目中添加文件:在项目名称上单击鼠标右键弹出右键菜单,选择【添加新文件】
弹出新建文件对话框
在此对话框中选择要添加的类或者文件,根据向导完成文件的添加。
在使用Qt向导生成的应用程序.pro文件格式如下:
QT+=coregui//模块的名字
greaterThan(QT_MAJOR_VERSION,4):QT+=widgets
TARGET=test//应用程序名
TEMPLATE=app//生成的makefile的模板类型
//源文件
SOURCES+=main.cpp\
mainwindow.cpp
//头文件
HEADERS+=mainwindow.h
//窗口设计文件
FORMS+=mainwindow.ui
.pro就是工程文件(project),它是qmake自动生成的用于生产makefile的配置文件。.pro文件的写法如下:
l注释
从“#”开始,到这一行结束。
l模板变量告诉qmake为这个应用程序生成哪种makefile。下面是可供使用的选择:TEMPLATE=app
napp-建立一个应用程序的makefile。这是默认值,所以如果模板没有被指定,这个将被使用。
nlib-建立一个库的makefile。
nvcapp-建立一个应用程序的VisualStudio项目文件。
nvclib-建立一个库的VisualStudio项目文件。
nsubdirs-这是一个特殊的模板,它可以创建一个能够进入特定目录并且为一个项目文件生成makefile并且为它调用make的makefile。
l#指定生成的应用程序名:
TARGET=QtDemo
l#工程中包含的头文件
HEADERS+=include/painter.h
l#工程中包含的.ui设计文件
FORMS+=forms/painter.ui
l#工程中包含的源文件
SOURCES+=sources/main.cppsources/painter.cpp
l#工程中包含的资源文件
RESOURCES+=qrc/painter.qrc
lgreaterThan(QT_MAJOR_VERSION,4):QT+=widgets
这条语句的含义是,如果QT_MAJOR_VERSION大于4(也就是当前使用的Qt5及更高版本)需要增加widgets模块。如果项目仅需支持Qt5,也可以直接添加“QT+=widgets”一句。不过为了保持代码兼容,最好还是按照QtCreator生成的语句编写。
l#配置信息
CONFIG用来告诉qmake关于应用程序的配置信息。
CONFIG+=c++11//使用c++11的特性
在这里使用“+=”,是因为我们添加我们的配置选项到任何一个已经存在中。这样做比使用“=”那样替换已经指定的所有选项更安全。
#include
#include
intmain(intargc,char*argv[])
{
QApplicationa(argc,argv);
QWidgetw;
w.show();
returna.exec();
}
解释:
lQt头文件没有.h后缀
lQt一个类对应一个头文件,类名就是头文件名
lQApplication应用程序类
n管理图形用户界面应用程序的控制流和主要设置。
n是Qt的整个后台管理的命脉它包含主事件循环,在其中来自窗口系统和其它资源的所有事件处理和调度。它也处理应用程序的初始化和结束,并且提供对话管理。
la.exec()
程序进入消息循环,等待对用户输入进行响应。这里main()把控制权转交给Qt,Qt完成事件处理工作,当应用程序退出的时候exec()的值就会返回。在exec()中,Qt接受并处理用户和系统的事件并且把它们传递给适当的窗口部件。
信号槽是Qt框架引以为豪的机制之一。所谓信号槽,实际就是观察者模式。当某个事件发生之后,比如,按钮检测到自己被点击了一下,它就会发出一个信号(signal)。这种发出是没有目的的,类似广播。如果有对象对这个信号感兴趣,它就会使用连接(connect)函数,意思是,将想要处理的信号和自己的一个函数(称为槽(slot))绑定来处理这个信号。也就是说,当信号发出时,被连接的槽函数会自动被回调。这就类似观察者模式:当发生了感兴趣的事件,某一个操作就会被自动触发。(这里提一句,Qt的信号槽使用了额外的处理来实现,并不是GoF经典的观察者模式的实现方式。)
为了体验一下信号槽的使用,我们以一段简单的代码说明:
lQt5的书写方式
#include
QApplicationapp(argc,argv);
QPushButtonbutton("Quit");
QObject::connect(&button,&QPushButton::clicked,
&app,&QApplication::quit);
button.show();
returnapp.exec();
connect()函数最常用的一般形式:
connect(sender,signal,receiver,slot);
参数:
nsender:发出信号的对象
nsignal:发送对象发出的信号
nreceiver:接收信号的对象
nslot:接收对象在接收到信号之后所需要调用的函数
信号槽要求信号和槽的参数一致,所谓一致,是参数类型一致。如果不一致,允许的情况是,槽函数的参数可以比信号的少,即便如此,槽函数存在的那些参数的顺序也必须和信号的前面几个一致起来。这是因为,你可以在槽函数中选择忽略信号传来的数据(也就是槽函数的参数比信号的少),但是不能说信号根本没有这个数据,你就要在槽函数中使用(就是槽函数的参数比信号的多,这是不允许的)。
如果信号槽不符合,或者根本找不到这个信号或者槽函数,比如我们改成:
connect(&button,&QPushButton::clicked,&QApplication::quit2);
由于QApplication没有quit2这样的函数,因此在编译时会有编译错误:
'quit2'isnotamemberofQApplication
这样,使用成员函数指针我们就不会担心在编写信号槽的时候出现函数错误。
lQt4的书写方式:
QPushButton*button=newQPushButton("Quit");
connect(button,SIGNAL(clicked()),&a,SLOT(quit()));
button->show();
这里使用了SIGNAL和SLOT这两个宏,将两个函数名转换成了字符串。注意到connect()函数的signal和slot都是接受字符串,一旦出现连接不成功的情况,Qt4是没有编译错误的(因为一切都是字符串,编译期是不检查字符串是否匹配),而是在运行时给出错误。这无疑会增加程序的不稳定性。
lQt5在语法上完全兼容Qt4
使用connect()可以让我们连接系统提供的信号和槽。但是,Qt的信号槽机制并不仅仅是使用系统提供的那部分,还会允许我们自己设计自己的信号和槽。
下面我们看看使用Qt的信号槽,实现一个报纸和订阅者的例子:
有一个报纸类Newspaper,有一个订阅者类Subscriber。Subscriber可以订阅Newspaper。这样,当Newspaper有了新的内容的时候,Subscriber可以立即得到通知。
#include
//////////newspaper.h//////////
classNewspaper:publicQObject
Q_OBJECT
public:
Newspaper(constQString&name):
m_name(name)
voidsend()
emitnewPaper(m_name);
signals:
voidnewPaper(constQString&name);
private:
QStringm_name;
};
//////////reader.h//////////
#include
classReader:publicQObject
Reader(){}
voidreceiveNewspaper(constQString&name)
qDebug()<<"ReceivesNewspaper:"< //////////main.cpp////////// #include #include"newspaper.h" #include"reader.h" QCoreApplicationapp(argc,argv); Newspapernewspaper("NewspaperA"); Readerreader; QObject::connect(&newspaper,&Newspaper::newPaper, &reader,&Reader::receiveNewspaper); newspaper.send(); l首先看Newspaper这个类。这个类继承了QObject类。只有继承了QObject类的类,才具有信号槽的能力。所以,为了使用信号槽,必须继承QObject。凡是QObject类(不管是直接子类还是间接子类),都应该在第一行代码写上Q_OBJECT。不管是不是使用信号槽,都应该添加这个宏。这个宏的展开将为我们的类提供信号槽机制、国际化机制以及Qt提供的不基于C++RTTI的反射能力。 lNewspaper类的public和private代码块都比较简单,只不过它新加了一个signals。signals块所列出的,就是该类的信号。信号就是一个个的函数名,返回值是void(因为无法获得信号的返回值,所以也就无需返回任何值),参数是该类需要让外界知道的数据。信号作为函数名,不需要在cpp函数中添加任何实现。 lReader类更简单。因为这个类需要接受信号,所以我们将其继承了QObject,并且添加了Q_OBJECT宏。后面则是默认构造函数和一个普通的成员函数。Qt5中,任何成员函数、static函数、全局函数和Lambda表达式都可以作为槽函数。与信号函数不同,槽函数必须自己完成实现代码。槽函数就是普通的成员函数,因此作为成员函数,也会受到public、private等访问控制符的影响。(如果信号是private的,这个信号就不能在类的外面连接,也就没有任何意义。) l发送者和接收者都需要是QObject的子类(当然,槽函数是全局函数、Lambda表达式等无需接收者的时候除外); l槽函数是普通的成员函数,作为成员函数,会受到public、private、protected的影响; l使用emit在恰当的位置发送信号; l使用QObject::connect()函数连接信号和槽。 l任何成员函数、static函数、全局函数和Lambda表达式都可以作为槽函数 l一个信号可以和多个槽相连 如果是这种情况,这些槽会一个接一个的被调用,但是它们的调用顺序是不确定的。 l多个信号可以连接到一个槽 只要任意一个信号发出,这个槽就会被调用。 l一个信号可以连接到另外的一个信号 当第一个信号发出时,第二个信号被发出。除此之外,这种信号-信号的形式和信号-槽的形式没有什么区别。 l槽可以被取消链接 这种情况并不经常出现,因为当一个对象delete之后,Qt自动取消所有连接到这个对象上面的槽。 l使用Lambda表达式 在使用Qt5的时候,能够支持Qt5的编译器都是支持Lambda表达式的。 我们的代码可以写成下面这样: QObject::connect(&newspaper,static_cast (constQString&)>(&Newspaper::newPaper), [=](constQString&name) {/*Yourcodehere.*/} ); 在连接信号和槽的时候,槽函数可以使用Lambda表达式的方式进行处理。 C++11中的Lambda表达式用于定义并创建匿名的函数对象,以简化编程工作。首先看一下Lambda表达式的基本构成: [函数对象参数](操作符重载函数参数)mutable或exception->返回值{函数体} ①函数对象参数; [],标识一个Lambda的开始,这部分必须存在,不能省略。函数对象参数是传递给编译器自动生成的函数对象类的构造函数的。函数对象参数只能使用那些到定义Lambda为止时Lambda所在作用范围内可见的局部变量(包括Lambda所在类的this)。函数对象参数有以下形式: n空。没有使用任何函数对象参数。 n=。函数体内可以使用Lambda所在作用范围内所有可见的局部变量(包括Lambda所在类的this),并且是值传递方式(相当于编译器自动为我们按值传递了所有局部变量)。 n&。函数体内可以使用Lambda所在作用范围内所有可见的局部变量(包括Lambda所在类的this),并且是引用传递方式(相当于编译器自动为我们按引用传递了所有局部变量)。 nthis。函数体内可以使用Lambda所在类中的成员变量。 na。将a按值进行传递。按值进行传递时,函数体内不能修改传递进来的a的拷贝,因为默认情况下函数是const的。要修改传递进来的a的拷贝,可以添加mutable修饰符。 n&a。将a按引用进行传递。 na,&b。将a按值进行传递,b按引用进行传递。 n=,&a,&b。除a和b按引用进行传递外,其他参数都按值进行传递。 n&,a,b。除a和b按值进行传递外,其他参数都按引用进行传递。 intm=0,n=0; [=](inta)mutable{m=++n+a;}(4); [&](inta){m=++n+a;}(4); [=,&m](inta)mutable{m=++n+a;}(4); [&,m](inta)mutable{m=++n+a;}(4); [m,n](inta)mutable{m=++n+a;}(4); [&m,&n](inta){m=++n+a;}(4); ②操作符重载函数参数; 标识重载的()操作符的参数,没有参数时,这部分可以省略。参数可以通过按值(如:(a,b))和按引用(如:(&a,&b))两种方式进行传递。 ③可修改标示符; ④错误抛出标示符; ⑤函数返回值; ->返回值类型,标识函数返回值的类型,当返回值为void,或者函数体中只有一处return的地方(此时编译器可以自动推断出返回值类型)时,这部分可以省略。 ⑥是函数体; {},标识函数的实现,这部分不能省略,但函数体可以为空。 以左上角为原点,X向右增加,Y向下增加。 对于嵌套窗口,其坐标是相对于父窗口来说的。 所有窗口及窗口控件都是从QWidget直接或间接派生出来的。 在Qt中创建对象的时候会提供一个Parent对象指针,下面来解释这个parent到底是干什么的。 lQObject是以对象树的形式组织起来的。 n当你创建一个QObject对象时,会看到QObject的构造函数接收一个QObject指针作为参数,这个参数就是parent,也就是父对象指针。 这相当于,在创建QObject对象时,可以提供一个其父对象,我们创建的这个QObject对象会自动添加到其父对象的children()列表。 n当父对象析构的时候,这个列表中的所有对象也会被析构。(注意,这里的父对象并不是继承意义上的父类!) 这种机制在GUI程序设计中相当有用。例如,一个按钮有一个QShortcut(快捷键)对象作为其子对象。当我们删除按钮的时候,这个快捷键理应被删除。这是合理的。 lQWidget是能够在屏幕上显示的一切组件的父类。 nQWidget继承自QObject,因此也继承了这种对象树关系。一个孩子自动地成为父组件的一个子组件。因此,它会显示在父组件的坐标系统中,被父组件的边界剪裁。例如,当用户关闭一个对话框的时候,应用程序将其删除,那么,我们希望属于这个对话框的按钮、图标等应该一起被删除。事实就是如此,因为这些都是对话框的子组件。 n当然,我们也可以自己删除子对象,它们会自动从其父对象列表中删除。比如,当我们删除了一个工具栏时,其所在的主窗口会自动将该工具栏从其子对象列表中删除,并且自动调整屏幕显示。 Qt引入对象树的概念,在一定程度上解决了内存问题。 l当一个QObject对象在堆上创建的时候,Qt会同时为其创建一个对象树。不过,对象树中对象的顺序是没有定义的。这意味着,销毁这些对象的顺序也是未定义的。 l任何对象树中的QObject对象delete的时候,如果这个对象有parent,则自动将其从parent的children()列表中删除;如果有孩子,则自动delete每一个孩子。Qt保证没有QObject会被delete两次,这是由析构顺序决定的。 如果QObject在栈上创建,Qt保持同样的行为。正常情况下,这也不会发生什么问题。来看下下面的代码片段: QWidgetwindow; QPushButtonquit("Quit",&window); 作为父组件的window和作为子组件的quit都是QObject的子类(事实上,它们都是QWidget的子类,而QWidget是QObject的子类)。这段代码是正确的,quit的析构函数不会被调用两次,因为标准C++要求,局部对象的析构顺序应该按照其创建顺序的相反过程。因此,这段代码在超出作用域时,会先调用quit的析构函数,将其从父对象window的子对象列表中删除,然后才会再调用window的析构函数。 但是,如果我们使用下面的代码: QPushButtonquit("Quit"); quit.setParent(&window); 情况又有所不同,析构顺序就有了问题。我们看到,在上面的代码中,作为父对象的window会首先被析构,因为它是最后一个创建的对象。在析构过程中,它会调用子对象列表中每一个对象的析构函数,也就是说,quit此时就被析构了。然后,代码继续执行,在window析构之后,quit也会被析构,因为quit也是一个局部变量,在超出作用域的时候当然也需要析构。但是,这时候已经是第二次调用quit的析构函数了,C++不允许调用两次析构函数,因此,程序崩溃了。 由此我们看到,Qt的对象树机制虽然帮助我们在一定程度上解决了内存问题,但是也引入了一些值得注意的事情。这些细节在今后的开发过程中很可能时不时跳出来烦扰一下,所以,我们最好从开始就养成良好习惯,在Qt中,尽量在构造的时候就指定parent对象,并且大胆在堆上创建。 QMainWindow是一个为用户提供主窗口程序的类,包含一个菜单栏(menubar)、多个工具栏(toolbars)、多个锚接部件(dockwidgets)、一个状态栏(statusbar)及一个中心部件(centralwidget),是许多应用程序的基础,如文本编辑器,图片编辑器等。 一个主窗口最多只有一个菜单栏。位于主窗口顶部、主窗口标题栏下面。 l创建菜单栏,通过QMainWindow类的menubar()函数获取主窗口菜单栏指针 QMenuBar*menuBar()const l创建菜单,调用QMenu的成员函数addMenu来添加菜单 QAction*addMenu(QMenu*menu) QMenu*addMenu(constQString&title) QMenu*addMenu(constQIcon&icon,constQString&title) l创建菜单项,调用QMenu的成员函数addAction来添加菜单项 QAction*activeAction()const QAction*addAction(constQString&text) QAction*addAction(constQIcon&icon,constQString&text) QAction*addAction(constQString&text,constQObject*receiver, constchar*member,constQKeySequence&shortcut=0) QAction*addAction(constQIcon&icon,constQString&text, constQObject*receiver,constchar*member, constQKeySequence&shortcut=0) Qt并没有专门的菜单项类,只是使用一个QAction类,抽象出公共的动作。当我们把QAction对象添加到菜单,就显示成一个菜单项,添加到工具栏,就显示成一个工具按钮。用户可以通过点击菜单项、点击工具栏按钮、点击快捷键来激活这个动作。 主窗口的工具栏上可以有多个工具条,通常采用一个菜单对应一个工具条的的方式,也可根据需要进行工具条的划分。 n直接调用QMainWindow类的addToolBar()函数获取主窗口的工具条对象,每增加一个工具条都需要调用一次该函数。 n插入属于工具条的动作,即在工具条上添加操作。 通过QToolBar类的addAction函数添加。 n工具条是一个可移动的窗口,它的停靠区域由QToolBar的allowAreas决定,包括: nQt::LeftToolBarArea停靠在左侧 nQt::RightToolBarArea停靠在右侧 nQt::TopToolBarArea停靠在顶部 nQt::BottomToolBarArea停靠在底部 nQt::AllToolBarAreas以上四个位置都可停靠 使用setAllowedAreas()函数指定停靠区域: setAllowedAreas(Qt::LeftToolBarArea|Qt::RightToolBarArea) 使用setMoveable()函数设定工具栏的可移动性: setMoveable(false)//工具条不可移动,只能停靠在初始化的位置上 n派生自QWidget类,使用方法与QWidget类似,QStatusBar类常用成员函数: //添加小部件 voidaddWidget(QWidget*widget,intstretch=0) //插入小部件 intinsertWidget(intindex,QWidget*widget,intstretch=0) //删除小部件 voidremoveWidget(QWidget*widget) Qt资源系统是一个跨平台的资源机制,用于将程序运行时所需要的资源以二进制的形式存储于可执行文件内部。如果你的程序需要加载特定的资源(图标、文本翻译等),那么,将其放置在资源文件中,就再也不需要担心这些文件的丢失。也就是说,如果你将资源以资源文件形式存储,它是会编译到可执行文件内部。 使用QtCreator可以很方便地创建资源文件。我们可以在工程上点右键,选择“添加新文件…”,可以在Qt分类下找到“Qt资源文件”: 点击“选择…”按钮,打开“新建Qt资源文件”对话框。在这里我们输入资源文件的名字和路径: 点击下一步,选择所需要的版本控制系统,然后直接选择完成。我们可以在QtCreator的左侧文件列表中看到“资源文件”一项,也就是我们新创建的资源文件: 右侧的编辑区有个“添加”,我们首先需要添加前缀,比如我们将前缀取名为images。然后选中这个前缀,继续点击添加文件,可以找到我们所需添加的文件。这里,我们选择document-open.png文件。当我们完成操作之后,QtCreator应该是这样子的: 接下来,我们还可以添加另外的前缀或者另外的文件。这取决于你的需要。当我们添加完成之后,我们可以像前面一章讲解的那样,通过使用:开头的路径来找到这个文件。比如,我们的前缀是/images,文件是document-open.png,那么就可以使用:/images/document-open.png找到这个文件。 这么做带来的一个问题是,如果以后我们要更改文件名,比如将docuemnt-open.png改成docopen.png,那么,所有使用了这个名字的路径都需要修改。所以,更好的办法是,我们给这个文件去一个“别名”,以后就以这个别名来引用这个文件。具体做法是,选中这个文件,添加别名信息: 这样,我们可以直接使用:/images/doc-open引用到这个资源,无需关心图片的真实文件名。 如果我们使用文本编辑器打开res.qrc文件,就会看到一下内容: 我们可以对比一下,看看QtCreator帮我们生成的是怎样的qrc文件。当我们编译工程之后,我们可以在构建目录中找到qrc_res.cpp文件,这就是Qt将我们的资源编译成了C++代码。 对话框是GUI程序中不可或缺的组成部分。很多不能或者不适合放入主窗口的功能组件都必须放在对话框中设置。对话框通常会是一个顶层窗口,出现在程序最上层,用于实现短期任务或者简洁的用户交互。 Qt中使用QDialog类实现对话框。就像主窗口一样,我们通常会设计一个类继承QDialog。QDialog(及其子类,以及所有Qt::Dialog类型的类)的对于其parent指针都有额外的解释:如果parent为NULL,则该对话框会作为一个顶层窗口,否则则作为其父组件的子对话框(此时,其默认出现的位置是parent的中心)。顶层窗口与非顶层窗口的区别在于,顶层窗口在任务栏会有自己的位置,而非顶层窗口则会共享其父组件的位置。 对话框分为模态对话框和非模态对话框。 l模态对话框,就是会阻塞同一应用程序中其它窗口的输入。 模态对话框很常见,比如“打开文件”功能。你可以尝试一下记事本的打开文件,当打开文件对话框出现时,我们是不能对除此对话框之外的窗口部分进行操作的。 l与此相反的是非模态对话框,例如查找对话框,我们可以在显示着查找对话框的同时,继续对记事本的内容进行编辑。 所谓标准对话框,是Qt内置的一系列对话框,用于简化开发。事实上,有很多对话框都是通用的,比如打开文件、设置颜色、打印设置等。这些对话框在所有程序中几乎相同,因此没有必要在每一个程序中都自己实现这么一个对话框。 Qt的内置对话框大致分为以下几类: lQColorDialog:选择颜色; lQFileDialog:选择文件或者目录; lQFontDialog:选择字体; lQInputDialog:允许用户输入一个值,并将其值返回; lQMessageBox:模态对话框,用于显示信息、询问问题等; lQPrintDialog:打印机配置; lQPrintPreviewDialog:打印预览; lQProgressDialog:显示操作过程。 Qt支持模态对话框和非模态对话框。 模态与非模态的实现: l使用QDialog::exec()实现应用程序级别的模态对话框 l使用QDialog::open()实现窗口级别的模态对话框 l使用QDialog::show()实现非模态对话框。 lQt有两种级别的模态对话框: n应用程序级别的模态 当该种模态的对话框出现时,用户必须首先对对话框进行交互,直到关闭对话框,然后才能访问程序中其他的窗口。 n窗口级别的模态 该模态仅仅阻塞与对话框关联的窗口,但是依然允许用户与程序中其它窗***互。窗口级别的模态尤其适用于多窗口模式。 一般默认是应用程序级别的模态。 在下面的示例中,我们调用了exec()将对话框显示出来,因此这就是一个模态对话框。当对话框出现时,我们不能与主窗口进行任何交互,直到我们关闭了该对话框。 voidMainWindow::open() QDialogdialog; dialog.setWindowTitle(tr("Hello,dialog!")); dialog.exec(); 下面我们试着将exec()修改为show(),看看非模态对话框: QDialogdialog(this); dialog.show(); 是不是事与愿违?对话框竟然一闪而过!这是因为,show()函数不会阻塞当前线程,对话框会显示出来,然后函数立即返回,代码继续执行。注意,dialog是建立在栈上的,show()函数返回,MainWindow::open()函数结束,dialog超出作用域被析构,因此对话框消失了。知道了原因就好改了,我们将dialog改成堆上建立,当然就没有这个问题了: QDialog*dialog=newQDialog; dialog->setWindowTitle(tr("Hello,dialog!")); dialog->show(); 如果你足够细心,应该发现上面的代码是有问题的:dialog存在内存泄露!dialog使用new在堆上分配空间,却一直没有delete。解决方案也很简单:将MainWindow的指针赋给dialog即可。还记得我们前面说过的Qt的对象系统吗? 不过,这样做有一个问题:如果我们的对话框不是在一个界面类中出现呢?由于QWidget的parent必须是QWidget指针,那就限制了我们不能将一个普通的C++类指针传给Qt对话框。另外,如果对内存占用有严格限制的话,当我们将主窗口作为parent时,主窗口不关闭,对话框就不会被销毁,所以会一直占用内存。在这种情景下,我们可以设置dialog的WindowAttribute: dialog->setAttribute(Qt::WA_DeleteOnClose); setAttribute()函数设置对话框关闭时,自动销毁对话框。 QMessageBox用于显示消息提示。我们一般会使用其提供的几个static函数: l显示关于对话框。 voidabout(QWidget*parent,constQString&title,constQString&text) 这是一个最简单的对话框,其标题是title,内容是text,父窗口是parent。对话框只有一个OK按钮。 l显示关于Qt对话框。该对话框用于显示有关Qt的信息。 voidaboutQt(QWidget*parent,constQString&title=QString()): l显示严重错误对话框。 StandardButtoncritical(QWidget*parent, constQString&title, constQString&text, StandardButtonsbuttons=Ok, StandardButtondefaultButton=NoButton): 这个对话框将显示一个红色的错误符号。我们可以通过buttons参数指明其显示的按钮。默认情况下只有一个Ok按钮,我们可以使用StandardButtons类型指定多种按钮。 l与QMessageBox::critical()类似,不同之处在于这个对话框提供一个普通信息图标。 StandardButtoninformation(QWidget*parent, StandardButtondefaultButton=NoButton) l与QMessageBox::critical()类似,不同之处在于这个对话框提供一个问号图标,并且其显示的按钮是“是”和“否”。 StandardButtonquestion(QWidget*parent, StandardButtonsbuttons=StandardButtons(Yes|No), l与QMessageBox::critical()类似,不同之处在于这个对话框提供一个黄色叹号图标。 StandardButtonwarning(QWidget*parent, 我们可以通过下面的代码来演示下如何使用QMessageBox。 if(QMessageBox::Yes==QMessageBox::question(this, tr("Question"),tr("AreyouOK"), QMessageBox::Yes|QMessageBox::No, QMessageBox::Yes)) QMessageBox::information(this,tr("Hmmm..."), tr("I'mgladtohearthat!")); else tr("I'msorry!")); 我们使用QMessageBox::question()来询问一个问题。 l这个对话框的父窗口是this。 QMessageBox是QDialog的子类,这意味着它的初始显示位置将会是在parent窗口的中央。 l第二个参数是对话框的标题。 l第三个参数是我们想要显示的内容。 这里就是我们需要询问的文字。下面,我们使用或运算符(|)指定对话框应该出现的按钮。这里我们希望是一个Yes和一个No。 l最后一个参数指定默认选择的按钮。 这个函数有一个返回值,用于确定用户点击的是哪一个按钮。按照我们的写法,应该很容易的看出,这是一个模态对话框,因此我们可以直接获取其返回值。 QMessageBox类的static函数优点是方便使用,缺点也很明显:非常不灵活。我们只能使用简单的几种形式。为了能够定制QMessageBox细节,我们必须使用QMessageBox的属性设置API。如果我们希望制作一个询问是否保存的对话框,我们可以使用如下的代码: QMessageBoxmsgBox; msgBox.setText(tr("Thedocumenthasbeenmodified.")); msgBox.setInformativeText(tr("Doyouwanttosaveyourchanges")); msgBox.setDetailedText(tr("Differenceshere...")); msgBox.setStandardButtons(QMessageBox::Save |QMessageBox::Discard |QMessageBox::Cancel); msgBox.setDefaultButton(QMessageBox::Save); intret=msgBox.exec(); switch(ret) caseQMessageBox::Save: qDebug()<<"Savedocument!"; break; caseQMessageBox::Discard: qDebug()<<"Discardchanges!"; caseQMessageBox::Cancel: qDebug()<<"Closedocument!"; msgBox是一个建立在栈上的QMessageBox实例。我们设置其主要文本信息为“Thedocumenthasbeenmodified.”,informativeText则是会在对话框中显示的简单说明文字。下面我们使用了一个detailedText,也就是详细信息,当我们点击了详细信息按钮时,对话框可以自动显示更多信息。我们自己定义的对话框的按钮有三个:保存、丢弃和取消。然后我们使用了exec()是其成为一个模态对话框,根据其返回值进行相应的操作。 QFileDialog,也就是文件对话框。在本节中,我们将尝试编写一个简单的文本文件编辑器,我们将使用QFileDialog来打开一个文本文件,并将修改过的文件保存到硬盘。 首先,我们需要创建一个带有文本编辑功能的窗口。借用我们前面的程序代码,应该可以很方便地完成: openAction=newQAction(QIcon(":/images/file-open"), tr("&Open..."),this); openAction->setShortcuts(QKeySequence::Open); openAction->setStatusTip(tr("Openanexistingfile")); saveAction=newQAction(QIcon(":/images/file-save"), tr("&Save..."),this); saveAction->setShortcuts(QKeySequence::Save); saveAction->setStatusTip(tr("Saveanewfile")); QMenu*file=menuBar()->addMenu(tr("&File")); file->addAction(openAction); file->addAction(saveAction); QToolBar*toolBar=addToolBar(tr("&File")); toolBar->addAction(openAction); toolBar->addAction(saveAction); textEdit=newQTextEdit(this); setCentralWidget(textEdit); 我们在菜单和工具栏添加了两个动作:打开和保存。接下来是一个QTextEdit类,这个类用于显示富文本文件。也就是说,它不仅仅用于显示文本,还可以显示图片、表格等等。不过,我们现在只用它显示纯文本文件。QMainWindow有一个setCentralWidget()函数,可以将一个组件作为窗口的中心组件,放在窗口中央显示区。显然,在一个文本编辑器中,文本编辑区就是这个中心组件,因此我们将QTextEdit作为这种组件。 我们使用connect()函数,为这两个QAction对象添加响应的动作: connect(openAction,&QAction::triggered, this,&MainWindow::openFile); connect(saveAction,&QAction::triggered, this,&MainWindow::saveFile); 下面是最主要的openFile()和saveFile()这两个函数的代码: //打开文件 voidMainWindow::openFile() QStringpath=QFileDialog::getOpenFileName(this, tr("OpenFile"),".",tr("TextFiles(*.txt)")); if(!path.isEmpty()) QFilefile(path); if(!file.open(QIODevice::ReadOnly|QIODevice::Text)) QMessageBox::warning(this,tr("ReadFile"), tr("Cannotopenfile:\n%1").arg(path)); return; QTextStreamin(&file); textEdit->setText(in.readAll()); file.close(); QMessageBox::warning(this,tr("Path"), tr("Youdidnotselectanyfile.")); //保存文件 voidMainWindow::saveFile() QStringpath=QFileDialog::getSaveFileName(this, if(!file.open(QIODevice::WriteOnly|QIODevice::Text)) QMessageBox::warning(this,tr("WriteFile"), QTextStreamout(&file); out< 在openFile()函数中,我们使用QFileDialog::getOpenFileName()来获取需要打开的文件的路径。这个函数原型如下: QStringgetOpenFileName(QWidget*parent=0, constQString&caption=QString(), constQString&dir=QString(), constQString&filter=QString(), QString*selectedFilter=0, Optionsoptions=0) 不过注意,它的所有参数都是可选的,因此在一定程度上说,这个函数也是简单的。这六个参数分别是: lparent:父窗口。 Qt的标准对话框提供静态函数,用于返回一个模态对话框; lcaption:对话框标题; ldir:对话框打开时的默认目录 n“.”代表程序运行目录 lfilter:过滤器。 我们使用文件对话框可以浏览很多类型的文件,但是,很多时候我们仅希望打开特定类型的文件。比如,文本编辑器希望打开文本文件,图片浏览器希望打开图片文件。过滤器就是用于过滤特定的后缀名。如果我们使用“ImageFiles(*.jpg*.png)”,则只能显示后缀名是jpg或者png的文件。如果需要多个过滤器,使用“;;”分割,比如“JPEGFiles(*.jpg);;PNGFiles(*.png)”; lselectedFilter:默认选择的过滤器; loptions:对话框的一些参数设定 比如只显示文件夹等等,它的取值是enumQFileDialog::Option,每个选项可以使用|运算组合起来。 QFileDialog::getOpenFileName()返回值是选择的文件路径。我们将其赋值给path。通过判断path是否为空,可以确定用户是否选择了某一文件。只有当用户选择了一个文件时,我们才执行下面的操作。 在saveFile()中使用的QFileDialog::getSaveFileName()也是类似的。使用这种静态函数,在Windows、MacOS上面都是直接调用本地对话框,但是Linux上则是QFileDialog自己的模拟。这暗示了,如果你不使用这些静态函数,而是直接使用QFileDialog进行设置,那么得到的对话框很可能与系统对话框的外观不一致。这一点是需要注意的。 Qt为我们应用程序界面开发提供的一系列的控件,下面我们介绍两种最常用的两种,所有控件的使用方法我们都可以通过帮助文档获取。 QLabel是我们最常用的控件之一,其功能很强大,我们可以用来显示文本,图片和动画等。 通过QLabel类的setText函数设置显示的内容: voidsetText(constQString&) l可以显示普通文本字符串 QLable*label=newQLable; label->setText(“Hello,World!”); l可以显示HTML格式的字符串 比如显示一个链接: QLabel*label=newQLabel(this); label->setText("Hello,World"); 百度一下"); label->setOpenExternalLinks(true); 其中setOpenExternalLinks()函数是用来设置用户点击链接之后是否自动打开链接,如果参数指定为true则会自动打开,如果设置为false,想要打开链接只能通过捕捉linkActivated()信号,在自定义的槽函数中使用QDesktopServices::openUrl()打开链接,该函数参数默认值为false //label->setOpenExternalLinks(true); connect(label,&QLabel::linkActivated, this,&MyWidget::slotOpenUrl); //槽函数 voidMyWidget::slotOpenUrl(constQString&link) QDesktopServices::openUrl(QUrl(link)); 可以使用QLabel的成员函数setPixmap设置图片 voidsetPixmap(constQPixmap&) 首先定义QPixmap对象 QPixmappixmap; 然后加载图片 pixmap.load(":/Image/boat.jpg"); 最后将图片设置到QLabel中 QLabel*label=newQLabel; label.setPixmap(pixmap); 可以使用QLabel的成员函数setMovie加载动画,可以播放gif格式的文件 voidsetMovie(QMovie*movie) 首先定义QMovied对象,并初始化: QMovie*movie=newQMovie(":/Mario.gif"); 播放加载的动画: movie->start(); 将动画设置到QLabel中: QLabel*label=newQLabel; label->setMovie(movie); Qt提供的单行文本编辑框。 QStringtext()const l设置编辑框内容 voidsetEchoMode(EchoModemode) EchoMode是一个枚举类型,一共定义了四种显示模式: lQLineEdit::Normal模式显示方式,按照输入的内容显示。 lQLineEdit::NoEcho不显示任何内容,此模式下无法看到用户的输入。 lQLineEdit::Password密码模式,输入的字符会根据平台转换为特殊字符。 lQLineEdit::PasswordEchoOnEdit编辑时显示字符否则显示字符作为密码。 另外,我们再使用QLineEdit显示文本的时候,希望在左侧留出一段空白的区域,那么,就可以使用QLineEdit给我们提供的setTextMargins函数: voidsetTextMargins(intleft,inttop,intright,intbottom) 用此函数可以指定显示的文本与输入框上下左右边界的间隔的像素数。 如果我们想实现一个与百度的搜索框类似的功能:输入一个或几个字符,下边会列出几个跟输入的字符相匹配的字符串,QLineEdit要实现这样的功能可以使用该类的成员函数setComleter()函数来实现: voidsetCompleter(QCompleter*c) 创建QCompleter对象,并初始化 QStringListtipList; tipList<<“Hello”<<“howareyou”<<“Haha”<<“oh,hello”; //不区分大小写 completer->setCaseSensitivity(Qt::CaseInsensitive); QCompleter*completer=newQCompleter(tipList,this); QCompleter类的setCaseSensitivity()函数可以设置是否区分大小写,它的参数是一个枚举类型: lQt::CaseInsensitive不区分大小写 lQt::CaseSensitive区分大小写 如果不设置该属性,默认匹配字符串时是区分大小写的。 voidsetFilterMode(Qt::MatchFlagsfilterMode) 其参数为Qt定义的宏,有多重类型,具体可参考Qt帮助稳定,要实现我们上边提到的功能,参数可以使用Qt::MatchContains: completer->setFilterMode(Qt::MatchContains); 属性设置完成之后,将QCompleter对象设置到QLineEdit中: QLineEdit*edit=newQLineEdit(this); edit->setCompleter(completer); Qt中控件的使用方法可参考Qt提供的帮助文档。 所谓GUI界面,归根结底,就是一堆组件的叠加。我们创建一个窗口,把按钮放上面,把图标放上面,这样就成了一个界面。在放置时,组件的位置尤其重要。我们必须要指定组件放在哪里,以便窗口能够按照我们需要的方式进行渲染。这就涉及到组件定位的机制。 Qt提供了两种组件定位机制:绝对定位和布局定位。 l绝对定位就是一种最原始的定位方法:给出这个组件的坐标和长宽值。 这样,Qt就知道该把组件放在哪里以及如何设置组件的大小。但是这样做带来的一个问题是,如果用户改变了窗口大小,比如点击最大化按钮或者使用鼠标拖动窗口边缘,采用绝对定位的组件是不会有任何响应的。这也很自然,因为你并没有告诉Qt,在窗口变化时,组件是否要更新自己以及如何更新。或者,还有更简单的方法:禁止用户改变窗口大小。但这总不是长远之计。 l布局定位:你只要把组件放入某一种布局,布局由专门的布局管理器进行管理。当需要调整大小或者位置的时候,Qt使用对应的布局管理器进行调整。 布局定位完美的解决了使用绝对定位的缺陷。 Qt提供的布局中以下三种是我们最常用的: lQHBoxLayout:按照水平方向从左到右布局; lQVBoxLayout:按照竖直方向从上到下布局; lQGridLayout:在一个网格中进行布局,类似于HTML的table; 下面我们通过一个例子来学习以下水平布局管理器的使用方法: window.setWindowTitle("Enteryourage"); QSpinBox*spinBox=newQSpinBox(&window); QSlider*slider=newQSlider(Qt::Horizontal,&window); spinBox->setRange(0,130); slider->setRange(0,130); QObject::connect(slider,&QSlider::valueChanged, spinBox,&QSpinBox::setValue); void(QSpinBox::*spinBoxSignal)(int)=&QSpinBox::valueChanged; QObject::connect(spinBox,spinBoxSignal, slider,&QSlider::setValue); spinBox->setValue(35); //给控件设置布局 QHBoxLayout*layout=newQHBoxLayout; layout->addWidget(spinBox); layout->addWidget(slider); window.setLayout(layout); window.show(); 我们在这段代码中引入了两个新的组件:QSpinBox和QSlider。QSpinBox就是只能输入数字的输入框,并且带有上下箭头的步进按钮。QSlider则是带有滑块的滑竿。 上面的代码中window.setLayout(layout);是将布局设置到窗口window中,在窗口中设置布局还有另一种写法: QHBoxLayout*layout=newQHBoxLayout(window); 在创建布局对象的时候给新对象指定父窗口,就等于给传入的窗口设置了布局。 另外布局与布局之间是可以嵌套使用的,使用addLayout()方法。QVBoxLayout的使用方法与QHBoxLayout完全相同。 关于上述代码中信号和槽连接的解释: 当数字输入框显示的内容发生改变的时候,会发出一股信息,滑块会接收这一信号,并作出改变。如果二者的信号槽连接写成下边这样: QObject::connect(spinBox,&QSpinBox::valueChanged, 编译器却会报错 nomatchingfunctionforcallto'QObject::connect(QSpinBox*&, 这是怎么回事呢?从出错信息可以看出,编译器认为QSpinBox::valueChanged是一个overloaded的函数。我们看一下QSpinBox的文档发现,QSpinBox的确有两个信号: lvoidvalueChanged(int) lvoidvalueChanged(constQString&) 当我们使用&QSpinBox::valueChanged取函数指针时,编译器不知道应该取哪一个函数(记住前面我们介绍过的,signal也是一个普通的函数。)的地址,因此报错。解决的方法很简单,编译器不是不能确定哪一个函数吗?那么我们就显式指定一个函数。方法就是,我们创建一个函数指针,这个函数指针参数指定为int: 然后我们将这个函数指针作为signal,与QSlider的函数连接: 这样便避免了编译错误。 通过布局管理器搭建如下登陆界面: 在搭建Qt窗口界面的时候,在一个项目中很多窗口,或者是窗口中的某个模块会被经常性的重复使用。一般遇到这种情况我们都会将这个窗口或者模块拿出来做成一个独立的窗口类,以备以后重复使用。 在使用Qt的ui文件搭建界面的时候,工具栏栏中只为我们提供了标准的窗口控件,如果我们想使用自定义控件怎么办? 例如:我们从QWidget派生出一个类SmallWidget,实现了一个自定窗口, //smallwidget.h classSmallWidget:publicQWidget explicitSmallWidget(QWidget*parent=0); publicslots: QSpinBox*spin; QSlider*slider; //smallwidget.cpp SmallWidget::SmallWidget(QWidget*parent):QWidget(parent) spin=newQSpinBox(this); slider=newQSlider(Qt::Horizontal,this); //创建布局对象 //将控件添加到布局中 layout->addWidget(spin); //将布局设置到窗口中 setLayout(layout); //添加消息响应 connect(spin, static_cast connect(slider,&QSlider::valueChanged, spin,&QSpinBox::setValue); 那么这个SmallWidget可以作为独立的窗口显示,也可以作为一个控件来使用: 打开Qt的.ui文件,因为SmallWidget是派生自Qwidget类,所以需要在ui文件中先放入一个QWidget控件,然后再上边鼠标右键 弹出提升窗口部件对话框 添加要提升的类的名字,然后选择添加 添加之后,类名会显示到上边的列表框中,然后单击提升按钮,完成操作. 我们可以看到,这个窗口对应的类从原来的QWidget变成了SmallWidget 再次运行程序,这个widget_3中就能显示出我们自定义的窗口了. 事件(event)是由系统或者Qt本身在不同的时刻发出的。当用户按下鼠标、敲下键盘,或者是窗口需要重新绘制的时候,都会发出一个相应的事件。一些事件在对用户操作做出响应时发出,如键盘事件等;另一些事件则是由系统自动发出,如计时器事件。 在前面我们也曾经简单提到,Qt程序需要在main()函数创建一个QApplication对象,然后调用它的exec()函数。这个函数就是开始Qt的事件循环。在执行exec()函数之后,程序将进入事件循环来监听应用程序的事件。当事件发生时,Qt将创建一个事件对象。Qt中所有事件类都继承于QEvent。在事件对象创建完毕后,Qt将这个事件对象传递给QObject的event()函数。event()函数并不直接处理事件,而是按照事件对象的类型分派给特定的事件处理函数(eventhandler),关于这一点,会在后边详细说明。 在所有组件的父类QWidget中,定义了很多事件处理的回调函数,如 nkeyPressEvent() nkeyReleaseEvent() nmouseDoubleClickEvent() nmouseMoveEvent() nmousePressEvent() nmouseReleaseEvent()等。 这些函数都是protectedvirtual的,也就是说,我们可以在子类中重新实现这些函数。下面来看一个例子: classEventLabel:publicQLabel protected: voidmouseMoveEvent(QMouseEvent*event); voidmousePressEvent(QMouseEvent*event); voidmouseReleaseEvent(QMouseEvent*event); voidEventLabel::mouseMoveEvent(QMouseEvent*event) this->setText(QString(" ").arg(QString::number(event->x()), QString::number(event->y()))); voidEventLabel::mousePressEvent(QMouseEvent*event) this->setText(QString("Move:(%1,%2)
Press:(%1,%2)
voidEventLabel::mouseReleaseEvent(QMouseEvent*event)
QStringmsg;
msg.sprintf("
Release:(%d,%d)
event->x(),event->y());
this->setText(msg);
EventLabel*label=newEventLabel;
label->setWindowTitle("MouseEventDemo");
label->resize(300,200);
label->show();
lEventLabel继承了QLabel,覆盖了mousePressEvent()、mouseMoveEvent()和MouseReleaseEvent()三个函数。我们并没有添加什么功能,只是在鼠标按下(press)、鼠标移动(move)和鼠标释放(release)的时候,把当前鼠标的坐标值显示在这个Label上面。由于QLabel是支持HTML代码的,因此我们直接使用了HTML代码来格式化文字。
lQString的arg()函数可以自动替换掉QString中出现的占位符。其占位符以%开始,后面是占位符的位置,例如%1,%2这种。
QString("[%1,%2]").arg(x).arg(y);
语句将会使用x替换%1,y替换%2,因此,生成的QString为[x,y]。
l在mouseReleaseEvent()函数中,我们使用了另外一种QString的构造方法。我们使用类似C风格的格式化函数sprintf()来构造QString。
运行上面的代码,当我们点击了一下鼠标之后,label上将显示鼠标当前坐标值。
为什么要点击鼠标之后才能在mouseMoveEvent()函数中显示鼠标坐标值?
这是因为QWidget中有一个mouseTracking属性,该属性用于设置是否追踪鼠标。只有鼠标被追踪时,mouseMoveEvent()才会发出。如果mouseTracking是false(默认即是),组件在至少一次鼠标点击之后,才能够被追踪,也就是能够发出mouseMoveEvent()事件。如果mouseTracking为true,则mouseMoveEvent()直接可以被发出。
知道了这一点,我们就可以在main()函数中添加如下代码:
label->setMouseTracking(true);
在运行程序就没有这个问题了。
事件对象创建完毕后,Qt将这个事件对象传递给QObject的event()函数。event()函数并不直接处理事件,而是将这些事件对象按照它们不同的类型,分发给不同的事件处理器(eventhandler)。
如上所述,event()函数主要用于事件的分发。所以,如果你希望在事件分发之前做一些操作,就可以重写这个event()函数了。例如,我们希望在一个QWidget组件中监听tab键的按下,那么就可以继承QWidget,并重写它的event()函数,来达到这个目的:
boolCustomWidget::event(QEvent*e)
if(e->type()==QEvent::KeyPress){
QKeyEvent*keyEvent=static_cast
if(keyEvent->key()==Qt::Key_Tab){
qDebug()<<"Youpresstab.";
returntrue;
returnQWidget::event(e);
CustomWidget是一个普通的QWidget子类。我们重写了它的event()函数,这个函数有一个QEvent对象作为参数,也就是需要转发的事件对象。函数返回值是bool类型。
l如果传入的事件已被识别并且处理,则需要返回true,否则返回false。如果返回值是true,那么Qt会认为这个事件已经处理完毕,不会再将这个事件发送给其它对象,而是会继续处理事件队列中的下一事件。
l在event()函数中,调用事件对象的accept()和ignore()函数是没有作用的,不会影响到事件的传播。
我们可以通过使用QEvent::type()函数可以检查事件的实际类型,其返回值是QEvent::Type类型的枚举。我们处理过自己感兴趣的事件之后,可以直接返回true,表示我们已经对此事件进行了处理;对于其它我们不关心的事件,则需要调用父类的event()函数继续转发,否则这个组件就只能处理我们定义的事件了。为了测试这一种情况,我们可以尝试下面的代码:
boolCustomTextEdit::event(QEvent*e)
if(e->type()==QEvent::KeyPress)
if(keyEvent->key()==Qt::Key_Tab)
returnfalse;
CustomTextEdit是QTextEdit的一个子类。我们重写了其event()函数,却没有调用父类的同名函数。这样,我们的组件就只能处理Tab键,再也无法输入任何文本,也不能响应其它事件,比如鼠标点击之后也不会有光标出现。这是因为我们只处理的KeyPress类型的事件,并且如果不是KeyPress事件,则直接返回false,鼠标事件根本不会被转发,也就没有了鼠标事件。
通过查看QObject::event()的实现,我们可以理解,event()函数同前面的章节中我们所说的事件处理器有什么联系:
//!!!Qt5
boolQObject::event(QEvent*e)
switch(e->type()){
caseQEvent::Timer:
timerEvent((QTimerEvent*)e);
caseQEvent::ChildAdded:
caseQEvent::ChildPolished:
caseQEvent::ChildRemoved:
childEvent((QChildEvent*)e);
//...
default:
if(e->type()>=QEvent::User){
customEvent(e);
这是Qt5中QObject::event()函数的源代码(Qt4的版本也是类似的)。我们可以看到,同前面我们所说的一样,Qt也是使用QEvent::type()判断事件类型,然后调用了特定的事件处理器。比如,如果event->type()返回值是QEvent::Timer,则调用timerEvent()函数。可以想象,QWidget::event()中一定会有如下的代码:
switch(event->type()){
caseQEvent::MouseMove:
mouseMoveEvent((QMouseEvent*)event);
事实也的确如此。timerEvent()和mouseMoveEvent()这样的函数,就是我们前面章节所说的事件处理器eventhandler。也就是说,event()函数中实际是通过事件处理器来响应一个具体的事件。这相当于event()函数将具体事件的处理“委托”给具体的事件处理器。而这些事件处理器是protectedvirtual的,因此,我们重写了某一个事件处理器,即可让Qt调用我们自己实现的版本。
由此可以见,event()是一个集中处理不同类型的事件的地方。如果你不想重写一大堆事件处理器,就可以重写这个event()函数,通过QEvent::type()判断不同的事件。鉴于重写event()函数需要十分小心注意父类的同名函数的调用,一不留神就可能出现问题,所以一般还是建议只重写事件处理器(当然,也必须记得是不是应该调用父类的同名处理器)。这其实暗示了event()函数的另外一个作用:屏蔽掉某些不需要的事件处理器。正如我们前面的CustomTextEdit例子看到的那样,我们创建了一个只能响应tab键的组件。这种作用是重写事件处理器所不能实现的。
有时候,对象需要查看、甚至要拦截发送到另外对象的事件。例如,对话框可能想要拦截按键事件,不让别的组件接收到;或者要修改回车键的默认处理。
通过前面的章节,我们已经知道,Qt创建了QEvent事件对象之后,会调用QObject的event()函数处理事件的分发。显然,我们可以在event()函数中实现拦截的操作。由于event()函数是protected的,因此,需要继承已有类。如果组件很多,就需要重写很多个event()函数。这当然相当麻烦,更不用说重写event()函数还得小心一堆问题。好在Qt提供了另外一种机制来达到这一目的:事件过滤器。
QObject有一个eventFilter()函数,用于建立事件过滤器。函数原型如下:
virtualboolQObject::eventFilter(QObject*watched,QEvent*event);
我们来看一段简单的代码:
classMainWindow:publicQMainWindow
MainWindow();
booleventFilter(QObject*obj,QEvent*event);
QTextEdit*textEdit;
MainWindow::MainWindow()
textEdit=newQTextEdit;
textEdit->installEventFilter(this);
boolMainWindow::eventFilter(QObject*obj,QEvent*event)
if(obj==textEdit){
if(event->type()==QEvent::KeyPress){
QKeyEvent*keyEvent=static_cast
qDebug()<<"Atekeypress"<
}else{
//passtheeventontotheparentclass
returnQMainWindow::eventFilter(obj,event);
lMainWindow是我们定义的一个类。我们重写了它的eventFilter()函数。为了过滤特定组件上的事件,首先需要判断这个对象是不是我们感兴趣的组件,然后判断这个事件的类型。在上面的代码中,我们不想让textEdit组件处理键盘按下的事件。所以,首先我们找到这个组件,如果这个事件是键盘事件,则直接返回true,也就是过滤掉了这个事件,其他事件还是要继续处理,所以返回false。对于其它的组件,我们并不保证是不是还有过滤器,于是最保险的办法是调用父类的函数。
leventFilter()函数相当于创建了过滤器,然后我们需要安装这个过滤器。安装过滤器需要调用QObject::installEventFilter()函数。函数的原型如下:
voidQObject::installEventFilter(QObject*filterObj)
这个函数接受一个QObject*类型的参数。记得刚刚我们说的,eventFilter()函数是QObject的一个成员函数,因此,任意QObject都可以作为事件过滤器(问题在于,如果你没有重写eventFilter()函数,这个事件过滤器是没有任何作用的,因为默认什么都不会过滤)。已经存在的过滤器则可以通过QObject::removeEventFilter()函数移除。
l我们可以向一个对象上面安装多个事件处理器,只要调用多次installEventFilter()函数。如果一个对象存在多个事件过滤器,那么,最后一个安装的会第一个执行,也就是后进先执行的顺序。
还记得我们前面的那个例子吗?我们使用event()函数处理了Tab键:
现在,我们可以给出一个使用事件过滤器的版本:
boolFilterObject::eventFilter(QObject*object,QEvent*event)
if(object==target&&event->type()==QEvent::KeyPress)
事件过滤器的强大之处在于,我们可以为整个应用程序添加一个事件过滤器。记得,installEventFilter()函数是QObject的函数,QApplication或者QCoreApplication对象都是QObject的子类,因此,我们可以向QApplication或者QCoreApplication添加事件过滤器。这种全局的事件过滤器将会在所有其它特性对象的事件过滤器之前调用。尽管很强大,但这种行为会严重降低整个应用程序的事件分发效率。因此,除非是不得不使用的情况,否则的话我们不应该这么做。
注意,
事件过滤器和被安装过滤器的组件必须在同一线程,否则,过滤器将不起作用。另外,如果在安装过滤器之后,这两个组件到了不同的线程,那么,只有等到二者重新回到同一线程的时候过滤器才会有效。
Qt的事件是整个Qt框架的核心机制之一,也比较复杂。说它复杂,更多是因为它涉及到的函数众多,而处理方法也很多,有时候让人难以选择。现在我们简单总结一下Qt中的事件机制。
Qt中有很多种事件:鼠标事件、键盘事件、大小改变的事件、位置移动的事件等等。如何处理这些事件,实际有两种选择:
l所有事件对应一个事件处理函数,在这个事件处理函数中用一个很大的分支语句进行选择,其代表作就是win32API的WndProc()函数:
LRESULTCALLBACKWndProc(HWNDhWnd,
UINTmessage,
WPARAMwParam,
LPARAMlParam)
在这个函数中,我们需要使用switch语句,选择message参数的类型进行处理,典型代码是:
switch(message)
caseWM_PAINT:
caseWM_DESTROY:
...
l每一种事件对应一个事件处理函数。Qt就是使用的这么一种机制:
nmouseEvent()
n…
Qt具有这么多种事件处理函数,肯定有一个地方对其进行分发,否则,Qt怎么知道哪一种事件调用哪一个事件处理函数呢?这个分发的函数,就是event()。显然,当QMouseEvent产生之后,event()函数将其分发给mouseEvent()事件处理器进行处理。
event()函数会有两个问题:
levent()函数是一个protected的函数,这意味着我们要想重写event(),必须继承一个已有的类。试想,我的程序根本不想要鼠标事件,程序中所有组件都不允许处理鼠标事件,是不是我得继承所有组件,一一重写其event()函数?protected函数带来的另外一个问题是,如果我基于第三方库进行开发,而对方没有提供源代码,只有一个链接库,其它都是封装好的。我怎么去继承这种库中的组件呢?
levent()函数的确有一定的控制,不过有时候我的需求更严格一些:我希望那些组件根本看不到这种事件。event()函数虽然可以拦截,但其实也是接收到了QMouseEvent对象。我连让它收都收不到。这样做的好处是,模拟一种系统根本没有那个事件的效果,所以其它组件根本不会收到这个事件,也就无需修改自己的事件处理函数。这种需求怎么办呢?
这两个问题是event()函数无法处理的。于是,Qt提供了另外一种解决方案:事件过滤器。事件过滤器给我们一种能力,让我们能够完全移除某种事件。事件过滤器可以安装到任意QObject类型上面,并且可以安装多个。如果要实现全局的事件过滤器,则可以安装到QApplication或者QCoreApplication上面。这里需要注意的是,如果使用installEventFilter()函数给一个对象安装事件过滤器,那么该事件过滤器只对该对象有效,只有这个对象的事件需要先传递给事件过滤器的eventFilter()函数进行过滤,其它对象不受影响。如果给QApplication对象安装事件过滤器,那么该过滤器对程序中的每一个对象都有效,任何对象的事件都是先传给eventFilter()函数。
事件过滤器可以解决刚刚我们提出的event()函数的两点不足:
l首先,事件过滤器不是protected的,因此我们可以向任何QObject子类安装事件过滤器;
l其次,事件过滤器在目标对象接收到事件之前进行处理,如果我们将事件过滤掉,目标对象根本不会见到这个事件。
virtualboolQCoreApplication::notify(QObject*receiver,
QEvent*event);
该函数会将event发送给receiver,也就是调用receiver->event(event),其返回值就是来自receiver的事件处理器。注意,这个函数为任意线程的任意对象的任意事件调用,因此,它不存在事件过滤器的线程的问题。不过我们并不推荐这么做,因为notify()函数只有一个,而事件过滤器要灵活得多。
现在我们可以总结一下Qt的事件处理,实际上是有五个层次:
l重写paintEvent()、mousePressEvent()等事件处理函数。这是最普通、最简单的形式,同时功能也最简单。
l重写event()函数。event()函数是所有对象的事件入口,QObject和QWidget中的实现,默认是把事件传递给特定的事件处理函数。
l在特定对象上面安装事件过滤器。该过滤器仅过滤该对象接收到的事件。
l在QCoreApplication::instance()上面安装事件过滤器。该过滤器将过滤所有对象的所有事件,因此和notify()函数一样强大,但是它更灵活,因为可以安装多个过滤器。全局的事件过滤器可以看到disabled组件上面发出的鼠标事件。全局过滤器有一个问题:只能用在主线程。
l重写QCoreApplication::notify()函数。这是最强大的,和全局事件过滤器一样提供完全控制,并且不受线程的限制。但是全局范围内只能有一个被使用(因为QCoreApplication是单例的)。
常见的窗体是各种方形的对话框,但有时候也需要非方形的窗体,如圆形,椭圆甚至是不规则形状的对话框。
实现步骤:
l新建一个项目,比如项目名称叫做“ShapeWidget”,给此项目添加一个类“ShapeWidget”,基类选择“QWidget”。
l为了使该不规则窗体可以通过鼠标随意拖拽,在类中重定义鼠标事件:mousePressEvent()、mouseMoveEvent()、以及绘制函数paintEvent()
l“ShapeWidget”的构造函数部分是实现该不规则窗体的关键,添加具体代码如下:
//新建一个Pixmap对象
//加载图片
pixmap.load(":/new/prefix1/image/sunny.png");
//固定窗口大小,将窗口大小设置为图片大小
setFixedSize(pixmap.width(),pixmap.height());
//给窗口去掉边框,设置窗口的flags
setWindowFlags(Qt::FramelessWindowHint|windowFlags());
//设置透明背景
setAttribute(Qt::WA_TranslucentBackground);
l重新实现鼠标事件和绘制函数
voidShareWidget::mousePressEvent(QMouseEvent*ev)
if(ev->button()==Qt::LeftButton)
//求出窗口移动之前的坐标
m_dragPoint=ev->globalPos()-frameGeometry().topLeft();
if(ev->button()==Qt::RightButton)
//鼠标右键关闭窗口
close();
voidShareWidget::mouseMoveEvent(QMouseEvent*ev)
if(ev->buttons()&Qt::LeftButton)
//如果是鼠标左键拖动,移动窗口
move(ev->globalPos()-m_dragPoint);
voidShareWidget::paintEvent(QPaintEvent*ev)
Q_UNUSED(ev)
QPainterpainter(this);
//重新绘制图片
painter.drawPixmap(0,0,QPixmap(":/ButterFly"));
Qt的绘图系统允许使用相同的API在屏幕和其它打印设备上进行绘制。整个绘图系统基于QPainter,QPainterDevice和QPaintEngine三个类。
QPainter用来执行绘制的操作;QPaintDevice是一个二维空间的抽象,这个二维空间允许QPainter在其上面进行绘制,也就是QPainter工作的空间;QPaintEngine提供了画笔(QPainter)在不同的设备上进行绘制的统一的接口。QPaintEngine类应用于QPainter和QPaintDevice之间,通常对开发人员是透明的。除非你需要自定义一个设备,否则你是不需要关心QPaintEngine这个类的。我们可以把QPainter理解成画笔;把QPaintDevice理解成使用画笔的地方,比如纸张、屏幕等;而对于纸张、屏幕而言,肯定要使用不同的画笔绘制,为了统一使用一种画笔,我们设计了QPaintEngine类,这个类让不同的纸张、屏幕都能使用一种画笔。
下图给出了这三个类之间的层次结构:
上面的示意图告诉我们,Qt的绘图系统实际上是,使用QPainter在QPainterDevice上进行绘制,它们之间使用QPaintEngine进行通讯(也就是翻译QPainter的指令)。
下面我们通过一个实例来介绍QPainter的使用:
classPaintedWidget:publicQWidget
PaintedWidget(QWidget*parent=0);
voidpaintEvent(QPaintEvent*);
注意我们重写了QWidget的paintEvent()函数。接下来就是PaintedWidget的源代码:
PaintedWidget::PaintedWidget(QWidget*parent):
QWidget(parent)
resize(800,600);
setWindowTitle(tr("PaintDemo"));
voidPaintedWidget::paintEvent(QPaintEvent*)
painter.drawLine(80,100,650,500);
painter.setPen(Qt::red);
painter.drawRect(10,10,100,400);
painter.setPen(QPen(Qt::green,5));
painter.setBrush(Qt::blue);
painter.drawEllipse(50,150,400,200);
在构造函数中,我们仅仅设置了窗口的大小和标题。而paintEvent()函数则是绘制的代码。首先,我们在栈上创建了一个QPainter对象,也就是说,每次运行paintEvent()函数的时候,都会重建这个QPainter对象。注意,这一点可能会引发某些细节问题:由于我们每次重建QPainter,因此第一次运行时所设置的画笔颜色、状态等,第二次再进入这个函数时就会全部丢失。有时候我们希望保存画笔状态,就必须自己保存数据,否则的话则需要将QPainter作为类的成员变量。
QPainter接收一个QPaintDevice指针作为参数。QPaintDevice有很多子类,比如QImage,以及QWidget。注意回忆一下,QPaintDevice可以理解成要在哪里去绘制,而现在我们希望画在这个组件,因此传入的是this指针。
QPainter有很多以draw开头的函数,用于各种图形的绘制,比如这里的drawLine(),drawRect()以及drawEllipse()等。当绘制轮廓线时,使用QPainter的pen()属性。比如,我们调用了painter.setPen(Qt::red)将pen设置为红色,则下面绘制的矩形具有红色的轮廓线。接下来,我们将pen修改为绿色,5像素宽(painter.setPen(QPen(Qt::green,5))),又设置了画刷为蓝色。这时候再调用draw函数,则是具有绿色5像素宽轮廓线、蓝色填充的椭圆。
绘图设备是指继承QPainterDevice的子类。Qt一共提供了四个这样的类,分别是QPixmap、QBitmap、QImage和QPicture。其中,
lQPixmap专门为图像在屏幕上的显示做了优化
lQBitmap是QPixmap的一个子类,它的色深限定为1,可以使用QPixmap的isQBitmap()函数来确定这个QPixmap是不是一***itmap。
lQImage专门为图像的像素级访问做了优化。
lQPicture则可以记录和重现QPainter的各条命令。
QBitmap继承自QPixmap,因此具有QPixmap的所有特性,提供单色图像。QBitmap的色深始终为1.色深这个概念来自计算机图形学,是指用于表现颜色的二进制的位数。我们知道,计算机里面的数据都是使用二进制表示的。为了表示一种颜色,我们也会使用二进制。比如我们要表示8种颜色,需要用3个二进制位,这时我们就说色深是3.因此,所谓色深为1,也就是使用1个二进制位表示颜色。1个位只有两种状态:0和1,因此它所表示的颜色就有两种,黑和白。所以说,QBitmap实际上是只有黑白两色的图像数据。
由于QBitmap色深小,因此只占用很少的存储空间,所以适合做光标文件和笔刷。
下面我们来看同一个图像文件在QPixmap和QBitmap下的不同表现:
voidPaintWidget::paintEvent(QPaintEvent*)
QPixmappixmap(":/Image/butterfly.png");
QPixmappixmap1(":/Image/butterfly1.png");
QBitmapbitmap(":/Image/butterfly.png");
QBitmapbitmap1(":/Image/butterfly1.png");
painter.drawPixmap(0,0,pixmap);
painter.drawPixmap(200,0,pixmap1);
painter.drawPixmap(0,130,bitmap);
painter.drawPixmap(200,130,bitmap1);
这里我们给出了两张png图片。butterfly1.png是没有透明色的纯白背景,而butterfly.png是具有透明色的背景。我们分别使用QPixmap和QBitmap来加载它们。注意看它们的区别:白色的背景在QBitmap中消失了,而透明色在QBitmap中转换成了黑色;其他颜色则是使用点的疏密程度来体现的。
QPixmap使用底层平台的绘制系统进行绘制,无法提供像素级别的操作,而QImage则是使用独立于硬件的绘制系统,实际上是自己绘制自己,因此提供了像素级别的操作,并且能够在不同系统之上提供一个一致的显示形式。
QImageimage(300,300,QImage::Format_RGB32);
QRgbvalue;
//将图片背景填充为白色
image.fill(Qt::white);
//改变指定区域的像素点的值
for(inti=50;i<100;++i)
for(intj=50;j<100;++j)
value=qRgb(255,0,0);//红色
image.setPixel(i,j,value);
//将图片绘制到窗口中
painter.drawImage(QPoint(0,0),image);
QImage与QPixmap的区别
lQPixmap主要是用于绘图,针对屏幕显示而最佳化设计,QImage主要是为图像I/O、图片访问和像素修改而设计的
lQPixmap依赖于所在的平台的绘图引擎,故例如反锯齿等一些效果在不同的平台上可能会有不同的显示效果,QImage使用Qt自身的绘图引擎,可在不同平台上具有相同的显示效果
l由于QImage是独立于硬件的,也是一种QPaintDevice,因此我们可以在另一个线程中对其进行绘制,而不需要在GUI线程中处理,使用这一方式可以很大幅度提高UI响应速度。
lQImage可通过setPixpel()和pixel()等方法直接存取指定的像素。
QImage与QPixmap之间的转换:
lQImage转QPixmap
使用QPixmap的静态成员函数:fromImage()
QPixmapfromImage(constQImage&image,
Qt::ImageConversionFlagsflags=Qt::AutoColor)
lQPixmap转QImage:
使用QPixmap类的成员函数:toImage()
QImagetoImage()const
最后一个需要说明的是QPicture。这是一个可以记录和重现QPainter命令的绘图设备。QPicture将QPainter的命令序列化到一个IO设备,保存为一个平***立的文件格式。这种格式有时候会是“元文件(meta-files)”。Qt的这种格式是二进制的,不同于某些本地的元文件,Qt的pictures文件没有内容上的限制,只要是能够被QPainter绘制的元素,不论是字体还是pixmap,或者是变换,都可以保存进一个picture中。
QPicture是平台无关的,因此它可以使用在多种设备之上,比如svg、pdf、ps、打印机或者屏幕。回忆下我们这里所说的QPaintDevice,实际上是说可以有QPainter绘制的对象。QPicture使用系统的分辨率,并且可以调整QPainter来消除不同设备之间的显示差异。
如果我们要记录下QPainter的命令,首先要使用QPainter::begin()函数,将QPicture实例作为参数传递进去,以便告诉系统开始记录,记录完毕后使用QPainter::end()命令终止。代码示例如下:
QPicturepic;
QPainterpainter;
//将图像绘制到QPicture中,并保存到文件
painter.begin(&pic);
painter.drawEllipse(20,20,100,50);
painter.fillRect(20,100,100,100,Qt::red);
painter.end();
pic.save("D:\\drawing.pic");
//将保存的绘图动作重新绘制到设备上
pic.load("D:\\drawing.pic");
painter.begin(this);
painter.drawPicture(200,200,pic);
文件操作是应用程序必不可少的部分。Qt作为一个通用开发库,提供了跨平台的文件操作能力。Qt通过QIODevice提供了对I/O设备的抽象,这些设备具有读写字节块的能力。下面是I/O设备的类图(Qt5):
lQIODevice:所有I/O设备类的父类,提供了字节块读写的通用操作以及基本接口;
lQFileDevice:Qt5新增加的类,提供了有关文件操作的通用实现。
lQFlie:访问本地文件或者嵌入资源;
lQTemporaryFile:创建和访问本地文件系统的临时文件;
lQBuffer:读写QbyteArray,内存文件;
lQProcess:运行外部程序,处理进程间通讯;
lQAbstractSocket:所有套接字类的父类;
lQTcpSocket:TCP协议网络数据传输;
lQUdpSocket:传输UDP报文;
lQSslSocket:使用SSL/TLS传输数据;
文件系统分类:
l顺序访问设备:
是指它们的数据只能访问一遍:从头走到尾,从第一个字节开始访问,直到最后一个字节,中途不能返回去读取上一个字节,这其中,QProcess、QTcpSocket、QUdpSoctet和QSslSocket是顺序访问设备。
l随机访问设备:
可以访问任意位置任意次数,还可以使用QIODevice::seek()函数来重新定位文件访问位置指针,QFile、QTemporaryFile和QBuffer是随机访问设备,
文件操作是应用程序必不可少的部分。Qt作为一个通用开发库,提供了跨平台的文件操作能力。在所有的I/O设备中,文件I/O是最重要的部分之一。因为我们大多数的程序依旧需要首先访问本地文件(当然,在云计算大行其道的将来,这一观点可能改变)。QFile提供了从文件中读取和写入数据的能力。
我们通常会将文件路径作为参数传给QFile的构造函数。不过也可以在创建好对象最后,使用setFileName()来修改。QFile需要使用/作为文件分隔符,不过,它会自动将其转换成操作系统所需要的形式。例如C:/windows这样的路径在Windows平台下同样是可以的。
QFile主要提供了有关文件的各种操作,比如打开文件、关闭文件、刷新文件等。我们可以使用QDataStream或QTextStream类来读写文件,也可以使用QIODevice类提供的read()、readLine()、readAll()以及write()这样的函数。值得注意的是,有关文件本身的信息,比如文件名、文件所在目录的名字等,则是通过QFileInfo获取,而不是自己分析文件路径字符串。
下面我们使用一段代码来看看QFile的有关操作:
QFilefile("in.txt");
if(!file.open(QIODevice::ReadOnly|QIODevice::Text)){
qDebug()<<"Openfilefailed.";
return-1;
while(!file.atEnd()){
qDebug()< QFileInfoinfo(file); qDebug()< qDebug()< qDebug()< qDebug()< qDebug()< qDebug()< l我们首先使用QFile创建了一个文件对象。 这个文件名字是in.txt。如果你不知道应该把它放在哪里,可以使用QDir::currentPath()来获得应用程序执行时的当前路径。只要将这个文件放在与当前路径一致的目录下即可。 l使用open()函数打开这个文件,打开形式是只读方式,文本格式。 这个类似于fopen()的r这样的参数。open()函数返回一个bool类型,如果打开失败,我们在控制台输出一段提示然后程序退出。否则,我们利用while循环,将每一行读到的内容输出。 l可以使用QFileInfo获取有关该文件的信息。 QFileInfo有很多类型的函数,我们只举出一些例子。比如: nisDir()检查该文件是否是目录; nisExecutable()检查该文件是否是可执行文件等。 nbaseName()可以直接获得文件名; ncompleteBaseName()获取完整的文件名 nsuffix()则直接获取文件后缀名。 ncompleteSuffix()获取完整的文件后缀 我们可以由下面的示例看到,baseName()和completeBaseName(),以及suffix()和completeSuffix()的区别: QFileInfofi("/tmp/archive.tar.gz"); QStringbase=fi.baseName();//base="archive" QStringbase=fi.completeBaseName();//base="archive.tar" QStringext=fi.suffix();//ext="gz" QStringext=fi.completeSuffix();//ext="tar.gz" QDataStream提供了基于QIODevice的二进制数据的序列化。数据流是一种二进制流,这种流完全不依赖于底层操作系统、CPU或者字节顺序(大端或小端)。例如,在安装了Windows平台的PC上面写入的一个数据流,可以不经过任何处理,直接拿到运行了Solaris的SPARC机器上读取。由于数据流就是二进制流,因此我们也可以直接读写没有编码的二进制数据,例如图像、视频、音频等。 QDataStream既能够存取C++基本类型,如int、char、short等,也可以存取复杂的数据类型,例如自定义的类。实际上,QDataStream对于类的存储,是将复杂的类分割为很多基本单元实现的。 结合QIODevice,QDataStream可以很方便地对文件、网络套接字等进行读写操作。我们从代码开始看起: QFilefile("file.dat"); file.open(QIODevice::WriteOnly); QDataStreamout(&file); out< out<<(qint32)42; l在这段代码中,我们首先打开一个名为file.dat的文件(注意,我们为简单起见,并没有检查文件打开是否成功,这在正式程序中是不允许的)。然后,我们将刚刚创建的file对象的指针传递给一个QDataStream实例out。类似于std::cout标准输出流,QDataStream也重载了输出重定向<<运算符。后面的代码就很简单了:将“theansweris”和数字42输出到数据流。由于我们的out对象建立在file之上,因此相当于将问题和答案写入file。 l需要指出一点:最好使用Qt整型来进行读写,比如程序中的qint32。这保证了在任意平台和任意编译器都能够有相同的行为。 如果你直接运行这段代码,你会得到一个空白的file.dat,并没有写入任何数据。这是因为我们的file没有正常关闭。为性能起见,数据只有在文件关闭时才会真正写入。因此,我们必须在最后添加一行代码: file.close();//如果不想关闭文件,可以使用file.flush(); 接下来我们将存储到文件中的答案取出来 file.open(QIODevice::ReadOnly); QDataStreamin(&file); QStringstr; qint32a; in>>str>>a; 唯一需要注意的是,你必须按照写入的顺序,将数据读取出来。顺序颠倒的话,程序行为是不确定的,严重时会直接造成程序崩溃。 那么,既然QIODevice提供了read()、readLine()之类的函数,为什么还要有QDataStream呢?QDataStream同QIODevice有什么区别?区别在于,QDataStream提供流的形式,性能上一般比直接调用原始API更好一些。我们通过下面一段代码看看什么是流的形式: file.open(QIODevice::ReadWrite); QDataStreamstream(&file); QStringstr="theansweris42"; stream< 上一节我们介绍了有关二进制文件的读写。二进制文件比较小巧,却不是人可读的格式。而文本文件是一种人可读的文件。为了操作这种文件,我们需要使用QTextStream类。QTextStream和QDataStream的使用类似,只不过它是操作纯文本文件的。 QTextStream会自动将Unicode编码同操作系统的编码进行转换,这一操作对开发人员是透明的。它也会将换行符进行转换,同样不需要自己处理。QTextStream使用16位的QChar作为基础的数据存储单位,同样,它也支持C++标准类型,如int等。实际上,这是将这种标准类型与字符串进行了相互转换。 QTextStream同QDataStream的使用基本一致,例如下面的代码将把“Theansweris42”写入到file.txt文件中: QFiledata("file.txt"); if(data.open(QFile::WriteOnly|QIODevice::Truncate)) QTextStreamout(&data); out<<"Theansweris"<<42; 这里,我们在open()函数中增加了QIODevice::Truncate打开方式。我们可以从下表中看到这些打开方式的区别: 枚举值描述 lQIODevice::NotOpen未打开 lQIODevice::ReadOnly以只读方式打开 lQIODevice::WriteOnly以只写方式打开 lQIODevice::ReadWrite以读写方式打开 lQIODevice::Append以追加的方式打开, 新增加的内容将被追加到文件末尾 lQIODevice::Truncate以重写的方式打开,在写入新的数据时会将原有 数据全部清除,游标设置在文件开头。 lQIODevice::Text在读取时,将行结束符转换成\n;在写入时, 将行结束符转换成本地格式,例如Win32平台 上是\r\n lQIODevice::Unbuffered忽略缓存 我们在这里使用了QFile::WriteOnly|QIODevice::Truncate,也就是以只写并且覆盖已有内容的形式操作文件。注意,QIODevice::Truncate会直接将文件内容清空。 虽然QTextStream的写入内容与QDataStream一致,但是读取时却会有些困难: if(data.open(QFile::ReadOnly)) QTextStreamin(&data); intans=0; in>>str>>ans; 在使用QDataStream的时候,这样的代码很方便,但是使用了QTextStream时却有所不同:读出的时候,str里面将是Theansweris42,ans是0。这是因为当使用QDataStream写入的时候,实际上会在要写入的内容前面,额外添加一个这段内容的长度值。而以文本形式写入数据,是没有数据之间的分隔的。因此,使用文本文件时,很少会将其分割开来读取,而是使用诸如使用: