Qt实现Socket从文件发送多幅图片(Qt③)

初学者记录学习内容,如有错误请各位前辈指点。

这次的项目实现将在上次完成“单幅图片从客户端发送到服务器”的基础上继续添加。
主要添加的功能的是
①客户端和服务器可以选择读取和保存图片的文件,而不是存入到内存中。
②发送接收不同格式的图片。
③服务器读取文件中所有的图片,显示到界面上,并且可以切换图片。
文档目录

服务器Server

服务器端的界面依旧是那样的简洁
server界面
sendButton发送图片,disconnectButton断开连接,picturePathEdit得出读取图片的路径,saveButton选择路径。
直接贴代码:
filepictureserver.h

#ifndef FILEPICTURESERVER_H
#define FILEPICTURESERVER_H

#include <QDialog>
#include <QTcpServer>
#include <QTcpSocket>
#include <QDebug>
#include <QFileDialog>
#include <QMessageBox>
#include <QStringList>
#include <vector>
#include <QByteArray>
#include <QDataStream>
#include <QBuffer>
#include <QString>

using namespace std;//std::vector

namespace Ui {
class filePictureServer;
}

class filePictureServer : public QDialog
{
    Q_OBJECT

public:
    explicit filePictureServer(QWidget *parent = 0);
    ~filePictureServer();

private slots:
    void sendPictures();
    void showDirectory();
    void disconnectSocket();
    void acceptConnection();
    void displayError(QAbstractSocket::SocketError);

private:
    Ui::filePictureServer *ui;
    QTcpServer *tcpServer;
    QTcpSocket *tcpSocket;
    QFileDialog *fileDialog;
    QStringList pictureString_list;
    vector<QPixmap> picturelist;
    int picIndex;

};

#endif // FILEPICTURESERVER_H

filepictureserver.cpp

#include "filepictureserver.h"
#include "ui_filepictureserver.h"
#include <windows.h>
#include "utility.h"

filePictureServer::filePictureServer(QWidget *parent) :
    QDialog(parent),
    ui(new Ui::filePictureServer)
{
    ui->setupUi(this);
    tcpServer = new QTcpServer(this);
    if(!tcpServer->listen(QHostAddress::Any,7777))
    {
        qDebug()<<tcpServer->errorString();
        close();
    }
    tcpSocket = NULL;
    connect(ui->sendButton,SIGNAL(clicked(bool)),this,SLOT(sendPictures()));
    connect(ui->saveButton,SIGNAL(clicked(bool)),this,SLOT(showDirectory()));
    connect(ui->disconnectButton,SIGNAL(clicked(bool)),this,SLOT(disconnectSocket()));
    connect(tcpServer,SIGNAL(newConnection()),this,SLOT(acceptConnection()));

    picIndex = 0;

}

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

void filePictureServer::sendPictures()
{
    if(tcpSocket==NULL)
        return;
    QByteArray block;
    QBuffer buffer;
    QString style;
    QDataStream out(&block,QIODevice::WriteOnly);
    out.setVersion(QDataStream::Qt_5_8);
    style = pictureString_list[picIndex].right(3);
    picturelist[picIndex].save(&buffer,style.toStdString().c_str());
    picIndex++;
    out<<(quint32)buffer.data().size();
    out<<style;
    block.append(buffer.data());
    tcpSocket->write(block);
    if(picIndex>=picturelist.size())
        picIndex = 0;
}

void filePictureServer::showDirectory()
{
    fileDialog=new QFileDialog(this);
    QString dir=fileDialog->getExistingDirectory(this,"Open Directory",
                  "D:\\QTproject\\pictureFromFileSocket",QFileDialog::ShowDirsOnly);
    ui->picturesPathEdit->setText(dir);
    if(dir==NULL)
        return;
    QString picturePath=ui->picturesPathEdit->text();
    GetFolderImages(picturePath,pictureString_list,false);
    for(int i=0;i<pictureString_list.size();i++)
    {
        QPixmap pix;
        pix.load(pictureString_list[i]);
        picturelist.push_back(pix);
    }
    if(fileDialog)
        delete fileDialog;
}

