Qt Widgets Application 是传统的C++,不适合移动端开发。在Qt5下,QWidget系列从QtGui中被剥离出去,成为单独的QtWidget模块。QT基本模块如下:
Qt Quick Application页面布局(UI)用的QML,但是内部的业务逻辑还是用QT语法。对于传统的桌面程序来说,优先考虑使用 Qt Widgets,若要开发更“现代“的 UI 与高级应用,建议使用 Qt5.x + QML 2.x + QtQuick 2.x。对于移动端开发来说,建议使用 QML,协同 JavaScript,简单快捷、渲染效果更佳、界面更炫酷。不建议使用 Qt Widgets,其显示效果、适应性都不好。
创建Qt项目时可以选择基类,QMainWindow、QWidget、QDialog 三种基类的区别:QMainWindow是一个提供了菜单、工具条的程序主窗口;QWidget是所有图形界面的基类;QDialog是对话框、多用于短时间与用户的交互。QMainWindow和QDialog都是QWidget的子类。
整个项目目录如上图。.ui是用 Qt designer 进行界面设计的文件(界面文件);.cpp编写具体的槽函数(源文件);.h对界面类进行声明(头文件);.qrc是一个xml格式的资源配置文件,与应用程序关联的应用程序由该文件来指定,它用XML记录硬盘上的文件和对应的随意指定的资源名称,应用程序通过资源名称来访问资源。
.pro 文件
pro即为工程文件。
TEMPLATE:这个变量是用来定义你的工程将被编译成什么模式。如果没有这个设置,系统将默认编译为application。app表示这个project将被编译成一个应用程序, lib(生成库的Makefile),subdirs(生成有多级目录管理的Makefile),vcapp,vclib,vcsubdirs(对应Windows 下面VC)。
TARGET:生成最后目标的名字。
DESTDIR:指定生成目标的路径。
CONFIG:告诉qmake应用程序的配置信息。 这个变量可以用来指定是生成debug模式还是release模式,也可以都生成。也可以用来打开编译器警告(warn_on-输出尽可能多的警告信息)或者关闭(warn_off-编译器会输出尽可能少的警告信息)。还可以用来配置要Qt加载库。 例如qt+多线程:CONFIG+=qt thread。
LIBS:加载动态库,引入的lib文件的路径。Release:LIBS+= -L folderPath,release 版引入的lib文件路径。Debug:LIBS+= -L folderPath,Debug 版引入的lib文件路径。
DEPENDPATH:工程的依赖路径。
MOC_DIR:MOC命令将含Q_OBJECT的头文件转换为标准的头文件存放的目录。
OBJECTS_DIR:生成的目标文件存放的目录。
UI_DIR:UIC将ui转化为头文件所存放的目录。
RCC_DIR:RCC将qrc文件转化为头文件所存放的目录。
RC_FILE:程序图标。
main函数中各行作用
#include "QtWidgetsApplication1.h"
#include <QtWidgets/QApplication> // 包含一个应用程序类的文件
int main(int argc, char *argv[]) // main程序入口 argc命令行变量的数量,argv命令行变量的数组
{
QApplication a(argc, argv); // a为应用程序对象,在Qt中应用程序对象有且只有一个
QtWidgetsApplication1 w; // w为窗口对象,默认不会显示,必须要调用show方法显示窗口
w.show();
return a.exec(); // a.exec() 让应用程序进入消息循环
}
运行时出现MSB4018错误
原因是没有设置平台工具集,在项目属性页设置。
QT中的对象树
当创建对象在堆区的时候,如果指定的父亲是QObject派生下来的类或者QObject子类派生下来的类,可以不用管理释放的操作,对象将会放入对象树中。一定程度上简化了内存回收机制。当父类对象析构的时候,子类对象也会被析构。如下图,创建时一层一层往下创建,析构时一层一层往上释放。在QT中,尽量在构造的时候就指定parent对象。
信号和槽
connect(信号的发送者,发送的具体信号,信号的接受者,信号的处理(槽))
参数2:函数的地址,可自定义
参数4:处理的槽函数,可自定义
connect(btn, &QPushButton::clicked, this, &MyApplication0507::close); // 点击按钮即关闭
信号槽松散耦合,信号发送端和接收端本身是没有关联的。在一个类的头文件里:
#pragma once
#pragma execution_character_set("utf-8") // 在有输出的页面加上,可以解决中文乱码问题
#include <QObject>
class teacher : public QObject
{
Q_OBJECT
public:
explicit teacher(QObject *parent=0);
signals:
// 自定义信号写到signals下,返回值是void,只需要声明不需要在.cpp实现,可以有参数,可以重载
// 信号可以连接信号,一个信号可以连接多个槽函数,多个信号可以连接一个槽函数
void hungry();
public slots :
// 早期Qt版本槽函数必须要写到public slots下,高级版本可以写到public或者全局下。需要声明也需要在.cpp实现,可有参数也可以重载
// 信号和槽函数的类型必须一一对应
// 信号参数的个数可以多于槽函数参数的个数,但是类型也需要一一对应
void treat();
};
Lambda表达式
lambda 来源于函数式编程的概念,也是现代编程语言的一个特点。lambda 表达式定义了一个匿名函数,并且可以捕获一定范围内的变量。lambda 表达式的语法形式可简单归纳如下:
[ capture ] ( params ) opt -> ret { body; };
例如:
f = [](int a) -> int { return a + 1; };
其中 capture 是捕获列表,params 是参数表,opt 是函数选项,ret 是返回值类型,body是函数体。可以通过捕获列表捕获一定范围内的变量:
- [] 不捕获任何变量。
- [&] 捕获外部作用域中所有变量,并作为引用在函数体中使用(按引用捕获)。
- [=] 捕获外部作用域中所有变量,并作为副本在函数体中使用(按值捕获)。
- [=,&foo] 按值捕获外部作用域中所有变量,并按引用捕获 foo 变量。
- [bar] 按值捕获 bar 变量,同时不捕获其他变量。
- [this] 捕获当前类中的 this 指针,让 lambda 表达式拥有和当前类成员函数同样的访问权限。如果已经使用了 & 或者 =,就默认添加此选项。捕获 this 的目的是可以在 lamda 中使用当前类的成员函数和成员变量。
默认状态下 lambda 表达式无法修改通过复制方式捕获的外部变量。如果希望修改这些变量的话,我们需要使用引用方式进行捕获。基本用法:
class A
{
public:
int i_ = 0;
void func(int x, int y)
{
auto x1 = []{ return i_; }; // error,没有捕获外部变量
auto x2 = [=]{ return i_ + x + y; }; // OK,捕获所有外部变量
auto x3 = [&]{ return i_ + x + y; }; // OK,捕获所有外部变量
auto x4 = [this]{ return i_; }; // OK,捕获this指针
auto x5 = [this]{ return i_ + x + y; }; // error,没有捕获x、y
auto x6 = [this, x, y]{ return i_ + x + y; }; // OK,捕获this指针、x、y
auto x7 = [this]{ return i_++; }; // OK,捕获this指针,并修改成员的值
}
};
int a = 0, b = 1;
auto f1 = []{ return a; }; // error,没有捕获外部变量
auto f2 = [&]{ return a++; }; // OK,捕获所有外部变量,并对a执行自加运算
auto f3 = [=]{ return a; }; // OK,捕获所有外部变量,并返回a
auto f4 = [=]{ return a++; }; // error,a是以复制方式捕获的,无法修改
auto f5 = [a]{ return a + b; }; // error,没有捕获变量b
auto f6 = [a, &b]{ return a + (b++); }; // OK,捕获a和b的引用,并对b做自加运算
auto f7 = [=, &b]{ return a + (b++); }; // OK,捕获所有外部变量和b的引用,并对b做自加运算
一个容易出错的细节是关于 lambda 表达式的延迟调用的:
int a = 0;
auto f = [=]{ return a; }; // 按值捕获外部变量
a += 1; // a被修改了
cout << f() << endl; // 输出?
在这个例子中,lambda 表达式按值捕获了所有外部变量。在捕获的一瞬间,a 的值就已经被复制到 f 中了。之后 a 被修改,但此时 f 中存储的 a 仍然还是捕获时的值,因此,最终输出结果是 0。
QMainWindow
是一个为用户提供主窗口程序的类,包含一个菜单栏(Menu bar)、多个工具栏(tool bars)、多个锚接部件(dock widgets)、一个状态栏(status bar)以及一个中心部件(central widget)。
QMainWindow实例:
QMainWindow0508::QMainWindow0508(QWidget *parent)
: QMainWindow(parent)
{
ui.setupUi(this);
resize(600, 400);
// 菜单栏创建,只能有一个
QMenuBar *bar = menuBar();
// 将菜单栏放入窗口中
setMenuBar(bar);
// 创建菜单
QMenu *fileMenu = bar->addMenu("文件");
QMenu *editMenu = bar->addMenu("编辑");
// 创建菜单项
QAction *newAction = fileMenu->addAction("新建");
// 添加分割线
fileMenu->addSeparator();
QAction *openAction = fileMenu->addAction("打开");
// 工具栏 可以有多个
QToolBar *toolBar = new QToolBar(this);
addToolBar(Qt::LeftToolBarArea, toolBar);
toolBar->setAllowedAreas(Qt::LeftToolBarArea | Qt::RightToolBarArea);
toolBar->setFloatable(false);
toolBar->addAction(newAction);
//toolBar->addSeparator();
toolBar->addAction(openAction);
QPushButton *btn = new QPushButton("hh", this);
toolBar->addWidget(btn);
// 状态栏,最多也只能有一个
QStatusBar * stBar = statusBar();
setStatusBar(stBar);
// 放标签控件
QLabel * label1 = new QLabel("提示信息", this);
stBar->addWidget(label1); // addWidget默认从左侧提示
QLabel * label2 = new QLabel("右侧提示信息", this);
stBar->addPermanentWidget(label2); // addPermanentWidget默认从右侧提示
// 铆接部件(浮动窗口),可以有多个
QDockWidget * dockWidget = new QDockWidget("浮动", this);
addDockWidget(Qt::BottomDockWidgetArea, dockWidget);
// 设置中心部件,只能一个
QTextEdit *edit = new QTextEdit(this);
setCentralWidget(edit);
}
菜单栏、状态栏和中心部件都只能有一个,故以 set...() 完成窗口的嵌入。菜单项、工具栏、铆接部件以及标签控件都可以有多个,故以 add...() 完成嵌入。
对话框
实现功能:点击新建按钮,弹出一个对话框,对话框分为模态对话框(不可以对其他窗口进行操作)和非模态对话框(可以对其他窗口进行操作)。其中ui.actionNew是一个按钮,这里使用 connect(对象,信号,槽函数) 这个重载方法。
connect(ui.actionNew, &QAction::triggered, [=]() {
// 模态对话框
QDialog dlg(this);
dlg.resize(200, 100);
dlg.exec();
// 非模态对话框,创建在堆区让他一直存活
QDialog *dlg2 = new QDialog(this);
dlg2->setAttribute(Qt::WA_DeleteOnClose);
dlg2->resize(200, 100);
dlg2->show();
});
常见的有错误对话框、信息对话框、问题对话框和警告对话框:
// 错误对话框
QMessageBox::critical(this, "critical", "错误");
// 信息对话框
QMessageBox::information(this, "info", "信息");
// 问题对话框
if (QMessageBox::Save == QMessageBox::question(this, "ques", "提问", QMessageBox::Save | QMessageBox::Cancel))
{
qDebug() << "保存";
}
// 警告对话框
QMessageBox::warning(this, "warn", "警告");
除此之外还有颜色对话框 QColorDialog::getColor、文件对话框 QFileDialog::getOpenFileName(father,title,path,filter) 和 字体对话框 QFontDialog::getFont 。
控件
- QPushButton,常用按钮
- QToolButton,工具按钮,用于显示图片,如想显示文字则需修改风格toolButtonStyle
- RadioButton,单选按钮,设置默认 ui.radioButton->setChecked(true);
- CheckBox,多选按钮,监听状态 &QCheckBox::stateChanged ,2为选中,1为半选中,0为未选
- QListWidget,列表容器, QListWidgetItem 为一行内容,设置居中方式 item->setTextAlignment(Qt::AlignHCenter) ,可以用addlists一次性添加数行内容
windowLayout::windowLayout(QWidget *parent)
: QMainWindow(parent)
{
ui.setupUi(this);
ui.radioButton->setChecked(true); // 设置默认
connect(ui.radioButton, &QRadioButton::clicked, [=]() { // 若选中按钮,打印相关信息
qDebug() << "选中男的";
});
connect(ui.radioButton_2, &QRadioButton::clicked, [=]() {
qDebug() << "选中女的";
});
connect(ui.checkBox, &QCheckBox::stateChanged, [=](int state) { // 监听状态
qDebug() << state;
});
QListWidgetItem * item = new QListWidgetItem("锄禾日当午"); // 利用listWidget写诗
ui.listWidget->addItem(item); // 将一行诗放入到listWidget中
item->setTextAlignment(Qt::AlignHCenter);
QStringList list;
list << "锄禾日当午" << "汗滴禾下土" << "谁之盘中餐" << "粒粒皆辛苦";
ui.listWidget->addItems(list); // 利用addItems将数行内容放入listWidget中
}
- QTreeWidget,树控件
QListWidgetItem * item = new QListWidgetItem("锄禾日当午"); // 利用listWidget写诗
ui.listWidget->addItem(item); // 将一行诗放入到listWidget中
item->setTextAlignment(Qt::AlignHCenter);
QStringList list;
list << "锄禾日当午" << "汗滴禾下土" << "谁之盘中餐" << "粒粒皆辛苦";
ui.listWidget->addItems(list); // 利用addItems将数行内容放入listWidget中
ui.treeWidget->setHeaderLabels(QStringList() << "英雄" << "英雄介绍"); // 设置水平头
QTreeWidgetItem * strengthItem = new QTreeWidgetItem(QStringList() << "力量"); // 添加顶层节点
QTreeWidgetItem * agileItem = new QTreeWidgetItem(QStringList() << "敏捷");
QTreeWidgetItem * intelligenceItem = new QTreeWidgetItem(QStringList() << "智力");
ui.treeWidget->addTopLevelItem(strengthItem); // 添加顶层节点
ui.treeWidget->addTopLevelItem(agileItem);
ui.treeWidget->addTopLevelItem(intelligenceItem);
QStringList hero1; // 追加子节点
hero1 << "刚被猪" << "前排坦克";
QTreeWidgetItem *l1 = new QTreeWidgetItem(hero1);
strengthItem->addChild(l1);
- QTableWidget,列表控件,设置列数、设置行数、设置水平表头、将表格中填入数据
ui.tableWidget->setColumnCount(3); // 设置列数
ui.tableWidget->setHorizontalHeaderLabels(QStringList() << "姓名" << "性别" << "年龄"); // 设置水平表头
ui.tableWidget->setRowCount(5); // 设置行数
QStringList nameList;
nameList << "亚瑟" << "赵云" << "张飞" << "关羽" << "花木兰";
QStringList sexList;
sexList << "男" << "男" << "男" << "男" << "男";
for (int i = 0; i < 5; i++)
{
int col = 0;
ui.tableWidget->setItem(i, col++, new QTableWidgetItem(nameList[i]));
ui.tableWidget->setItem(i, col++, new QTableWidgetItem(sexList.at(i)));
ui.tableWidget->setItem(i, col++, new QTableWidgetItem(QString::number(i+18))); // int转QString
}
- stackedWidget,栈控件
- comboBox,下拉框
- QLable,显示图片,动图
https://blog.51cto.com/u_9291927/1879125
自定义控件
设置一个自定义控件,实现滑动 Slider 时 spinBox 里的值也跟着改变,同时改变 spinBox 时 Slider 也实现相应的滑动。点击获取当前值按钮时打印当前 Slider/spinBox 的值,点击设置为一半按钮时 spinBox 设置为50,Slider 滑动到正*。
添加一个 Qt Widget 类取名 SmallWidget,设置 Slider 和 spinBox 控件,在主界面上拉一个 Widget 将其提升为 SmallWidget,将新创建的类的界面设置为主界面的一个子界面。在 SmallWidget 中实现信号槽连接,将 spinBox 改变的值设置到 Slider 上。因为 QSpinBox 中的 valueChanged 信号有重载函数,故需要函数指针的方式将要实现的重载函数地址传到 connect 中。
void(QSpinBox:: * SpinB)(int) = &QSpinBox::valueChanged;
connect(ui.spinBox, SpinB, ui.horizontalSlider, &QSlider::setValue);
同样将 Slider 上值的改变设置到 spinBox 中,QSlider 没有信号函数,将其父类 QAbstractSlider 的信号函数 valueChanged 拿来用,处理的槽函数为 QSpinBox 的槽函数。
connect(ui.horizontalSlider, &QSlider::valueChanged, ui.spinBox, &QSpinBox::setValue);
在 SmallWidget 要实现获取当前值 getNum 和设置为一半 setNum 这两个函数,这样在主界面的两个按钮可以通过信号槽机制来调用,实现相应的功能。
void SmallWidget::setNum(int num)
{
ui.spinBox->setValue(num);
}
int SmallWidget::getNum()
{
return ui.horizontalSlider->value();
}
在主界面上用信号槽实现按钮与子界面的互动,子界面的 id 为 widget。
connect(ui.pushButton, &QPushButton::clicked, [=]() {
qDebug() << ui.widget->getNum();
});
connect(ui.pushButton_2, &QPushButton::clicked, [=]() {
ui.widget->setNum(50);
});
鼠标事件
- 鼠标进入事件 enterEvent( QEvent )
- 鼠标离开事件 leaveEvent
- 鼠标按下 mousePressEvent( QMouseEvent )
- 鼠标释放 mouseReleaseEven
- 鼠标移动 mouseMoveEvent
实现一个简单的需求:对于界面上的一个 QLabel 控件,鼠标进入、离开、按下、移动、释放事件分别实现打印相应的信息。
在项目中新加一个 Qt 的类名为 myLabel,基类设置为 QWidget ,因为控件为 QLabel 类型,所以新加类的构造函数需要改变。
myLabel.cpp:
myLabel::myLabel(QWidget * parent) : QLabel(parent) { // 1
}
myLabel.h:
#include <QLabel> // 2
class myLabel : public QLabel { // 3
Q_OBJECT
...
};
在 UI 界面将该 QLabel 控件提升为 myLabel 类,分别实现对应事件的函数。鼠标移动事件与其余事件有所不同,因为是持续性的动作,所以需要用位操作符 & 来判断持续性的状态。组合按键函数 buttons() 可判断鼠标左右键和滚轮的按下情况。
void myLabel::mousePressEvent(QMouseEvent *ev)
{
if (ev->button() == Qt::LeftButton)
{
QString str = QString("鼠标左键按下 x=%1 y=%2 ").arg(ev->x()).arg(ev->y());
qDebug() << str;
}
}
void myLabel::mouseReleaseEvent(QMouseEvent *ev)
{
qDebug() << "鼠标释放";
}
void myLabel::mouseMoveEvent(QMouseEvent *ev) // 移动是持续性的过程,所以需要用位操作符&来判断持续性的状态
{
if (ev->buttons() & Qt::LeftButton)
{
QString str1 = QString("鼠标左键按下移动 x=%1 y=%2 ").arg(ev->x()).arg(ev->y());
qDebug() << str1;
}
}
void myLabel::enterEvent(QEvent *Event)
{
qDebug() << "鼠标进入";
}
void myLabel::leaveEvent(QEvent *)
{
qDebug() << "鼠标离开";
}
如果要实现鼠标移动到 Label 上不用按下就打印相关的信息,就在构造函数里加上 setMouseTracking(true),格式化字符串QString("%1 %2").arg(ev->x()).arg(ev->y()) 。
定时器Timer
有两种实现,第一种利用事件 timerEvent(),需要在构造函数里启动定时器并设置定时时间 startTimer( 毫秒 ),该函数的返回值是定时器 ID。第二种实现利用定时器类 QTimer,创建定时器对象并将其挂载到对象树再启动定时器,可以利用 &QPushButton::clicked 事件进行监听,来实现想要的逻辑。
Timer0511::Timer0511(QWidget *parent)
: QWidget(parent)
{
ui.setupUi(this);
// 第一种方式
id1 = startTimer(1000);
id2 = startTimer(2000);
// 第二种方式
QTimer *timer = new QTimer(this);
timer->start(500);
connect(timer, &QTimer::timeout, [=]() {
static int num = 1; // static 的性质:
ui.label_3->setText(QString::number(num++)); // 局部特性: 作用范围仅限于本函数,所以这里跟下面第一种方式 id1 里的变量不冲突
}); // 静态特性:存储在静态区,函数调用结束后不孝顺而保留原值。在下一次调用时,保留上一次调用结束时的值
connect(ui.pushButton, &QPushButton::clicked, [=]() {
timer->stop();
});
connect(ui.pushButton_2, &QPushButton::clicked, [=]() {
timer->start();
});
}
// 第一种方式
void Timer0511::timerEvent(QTimerEvent *ev)
{
if( ev->timerId()==id1 )
{
static int num = 1;
ui.label->setText(QString::number(num++));
}
if (ev->timerId() == id2)
{
static int num2 = 1;
ui.label_2->setText(QString::number(num2++));
}
}
事件分发器
以上面的鼠标事件为例,鼠标按下时先进入事件分发器 bool event(QEvent *e),返回值类型为bool,该分发器进行一定的判断,当然也可以实现自己的逻辑,判断是否向下分发到 mousePressEvent( QMouseEvent ),若在此进行拦截让分发器返回 true 则不会继续向下分发。
bool myLabel::event(QEvent *e)
{
if (e->type() == QEvent::MouseButtonPress)
{
QMouseEvent *ev = static_cast<QMouseEvent *>(e); // static_cast用于良性转换,这样的转换风险较低,一般不会发生什么意外
QString str = QString("Event函数中鼠标左键按下 x=%1 y=%2 ").arg(ev->x()).arg(ev->y());
qDebug() << str;
return true; // return true则表示用户进行处理,不会继续向下分发,不建议
}
// 其他事件交给父类处理
return QLabel::event(e);
}
事件过滤器
在事件到达事件分发器 bool event(QEvent *e) 之前,还可以通过事件过滤器再进行一次拦截。其使用有两个步骤,给控件安装事件过滤器再重写 eventfilter 事件。
.h:
ui.label->installEventFilter(this); // 1
.cpp:
bool Event0511::eventFilter(QObject *obj, QEvent *e) // 2
{
if (obj == ui.label)
{
if (e->type() == QEvent::MouseButtonPress)
{
QMouseEvent *ev = static_cast<QMouseEvent *>(e); // static_cast用于良性转换,这样的转换风险较低,一般不会发生什么意外
QString str = QString("过滤器中鼠标左键按下 x=%1 y=%2 ").arg(ev->x()).arg(ev->y());
qDebug() << str;
return true; // return true则表示用户进行处理,不会继续向下分发,不建议
}
// 其他事件交给父类处理
return QWidget::event(e);
}
}
绘图设备
绘图设备是指继承 QPaintDevice 的子类,一共提供了四个这样的类,分别为 QPixmap、QBitmap、QImage 和 QPicture。
- QPixmap 专门为图像在屏幕上的显示做了优化。
- QBitmap 是 QPixmap 的一个子类,色深为1,可以使用 QPixmap 的 isQBitmap() 来确定这个QPixmap 是不是一个 QBitmap。
- QImage 专门为图像像素级访问做了优化。
- QPicture 则可以记录和重现 QPainter 的各条指令。
QFile文件读写
主要用到两个函数:
QString fileName = QFileDialog::getSaveFileName(this, tr("Save File"),
"/home/jana/untitled.png",
tr("Images (*.png *.xpm *.jpg)"));
QString fileName = QFileDialog::getOpenFileName(this, tr("Open File"),
"/home",
tr("Images (*.png *.xpm *.jpg)"));
其中参数的作用为设置父类界面,对话框的提示,默认路径以及文件过滤器。
tr()函数:
QString text1 = QObject::tr("hello"); QString text2 = QString("hello");tr是用来实现国际化,如果你为这个程序提供了中文翻译包(其中hello被翻译成中文“你好”),那么text1的内容将是中文“你好”;如果你为程序提供且使用日文翻译包,那么text1的内容将是日文。