Qt入门基础知识

跨平台;一定程度上简化了内存回收(对象树 new出来的对象不用写delete)。

对象树:当创建的对象  父窗口是  QObject 或者是QObject派生下来的类,此对象会放到对象树上,当程序执行完毕,树上的内容会从下往上依次释放。

通过QObject::dumpObjectTree()可以在调试输出中打印对象树的内容。

CMake组织Qt工程

        在实际的项目开发中,我们会遇到各种各样特殊情况:不同开发者的电脑环境不一样、项目

需要跨平台、项目中使用了大量其它库。如果采用传统的方法,我们需要修改繁杂的工程配置,费

时费力,并且很容易出现问题。

# 指定CMake最小版本
cmake_minimun_required(VERSION 3.10.0)

# 设置工程名称
project(Demo)

# 查找Qt库
if(WIN32)
    if(MSVC)
        # VS2015及以后默认查找Qt5
        if(MSVC_VERSION GREATER_EQUAL 1900)
            set(QT_VERSION "5" CACHE STRING "Qt Version")
        else()
            set(QT_SERSION "4" CACHE STRING "Qt Version")
        endif()
    else()
        set(QT_VERSION "5" CACHE STRING "Qt Version")
    endif()
else()
    set(QT_VERSION "5" CACHE STRING "Qt Version")
endif()

set_property(CACHE QT_VERSION PROPERTY STRINGS 4 5)

if(QT_VERSION VERSION_GREATER "4")
    find_package(Qt5 REQUIRED Core Widgets Xml)
else()
    find_package(Qt4 REQUIRED QtCore QtGui QtXml)

#设置自动处理Qt的moc、uic、rcc
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIX ON)
set(CMAKE_AUTORCC ON)

#头文件包含当前目录
set(CMAKE_INCLUDE_CURRENT_DIR ON)

#将自动生成的文件放到单独文件夹
set_property(GLOBAL PROPERTY AUTOGEN_SOURCE_GROUP "Generated Files")

#生成可执行程序
add_executable(
    ${PROJECT_NAME}
    TestWidget.h
    TestWidget.cpp
    TestWidget.ui
    main.cpp
)

#链接Qt库
if(Qt5Widgets_FOUND)
    target_link_libraries(
        ${PROJECT_NAME}
        Qt5::Core
        Qt5::Gui
        Qt5::Widgets
    )
elseif(Qt4_FOUND)
    target_link_library(
        ${PROJECT_NAME}
        Qt4::QtGui
        Qt4::QtXml
    )
endif()

mian入口函数中 exec()

int main(int argc, char *argv[])

{

    QApplication a(argc, argv);

    ...

    return a.exec();

}

a.exec()

程序进入消息循环,等待对用户输入进行响应。这里main()把控制权转交给Qt,Qt完成事件处理工作,当应用程序退出的时候exec()的值就会返回。在exec()中,Qt接受并处理用户和系统的事件并且把它们传递给适当的窗口部件。

动态转换

C++中提供了dynamic_cast()函数,用于动态转换。需要RTTI支持。Qt中也有类似的函数:qobject_cast()。它不需要RTTI支持,执行速度比dynamic_cast()快,并且可以跨动态库边界工作。应尽量使用qobject_cast()。

使用qobject_cast()有两个限制条件:

 1.被转换的对象必须直接或间接继承自QObject

 2.被转换的对象必须声明QObject宏

中文乱码问题

法一:#pragma execution_character_set("utf-8") //设置执行字符集

法二:QStringLiteral("str")

字符串

C++语言提供了两种字符串的实现:C风格字符串,以\0结尾;std::string。Qt使用QString,可包含\0符号,length()会返回整个字符串的长度,而非到'\0'的长度。

//常用接口

toUpper()    //将字符串所有字母转为大写形式
toLower()    //将字符串所有字母转为小写形式

//trimmed可去掉字符串首位的空格, simplified 不仅去掉首位的空格,中间连续的空格也用一个空格替换掉
QString str1 = " Are you   OK? ";
QString str2 = str1.trimmed();    //str2 == "Are you   OK?"
QString str3 = str1.simplified(); //str3 == "Are you OK?"

QString str1 = "Everything is Visual, hello everyone";
QString substr = "eve";
int pos = str.indexOf(str1); //pos == 29
pos = str.indexOf(str, 0, Qt::CaseInsensitive); // pos == 0
pos = str.indexof(str,pos+1, Qt::CaseInsensitive); // pos == 29
pos = str.lastIndexOf(str); //pos == 29

//isNull 判断字符串是否未赋值, isEmpty 判断字符串是否为空
QString str1;
QString str2 = "";
str1.isNull(); // true
str1.isEmpty(); // true
str2.isNull(); // false
str2.isEmpty(); // true
str1.clear();
str1.isNull(); // false
str1.isEmpty(); // true

//判断是否包含子串 可设置大小写敏感 默认敏感
QString str = "Everything is Visual";
QString substr = "Ev";
str.contains(substr); // true
str.startsWith(substr); // true
str.endsWith(substr); // false

QString str = "Everything is Visual";
QString strLeft = str.left(5); // strLeft == "Every"
QString strRight = str.right(6);//strRight == "Visual"
QString strMid = str.mid(5, 2); //strMid == "th"

QString str0("%1和%2").arg("李雷").arg("韩梅梅"); //str0 == “李雷和韩梅梅”
QString str1("%1+%2").arg(1).arg(QString::number(2)); //str1 == "1+2"

QString number0 = QString::number(12.2754, 'g', 4); //12,28
QString number1 = QString("%1 %L2").arg(1234).arg(1234); //1234 1,234
//总宽度7位,小数点后3位,总位数不够以下划线'_'填充
QString number2 = QString("%1").arg(12.34, 7, 'f', 3, '_'); //“_12.340”

//QString 和 std::string 的转换
std::string str1 = "CSDN";
QString str2 = QString::fromStdString(str1);
str1 = str2.toStdString();
//对应的转换宽字符 std::wstring 的函数为 toStdWString 、 fromStdWString 。

//QString 和 char* 的转换
//不含中文时转 char* ,需用到QByteArray类,因为 char* 需要以 \0 结尾,而通过 QString::toLatin1() 转换为QByteArray时会在字符串后面自动加上\0
QString str = "Everything is Visual.";
char* ch = nullptr;
QByteArray ba = str.toLatin1();
ch = ba.data();
//含中文时转 char* ,因为 QString::toLatin1() 只支持拉丁字母,故改用 QString::toLocal8Bit() 

//char* 转 QString ,直接用 QString(const char*) 构造函数即可
char* ch = "科技改变生活";
QString str = QString(ch);

通用容器

QVariant可以存储各种数据类型。QVariant内置支持所有QMetaType::Type里声明的类型如:int、QString、QFont等,甚至QList、QMap<QString,QVariant>,用户自定义的数据结构。简而言之,QVariant可以存储任意数据类型。

内置类型
QVariant var0(27); //生成一个包含整形数字27的值
int iNum = var0.toInt(); //iNum == 27
QVariant var1("hello"); //生成一个包含字符串的值
QString str = var1.toString(); //str == "hello"
QVariant var2(Qt::red); //生成一个包含颜色QColor的值
QColor color = var2.value<QColor>(); //color == Qt::red
QVariant var3;
var3.setValue(true);
bool b = var3.toBool(); //b == true;
QList<int> list = {1,2,3};
QVairant var4 = QVairant::fromValue(list);
QList<int> listAnother = var4.value< QList<int> >();
自定义类型

用户自定义的数据结构或类(含默认构造、默认析构、拷贝构造),需要使用Q_DECLARE_METATYPE注册,注册后,可被QVariant处理。通过canConvert可以确认是否转换。包含命名空间的,Q_DECLARE_METATYPE应在命名空间外面。

Q_DECLARE_METATYPE结尾不需要分号。

namespace MyNamespace
{
    struct MyStruct
    {
        int num;
        QString id;
    };
}
Q_DECLARE_METATYPE(MyNamespace::MyStruct)

using namespace MyNamespace;
MyStruct data, data2;
QVariant var = QVariant::fromValue(data);
QVariant var2;
var2.setValue(data2);
//检查是否可以转换
if(var.canConvert<MyStruct>() && var2.canConverrt<MyStruct>())
{
    MyStruct data3 = var.value<MyStruct>(); //data3 == data
    MyStruct data4 = var2.value<MyStruct>();//data4 == data2
}
QListWidgetItem中存入数据
class DataForInternal
{
public:
    QString strData;

    DataForInternal(const QString& str="")
        : strData(str)
    {}
};
Q_DECLARE_METATYPE(DataForInternal)

class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = nullptr)
    : QWidget(parent)
    , ui(new Ui::Widget)
    {
        ui->setupUi(this);

        init();
    }

    ~Widget()
    {
        delete ui;
    }

private Q_SLOTS:
    //添加项
    void slotAddItem()
    {
        QString strText = ui->leText->text().trimmed();
        QString strData = ui->leInternalData->text().trimmed();
        QListWidgetItem *pItem = new QListWidgetItem();
        pItem->setText(QString("%1(%2)").arg(strText, strData));
        pItem->setData(Qt::UserRole, QVariant::fromValue(DataForInternal(strData)));
        ui->listWidget->addItem(pItem);
    }

    //搜索内部数据
    void slotSearchItem()
    {
        QString strData = ui->leInternalDataForSearch->text().trimmed();
        //遍历列表
        for (int i = 0; i < ui->listWidget->count(); i++)
        {
            QListWidgetItem *pItem = ui->listWidget->item(i);
            if(!pItem)
            {
                continue;
            }
            QVariant var = pItem->data(Qt::UserRole);
            DataForInternal data = var.value<DataForInternal>();
            bool bMatch = (var.canConvert<DataForInternal>() && data.strData == strData);
            pItem->setBackground(bMatch ? Qt::red : Qt::white);
        }
    }

private:
    void init()//初始化
    {
        connect(ui->btnAdd, &QPushButton::clicked, this, &Widget::slotAddItem);
        connect(ui->btnSearch, &QPushButton::clicked, this, &Widget::slotSearchItem);
    }

private:
    Ui::Widget *ui;
};

几种简单的控件

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;
//设置窗口尺寸
resize(600, 400);
//设置固定尺寸
setFixedSize(600, 400);
//...

QMainWndow

MainWindow::MainWindow(QWidget* parent)
    :QMainWindow(parent)
{
    // 重置窗口尺寸
    resize(600, 400);
    // 1.菜单栏 只有一个
    QMenuBar* bar = menuBar();
    // 菜单栏设置到窗口中
    this->setMenuBar(bar);
    // 添加菜单
    QMenu* fileMenu = bar->addMenu("文件");
    QMenu* editMenu = bar->addMenu("编辑");
    // 添加菜单项
    QAction* newAction = fileMenu->addAction("新建");
    // 添加分割线
    fileMenu->addSeparator();
    QAction* openAction = fileMenu->addAction("打开");
    // 菜单项中添加子菜单
    QMenu* subMenu = new QMenu;
    subMenu->addAction("子菜单1");
    subMenu->addAction("子菜单2");
    newAction->setMenu(subMenu);

    // 2.工具栏 可以多个
    QToolBar* toolBar = new QToolBar(this);
    // 将工具栏设置到窗口中
    addToolBar(Qt::LeftToolBarArea, toolBar);
    // 设置只允许左右停靠
    toolBar->setAllowedAreas(Qt::LeftToolBarArea | Qt::RightToolBarArea);
    // 设置浮动
    toolBar->setFloatable(false);
    // 设置移动
    toolBar->setMovable(false);
    // 添加菜单项
    toolBar->addAction(newAction);
    // 添加分割线
    toolBar->addSepartor();
    toolBar->addAction(openAction);

    // 3.状态栏 只能一个
    QStatusBar* sBar = statusBar();
    setStatusBar(sBar);
    QLabel* label0 = new QLabel("左侧信息", this);
    sBar->addWidget(label0);
    QLabel* label1 = new QLabel("右侧信息", this);
    sBar->addPermanentWidget(label1);
    QLabel* label2 = new QLabel("左侧信息2", this);
    sBar->insertWidget(0, label2);
    
    // 4.铆接部件/浮动窗口 可以多个
    QDockWidget* dock = new QDockWidget("标题", this);
    addDockWidget(Qt::BottomDockWidgetArea, dock);
    // 设置停靠
    dock->setAlowedArea(Qt::BottomDockWidgetArea);

    // 5.中心部件/核心部件 只有一个
    QTextEdit* edit = new QTextEdit(this);
    setCenterWidget(edit);
}

QDialag

// 点击按钮,弹出对话框
connect(ui->actionNew, &QAction::triggered,[=](){
    // 对话框分为
    // 模态对话框    不可以对其他窗口进行操作 阻塞
    // 非模态对话框  可以对其他窗口进行操作 不会阻塞

    // 创建模态对话框
    QDialog dlg(this);
    dlg.resize(120, 30);
    dlg.exec();

    // 创建非模态对话框
    QDialog* dlg = new QDialog(this);
    dlg->resize(120, 30);
    dlg->show();
    //设置属性 55号
    dlg->setAttribute(Qt::WA_DeleteOnClose);
});

QLabel

最简单的文本显示控件

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

pLabel->setAutoFillBackground(true);
QPalette palette = this->palette();    //取色器
palette.setColor(QPalette::Background, color); //QPalette::Text
pLabel->setPalette(palette);

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

QPushButton

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

QLineEdit

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

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

QMessageBox

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

// 错误提示
QMessage::critical(this, "critical", "错误!");

// 信息提示
QMessageBox::information(this, "info", "信息提示!");

// 询问提示
if(QMessageBox::Save == QMessageBox::question(this, "ques", "询问!", 
    QMessageBox::Save | QMessageBox::Cancel, QMessageBox::Cancel))
{
    qDebug() << "选择的是保存"; 
}
else
{
    qDebug() << "选择的是取消";
}

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

其他对话框

// 颜色对话框
QColor color = QColorDialog::getColor(QColor(255, 0, 0));
qDebug() << color.red() << color.green() << color.blue();

// 字体对话框
bool ok;
QFont font = QFontDialog::getFont("华文彩云", 36);
qDebug() << "字体:" << font.family() << "字号:" << font.pointSize()
    << "加粗:" << font.bold() << "倾斜:" << font.italic();

// 文件对话框
QString str = QFileDialog::getOpenFileName(this, "打开文件", "filePath", "(*.txt *.doc)");
qDebug() << str;

常用控件

QRadioButton 单选按钮配合Group Box

QCheckBox  复选按钮

半选状态属性设置

监听某选项是否被选中信号:QCheckBox::stateChanged(int state);

快捷键 QShortcut

信号槽

很多其他语言或框架是通过回调实现的,而Qt提供了一种特殊的方法——信号槽。

该机制类似于设计模式中的“观察者模式”,发布者发布信息,订阅者接收信息。优点:松散耦合。

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

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

