您的位置:首页 > 编程语言 > Qt开发

Qt 学习之路 2笔记1

2016-07-11 21:08 435 查看

不(可以)建议这样做

#include <QApplication>
#include <QLabel>

int main(int argc, char *argv[])
{
QApplication app(argc, argv);

QLabel *label = new QLabel("Hello, world");
label->show();

return app.exec();
}


首先,按照标准 C++ 来看这段程序。这里存在着内存泄露。当exec()退出时(也就是事件循环结束的时候。窗口关闭,事件循环就会结束),label 是没办法 delete 的。这就造成了内存泄露。当然,由于程序结束,操作系统会负责回收内存,所以这个问题不会很严重。即便你这样修改了代码再运行,也不会有任何错误。

早期版本的 Qt 可能会有问题,不过在新版本的 Qt 基本不存在问题。在新版本的 Qt 中,app.exec()的实现机制确定,当最后一个可视组件关闭之后,主事件循环(也就是app.exec())才会退出,main()函数结束(此时会销毁app)。这意味着,所有可视元素已经都关闭了,也就不存在后文提到的,QPaintDevice没有QApplication实例这种情况。另外,如果你是显式关闭了QApplication实例,例如调用了qApp->quit()之类的函数,QApplication的最后一个动作将会是关闭所有窗口。所以,即便在这种情况下,也不会出现类这种问题。由于是在main()函数中,当main()函数结束时,操作系统会回收进程所占用的资源,相当于没有内存泄露。不过,这里有一个潜在的问题:操作系统只会粗暴地释放掉所占内存,并不会调用对象的析构函数(这与调用delete运算符是不同的),所以,很有可能有些资源占用不会被“正确”释放。

如果你必须在堆上创建对象,不妨增加一句:

label->setAttribute(Qt::WA_DeleteOnClose);


信号槽

QMetaObject::Connection connect(const QObject *, const char *,
const QObject *, const char *,
Qt::ConnectionType);

QMetaObject::Connection connect(const QObject *, const QMetaMethod &,
const QObject *, const QMetaMethod &,
Qt::ConnectionType);

QMetaObject::Connection connect(const QObject *, const char *,
const char *,
Qt::ConnectionType) const;

QMetaObject::Connection connect(const QObject *, PointerToMemberFunction,
const QObject *, PointerToMemberFunction,
Qt::ConnectionType)

QMetaObject::Connection connect(const QObject *, PointerToMemberFunction,
Functor);
QObject::connect(&button, &QPushButton::clicked, &QApplication::quit2);


自定义信号槽需要注意的事项:

发送者和接收者都需要是QObject的子类(当然,槽函数是全局函数、Lambda 表达式等无需接收者的时候除外);

使用 signals 标记信号函数,信号是一个函数声明,返回 void,不需要实现函数代码;

槽函数是普通的成员函数,作为成员函数,会受到 public、private、protected 的影响;

使用 emit 在恰当的位置发送信号;

使用QObject::connect()函数连接信号和槽。

“槽函数是普通的成员函数,作为成员函数,会受到public、private、protected的影响”,public、private 这些修饰符是供编译器在编译期检查的,因此其影响在于编译期。对于 Qt4 的信号槽连接语法,其连接是在运行时完成的,因此即便是 private 的槽函数也是可以作为槽进行连接的。但是,如果你使用了 Qt5 的新语法,新语法提供了编译期检查(取函数指针),因此取 private 函数的指针是不能通过编译的。

MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)


MainWindow 的构造函数之前需要调用父类 QMainWindow 的带有参数的构造函数。一般而言,子类应该调用父类的同参数构造函数,以便正确构造父类。如果没有调用,可以认为编译器会调用 QMainWindow(0) 构造函数,这样的话,parent 指针就不能传递给父类,可能会造成奇怪的错误。

资源文件

:/images/document-open.png

可以给这个文件取一个“别名”—->

:/images/doc-open

元对象编译器

信号槽机制,用于解决对象之间的通讯,这个我们已经了解过了,可以认为是 Qt 最明显的特性之一;

可查询,并且可设计的对象属性;

强大的事件机制以及事件过滤器;

基于上下文的字符串翻译机制(国际化),也就是 tr() 函数,我们简单地介绍过;

复杂的定时器实现,用于在事件驱动的 GUI 中嵌入能够精确控制的任务集成;

层次化的可查询的对象树,提供一种自然的方式管理对象关系。

智能指针(QPointer),在对象析构之后自动设为 0,防止野指针;

