QT

QT

Qt Widgets Application 是传统的C++,不适合移动端开发。在Qt5下,QWidget系列从QtGui中被剥离出去,成为单独的QtWidget模块。QT基本模块如下:

QT

Qt Quick Application页面布局(UI)用的QML,但是内部的业务逻辑还是用QT语法。对于传统的桌面程序来说,优先考虑使用 Qt Widgets,若要开发更“现代“的 UI 与高级应用,建议使用 Qt5.x + QML 2.x + QtQuick 2.x。对于移动端开发来说,建议使用 QML,协同 JavaScript,简单快捷、渲染效果更佳、界面更炫酷。不建议使用 Qt Widgets,其显示效果、适应性都不好。

QT

创建Qt项目时可以选择基类,QMainWindow、QWidget、QDialog 三种基类的区别:QMainWindow是一个提供了菜单、工具条的程序主窗口;QWidget是所有图形界面的基类;QDialog是对话框、多用于短时间与用户的交互。QMainWindow和QDialog都是QWidget的子类。

QT

整个项目目录如上图。.ui是用 Qt designer 进行界面设计的文件(界面文件);.cpp编写具体的槽函数(源文件);.h对界面类进行声明(头文件);.qrc是一个xml格式的资源配置文件,与应用程序关联的应用程序由该文件来指定,它用XML记录硬盘上的文件和对应的随意指定的资源名称,应用程序通过资源名称来访问资源。

.pro 文件

pro即为工程文件。

TEMPLATE:这个变量是用来定义你的工程将被编译成什么模式。如果没有这个设置,系统将默认编译为application。app表示这个project将被编译成一个应用程序, lib(生成库的Makefile),subdirs(生成有多级目录管理的Makefile),vcapp,vclib,vcsubdirs(对应Windows 下面VC)。

TARGET:生成最后目标的名字。

DESTDIR:指定生成目标的路径。

CONFIG:告诉qmake应用程序的配置信息。 这个变量可以用来指定是生成debug模式还是release模式,也可以都生成。也可以用来打开编译器警告(warn_on-输出尽可能多的警告信息)或者关闭(warn_off-编译器会输出尽可能少的警告信息)。还可以用来配置要Qt加载库。 例如qt+多线程:CONFIG+=qt thread。

LIBS:加载动态库,引入的lib文件的路径。Release:LIBS+= -L folderPath,release 版引入的lib文件路径。Debug:LIBS+= -L folderPath,Debug 版引入的lib文件路径。

DEPENDPATH:工程的依赖路径。

MOC_DIR:MOC命令将含Q_OBJECT的头文件转换为标准的头文件存放的目录。

OBJECTS_DIR:生成的目标文件存放的目录。

UI_DIR:UIC将ui转化为头文件所存放的目录。

RCC_DIR:RCC将qrc文件转化为头文件所存放的目录。

RC_FILE:程序图标。

main函数中各行作用

#include "QtWidgetsApplication1.h"
#include <QtWidgets/QApplication>	// 包含一个应用程序类的文件

int main(int argc, char *argv[])	// main程序入口  argc命令行变量的数量,argv命令行变量的数组
{
    QApplication a(argc, argv);		// a为应用程序对象,在Qt中应用程序对象有且只有一个
    QtWidgetsApplication1 w;	// w为窗口对象,默认不会显示,必须要调用show方法显示窗口
    w.show();
    return a.exec();	//	a.exec() 让应用程序进入消息循环
}

运行时出现MSB4018错误

原因是没有设置平台工具集,在项目属性页设置。

QT

QT中的对象树

当创建对象在堆区的时候,如果指定的父亲是QObject派生下来的类或者QObject子类派生下来的类,可以不用管理释放的操作,对象将会放入对象树中。一定程度上简化了内存回收机制。当父类对象析构的时候,子类对象也会被析构。如下图,创建时一层一层往下创建,析构时一层一层往上释放。在QT中,尽量在构造的时候就指定parent对象。

QT

信号和槽

connect(信号的发送者,发送的具体信号,信号的接受者,信号的处理(槽))
参数2:函数的地址,可自定义
参数4:处理的槽函数,可自定义 
connect(btn, &QPushButton::clicked, this, &MyApplication0507::close);   // 点击按钮即关闭

信号槽松散耦合,信号发送端和接收端本身是没有关联的。在一个类的头文件里:

#pragma once
#pragma execution_character_set("utf-8")  // 在有输出的页面加上,可以解决中文乱码问题
#include <QObject>
class teacher : public QObject
{
	Q_OBJECT
public:
	explicit teacher(QObject *parent=0);

	signals:   
        // 自定义信号写到signals下,返回值是void,只需要声明不需要在.cpp实现,可以有参数,可以重载
        // 信号可以连接信号,一个信号可以连接多个槽函数,多个信号可以连接一个槽函数
        void hungry();

	public slots :  
        // 早期Qt版本槽函数必须要写到public slots下,高级版本可以写到public或者全局下。需要声明也需要在.cpp实现,可有参数也可以重载
        // 信号和槽函数的类型必须一一对应
        // 信号参数的个数可以多于槽函数参数的个数,但是类型也需要一一对应
		void treat();
};

Lambda表达式

lambda 来源于函数式编程的概念,也是现代编程语言的一个特点。lambda 表达式定义了一个匿名函数,并且可以捕获一定范围内的变量。lambda 表达式的语法形式可简单归纳如下:

[ capture ] ( params ) opt -> ret { body; };
例如:
f = [](int a) -> int { return a + 1; };

其中 capture 是捕获列表,params 是参数表,opt 是函数选项,ret 是返回值类型,body是函数体。可以通过捕获列表捕获一定范围内的变量:

  • [] 不捕获任何变量。
  • [&] 捕获外部作用域中所有变量,并作为引用在函数体中使用(按引用捕获)。
  • [=] 捕获外部作用域中所有变量,并作为副本在函数体中使用(按值捕获)。
  • [=,&foo] 按值捕获外部作用域中所有变量,并按引用捕获 foo 变量。
  • [bar] 按值捕获 bar 变量,同时不捕获其他变量。
  • [this] 捕获当前类中的 this 指针,让 lambda 表达式拥有和当前类成员函数同样的访问权限。如果已经使用了 & 或者 =,就默认添加此选项。捕获 this 的目的是可以在 lamda 中使用当前类的成员函数和成员变量。

默认状态下 lambda 表达式无法修改通过复制方式捕获的外部变量。如果希望修改这些变量的话,我们需要使用引用方式进行捕获。基本用法:

class A
{
    public:
    int i_ = 0;
    void func(int x, int y)
    {
        auto x1 = []{ return i_; };                    // error,没有捕获外部变量
        auto x2 = [=]{ return i_ + x + y; };           // OK,捕获所有外部变量
        auto x3 = [&]{ return i_ + x + y; };           // OK,捕获所有外部变量
        auto x4 = [this]{ return i_; };                // OK,捕获this指针
        auto x5 = [this]{ return i_ + x + y; };        // error,没有捕获x、y
        auto x6 = [this, x, y]{ return i_ + x + y; };  // OK,捕获this指针、x、y
        auto x7 = [this]{ return i_++; };              // OK,捕获this指针,并修改成员的值
    }
};
int a = 0, b = 1;
auto f1 = []{ return a; };               // error,没有捕获外部变量
auto f2 = [&]{ return a++; };            // OK,捕获所有外部变量,并对a执行自加运算
auto f3 = [=]{ return a; };              // OK,捕获所有外部变量,并返回a
auto f4 = [=]{ return a++; };            // error,a是以复制方式捕获的,无法修改
auto f5 = [a]{ return a + b; };          // error,没有捕获变量b
auto f6 = [a, &b]{ return a + (b++); };  // OK,捕获a和b的引用,并对b做自加运算
auto f7 = [=, &b]{ return a + (b++); };  // OK,捕获所有外部变量和b的引用,并对b做自加运算

一个容易出错的细节是关于 lambda 表达式的延迟调用的:

int a = 0;
auto f = [=]{ return a; };      // 按值捕获外部变量
a += 1;                         // a被修改了
cout << f() << endl;  // 输出?

在这个例子中,lambda 表达式按值捕获了所有外部变量。在捕获的一瞬间,a 的值就已经被复制到 f 中了。之后 a 被修改,但此时 f 中存储的 a 仍然还是捕获时的值,因此,最终输出结果是 0。

QMainWindow

是一个为用户提供主窗口程序的类,包含一个菜单栏(Menu bar)、多个工具栏(tool bars)、多个锚接部件(dock widgets)、一个状态栏(status bar)以及一个中心部件(central widget)。