Qt4版本的信号和槽:

优点:参数直观,写法简单

缺点:编译器不检查参数类型

自定义信号槽

自定义信号使用宏signals或Q_SIGNALS,自定义槽函数使用宏slots或Q_SLOTS。(Qt定义Q_SIGNALS、Q_SLOTS、Q_EMIT关键字是为了避免冲突,因为signals、slots、emit可能会在第三方库,如boost中使用)如:

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。

对于带有参数的信号槽连接,跨线程的信号槽,对于自定义的参数类型,需要使用qRegisterMetaType对该类型进行注册。

参数说明

信号和槽可重载。重载后,需要函数指针指向函数地址
connect(m_leftWgtBtnBox, static_cast<void(QButtonGroup::*)(int)>(&QButtonGroup::buttonClicked), [=](int id) {Slot_leftWidgetButtonClicked(id); });

class Teacher : public QObject
{
    Q_OBJECT
    ...

signals:
    void signalHungry();
    //重载
    void signalHungry(QString foodName);
};

class Student : public QObject
{
    ...
public slots:
    void slotTreat()
    {qDebug() << "请老师吃饭";}

    void slotTreat(QString foodName)
    {
        //QString 转char*
        //先调用toUtf8()转为QByteArray,再调用data()转char*
        qDebug() << "请老师吃饭,老师要吃 :" <<foodName.toUtf8().data();
    }
};

class Widget : QWidget
{
    Widget(QWidget* parent)
    :QWidget(parent)
    {
        this->m_teacher = new Teacher(this);
        this->m_student = new Student(this);

        //信号和槽发生重载后,需要函数指针指向函数地址
        void(Teacher::* teacherSignal)(QString) = &Teacher::signalHungry;
        void(Student::* studentSlot)(QString) = &Student::slotTreat;
        connect(this->m_teacher, teacherSignal, this->student, studentSlot);
    }
    ...
    void classISOver()
    {
        emit this->m_teacher->signalHungry("炸鸡");
    }

    Teacher* m_teacher;
    Student* m_student;
};
一个信号可以连接多个槽
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);
connect(sender, SIGNAL(signal), receiver, SLOT(slot), Qt::UniqueConnection);

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

Qt connect第5个参数(一般不填,为默认值)
  1. Qt::AutoConnection:为默认值。使用此值连接类型会在信号发送时决定。若接收者与发送者在同一个线程,则自动使用Qt::DirectConnection类型。若接收者与发送者不在一个线程,则自动使用Qt::QueuedConnection类型。
  2. Qt::DirectConnection:槽函数会在信号发送时直接被调用。槽函数与信号发送者在同一线程。效果就像是直接在信号发送位置调用了槽函数,像函数调用,同步执行。emit语句后面的代码将在与信号关联的所有槽函数执行完毕后才被执行
  3. Qt::QueuedConnection:信号发出后,信号会暂时被放在一个消息队列中。需等到接收对象所属线程的事件循环取得控制权时才获取该信号,再执行与信号关联的槽函数。这种方式既可在同一线程内传递消息,也可跨线程操作。emit语句后的代码将在发出信号后立即被执行,无需等待槽函数执行完毕。
  4. Qt::BlockingQueuedConnection:槽函数调用时机与Qt::QueuedConnection一致,不过发完信号后发送者所在线程阻塞,直到槽函数运行完。且接收者与发送者绝对不能在一个线程,否则会造成死锁。用在多线程同步时的场合。
  5. Qt::UniqueConnection:这个标志可通过按位或|与以上四个结合在一起使用。当设置此标志时,当某个信号与槽已经连接时,再进行重复的连接会失败。即就是为避免重复连接

一般Qt connect的第5个参数会在多线程中运用到。注意:

QThread是管理线程的,QThread对象所依附的线程和所管理的线程并非一个概念。QThread所依附的线程,就是创建QThread对象的线程;QThread所管理的线程,就是run启动的线程,即新建线程。如,QThread对象依附在主线程上,QTread对象的slot函数会在主线程中执行,而非次线程。除非QThread对象依附到次线程中(通过moveToThread)

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

//取消指定信号的所有连接
disconnect(myObject, SIGNAL(mySignal()), 0, 0);
myObject->disconnect(SIGNAL(mySignal()));

//取消指定接收者的连接
disconnect(myObject, 0, myReceiver, 0);
myObject->disconnect(myReceiver);
阻塞信号发出

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

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

若想在槽函数中获取发出信号的对象,可使用函数QObject::sender()

//获取触发信号的菜单,并设置播放速度
QAction* pAct = qobject_cast<QAction*>(sender());
double speed = pAct->data().toDouble();

总结

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

作业1

现在,我们将投⼊实战项⽬——设计⼀个思维导图软件,每天完成其中⼀部分,最终效果如下:

⾸先需要实现软件的登录功能,设计⼀个如下图所示的登录窗⼝:

⽤户输⼊⽤户名、密码之后,点击登录按钮。

登录成功(即⽤户名、密码完全匹配)则显示关闭登录窗⼝,并显示主窗⼝,在窗⼝右上⻆显示⽤户昵称;

登录失败则弹出错误提示。

假设已注册的所有⽤户数据如下:
⽤户名: admin
密码: admin
昵称:恒歌科技
⽤户名: anqi
密码: angelababy
昵称:安琪拉
其它要求:
  1. 密码输⼊框不显示明⽂密码
  2. 主窗⼝⽆边框,并在右上⻆添加按钮,实现窗⼝的“最⼩化、最⼤化、恢复、关闭”等功能

帮助与提示

QLineEdit 提供接⼝设置⽂字的回显模式 (echoMode,即输入内容的过程中内容的显示方式,有正常Normal、不显示NoEcho、密码形式PassWord等)
弹出的消息窗⼝使⽤ 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);
pHBoxLayout->addWidget(pButtonTwo, 1, Qt::AlignLeft | Qt::AlignBottom);
pHBoxLayout->addWidget(pButtonThree,2);
pHBoxLayout->addWidget(pButtonFour, 1, Qt::AlignHCenter | Qt::AlignBottom);
pHBoxLayout->addWidget(pButtonFive, 0, Qt::AlignLeft | Qt::AlignTop);

分割器 QSplitter

与QBoxLayout相似,可以完成布局管理器的功能。但是包含在其里面的部件,在运行时可根据鼠标拖拽动态调整其分割大小。

//主分割器
QSplitter *pMainSplitter = new QSplitter(Qt::Horizontal);
QTextEdit *pLeftEdit = new QTextEdit("左部件");
pLeftEdit->setAlignment(Qt::AlignCenter);
//创建右侧分割器
QSplitter *pRightSplitter = new QSplitter(Qt::Vertical, this);
// 设置拖拽分割条时是否实时更新。若为true,则实时更新,否则拖拽时显示一条虚线
pRightSplitter->setOpaqueResize(false); 
QTextEdit *pUpEdit = new QTextEdit("上部件");
pUpEdit->setAlignment(Qt::AlignCenter);
QTextEdit *pMiddleEdit = new QTextEdit("中间部件");
pMiddleEdit->setAlignment(Qt::AlignCenter);
QTextEdit *pBottomEdit = new QTextEdit("底部部件");
pBottomEdit->setAlignment(Qt::AlignCenter);
//向右侧分割器添加控件
pRightSplitter->addWidget(pUpEdit);
pRightSplitter->addWidget(pMiddleEdit);
pRightSplitter->addWidget(pBottomEdit);
//向主分割器中添加控件
pMainSplitter->addWidget(pLeftEdit);
pMainSplitter->addWidget(pRightSplitter);
// 设置控件是否可伸缩。
// 参数1 指定控件序号 参数2 大于0时,表示控件可伸缩。小于0则不可伸缩
pMainSplitter->setStretchFactor(0, 1);
//主布局
QHBoxLayout *pHBoxLayout = new QHBoxLayout;
pHBoxLayout->addWidget(pMainSplitter);
this->setLayout(pHBoxLayout)

基本控件

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. 调⽤思维导图库的接⼝,使⽤模拟数据初始化思维导图:

帮助与提示

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

StMindData

列表、树、表格控件

QListWidget

QListWidget类的继承关系如下:

QListWidget -> QListView ->QAbstractItemView ->QAbstractScrollArea -> QFrame ->QWidget.

每个列表项中可以包含文字、图标等内容。实际开发中,我们还可以将指定的窗口或者控件放置到列表项中显示。如QWidget窗口、QLabel文本框、QPushButton按钮、QLineEdit输入框等。

//创建QListWidget
QListWidget *pListWidget = new QListWidget(this);
//添加项
//QListWidgetItem* item = new QListWidgetItem("#########");
//item->setTextAligment(Qt::AlignHCenter | Qt::AlignVCenter); //设置文本对齐方式
//pListWidget->addItem(item);

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();

//将指定Widget窗口添加到item列表项中
void setItemWidget(QListWidgetItem* item, QWidget* wgt);

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

QTreeWidget

类的继承关系如下:

QTreeWidget->QTreeView->QAbstractItemView->QAbstractScrollArea->QFrame->QWidget

//创建树
QTreeWidget* pTreeWgt = new QTreeWidget(this);
//设置头标签
pTreeWgt->setHeaderLabels(QStringList() << "英雄" << "英雄介绍");
//item创建
QTreeWidgetItem* liItem = new QTreeWidgetItem(QStringList() << "力量");
//添加顶层级别item
pTreeWgt->addTopLevelItem(liItem);
QStringList heroL1, heroL2;
heroL1 << "刚被猪" << "xxxxxxxx";
heroL2 << "船长" << "xxxxxxxxxxxxx";
//创建子Item,挂载到顶层item上
QTreeWidgetItem* l1 = new QTreeWidgetItem(heroL1);
liItem->addChild(l1);
QTreeWidgetItem* l2 = new QTreeWidgetItem(heroL2);
liItem->addChild(l2);

//表头是否隐藏,此处隐藏标题
pTreeWgt->setHeaderHidden(true);

//树形结构构造后默认是折叠的,可将设置项全部展开
pTreeWgt->expandAll();

//给每个项添加图标
pFirstRootItem->setIcon(0, QIcon(":/myIcon.png"));

//添加勾选框(树形控件经常需要展示勾选状态)
pFirstRootItem->setCheckState(0, Qt::Unchecked);

当树中的项的勾选状态发生变化时,树形控件会发射itemChanged信号,我们可以连接该信号到槽函数进行一些处理。如使其所有子节点保持和该节点一样的勾选状态。

connect(pTreeWidget, SIGNAL(itemChanged(QTreeWidgetItem*,int)), this, SLOT(slotProcessItem(QTreeWidgetItem*,int);

void slotProcessItem(QTreeWidgetItem* item, int column)
{
    if(!item) return;
    //获取当前勾选状态
    Qt::CheckState state = item->checkState(column);
    //屏蔽树形信号
    pTreeWidget->blockSignals(true);
    //遍历子节点
    QTreeWidgetItemIterator it(item);
    while (*it)
    {
        //设置勾选状态
        (*it)->setCheckState(0, state);
        ++it;
    }
    //解除信号屏蔽
    pTreeWidget->blockSignals(true);
}

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);
//设置正文
pTableWgt->setItem(0,0,new QTableWidgetItem("xx"));


//点击添加赵云,实现添加
connect(ui->btn_add, &QPushButton::cilcked, [=](){
    bool isEmpty = pTableWgt->findItems("赵云", Qt::MatchExactly).isEmpty();
    if(!isEmpty)
    {
        QMessageBox::warning(this, "警告", "已有赵云,添加失败!");
    }
    else
    {
        pTableWgt->insertRow(0);
        pTableWgt->setItem(0,0,new QtableWidgetItem(QString("赵云")));
        pTableWgt->setItem(0,1,new QTableWidgetItem(QString("男")));
        pTableWgt->setItem(0,2,new QTableWidgetItem(QString::number(30)));
    }
});

//点击删除赵云,实现删除
connect(ui->btn_del, &QPushButton::cilcked, [=](){
    bool isEmpty = pTableWgt->findItems("赵云", Qt::MatchExactly).isEmpty();
    if(isEmpty)
    {
        QMessageBox::warning(this, "警告", "没有赵云,删除失败!");
    }
    else
    {
        int rowNum = pTableWgt->findItems("赵云", Qt::MatchExactly).first()->row(0);
        pTableWgt->removeRow(rowNum);
    }
});

其他常用控件

// stacked widget

//设置默认选中第一个
ui->stackedWidget->setCurrentIndex(0);
connect(ui->btn_scroll, &QPushButton::clicked,[=](){
    // 设置当前索引
    ui->stackedWidget->setCurrentIndex(0);
});

//combo box下拉框
//点击保时捷,定位到相应选项
connect(ui->btn_select,&QPushButton::clicked,[=](){
    //ui->comboBox->setCurrentIndex(2);
    ui->comboBOx->setCurrentText("保时捷");
});

//利用QLabel显示图片
QPixmap pix;
pix.load(":/Image/butterFly.png");
ui->label_img->setPixmap(pix);
ui->label_img->setFixedSize(pix.width(), pix.height());

//利用Qlabel显示动图
QMovie* movie = new QMovie(":/Image/mario.gif");
ui->label_movie->setMovie(movie);
movie->start();

movie->setSpeed(300);

connect(movie, &QMovie::frameChanged,[=](int frameId){
    if(frameId == movie->frameCount()-1)
        movie->stop();
});

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

自定义控件

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

封装

提升

复用

Qt程序编译流程

在构建 Qt 程序的过程中,打开编译器的编译输出窗⼝,可以看到以下内容:

uic TestProject.ui

rcc TestProject.qrc

moc TestProject.h

3 条命令分别⽤于解析 ui ⽂件、qrc ⽂件,以及头⽂件。

Qt 的安装⽬录下,可以找到 uicrccmoc 3 个⼯具。

uic User Interface Compiler,将界⾯⽂件(.ui)⽣成 C++ 代码。

rcc Resource Compiler,将 qrc ⽂件⽣成 C++ 代码。

moc Meta-Object Compilermoc ⼯具读取头⽂件,如果找到⼀个或多个包含 Q_OBJECT 宏的类,它 将为这些类⽣成⼀个包含元对象(meta-object)代码的 C++ 源⽂件。元对象代码⽤于信号槽机制、运⾏时类型识别、动态属性等⽅⾯。(这就是⾃定义信号槽必须添加 Q_OBJECT 宏的原因)

结果:

uic 根据 TestProject.ui ⽣成 ui_TestProject.h ⽂件;

rcc 根据 TestProject.qrc ⽣成 qrc_TestProject.cpp ⽂件;

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。在上⽅搜索框中输⼊⽂字,⽤于搜索匹配的树节点。

接下来,将该界⾯放到主界⾯左侧,叠加在思维导图上⾯,参考下图(暂不考虑样式)

帮助与提示

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

容器

容器的遍历

//以 QList 为例,Qt 容器的遍历有很多种写法。
QList<QString> list;
list << "A" << "B" << "C" << "D";
//Java ⻛格的迭代器
QListIterator<QString> i(list);
while (i.hasNext())
{
 qDebug() << i.next();
}
//STL ⻛格的迭代器
QList<QString>::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,多个选择器用逗号隔开

属性:color,多个属性时用分号隔开

值:red,多个关键字时大多以空格隔开

选择器类型

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

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

QLabel* pNameLbl = new QLabel;
pNameLbl->setObjectName("nameLabel");

然后再通过“ID选择器”设置样式:

#nameLabel {border: 1px soild red;}

选择器类型:

通用选择器 * 匹配所有控件

类型选择器 QPushButton 匹配给定类型控件,包括子类

类选择器 .QPushButton 匹配给定类型控件,不包括子类

属性选择器 QPushButton[flat=“false”] 匹配给定类型控件中符合[属性]的控件

ID选择器 QPushButton#closeBtn 匹配给定类型,且对象名为closeBtn的控件

子对象选择器 QDialog>QPushButton 匹配给定类型的直接子控件

子孙对象选择器 QDialog QPushButton 匹配给定类型的子孙控件

子控件选择器 QComboBox::drop-down 复杂对象的子控件

伪状态选择器 QPushButton:hover 控件的特定状态下的样式

子控件

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

QComboBox::drop-down {image: url(dropdown.png)}

常用辅助控制器:

::indicator              单选框、复选框、可选菜单项或可选群组项的指示器  

::menu-indicator         按钮的菜单指示器  

::item                   菜单、菜单栏或状态栏项  

::up-button              微调框或滚动条的向下按钮  

::down-button            微调框或滚动条的向上按钮  

::up-arrow               微调框、滚动条或标题视图的向上按钮  

::down-arrow             微调框、滚动条或标题视图的向下按钮  

::drop-down              组合框的下拉箭头  

::title                  群组框的标题

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

伪状态还可以多个连用,达到逻辑与效果。例如,当鼠标悬停在一个被选中的QCheckBox部件上时才应用规则,那么这个规则可以写为:

QCheckBox: hover:checked{color:white}

如果有需要,也可以使用逗号来表示逻辑或操作,例如:

QCheckBox : hover,QCheckBox:checked{color :white)

常用伪状态选择器:

状态 描述

:disabled 控件禁用

:enabled 控件启用

:focus 控件获取输入焦点

:hover 鼠标在控件上悬停

:pressed 鼠标按下

:checked 控件被选中

:unchecked 控件没有选中

:indeterminate 控件部分被选中

:open 控件打开

:closed 控件关闭

:on 控件可以切换,且处于on状态

:off 控件可以切换,且处于off状态

! 对以上状态的否定

这⾥: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 ,但是类型选择器有同样的优先级,与继承关系⽆关,所以这⾥仍是后⾯的样式⽣效。
层叠

样式表可以设置在QApplication上、父部件或者子部件上。部件有效的样式表是通过部件祖先的样式表和QApplication上的样式表合并得到。发生冲突时,部件自己的样式表优先于任何继承的样式表,同理,父部件的样式表优先于祖先的样式表。

继承

使用Qt样式表时,部件并不会自动从父部件继承字体和颜色设置。如,一个QPushButton包含在一个QGroupBox设置样式表。

qApp ->setStyleSheet("QGroupBox {color: red;}");

但没有对QPushButton设置样式表。此时,QPushButton会使用系统颜色,而不会继承QGroupBox的颜色。若想要QGroupBox的颜色设置到子部件上,可以:

aApp ->setStyleSheet( "QGroupBox,QGroupBox * { color: red;}");

设置QObject属性

任何可设计的Q_PROPERTY都可以使用“qproperty-属性名称”语法来设置样式表。如,

MyLabel{ qproperty- pixmap:url(pixmap. png);}

MyGroupBox{ qproperty- titleColor: rgb(100,200,100);}

QPushButton{ qaproperty- iconSize: 20px 20px;}

Demo:

/*style.qss*/

/* 设置Q_PROPERTY定义的属性样式 */
#Widget[checked = true] {
    background-color: rgb(0, 0, 0);
}