能够跨越库边界的动态转换机制。

通过继承QObject类,我们可以很方便地获得这些特性。当然,这些特性都是由 moc 帮助我们实现的。moc 其实实现的是一个叫做元对象系统(meta-object system)的机制。在 Qt 中,尽量在构造的时候就指定 parent 对象,并且大胆在堆上创建。

BUT,如果关闭一个对象时不希望在它里边声明的对象关闭,可以不指定parent。

布局管理器

QHBoxLayout:按照水平方向从左到右布局;

QVBoxLayout:按照竖直方向从上到下布局;

QGridLayout:在一个网格中进行布局,类似于 HTML 的 table;

QFormLayout:按照表格布局,每一行前面是一段文本,文本后面跟随一个组件(通常是输入框),类似 HTML 的 form;

QStackedLayout:层叠的布局,允许我们将几个组件按照 Z 轴方向堆叠,可以形成向导那种一页一页的效果。

对话框

Qt 中使用QDialog类实现对话框。就像主窗口一样,我们通常会设计一个类继承QDialog。QDialog(及其子类,以及所有Qt::Dialog类型的类)对于其 parent 指针都有额外的解释:如果 parent 为 NULL,则该对话框会作为一个顶层窗口,否则则作为其父组件的子对话框(此时,其默认出现的位置是 parent 的中心)。顶层窗口与非顶层窗口的区别在于,顶层窗口在任务栏会有自己的位置,而非顶层窗口则会共享其父组件的位置。

对话框分为模态对话框非模态对话框。所谓模态对话框,就是会阻塞同一应用程序中其它窗口的输入。模态对话框很常见,比如“打开文件”功能。与此相反的是非模态对话框,例如查找对话框,我们可以在显示着查找对话框的同时,继续对记事本的内容进行编辑。

Qt 支持模态对话框和非模态对话框。其中,Qt 有两种级别的模态对话框:应用程序级别的模态和窗口级别的模态,默认是应用程序级别的模态。应用程序级别的模态是指,当该种模态的对话框出现时,用户必须首先对对话框进行交互,直到关闭对话框,然后才能访问程序中其他的窗口。窗口级别的模态是指,该模态仅仅阻塞与对话框关联的窗口,但是依然允许用户与程序中其它窗口交互。窗口级别的模态尤其适用于多窗口模式。

Qt 使用QDialog::exec()实现应用程序级别的模态对话框,使用QDialog::open()实现窗口级别的模态对话框,使用QDialog::show()实现非模态对话框。

看看非模态对话框:

void MainWindow::open()
{
QDialog dialog(this);
dialog.setWindowTitle(tr("Hello, dialog!"));
dialog.show();
}


对话框竟然一闪而过!这是因为,show()函数不会阻塞当前线程,对话框会显示出来,然后函数立即返回,代码继续执行。注意,dialog 是建立在栈上的,show()函数返回,MainWindow::open()函数结束,dialog 超出作用域被析构,因此对话框消失了。知道了原因就好改了,我们将 dialog 改成堆上建立,当然就没有这个问题了:

void MainWindow::open()
{
QDialog *dialog = new QDialog;
dialog->setWindowTitle(tr("Hello, dialog!"));
dialog->show();
}


!!!dialog 存在内存泄露!解决办法:

将 MainWindow 的指针赋给 dialog 即可,(this)。

设置 dialog 的WindowAttribute

void MainWindow::open()
{
QDialog *dialog = new QDialog;
dialog->setAttribute(Qt::WA_DeleteOnClose);
dialog->setWindowTitle(tr("Hello, dialog!"));
dialog->show();
}


setAttribute()函数设置对话框关闭时,自动销毁对话框。另外,QObject还有一个deleteLater()函数,该函数会在当前事件循环结束时销毁该对话框。

如果是 show() 函数的话,只能使用 dialog->setAttribute(Qt::WA_DeleteOnClose);如果要使用 dialog->deleteLater(),则必须使用 exec() 函数。这是因为 deleteLater() 会在事件循环结束时执行,show() 不开始新的事件循环,所以会一闪而过。

void MainWindow::open()
{
QDialog dialog(this);
dialog.setWindowTitle(tr("Hello, dialog!"));
dialog.exec();
qDebug() << dialog.result();
}


我们使用exec()显示一个模态对话框。exec()开始了一个事件循环,代码被阻塞到这里。由于exec()函数没有返回,因此下面的result()(Accepted or Rejected)函数也就不会被执行。直到对话框关闭,exec()函数返回,此时,我们就可以取得对话框的数据。

