1.简述
有时在windows环境下通过远程访问操作另一台linux主机。如使用putty和xshell来实现远程终端,使用Xftp来相互传输文件。现在想自己使用Qt开发一个文件传输软件。
FTP是基于TCP/IP的一种应用层文件传输协议,通过建立FTP服务器(默认端口21)-客服端的形式在各个主机间传输文件。
SFTP是基于FTP服务和ssh协议实现的一种加密文件传输协议,数据传输更加安全,但是相比喻FTP会损失一点效率。
开发者一般通过ssh工具来访问目标linux主机,linux系统一般集成ssh服务(默认端口为22)。用来传输文件的SFTP协议是一ssh的子协议,一般有ssh服务的主机都会自动开启sftp服务。但是linux主机并不一定会安装FTP服务器,需要自行安装。
Qt5.0之后移除了QFtp类(基于FTP协议实现的一个类),使用 QNetworkAccessManager 可以实现 Ftp 的上传/下载功能,后者在性能上和稳定性上有所提升。但有些原本 QFtp 有的功能 QNetworkAccessManager 却没有提供,例如:list、cd、remove、mkdir、rmdir、rename 等,前者的功能更加完善,提供的API更多,更加便于开发者使用。最为新手,开发一般的应用,当然选择QFtp更加有利,值得庆幸的是 QFtp 一直在维护,只需要下载源码自行编译即可使用。
本人的开发环境为Qt 5.12.0,MSVC2017 64bits编译器。下面将总结QFtp在Qt5以上版本编译和使用的经历,开对官方提供的示例进行了重构,写了个Ftp客户端,最终效果见下。
2.QFtp编译与部署
2.1 下载
到github,下载QFtp源码,https://github.com/qt/qtftp
git clone https://github.com/qt/qtftp.git
2.2 修改
打开qt工程,修改qftp.pro文件中框选的部分,修改为下图所示。修改qftp.h文件的qurlinfo.h头文件,改为下图,该头文件路径有问题。
2.3 编译
只构建src,构建需要perl环境,如果之前装qt时一起装了就可以正常编译,如果没装会报错,去官网下载安装就行。
2.4 部署
编译完成后见下图,将编译出来的文件分别放到Qt安装路径对应的目录下,使得其和Qt自带的模块一样使用。放置文件的标准就是生成的bin文件夹中的文件放置到Qt对应编译器目录下的bin文件夹中,其他类似。我的是mscv2017_64。
- bin目录下的两个dll文件复制到Qt5.12.1\5.12.1\msvc2017_64\bin目录下;
- lib目录下的.lib文件和.prl文件复制到Qt5.12.1\5.12.1\msvc2017_64\lib目录下;
- include目录下的QtFtp文件夹整个复制到Qt5.12.1\5.12.1\msvc2017_64\include目录下。这里有个坑,include中的qftp.h和qurlinfo.h不是头文件实体,里面只有一句include将源码的头文件分别包含进来,因此还需要把src文件夹下qftp.h和qurlinfo.h两个文件复制覆盖到Qt5.12.1\5.12.1\msvc2017_64\include中;
- mkspecs\modules-inst目录下的两个.pri文件复制到Qt5.12.1\5.12.1\msvc2017_64\mkspecs\modules目录下。
通过上述四步,相当于给Qt新增了一个QtFtp的组件。第一步和第二步准备该组件动态库,第三步准备头文件,第四步相当于定义QtFtp组件,配置IDE,下面是自带的QtNetwork组件(左)和QtFtp组件的对比。
2.5 使用
上面配置好后,在一个独立的工程里,需要先在.pro中添加组件,然后包含头文件就行。
QT += ftp
#include <QFtp>
3.QFtp运用
3.1 Ftp客户端
参考官方提供的例子,将其独立出来,重构了一下,继承QWidget写了一个类FtpWidget,方便以后在其他的综合项目里直接提升。
先用Qt Designer绘制一个出ui,结构比较简单。
然后,头文件ftpwidget.h
#ifndef FTPWIDGET_H
#define FTPWIDGET_H
#include <QWidget>
#include <QUrl>
#include <QFtp>
#include <QTreeWidgetItem>
#include <QFile>
namespace Ui {
class FtpWidget;
}
class FtpWidget : public QWidget
{
Q_OBJECT
public:
explicit FtpWidget(QWidget *parent = nullptr);
~FtpWidget();
private:
Ui::FtpWidget *ui;
private slots:
void downloadFile();
void cancelDownload();
void connectToFtp();
void ftpCommandFinished(int commandId, bool error);
void addToList(const QUrlInfo &urlInfo);
void processItem(QTreeWidgetItem *item, int column);
void cdToParent();
void updateDataTransferProgress(qint64 readBytes, qint64 totalBytes);
void enableDownloadButton();
void connectOrDisconnect();
private:
QHash<QString, bool> isDirectory;
QString currentPath;
QFtp *ftp = nullptr;
QFile *file = nullptr;
};
#endif // FTPWIDGET_H
然后,ftpwidget.cpp 源文件
#include "ftpwidget.h"
#include "ui_ftpwidget.h"
#include <QMessageBox>
FtpWidget::FtpWidget(QWidget *parent) :
QWidget(parent),
ui(new Ui::FtpWidget)
{
ui->setupUi(this);
this -> ui -> label_status -> setText("Please enter the name of an FTP server.");
this -> ui -> treeWidget_fileList -> setEnabled(false);
this -> ui -> treeWidget_fileList -> setRootIsDecorated(false);
this -> ui -> treeWidget_fileList -> setHeaderLabels(QStringList() << tr("Name") << tr("Size") << tr("Owner") << tr("Group") << tr("Time"));
this -> ui -> treeWidget_fileList -> header() ->setStretchLastSection(false);
this -> ui ->pushButton_cdToParent -> setIcon(QPixmap(":/images/images/cdtoparent.png"));
this -> ui ->pushButton_cdToParent -> setEnabled(false);
this -> ui ->pushButton_download -> setEnabled(false);
connect(ui->treeWidget_fileList, &QTreeWidget::itemActivated,this, &FtpWidget::processItem);
connect(ui->treeWidget_fileList, &QTreeWidget::currentItemChanged,this, &FtpWidget::enableDownloadButton);
connect(ui->pushButton_connect, &QPushButton::clicked, this, &FtpWidget::connectOrDisconnect);
connect(ui->pushButton_cdToParent, &QPushButton::clicked, this, &FtpWidget::cdToParent);
connect(ui->pushButton_download, &QPushButton::clicked, this, &FtpWidget::downloadFile);
setWindowTitle("FTPWidget");
}
FtpWidget::~FtpWidget()
{
delete ui;
}
void FtpWidget::connectOrDisconnect()
{
if (ftp) {
ftp -> abort();
ftp -> deleteLater();
ftp = nullptr;
this -> ui -> treeWidget_fileList -> clear();
this -> ui -> treeWidget_fileList -> setEnabled(false);
this -> ui ->pushButton_cdToParent -> setEnabled(false);
this -> ui ->pushButton_download -> setEnabled(false);
this -> ui ->pushButton_connect -> setEnabled(true);
this -> ui ->pushButton_connect -> setText(tr("Connect"));
this -> ui -> label_status -> setText("Please enter the name of an FTP server.");
return;
}
connectToFtp();
}
void FtpWidget::connectToFtp()
{
ftp = new QFtp(this);
connect(ftp, &QFtp::commandFinished,this, &FtpWidget::ftpCommandFinished);
connect(ftp, &QFtp::listInfo,this, &FtpWidget::addToList);
connect(ftp, &QFtp::dataTransferProgress,this, &FtpWidget::updateDataTransferProgress);
this -> ui -> treeWidget_fileList ->clear();
currentPath.clear();
isDirectory.clear();
QString ftpServer = this->ui->lineEdit_ftpServer->text();
ftp -> connectToHost(ftpServer,21);
ftp -> login("pi","302302302");
this -> ui -> label_status ->setText(tr("Connecting to FTP server %1...").arg(this->ui->lineEdit_ftpServer->text()));
}
void FtpWidget::downloadFile()
{
QString fileName = this -> ui -> treeWidget_fileList->currentItem()->text(0);
if (QFile::exists(fileName)) {
QMessageBox::information(this, tr("FTP"),
tr("There already exists a file called %1 in "
"the current directory.")
.arg(fileName));
return;
}
file = new QFile(fileName);
if (!file->open(QIODevice::WriteOnly)) {
QMessageBox::information(this, tr("FTP"),
tr("Unable to save the file %1: %2.")
.arg(fileName).arg(file->errorString()));
delete file;
return;
}
ftp->get(this -> ui -> treeWidget_fileList->currentItem()->text(0), file);
}
void FtpWidget::cancelDownload()
{
ftp->abort();
if (file->exists()) {
file->close();
file->remove();
}
delete file;
}
void FtpWidget::ftpCommandFinished(int, bool error)
{
if (ftp->currentCommand() == QFtp::ConnectToHost)
{
if (error) {
QMessageBox::information(this, tr("FTP"),
tr("Unable to connect to the FTP server "
"at %1. Please check that the host "
"name is correct.")
.arg(this->ui->lineEdit_ftpServer->text()));
connectOrDisconnect();
return;
}
else {
this -> ui -> label_status -> setText(tr("Logged onto %1.").arg(this->ui->lineEdit_ftpServer->text()));
this -> ui -> treeWidget_fileList-> setFocus();
this -> ui -> pushButton_download->setDefault(true);
this -> ui -> pushButton_connect-> setText("Disconnect");
return;
}
}
if (ftp->currentCommand() == QFtp::Login)
ftp->list();
if (ftp->currentCommand() == QFtp::Get) {
if (error) {
this->ui->label_status->setText(tr("Canceled download of %1.")
.arg(file->fileName()));
file->close();
file->remove();
} else {
this->ui->label_status->setText(tr("Downloaded %1 to current directory.")
.arg(file->fileName()));
this -> ui -> progressBar -> setValue(100);
file->close();
}
delete file;
enableDownloadButton();
} else if (ftp->currentCommand() == QFtp::List) {
if (isDirectory.isEmpty()) {
this -> ui -> treeWidget_fileList -> addTopLevelItem(new QTreeWidgetItem(QStringList() << tr("<empty>")));
this -> ui -> treeWidget_fileList -> setEnabled(false);
}
}
}
void FtpWidget::addToList(const QUrlInfo &urlInfo)
{
QTreeWidgetItem *item = new QTreeWidgetItem;
item->setText(0, urlInfo.name());
item->setText(1, QString::number(urlInfo.size()));
item->setText(2, urlInfo.owner());
item->setText(3, urlInfo.group());
item->setText(4, urlInfo.lastModified().toString("MMM dd yyyy"));
QPixmap pixmap(urlInfo.isDir() ? ":/images/images/dir.png" : ":/images/images/file.png");
item->setIcon(0, pixmap);
isDirectory[urlInfo.name()] = urlInfo.isDir();
this -> ui -> treeWidget_fileList->addTopLevelItem(item);
if (!this -> ui -> treeWidget_fileList->currentItem()) {
this -> ui -> treeWidget_fileList->setCurrentItem(this -> ui -> treeWidget_fileList->topLevelItem(0));
this -> ui -> treeWidget_fileList->setEnabled(true);
}
}
void FtpWidget::processItem(QTreeWidgetItem *item, int /*column*/)
{
QString name = item->text(0);
if (isDirectory.value(name)) {
this -> ui -> treeWidget_fileList->clear();
isDirectory.clear();
currentPath += '/';
currentPath += name;
ftp->cd(name);
ftp->list();
this-> ui -> pushButton_cdToParent->setEnabled(true);
return;
}
}
void FtpWidget::cdToParent()
{
this -> ui -> treeWidget_fileList -> clear();
isDirectory.clear();
currentPath = currentPath.left(currentPath.lastIndexOf('/'));
if (currentPath.isEmpty()) {
this-> ui -> pushButton_cdToParent ->setEnabled(false);
ftp->cd("/");
} else {
ftp->cd(currentPath);
}
ftp->list();
}
void FtpWidget::updateDataTransferProgress(qint64 readBytes,qint64 totalBytes)
{
this -> ui -> progressBar -> setValue(static_cast<int>(readBytes/totalBytes));
}
void FtpWidget::enableDownloadButton()
{
QTreeWidgetItem *current = this -> ui -> treeWidget_fileList->currentItem();
if (current) {
QString currentFile = current->text(0);
this->ui->pushButton_download->setEnabled(!isDirectory.value(currentFile));
} else {
this->ui->pushButton_download->setEnabled(false);
}
}
完整的源码分享出来放在github上,也放一份在CSDN上。
3.2 树莓派搭建FTP服务器
在树莓派上搭建一个FTP服务器用于测试
很简单,很多参考。
3.3 测试效果
文章最前面的视频,目前只实现了下载功能。