/* 设置动态属性样式 */
#Widget[property1 = true] {
    background-color: rgb(255, 0, 0);
}

/* 通过Qss设置Q_PROPERTY定义的属性的值 */
#Widget {
    qproperty-BgColor: rgb(0, 0, 255);
    qproperty-age: age3;                 /* 通过Q_ENUM注册的枚举修改自定义属性值*/
}
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>

QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE

class Widget : public QWidget
{
    Q_OBJECT

    Q_PROPERTY(bool checked READ isChecked WRITE setChecked)
    Q_PROPERTY(QColor BgColor READ isBgColor WRITE setBgColor)
    Q_PROPERTY(AgeEnum age READ age WRITE setAge)               // 想要通过Q_ENUM注册的枚举修改属性值,属性的类型就需要时【枚举的类型】,而不能是其它类型,例如int

public:
    Widget(QWidget *parent = nullptr);
    ~Widget();

    enum AgeEnum {
        age1 = 10,
        age2 = 20,
        age3 = 30
    };
    Q_ENUM(AgeEnum)  // 向元对象系统注册枚举类型(可以使用Q_ENUM或者Q_ENUMS,不过后者已经过时)

    bool isChecked() const;
    void setChecked(bool value);
    QColor isBgColor() const;
    void setBgColor(QColor color);
    AgeEnum age() const;
    void setAge(AgeEnum value);

private slots:
    void on_pushButton_clicked();

    void on_pushButton_2_clicked();

    void on_pushButton_3_clicked();

private:
    void initStyle();

private:
    Ui::Widget *ui;

    bool m_checked = false;
    QColor m_bgColor = QColor(255, 255, 255);
    AgeEnum m_age;
};
#endif // WIDGET_H
#include "widget.h"
#include "ui_widget.h"

#include <QFile>
#include <QTextStream>
#include <QDebug>
#include <QStyle>

Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);
    this->setWindowTitle(QString("Qss属性功能 - V%1").arg(APP_VERSION));

    initStyle();

    qDebug() << "在构造函数中获取属性值:" << m_bgColor.name();   // 无法获取到qss修改后的属性值
}

Widget::~Widget()
{
    delete ui;
}

bool Widget::isChecked() const
{
    return m_checked;
}

void Widget::setChecked(bool value)
{
    m_checked = value;
}

QColor Widget::isBgColor() const
{
    return m_bgColor;
}

void Widget::setBgColor(QColor color)
{
    m_bgColor = color;
}

Widget::AgeEnum Widget::age() const
{
    return m_age;
}

void Widget::setAge(AgeEnum value)
{
    m_age = value;
}

/**
 * @brief 加载qss文件
 */
void Widget::initStyle()
{
    QString strFile = qApp->applicationDirPath() + "/style.css";   // 这里我没有使用资源文件,而是把样式表文件放在当前路径下,便于随时更换
    QFile file(strFile);
    if(file.open(QIODevice::ReadOnly))
    {
        QTextStream stream(&file);

        QString strQss;
        while (!stream.atEnd())
        {
            strQss.append(stream.readLine());
        }
        qApp->setStyleSheet(strQss);               // 设置整个程序的样式表而不是当前窗口
    }
    else
    {
        qWarning() << "打开qss文件失败!";
    }
}

/**
 * @brief 通过Q_PROPERTY定义的属性更新Qss样式
 *        设置属性的方式有两种
 *        方式一:setChecked
 *        方式二:setProperty("checked", value)  : 设置成功返回true,否则返回false
 */
void Widget::on_pushButton_clicked()
{
    //this->setProperty("checked", true);
    this->setChecked(!this->isChecked());   // 更改控件的属性 【Q_PROPERTY】
    this->style()->polish(this);            // 属性值更改后重新初始化给定控件的样式。
}

/**
 * @brief 通过动态属性的方式更新QSS样式
 *        如果没有通过Q_PROPERTY定义属性,使用setProperty("property1", value)
 *        设置后会将property1添加为动态属性,并且返回false,
 *        效果和使用Q_PROPERTY定义的属性类似
 */
void Widget::on_pushButton_2_clicked()
{
    static bool value = true;
    qDebug() << this->setProperty("property1", value);   // 设置动态属性 false
    value = !value;

    this->style()->polish(this);                         // 属性值更改后重新初始化给定控件的样式。
}

/**
 * @brief 在Qss通过qproperty-属性 的方式修改属性的值,qproperty 语法只在程序启动显示控件是生效一次
 *        在构造函数中由于控件还没有开始显示,所以qproperty没生效,是无法获取修改后的属性值的,在窗口显示后就可以获取到属性值
 *        注意:虽然主要继承于QObject的类都可以通过Q_PROPERTY定义属性,但是只有继承于QWidget的类定义的属性可以通过Qss修改,
 *             因为QObject不包含QStyle
 */
void Widget::on_pushButton_3_clicked()
{
    qDebug() << "程序启动后获取属性值:" << m_bgColor.name();
    qDebug() <<"Qss设置的属性值:" << m_age;
}

Qss自定义属性-CSDN博客

Qt之QSS(Q_PROPERTY-自定义属性)_qt qss-CSDN博客

盒子模型

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

示例 1
下图中 QWidget ⾥⾯布局放了⼀个 QLabel ,然后设置样式表:
QLabel {
border: 5px solid green;
margin: 10px 30px 20px 40px;
padding: 20px 30px 40px 10px;
}

即外边距上右下左的顺序,分别为 10px30px20px40px。 边框为 5px

内边距上右下左顺序,分别为 20px30px40px10px

示例 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

控件示例

QLabel
设置字体样式
QLabel{
/*分开设置*/
/*font-family:”楷体”;
font-size: 20px;
font-style: italic; 
font-weight:bold ; */
/*快捷设置*/
 font:bold italic 18px "微软雅黑"; 
color:cornflowerblue;
}

px 像素 italic 倾斜 normal 不倾斜 bold 加粗 normal 不加粗

font 同时设置字体style weight size family的样式。但是style和weight必须出现在开头,size和family在后面,且size必须在family之前,否则样式不生效。Font中不能设置颜色。

color 可使用十六进制表示颜色,也可使用某些特殊的字体颜色:red green blue等,或者使用rgb(r,g,b)和rgba(r,g,b,a)来设置。其中 r、g、b、a 值为0~255,如果想不显示颜色可以设置值为透明 transparent。

文字位置
QLabel{
padding-left: 10px;
 padding-top: 8px;
 padding-right: 7px; 
padding-bottom: 9px;
} 

一般 padding-left 相当于 x 坐标,padding-top 相当于 y 坐标

边框样式
QLabel
{
    /*分开设置*/
    border-style: solid;
    border-width: 2px;
    border-color:darkgoldenrod;
    /*快捷设置*/
    border:2px solid red;
}

solid 为实线, dashed 为虚线, dotted 为点线, none 为不显示(如果不设置 border-style 的话,默认会设置为 none)

border 为同时设置 border 的 width style color 属性,但值的顺序必须是按照 width style color 来写,不然不会生效!

单独设置某条边框的样式
QLabel
{
    border-left: 2px solid red;
    border-top: 2px solid black;
    border-right: 2px solid blue;
    border-bottom-color: transparent;	/*下边框透明,不显示*/
}
设置边框半径(圆角)
QLabel
{
    border-left: 2px solid red;
    border-top: 2px solid black;
    border-right: 2px solid blue;
    border-bottom: 2px solid yellow;

	border-top-left-radius: 20px;
	border-top-right-radius: 15px;
	border-bottom-left-radius: 10px;
	border-bottom-right-radius: 5px;	
	/*border-radius: 20px;*/
}

border-radius 为设置所有边框圆角半径,单位为 px 像素,通过圆角半径可以实现圆形的 Label

背景样式
QLabel
{
    background-color: #2E3648;
	background-image: url("./image.png");
	background-repeat: no-repeat; 
	background-position: left center;
	/*background: url("./image.png") no-repeat left center #2E3648;*/
}

background-repeat 为设置背景图是否重复填充背景,如果背景图片尺寸小于背景实际大小的话,默认会自动重复填充图片,可以设置为 no-repeat 不重复,repeat-x 在x轴重复,repeat-y 在y轴重复。

background-position 为设置背景图片显示位置,只支持 left right top bottom center;值 left right center为设置水平位置,值 top bottom center 为设置垂直位置。

background 为设置背景的所有属性,color image repeat position 这些属性值出现的顺序可以任意。

QPushButton
QPushButton
{
    /*1*/
	border:none;	/*去掉边框*/
	border-radius:10px;
    /*2,添加图片*/
  	background-image: url(:/images/quit.png);
	background-repeat:none;
	background-position:center;
    /*3,把图片作为边框,会自动铺满背景*/
    border-image: url(:/images/quit.png);
}
QPushButton:hover
{
	background-color:rgba(102,205,227,255);
}
QPushButton:pressed
{
    /*按键按下瞬间显示*/
	background-color:rgb(48,188,218);
}
QPushButton:checked
{
    /*按键按下显示*/
	background-color:rgb(48,188,218);
}
QCheckBox QRadioButton
QCheckBox
{
	color:red;
}
QCheckBox::indicator
{
	width:16px;
	height:16px;
	border-image: url(:/images/checkbox-unchecked.png);
	border-radius:5px;		
}
QCheckBox::indicator:checked
{	
	border-image: url(:/images/checkbox-checked.png);
}
QCheckBox::indicator:unchecked:hover
{	
	border-image: url(:/images/checkbox-unchecked-hover.png);
}
QCheckBox::indicator:checked:hover
{
	border-image: url(:/images/checkbox-checked-hover.png);
}
QGroupBox
QGroupBox {
    background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
                      stop: 0 #E0E0E0, stop: 1 #EEEEEE);
    border: 2px solid gray;
    border-radius: 5px;
    margin-top: 10px; /* leave space at the top for the title */
}
QGroupBox::title {
    subcontrol-origin: margin;
    subcontrol-position: top center; /* position at the top center */
    padding: 2px 3px;
    color: white;
    margin-top: 2px;
    background-color: gray;
    border-radius: 3px;
    spacing: 5px;
}
QGroupBox::indicator {
    width: 13px;
    height: 13px;
    border: 1px solid black;
    background: white;
}
QGroupBox::indicator:checked {
    background: yellow;
}
QComboBox
QComboBox {
    color: black;
	border:1px solid black;
	border-radius:5px;
	padding: 1px 1px 1px 1px;	/*不加这个圆角会有缺失*/
}
QComboBox::drop-down
{
	width:25px;
	border-image: url(:/images/comboBox/drop-down.png);
}
QComboBox::drop-down:hover
{
	border-image: url(:/images/comboBox/drop-down-hover.png);
}
/*把checked换成on也行*/
QComboBox::drop-down:checked
{
	border-image: url(:/images/comboBox/drop-down-on.png);
}
QComboBox::drop-down:checked:hover
{
	border-image: url(:/images/comboBox/drop-down-on-hover.png);
}
QSpinBox

        QSpinBox的subcontrol有::up-button,::down-button,::up-arrow,::down-arrow。up-button 显示在QSpinBox里,它的subcontrol-origin是相对于QSpinBox的;down-button 显示在QSpinBox里,它的subcontrol-origin是相对于QSpinBox的;up-arrow 显示在up-button里,它的subcontrol-origin是相对于up-button的;down-arrow 显示在 down-button里,它的subcontrol-origin是相对于down-button的。