需要注意的一点是,如果我们设置 dialog 的属性为WA_DeleteOnClose,那么当对话框关闭时,对象被销毁,我们就不能使用这种办法获取数据了。

实际上,QDialog::exec()是有返回值的,其返回值是QDialog::Accepted或者QDialog::Rejected。一般我们会使用类似下面的代码:

QDialog dialog(this);
if (dialog.exec() == QDialog::Accepted) {
// do something
} else {
// do something else
}


非模态对话框在关闭时可以调用QDialog::accept()或者QDialog::reject()或者更通用的QDialog::done()函数,所以我们可以在这里发出信号。另外,如果找不到合适的信号发出点,我们可以重写QDialog::closeEvent()函数,在这里发出信号。在需要接收数据的窗口(这里是主窗口)连接到这个信号即可。类似的代码片段如下所示:

//!!! Qt 5
// in dialog:
void UserAgeDialog::accept()
{
emit userAgeChanged(newAge); // newAge is an int
QDialog::accept();
}

// in main window:
void MainWindow::showUserAgeDialog()
{
UserAgeDialog *dialog = new UserAgeDialog(this);
connect(dialog, &UserAgeDialog::userAgeChanged, this, &MainWindow::setUserAge);
dialog->show();
}

// ...

void MainWindow::setUserAge(int age)
{
userAge = age;
}


不要担心如果对话框关闭,是不是还能获取到数据。因为 Qt 信号槽的机制保证,在槽函数在调用的时候,我们始终可以使用sender()函数获取到 signal 的发出者。

对话框

QColorDialog:选择颜色;

QFileDialog:选择文件或者目录;

QFontDialog:选择字体;

QInputDialog:允许用户输入一个值,并将其值返回;

QMessageBox:模态对话框,用于显示信息、询问问题等;

QPageSetupDialog:为打印机提供纸张相关的选项;

QPrintDialog:打印机配置;

QPrintPreviewDialog:打印预览;

QProgressDialog:显示操作过程。

标准对话框QMessageBox

QMessageBox用于显示消息提示。我们一般会使用其提供的几个 static 函数:

void about(QWidget * parent, const QString & title, const QString & text):显示关于对话框。这是一个最简单的对话框,其标题是 title,内容是 text,父窗口是 parent。对话框只有一个 OK 按钮。

void aboutQt(QWidget * parent, const QString & title = QString()):显示关于 Qt 对话框。该对话框用于显示有关 Qt 的信息。

StandardButton critical(QWidget * parent, const QString & title, const QString & text, StandardButtons buttons = Ok, StandardButton defaultButton = NoButton):显示严重错误对话框。这个对话框将显示一个红色的错误符号。我们可以通过 buttons 参数指明其显示的按钮。默认情况下只有一个 Ok 按钮,我们可以使用StandardButtons类型指定多种按钮。

StandardButton information(QWidget * parent, const QString & title, const QString & text, StandardButtons buttons = Ok, StandardButton defaultButton = NoButton):QMessageBox::information()函数与QMessageBox::critical()类似,不同之处在于这个对话框提供一个普通信息图标

StandardButton question(QWidget * parent, const QString & title, const QString & text, StandardButtons buttons = StandardButtons( Yes | No ), StandardButton defaultButton = NoButton):QMessageBox::question()函数与QMessageBox::critical()类似,不同之处在于这个对话框提供一个问号图标,并且其显示的按钮是“是”和“否”两个。

StandardButton warning(QWidget * parent, const QString & title, const QString & text, StandardButtons buttons = Ok, StandardButton defaultButton = NoButton):QMessageBox::warning()函数与QMessageBox::critical()类似,不同之处在于这个对话框提供一个黄色叹号图标

QMessageBox类的 static 函数优点是方便使用,缺点也很明显:非常不灵活。我们只能使用简单的几种形式。为了能够定制QMessageBox细节,我们必须使用QMessageBox的属性设置 API。

QMessageBox msgBox;
msgBox.setText(tr("The document has been modified."));
msgBox.setInformativeText(tr("Do you want to save your changes?"));
msgBox.setDetailedText(tr("Differences here..."));
msgBox.setStandardButtons(QMessageBox::Save
| QMessageBox::Discard
| QMessageBox::Cancel);
msgBox.setDefaultButton(QMessageBox::Save);
int ret = msgBox.exec();
switch (ret) {
case QMessageBox::Save:
qDebug() << "Save document!";
break;
case QMessageBox::Discard:
qDebug() << "Discard changes!";
break;
case QMessageBox::Cancel:
qDebug() << "Close document!";
break;
}