QT

 QMainWindow实例:

QMainWindow0508::QMainWindow0508(QWidget *parent)
    : QMainWindow(parent)
{
    ui.setupUi(this);

	resize(600, 400);
	// 菜单栏创建,只能有一个
	QMenuBar *bar = menuBar();
	// 将菜单栏放入窗口中
	setMenuBar(bar);

	// 创建菜单
	QMenu *fileMenu = bar->addMenu("文件");
	QMenu *editMenu = bar->addMenu("编辑");

	// 创建菜单项
	QAction *newAction = fileMenu->addAction("新建");
	// 添加分割线
	fileMenu->addSeparator();
	QAction *openAction = fileMenu->addAction("打开");
		
	// 工具栏 可以有多个
	QToolBar *toolBar = new QToolBar(this);
	addToolBar(Qt::LeftToolBarArea, toolBar);
	toolBar->setAllowedAreas(Qt::LeftToolBarArea | Qt::RightToolBarArea);
	toolBar->setFloatable(false);
	toolBar->addAction(newAction);
	//toolBar->addSeparator();
	toolBar->addAction(openAction);
	QPushButton *btn = new QPushButton("hh", this);
	toolBar->addWidget(btn);

	// 状态栏,最多也只能有一个
	QStatusBar * stBar = statusBar();
	setStatusBar(stBar);
	// 放标签控件
	QLabel * label1 = new QLabel("提示信息", this);
	stBar->addWidget(label1); // addWidget默认从左侧提示
	QLabel * label2 = new QLabel("右侧提示信息", this);
	stBar->addPermanentWidget(label2); // addPermanentWidget默认从右侧提示

	// 铆接部件(浮动窗口),可以有多个
	QDockWidget * dockWidget = new QDockWidget("浮动", this);
	addDockWidget(Qt::BottomDockWidgetArea, dockWidget);

	// 设置中心部件,只能一个
	QTextEdit *edit = new QTextEdit(this);
	setCentralWidget(edit);

}

QT

菜单栏、状态栏和中心部件都只能有一个,故以 set...() 完成窗口的嵌入。菜单项、工具栏、铆接部件以及标签控件都可以有多个,故以 add...() 完成嵌入。

对话框

实现功能:点击新建按钮,弹出一个对话框,对话框分为模态对话框(不可以对其他窗口进行操作)和非模态对话框(可以对其他窗口进行操作)。其中ui.actionNew是一个按钮,这里使用 connect(对象,信号,槽函数) 这个重载方法。

connect(ui.actionNew, &QAction::triggered, [=]() {
    // 模态对话框
    QDialog dlg(this);
    dlg.resize(200, 100);
    dlg.exec();

    // 非模态对话框,创建在堆区让他一直存活
    QDialog *dlg2 = new QDialog(this);
    dlg2->setAttribute(Qt::WA_DeleteOnClose);
    dlg2->resize(200, 100);
    dlg2->show();
});

常见的有错误对话框、信息对话框、问题对话框和警告对话框:

// 错误对话框
QMessageBox::critical(this, "critical", "错误");
// 信息对话框
QMessageBox::information(this, "info", "信息");
// 问题对话框
if (QMessageBox::Save == QMessageBox::question(this, "ques", "提问", QMessageBox::Save | QMessageBox::Cancel))
{
	qDebug() << "保存";
}
// 警告对话框
QMessageBox::warning(this, "warn", "警告");

除此之外还有颜色对话框 QColorDialog::getColor、文件对话框 QFileDialog::getOpenFileName(father,title,path,filter) 和 字体对话框 QFontDialog::getFont 。

控件

  • QPushButton,常用按钮
  • QToolButton,工具按钮,用于显示图片,如想显示文字则需修改风格toolButtonStyle
  • RadioButton,单选按钮,设置默认 ui.radioButton->setChecked(true);
  • CheckBox,多选按钮,监听状态 &QCheckBox::stateChanged ,2为选中,1为半选中,0为未选
  • QListWidget,列表容器, QListWidgetItem 为一行内容,设置居中方式 item->setTextAlignment(Qt::AlignHCenter) ,可以用addlists一次性添加数行内容