QSpinBox
{
	border:1px solid black;
	border-radius:5px;
}
/*按钮*/
QSpinBox:down-button,QSpinBox:up-button
{
	width:16px;
	height:15px;
	subcontrol-origin:padding;
	background:white;
	border:2px solid rgb(217,217,217);
	border-radius:5px;
}
QSpinBox:down-button
{
	subcontrol-position:left center;
}
QSpinBox:up-button
{
	subcontrol-position:right center;
}
QSpinBox:down-button:hover,QSpinBox:up-button:hover
{
	border:2px solid rgb(138,138,138);
}
/*箭头*/
QSpinBox:down-arrow
{	
	border-image: url(:/images/spinBox/down-arrow.png);
}
QSpinBox:up-arrow
{
	border-image: url(:/images/spinBox/up-arrow.png);
}
QSpinBox:down-arrow:hover
{	
	border-image: url(:/images/spinBox/down-arrow-hover.png);
}
QSpinBox:up-arrow:hover
{
	border-image: url(:/images/spinBox/up-arrow-hover.png);
}
QSlider

        QSlider的subcontrol有 ::groove(槽),::handle,::add-page 和 ::sub-page。groove 显示在 QSlider 里,它的 subcontrol-origin 是相对于 QSlider 的;handle 显示在 groove 里,它的 subcontrol-origin 是相对于 groove 的;sub-page 显示在 groove 里,它的 subcontrol-origin 是相对于 groove 的;add-page 显示在 groove 里,它的 subcontrol-origin 是相对于 groove 的;handle, sub-page, add-page虽然都显示在groove里,但是都可以把它们扩展到groove外。

QSlider::groove:horizontal
{
    border: 1px solid skyblue;
    background-color: skyblue;
    height: 10px;
    border-radius: 5px;
}
QSlider::handle:horizontal
{
	background: qradialgradient(spread:pad, cx:0.5, cy:0.5, radius:0.5, fx:0.5, fy:0.5, stop:0.7 white,stop:0.8 rgb(143,212,255));
    width: 20px;
    border-radius: 10px;    
    margin-top: -5px;
    margin-bottom: -5px;
}
QSlider::sub-page:horizontal 
{
    background: #999;
    margin: 5px;
    border-radius: 5px;
}
QSlider::add-page:horizontal 
{
    background: #666;
    margin: 5px;
    border-radius: 5px;
}
QProgressBar

        对于QProgressBar的QSS,大多数都是想把chunk定义为圆角矩形的样子。但是当它的value较小时,chunk的圆角会变成直角。若想要更改QProgressBar外观,推荐继承QProgressBar自己绘制或者使用QStyle。

/*边框*/
QProgressBar
{
	border:1px solid skyblue;
	border-radius:5px;
	height:5px;
    text-align: center;
}
/*进度条*/
QProgressBar::chunk
{
    background-color: steelblue;
    border-radius: 5px;
}

QProgressBar
{
    border-color: 1px solid blue;
    border-radius: 5px;
    text-align: center;
}

QProgressBar:chunk
{
    background-color: aqua;	/*设置块的颜色*/
    width: 5px;		/*块的宽度*/
    margin: 0.5px;	/*让每个块之间有点间隔*/
}

若最大值最小值都是0,则会显示一个繁忙提示,等待系统容错处理结束,再继续恢复加载。

progressBar->setRange(0,0);

QWidget#TitleWidget {
	image: url(:/title/images/title/title.png);
}
#leftSideBtn {
	border-image:url(:/title/images/title/left_show.png);
}
#leftSideBtn:hover {
	border-image:url(:/title/images/title/left_show_h.png);
}
#leftSideBtn:checked {
	border-image:url(:/title/images/title/left_hide.png);
}
#leftSideBtn:checked:hover {
	border-image:url(:/title/images/title/left_hide_h.png);
}

#rightSideBtn {
	border-image:url(:/title/images/title/right_show.png);
}
#rightSideBtn:hover {
	border-image:url(:/title/images/title/right_show_h.png);
}
#rightSideBtn:checked {
	border-image:url(:/title/images/title/right_hide.png);
}
#rightSideBtn:checked:hover {
	border-image:url(:/title/images/title/right_hide_h.png);
}

#settingBtn, #exitBtn {
	border-width: 0;
	border-style: none;
	border-image: none;
	border-left-width: 10px;
	padding-left: 20px;
	padding-right: 10px;
	padding-top: 6px;
	padding-bottom: 6px;
	background-repeat: no-repeat;
	background-position: left center;
	background-origin: padding;
	color:#7e97ae;
}

#settingBtn {
	background-image: url(:/title/images/title/setting.png);
}
#settingBtn:hover {
	background-image: url(:/title/images/title/setting_h.png);
	color:#34c0be;
}

#exitBtn {
	background-image: url(:/title/images/title/exit.png);
}
#exitBtn:hover {
	background-image: url(:/title/images/title/exit_h.png);
	color:#34c0be;
}
#timeLbl {
	color: #C4F2FF;
	font-size: 26px;
	font-family: "LCD AT&T Phone Time/Date";
}

* {
	font-family: "Microsoft YaHei";
	outline: 0px;
}

#MainWindow {
	background: #000000;
}

/* HLine 水平线 */
QFrame[frameShape="4"] {
	border: none;
	background-color: #233245;
	max-height: 1px;
}

/* VLine 垂直线 */
QFrame[frameShape="5"] {
	border: none;
	background-color: #233245;
	max-width: 1px;
}

/* QLabel */
QLabel {
	color: #7E97AE;
}

/* 按钮 begin */
QPushButton {
	border: none;
}

QPushButton {
	border-image: url(:/button/images/button/normal.png) 2px;
	border-width: 2px;
	color: #A9BEDF;
}

QPushButton:hover {
	border-image: url(:/button/images/button/hover.png) 2px;
}

QPushButton:pressed {
	border-image: url(:/button/images/button/pressed.png) 2px;
}

QPushButton:checked {
	border-image: url(:/button/images/button/checked.png) 2px;
}

QPushButton:disabled {
	border-image: url(:/button/images/button/disabled.png) 2px;
}
/* 按钮 end */

/* 文本输入框 begin */
QLineEdit, QTextEdit, QPlainTextEdit {
	color: #7E97AE;
	padding: 0px 8px;
	border-image: url(:/edit/images/edit/normal.png) 1px;
	border-width: 1px;
	font-size: 13px;
}

QLineEdit:hover, QTextEdit:hover, QPlainTextEdit:hover {
	border-image: url(:/edit/images/edit/hover.png) 1px;
}

QLineEdit:focus, QTextEdit:focus, QPlainTextEdit:focus {
	border-image: url(:/edit/images/edit/focus.png) 1px;
	color: #bed6fa;
}

QLineEdit[readOnly="true"], QTextEdit[readOnly="true"], QPlainTextEdit[readOnly="true"] {
	border-image: url(:/edit/images/edit/Readonly_normal.png) 1;
}

QLineEdit:disabled, QTextEdit:disabled, QPlainTextEdit:disabled {
	border-image: url(:/edit/images/edit/Readonly_normal.png) 1;
	color: #566b8b;
}
/* 文本输入框 end */

/* 勾选框 begin */
QCheckBox {
	color: #bed6fa;
}

QCheckBox:indicator {
	margin-top: 2px;
}

QCheckBox:disabled {
	color: #566b8b;
	background: transparent;
}

QCheckBox:indicator:unchecked {
	image: url(:/checkbox/images/checkbox/unchecked_normal.png);
}

QCheckBox:indicator:unchecked:hover {
	image: url(:/checkbox/images/checkbox/unchecked_hover.png);
}

QCheckBox:indicator:unchecked:pressed {
	image: url(:/checkbox/images/checkbox/unchecked_pressed.png);
}

QCheckBox:indicator:unchecked:disabled {
	image: url(:/checkbox/images/checkbox/unchecked_disabled.png);
}

QCheckBox:indicator:indeterminate {
	image: url(:/checkbox/images/checkbox/half_normal.png);
}

QCheckBox:indicator:indeterminate:hover {
	image: url(:/checkbox/images/checkbox/half_hover.png);
}

QCheckBox:indicator:indeterminate:pressed {
	image: url(:/checkbox/images/checkbox/half_pressed.png);
}

QCheckBox:indicator:indeterminate:disabled {
	image: url(:/checkbox/images/checkbox/half_disabled.png);
}

QCheckBox:indicator:checked {
	image: url(:/checkbox/images/checkbox/checked_normal.png);
}

QCheckBox:indicator:checked:hover {
	image: url(:/checkbox/images/checkbox/checked_hover.png);
}

QCheckBox:indicator:checked:pressed {
	image: url(:/checkbox/images/checkbox/checked_pressed.png);
}

QCheckBox:indicator:checked:disabled {
	image: url(:/checkbox/images/checkbox/checked_disabled.png);
}
/* 勾选框 end */

/* 多选框 begin */
QComboBox {
	background: transparent;
	color: #7E97AE;
	border-image: url(:/edit/images/edit/normal.png) 1;
	border-width: 1px;
	padding: 0px 8px;
	font-size: 13px;
}

QComboBox:hover {
	border-image: url(:/edit/images/edit/hover.png) 1;
}

QComboBox:disabled {
	border-image: url(:/edit/images/edit/Readonly_normal.png) 1;
	color: #566b8b;
}

QComboBox::drop-down:disabled {
	image: url(:/combobox/images/combobox/dropdown_disabled.png);
}

QComboBox::drop-down {
	image: url(:/combobox/images/combobox/dropdown_normal.png);
}

QComboBox::drop-down:hover {
	image: url(:/combobox/images/combobox/dropdown_hover.png);
}

QComboBox::drop-down:on {
	image: url(:/combobox/images/combobox/dropdown_presssed.png);
}

QComboBox QAbstractItemView {
	/*background-color: rgba(18, 23, 31, 0.9);
	border: 1px solid #7594b8;*/
	background-color:transparent;
	margin-top: 1px;
	border-image: url(:/edit/images/edit/normal.png) 1px;
	border-width: 1px;
	selection-color: #5affca;
	selection-background-color: transparent;
	color: #7E97AE;
}
/* 多选框 end */

QSpinBox, QDoubleSpinBox, QTimeEdit, QDateTimeEdit {
	padding: 0 5;
	border-image: url(:/edit/images/edit/normal.png) 1;
	border-width: 1px;
	color: #7e97ae;
	selection-background-color: #30d0de;
	selection-color: #121820;
	font-size: 13px;
}

QSpinBox:hover, QDoubleSpinBox:hover, QTimeEdit:hover, QDateTimeEdit:hover {
	border-image: url(:/edit/images/edit/hover.png) 1;
	border-width: 1px;
}

QSpinBox:focus, QDoubleSpinBox:focus, QTimeEdit:focus, QDateTimeEdit:focus {
	border-image: url(:/edit/images/edit/focus.png) 1;
	border-width: 1px;
	color: #bed6fa;
}

QSpinBox:disabled, QDoubleSpinBox:disabled, QTimeEdit:disabled, QDateTimeEdit:disabled {
	border-image: url(:/edit/images/edit/Readonly_normal.png) 1;
	border-width: 1px;
	color: #566b8b;
}

QSpinBox::up-arrow, QDoubleSpinBox::up-arrow, QTimeEdit::up-arrow, QDateTimeEdit::up-arrow {
	image: url(:/spinbox/images/spinbox/spinup_normal.png);
	width: 23px;
	height: 12px;
}

QSpinBox::up-arrow:hover, QDoubleSpinBox::up-arrow:hover, QTimeEdit::up-arrow:hover, QDateTimeEdit::up-arrow:hover {
	image: url(:/spinbox/images/spinbox/spinup_hover.png);
}

QSpinBox::up-arrow:pressed, QDoubleSpinBox::up-arrow:pressed, QTimeEdit::up-arrow:pressed, QDateTimeEdit::up-arrow:pressed {
	image: url(:/spinbox/images/spinbox/spinup_pressed.png);
}

QSpinBox::down-arrow, QDoubleSpinBox::down-arrow, QTimeEdit::down-arrow, QDateTimeEdit::down-arrow {
	image: url(:/spinbox/images/spinbox/spindown_normal.png);
	width: 23px;
	height: 12px;
}

QSpinBox::down-arrow:hover, QDoubleSpinBox::down-arrow:hover, QTimeEdit::down-arrow:hover, QDateTimeEdit::down-arrow:hover {
	image: url(:/spinbox/images/spinbox/spindown_hover.png);
}

QSpinBox::down-arrow:pressed, QDoubleSpinBox::down-arrow:pressed, QTimeEdit::down-arrow:pressed, QDateTimeEdit::down-arrow:pressed {
	image: url(:/spinbox/images/spinbox/spindown_pressed.png);
}

QSpinBox::up-button, QDoubleSpinBox::up-button, QTimeEdit::up-button, QDateTimeEdit::up-button {
	background: transparent;
	subcontrol-origin: border;
	subcontrol-position: top right;
	width: 23px;
	height: 12px;
	border-width: 1px;
}

QSpinBox::down-button, QDoubleSpinBox::down-button, QTimeEdit::down-button, QDateTimeEdit::down-button {
	background: transparent;
	subcontrol-origin: border;
	subcontrol-position: bottom right;
	width: 23px;
	height: 12px;
}

QDateTimeEdit::drop-down {
	min-width: 8;
	max-width: 8;
	min-height: 5;
	max-height: 5;
	image: url(:/spinbox/images/spinbox/spindown_normal.png);
}

/* ListView begin */

QListView {
	border: none;
	outline: none;
	background-color: transparent;
	color: #7E97AE;
	font-size: 13px;
}

QListView::item {
	padding-left: 10px;
	height: 26px;
	border: none;
}

QListView::item:hover {
	color: #78FFFA;
}

QListView::item:selected {
	padding-left: 5px;
	border-image: url(:/treeview/images/treeview/item_selected.png) 1px;
	border-width: 1px;
	color: #78FFFA;
}

/* ListView end */

/* 树 begin */

QTreeView {
	background-color: transparent;
	outline: 0px;
	border: none;
}

QTreeView::item {
	font-size: 13px;
	height: 26px;
	outline: 0px;
	color: #687f95;
	margin-right: 1px;
	margin-bottom: 1px;
	padding-left:1px;
}