void filePictureServer::acceptConnection()
{
    tcpSocket = tcpServer->nextPendingConnection();
    connect(tcpSocket,SIGNAL(disconnected()),this,SLOT(deleteLater()));
    connect(tcpSocket,SIGNAL(error(QAbstractSocket::SocketError)),
   this,SLOT(displayError(QAbstractSocket::SocketError)));
}

void filePictureServer::disconnectSocket()
{
    if(tcpSocket==NULL)
        return;
    tcpSocket->abort();
    QMessageBox::about(NULL,"Connection","Connection stoped!");
}

void filePictureServer::displayError(QAbstractSocket::SocketError)
{
    qDebug()<<tcpSocket->errorString();
}

由于我只是记录这一周学习的结果,对于涉及到的函数并不能深刻地说清楚,只简单描述需要的功能可以用什么函数去实现,希望各位道友能有所获。
我们从showDirectory()开始说起,这里用到了QFileDialog,它提供了一个标准对话框通过文件系统去选择一个或者多个文件或目录。
getExistingDirectory()函数获取文件夹路径,注意第三个参数可以指定一个路径(绝对/相对皆可),在对话框打开的时候默认先到这个路径下,然后继续选择。然后由setText()显示到lineEdit上。
在使用文件路径之前,要加个判断得到的路径不为空。

隆重介绍自定义函数GetFolderImages(),并附上utility.cpp,在以后的学习和编程过程中,如果涉及到需要获取一个文件中的所有图片,我们可以另写一个utility.h与.cpp,直接将其拿来使用。

utility.h

#ifndef UTILITY_H
#define UTILITY_H
#include <QString>
#include <QStringList>

int GetFolderImages(const QString path, QStringList &string_list, bool sub_dir);

#endif // UTILITY_H

utility.cpp

#include <QDir>
#include <QDirIterator>
#include "utility.h"
int GetFolderImages(const QString path, QStringList &string_list, bool sub_dir)
{
    string_list.clear();
    int result = 0;
//    QString s = QDir::currentPath();
    QDir dir(path);
    if(!dir.exists())
    {
        return result;
    }

    QStringList filters;
    //用于设置文件名称过滤器,只为filters格式(后缀为.jpeg等图片格式)
    filters<<QString("*.jpeg")<<QString("*.jpg")<<QString("*.png")<<QString("*.tiff")<<QString("*.gif")<<QString("*.bmp")
          <<QString("*.mov")<<QString("*.mp4");

    QDirIterator *dir_iterator = NULL;
    if(sub_dir)
         //定义迭代器并设置过滤器,sub_dir若为true遍历子目录,若为false不遍历子目录。
        dir_iterator = new QDirIterator(path,filters,QDir::Files | QDir::NoSymLinks,    QDirIterator::Subdirectories);
    else
        dir_iterator = new QDirIterator(path,filters,QDir::Files | QDir::NoSymLinks);

    while(dir_iterator->hasNext())
    {
        dir_iterator->next();
        QFileInfo file_info = dir_iterator->fileInfo();
        QString absolute_file_path = file_info.absoluteFilePath();
        string_list.append(absolute_file_path);
        ++result;
    }
    if(dir_iterator!=NULL)
        delete dir_iterator;
  //该函数返回的图片路径储存在QStringList &string_list中,可取出数据调用。
    return result;
}

GetFolderImages()函数的三个参数,QString类型的path给出文件的路径;bool型的sub_dir若为true则还需要遍历当前目录的子目录下的图片,若为false则不遍历子目录。最后遍历得到的所有图片的路径会保存到QStringList类型的string_list中,注意查询操作QStringList类型变量的函数。

