本来是打算用新的类QNetworkAccessManager实现的客户端的文件上传、下载、新建文件夹、重命名、删除和刷新等功能,但是QNetworkAccessManager没有提供原本在QFtp提供的list()、cd()、remove()、mkdir()、rmdir()、rename() 和 rawCommand()等操作,所以无奈之下只能选用了旧版本的QFtp来实现,毕竟既然官方都废弃了QFtp而选用QNetworkAccessManager来代替,那肯定是后者比前者更加可靠稳定。
虽然QFtp在Qt5.0之后就被官方移除了,但是其基本功能还是挺全的,虽然在上传下载文件的时候带中文时会乱码,但也是能解决的。
最近在做一个项目也是用到了QFtp上传文件到别人的服务器,突然就想到了要做一个简单的客户端实现其基本的功能,于是在闲暇时就慢慢的把功能给加上去了,虽然有点简陋但是还是实现了有不少的功能,当然也包括了前面提到上传下载带中文的文件乱码问题。
其实登录很简单,只需要先connect主机然后在登录,connect的时候需要主机的IP地址和端口,登录的时候需要用户名和密码,当然如果没设置当然就不需要了。
void MainWnd::on_tbConnent_clicked()
{
QString serverAddress = ui->leServerAddress->text();
if (serverAddress.isEmpty())
{
statusBar()->showMessage("服务器地址为空!", 2000);
return;
}
QString port = ui->lePort->text();
if (port.isEmpty())
{
statusBar()->showMessage("端口号为空!", 2000);
return;
}
QString account = ui->leAccount->text();
QString password = ui->lePassword->text();
// 如果已经登录了就不需要重复登录
if (ftp.state() != QFtp::LoggedIn)
{
ftp.connectToHost(serverAddress, port.toInt());
ftp.login(account, password);
}
// 保存到配置文件
saveToIni();
}
文件的上传和下载都是有可能带有中文的,所以在上传的时候在获取到的路径后需要把路径转为QFtp可识别的格式,下载也一样在右键获取到选中行的名称后需要把目录一起转格式后再传递给QFtp,因为都是从本地上传到FTP所以转的格式都是一样的,如果是列举获取到FTP目录时则是从FTP到本地,则转换刚好相反,后满会在列出目录时给出。
void MainWnd::onUpload()
{
QString path = QFileDialog::getOpenFileName(NULL, "", QString("C:/Users/Pangs/Desktop/"));
if (path.isEmpty()) return;
file.setFileName(path);
if (!file.open(QIODevice::ReadOnly)) return;
uploadPath = path;
// 解决中文乱码问题
QString name = path.mid(path.lastIndexOf("/") + 1);
path = QString("%1/%2").arg(currentPath).arg(name);
ftp.put(&file, QString::fromLatin1(path.toLocal8Bit()));
}
void MainWnd::onDownload()
{
int row = ui->tableWidget->currentRow();
if (row < 0) return;
QString name = ui->tableWidget->item(row, 0)->text();
if (listPath[name]) return;
QString path = QFileDialog::getSaveFileName(NULL, "", QString("C:/Users/Pangs/Desktop/%1").arg(name));
if (path.isEmpty()) return;
file.setFileName(path);
if (!file.open(QIODevice::WriteOnly)) return;
// 解决中文乱码问题
path = QString("%1/%2").arg(currentPath).arg(name);
ftp.get(QString::fromLatin1(path.toLocal8Bit()), &file);
}
新建文件夹首先就是自己判断列表中的名字然后在生成新的名称了,然后日期等当然是当前生成的时间了,然后发送命令去建立目录,当然这之前也要处理乱码问题之前就说过了这里就不再复述,最后因为是新建的文件夹我们只是默认给定一个目录名,具体需要什么还需要自己决定,所以我们默认就打开编辑器,当用户点击到别的地方时才会关闭编辑器。
void MainWnd::onCreateFolder()
{
QString name = createFolderName();
// 在底部插入
int row = ui->tableWidget->rowCount();
// 插入新的一行
ui->tableWidget->insertRow(row);
// 名称
ui->tableWidget->setItem(row, 0, new QTableWidgetItem(folderIcon(), name));
// 日期
ui->tableWidget->setItem(row, 1, new QTableWidgetItem(QDateTime::currentDateTime().toString("yyyy/MM/dd hh:mm")));
// 类型
QString type = folderType();
ui->tableWidget->setItem(row, 2, new QTableWidgetItem(type));
// 创建目录 解决中文乱码问题
oldName = name;
createFolder = true;
ftp.mkdir(QString::fromLatin1(oldName.toLocal8Bit()));
editRow = row;
listPath[oldName] = true;
listType[oldName] = type;
// 打开编辑
QTableWidgetItem *item = ui->tableWidget->item(row, 0);
ui->tableWidget->setCurrentCell(row, 0);
ui->tableWidget->openPersistentEditor(item); // 打开编辑
ui->tableWidget->editItem(item);
}
重命名跟删除文件事先都会判断当前要选中的行是否是返回上一级的那一行,如果不是才会进行下一步处理,重命名也是跟新建文件夹一样打开选中那一行的名称那项的编辑器,让用户输入后点击别的地方后关闭,注意的是如果用户输入的名称已存在则在用户点击其他地方关闭编辑器的时候把文件名还原回没改之前,如果不重复则可以rename修改。
删除文件还要看用户选中的是文件还是目录,删除文件用remove删除目录用rmdir,传入的都是要删除的那项的名称。
void MainWnd::onRename()
{
// 如果是多级目录,则选中的第一级就不给重命名
int row = ui->tableWidget->currentIndex().row();
if (currentPath.indexOf("/") >= 0 && row <= 0) return;
editRow = row;
oldName = ui->tableWidget->item(row, 0)->text();
QTableWidgetItem *item = ui->tableWidget->item(row, 0);
ui->tableWidget->setCurrentCell(row, 0);
ui->tableWidget->openPersistentEditor(item); // 打开编辑
ui->tableWidget->editItem(item);
}
void MainWnd::closePersistentEditor()
{
if (editRow < 0 || editRow >= ui->tableWidget->rowCount()) return;
QTableWidgetItem *item = ui->tableWidget->item(editRow, 0);
ui->tableWidget->closePersistentEditor(item); // 关闭编辑
// 重命名
QString newname = ui->tableWidget->item(editRow, 0)->text();
for (int i = 0; i < ui->tableWidget->rowCount(); i++)
{
QString name = ui->tableWidget->item(i, 0)->text();
if ((name == newname) && (listType[oldName] == listType[newname]))
{
if (!createFolder) statusBar()->showMessage("文件名已存在!", 2000);
ui->tableWidget->item(editRow, 0)->setText(oldName);
editRow = -1;
createFolder = false;
return;
}
}
editRow = -1;
ftp.rename(QString::fromLatin1(oldName.toLocal8Bit()),QString::fromLatin1(newname.toLocal8Bit()));
listPath[newname] = listPath[oldName];
listPath.remove(oldName);
listType[newname] = listType[oldName];
listType.remove(oldName);
}
void MainWnd::onRemove()
{
// 如果是多级目录,则选中的第一级就不给删
int row = ui->tableWidget->currentIndex().row();
if (currentPath.indexOf("/") >= 0 && row <= 0) return;
removeRow = row;
// 解决中文乱码问题
QString name = ui->tableWidget->item(row, 0)->text();
if (listPath[name]) ftp.rmdir(QString::fromLatin1(name.toLocal8Bit()));
else ftp.remove(QString::fromLatin1(name.toLocal8Bit()));
}
刷新只是先把列表数据全部删除,然后再根据当前是否是根目录,如果当前不是根目录则先插入一行用来双击返回上一级用,最后发送list命令列出当前目录下所有文件及目录,这里也要注意使用listPath以及listType记录列出来的文件或目录用作双击返回或者进入下一级等用途。
void MainWnd::clear()
{
listPath.clear();
listType.clear();
int rowCount = ui->tableWidget->rowCount();
for (int i = 0; i < rowCount; i++)
{
ui->tableWidget->removeRow(0);
}
}
void MainWnd::onRefresh()
{
// 清除表格
clear();
// 如果当前目录不是根目录,则先插入一行用来双击返回上一级
if (currentPath.indexOf("/") >= 0)
{
ui->tableWidget->insertRow(0);
ui->tableWidget->setItem(0, 0, new QTableWidgetItem(folderIcon(), "..."));
listType["..."] = folderType();
}
ftp.list();
}
在获取文件列表信息的时候文件名是有可能为中文的,所以这里需要转格式把QFtp的格式转为Qt的格式才不会乱码,所以之前是Qt的格式转为QFtp的格式使用QString::fromLatin1(name.toLocal8Bit());这里就返过来把QFtp格式转为Qt的格式使用QString::fromLocal8Bit(name.toLatin1())。
void MainWnd::listInfo(QUrlInfo url)
{
// 解决中文乱码问题
QString name = QString::fromLocal8Bit(url.name().toLatin1());
QString type = url.isDir() ? folderType() : fileType(name);
// 记录是否为目录
listType[name] = type;
listPath[name] = url.isDir();
int row = ui->tableWidget->rowCount();
// 插入新的一行
ui->tableWidget->insertRow(row);
// 名称
ui->tableWidget->setItem(row, 0, new QTableWidgetItem(url.isDir() ? folderIcon() : fileIcon(name), name));
// 日期
ui->tableWidget->setItem(row, 1, new QTableWidgetItem(url.lastModified().toString("yyyy/MM/dd hh:mm")));
// 类型
ui->tableWidget->setItem(row, 2, new QTableWidgetItem(type));
// 大小
if (url.isDir()) return;
ui->tableWidget->setItem(row, 3, new QTableWidgetItem(QString("%1 KB").arg(qMax(int(url.size() / 1000), 1))));
}
如果当前双击的是目录则把当前记录的目录名在加上现在进入的一层的名称,因为要显示新的数据所以要把旧数据清除,现在是进入下一级所以一定在顶端是有一条返回上一级的目录,所以在发送list前就把返回的目录建立好。
如果当前双击的是目录并且是返回上一级的目录,则把当前记录的目录名去掉最后一层,然后清除数据,做好准备接受新数据,因为是返回上一级,所以也有可能上一级就是根目录所以这时候就没有必要建立返回上一级的节点因为没有上一级了,如果上一级还不是根目录则需要建立一个返回上一级的节点。
void MainWnd::on_tableWidget_doubleClicked(const QModelIndex &index)
{
int row = index.row();
QString name = ui->tableWidget->item(row, 0)->text();
// 如果双击的是第0行,并且不是根目录,因为根目录没有返回上一级项,表示返回上一级
if (row == 0 && currentPath.indexOf("/") >= 0)
{
// 将当前目录减少一级
currentPath = currentPath.left(currentPath.lastIndexOf("/"));
// 清除表格
clear();
// 如果当前目录不是根目录,则先插入一行用来双击返回上一级
if (currentPath.indexOf("/") >= 0)
{
ui->tableWidget->insertRow(0);
ui->tableWidget->setItem(0, 0, new QTableWidgetItem(folderIcon(), "..."));
listType["..."] = folderType();
}
// 发送命令返回上一级,然后列出所有项
ftp.cd("../");
ftp.list();
}
// 如果双击的是其他行,并且是目录行,表示进入下一级
else if (listPath[name])
{
// 当前目录进入下一级
currentPath += QString("/%1").arg(name);
// 清除表格
clear();
// 如果当前目录不是根目录,则先插入一行用来双击返回上一级
ui->tableWidget->insertRow(0);
ui->tableWidget->setItem(0, 0, new QTableWidgetItem(folderIcon(), "..."));
listType["..."] = folderType();
// 发送命令进入下一级,然后列出所有项
ftp.cd(currentPath);
ftp.list();
}
}
在右键弹出菜单之前先判断是否有正在编辑名称的项,如果有则先要关闭,然后再把菜单的各个项加入到菜单并添加信号槽,使用exec函数阻塞,contextMenuEvent函数是重写了QTableWidget后重写的,只不过把它转到主窗口方便点。
void MainWnd::contextMenuEvent(QContextMenuEvent *event)
{
Q_UNUSED(event);
if (ui->tableWidget->rowCount() <= 0) return;
// 如果有未关闭的编辑项则先关闭
closePersistentEditor();
QMenu menu;
menu.addAction("上传", this, &onUpload);
menu.addAction("下载", this, &onDownload);
menu.addSeparator();
menu.addAction(folderIcon(), "新建文件夹", this, &onCreateFolder);
menu.addSeparator();
menu.addAction("重命名", this, &onRename);
menu.addAction("删除", this, &onRemove);
menu.addAction("刷新", this, &onRefresh);
menu.exec(QCursor::pos());
}
在各个命令结束后会响应commandFinished函数,如果命令成功err为false,否则为true,然后再根据各个步骤做出其他操作,statusBar()->showMessage()第二个参数2000表示显示多长时间,单位是ms。
void MainWnd::commandFinished(int, bool err)
{
int cmd = ftp.currentCommand();
switch (cmd)
{
case QFtp::Login:
if (!err) ftp.list(); // 成功则显示列表
statusBar()->showMessage(err ? ftp.errorString() : "服务器连接成功!", 2000);
break;
case QFtp::Close:
if (!err) {clear(); currentPath.clear(); } // 清除列表
statusBar()->showMessage(err ? ftp.errorString() : "断开服务器连接!", 2000);
break;
case QFtp::Get:
if (file.isOpen()) { file.flush(); file.close(); }
statusBar()->showMessage(err ? ftp.errorString() : "文件下载成功!", 2000);
break;
case QFtp::Put:
if (file.isOpen()) file.close();
if (!err) onInsertRow();
statusBar()->showMessage(err ? ftp.errorString() : "文件上传成功!", 2000);
break;
case QFtp::Rename:
if (!createFolder) statusBar()->showMessage(err ? ftp.errorString() : "重命名成功!", 2000);
createFolder = false;
break;
case QFtp::Remove:
if (!err) ui->tableWidget->removeRow(removeRow);
statusBar()->showMessage(err ? ftp.errorString() : "删除文件成功!", 2000);
break;
case QFtp::Mkdir:
statusBar()->showMessage(err ? ftp.errorString() : "创建文件夹成功!", 2000);
break;
case QFtp::Rmdir:
if (!err) ui->tableWidget->removeRow(removeRow);
statusBar()->showMessage(err ? ftp.errorString() : "删除文件夹成功!", 2000);
break;
}
}
readBytes表示当前上传或者下载的进度,totalBytes表示文件总大小,当开始下载或者上传时显示进度条,其他时间隐藏进度条。
void MainWnd::dataTransferProgress(qint64 readBytes, qint64 totalBytes)
{
progress.setVisible(readBytes != totalBytes);
progress.setMaximum(totalBytes);
progress.setValue(readBytes);
}
重载了QStyledItemDelegate,然后重载paint把选中后的焦点去掉,这样没有焦点就不会有虚线框了。
class MyStyledItemDelegate : QStyledItemDelegate
{
Q_OBJECT
public:
MyStyledItemDelegate(QObject *parent = 0) : QStyledItemDelegate(parent) {}
~MyStyledItemDelegate() {}
private:
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
QStyleOptionViewItem viewOption(option);
if (option.state.testFlag((QStyle::State_HasFocus)))
{
viewOption.state = viewOption.state ^ QStyle::State_HasFocus;
}
QStyledItemDelegate::paint(painter, viewOption, index);
}
};
设置表格列宽,重新设置itemDelegate,添加进度条,添加信号槽。
// 设置表格列宽
ui->tableWidget->setColumnWidth(0, 140);
ui->tableWidget->setColumnWidth(1, 120);
ui->tableWidget->setColumnWidth(2, 90);
ui->tableWidget->setColumnWidth(3, 70);
// 去除选中的虚线框
ui->tableWidget->setItemDelegate((QStyledItemDelegate *)new MyStyledItemDelegate);
// 设置进度条
progress.hide();
progress.setFixedHeight(10);
progress.setAlignment(Qt::AlignCenter);
statusBar()->addWidget(&progress, width());
// 信号槽
connect(&ftp, SIGNAL(listInfo(QUrlInfo)), SLOT(listInfo(QUrlInfo)));
connect(&ftp, SIGNAL(commandFinished(int,bool)), SLOT(commandFinished(int,bool)));
connect(&ftp, SIGNAL(dataTransferProgress(qint64,qint64)), SLOT(dataTransferProgress(qint64,qint64)));
匆忙写完,当然了也会有很多的问题,这里就不再多做修改了,写这个简单的客户端:一是为了让自己更加对FTP传输更加的了解,二是给出具体步骤代码让更多人的少走歪路,三是想记录自己的新的体会;当然了,也少不了各位网友的帮助,比如去掉虚线框、乱码、获取系统图标、文件夹类型等。
对于搭建FTP服务不太了解的可以看看:Windows 10下 搭建FTP服务器_Ilson_的博客-CSDN博客
下载链接https://download.csdn.net/download/jiesunliu3215/13718538?spm=1001.2014.3001.5503
QFtp在客户端实现给服务器一次性创建多级目录:Qt之QFtp 在客户端实现给服务器一次性创建多级目录_Ilson_的博客-CSDN博客
QetworkAccessManager实现FTP文件上传/下载功能:Qt之QNetworkAccessManager 实现FTP文件上传/下载功能_Ilson_的博客-CSDN博客
附加:测试put时,每次都会完成后报错 暂未找到问题根源
调用ftp->put 上传文件,出现“QIODevice::read (QTcpSocket, "QFtpDTP Passive state socket"): device not open”,文件实际上传成功。