QTreeView::item:hover {
	background-color: transparent;
	color: #78fffa;
	padding-left:1px;
}

QTreeView::item:selected {
	border-image: url(:/treeview/images/treeview/item_selected.png) 1px;
	border-width:1px;
	color: #78fffa;
}

QTreeView::branch:has-siblings:!adjoins-item {
	border-image: url(:/treeview/images/treeview/vline.png);
}

QTreeView::branch:has-siblings:!adjoins-item:disabled {
	border-image: url(:/treeview/images/treeview/vline.png);
}

QTreeView::branch:has-siblings:adjoins-item {
	border-image: url(:/treeview/images/treeview/branch_more.png);
}

QTreeView::branch:has-siblings:adjoins-item:disabled {
	border-image: url(:/treeview/images/treeview/branch_more.png);
}

QTreeView::branch:!has-children:!has-siblings:adjoins-item {
	border-image: url(:/treeview/images/treeview/branch_end.png);
}

QTreeView::branch:!has-children:!has-siblings:adjoins-item:disabled {
	border-image: url(:/treeview/images/treeview/branch_end.png);
}

QTreeView::branch:has-children:!has-siblings:closed, QTreeView::branch:closed:has-children:has-siblings {
	image: url(:/treeview/images/treeview/branch_close.png);
	border-image: none;
}

QTreeView::branch:has-children:!has-siblings:disabled, QTreeView::branch:closed:has-children:has-siblings:disabled {
	image: url(:/treeview/images/treeview/branch_close.png);
	border-image: none;
}

QTreeView::branch:open:has-children:!has-siblings, QTreeView::branch:open:has-children:has-siblings {
	image: url(:/treeview/images/treeview/branch_open.png);
	border-image: none;
}

QTreeView::branch:open:has-children:!has-siblings:disabled, QTreeView::branch:open:has-children:has-siblings:disabled {
	image: url(:/treeview/images/treeview/branch_open.png);
	border-image: none;
}

/* 树 end */

/* 表格 begin */

QHeaderView {
	background-color: transparent;
	border-image: url(:/tableview/images/tableview/cornerButton_normal.png);
	color: #ffffff;
	qproperty-showSortIndicator: 0;
	qproperty-highlightSections: 0;
}

QHeaderView::section {
	background: transparent;
	min-height: 30px;
	border: 1px solid #263142;
}

QHeaderView::section:first:horizontal {
	border-top: none;
	border-bottom: none;
	border-image: url(:/tableview/images/tableview/h_section_normal.png);
}

QHeaderView::section:vertical {
	border-left: none;
	border-right: none;
}

QHeaderView::section:first:vertical, QHeaderView::section:last {
	border: none;
	border-image: url(:/tableview/images/tableview/cornerButton_normal.png);
}

QHeaderView::section:vertical {
	background: transparent;
	min-width: 30px;
	border-image: url(:/tableview/images/tableview/cornerButton_normal.png);
}

QHeaderView::section:vertical:hover {
	border-image: url(:/tableview/images/tableview/v_section_hover.png);
}

QHeaderView::section:vertical:pressed {
	border-image: url(:/tableview/images/tableview/v_section_pressed.png);
}

QTableView {
	background: #101b26;
	alternate-background-color: #0a131b;
	outline: none;
	border: none;
	qproperty-showGrid: 1;
	color: #7e97ae;
	gridline-color: #263142;
}

QTableView::item {
	border-image: url();
}

QTableView::item:hover {
	border-image: url(:/tableview/images/tableview/item_hover.png);
	border-width: 2px;
}

QTableView::item:selected {
	border-image: url(:/tableview/images/tableview/item_selected.png);
	border-width: 2px;
	color: #2aa8f0;
}

QTableView QTableCornerButton::section {
	background-color: transparent;
	border-image: url(:/tableview/images/tableview/cornerButton_normal.png);
}

QTableView QTableCornerButton::section:pressed {
	background-color: transparent;
	border-image: url(:/tableview/images/tableview/cornerButton_pressed.png);
}

/* 表格 end */

/* ListView、TreeView、TableView通用 begin */

QTreeView:indicator, QTableView:indicator, QListView:indicator {
	width: 18px;
	height: 18px;
	margin-top: 0px;
}

QTreeView:indicator:unchecked, QTableView:indicator:unchecked, QListView:indicator:unchecked {
	image: url(:/checkbox/images/checkbox/unchecked_normal.png);
}

QTreeView:indicator:unchecked:hover, QTableView:indicator:unchecked:hover, QListView:indicator:unchecked:hover {
	image: url(:/checkbox/images/checkbox/unchecked_hover.png);
}

QTreeView:indicator:unchecked:pressed, QTableView:indicator:unchecked:pressed, QListView:indicator:unchecked:pressed {
	image: url(:/checkbox/images/checkbox/unchecked_pressed.png);
}

QTreeView:indicator:unchecked:disabled, QTableView:indicator:unchecked:disabled, QListView:indicator:unchecked:disabled {
	image: url(:/checkbox/images/checkbox/unchecked_disabled.png);
}

QTreeView:indicator:indeterminate, QTableView:indicator:indeterminate, QListView:indicator:indeterminate {
	image: url(:/checkbox/images/checkbox/half_normal.png);
}

QTreeView:indicator:indeterminate:hover, QTableView:indicator:indeterminate:hover, QListView:indicator:indeterminate:hover {
	image: url(:/checkbox/images/checkbox/half_hover.png);
}

QTreeView:indicator:indeterminate:pressed, QTableView:indicator:indeterminate:pressed, QListView:indicator:indeterminate:pressed {
	image: url(:/checkbox/images/checkbox/half_pressed.png);
}

QTreeView:indicator:indeterminate:disabled, QTableView:indicator:indeterminate:disabled, QListView:indicator:indeterminate:disabled {
	image: url(:/checkbox/images/checkbox/half_disabled.png);
}

QTreeView:indicator:checked, QTableView:indicator:checked, QListView:indicator:checked {
	image: url(:/checkbox/images/checkbox/checked_normal.png);
}

QTreeView:indicator:checked:hover, QTableView:indicator:checked:hover, QListView:indicator:checked:hover {
	image: url(:/checkbox/images/checkbox/checked_hover.png);
}

QTreeView:indicator:checked:pressed, QTableView:indicator:checked:pressed, QListView:indicator:checked:pressed {
	image: url(:/checkbox/images/checkbox/checked_pressed.png);
}

QTreeView:indicator:checked:disabled, QTableView:indicator:checked:disabled, QListView:indicator:checked:disabled {
	image: url(:/checkbox/images/checkbox/checked_disabled.png);
}

/* ListView、TreeView、TableView通用 end */

/* 滚动条 begin*/

QScrollBar::add-page, QScrollBar::sub-page {
	background: none;
}

/* 垂直 ------*/

QScrollBar::vertical {
	width: 8px;
	border-image: url(:/scrollbar/images/scrollbar/v_groove_normal.png);
	padding: 10px 0px;
}

QScrollBar::handle:vertical {
	border-image: url(:/scrollbar/images/scrollbar/V_handle_bg_normal.png);
	image: url(:/scrollbar/images/scrollbar/V_handle_normal.png)
}

QScrollBar::handle:vertical:hover {
	border-image: url(:/scrollbar/images/scrollbar/V_handle_bg_hover.png);
	image: url(:/scrollbar/images/scrollbar/V_handle_hover.png);
}

QScrollBar::handle:vertical:pressed {
	border-image: url(:/scrollbar/images/scrollbar/V_handle_bg_pressed.png);
	image: url(:/scrollbar/images/scrollbar/V_handle_pressed.png);
}

QScrollBar::handle:vertical:disabled {
	border-image: url(:/scrollbar/images/scrollbar/V_handle_bg_disabled.png);
	image: url(:/scrollbar/images/scrollbar/V_handle_disabled.png);
}

QScrollBar::up-arrow {
	image: url(:/scrollbar/images/scrollbar/arrowup_normal.png);
}

QScrollBar::up-arrow:hover {
	image: url(:/scrollbar/images/scrollbar/arrowup_hover.png);
}

QScrollBar::up-arrow:pressed {
	image: url(:/scrollbar/images/scrollbar/arrowup_pressed.png);
}

QScrollBar::down-arrow {
	image: url(:/scrollbar/images/scrollbar/arrowdown_normal.png);
}

QScrollBar::down-arrow:hover {
	image: url(:/scrollbar/images/scrollbar/arrowdown_hover.png);
}

QScrollBar::down-arrow:pressed {
	image: url(:/scrollbar/images/scrollbar/arrowdown_pressed.png);
}

QScrollBar::add-line:vertical {
	subcontrol-position: bottom;
	subcontrol-origin: margin;
	height: 8;
	border: none;
}

QScrollBar::sub-line:vertical {
	subcontrol-position: top;
	subcontrol-origin: margin;
	height: 8;
	border: none;
}

/* 垂直 ------ */

/* 水平 ------ */

QScrollBar:horizontal {
	height: 8px;
	border-image: url(:/scrollbar/images/scrollbar/h_groove_normal.png);
	padding: 0px 10px;
}

QScrollBar::handle:horizontal {
	border-image: url(:/scrollbar/images/scrollbar/H_handle_bg_normal.png);
	image: url(:/scrollbar/images/scrollbar/H_handle_normal.png);
}

QScrollBar::handle:horizontal:hover {
	border-image: url(:/scrollbar/images/scrollbar/H_handle_bg_hover.png);
	image: url(:/scrollbar/images/scrollbar/H_handle_hover.png);
}

QScrollBar::handle:horizontal:pressed {
	border-image: url(:/scrollbar/images/scrollbar/H_handle_bg_pressed.png);
	image: url(:/scrollbar/images/scrollbar/H_handle_pressed.png);
}

QScrollBar::handle:horizontal:disabled {
	border-image: url(:/scrollbar/images/scrollbar/H_handle_bg_disabled.png);
	image: url(:/scrollbar/images/scrollbar/H_handle_disabled.png);
}

QScrollBar::left-arrow {
	image: url(:/scrollbar/images/scrollbar/arrowleft_normal.png);
}

QScrollBar::left-arrow:hover {
	image: url(:/scrollbar/images/scrollbar/arrowleft_hover.png);
}

QScrollBar::left-arrow:pressed {
	image: url(:/scrollbar/images/scrollbar/arrowleft_pressed.png);
}

QScrollBar::right-arrow {
	image: url(:/scrollbar/images/scrollbar/arrowdright_normal.png);
}

QScrollBar::right-arrow:hover {
	image: url(:/scrollbar/images/scrollbar/arrowdright_hover.png);
}

QScrollBar::right-arrow:pressed {
	image: url(:/scrollbar/images/scrollbar/arrowdright_pressed.png);
}

QScrollBar::add-line:horizontal {
	subcontrol-position: right;
	subcontrol-origin: margin;
	width: 8;
	border: none;
}

QScrollBar::sub-line:horizontal {
	subcontrol-position: left;
	subcontrol-origin: margin;
	width: 8;
	border: none;
}

/* 水平 ------ */

/* 滚动条 end*/

/* TabWidget begin */
QTabWidget::pane
{
	border-top:1px solid #b82525;
}
QTabWidget::tab-bar
{
	/*height:27px;*/
}
QTabBar::tab
{
	width:82px;
	height:30px;
	background-color:#294564;
	margin-right:4px;
    color: #687f95;
}
QTabBar::tab:hover
{
	background-color:#132c3f;
    color: #78fffa;
}
QTabBar::tab:selected
{
	background-color:#132c3f;
    color: #78fffa;
}
QTabBar::tab:first
{
	margin-left:10px;
}
/* TabWidget end */

/* ScrollArea背景透明 begin */
QScrollArea {
	background: transparent;
	border: none;
}

QScrollArea>QWidget>QWidget {
	background: transparent;
}
/* ScrollArea背景透明 end */

#taskListLbl, #taskDetailLbl, #deviceInfoLbl {
	border-image: url(:/others/images/others/title.png) 2px 24px 2px 2px;
	border-width: 2px 24px 2px 2px;
}

注意 :样式表不⽣效

有很多情况会造成样式表不⽣效,⽐如图⽚丢失、图⽚路径错误、样式受⽗对象样式影响、样式被覆盖等。
有⼀种特殊情况,在 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 转为全局坐标。

QPoint QWidget::mapFromGlobal(const QPoint &pos) const;

将全局坐标转为widget中的局部坐标

2. QPoint QWidget::mapToParent(const QPoint &pos) const;

将当前 widget 的坐标 pos 转为相对于⽗ widget 的坐标。

QPoint QWidget::mapFromParent(const QPoint &pos) const;

将父widget中的坐标转为在当前子widget中的坐标

3. QPoint QWidget::mapTo(const QWidget *parent, const QPoint &pos) const;

将当前 widget 中的坐标 pos 转为相对于 parent 的坐标,parent 不能为空,并且必须是当前widget 的直接或间接⽗ widget

QPoint QWidget::mapFrom(const QWidget *parent, const QPoint &pos) const

将父widget中的坐标转为在当前子widget中的坐标(必须满足父子关系)

// 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() 获取到的是⿏标的全局坐标。
void MainWindow::keyPressEvent(QKeyEvent *event)
{
	// 键盘按下事件

	// 修饰键是ctrl
	if (event->modifiers() & Qt::ControlModifier)
	{
		if (event->key() == Qt::Key_C)
		{
			qDebug() << QStringLiteral("复制");
		}
		else if (event->key() == Qt::Key_V)
		{
			qDebug() << QStringLiteral("粘贴");
		}
	}
	// 修饰键是shift
	else if (event->modifiers() == Qt::ShiftModifier)
	{
		qDebug() << event->text();
	}
}

事件

Qt提供了两种方法对应用程序中发生的事情做出响应。一是信号槽,二是使用事件。

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

事件处理

方法一:重写QApplication::notify()

继承QApplication,并重写notify()函数

bool CustomApplication::notify(QObject* receiver, QEvent* evnet)
{
    if(event->type() == QEvent::MouseButtonPress)
    {
        qDebug() << "类名:" << receiver->metaObject()->className();
        return false;
    }

    return QApplication::notify(receiver, event);
}

注意:任何线程中任何对象发生的事件,都会调用该函数,重写QApplication::notify()如果发生问题,会对整个应用程序造成严重影响,应谨慎使用。

方法二:重写QObject::event()

QObject::event()虽然可进行事件处理,但其真正用途是根据事件类型进行事件分发。试想一个按钮被禁用,那么鼠标按下事件将不会分发给按钮,而QObject::event()中对鼠标事件的处理仍会发生,可能会造成问题。所以不推荐使用该法处理事件。

方法三:重写特定事件

事件类型

QResizeEvent:窗⼝⼤⼩发⽣改变事件

QMouseEvent:⿏标事件

QKeyEvent:键盘事件

QCloseEvent:窗⼝关闭事件