由于我们并不知道文件中图片的数量,因此应该使用vector容器来存储图片。

vector是一个动态数组,用于元素数量变化的对象数组。像数组一样,vector类也用从0开始的下标表示元素的位置;但和数组不同的是,当vector对象创建后,数组的元素个数会随着vector对象元素个数的增大和缩小而自动变化。

注意使用之前要添加#include ;和using namespace std;

   QStringList pictureString_list;
   vector<QPixmap> picturelist;

pictureString_list保存着所有图片的途径,声明一个QPixmap类型的vector容器。
注意函数bool QPixmap::load(const QString &fileName,……)会从给定的文件路径下载一个pixmap图片,返回值是bool型,得出是否加载成功。

   QPixmap pix;
   pix.load(pictureString_list[i]);
   picturelist.push_back(pix);

循环由路径加载图片到pix,然后将pix加入vector容器中。注意操作vector元素的函数void push_back(const T& x):向量尾部增加一个元素X。
然后说sendPictures()函数,与上一篇的相同,用QBuffer保存图片,写入到数据流中,并写入图片的大小,write()发送。不同之处在于,如果我们不只是发送BMP格式的图片,我们还需要保存图片的格式,一同发送过去,便于之后在客户端由数据流再转回到原本的格式。
一般情况下,图片路径的最后3个字符就是该图片的格式,我们用.right(3)获取。right()函数的功能是从字符串右端获取指定个数字符,保存到style中,写入到流中。
记得需要定义一个编号picIndex,注意每点击一次,发送一个图片,编号自增1,并加入判断,编号超过图片总数的时候,重置为0,可进行循环发送。
如果又看上一篇的道友应该会注意到这句代码,即将如下路径的图片保存到buffer中。

QPixmap(":/new/prefix1/sendPicture/007.bmp").save(&buffer,"BMP");

这次也是同样

picturelist[picIndex].save(&buffer,style.toStdString().c_str());

注意style是QString类型的图片格式,使用style.toStdString().c_str() 可以将Qstring转成char*。因为save函数的原型,第二个参数的类型是char*,这个转化方法需要记得,会经常使用。

bool QPixmap::save(QIODevice *device, const char *format = Q_NULLPTR, int quality = -1) const

客户端Client

客户端界面
贴代价如下:
filepictureclient.h

#ifndef FILEPICTURECLIENT_H
#define FILEPICTURECLIENT_H

#include <QDialog>
#include <QObject>
#include <QFileDialog>
#include <QTcpSocket>
#include <QMessageBox>
#include <QBuffer>
#include <QByteArray>
#include <QImageReader>
#include <vector>
//#include <QDateTime>
//#include <QString>

namespace Ui {
class filePictureClient;
}

class filePictureClient : public QDialog
{
    Q_OBJECT

public:
    explicit filePictureClient(QWidget *parent = 0);
    ~filePictureClient();

private slots:
    void showDirectory();
    void sendConnection();
    void displayError(QAbstractSocket::SocketError);
    void receivePictures();
    void plusPicture();
    void reducePicture();
private:
    Ui::filePictureClient *ui;
    QFileDialog *fileDialog;
    QTcpSocket *tcpSocket;
    quint32 blockSize;
    std::vector<QPixmap> pictureList;
    QPixmap pix;
    QImage image;
    int currentImgIndex;
    int pictureNumber;
};

#endif // FILEPICTURECLIENT_H

filePictureClient.cpp

#include "filepictureclient.h"
#include "ui_filepictureclient.h"

