【无标题】

几种简单的控件

QWidget

QWidget是所有用户界面的基类,其他几乎所有控件都直接或间接继承自QWidget。

QWidget是用户界面的基本单元:它从窗口系统中接收键盘、鼠标等事件,并在屏幕上绘制自己,每个widget在屏幕上都是一个矩形区域。

QWidget的构造函数:

QWidget::QWidget(QWidget* parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags())

参数2:Qt::WindowFlags f = {}设置窗口标识,默认适用于大多数widget,也可通过函数void setWindowFlags(Qt::WindowFlags type)进行设置。如:

//设置窗口无边框
setWindowFlags(Qt::FramelessWindowHint);
//设置控件大小和位置
void QWidget::setGeometry(int x, int y, int w, int h);
//获取位置
QPoint QWidget::pos() const;
//...

QLabel

最简单的文本显示控件

#include 
//...
QLabel* pLabel = new QLabel(this);    //创建一个QLabel对象
pLabel->setText("Hello,Qt");
pLabel->setAlignment(Qt::AlignRight);    //设置文字居右对齐
pLabel->setGeometry(10, 10, 200, 20);

注:创建QLabel对象时,需指定父对象。

QPushButton

#include 
//...
QPushButton* pbtnOk = new QPushButton(this);    //创建按钮
pbtnOk->setText("Ok");

QLineEdit

单行文本输入框,用于文本的输入或编辑。

#include 
//...
QLineEdit* pLineEdit = new QLineEdit(this);    //创建对象
//设置占位文字,即输入框为空时显示的提示文字
pLineEdit->setPlaceholderText("请输入姓名");
//设置最大输入长度
pLineEdit->maxLength(5);

QMessageBox

弹出式的消息提示框。一般调用QMessageBox类的静态函数。

int res = QMessageBox::warning(this, "删除", "确定删除?");
if(res == QMessageBox::Ok)
{
    //...
}
else
{
    //...
}

信号槽

该机制类似于设计模式中的“观察者模式”,发布者发布信息,订阅者接收信息。

信号槽连接(Qt4风格)语法如下:

connect(sender, SIGNAL(signal), receiver, SLOT(slot));
//点击按钮,关闭窗口[cilcked()是类QPushBUtton内置的信号,close()是类QWidget内置的槽函数]
connect(pBtnClose, SIGNAL(clicked()), pWgt, SLOT(close()));

自定义信号槽

自定义信号使用宏signals或Q_SIGNALS,自定义槽函数使用宏slots或Q_SLOTS。如:

class Counter:public QObject
{
    Q_OBJECT

public:
    Counter(){m_value = 0;}
    int value() const {return m_value;}

signals:
    void valueChanged(int newValue);

public slots:
    void setValue(int newValue);
};

void Counter::setValue(int newValue)
{
    if(value != m_value)
    {
        m_value = newValue;
        //发出信号
        emit valueChanged(newValue);
    }
}

//...
//创建Counter对象
Counter a,b;
QOject::connect(&a, SIGNAL(valueChanged(int)), &b, SLOT(setValue(int)));
a.setValue(12);
b.setValue(38);

可根据返回值判断信号是否连接成功

bool res = connect(sender, SLGNAL(signal), recever, SLOT(slot));

连接成功返回true,失败返回false。

参数说明

一个信号可以连接多个槽

connect(slider, SIGNAL(valueChanged(int)), spinBox, SLOT(setValue(int)));
connect(slider, SIGNAL(valueChanged(int)), this, SLOT(updateStatusBarIndicator(int)));

注:信号发出时,会以不确定的顺序调用这些槽函数。

多个信号可以连接同一个槽函数

connect(lcd, SIGNAL(overflow()), this, SLOT(handleMathError()));
connect(calculator, SIGNAL(divisionByZero()), this, SLOT(handleMathError()));

唯一连接

可以通过 QObject::connect() 的第 5 个参数,控制信号槽属性,⽐如只能连接⼀次。
connect(sender, SIGNAL(signal), receiver, SLOT(slot), Qt::UniqueConnection);

信号槽重复连接,加上 Qt::UniqueConnection,当发出信号时,槽函数只会调⽤⼀次;否则会多次调⽤槽函数。

移除连接

disconnect(lcd, SIGNAL(overflow()), this, SLOT(handleMathError()));
这⾥是⼿动移除连接。当对象被删除时, Qt 会⾃动移除和这个对象有关的所有连接。

阻塞信号发出

通过blockSignals()可以阻塞信号的发出。

btn->blockSignals(true);    //阻塞信号
btn->blockSignals(flase);   //解除阻塞

总结

1.有信号或槽函数的类必须加上 Q_OBJECT 宏;
2.建⽴连接时,信号和槽函数不要写上参数名称;
3. 不要重复连接,否则发出信号后,槽函数会被重复多次调⽤;
4. 信号须与发出信号的对象对应 同样地 槽函数须与定义槽函数的类对象对应。

作业1

现在,我们将投⼊实战项⽬ —— 设计⼀个思维导图软件,每天完成其中⼀部分,最终效果如下:
【无标题】_第1张图片
⾸先需要实现软件的登录功能,设计⼀个如下图所示的登录窗⼝:
【无标题】_第2张图片
⽤户输⼊⽤户名、密码之后,点击登录按钮。
登录成功 ( 即⽤户名、密码完全匹配 ) 则显示关闭登录窗⼝,并显示主窗⼝,在窗⼝右上⻆显示⽤户昵称;
登录失败则弹出错误提示。
【无标题】_第3张图片
假设已注册的所有⽤户数据如下:
⽤户名: admin
密码: admin
昵称:恒歌科技
⽤户名: anqi
密码: angelababy
昵称:安琪拉
其它要求:
  1. 密码输⼊框不显示明⽂密码
  2. 主窗⼝⽆边框,并在右上⻆添加按钮,实现窗⼝的“最⼩化、最⼤化、恢复、关闭”等功能

帮助与提示

QLineEdit 提供接⼝设置⽂字的回显模式 (echoMode)
弹出的消息窗⼝使⽤ QMessageBox
窗⼝设置⽆边框之后⽆法通过⿏标拖动了,这是正常现象
建议采⽤ MVC 的⽅式,将界⾯、⽤户数据、登录的处理逻辑分开
编码时请按照规范对类、函数、变量命名,养成好习惯

布局管理

布局是为了让子控件能够自动调整大小和位置,从而保证能够充分利用空间。作用:

1.调整子控件位置

2.感知窗口大小的改变;

3.当字体、文字、控件显示、隐藏时,能够自动更新窗口内控件的大小和位置。

分类

布局类都是继承自QLayout

常见的布局有:1.水平布局QHBoxLayout 2.垂直布局QVBoxLayout 3.层叠布局QStackedLayout 4.栅格布局QGridLayout

常用接口

//1.设置外边距
QLayout::setMargin(int margin);
//分别设置左、右、上、下的外边距
QLayout::setContentMargins(int left, int top, int right, int bottom);
QLayout::setContentMargins(const QMargins& margins);

//2.设置间距
setSpacing(int);

//3.添加弹簧
addStretch(); //添加伸缩弹簧,理解为占位

//4.设置拉伸比例
QBoxLayout::setStretch(int index, int stretch);
QBoxLayout::setStretchFactor(QWidget* widget, int stretch);

基本控件

QWidget: 所有其他控件的基类

QLabel: 文本显示框

QPushButton: 按钮

QMenu: 菜单

QLineEdit: 单行文本输入框

QTextEdit: 多行文本输入框

QTextBrowser: 文本显示控件

QCheckBox: 多选

QRadioButton: 单选

QComboBox: 下拉选择控件

QSpinBox: 

QGroupBox: 

QTimeEdit: 时间显示

QDateTimeEdit: 

QSlider: 

QProgressBar: 进度条

QTableWidget: 

QStackedWidget: 

1. 熟悉每个控件常用接口

2. 熟悉每个控件提供的信号

3。查找接口及其用法

创建和使用动态库

静态库与动态库的区别

编译一个程序,主要经过以下几个阶段:

1. 预编译:替换宏定义等;

2. 编译:将源码转为汇编码;

3. 汇编:将汇编码转为机器码(字节码),生成目标文件;

4. 链接:将目标文件转为可执行文件

静态库、动态库的区别主要在链接阶段如何处理,链接成可执行程序。在链接阶段,静态库会将汇编生成的目标文件.o与引用的库一起打包到可执行文件中;而动态库是在程序运行时才被载入。

动态库

Windows系统中,动态库(Dynamic-link library)文件大多以dll为后缀。

优点:

1. 节省空间。不同程序调用相同的库,内存中只需要有一份该共享库即可;

2. 便于部署更新。用户只需要更新动态库,进行增量更新,而不必重新编译整个应用程序。

创建动态库

创建动态库通常需要先定义导出宏,然后使用导出宏修饰需要导出的类/函数。

使用动态库

使用动态库通常需要三个方面:头文件、导入库lib文件、动态库dll文件

作业2

需求

  1. 引⼊思维导图库 MindMap ,在 VS 的⼯程属性中配置好相关路径和⽂件名
  2. 查看思维导图库提供的相关接⼝,将思维导图加到主界⾯下⽅,并使⽤布局调整标题栏和主界⾯的结构
  3. 调⽤思维导图库的接⼝,使⽤模拟数据初始化思维导图:
【无标题】_第4张图片

帮助与提示

从思维导图的接⼝ SimDataManager::getSimData 获取模拟数据,它的返回值是⼀个结构体
StMindData

列表、树、表格控件

QListWidget

//创建QListWidget
QListWidget *pListWidget = new QListWidget(this);
//添加项
QString strBook0 = "The Call of the Wild";
QString strBook1 = "Sophie's Word";
pListWidget->addItem(strBook0);
pListWidget->addItem(strBook1);
//删除项
int row = pListWidget->currentRow(); //当前行行号
if(-1 != row)
{
    QListWidgetItem* pItem = pListWidget->takeItem(row);
    delete pItem;
    pItem = nullptr;
}
//重命名
int iRow = pListWidget->currentRow();
if(-1 != iRow)
{
    QListWidgetItem* pItem = pListWidget->takeItem(iRow);
    pItem->setText("The Moon and Sixpence");
}
//清空
pListWidget->clear();

此外,QListWidget还提供很多有用的信号和接口来处理用户操作,如currentItemChanged()信号会在当前选中的Item发生改变时发出。

QTreeWidget

//创建树
QTreeWidget* pTreeWgt = new QTreeWidget(this);
//添加根节点
QTreeWidgetItem* pRootItem = new QTreeWidgetItem(pTreeWgt);
//添加树节点的子节点
QTreeWidgetItem* pChildItem = new QTreeWidgetItem();
pRootItem->addChild(pChildItem);

QTableWidget

//创建表格
QTableWidget pTableWgt = new QTableWidget(this);
//设置行数、列数
pTableWgt->setRowCount(10);
pTableWgt->setColumnCount(3);
//设置水平头标签
QListItem tableHeader;
tableHeader<<"#"<<"Name"<<"Text";
pTableWgt->setHorizontalHeaderLabels(tableHeader);
//隐藏垂直表头
pTableWgt->verticalHeader()->setVisible(false);
//禁止编辑
pTableWgt->setEditTriggers(QAbstractItemView::NoEditTriggers);
//设置选中模式为选中整行
pTableWgt->setSelectionBehavior(QAbstractItemView::SelectRows);
//设置选中模式为只能选中一行
pTableWgt->setSelectionMode(QAbstractItemView::SingleSelection);
//不显示网格线
pTableWgt->setShowGrid(false);

注:所有这些控件,可直接通过代码编写,也可在界面及逆行编辑数据、查看属性。

自定义控件

即通过封装的方式,将已有的简单控件组装成复杂的控件,实现更丰富的界面效果。

封装

提升

复用

Qt程序编译流程

在构建 Qt 程序的过程中,打开编译器的编译输出窗⼝,可以看到以下内容:
uic TestProject.ui
rcc TestProject.qrc
moc TestProject.h
3 条命令分别⽤于解析 ui ⽂件、 qrc ⽂件,以及头⽂件。
Qt 的安装⽬录下,可以找到 uic rcc moc 3 个⼯具。
uic User Interface Compiler ,将界⾯⽂件( .ui )⽣成 C++ 代码。
rcc Resource Compiler ,将 qrc ⽂件⽣成 C++ 代码。
moc Meta-Object Compiler moc ⼯具读取头⽂件,如果找到⼀个或多个包含 Q_OBJECT 宏的类,它 将为这些类⽣成⼀个包含元对象(meta-object )代码的 C++ 源⽂件。元对象代码⽤于信号槽机制、运⾏时类型识别、动态属性等⽅⾯。(这就是⾃定义信号槽必须添加 Q_OBJECT 宏的原因)
结果:
uic 根据 TestProject.ui ⽣成 ui_TestProject.h ⽂件;
moc 根据 TestProject.h ⽣成 moc_TestProject.cpp ⽂件。
这些⽣成后的⽂件,都可以在⼯程的⽣成⽬录下找到。
全部解析成 C++ 编译器可识别的⽂件之后,接下来便按照”预处理 - 编译 - 汇编 - 链接“的过程进⾏编译,最终⽣成可执⾏⽂件。

ui文件

ui界面文件,使用Qt Designer打开,编辑控件和布局。本质是xml文件。其生成的代码也是在窗口中创建控件、调整位置、设置布局。

对于动态变化较大的控件,如通过配置文件创建不同数量的按钮,只用Designer是无法实现的,仍需要通过编码来做。

qrc文件

qrc资源文件,用于添加图片、样式表、配置文本文件等资源。图片添加完成后,可获得资源路径,可直接在程序中使用。qrc本质也是一个xml文件。

qrc ⽂件中添加的资源是写⼊到程序中的(可执⾏⽂件或者库⽂件中),所以在程序打包时不⽤考虑资源的路径问题。如果使⽤相对路径(如 images/bg-blue.png)或者绝对路径(如E:/images/bg-blue.png),那 么打包时需要将资源放到合适的路径下,很容易出现路径错误找不到图⽚等问题。建议在开发中使⽤ qrc ⽂件来管理资源。

作业3

在上⼀节我们在窗⼝中添加了思维导图,并使⽤模拟数据进⾏初始化。
下⾯,我们将使⽤ QTreeWidget 展示思维导图的数据。
创建⼀个界⾯,上⽅是搜索框,下⽅是⼀个 QTreeWidget
获取思维导图的数据,并初始化 QTreeWidget 。在上⽅搜索框中输⼊⽂字,⽤于搜索匹配的树节点。
【无标题】_第5张图片
接下来,将该界⾯放到主界⾯左侧,叠加在思维导图上⾯,参考下图(暂不考虑样式)
【无标题】_第6张图片

帮助与提示

  1. 思维导图每⼀项与树的每个节点对应,树节点中需保存名称、类型(根节点或叶⼦节点)、对应思维导图那⼀项的指针
  2. 树的搜索:匹配到的节点显示,不匹配的节点隐藏;搜索框为空,则显示所有节点;
  3. 树的层级不⼀定只有3

容器

容器的遍历

//以 QList 为例,Qt 容器的遍历有很多种写法。
QList list;
list << "A" << "B" << "C" << "D";
//Java ⻛格的迭代器
QListIterator i(list);
while (i.hasNext())
{
 qDebug() << i.next();
}
//STL ⻛格的迭代器
QList::iterator i;
for (i = list.begin(); i != list.end(); ++i)
{
 *i = (*i).toLower();
}
//foreach 关键字
foreach (const QString &str, list)
{
    if (str.isEmpty())
     {
         break;
     }
     qDebug() << str;
}

容器算法

qSort: 排序

qCopy: 复制

qFill: 覆盖

qFind: 查找

样式表

Qt中的样式表(Qt Style Sheet,qss)用来设计界面样式和元素布局。

Qt 中,调整界⾯的样式有很多种⽅法,最常⽤、最基础的就是样式表。对于复杂的样式,需要⼦类化QStyle,实现更定制化的效果。
样式表学习,最好是参考 Qt 官⽅⽂档: https://doc.qt.io/qt-5/stylesheet.html

引入样式

通常采⽤ 2 种⽅式引⼊样式:
通过 Qt Designer ui ⽂件中设置样式
读取⽂件设置样式 ⽐如:先将样式写进⽂件 style.qss 中,然后读取⽂件内容
QFile file("style.qss");
if (file.open(QIODevice::ReadOnly | QIODevice::Text))
{
    this->setStyleSheet(file.readAll());
}
QApplication::setStyleSheet() 为整个应⽤程序设置样式表:
qApp->setStyleSheet( "..." );
使⽤ QWidget::setStyleSheet() 给指定的 widget 及其⼦ widget 设置样式:
pMainWgt->setStyleSheet( "..." );

样式表语法

语法规则

样式表由多组“规则”组成,每个规则由“选择器“( selector )、“属性”( property )和”值“( value )组成。如:QPushButton{color: red}
选择器:QPushButton,多个选择器用逗号隔开
属性:rolor
值:red,多个关键字时大多以空格隔开

选择器类型

最常用的选择器是“类型选择器”和“ID选择器”。

ui文件中的控件在编译时经过uic处理,会将其对象名(object name)设置为Qt Designer中指定的名称;若是在代码中创建的控件,需调用setObjectName()手动指定其对象名(object name)

QLabel* pNameLbl = new QLabel;
pNameLbl->setObjectName("nameLabel");
然后再通过“ID选择器”设置样式:
#nameLabel {border: 1px soild red;}

子控件

有的复杂控件须通过获取其⼦控件来设置样式,⽐如 QComboBox 的下拉按钮, QSpinBox 向上和向下的箭头。通过两个冒号(::)来获取⼦控件
QComboBox::drop-down {image: url(dropdown.png)}

伪状态

QPushButton { color: red; }
QPushButton:hover { color: green; }
QPushButton:pressed { color: #ffffff; }

这⾥:https://doc.qt.io/qt-5/stylesheet-reference.html#list-of-pseudo-states 列出了 Qt 控件的所有伪状态。

冲突解决

当多条样式规则,用不同的值指定同一属性时,会发生冲突。如:

QPushButton#okButton { color: gray }
QPushButton { color: red }

这⾥ QPushButton#okButton 优先于 QPushButton,因为它指定了特定的对象。
同样,指定了伪状态的规则,优先于未指定伪状态的规则:

QPushButton:hover { color: white }
QPushButton { color: red }

当⿏标悬浮于按钮时,按钮上的⽂字呈红⾊。然⽽:

QPushButton:hover { color: white }
QPushButton:enabled { color: red }

这两个选择器有同样的优先级,此时最后⼀条规则⽣效。再看下⾯的情况:

QPushButton { color: red }
QAbstractButton { color: gray }
虽然 QPushButton 继承⾃ QAbstractButton ,但是类型选择器有同样的优先级,与继承关系⽆关,所以这⾥仍是后⾯的样式⽣效。

盒子模型

使用样式表时,每个widget都被当做有4个同心矩形的盒子:margin(外边距)、border(边框)、padding(内边距)、content(内容)。

示例 1
下图中 QWidget ⾥⾯布局放了⼀个 QLabel ,然后设置样式表:
QLabel {
border: 5px solid green;
margin: 10px 30px 20px 40px;
padding: 20px 30px 40px 10px;
}
即外边距上右下左的顺序,分别为 10px 30px 20px 40px 。 边框为 5px
内边距上右下左顺序,分别为 20px 30px 40px 10px
示例 2 :设置按钮的背景图⽚
#pushButton {
border-image: url(bg.png) 4px 6px 4px 6px;
border-width: 4px 6px 4px 6px;
}
后⾯ 4 个数值是 border 的值,顺序是上右下左,
注意:如果 border-image 后⾯有这⼏个值,那么必须要同时写上border-width!!!并且值要⼀⼀对应。

常⽤控件样式

Qt Assistant 中提供了⼀些样式表示例,参考 https://doc.qt.io/qt-5/stylesheet-examples.html

注意 :样式表不⽣效

有很多情况会造成样式表不⽣效,⽐如图⽚丢失、图⽚路径错误、样式受⽗对象样式影响、样式被覆盖等。
有⼀种特殊情况,在 Qt Assistant 的官⽅⽂档中指出:如果你⼦类化 QWidget ,你需要为其提供⼀个paintEvent。
void CustomWidget::paintEvent(QPaintEvent *)
{
    QStyleOption opt;
    opt.init(this);
    QPainter p(this);
    style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
}
参考链接 https://doc.qt.io/qt-5/stylesheet-reference.html#list-of-stylable-widgets 中, List of Stylable Widgets 下⾯ QWidget 的部分。

使用建议

1. 统⼀设置⼀个 widget 中所有控件的样式,不要在代码中单独给每个控件设置样式,这样做会造成混乱;
2. 图⽚资源先添加到 qrc ⽂件中,再在样式表中使⽤;
3. 同⼀个类,不要混⽤多种⽅式设置样式表,⽐如既在 ui ⽂件中设置样式,⼜在代码中读取样式表⽂件,这样做很可能造成样式覆盖,并且问题难以查找。

作业4

调整界⾯样式
修改界⾯结构,添加时间显示(暂时不需要动态更新),添加标题“思维导图”,通过样式表设置主界⾯样式,参考样式
主要包含以下内容:
  1. 顶部标题栏区域
  2. 标题栏右上⻆的昵称显示样式、最⼤化、最⼩化、关闭按钮样式
  3. 标题栏左下⽅⼏个菜单的样式
  4. 主界⾯背景
  5. 主界⾯左侧树的样式

坐标系统

下⾯是 QWidget 中与坐标位置有关的⼀些函数:
1. QPoint QWidget::pos() const;
获取⼀个 widget 相对于它的⽗ widget 的位置。
如果该 widget 是⼀个窗⼝,那么得到的将是它在桌⾯上的位置(包含边框)。
2. QRect QWidget::geometry() const;
获取 widget 相对于⽗ widget geometry (即位置和⼤⼩),不包括窗⼝边框。
3. QSize QWidget::size() const;
获取⼀个 widget ⼤⼩(即宽和⾼),如果是窗⼝则不包括窗⼝边框。
4. QRect QWidget::frameGeometry() const;
widget 相对于它⽗ widget geometry ,如果是窗⼝则包括窗⼝边框。
5. QSize QWidget::frameSize() const;
获取 widget 的⼤⼩,包括窗⼝边框。
其它还有⼀些函数,⽐如 x() y() width() height() 等,可查看 Qt ⽂档中的相关介绍。

窗口

如果⼀个 widget 没有指定⽗对象(即⽗对象为空指针),那么它将作为⼀个窗⼝显示,窗⼝位置的计算是相对于桌⾯的左上⻆的。在窗⼝显示之后,获取窗⼝的信息:
qDebug() << this->geometry();
qDebug() << this->frameGeometry();
qDebug() << this->pos();
在输出窗⼝可以看到:
QRect(686,404 547x361)
QRect(678,373 563x400)
QPoint(678,373)
可以看到,这些位置是相对于整个桌⾯计算得到的,并且 frameGeometry 相对于 geometry 多了⼀个边框的⼤⼩。

非窗口控件

如果⼀个 widget 指定了其它 widget 为⽗对象,那么它的位置信息将会是相对于⽗ widget 的。
qDebug() << ui.widget->geometry();
qDebug() << ui.pushButton->geometry();
输出窗⼝:
QRect ( 30 , 40 400x250)
QRect ( 80 , 110 75x23)
绿⾊边框的 widget ,相对于主窗⼝的位置是 (30, 40)
红⾊边框 pushButton ,相对于绿⾊边框 widget 的位置是 (80, 110)

坐标转换

上⾯算的都是相对于⽗ widget 的位置,那么如何得到间接的坐标,⽐如上⾯ pushButton 相对于桌⾯、相对于主窗⼝的坐标呢?
先了解⼀些与坐标转换有关的函数:
1. QPoint QWidget::mapToGlobal(const QPoint &pos) const;
将当前 widget 中的坐标 pos 转为全局坐标。
2. QPoint QWidget::mapToParent(const QPoint &pos) const;
将当前 widget 的坐标 pos 转为相对于⽗ widget 的坐标。
3. QPoint QWidget::mapTo(const QWidget *parent, const QPoint &pos) const;
将当前 widget 中的坐标 pos 转为相对于 parent 的坐标, parent 不能为空,并且必须是当前 widget 的直接或间接⽗ widget
除此之外,还有 mapFromGlobal() mapFromParent() mapTo() 等进⾏坐标转换的函数
// pushButton 的全局坐标
qDebug() << ui.pushButton->mapToGlobal(QPoint(0, 0));
// pushButton 相对于主窗⼝的坐标
qDebug() << ui.pushButton->mapTo(this, QPoint(0, 0));
// 输出窗⼝结果:
QPoint(796,554)
QPoint(110,150)

事件的坐标

以⿏标按下事件为例:
void TestWidget::mousePressEvent(QMouseEvent *event)
{
    qDebug() << event->pos();
    qDebug() << event->globalPos();
}
QMouseEvent::pos() 获取到的是⿏标在当前 widget 对象中的位置;
QMouseEvent::globalPos() 获取到的是⿏标的全局坐标。

事件

当事件发⽣时, Qt 创建了⼀个事件对象,并将它传给特定的对象。事件⼤多是⽤户与界⾯交互时触发,但也会有系统⾃动触发,⽐如:当⿏标按下时会触发⿏标事件(QMouseEvent ),键盘按下时会触发键盘事件( QKeyEvent ),窗⼝显示时会触发绘图事件(QPaintEvent ),定时器超时后触发定时器事件( QTimerEvent )。

事件类型

QResizeEvent :窗⼝⼤⼩发⽣改变事件
QMouseEvent :⿏标事件
QKeyEvent :键盘事件
QCloseEvent :窗⼝关闭事件
QPaintEvent :绘图事件
QTimerEvent :定时器事件
……
处理这些事件,通常需要重写事件函数,以⿏标按下时触发⿏标事件为例:
void TestWidget::mousePressEvent(QMouseEvent *event)
{
    // 调⽤⽗类的函数
    QWidget::mousePressEvent(event);
    // 获取⿏标按下的位置
    QPoint pos = event->pos();
}

事件和信号槽区别

事件由外部事物生成,并在应用中的事件循环传递;
信号槽⽤于类之间的通信;
事件需要你来处理,⽽信号发送你是接收到通知。

事件过滤器

⼀个 QObject 对象可以⽤来监视另⼀个 QObject 对象的事件。 设置事件过滤器分为以下两步:
安装事件过滤器:  installEventFilter()
重写事件过滤器函数: eventFilter()

事件传递

事件触发后会进⾏传递,从⼦ widget 向⽗ widget 逐层传递。
如主窗⼝中有⼀个 widget1 ,在 widget1 中有 widget2
widget2 中按下⿏标,触发 widget2 mousePressEvent ,然后⿏标事件会先传到 widget1 ,再传到主窗⼝当中。
如果不想让事件传递该怎么做呢?
可以在 widget2 mousePressEvent 中调⽤ QMouseEvent::accept() ,这样⿏标事件便只会在 widget2 中发⽣,不会传递给它的⽗对象了。

作业5

截⽌⽬前,主窗⼝是⽆边框的,⿏标⽆法拖动。
  1. 请通过重写⿏标事件,实现主窗⼝的拖动功能。
  2. 为左侧QTreeWidget添加右键菜单。如果树没有根节点,则包含菜单项“添加根节点”,根节点只能是组类型;如果⿏标位置有类型是组的节点,包含菜单项“添加⼦项、删除、重命名”;对于类型是叶⼦的节点,包含菜单项“删除、重命名”(设定只有类型是组的节点可以添加⼦节点,并在添加⼦节点时,可以选择⼦节点类型是组还是叶⼦)。
  3. 实现上⾯右键菜单的所有功能,删除时提示是否确定。
  4. 添加快捷键:Del⽤于删除、F2⽤于重命名

读写配置文件

读写文件

文件读写使用QFile类。

QFile file("a.txt");
if(file.open(QIODevice::ReadOnly | QIODevice::Text))
{
    QString strText = file.readAll();
}

xml

ini

Json

作业6

直到现在,⼀旦我们将思维导图软件关闭,界⾯上所有修改都会丢失。
因为数据都是保存在内存中的,为了能够在下次打开软件时恢复数据,我们需要将思维导图数据保存到⽂件中,请实现下⾯的功能:
  1. 通过菜单:⽂件 - 保存,或者通过快捷键Ctrl+S,将界⾯中的思维导图树保存到本地的配置⽂件中
  2. 通过菜单:⽂件 - 打开,或者通过快捷键 Ctrl+O,打开本地配置⽂件,并初始化左侧的树、以及中间的思维导图区域
配置⽂件可采⽤ xml 或者 json 任⼀种,参考格式如下:


    
        ...(不往下写了,请⾃⼰设计配置⽂件格式)
    

帮助与提示

  1. 需要在配置⽂件中保存每个树节点的⽂字、类型(组节点还是叶⼦节点)
  2. 配置⽂件保存:建议先根据树结构将数据保存到结构体中,再写⼊配置⽂件

QPainter绘图

通过QPainter实现Qt二维绘图:

1. 几何图形,点、线、多边形等;

2. 特殊效果,如渐变;

3. 变换、平移、旋转、缩放等。

用法

绘图时,需实现paintEvent函数,并创建QPainter对象。

void CustomWidget::painterEvent(QPainterEvent* event)
{
    QPainter painter(this);
    //...
}

画笔

通过QPainter::setPen()设置画笔,使用画笔设置线的属性。如:线宽、线色、线型

画刷

通过QPainter::setBrush()设置画刷,用于控制绘图时的填充属性:填充色、填充样式。

QPainter绘制接口

使用QPainter可以画各种各样的图形或文字等元素,如:

1. 点:QPainter::drawPoint()

2. 线:QPainter::drawLine()

3. 矩形:QPainter::drawRect()

4. 圆角矩形:QPainter::drawRoundedRect()

5. 多边形:QPainter::drawPolygon()

6. 扇形:QPainter::drawPie()

7. 文字:QPainter::drawText()

8. 图片:QPainter::drawPixmap()

...

除此之外,还有⼀个特殊的接⼝:
QPainter::drawPath(const QPainterPath &path);
通过 QPainterPath 可以根据连接路径来绘图,可以添加弧形、⻉塞尔曲线等路径,绘制出复杂的、不规则的形状。
QPainterPath path;
path.moveTo(20, 80);
path.lineTo(20, 30);
path.cubicTo(80, 0, 50, 50, 80, 80);
QPainter painter(this);
painter.drawPath(path);

坐标系统

在默认的坐标系中,点 (0, 0) 位于⼀个 widget 的左上⻆, x 坐标向右增⻓, y 坐标向下增⻓。
坐标系可以进⾏⼀些变换:
平移: QPainter::translate()
旋转: QPainter::rotate()
伸缩: QPainter::scale()
……
void CustomWidget::paintEvent(QPaintEvent *event)
{
     QPainter painter(this);
     painter.setPen(Qt::red);
 
     // 坐标变换
     painter.translate(100, 200);
     painter.rotate(90);
     painter.scale(0.5, 0.5);
     painter.drawLine(0, 0, 200, 200);
}

重设坐标变换

通过 QPainter::resetTransform() 将所有经过平移、旋转、伸缩等变换进⾏重设。

保存、恢复变换状态
坐标变换的状态可以保存和恢复,类似⼀个栈结构,保存时将状态⼊栈,恢复时将状态出栈。
保存:QPainter::save()
恢复:QPainter::restore()

void CustomWidget::paintEvent(QPaintEvent *event)
{
     QPainter painter(this);
     painter.setPen(Qt::red);
     // 保存状态
     painter.save();
     painter.translate(100, 200);
     painter.rotate(90);
     painter.scale(0.5, 0.5);
     // 恢复状态
     painter.restore();
     // 恢复之前的状态后,在该状态下继续进⾏坐标变换
     painter.translate(200, 30);
     painter.drawLine(0, 0, 200, 200);
}

GraphicsView

GraphicsView提供了用于管理大量图形元素及其交互的方式,并提供了一个用于显示这些元素并且支持放缩、旋转的widget。在图形元素多、坐标变换多的情况下,更偏向使用GraphicsView,而非QWidget

场景QGraphicsScence

QGraphicsScence提供了图形显示场景,主要负责:提供用于管理大量item的场景;将事件传给每个item;管理item的状态,如选中和聚焦处理。

场景(Scene)是QGraphicsItem对象的容器,通过调用QGraphicsScene::addItem()添加Item,再获取item。

QGraphicsScene* pscene = new QGraphicsScene();

视图QGraphicsView

QGraphicsView继承自QWidget,用于显示场景中的内容。

QGraphicsView* pView = new QGraphicsView();
pView->setScene(pScene);

图元QGraphicsItem

QGraphicsItem是场景中图元的基类,如矩形(QGraphicsRectItem)、椭圆(QGraphicsEllipseItem)、文本(QGraphicsTextItem)。不过更强大的是,你可实现自定义item。QGraphicsItem支持以下特性:

  • 鼠标按下、移动、松开和双击事件,鼠标悬浮事件、滚轮事件、上下文菜单事件等;
  • 键盘输入、键盘事件
  • Drag、Drop
  • 分组
  • 碰撞检测

若是自定义图元,首先需继承QGraphicsItem,然后实现以下的纯虚函数:

  1. boundingRect(),返回可绘制区域的范围
  2. paint(),进行绘图
class SimpleItem : public QGraphicsItem
{
public:
     QRectF boundingRect() const override
     {
         qreal penWidth = 1;
         return QRectF(-10 - penWidth / 2, -10 - penWidth / 2,
             20 + penWidth, 20 + penWidth);
     }
     void paint(QPainter *painter, const QStyleOptionGraphicsItem *option,
         QWidget *widget) override
     {
         painter->drawRoundedRect(-10, -10, 20, 20, 5, 5);
     }
};
通过场景,将图元添加进来。
SimpleItem *pItem = new SimpleItem();
pScene->addItem(pItem);

作业7

真相⼤⽩的时候到了!前⾯思维导图库就是通过 GraphicsView 实现的。
⾸先尝试下⾯ 2 个⼩练习:
QWidget 窗⼝中⼼画⼀个边框红⾊、背景绿⾊、半径为 150 像素的圆形。
⿏标在边框区域按下,将该圆改为边框绿⾊、背景红⾊;
⿏标松开,恢复圆的边框为红⾊,背景为绿⾊。
使⽤ QGraphicsView ,⼀个边框红⾊、背景绿⾊、半径为 150 像素的圆形 item
item 放⼤ 1.5 倍,并旋转 90 度。
接下来实现下⾯的功能:
使⽤ QGraphicsView ,在场景中创建 2 item ,分别显示⽂字“⽜郎”、“织⼥”,并创建⼀条连线连接这两项。可通过⿏标拖动这两项,线也会跟根据item 位置变化,始终连接这两项。

时间与定时器

定时器

Qt提供了2种使用定时器的方法,一是使用QTimer类,二是使用QObject提供的定时器接口和事件。

方式一 QTimer

QTimer类提供了定时器接口,通过QTimer::start()启动定时器。定时器到达间隔时间后会发出信号timeout(),然后重复计时。

// 创建定时器
QTimer* pTimer = new QTimer(this);
// 信号timeout()连接到槽函数
connect(pTimer, SIGNAL(timeout()), this, SLOT(timeoutSlot()));
// 启动定时器
pTimer->start(1000);

在这个示例中,定时器的间隔是 1000 ms。即每隔 1s 钟,定时器都会发出⼀次 timeout() 信号。
如果想让定时器只运⾏⼀次,可以调⽤它的 setSingleShot(bool) ⽅法。

pTimer->setSingleShot(true);

另外⼀种⽅式是使⽤静态函数 QTimer::singleShot():

// 开启定时器,200毫秒后,进⼊槽函数updateCaption()
QTimer::singleShot(200, this, SLOT(updateCaption()));

方式二 QTimerEvent

QObject 的成员函数 startTimer 可以启动⼀个定时器,返回值是⼀个整型数值,也就是该定时器的唯⼀ id。
// 启动⼀个定时器,返回定时器的id
int QObject::startTimer(int interval);
// 移除定时器
void QObject::killTimer(int id);

每隔 interval 的时间,都会进⼊⼀次 QObject::timerEvent(QTimerEvent *event) 事件,通过
QTimerEvent::timerId() 可以获取当前哪个定时器被激活。

//启动定时器
m_timerId = startTimer(1000);
//...
//定时器事件
void TestWidget::timerEvent(QTimerEvent* event)
{
    if(event->timerId() == m_timerId)
    {
        //...
    }
}

时间

QDateTime 类⽤于处理⽇期, QTime 类⽤于处理时间。
示例:
获取当前的系统时间:
QTime::currentTime()

将时间转换为字符串:

QString strCurTime = QTime::currentTime().toString("hh:mm:ss");

网络

UDP服务端(发送方)

m_pUdpSocket = new QUDPSocket(this);
//...
QByteArray datagram = "Broadcast message " + QByteArray::number(messageNo);
m_pUdpSocket->writeDatagram(datagram, QHostAddress::Broadcast, 45454);

UDP客户端(接收方)

m_pUdpSocket = new QUdpSocket(this);
m_pUdpSocket->bind(45454, QUdpSocket::SharedAddress);
connect(m_pUdpSocket, &QUdpSocket::readyRead, this, &Receiver::processPendingDatagrams);

void Receiver::processPendingDatagrams()
{
    QByteArray datagram;
    while(m_pUdpSocket->hasPendingDatagrams())
    {
        datagram.resize(int(m_pUdpSocket->pendingDatagramSize()));
        m_pUdpSocket->readDatagram(datagram.data(), datagram.size());
        m_pStatusLabel->setText(tr("Received datagram: \"%1\"").arg(datagram.constData()));
    }
}

线程与进程

QThread

QThread是Qt线程类,最简单的用法就是子类化QThread,然后重新实现run()方法。

class WorkerThread : public QThread
{
    Q_OBJECT
    
protected:
    void run() override
    {
        QString res;
        emit resultReady(result);
    }

signals:
    void resultReady(const QString& s);
};

void MyObject::startWorkInThread()
{
    WorkerThread* workThread = new WorkerThread(this);
    connect(workerThread, &WorkerThread::resultReady, this, &MyObject::handleResults);
    connect(workerThread, &WorkerThread::finished, workerThread, &QObject::deleteLater);
    workerThread->start();
}
在线程中运行时,若需要通知界面,通常使用发送信号的方式。
除此之外,还有moveToThread、QtConcurrent、线程池等方式使用线程。使用QMutex进行互斥处理。

GUI线程

当一个Qt程序启动时,就有一个进程被操作系统创建,同时一个线程也立即运行,这个线程称主线程,也叫GUI线程。

Qt中,所有与界面有关的操作只能在主线程中进行,不能在其他线程中处理界面,否则很可能造成程序崩溃。

作业8

请新建⼀个⼯程.
界⾯上有⼀个⽂本显示框 QLabel ,计算 2 50 的斐波那契数列结果,每次计算完成后,将计算结果显示在QLabel上。
要求:
  1. 计算过程中界⾯不能卡顿;
  2. 程序关闭时⽴即退出,并且不会出现崩溃;
  3. 分别使⽤QThreadmoveToThread两种⽅式实现。

图表

Qt4未提供图标库,但是你可以通过使用QGraphicsView自己封装,也可以使用第三方库。Qt5中提供了图表库QCharts。

折线图、饼图、柱状图

程序打包部署

程序开发完成之后,需要将其打包部署,将程序放到没有开发环境的机器上,能正常打开使⽤。

准备工作

⾸先请将程序改为 Release 版本。
⼤多时候,我们是在 Debug 环境下开发的,因为开发过程中需要⽣成调试信息,通过调试⽅便理解程序或者解决 bug
但是在打包部署后,软件是给⽤户使⽤的,不再需要调试信息了。 Release (即发布的意思)版本的应⽤程序体积更⼩,执⾏速度更快

Qt 4 程序打包

要部署应⽤程序,必须确保将相关的动态库( dll ⽂件)以及可执⾏⽂件( exe ⽂件)复制到同⼀⽬录中。
可以通过使⽤依赖⼯具来检查你的应⽤程序需要链接哪些库,⽐如 Dependency Walker 插件的⼯作⽅式与普通的 dll 不⼀样,所以我们不能像对台 dll 那样,直接将它们复制到与应⽤程序可执⾏⽬录相同的⽬录中。在寻找插件时,应⽤程序会在应⽤程序可执⾏⽂件⽬录内的插件⼦⽬录中进⾏搜索。
Qt 4.8.7 为例, Qt ⾃带的插件在安装⽬录下的 plugins ⽬录下,⽐如: C:/qt-4.8.7/plugins
所以,为了能使⽤这些插件,必须创建插件⼦⽬录,并将相关的 dll 复制过来。
除此之外,还需考虑资源⽂件、配置⽂件等是否也加了进来,并且确保路径正确。
综上所述, Qt4 程序打包时需考虑以下内容:
可执⾏⽂件( exe
可执⾏⽂件链接的动态库( dll
插件(插件⼦⽬录、插件 dll
资源⽂件、配置⽂件
其它

Qt 5 程序打包

除了像 Qt 4 那样通过拷⻉所需的⽂件打包程序, Qt 5 还有⼀种更⽅便的打包⼯具: windeployqt。
【无标题】_第7张图片通过命令⾏, windeployqt 后接可执⾏⽂件的路径,即可将程序打包。
windeployqt xxx.exe
打包后⽂件夹中的⽂件如下:
【无标题】_第8张图片
这种⽅式只能将依赖的 Qt 相关的库进⾏打包,不会打包⾮ Qt 的库,⽐如 OSG boost 或者⾃⼰编写的库等,所以仍需⼿动将它们拷⻉到应⽤程序⽂件夹中。
学⽆⽌境,实际项⽬中,你可能会遇到很多新知识,⽐如数据库、模型与视图、动画等等。这⾥我们并没有详细去讲,因为重点不是学更多知识点,⽽是学习的态度和解决实际问题的能⼒。

你可能感兴趣的:(Qt入门,qt)