QPaintEvent:绘图事件

QTimerEvent:定时器事件

……

处理这些事件,通常需要重写事件函数,以⿏标按下时触发⿏标事件为例:

void TestWidget::mousePressEvent(QMouseEvent *event)
{
    // 调⽤⽗类的函数
    QWidget::mousePressEvent(event);
    // 获取⿏标按下的位置
    QPoint pos = event->pos();
}
class MyLabel : public QLabel
{
    Q_OBJECT
    
public:
    explicit MyLabel(QWidget* parent):QLabel(parent)
    {
        //设置鼠标追踪
        this->setMouseTracking(true);

        //利用事件过滤器,拦截label的鼠标按下事件
        ui->label->installEventFilter(this);    //给控件安装过滤器;重写过滤器事件
    }

    void enterEvent(QEvent*)
    {qDebug() << "鼠标进入了";}

    void leaveEvent(QEvent*)
    {qDebug() << "鼠标离开了";}

    void mouseMoveEvent(QMouseEvent* ev)
    {
        if(ev->buttons() & Qt::LeftButton)
        {
            QString str = QString("鼠标移动了,x = %1, y = %2").arg(ev->x()).arg(ev->y());
            qDebug() << str;
        }
    }

    // 事件分发器
    bool event(QEvent* e)
    {
        if(e->type() == QEvent::MouseButtonPress)
        {
            QMouseEvent* ev = static_cast<QMouseEvent*>(e);
            QString str = QString("在event事件中 鼠标按下了,x=%1,y=%2").arg(ev->x()).arg(ev->y());
            qDebug() << str;
            return true;    //拦截事件,不再向下分发事件
        }
        
        //其他事件交给父类处理
        return QLabel::event(e);
    }

    //过滤器事件
    // 参数1 判断控件 参数2 判断事件
    bool eventFilter(QObject* obj, QEvent* e)
    {
        if(obj == ui->label)
        {
            if(e->type() == QEvent::MouseButtonPress)
            {
                QMouseEvent* ev = static_cast<QMouseEvent*>(e);
                QString str = QString("在事件过滤器中,鼠标按下了,x=%1,y=%2").arg(ev->x()).arg(ev->y());
                qDebug() << str;
                return true;    //拦截事件,不再向下分发
            }
        }
        
        //其他事件抛给父类处理
        return QLabel::eventFilter(obj,e);
    }
};

事件分发器

事件和信号槽区别

事件由外部事物生成,并在应用中的事件循环处理;

信号槽用于类之间的通信。

事件过滤器

⼀个 QObject 对象可以⽤来监视另⼀个 QObject 对象的事件。 设置事件过滤器分为以下两步:

安装事件过滤器: installEventFilter()

重写事件过滤器函数: eventFilter()

ui->widget_deepspace_bg->installEventFilter(this);

bool InitWidget::eventFilter(QObject *watched, QEvent *event)
{
	switch (event->type())
	{
	case QEvent::Enter:
	{
		QString path("border-image: url(:/data/image/bottomWidget/subwindows/card_hover.png);");
		if (watched->objectName() == ui->widget_deepspace_bg->objectName())
		{
			QString style = QString("QWidget#%1{%2;}").arg(ui->widget_deepspace_bg->objectName()).arg(path);
			ui->widget_deepspace_bg->setStyleSheet(style);
		}
		else if (watched->objectName() == ui->widget_jd_bg->objectName())
		{
			QString style = QString("QWidget#%1{%2;}").arg(ui->widget_jd_bg->objectName()).arg(path);
			ui->widget_jd_bg->setStyleSheet(style);
		}
	}
	break;
	case QEvent::Leave:
	{
		QString path("border-image: url(:/data/image/bottomWidget/subwindows/card_normal.png);");
		if (watched->objectName() == ui->widget_deepspace_bg->objectName())
		{
			QString style = QString("QWidget#%1{%2;}").arg(ui->widget_deepspace_bg->objectName()).arg(path);
			ui->widget_deepspace_bg->setStyleSheet(style);
		}
		else if (watched->objectName() == ui->widget_jd_bg->objectName())
		{
			QString style = QString("QWidget#%1{%2;}").arg(ui->widget_jd_bg->objectName()).arg(path);
			ui->widget_jd_bg->setStyleSheet(style);
		}
	}
	break;
	case QEvent::MouseButtonPress:
	{
		if (watched->objectName() == ui->widget_deepspace_bg->objectName())
		{
			emit Signal_switchSceneWgt(STSceneType::ST_DeepSpace);
		}
		else if (watched->objectName() == ui->widget_jd_bg->objectName())
		{
			emit Signal_switchSceneWgt(STSceneType::ST_NearGround);
		}
	}
	break;
	default:
		break;
	}
	return QWidget::eventFilter(watched, event);
}

事件传递

事件触发后会进⾏传递,从⼦ widget 向⽗ widget 逐层传递。

如主窗⼝中有⼀个 widget1,在 widget1 中有 widget2

widget2 中按下⿏标,触发 widget2 mousePressEvent,然后⿏标事件会先传到 widget1,再传到主窗⼝当中。

如果不想让事件传递该怎么做呢?

可以在 widget2 mousePressEvent 中调⽤ QMouseEvent::accept(),这样⿏标事件便只会在widget2 中发⽣,不会传递给它的⽗对象了。

若接受事件,即QEvent::accept(),表明接收者想要该事件,事件不会继续传递;

若忽略事件,即QEvent::ignore(),表明接收者不想要该事件,事件会传递给父对象来处理。

作业5

截⽌⽬前,主窗⼝是⽆边框的,⿏标⽆法拖动。

  1. 请通过重写⿏标事件,实现主窗⼝的拖动功能。

  2. 为左侧QTreeWidget添加右键菜单。如果树没有根节点,则包含菜单项“添加根节点”,根节点只能是组类型;如果⿏标位置有类型是组的节点,包含菜单项“添加⼦项、删除、重命名”;对于类型是叶⼦的节点,包含菜单项“删除、重命名”(设定只有类型是组的节点可以添加⼦节点,并在添加⼦节点时,可以选择⼦节点类型是组还是叶⼦)。

  3. 实现上⾯右键菜单的所有功能,删除时提示是否确定。

  4. 添加快捷键:Del⽤于删除、F2⽤于重命名

读写配置文件

Qt提供了丰富的类来支持文件操作:

  1. QFileInfo获取文件和文件夹的属性,QDir封装了文件夹的操作,QFile封装了文件的操作
  2. QTextStream封装了文本文件的读写,QDataStream封装了二进制文件的读写
  3. QFileDialog可调用系统资源管理器实现选择文件和文件夹功能
  4. QDomNode和QXmlStream(Reader|Writer)封装了xml结构文件的功能
  5. QJsonObject封装了Json结构文件的读写
  6. QSetting封装了ini配置文件和注册表的读写

 文件信息类 QFileInfo

QString strFileName = QFileDialog::getOpenFileName(this, "选择文件", ".");
QFileInfo info(strFileName);
qDebug() << "是否存在?" << info.exists() << "文件名不含后缀:" << info.baseName()
    << "后缀名:" << info.suffix().toUtf8().data() << "大小:" << info.size()
     << "文件名:" << info.fileName() << "文件路径:" << info.filePath()
     << "创建日期:" << info.created().toString("yyyy-MM-dd hh:mm:ss")
     << "最后修改日期:" << info.lastModified().toStrin("yyyy-MM-dd hh:mm:ss")
     << "文件权限:" << info.permissions();

目录操作 QDir

QDir dir("/home/dami");
qDebug() << "文件夹名称:" << dir.dirName();
qDebug() << "绝对路径" << dir.absolutePath();
qDebug() << "创建子文件夹" << dir.mkdir("newfolder1");
qDebug() << "进入子文件夹" << dir.cd("newfolder1");
qDebug() << "创建子文件夹" << dir.mkdir("newfolder2");
qDebug() << "删除子文件夹" << dir.rmdir("newfolder2");
qDebug() << "创建子文件夹" << dir.mkdir("newfolder3");
qDebug() << "创建子文件夹" << dir.mkdir("newfolder4");
qDebug() << "当前绝对路径" << dir.absolutePath();
//获取当前目录下文件和文件夹信息,不包括.和..
QFileInfoList dirlist = dir.entryInfoList(QDir::AllEntries | QDir::NoDotAndDotDot);
foreach(QFileInfo info, dirlist)
{
    //现实子文件夹名称
    qDebug() << info.fileName();
}

exists() 检测当前目录是否存在
entryList() 获取子目录的目录名信息
count() 获得当前目录下文件和文件夹的数量
remove() 删除指定文件
fromNativeSeparators() 把系统特定的路径格式转换为标准格式,比如Windows平台上 "c:\\winnt\\system32" 转换为 "c:/winnt/system32"
toNativeSeparators() 和 fromNativeSeparators() 相反,把标准路径格式转换为操作系统特定的路径格式

读写文件

文件读写使用QFile类。

QFile file("a.txt");
if(file.open(QIODevice::ReadOnly | QIODevice::Text))
{
    QString strText = file.readAll();
}
  • QIODevice::ReadOnly 以只读方式打开文件
  •  QIODevice::WriteOnly 以只写方式打开文件
  •  QIODevice::ReadWrite 以读写方式打开文件;
  • QIODevice::Append 以追加模式打开文件,新写入文件的数据添加到文件尾部
  •  QIODevice::Truncate 以截取方式打开文件,文件打开之前原有内容全部被清空
  • QIODevice::Text 以文本方式打开文件,读取时 \n 被自动译为换行符。

QTextStream读写文件

QTextStream封装了更多数据类型的读写操作,使用起来更加方便。并且QTextStream的读写速率相比QFile有了很大的提升,并且文件越大提升幅度越明显。

//读文件
QFile file(path);//参数就是文件的路径
//设置打开方式
if(!file.open(QIODevice::ReadOnly))
{
    getOpenFileName()是QFileDialog类的一个静态函数,返回用户选择的文件名,如果用户选择取消(Cancel),则返回一个空串
}
//用QTextStream类去读取文本信息
QTextStream stream(&file);
//用QString类去接收读取的信息
QString str;
while(stream.readLineInto(&str))
//while(str = stream.readLine())
{
    //...
}
file.close();

//写文件
QFile file(path);
if(!file.open(QIODevice::WriteOnly | QIODevice::Text))
{
    return;
}
QTextStream out(&file);
out << QString("你好");
out << int(32);
out << double(3.141592653);
out << "\n"
file.close();
QDataStream 二进制文件
xml

xml是最常见的配置文件。对于xml文件的读写方式有很多,如流、SAX、DOM等。一般通过DOM对xml文件进行操作,因为这种方式简单、易懂、易于修改、维护。

使用Qt写xml,首先引入xml模块:

Qt+=xml

xml内容:

<?xml version="1.0" encoding="UTF-8"?>
<library>
    <book id="01">
 		<title>Qt</title>
        <author>Jack</author>
    </book>
    <book id="02">
        <title>Linux</title>
        <author>Bob</author>
    </book>
</library>

首先读取文件内容,并创建QDomDocument对象。

QDomDocument doc(“mydocument”);
QFile file(“mydocument.xml”);
if(!file.open(QIODevice::ReadOnly))return;
if(!doc.setContent(&file))
{
    file.close();
    return;
}
file.close();

然后读取xml内容。

if(n.isElement())
{
    QDomElement e=n.toElement();
    qDebug()<<qPrintable(e.tagName())<<qPrintable(e.attribute(“id”));
    //获取元素e的所有子节点的列表
    QDocNodeList list=e.childNodes();
    for(int i=0;i<list.count();++i)
    {
        QDocNode node=list.at(i);
        if(node.isElement())
            qDebug()<<”	”<<qPrintable(node.toElement().tagName())
                <<qPrintable(node.toElement().text());
    }
}
<?xml version="1.0" encoding="UTF-8"?>
<class>
    <student no="01" sex="male">
        <name>张三</name>
        <grade>98</grade>
    </student>
    <student no="02" sex="female">
        <name>丽丝</name>
        <grade>95</grade>
    </student>
    <student no="03" sex="male">
        <name>王五</name>
        <grade>89</grade>
    </student>