文件对话框

打开一个文本文件,并将修改过的文件保存到硬盘

void MainWindow::openFile()
{
QString path = QFileDialog::getOpenFileName(this,
tr("Open File"),
".",
tr("Text Files(*.txt)"));
if(!path.isEmpty()) {
QFile file(path);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
QMessageBox::warning(this, tr("Read File"),
tr("Cannot open file:\n%1").arg(path));
return;
}
QTextStream in(&file);
textEdit->setText(in.readAll());
file.close();
} else {
QMessageBox::warning(this, tr("Path"),
tr("You did not select any file."));
}
}

void MainWindow::saveFile()
{
QString path = QFileDialog::getSaveFileName(this,
tr("Open File"),
".",
tr("Text Files(*.txt)"));
if(!path.isEmpty()) {
QFile file(path);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
QMessageBox::warning(this, tr("Write File"),
tr("Cannot open file:\n%1").arg(path));
return;
}
QTextStream out(&file);
out << textEdit->toPlainText();
file.close();
} else {
QMessageBox::warning(this, tr("Path"),
tr("You did not select any file."));
}
}


在openFile()函数中,我们使用QFileDialog::getOpenFileName()来获取需要打开的文件的路径。这个函数具有一个长长的签名:

QString getOpenFileName(QWidget * parent = 0,
const QString & caption = QString(),
const QString & dir = QString(),
const QString & filter = QString(),
QString * selectedFilter = 0,
Options options = 0)


不过注意,它的所有参数都是可选的,因此在一定程度上说,这个函数也是简单的。这六个参数分别是:

parent:父窗口。我们前面介绍过,Qt 的标准对话框提供静态函数,用于返回一个模态对话框(在一定程度上这就是外观模式的一种体现);

caption:对话框标题;

dir:对话框打开时的默认目录,“.” 代表程序运行目录,“/” 代表当前盘符的根目录(特指 Windows 平台;Linux 平台当然就是根目录),这个参数也可以是平台相关的,比如“C:\”等;

filter:过滤器。我们使用文件对话框可以浏览很多类型的文件,但是,很多时候我们仅希望打开特定类型的文件。比如,文本编辑器希望打开文本文件,图片浏览器希望打开图片文件。过滤器就是用于过滤特定的后缀名。如果我们使用“Image Files(*.jpg *.png)”,则只能显示后缀名是 jpg 或者 png 的文件。如果需要多个过滤器,使用“;;”分割,比如“JPEG Files(*.jpg);;PNG Files(*.png)”;

selectedFilter:默认选择的过滤器;

options:对话框的一些参数设定,比如只显示文件夹等等,它的取值是enum QFileDialog::Option,每个选项可以使用 | 运算组合起来。

事件

事件也就是我们通常说的“事件驱动(event drive)”程序设计的基础概念。事件的出现,使得程序代码不会按照原始的线性顺序执行。

在 Qt 中,事件的概念似乎同信号槽类似。的确如此,一般来说,使用 Qt 组件时,我们并不会把主要精力放在事件上。因为在 Qt 中,我们关心的更多的是事件关联的一个信号。比如,对于QPushButton的鼠标点击,我们不需要关心这个鼠标点击事件,而是关心它的clicked()信号的发出。

但是,Qt 中的事件和信号槽却并不是可以相互替代的。信号由具体的对象发出,然后会马上交给由connect()函数连接的槽进行处理;而对于事件,Qt 使用一个事件队列对所有发出的事件进行维护,当新的事件产生时,会被追加到事件队列的尾部。前一个事件完成后,取出后面的事件进行处理。但是,必要的时候,Qt 的事件也可以不进入事件队列,而是直接处理。信号一旦发出,对应的槽函数一定会被执行。但是,事件则可以使用“事件过滤器”进行过滤,对于有些事件进行额外的处理,另外的事件则不关心。总的来说,如果我们使用组件,我们关心的是信号槽;如果我们自定义组件,我们关心的是事件。因为我们可以通过事件来改变组件的默认操作。比如,如果我们要自定义一个能够响应鼠标事件的EventLabel,我们就需要重写QLabel的鼠标事件,做出我们希望的操作,有可能还得在恰当的时候发出一个类似按钮的clicked()信号(如果我们希望让这个EventLabel能够被其它组件使用)或者其它的信号。