windowLayout::windowLayout(QWidget *parent)
    : QMainWindow(parent)
{
    ui.setupUi(this);
	ui.radioButton->setChecked(true); // 设置默认
	connect(ui.radioButton, &QRadioButton::clicked, [=]() { // 若选中按钮,打印相关信息
		qDebug() << "选中男的";
	});
	connect(ui.radioButton_2, &QRadioButton::clicked, [=]() { 
		qDebug() << "选中女的";
	});
	connect(ui.checkBox, &QCheckBox::stateChanged, [=](int state) { // 监听状态
		qDebug() << state;
	});

	QListWidgetItem * item = new QListWidgetItem("锄禾日当午");	// 利用listWidget写诗
	ui.listWidget->addItem(item);  // 将一行诗放入到listWidget中
	item->setTextAlignment(Qt::AlignHCenter);
	QStringList list;
	list << "锄禾日当午" << "汗滴禾下土" << "谁之盘中餐" << "粒粒皆辛苦";
	ui.listWidget->addItems(list); // 利用addItems将数行内容放入listWidget中
}
  • QTreeWidget,树控件
QListWidgetItem * item = new QListWidgetItem("锄禾日当午");	// 利用listWidget写诗
ui.listWidget->addItem(item);  // 将一行诗放入到listWidget中
item->setTextAlignment(Qt::AlignHCenter);
QStringList list;
list << "锄禾日当午" << "汗滴禾下土" << "谁之盘中餐" << "粒粒皆辛苦";
ui.listWidget->addItems(list); // 利用addItems将数行内容放入listWidget中

ui.treeWidget->setHeaderLabels(QStringList() << "英雄" << "英雄介绍"); // 设置水平头
QTreeWidgetItem * strengthItem = new QTreeWidgetItem(QStringList() << "力量"); // 添加顶层节点
QTreeWidgetItem * agileItem = new QTreeWidgetItem(QStringList() << "敏捷");
QTreeWidgetItem * intelligenceItem = new QTreeWidgetItem(QStringList() << "智力");
ui.treeWidget->addTopLevelItem(strengthItem); // 添加顶层节点
ui.treeWidget->addTopLevelItem(agileItem);
ui.treeWidget->addTopLevelItem(intelligenceItem);
QStringList hero1;										// 追加子节点
hero1 << "刚被猪" << "前排坦克";
QTreeWidgetItem *l1 = new QTreeWidgetItem(hero1);
strengthItem->addChild(l1);
  • QTableWidget,列表控件,设置列数、设置行数、设置水平表头、将表格中填入数据
ui.tableWidget->setColumnCount(3);	// 设置列数
ui.tableWidget->setHorizontalHeaderLabels(QStringList() << "姓名" << "性别" << "年龄");  // 设置水平表头
ui.tableWidget->setRowCount(5);   // 设置行数
QStringList nameList;
nameList << "亚瑟" << "赵云" << "张飞" << "关羽" << "花木兰";
QStringList sexList;
sexList << "男" << "男" << "男" << "男" << "男";
for (int i = 0; i < 5; i++)
{
	int col = 0;
	ui.tableWidget->setItem(i, col++, new QTableWidgetItem(nameList[i]));
	ui.tableWidget->setItem(i, col++, new QTableWidgetItem(sexList.at(i)));
	ui.tableWidget->setItem(i, col++, new QTableWidgetItem(QString::number(i+18)));  // int转QString
}
  • stackedWidget,栈控件
  • comboBox,下拉框
  • QLable,显示图片,动图

https://blog.51cto.com/u_9291927/1879125

自定义控件

设置一个自定义控件,实现滑动 Slider 时 spinBox 里的值也跟着改变,同时改变 spinBox 时 Slider 也实现相应的滑动。点击获取当前值按钮时打印当前 Slider/spinBox 的值,点击设置为一半按钮时 spinBox 设置为50,Slider 滑动到正*。

QT

添加一个 Qt Widget 类取名 SmallWidget,设置 Slider 和 spinBox 控件,在主界面上拉一个 Widget 将其提升为 SmallWidget,将新创建的类的界面设置为主界面的一个子界面。在 SmallWidget 中实现信号槽连接,将 spinBox 改变的值设置到 Slider 上。因为 QSpinBox 中的 valueChanged 信号有重载函数,故需要函数指针的方式将要实现的重载函数地址传到 connect 中。

void(QSpinBox:: * SpinB)(int) = &QSpinBox::valueChanged;
connect(ui.spinBox, SpinB, ui.horizontalSlider, &QSlider::setValue);

同样将 Slider 上值的改变设置到 spinBox 中,QSlider 没有信号函数,将其父类 QAbstractSlider 的信号函数 valueChanged 拿来用,处理的槽函数为 QSpinBox 的槽函数。

connect(ui.horizontalSlider, &QSlider::valueChanged, ui.spinBox, &QSpinBox::setValue);

在 SmallWidget 要实现获取当前值 getNum 和设置为一半 setNum 这两个函数,这样在主界面的两个按钮可以通过信号槽机制来调用,实现相应的功能。

void SmallWidget::setNum(int num)
{
	ui.spinBox->setValue(num);
}
int SmallWidget::getNum()
{
	return ui.horizontalSlider->value();
}

在主界面上用信号槽实现按钮与子界面的互动,子界面的 id 为 widget。 

	connect(ui.pushButton, &QPushButton::clicked, [=]() {
		qDebug() << ui.widget->getNum();
	});
	connect(ui.pushButton_2, &QPushButton::clicked, [=]() {
		ui.widget->setNum(50);
	});

鼠标事件

  • 鼠标进入事件 enterEvent( QEvent )
  • 鼠标离开事件 leaveEvent
  • 鼠标按下 mousePressEvent( QMouseEvent )
  • 鼠标释放 mouseReleaseEven
  • 鼠标移动 mouseMoveEvent

实现一个简单的需求:对于界面上的一个 QLabel 控件,鼠标进入、离开、按下、移动、释放事件分别实现打印相应的信息。

在项目中新加一个 Qt 的类名为 myLabel,基类设置为 QWidget ,因为控件为 QLabel 类型,所以新加类的构造函数需要改变。

myLabel.cpp:
myLabel::myLabel(QWidget * parent) : QLabel(parent) {    // 1
	
}

myLabel.h:  
#include <QLabel>                            // 2
class myLabel : public QLabel {              // 3
	Q_OBJECT
    ...
};

在 UI 界面将该 QLabel 控件提升为 myLabel 类,分别实现对应事件的函数。鼠标移动事件与其余事件有所不同,因为是持续性的动作,所以需要用位操作符 & 来判断持续性的状态。组合按键函数 buttons() 可判断鼠标左右键和滚轮的按下情况。

void myLabel::mousePressEvent(QMouseEvent *ev)
{
	if (ev->button() == Qt::LeftButton)
	{
		QString str = QString("鼠标左键按下 x=%1 y=%2 ").arg(ev->x()).arg(ev->y());
		qDebug() << str;
	}
}
void myLabel::mouseReleaseEvent(QMouseEvent *ev)
{
	qDebug() << "鼠标释放";
}
void myLabel::mouseMoveEvent(QMouseEvent *ev)  // 移动是持续性的过程,所以需要用位操作符&来判断持续性的状态
{
	if (ev->buttons() & Qt::LeftButton)
	{
		QString str1 = QString("鼠标左键按下移动 x=%1 y=%2 ").arg(ev->x()).arg(ev->y());
		qDebug() << str1;
	}
}

void myLabel::enterEvent(QEvent *Event)
{
	qDebug() << "鼠标进入";
}
void myLabel::leaveEvent(QEvent *)
{
	qDebug() << "鼠标离开";
}

如果要实现鼠标移动到 Label 上不用按下就打印相关的信息,就在构造函数里加上 setMouseTracking(true),格式化字符串QString("%1 %2").arg(ev->x()).arg(ev->y()) 。

定时器Timer

有两种实现,第一种利用事件 timerEvent(),需要在构造函数里启动定时器并设置定时时间 startTimer( 毫秒 ),该函数的返回值是定时器 ID。第二种实现利用定时器类 QTimer,创建定时器对象并将其挂载到对象树再启动定时器,可以利用 &QPushButton::clicked 事件进行监听,来实现想要的逻辑。

Timer0511::Timer0511(QWidget *parent)
    : QWidget(parent)
{
    ui.setupUi(this);
	// 第一种方式
	id1 = startTimer(1000);   
	id2 = startTimer(2000);

	// 第二种方式	
	QTimer *timer = new QTimer(this);
	timer->start(500);

	connect(timer, &QTimer::timeout, [=]() {
		static int num = 1;							     // static 的性质:
		ui.label_3->setText(QString::number(num++));     // 局部特性: 作用范围仅限于本函数,所以这里跟下面第一种方式 id1 里的变量不冲突 
	});													 // 静态特性:存储在静态区,函数调用结束后不孝顺而保留原值。在下一次调用时,保留上一次调用结束时的值
	connect(ui.pushButton, &QPushButton::clicked, [=]() {
		timer->stop();
	});
	connect(ui.pushButton_2, &QPushButton::clicked, [=]() {
		timer->start();
	});
}

// 第一种方式
void Timer0511::timerEvent(QTimerEvent *ev)
{
	if( ev->timerId()==id1 )
	{
		static int num = 1;  
		ui.label->setText(QString::number(num++));
	}
	if (ev->timerId() == id2)
	{
		static int num2 = 1;
		ui.label_2->setText(QString::number(num2++));
	}
}

事件分发器

以上面的鼠标事件为例,鼠标按下时先进入事件分发器 bool event(QEvent *e),返回值类型为bool,该分发器进行一定的判断,当然也可以实现自己的逻辑,判断是否向下分发到 mousePressEvent( QMouseEvent ),若在此进行拦截让分发器返回 true 则不会继续向下分发。

bool myLabel::event(QEvent *e)
{
	if (e->type() == QEvent::MouseButtonPress)
	{
		QMouseEvent *ev = static_cast<QMouseEvent *>(e);  // static_cast用于良性转换,这样的转换风险较低,一般不会发生什么意外
		QString str = QString("Event函数中鼠标左键按下 x=%1 y=%2 ").arg(ev->x()).arg(ev->y());
		qDebug() << str;
		return true;   // return true则表示用户进行处理,不会继续向下分发,不建议
	}
	// 其他事件交给父类处理
	return QLabel::event(e);
}

事件过滤器

在事件到达事件分发器 bool event(QEvent *e) 之前,还可以通过事件过滤器再进行一次拦截。其使用有两个步骤,给控件安装事件过滤器再重写 eventfilter 事件。

.h:
ui.label->installEventFilter(this);    // 1
.cpp:
bool Event0511::eventFilter(QObject *obj, QEvent *e)   // 2
{
	if (obj == ui.label)
	{
		if (e->type() == QEvent::MouseButtonPress)
		{
			QMouseEvent *ev = static_cast<QMouseEvent *>(e);  // static_cast用于良性转换,这样的转换风险较低,一般不会发生什么意外
			QString str = QString("过滤器中鼠标左键按下 x=%1 y=%2 ").arg(ev->x()).arg(ev->y());
			qDebug() << str;
			return true;   // return true则表示用户进行处理,不会继续向下分发,不建议
		}
		// 其他事件交给父类处理
		return QWidget::event(e);
	}
}

QT

绘图设备

绘图设备是指继承 QPaintDevice 的子类,一共提供了四个这样的类,分别为 QPixmap、QBitmap、QImage 和 QPicture。

  • QPixmap 专门为图像在屏幕上的显示做了优化。
  • QBitmap 是 QPixmap 的一个子类,色深为1,可以使用 QPixmap 的 isQBitmap() 来确定这个QPixmap 是不是一个 QBitmap。
  • QImage 专门为图像像素级访问做了优化。
  • QPicture 则可以记录和重现 QPainter 的各条指令。

QFile文件读写

主要用到两个函数:

QString fileName = QFileDialog::getSaveFileName(this, tr("Save File"),
                             "/home/jana/untitled.png",
                             tr("Images (*.png *.xpm *.jpg)"));
QString fileName = QFileDialog::getOpenFileName(this, tr("Open File"),
                             "/home",
                             tr("Images (*.png *.xpm *.jpg)"));

其中参数的作用为设置父类界面,对话框的提示,默认路径以及文件过滤器。

tr()函数:

QString text1 = QObject::tr("hello"); QString text2 = QString("hello");

tr是用来实现国际化,如果你为这个程序提供了中文翻译包(其中hello被翻译成中文“你好”),那么text1的内容将是中文“你好”;如果你为程序提供且使用日文翻译包,那么text1的内容将是日文。

上一篇:qt 获取汉字拼音首字母


下一篇:Qt ftp 文件上传工具开发