filePictureClient::filePictureClient(QWidget *parent) :
    QDialog(parent),
    ui(new Ui::filePictureClient)
{
    ui->setupUi(this);
    tcpSocket = new QTcpSocket(this);
    connect(ui->savingButton,SIGNAL(clicked(bool)),this,SLOT(showDirectory()));
    connect(ui->connectingButton,SIGNAL(clicked(bool)),this,SLOT(sendConnection()));
    connect(tcpSocket,SIGNAL(error(QAbstractSocket::SocketError)),this,SLOT(displayError(QAbstractSocket::SocketError)));
    connect(tcpSocket,SIGNAL(readyRead()),this,SLOT(receivePictures()));
    connect(ui->lastPictureButton,SIGNAL(clicked(bool)),this,SLOT(reducePicture()));
    connect(ui->nextPictureButton,SIGNAL(clicked(bool)),this,SLOT(plusPicture()));
    blockSize=0;
    currentImgIndex=0;
    pictureNumber = 1;
}

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

void filePictureClient::sendConnection()
{
    if(tcpSocket->state()!=QAbstractSocket::ConnectedState)
    {
    tcpSocket->connectToHost(ui->ipAdressLineEdit->text(),ui->portLineEdit->text().toInt());
    if(tcpSocket->waitForConnected(1000))
    {
        QMessageBox::about(NULL,"Connection","Connection success");
    }
    else
    {
        QMessageBox::about(NULL,"Connection","Connection timed out");
    }
    }
    else
        QMessageBox::information(NULL,"Connection","Connected!");
}

void filePictureClient::showDirectory()
{
    fileDialog=new QFileDialog(this);
    QString dir=fileDialog->getExistingDirectory(this,"Open Directory",
                  "D:\\QTproject\\pictureFromFileSocket",QFileDialog::ShowDirsOnly);
    ui->picturesPathLineEdit->setText(dir);
    if(fileDialog)
        delete fileDialog;
}

void filePictureClient::displayError(QAbstractSocket::SocketError)
{
    qDebug()<<tcpSocket->errorString();
}

void filePictureClient::receivePictures()
{
    while(tcpSocket->bytesAvailable()>0)
    {
        QDataStream in(tcpSocket);
        if(blockSize==0)
        {

            in.setVersion(QDataStream::Qt_5_8);
            if(tcpSocket->bytesAvailable()<sizeof(quint32))
                return;
            in>>blockSize;
        }

        if(tcpSocket->bytesAvailable()<blockSize)
            return;
        QString style;
        in>>style;
        QByteArray array = tcpSocket->read(blockSize);
        QBuffer buffer(&array);
        buffer.open(QIODevice::ReadOnly);

        QImageReader reader(&buffer,style.toStdString().c_str());
        image = reader.read();
        blockSize=0;

        if(!image.isNull())
        {
//  QDateTime time=QDateTime::currentDateTime();
//  QString str=time.toString("yyyy-MM-dd hh:mm:ss ddd");
            QString filename = ui->picturesPathLineEdit->text()+"/"+QString("%1.").arg(pictureNumber)+style;
            image.save(filename);
            pix.load(filename);
            pictureList.push_back(pix);
            pictureNumber++;
            blockSize=0;
        }
        pictureList[0]= pictureList[0].scaled(ui->showPictureLabel->size());
        ui->showPictureLabel->setPixmap(pictureList[0]);
    }

}

void filePictureClient::reducePicture()
{
    if(currentImgIndex==0)
        return;
    --currentImgIndex;
    pictureList[currentImgIndex] = pictureList[currentImgIndex].scaled(ui->showPictureLabel->size());
    ui->showPictureLabel->setPixmap(pictureList[currentImgIndex]);

}

void filePictureClient::plusPicture()
{
    if(currentImgIndex==pictureList.size()-1)
            return;
    ++currentImgIndex;
    pictureList[currentImgIndex] = pictureList[currentImgIndex].scaled(ui->showPictureLabel->size());
    ui->showPictureLabel->setPixmap(pictureList[currentImgIndex]);
}

同样savingButon得到保存图片的路径,最下面的lastPictureButton和nextPictureButton用于切换显示showPictureLabel上的保存到服务器文件中的图片。
同样在showDirectory()中,完成标准对话框选择文件路径的功能,注意和服务器相同,在局部函数中new一部分内存空间,要记得删除。
然后在receivePictures()中,与上一篇说到的相同,写出图片的大小,做两次判断。然后依数据流存入的顺序,用style接收QString类型的图片的格式,转化回原本的格式保存到QImage中。
保存图片就涉及到图片的命名问题,

bool QPixmap::save(const QString &fileName, const char *format = Q_NULLPTR, int quality = -1) const

注意这filename的格式是我们平常说的所在文件的路径+图片的命名,像上面提到的这个样子”:/new/prefix1/sendPicture/007.bmp”,此处常用的有两种方法:
①所在文件的路径+分隔符”/”+当前时间+”.”+图片格式。
我们也可以定义一个计数器,保存图片的同时让计数器变量自增,得到:
②所在文件的路径+分隔符”/”+计数变量+”.”+图片格式。
注意到注释中有按格式获取当前时间的函数,可做参考。我们用第二种方法

filename = ui->picturesPathLineEdit->text()+"/"+QString("%1.").arg(pictureNumber)+style;

需要注意到QString(“%1.”).arg(pictureNumber),这里是将arg()括号中的内容加转化为QString,然后替换%1,这个函数在数据库中最常用,下一篇就进行讲解。最后将图片加载到vector容器中,用于计数的图片编号+1以便下次点击时调用。
reducePicture(),plusPicture()两个槽函数不必多说,图片作为QPixmap类型已经加入到pictureList的vector容器中,直接调用即可。
最后的最后,用Label显示图片,需要注意图片自适应的问题。.scaled()括号内是Label的尺寸,然后再setPixmap()。

功能拓展与反思

在最初的设想中,是准备在一次点击之后,文件中的所有图片全部从服务器发送到客户端,实现方式是将全部图片一次性全部写入到流,只管发送,不考虑接收那边如何。原本想到是本地的服务器和客户端,应该可以传输成功的,但十张图片发送只能接收到一张。
后来改变思路,变成了如上解释的这种结构,没点击一次发送一张的结构。
随后贫僧还是想实现,一次点击能发送所有图片的功能,后简单实现实现如下:
在客户端实现了一张图片的保存和发送之后,向服务器反馈一个信号,即从客户端向服务器发送一个信息。

 if(tcpSocket==NULL)
        return;
    QByteArray block;
    QDataStream back(&block,QIODevice::WriteOnly);
    back.setVersion(QDataStream::Qt_5_8);
    back <<(int)0;
    tcpSocket->write(block);

然后在服务器端中利用发送图片时建立的tcpSocket句柄,去接收这个信号。

connect(tcpSocket,SIGNAL(readyRead()),this,SLOT(sendPictures()));

注意写的位置不是在构造函数中,而是在

void filePictureServer::acceptConnection()

中,tcpSocket句柄就是在这个函数中建立的。收到信号,触发sendPictures()函数,发送一张图片,发完一张图片后再次发送信号,可以形成循环。
或者还有一种挺常用的思路,比如可以设置一个变量(int)switchSignal=0,然后在服务器端接收客户端发来的信号,如果收到1,将switchSignal置为1,服务器端第二次及其之后发送图片之前将判断条件与switchSignal相与。如伪代码:

while(要发送的图片数量)
{
    if(switchSignal == 0)
        continue;
    ……
}

注意continue的用法,continue作用为结束本次循环,即跳过循环体中下面尚未执行的语句,接着进行下一次是否执行循环的判定。用来解决switchSignal第一次为0,后面为1的判断情况。

其实在不是本地传输,网速慢或者传输的文件比较大时候,一旦开始传输就会卡死,这就需要需要我们使用多线程的思路,使用QThread,将收发的函数写在run()中,调用另外的线程去执行,当需要断开或者暂停时,界面上可以操作而不至于卡住。

结束,如有错误,还望指正。

阅读更多
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页