在前面我们也曾经简单提到,Qt 程序需要在main()函数创建一个QCoreApplication对象,然后调用它的exec()函数。这个函数就是开始 Qt 的事件循环。在执行exec()函数之后,程序将进入事件循环来监听应用程序的事件。当事件发生时,Qt 将创建一个事件对象。Qt 中所有事件类都继承于QEvent。在事件对象创建完毕后,Qt 将这个事件对象传递给QObject的event()函数。event()函数并不直接处理事件,而是按照事件对象的类型分派给特定的事件处理函数(event handler)。

在所有组件的父类QWidget中,定义了很多事件处理的回调函数,如keyPressEvent()、keyReleaseEvent()、mouseDoubleClickEvent()、mouseMoveEvent()、mousePressEvent()、mouseReleaseEvent()等。这些函数都是 protected virtual 的,也就是说,我们可以在子类中重新实现这些函数。

QString("[%1, %2]").arg(x, y);


QWidget中有一个mouseTracking属性,该属性用于设置是否追踪鼠标。只有鼠标被追踪时,mouseMoveEvent()才会发出。如果mouseTracking是 false(默认),组件在至少一次鼠标点击之后,才能够被追踪,也就是能够发出mouseMoveEvent()事件。如果mouseTracking为 true,则mouseMoveEvent()直接可以被发出。

事件的接受与忽略

当重写事件回调函数时,时刻注意是否需要通过调用父类的同名函数来确保原有实现仍能进行!

//!!! Qt5
...
textEdit = new QTextEdit(this);
setCentralWidget(textEdit);
connect(textEdit, &QTextEdit::textChanged, [=]() {
this->setWindowModified(true);
});

setWindowTitle("TextPad [*]");
...

void MainWindow::closeEvent(QCloseEvent *event)
{
if (isWindowModified()) {
bool exit = QMessageBox::question(this,
tr("Quit"),
tr("Are you sure to quit this application?"),
QMessageBox::Yes | QMessageBox::No,
QMessageBox::No) == QMessageBox::Yes;
if (exit) {
event->accept();
} else {
event->ignore();
}
} else {
event->accept();
}
}


使用Lambda表达式连接QTextEdit::textChanged()信号,将windowModified设置为true。然后我们需要重写closeEvent()函数。在这个函数中,我们首先判断是不是有过修改,如果有,则弹出询问框,问一下是否要退出。如果用户点击了“Yes”,则接受关闭事件,这个事件所在的操作就是关闭窗口。因此,一旦接受事件,窗口就会被关闭;否则窗口继续保留。当然,如果窗口内容没有被修改,则直接接受事件,关闭窗口。

event()函数

event()函数主要用于事件的分发。event()函数并不直接处理事件,而是将这些事件对象按照它们不同的类型,分发给不同的事件处理器(event handler)。如果你希望在事件分发之前做一些操作,就可以重写这个event()函数了。例如,我们希望在一个QTextEdit组件中监听 tab 键的按下,那么就可以继承QTextEdit,并重写它的event()函数,来达到这个目的:

bool CustomTextEdit::event(QEvent *e)
{
if (e->type() == QEvent::KeyPress) {
QKeyEvent *keyEvent = static_cast<QKeyEvent *>(e);
if (keyEvent->key() == Qt::Key_Tab) {
qDebug() << "You press tab.";
return true;
}
}
return false;
}


我们可以通过使用QEvent::type()函数可以检查事件的实际类型,其返回值是QEvent::Type类型的枚举。

注意,在event()函数中,调用事件对象的accept()和ignore()函数是没有作用的,不会影响到事件的传播。

CustomTextEdit是QTextEdit的一个子类。我们重写了其event()函数,却没有调用父类的同名函数。这样,我们的组件就只能处理 Tab 键,再也无法输入任何文本,也不能响应其它事件,比如鼠标点击之后也不会有光标出现。这是因为我们只处理的KeyPress类型的事件,并且如果不是KeyPress事件,则直接返回 false,鼠标事件根本不会被转发,也就没有了鼠标事件。

//!!! Qt5
bool QObject::event(QEvent *e)
{
switch (e->type()) {
case QEvent::Timer:
timerEvent((QTimerEvent*)e);
break;

case QEvent::ChildAdded:
case QEvent::ChildPolished:
case QEvent::ChildRemoved:
childEvent((QChildEvent*)e);
break;
// ...
default:
if (e->type() >= QEvent::User) {
customEvent(e);
break;
}
return false;
}
return true;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: