入职培训总结之Qt项目QSsh开发笔记

一、信号与槽机制

信号与槽机制的实质就是观察者模式。

原理

信号与槽,分为信号与槽两部分,信号就是某种事件发生时,一个对象主动(通常是自动事件)产生的消息,如pushbutton的点击、slider的滑动等;而槽就是对信号的处理操作。

信号与槽的绑定,有三种方式:

  1. 在UI设计界面,通过右键控件,直接转到对应的槽(信号)函数,编写函数体即可;

  2. 在页面对应的.cpp文件中,通过类名::on_对象名_事件名的格式手动编写函数体,函数声明需要写在private slots下;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    void Stack::on_mend_clicked()
    {
    int id;
    if(ui->id->text().isEmpty()){
    QMessageBox::warning(this,"Failed","请将信息填完整!");
    }else{
    id=ui->id->text().toInt();
    ui->id->clear();
    QSqlQuery query,query1,query2;
    //……下面是一些数据库操作
    }
    }
  3. 如果是自己自定义的函数,不方便改成上面的命名规范,可以使用connect函数进行手动连接:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    //mainwindow.cpp
    connect(ui->logout_2,SIGNAL(triggered(bool)),this,SLOT(doLogOut()));

    void MainWindow::doLogOut(){
    this->close();
    Dialog l;
    if(!l.exec()) return;
    this->show();
    this->initSystem(l.reIndex());
    }

    上面的示例代码是将mainwindow页面中,id为的logout_2的对象产生的triggered信号,与doLogout()函数进行了绑定。connect函数的四个参数,分别为信号发送者,信号名称、信号的接收者、接收者对应的槽函数;

  4. 在Qt quick2中,如果使用qml进行编程,则信号处理的方式略微不同:需要在qml文件中设置自己的id,指定target(另一个对象),使用connect函数对signal和receive进行绑定,最后在target中实现receive函数。下面是一个示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    //sender.qml
    Circle {
    id:sender
    property int counter: 0
    signal send(string value)
    property Receiver target: null
    onTargetChanged : {
    send.connect(target.receive);
    }
    MouseArea{
    anchors.fill: parent
    onClicked: {
    sender.counter++;
    sender.send(counter);
    console.log("clicked");
    }
    onPressed: {
    sender.circleColor="blue" ;
    }
    onReleased: {
    sender.circleColor= "red" ;
    }
    }
    }

    //receiver.qml
    Circle {
    id:receiver
    function receive(value){
    contentText=value;
    colornotify.running=true;
    }
    SequentialAnimation on circleColor {
    id:colornotify
    running: false
    ColorAnimation {
    from: "red"
    to: "blue"
    duration: 200
    }
    ColorAnimation {
    from : "blue"
    to: "red"
    duration:200
    }
    }
    }

    这两个qml文件中的Circle是实现设计好的一个圆圈类,用来形象化展示不同的实体。

    send-receive

    即:两个实体均继承自Circle(主要的属性有颜色和文字)。sender和receiver都放置在main.qml中,这样两个对象的信号就能连接。

    项目结构

    上面的示例代码中,send相当于是一个广播,信号管道,对于其内部实现这里并没有涉及。相反更重要的是receive信号处理,其函数体内进行了文字变更和变色渲染的调用。

slot槽函数可以当做普通函数使用,即可以直接调用

关于QT4与QT5中信号和槽绑定的不同语法

  • QT4中,信号和槽必须使用SIGNAL和SLOT关键字进行包含,但QT本身在编译的时候不会对其进行类型检查,如果信号或槽不存在也不会报错,而是等到运行过程中才会debug:no such signal(slot) :xxx

    connect(sender,SIGNAL(signal(params)),recever,SLOT(receiver(params)));

    需要注意的是signal和receiver都需要在对应的类里面进行声明(分别声明在signal、slot下面),且类型都必须是void,信号只能声明不能实现

  • QT5中,信号与槽不需要使用关键字进行包围,相应地会在编译阶段进行存在检查和类型校验。

    connect(sender, &ClassSender::signal, receiver, &ClassReceiver::slot);

    这种方式中,槽函数不必声明在头文件”slot”域下。

    可以看到,这种情况下signal和slot没有括号包围,即默认不能带参数,这给函数的重载带来了阻碍,解决方案可以参考Qt5教程: (4) 带参数信号与槽以及:【Qt教程】1.7 - Qt5带参数的信号、信号重载、带参数的槽函数、槽函数重载

    以服务器远程连接工具为例,有两个函数是重载关系:

    1
    2
    void CSshUtils::slotSend(QString cmd);
    void CSshUtils::slotSend(QString hostAddress,QString cmd);

    这两个函数作为槽函数响应当前类(CFileWidget)的信号,则我们在connect的时候,需要指明具体连接的是哪一个槽:

    1
    2
    void (CSshUtils::*slotSend_no_add)(QString cmd)=&CSshUtils::slotSend;
    connect(this,&CFileWidget::sigSend,sshSocket,slotSend_no_add);

二、Qt页面组织方式

Qt中的页面显示,以及不同页面间的跳转,其基本原理就是创建不同的页面对象,并在信号关键点,对Widget或Mainwindow对象的show()或Dialog对象的exec()方法进行调用,从而实现页面的跳转。

QWidget类是所有用户界面对象的基类,即所有界面类都直接或继承自QWidget,上面提到的不同页面对象,在创建并初始化时,会调用根据设计的UI文件自动生成的setupUi函数(可通过ui_对象名.h文件找到),对页面及内部的对象树进行渲染。当调用show()当接口时,页面就会显示出来。

1. 通过QWidget(QMainwindow)

这种方式直白描述,就是如下步骤:

  • 预先定义好多个不同的继承自QWidget或QMainwindow的类并设计好其页面。

  • 程序运行时,根据首先运行的main.cpp,如果未加配置且项目是继承自QMainWindow,则首先展示的是mainwindow.ui页面。下面稍作了修改:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    //main.cpp
    int main(int argc, char *argv[])
    {
    QApplication a(argc, argv);
    Dialog l;
    MainWindow w;
    if(l.exec()==Dialog::Accepted){
    w.show();
    w.initSystem(l.reIndex());
    return a.exec();
    }
    }

    这里Dialog是一个登录框。程序的执行逻辑是:首先弹出登录框;如果登录框执行(显示,根据用户的输入进行处理)的结果是Dialog::Accepted,则显示MainWindow实例的页面。最后返回值是a.exec(),代表程序会一直保持页面打开不退出(如果没有这句,页面就会一闪而过)

  • mainwindow.ui中设计好一些按钮,并在main.cpp中实现并绑定对应的槽函数(方式如上1、2),在槽函数中指向其余的页面即可

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    //mainwindow.cpp
    #include<bookborrow.h>
    MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
    {
    ui->setupUi(this);
    this->resize( QSize( 1300, 800));
    setWindowTitle("主界面");
    connect(ui->borrowButton,SIGNAL(clicked(bool)),this,SLOT(doBookBorrow()));
    ……
    }

    void MainWindow::doBookBorrow(){
    Bookborrow bb;
    this->hide();
    bb.initSystem(username,major,phone);
    bb.exec();
    this->show();
    }
    ……

    上面的示例中,信号发送者为borrowButton,绑定到了槽函数doBookBorrow()上,去创建并打开另一个借书页面,隐藏当前页面。当借书页面被关闭,mainwindow就会重新显示。

2. 通过QML

这种方式基本原理和上面并没有太大区别,但是由于QML(Qt Meta-Object Language)独特的继承方式,使得页面的嵌套、信号与槽的绑定变得更加简单:

  • 首先程序运行的同样是main.cpp

    点击展开
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    #include <QGuiApplication>
    #include <QQmlApplicationEngine>
    int a=0;
    int main(int argc, char *argv[])
    {
    QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
    QGuiApplication app(argc, argv);
    QQmlApplicationEngine engine;
    const QUrl url(QStringLiteral("qrc:/main.qml"));
    QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
    &app, [url](QObject *obj, const QUrl &objUrl) {
    if (!obj && url == objUrl)
    QCoreApplication::exit(-1);
    }, Qt::QueuedConnection);
    engine.load(url);
    return app.exec();
    }

    这段代码是一个Qtquick项目创建后main.cpp的默认模板,可以看到其内部引入了页面文件main.qml,通过QQmlApplicationEngine来对其进行load,最后依然是阻塞式app.exec()防止退出。

  • 用标准组件设计、拼装自己的页面,下面是一个实现登录功能的main.qml示例:

    点击展开
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    import QtQuick 2.6
    import QtQuick.Window 2.2
    import QtQuick.Controls 1.1
    import QtQuick.Controls.Styles 1.1
    import QtQuick.Layouts 1.1
    Window {
    id:win_home
    visible: true
    width: 480
    height: 360
    title: qsTr("Ev_viewer")
    SelectData
    {
    id:select_data
    }
    MainForm {
    AnimatedImage //动画图
    {
    id:png
    width: 480
    height: 150
    source: "images/png.gif"
    }
    Text //用户名称
    {
    id:textname
    x:80
    y:png.y + png.height + 20;//文本顶部距离rocket图片底部20
    text:"用户名:"
    color: "Black"
    font.family: "楷体"; //设置字体
    font.pixelSize: 25;//字体大小
    }
    Text //密码
    {
    id:textPsd
    x:80
    y:png.y + png.height + 70;//文本顶部距离rocket图片底部70
    text:"密 码:"
    color: "Black"
    font.family: "楷体"; //设置字体
    font.pixelSize: 25;//字体大小
    }
    TextField{ //用户名输入框
    x:180
    y:png.y + png.height + 18;//输入框顶部距离rocket图片底部18
    width:200
    height:28
    id:textfield_Name
    }
    TextField{ //密码输入框
    x:180
    y:png.y + png.height + 68;//输入框顶部距离rocket图片底部68
    width:200
    height:28
    id:textfield_Psd
    }
    Component{ //登录与退出按钮
    id: btnStyle;
    ButtonStyle {
    background: Rectangle {
    implicitWidth: 70;
    implicitHeight: 25;
    color: "#DDDDDD";
    border.width: control.pressed ? 2 : 1;
    border.color: (control.hovered || control.pressed) ? "green" : "#888888";
    }
    }
    }
    Button {
    id: openButton;
    text: "登录";
    onClicked:{
    select_data.show();
    }
    anchors.left: parent.left;
    anchors.leftMargin: 100;
    anchors.bottom: parent.bottom;
    anchors.bottomMargin: 60;
    style: btnStyle;
    }
    Button {
    text: "退出";
    onClicked: Qt.quit();
    anchors.left: openButton.right;
    anchors.leftMargin: 150;
    anchors.bottom: openButton.bottom;
    style: btnStyle;
    }
    anchors.fill: parent
    color: "gray";
    }
    }

    在这个例子中,定义了两个信号事件:一个是点击登录按钮时,select_data页面显示,另一个是点击退出按钮时当前程序退出。而整个main.qml页面上的组件,是放在一个自定义组件MainForm中,下面是其内容:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import QtQuick 2.6
    Rectangle {
    property alias mouseArea: mouseArea
    width: 360
    height: 240
    MouseArea {
    id: mouseArea
    anchors.fill: parent
    }
    }

    可以看到其中只有一个Rectangle(长方形)元素,是QtQuick的基本组件。

  • 通过上面的简介,可以发现QtQuick中一个qml文件对应一个组件或一个页面,对于页面的跳转,可以直接使用Button的onClicked等事件,但前提必须提前在组件中手动声明(如本例中的select_data)。

三、APP打包构建相关

通过Qt开发APP,不是跳过了JAVA,而是借助Qt,帮我们自动完成了这一步。Qt for android,底层是通过Gradle将编译好的程序打包构建生成apk,并在真机调试的过程中通过USB调试安装到客户机上。具体的apk文件可以通过如下路径找到:

  • apk文件默认保存在:项目编译输出路径\android-build\build\outputs\apk\debug

  • 如果希望控制APP的图标,将相关资源在Qt中也能统一管理,可按如下步骤开启安卓模板文件管理:

    AndroidTemplateFileWizard

    设置完成后,点击项目文件视图中的OtherFiles->AndroidManifest.xml,即可进入app设置界面:

    AndroidManifest.xml

    此界面设置的选项包括应用图标(上图中间那三个空白的按钮)、构建目标API(这个太低的话会导致构建失败)、包名等等。

  • 按上面的方式设置的话,会将用于生成apk的文件转移到一个共同目录下:

    AndroidTemplateFiles

四、关于APP适配的问题

屏幕尺寸

获取屏幕尺寸,并设置创建页面的大小:参照Qt For Android 如何获取手机屏幕大小

关于屏幕分辨率不同级别及gradle打包时生成的不同目录:参照教你来彻底理解ldpi、mdpi、hdpi、xhdpi、xxhdpi

屏幕方向问题

在屏幕转向时,Qt控制台会输出windowmode=1

五、关于QSsh的交叉编译

参照这篇博客:QSsh之android版编译,使用QtCreator成功编译出了安卓(arm-v7a)的SSH库

编译结果(动态库)

六、关于QWidget如何一直显示的问题

QWidget不像QDialog可以直接调用exec函数一直显示,当槽函数close调用时退出,因此需要

自己利用QEventloop进行实现,参考了:Qt :QWidget 实现QDialog exec() 模态显示效果

实现的过程中还遇到了下一个问题(其实和本问题是无关的):

七、关于指针导致的问题/Bug

1.delete this导致程序崩溃

场景描述:一个主页面调用一个外部页面(其他类),当外部页面点击退出时关闭页面,重新显示主页面。问题发生在外部页面点击了退出后,外部页面关闭了,但同时主页面也推出了,程序报错:

1
2
3
程序异常结束。
The process was ended forcefully.
E:/Qt/Projects/remoteTool/debug/remoteTool.exe crashed.

一开始以为是自定义的exec函数不兼容,导致外部页面的close函数把主页面也关闭掉了,但是经过多次尝试发现问题根源在于外部页面(类)的析构函数

delete this错误

这是一开始没有认真分析,参考之前的示例代码加的一行,用于清除申请的资源。但经过后面查询得知,页面退出后Qt会自动释放掉内存

Qt有父子对象机制,即如果创建的类是直接或者间接继承QObject类,动态内存由Qt来管理,无须手动delete。

如果手动delete掉,就会破坏掉父子对象树,导致主页面也崩溃。

参考了Qt QCloseEvent中delete this的bug

用代码绘制Qcharts图表:Qt 绘制图表 - Qt Charts版

程序当前路径

  • qApp->applicationDirPath()
  • qApp->applicationFilePath()
  • QDir::currentPath();

Qtchart单页版:QT – QChart画饼状图

八、Qt常见报错集锦

编译过程:

1.undefined reference to ‘MyClass::Myfunction()’(自定义函数)

undefinedReferenceto(莫某自定义类的方法)

这种情况多出现于一个自定义类还未实现完全的时候,想要编译一下看有没有错误

但是由于声明了一些函数还未进行实现(尤其是槽函数),而Qt会为每个类自动编译生成moc_xxx.cpp,其中会引用到这些函数,因此就会判定为未知引用

解决方法:

  • 注释或删除掉未实现的槽函数
  • 若槽函数后面必须实现,可先写一些简单的语句如qDebug()、return等

参考了QT:error: undefined reference to ‘xxxx’错误提示,解决方式

2.skipping incompatible xxx when searching for最后报错:undefine reference to xxx (库函数)

这种情况一般是由于库文件与使用的编译器不兼容所导致的,具体可分为:

  • 32位与64位不兼容,这种情况,如果本机有对应的环境,在QtCreator中切换一下项目构建环境即可

    32位64位
  • 构建库所使用的工具与当前编译工具不兼容,比如:编译时使用的是MSVC,而当前构建工具是mingw。这种情况多出现在Qt早期版本(5.9以前)

  • 如果本机没有对应的构建环境,而所需的库是开源项目建议直接下载源代码,跳过对库的链接进行开发,(也可自行编译后对库进行链接)

参考了windows10环境下QtCreator中出现skipping incompatible xxx when searching for xxx 问题解决办法

3.invlaid use of incomplete type ‘class UI::xxx’(自定义类)

image-20230329090407555

一般出现在自定义类的源文件中,且多出现于构造函数初始化UI时。由于Qt中新建一个类的时候并不会同时创建其对应的界面文件,因此需要自行在源文件中对界面文件的命名空间进行声明,即:导致这项报错的原因无非下面几条

  1. 头文件中没有引入“ui_xxx.h”
  2. 界面文件最外层的对象没有改名,或者改了但没改对(此处报错中声明的对象名)
  3. 头文件中没有声明Ui的命名空间和ui对象。
  4. 前面配错了,后面把名字改过来了但是由于缓存的存在导致没有更新ui_xx文件,此时将对应文件删除,清除项目在重新编译即可。

4.expected class name

expected class name

一般出现于自定义的类,且继承自其他类,而未在头文件中进行包含,上图中加入#include <QDialog>即可。

5.undefined reference to ‘vtable for xxx(自定义类)’

image-20230329102139122

问题出现的原因是,虚函数没有函数体(或者直接没有虚函数),直接在源文件中加一个空的函数体即可。

注意,由于之前编译时会生成缓存文件,如果添加之后问题还是存在,可尝试右键项目名->清除,去掉Makefile中过时的信息后重新编译即可。

参考了[undefined reference to `vtable for 原因解决](https://icharle.com/undefinedreferencevtable.html)

6.error: ‘QChartView’ does not name a type

参考https://blog.csdn.net/u011046042/article/details/104749154

运行过程:

7.error: invalid use of ‘this’ outside of a non-static member function

image-20230403202051038

没写类名,导致QT找不到this域,加上域名和双冒号即可

8.call to non-static member function without an object argument

image-20230403215705670

信号和槽要加取地址符

9.static assertion failed: Signal and slot arguments are not compatible

具体原因,槽和信号声明和定义不一致,或类型不对应https://blog.csdn.net/weixin_41320969/article/details/105579389

10.QSqlquery.size()返回值为-1

这个问题的根本原因是:QSqlquery的size()方法,如果大小不能确认或者如果数据库不支持报告有关查询大小的信息,会直接返回-1。因此返回-1并不代表获取失败。如果想要判断是否查询成功以及获取查询结果数量,可通过如下方式:

1
2
3
4
5
6
7
// 查询结果不为空
if(query.last()){
// 获取结果数量后,指针返回列表头,用于后续循环查询
int count=query.at();
query.first();
query.previous();
}
11.QSqlquery.exec()不成功

有可能是bindvalue不成功导致的。当sql语句中含有字符串,需要将字符串使用单引号进行包围,而又不能直接写进去然后bindvalue,应当使用字符串拼接一点一点连接起来。下面是范例:

1
2
3
QString sql="select default_username,default_password from hosts where ip = '";
sql.append(ip).append("'").append(" and port =").append(port).append(";");
query_hosts.prepare(sql);

MaintenanceTool相关

  1. 首页,“此操作至少需要一个处于启用状态的有效资料档案库。”

添加一个临时的镜像站即可。点击左下角“设置”,选择“资料档案库”->临时资料档案库,选择一个开源镜像站,填入其URL,这里我选择的是清华源

但是经过本人多次测试,如果只填一个主站点而不写到具体版本的话,后续信息会出错,导致只能卸载已安装的组件,无法更新。

因此,要确认已安装Qt的版本,选择到镜像源的正确路径。以windows平台下5.13.2版本为例,需添加以下四个连接,缺一不可:

镜像站资料档案库四个链接

最后

有时Qt的编译过程中间文件与实际不符会导致报错,此时删除编译中间文件夹可解决问题。但大部分情况下,问题都是出在源码上,要仔细核对库引入及其版本匹配、槽函数与信号、头文件等容易忽略的地方


入职培训总结之Qt项目QSsh开发笔记
https://dockingyuan.top/2023/04/18/Qt复习与开发笔记/
作者
Yuan Yuan
发布于
2023年4月18日
许可协议