</class>
void ReadXml()
{
    //打开或创建文件
    QFile file("test.xml"); //相对路径、绝对路径、资源路径都行
    if(!file.open(QFile::ReadOnly))
    {
        return;
    }
    QString error;
    int row = 0, column = 0;
    QDomDocument doc;
    if(!doc.setContent(&file, fasle, &error, &row, &column))
    {
        QMessageBox::information(this, "title", "parse file failed at line row and column:");
        file.close();
        return;
    }
    file.close();
    
    QDomElement root = doc.documentElement(); //返回根节点,即class节点
    // 学生节点
    QDomNode studentNode = element.firstChild();
    while (!studentNode.isNull())
    {
        QDomElement eleStudent = studentNode.toElement();
        if ("student" == eleStudent.tagName())
        {
            // 学号
            QString strNo = eleStudent.attribute("no"); //读取no标签
            // 性别
            QString strSex = eleStudent.attribute("sex"); //读取sex标签
            //子节点,姓名和分数
            for(QDomElement eleChild = eleStudent.firstChildElement(); !eleChild.isNull();
            {
                QString strTagName = eleChild.tagName();
                if("name" = strTagName)
                {
                    QString strName = eleChild.text(); //读取文本
                }
                else if("grade" = strTagName)
                {
                    QString strGrade = eleChild.text(); //读取文本
                }
            }
        }
        //下一个学生节点
        studentNode = studentNode.nextSibling();
    }
}

xml的写入

void writeXML()
{
    QFile file("test.xml");
    //开打XML
    if (!file.open(QFile::WriteOnly | QFile::Text))
    {
        qDebug() << "Error: cannot open file";
        return;
    }
    QXmlStreamWriter stream(&file);
    stream.setAutoFormatting(true); //自动缩进
    stream.writeStartDocument(); //开始文档,写入文件头
    stream.writeStartElement("class");//生成一个class项
    //写入三个学生的信息
    writeStudent(stream, "01", "male", "张三", 98);
    writeStudent(stream, "02", "female", "李四", 95);
    writeStudent(stream, "03", "male", "王五", 89);
    stream.writeEndElement(); //结束class项
    stream.writeEndDocument(); //结束文档
    file.close();
}

void writeStudent(QXmlStreamWriter& writer, const QString& no, const QString& sex, const QString& name, int grade)
{
    stream.writeStartElement("student"); //生成一个student项
    stream.writeAttribute("no", no); //写入no标签
    stream.writeAttribute("sex", sex); //写入no标签
    stream.writeTextElement("name", name); //写入name项
    stream.writeTextElement("grade", QString::number(grade)); //写入grade项
    stream.writeEndElement(); //结束student项
}

日常开发中我们通常用DOM来读取XML,用SAX来写入XML,充分发挥两者的优势

Json

Json用于存储结构化的数据,存储的值有6种:booldoublestringarrayobjectnull

array数组是其他值类型的列表,用方括号”[]”表示;object对象是键值对组合,且都是字符串,不能包含重复的key,用大括号”{}”表示

{
    "age": 26,
    "color": [
        "black",
        "white"
    ],
    "interest": {
        "badminton": "羽毛球",
        "basketball": "篮球"
    },
    "name": "张三",
    "vip": true
}
void writeJson()
{
    // 定义对象
    QJsonObject interestObj;
    // 插入元素,对应键值对
    interestObj.insert("basketball", "篮球");
    interestObj.insert("badminton", "羽毛球");
    // 定义数组
    QJsonArray colorArray;
    // 往数组中添加元素
    colorArray.append("black");
    colorArray.append("white");
    // 定义根节点
    QJsonObject root;
    root.insert("name", "张三");
    root.insert("age", 26);
    root.insert("vip", true);
    root.insert("interest", interestObj);
    root.insert("color", colorArray);
    //创建一个JSON文档
    QJsonDocument doc;
    //把跟节点放进来
    doc.setObject(root);
    //写入到文件
    QFile file("test.json");
    if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate))
    {
        return;
    }
    QTextStream stream(&file);
    stream.setCodec("UTF-8");
    // 写入文件
    stream << doc.toJson();
    file.close();
}
void readJson()
{
    QFile file("test.json");
    if (!file.open(QIODevice::ReadOnly))
    {
        return;
    }
    // 读取json文件的所有内容
    QByteArray jsonArray = file.readAll();
    file.close();
    // QJsonParseError输出解析时的错误报告
    QJsonParseError jsonError;
    QJsonDocument doc = QJsonDocument::fromJson(jsonArray, &jsonError);
    // 判断是否解析成功
    if (QJsonParseError::NoError != jsonError.error && doc.isNull())
    {
        qDebug() << "read json file failed : " << jsonError.error;
        return;
    }

    // 获取根节点
    QJsonObject root = doc.object();
    // 根据键获取值
    QJsonValue nameValue = root.value("name");
    ui->textEdit->append(QString("name:") + QString(nameValue.toString()));
    QJsonValue ageValue = root.value("age");
    ui->textEdit->append(QString("age:") + QString::number(ageValue.toInt()));
    QJsonValue vipValue = root.value("vip");
    ui->textEdit->append(QString("vip:") + QString(vipValue.toBool() ? "true" : "false");
    // 解析对象
    QJsonValue interestValue = root.value("interest");
    // 判断是否为object类型
    if (QJsonValue::Object == interestValue.type())
    {
        // 转为QJsonObject类型
        QJsonObject interestObject = interestValue.toObject();
        QJsonValue basketBallValue = interestObject.value("basketBall");
        ui->textEdit->append(QString("basketball:") + basketBallValue.toString());
        QJsonValue badmintonValue = interestObject.value("badminton");
        ui->textEdit->append(QString("badminton:") + badmintonValue.toString());
    }
    // 解析数组
    QJsonValue colorValue = root.value("color");
    // 判断是否是Array类型
    if (QJsonValue::Array == colorValue.type())
    {
        // 转换为QJsonArray类型
        QJsonArray colorArray = colorValue.toArray();
        QJsonValue color;
        for (int i = 0; i < colorArray.size(); i++)
        {
            QJsonValue color = colorArray.at(i);
            ui->textEdit->append(QString("color:") + color.toString());
        }
    }
}
ini

用于初始化程序。Windows系统后来以注册表的形式取代ini文件。

格式:

节/组 [section]

参数 name=value

注:name不要使用中文,否则会对读取造成麻烦。
注解

使用;表示。分号后面的文字,直到该行结束全为注解。

;comment text

//保存
QSetting setting(“YourOrg”,”YourApp”);
settings.beginGroup(“MainWindow”);
settings.setValue(“geomotry”,this->geometry());
settings.endGroup();
//加载
QSettings settings(“YourOrg”,”YourApp”);
settings.beginGroup(“MainWindow”);
QRect savadRect=settings.value(“geometry”).toRect();
this->setGeometry(savedRect);
settings.endGroup();

在windows系统中,这些信息是默认保存在注册表中,也可将其保存在文件中:

QSettings settings(“config.ini”,QSetting::IniFormat);

打开文件config.ini,可以看到:

[MainWindow]geometry=@Rect(0 0 600 400)

 作业6

直到现在,⼀旦我们将思维导图软件关闭,界⾯上所有修改都会丢失。

因为数据都是保存在内存中的,为了能够在下次打开软件时恢复数据,我们需要将思维导图数据保存到⽂件中,请实现下⾯的功能:

  1. 通过菜单:⽂件 - 保存,或者通过快捷键Ctrl+S,将界⾯中的思维导图树保存到本地的配置⽂件中

  2. 通过菜单:⽂件 - 打开,或者通过快捷键 Ctrl+O,打开本地配置⽂件,并初始化左侧的树、以及中间的思维导图区域

配置⽂件可采⽤xml或者json任⼀种,参考格式如下:

<?xml version="1.0" encoding="utf-8"?>
<MindMap>
    <Item type="group" name="⽣物">
        ...(不往下写了,请⾃⼰设计配置⽂件格式)
    </Item>
</MindMap>

帮助与提示

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

QPainter绘图

通过QPainter实现Qt二维绘图:

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

2. 特殊效果,如渐变;

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

用法

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

void CustomWidget::painterEvent(QPainterEvent* event)
{
    QPainter painter(this);
    
    QPen pen(QColor(255,0,0));
    pen.setWidth(3);
    pen.setStyle(Qt::DotLine); //设置画笔风格
    painter.setPen(pen);
    
    QBrush brush(Qt::cyan);
    brush.setStyle(Qt::Dense5Pattern);
    painter.setBrush(brush);

    //设置抗锯齿
    painter.setRenderHint(QPainter::Antialiasing);
    
    painter.drawLine(QPoint(0,0), QPoint(100,100));
    painter.drawEllipes(QPoint(100,100), 50, 50);
    painter.drawRect(QRect(20,20,50,50));
    QFont font;
    font.setFamily("Microsoft YaHei");
    font.setPixelSize(20);
    font.setBold(true);
    painter.setFont(font);
    //painter.setFont(QFont("华文彩云", 20);
    painter.drawText(QRect(0,200,150,100), Qt::AlignCenter, "好好学习,天天向上");

    //画成品图案
    QPixmap pix;
    pix.load(":/Image/Luffy.png");
    pix = pix.scaled(pix.width()*0.5,pix.height()*0.5);
    if(posX > this->width())
        posX = -pix.width();
    painter.drawPixmap(posX, 0, pix);
}

{
    posX = 0;
    //点击移动按钮,移动图片
    connect(ui->btn_move, &QPushButton::clicked, [=](){
        posX += 10;
        //手动调用绘图事件!!!!
        update();
    });
}

{
    //实现自动让图片向右移动
    QTimer* timer = new QTimer(this);
    timer->start(10);
    connect(timer, &QTimer::timeout, [=](){
        posX++;
        //!!!
        update();
    });
}

// 平铺图 通过平铺,可以将一张图片铺到整个widget中,常用于绘制棋盘形状的网格背景
painter.drawTiledPixmap(this->rect(), m_pixmap);

对于平铺,比如原图如下:

平铺之后:

画笔

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

画刷

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

QBrush还可以设置渐变色:

QLinearGradient gradient(0, 0, 200, 0);
gradient.setColorAt(0, QColor(255, 0, 0));
gradient.setColorAt(0.5, QColor(0, 255, 0));
gradient.setColorAt(1, QColor(0, 0, 255));
QBrush brush(gradient);
painter.setBrush(brush);
QPainter绘制接口

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

1. 点:QPainter::drawPoint()

2. 线:QPainter::drawLine()

3. 矩形:QPainter::drawRect()

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

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

6. 扇形:QPainter::drawPie()

7.弧线:QPainter::drawArc()

8. 文字:QPainter::drawText()

9. 图片: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);
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing, true);
painter.setPen(Qt::NoPen);
painter.setBrush(Qt::green);
QPainterPath path;
path.addRect(10, 10, 200, 300);
path.addRect(250, 10, 200, 300);
painter.drawPath(path);

试想,如果这两个图形存在重合会发生什么?

QPainterPath有一个设置填充规则的接口,根据指定的规则来判断一个点是否在我们的图形中:

void QPainterPath::setFillRule(Qt::FillRule fillRule);

QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing, true);
painter.setPen(Qt::NoPen);
painter.setBrush(Qt::green);
QPainterPath path;
path.setFillRule(Qt::OddEvenFill); //默认值即为Qt::OddEvenFill
path.addEllipse(QPointF(200, 200), 100, 100);
path.addEllipse(QPointF(266, 200), 100, 100);
painter.drawPath(path);

若把填充规则改为path.setFillRule(Qt::WindingFill);绘制的效果就变成了:

常用的绘图设备

QPixmap QBitmap QImage QPicture QWidget

坐标系统

在默认的坐标系中,点 (0, 0) 位于⼀个 widget 的左上⻆,x 坐标向右增⻓,y 坐标向下增⻓。

坐标系可以进⾏⼀些变换:

平移:QPainter::translate()

旋转:QPainter::rotate()

伸缩:QPainter::scale()

……

原始坐标系统如图 1左所示;若执行平移函数 translate(150,100),则坐标系统水平向右平移 150 像素,向下平移 100 像素,平移后的坐标系统如图 1 右所示,坐标原点在窗口的中心,而左上角的坐标变为(-150,-100),右下角的坐标变为(150.100)。如此将坐标原点变换到窗口中心在绘制某些图形时是非常方便的。

将坐标系统绕坐标原点顺时针旋转 angle 角度,单位是度。当 angle 为正数时是顺时针旋转,为负数时是逆时针旋转。

在图1右的基础上,若执行 rotate(90),则得到图2所示的坐标系统。在图1 的新坐标系下,窗口左上角的坐标变成了(-100,150),而右下角的坐标变成了(100,-150)。

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

使用QWidget进行自定义控件、窗口、创建布局时,存在一些局限性。当界面中图形元素很多,或需进行复杂变换时,QWidget处理棘手。此时,可选用Graphics View框架。

Graphics View提供了一个用于管理大量图形元素及其交互方式,且提供了用于显示这些元素并支持放缩、旋转的widget。

在图形元素多、坐标转换多时,更偏向与使用Graphics View,而非QWidget。

它是一种基于图形项(GraphicsItem)的模型/视图模式,这种方式可以在一个场景中绘制大量图元项,每个图元项都是可选择、可交互的。

QPainter采用面向过程的描述方式绘图,Graphics View采用面向对象的描述方式绘图。对于复杂的图像来说,若图像包含大量直线、曲线、多边形等图元对象,则管理图元对象比管理QPainter的绘制过程语句更容易。且图元对象更符合面向对象的思想,图形的可复用性更好。

场景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

  • 分组

  • 碰撞检测

标准图元,如:

QGraphicsEllipseItem 椭圆

QGraphicsLineItem 直线

QGraphicsPathItem 路径

QGraphicsPixmapItem 图像

QGraphicsPolygonItem 多边形

QGraphicsRectItem 矩形

QGraphicsSimpleTextItem 简单文本

QGraphicsTextItem 文本

QGraphicsItem 图元的基类,用户可继承QGraphicsItem实现自定义图元。

若是自定义图元,首先需继承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);

属性

Qt使用 Q_PROPERTY() 宏在一个类中声明一个属性,他们通过Qt的元对象系统增加了一些额外的特性。该宏是Qt特有的,需moc进行编译,必须继承QObject类。该宏原型如下:

Q_PROPERTY(type name
            (READ getFunction [WRITE setFunction] |
            MEMBER memberName [(READ getFunction | WRITE setFunction)])
            [RESET resetFunction]
            [NOTIFY notifySignal]
            [REVISION int]
            [DESIGNABLE bool]
            [SCRIPTABLE bool]
            [STORED bool]
            [USER bool]
            [CONSTANT]
            [FINAL]
            [REQUIRED])

其中,

1. 小括号“()”中的关键字是必须实现的,中括号“[]”中的关键字是可选的;

2. type name:属性的类型和名字。属性类型可以是QVariant支持的任何类型,也可以是自定义类型;

3. 如果没有MEMBER,就必须要有READREAD后面定义获取属性值的函数,该函数是const的。WRITE设置属性值,是可选的;

4. NOTIFY定义信号函数,只要该属性值发生改变,就会发出该信号。该信号函数必须采用零个或一个参数,该参数必须与属性类型相同。

说明:属性是可以继承的。

使用元对象系统读写属性

一个属性可以使用常规函数 QObject::property() QObject::setProperty() 进行读写,除

了属性的名字,不用知道属性所在类的任何细节。

比如 QPushButton 的基类 QAbstractButton 有一个属性叫做 down ,描述的是该按钮是否被按

下。通过这个属性,我们可以在不调用 QAbstractButton 函数而是通过属性修改这个值

QObject *pButton = new QPushButton(this);
bool bDown = pButton->property("down").toBool();
pButton->setProperty("down", !bDown);

//可以通过 QMetaObject 和 QMetaProperties 查询一个对象的所有属性和对应的值
QObject *pButton = new QPushButton(this);
const QMetaObject *metaobject = pButton->metaObject();
int count = metaobject->propertyCount();
for (int i=0; i<count; ++i)
{
    QMetaProperty metaproperty = metaobject->property(i);
    const char *name = metaproperty.name();
    QVariant value = pButton->property(name);
    qDebug() << name << value;
}

动态属性

QObject::setProperty() 也可以用在运行时向一个类的实例添加新的属性。

当使用一个名字和值调用它时,如果QObject中该名称的属性已经存在,并且给定的值与属性类

型兼容,那么,值就被存储到属性中,然后返回true。如果值与属性类型不兼容,属性的值就不

会发生改变并返回false

但是如果QObject中该名称的属性不存在,该指定名称和值的新属性就被自动添加到QObject中,但是依然会返回false

QObject *pButton = new QPushButton(this);
pButton->setProperty("hgtec", true);
bool bIs = pButton->property("hgtect").toBool(); //true

注意: 动态属性的属性类型是以QVariant存储的,因此如果需要存储自定义的数据类型,需

Q_DECLARE_METATYPE() 注册

动画

//构造时指定要控制哪个对象的哪个属性
QPropertyAnimation* pAnimation = new QPropertyAnimation(pLabel, "geometry");
//设置持续时间
pAnimation->setDuration(1000);
//设置开始值
pAnimation->setStartValue(QRect(190, 230, 0, 0));
//设置结束值
pAnimation->setEndValue(QRect(120, 160, 140, 140));
//设置动画曲线
pAnimation->setEasingCurve(QEasingCurve::InOutQuad);
//启动动画,并在动画停止后释放该动画
pAnimation->start(QAbstractAnimation::DeleteWhenStopped);

串行动画组

QSequentialAnimationGroup* pPosGroup = new QSequentialAnimationGroup(this);
pPosGroup->addAnimation(pFirstScaleAnimation); //添加第一个动画
pPosGroup->addPause(500); //添加一个500毫秒的暂停,详见3.4
pPosGroup->addAnimation(pSecondScaleAnimation); //添加第二个动画
pPosGroup->start(); //启动动画

//添加到动画组内的动画,不再需要单独启动。只需要启动动画组即可,内部的动画会由动画组来控制启动和停止

并行动画组

QParallelAnimationGroup* pPosGroup = new QParallelAnimationGroup(this);
pPosGroup->addAnimation(pFirstScaleAnimation); //添加第一个动画
pPosGroup->addAnimation(pSecondScaleAnimation); //添加第二个动画
pPosGroup->start(); //启动动画

//假设第一个动画持续时间为200毫秒,第二个动画持续时间为500毫秒。那么动画组开始后,两个动画会同时开始。
//200毫秒后第一个动画先结束,500毫秒后第二个动画也结束。此时,整个并行动画组结束

暂停动画

//在串行动画组中插入 QPauseAnimation ,就可以实现延时播放的效果
QSequentialAnimationGroup* pPosGroup = new QSequentialAnimationGroup(this);
pPosGroup->addAnimation(pFirstScaleAnimation); //添加第一个动画
QPauseAnimation *pPauseAnimation = new QPauseAnimation(this);
pPauseAnimation->setDuration(500); //500毫秒的暂停动画
pPosGroup->addAnimation(pPauseAnimation);
pPosGroup->addAnimation(pSecondScaleAnimation); //添加第二个动画
pPosGroup->start();

//在串行动画组启动并且第一个动画结束后,会加入一个500毫秒的暂停,这段时间内串行动画处于空白期,什么都不会做。然后才会继续执行第二个动画。
//串行动画组本身提供了一个函数 addPause() 可以快速插入一个暂停动画,和构造一个 QPauseAnimation 然后通过 addAnimation() 插入从效果上看是完全一样的

动画方向

QAbstractAnimation 提供了一个 setDirection() 函数,可以设置动画的执行方向。
  • QAbstractAnimation::Forward 默认值,正向,动画会按照正常顺序执行。
  • QAbstractAnimation::Backward 反向,动画会从持续时间的最后往0方向执行
QPushButton* pButton = new QPushButton("按钮", this);
//设置动画
QPropertyAnimation* pPosAnimation = new QPropertyAnimation(pButton, "pos");
pPosAnimation->setDuration(1000);
pPosAnimation->setStartValue(QPoint(0, 0));
pPosAnimation->setEndValue(QPoint(100, 150));
//按钮点击后处理
connect(pButton, &QPushButton::clicked, [=](){
    //根据按钮勾选状态
    pPosAnimation->setDirection( \
            (pPosAnimation->direction()==QAbstractAnimation::Backward) ? \
            QAbstractAnimation::Forward : QAbstractAnimation::Backward);
    //如果动画正在运行
    if(pPosAnimation->state() == QAbstractAnimation::Running)
    {
        //则先停止动画
        pPosAnimation->stop();
    }
    //启动动画
    pPosAnimation->start();
});

循环次数

QAbstractAnimation 提供了一个 setLoopCount() 函数,可以设置动画的循环次数。
  • 设置为正整数n时,会循环n
  • 设置为0时,动画不会启动
  • 设置为-1时,动画会无限循环,直到主动停止动画

作业7

真相⼤⽩的时候到了!前⾯思维导图库就是通过GraphicsView实现的。

⾸先尝试下⾯2个⼩练习:

QWidget 窗⼝中⼼画⼀个边框红⾊、背景绿⾊、半径为 150 像素的圆形。

⿏标在边框区域按下,将该圆改为边框绿⾊、背景红⾊;

⿏标松开,恢复圆的边框为红⾊,背景为绿⾊。

使⽤ QGraphicsView,⼀个边框红⾊、背景绿⾊、半径为 150 像素的圆形 item

item 放⼤ 1.5 倍,并旋转 90 度。

接下来实现下⾯的功能:

使⽤QGraphicsView,在场景中创建2item,分别显示⽂字“⽜郎”、“织⼥”,并创建⼀条连线连接这两项。可通过⿏标拖动这两项,线也会跟根据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");

网络

字节对齐

//针对项目中的场景,我们指定数据结构以1字节方式对齐,足够我们解决绝大多数问题
//开始指定字节对齐为1字节
#pragma pack(push, 1)
#pragma pack(show) //查看当前编译器的字节对齐数
struct Node
{
    int num;
    char c;
};
//结束指定的字节对齐,恢复默认对齐
#pragma pack(pop)
#pragma pack(show) //查看当前编译器的字节对齐数

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()));
    }
}

线程与进程

在进行桌面应用程序开发时,假设应用程序在某些情况下需要处理比较耗时的操作,若只有一个线程去处理,会导致程序无法及时响应用户的窗口操作,表现在窗口卡顿。需要使用多线程,其中主线程处理窗口事件,其他线程进行逻辑运算或其他耗时操作,可提高用户体验和程序执行效率。

默认的线程在Qt中称为窗口线程,也叫主线程,负责窗口事件处理或者窗口渲染的更新。

进程内其他线程为子线程,子线程不能对窗口做任何操作,否则会导致程序大概率崩溃。

QThread

通过QThread创建线程有两种方式,一种是子类化QThread 并重写 run 函数实现,一种是把需要

在子线程中执行的对象通过 QObject::moveToThread 整体放到线程中执行。

QObject::moveToThread 的方式把逻辑实现和线程的控制完全剥离开,并通过信号槽在控制

端、逻辑端、线程端传递消息,结构虽稍微复杂但更适合程序扩展和修改,尤其是对规模和复杂

度较大的程序。实际开发中推荐优先考虑 QObject::moveToThread 的方式Qt还提供了 QMutex QReadWriteLock 等保证线程同步。

QThread的常见接口如下:

  • start ,启动线程,是子线程执行起来。在支持的操作系统上可通过形参调整子线程的运行优先级
  • run ,虚函数,此函数内的代码运行于子线程内。调用 start 函数后其内部最终会执行到 run
  • terminate ,强制停止线程。线程可能不会马上停下来,所以一般需要配合 wait 使
  • exec protected函数,使线程进入实现循环。默认不执行。只能在子线程内即 run 函数内调用(解决没有时间循环时槽函数响应不到的问题)
  • exit ,退出时间循环,只有执行了 exec 之后才有效果。 quit exit(0)
  • isRunning ,判断线程是否在运行
  • sleep ,静态方法,强制当前线程休眠一定时间
  • started finished ,信号,线程启动和结束时分别会发送

生存线程:一个QObject对象,它在哪个线程中构造,其生存线程就是哪个。

多线程开发中这是一个需要特别关注的点,经常会发现写好了线程但是某些函数执行的时候会阻

塞主线程,大概率是因为信号槽的连接方式有问题!在连接信号槽时直接指定 Qt::DirectConnection Qt::QueuedConnection

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();
}

//总结:这种在程序中添加子线程的方式是非常简单的,但是也有弊端,假设要在一个子线程中处理多个任务,
//所有的处理逻辑都需要写到 run 函数中,这样该函数中的处理逻辑就会变得非常混乱,不太容易维护

在线程中运行时,若需要通知界面,通常使用发送信号的方式。

Qt内能够被元系统识别的数据类型只有Qt提供的数据类型和c++基础类型如intdouble等,其他

STL数据类型或自定义的数据结构等,需要在信号槽连接之前,通过 qRegisterMetaType() 注册

才能够被元系统识别,否则在运行时会得到如下错误:

QObject::connect: Cannot queue arguments of type 'YourCustomType'

struct MyStruct
{
    int num;
};

void initSignalAndSlot()
{
    qRegisterMetaType<MyStruct>();
    qRegisterMetaType<std::string>();
    connnect(first, SIGNAL(firstSginal(MyStruct)), second,SLOT(secondSlot(MyStruct)), Qt::QueuedConnection);
           connnect(first,SIGNAL(firstSginal(std::string)),second,SLOT(secondSlot(std::string));
}

注意启动线程不能直接调用 run ,通过调用 start 线程内部会自动执行 run 函数。 run 函数

运行于子线程中,其他函数除非被 run() 方法直接调用或同步调用,否则都运行于其生存线程

内。

注意:多线程的退出与释放,是极其容易导致程序崩溃的点,一定要非常关注。

除此之外,还有moveToThread、QtConcurrent、线程池等方式使用线程。使用QMutex进行互斥处理。

GUI线程

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

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

线程同步

多个线程 访问共享变量时,应当使用锁把对共享变量的操作锁住,一保证同步性。
注意:仅在有必要的时候使用锁 ,并尽量减少锁保护的代码段
void GxFileSaver::push(const QString &str)
{
    m_mutex.lock(); //上锁,写保护
    m_queueContents.enqueue(str);
    m_mutex.unlock(); //解锁,尽量减少锁保护的代码段
}

void GxFileSaver::work()
{
    ...
    //取出一个操作请求
    m_mutex.lock(); //上锁,读保护
    QString strContent = m_queueContents.dequeue();
    m_mutex.unlock(); //解锁,尽量减少锁保护的代码段
    ...
}

if(1)
{
    QMutexLocker locker(&mutex); //QMutexLocker实现自动解锁
    ...//do something
}

//弊端是使用 QMutexLocker 大概率会扩大锁的锁定范围,有可能导致程序性能下降

作业8

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

图表

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

折线图、饼图、柱状图

桌面信息

QList<QScreen *> screens = QApplication::screens();
foreach(QScreen* pScreen, screens)
{
    QPushButton *pButton = new QPushButton(pScreen->name(), this);
    if (pScreen == qApp->primaryScreen()) //是否是主屏幕
    {
        pButton->setChecked(true);
    }
}
...
QRect rectScreen = pScreen->geometry();
m_pBall->setGeometry(rectScreen);

正则表达式

在项目中经常会遇到对字符串进行操作的情况,我们可以直接使用QString的一些函数,但Qt

供了一个更加强大的类——QRegExp,使用正则表达式来操作字符串。

QRegExp的常用场景:

  • 对输入框LineEdit中的输入内容加以限制,比如只能输入数字,并且最多5位数(因int类型不限制位数会导致溢出问题);
  • 检查输入是否正确,比如判断是否是1-9999之间的数;
  • 获取一个字符串中的一段内容,比如获取2015-11-20中的2015

用途:

1. 验证

判断字符串是否符合某个标准,比如是一个整数或者没有空格

2. 搜索

正则表达式提供了比普通字符串匹配更为强大的匹配方式,比如匹配下面的词语:

mail, letter, correspondence,但是不包括email, mailman, letterbox等等。

3. 查找并替换

正则表达式能够用一个不同的字符串,替换所有出现另一个字符串的地方,比如用&替换&,如果原先&后面已经有了amp;那么不替换。

4. 分割字符串

比如,根据tab来分割字符串。

Qt中的用法如下:

1. 对用户输入的限制

void QLineEdit::setValidator(const QValidator * v) QLineEdit中的这个函数意思是,令LineEidt只接受验证器v所匹配的输入,你可以对要输入的内容进行任意的限制。

比如:限制输入框只能输入099999

QRegExp regExp("0|[1-9]\\d{0,4}");
ui.lineEdit->setValidator(new QRegExpValidator(regExp, this));

Qt内置了一些已经实现好的验证器,如QIntValidator用来保证用户输入数字,QDoubleValidator用来保证用户输入浮点数的范围

//只允许用户输入10到20之间的数字
ui.lineEdit->setValidator(new QIntValidator(10, 20, this));
//只允许用户输入10.00-20.00之间的数字,且小数点最多2位
i.lineEdit->setValidator(new QDoubleValidator(10, 20, 2, this));

2. 检查字符串

函数原型为 QValidator::State QRegExpValidator::validate(QString &input, int &pos) const

  • 如果输入与正则表达式相匹配,则返回 QValidator::Acceptable
  • 如果部分匹配,则返回 QValidator::Intermediate (部分匹配,意思是如果给它增加额外的字符则能够匹配正则表达式);
  • 如果不匹配则返回 QValidator::Invalid。
// 整形[1,9999]
QRegExp rx("[1-9]\\d{0,3}");
QRegExpValidator v(rx);
QString s;
int pos = 0;
s = "0"; v.validate(s, pos); // returns Invalid
s = "12345"; v.validate(s, pos); // returns Invalid
s = "1"; v.validate(s, pos); // returns Acceptable
void GxRegExp::slotInputChanged(const QString& strText)
{
    QRegExp rx("^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.([a-zA-Z0-9_-]+)){1,3}$");
    QRegExpValidator v(rx);
    int pos = 0;
    QString strEmail = strText;
    QValidator::State state = v.validate(strEmail, pos);
    switch(state)
    {
        case QValidator::Invalid:
        {
            ui->labelFit->setText("不匹配");
            ui->labelDomain->clear();
        }
        break;

        case QValidator::Intermediate:
        {
            ui->labelFit->setText("部分匹配");
            ui->labelDomain->clear();
        }
        break;

        case QValidator::Acceptable:
        {
            ui->labelFit->setText("匹配");
            grabDomain(strEmail);
        }
        break;
    }
}

void GxRegExp::grabDomain(const QString& strEmail)
{
    QRegExp rxlen("@([\\w-.]+)");
    int pos = rxlen.indexIn(strEmail);
    if (pos > -1)
    {
        QString value = rxlen.cap(0);
        value.remove("@");
        ui->labelDomain->setText(value);
    }
}

3. 截取字符串

函数原型为 QString QRegExp::cap(int nth = 0) const

这个函数返回被第n个子表达式捕获的文本,整个匹配拥有下标0,带括号的子表达式下标从1开始。

QRegExp rxlen("(\\d+)(?:\\s*)(cm|inch)");
int pos = rxlen.indexIn("Length: 189cm");
if (pos > -1)
{
    QString value = rxlen.cap(1); // "189"
    QString unit = rxlen.cap(2); // "cm"
    // ...
}

程序打包部署

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

准备工作

⾸先请将程序改为 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。
通过命令⾏, windeployqt 后接可执⾏⽂件的路径,即可将程序打包。
windeployqt xxx.exe
打包后⽂件夹中的⽂件如下:
这种⽅式只能将依赖的 Qt 相关的库进⾏打包,不会打包⾮ Qt 的库,⽐如 OSG boost 或者⾃⼰编写的库等,所以仍需⼿动将它们拷⻉到应⽤程序⽂件夹中。
学⽆⽌境,实际项⽬中,你可能会遇到很多新知识,⽐如数据库、模型与视图、动画等等。这⾥我们并没有详细去讲,因为重点不是学更多知识点,⽽是学习的态度和解决实际问题的能⼒。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值