QT项目二支持大文件传输的文件传输程序
目录
一、关键技术点
1、服务器多线程
要实现服务器一对多需要自定义一个继承于qtcpserver的类,并重写该类中的inconingconnection函数。
在该函数中再new一个新线程,在线程中接收数据。当有客户端连接时,就会调用inconingconnection函数,实例化一个线程。
2、收发数据
1、connect(newsocet,SIGNAL(readyRead()),this,SLOT(parsing_data()));//有数据来就读取
服务器中readyRead(),一有数据就读取,但是经过多次实验发现:发送端发送数据与接收端接收端接收数据并不是一个一个地对应的,这应该是qt做过某种优化。有可能,发送了几次数据,而接收端只触发一次信号。再有,如果发送端速率远远大于接收的速率,程序直接就崩溃了。如果是只是发一些小的文件,这些完全不用考虑,收发各一次就可以了。
本项目想收发个超级大的文件,就必须解决该问题。归根到底就是,tcp发送与接收不一致导致的粘包问题。
思路:
1、自定义一个结构体,对收发数据进行分类。先保证双方具有收发条件(发送方能打开文件并读取文件,接收方能创建文件并写入文件),再进行发送数据。
2、由于qt中用readyRead(),本来就会导致收发速率不一致,那就只有牺牲效率保证效果(我自己的想法)。进行约定,发送方发一个包,等到接收放接收后发一个反馈包后再发下一个(我这里默认接收方发的反馈包发送方一定能接收到,其实这里应该弄一个定时器,到了一定时间还没有反馈包就应该退出程序了,否则一直阻塞在等待反馈)。
3、收发双方,每次收到的数据都会根据结构体不同的标志位,拥有不同的处理方式。
4、客户端发数据线程与主界面用信号与槽交流打印进度条。
效果演示:
具体代码
1、服务器
main.cpp
-
-
-
-
int main(int argc, char *argv[])
-
{
-
QCoreApplication a(argc, argv);
-
myservices server;
-
-
server.listen(QHostAddress::AnyIPv4,6150);
-
-
return a.exec();
-
}
myservices.cpp
-
-
-
myservices::myservices(QObject * parent )
-
: QTcpServer(parent)
-
{
-
-
}
myservices.h
-
-
-
-
-
-
-
-
-
-
-
class myservices : public QTcpServer
-
{
-
Q_OBJECT
-
public:
-
myservices(QObject * parent =nullptr );
-
-
private:
-
void incomingConnection(qintptr s)
-
{
-
th = new qthread(s);
-
th->start();
-
}
-
public slots:
-
//void stat_run();
-
-
private:
-
-
qthread * th;
-
-
};
-
-
thread.cpp
-
#include "thread.h"
-
-
qthread::qthread(qintptr newsocket_qintptr)
-
:newsocket_qintptr(newsocket_qintptr)
-
{
-
newsocet =new QTcpSocket;
-
newsocet->setSocketDescriptor(newsocket_qintptr);//由描述符获得套接字
-
-
connect(newsocet,SIGNAL(readyRead()),this,SLOT(parsing_data()));//有数据来就读取
-
-
port= newsocet->peerPort();
-
-
}
-
void qthread::run()
-
{
-
-
}
-
-
void qthread::parsing_data()//每次收到的信息需要分类处理
-
{
-
-
struct stu buf;//接收信息的结构体
-
array1 = new QByteArray(newsocet->readAll());//接收信息的数组
-
//qDebug()<<"mm"<< mm;
-
memcpy(&buf,array1->data(),sizeof(struct stu));//转化到结构体
-
if(0 == buf.type)//本次数据不是文件
-
{
-
get_data(array1);
-
}else if(1 == buf.type)//本次数据为文件相关
-
{
-
if(1 == buf.a )
-
get_filename(array1);//获取文件名
-
if(1 == buf.b)//客户端文件打开成功
-
{
-
type2 =1;
-
read_num = buf.read_num;
-
tail_num = buf.tail_num;
-
// qDebug()<<"readnum:"<<read_num<<"tailnum:"<<tail_num;
-
struct stu buf;
-
buf.c =0;
-
buf.d = 2;
-
send_buf_to_client(buf);//服务器最后一次确认,客户端收到后直接发数据
-
return ;
-
}
-
else if(-1 == buf.b )
-
{//不能进行文件传输,两个都置零,意味整个过程需要重新来.并且删除已经创建的文件
-
type2 =0;
-
type1 =0;
-
file->remove();
-
}
-
if((1==type1)&& (1==type2))//准备工作已经做好,可以进入接收数据函数
-
-
{
-
get_file();//开始接收数据
-
}
-
}
-
}
-
-
void qthread::get_data(QByteArray *array)//获取其它信息
-
{
-
QByteArray array2;
-
-
int i =0;
-
struct stu buf;//接收信息的结构体
-
int read_num =0;//数据包个数
-
int tail_num =0 ;//最后一次数句字数
-
memcpy(&buf,array->data(),sizeof(struct stu));//转化到结构体
-
read_num = buf.read_num ;//将数据包个数读出来
-
tail_num = buf.tail_num;//将最后一次字数读出来
-
while(read_num--)
-
{
-
memcpy(&buf, array->data() i*sizeof(struct stu),sizeof(struct stu));//将每个数据包从array中拿出来,发的结构体,拿的时候也拿结构体大小
-
array2.append(buf.ar);
-
i ;
-
}
-
qDebug()<<array2.toStdString().c_str();//打印到屏幕
-
array->resize(0);
-
}
-
-
-
void qthread::get_filename(QByteArray *array)//获取文件名
-
{
-
qDebug()<<"获取文件名";
-
struct stu buf;//接收信息的结构体
-
struct stu buf2;//回信息的结构体
-
memcpy(&buf,array->data(),sizeof(struct stu));//转化到结构体
-
qDebug()<< buf.filename;
-
//根据文件名检查文件名是否重名
-
QString str( buf.filename);
-
filename = str.section('/',-1);//去除路径,获得文件名,
-
QFileInfo fi(filename);
-
if( fi.isFile())//文件已经存在。
-
{
-
buf2.c = -1;
-
qDebug()<<"文件已经存在";
-
}else{//文件不存在,可以创建文件。
-
file = new QFile(filename);
-
if( file->open(QIODevice::WriteOnly))
-
{
-
buf2.c = 2;
-
type1=1;
-
qDebug()<<"创建文件成功";
-
qDebug()<<"type1"<<type1;
-
}//文件创建成功标志置位
-
else {buf2.c =-2;}//创建失败
-
}
-
buf2.d=0;
-
send_buf_to_client(buf2);//反馈创建文件情况
-
array->resize(0);
-
}
-
-
void qthread::get_file()
-
{
-
struct stu buf;//接收数据结构体
-
memcpy(&buf, (array1->data()),sizeof(buf));//将每个数据包从array中拿出来,发的结构体,拿的时候也拿结构体大小
-
ii ;//收取数据包计数
-
struct stu buff;//反馈数据结构体
-
buff.d =1;//反馈数据标志位
-
buff.c = 0;//反馈数据标志位
-
-
if(ii == (read_num))//最后一个包
-
{
-
file->write(buf.ar,tail_num);
-
type1 =0;
-
type2 = 0;
-
ii=0;
-
file->close();//传输完毕,所有标志位复位。等待下一次传输
-
qDebug()<< filename<<"传输完毕";
-
return;
-
}else{
-
qDebug()<< buf.ar;
-
file->write(buf.ar,sizeof(buf.ar));
-
}
-
if(read_num !=1)//文件太小只需要传输一次,不需要再反馈
-
{
-
usleep(1000);
-
send_buf_to_client(buff);//反馈创建文件情况
-
}
-
}
-
void qthread::send_buf_to_client(struct stu buf)//给客户端回消息
-
{
-
qDebug()<<"port: " <<port<<"反馈信息";
-
QByteArray array1;
-
array1.resize(sizeof(struct stu));
-
memcpy(array1.data(),&buf,sizeof(struct stu));
-
qDebug()<<"fanhui: "<< newsocet->write(array1) << "d" << buf.d;
-
newsocet->waitForBytesWritten();
-
}
-
thread.h
-
#ifndef THREAD_H
-
#define THREAD_H
-
#define MAX (1024*13)
-
#include<QThread>
-
#include<QTcpSocket>
-
#include<iostream>
-
#include<QAbstractSocket>
-
#include<QDebug>
-
#include<QHostAddress>
-
#include<QInternal>
-
#include<QFile>
-
#include<QByteArray>
-
#include<QFile>
-
#include<QFileInfo>
-
struct stu{
-
char filename[128];
-
char ar[MAX];
-
int type;
-
int read_num;
-
int a;//告诉服务器创建文件
-
int b;//告诉服务器文件打开情况
-
int c;//服务器反馈信息
-
int d;
-
int tail_num;
-
};
-
-
-
class qthread : public QThread
-
{
-
Q_OBJECT
-
public:
-
qthread(qintptr newsocket_qintptr);
-
~qthread()
-
{
-
newsocet->close();
-
delete newsocet;
-
}
-
void run();
-
void send_buf_to_client(struct stu);
-
public slots:
-
void get_file();//收文件
-
void get_data(QByteArray*);//收数据
-
void parsing_data();//解析数据,判断
-
void get_filename(QByteArray*);//收取文件名
-
-
signals:
-
-
private:
-
qintptr newsocket_qintptr;//套接字描述符
-
QTcpSocket *newsocet;//连接套接字
-
struct stu stu1;
-
int num =0;
-
QHostAddress ip;
-
QFile *file;
-
int type1 =0;//文件创建成功标志(0失败)
-
int type2 = 0;//客户端文件打开成功标志(0失败,)
-
int read_num;
-
int tail_num;
-
int ii=0;
-
QByteArray *array1;
-
int mm=0;
-
int d=0;
-
QString filename;
-
-
int port;
-
};
-
-
#endif // THREAD_H
-
客户端
main.cpp
-
-
-
-
int main(int argc, char *argv[])
-
{
-
QApplication a(argc, argv);
-
Widget w;
-
w.show();
-
-
return a.exec();
-
}
-
widget.cpp
-
#include "widget.h"
-
Widget::Widget(QWidget *parent)
-
: QWidget(parent)
-
{
-
// pgd = new QProgressDialog;
-
box = new QMessageBox;
-
tcp = new QTcpSocket;
-
te1 = new QTextEdit;//提示框
-
te1->setMinimumSize(200,200);
-
le = new QLineEdit("192.168.174.1");
-
bt1 = new QPushButton("发送信息");
-
bt3 = new QPushButton("取消连接");
-
bt2 =new QPushButton("连接");
-
bt4 = new QPushButton("选择文件");
-
bt5= new QPushButton("发送文件");
-
te2 = new QTextEdit;//ip输入
-
// te2->setMaximumSize(200,70);
-
QGridLayout *gri = new QGridLayout;
-
gri->addWidget(te1,0,0,1,3);//提示框
-
gri->addWidget(le,1,0,1,2);//行编辑框
-
gri->addWidget(bt2,1,2,1,1);//连接按钮
-
gri->addWidget(te2,2,0,1,2);//输入框
-
gri->addWidget(bt4,2,2,1,1);//选择文件按钮
-
gri->addWidget(bt1,3,0,1,1);//发送按钮
-
gri->addWidget(bt5,3,1,1,1);//发送文件按钮
-
gri->addWidget(bt3,3,2,1,1);//取消连接按钮
-
setLayout(gri);
-
bt1->setDisabled(true);
-
bt5->setDisabled(true);
-
-
th = new mythread(te1,le,te2);
-
connect(bt2,SIGNAL(clicked(bool)),th,SLOT(connect_server()));//连接服务器
-
connect(th,&mythread::connect_sucess,[&](){//线程返回连接成功,使能按钮
-
bt5->setDisabled(false);
-
bt1->setDisabled(false);
-
});
-
connect(bt3,SIGNAL(clicked(bool)),th,SLOT(discinnect()));//断开连接
-
connect(th,&mythread::disconnect_success,[&](){//线程返回断开连接成功,使能按钮
-
bt5->setDisabled(true);//失能按钮
-
bt1->setDisabled(true);//失能按钮
-
});
-
connect(bt4,SIGNAL(clicked(bool)),th,SLOT(choose_filename()));//选择文件
-
connect(bt1,SIGNAL(clicked(bool)),th,SLOT(send_data()));//发送信息
-
connect(bt5,SIGNAL(clicked(bool)),th,SLOT(send_filename()));//发送文件
-
connect(th,SIGNAL(remind_send_file()),this,SLOT(start_send_file()));//收到线程信号,可以让线程工作了
-
pgd = new QProgressBar;
-
pgd->setRange(0,100);
-
pgd->setMinimumSize(500,50);
-
pgd->setMaximumSize(500,50);
-
//pgd->setParent(this);
-
pgd->hide();
-
connect(th,SIGNAL(send_progress(int)),pgd,SLOT(setValue(int)));//进度条实时赋值
-
-
}
-
-
Widget::~Widget()
-
{
-
-
}
-
void Widget::start_send_file()
-
{
-
th->start();
-
pgd->show();
-
}
widget.h
-
#ifndef WIDGET_H
-
#define WIDGET_H
-
-
#include <QWidget>
-
#include<QDebug>
-
#include<QLineEdit>
-
#include<QTextEdit>
-
#include<QPushButton>
-
#include<QVBoxLayout>
-
#include<QHBoxLayout>
-
#include<QTcpSocket>
-
#include<QHostAddress>
-
#include<QGridLayout>
-
#include<QByteArray>
-
#include<QFileDialog>
-
#include<QByteArray>
-
#include<QMessageBox>
-
#include<QProgressDialog>
-
#include<QThread>
-
#include<QProgressBar>
-
#include "mythread.h"
-
-
-
class Widget : public QWidget
-
{
-
Q_OBJECT
-
-
public:
-
Widget(QWidget *parent = 0);
-
~Widget();
-
public slots:
-
void start_send_file();
-
-
// void stop_send_file();
-
-
signals:
-
-
private:
-
QTextEdit *te1;
-
QTextEdit *te2;
-
QLineEdit *le;
-
QPushButton *bt1;
-
QPushButton *bt2;
-
QPushButton *bt3;
-
QPushButton *bt4;
-
QPushButton *bt5;
-
QTcpSocket *tcp;
-
QMessageBox *box;
-
QStringList *filepaths;
-
QFile *file;
-
// int b=0;//服务器确认项,创建新文件成功(0失败)
-
QProgressBar *pgd;
-
-
int read_num;//传输次数
-
int tail_num;//最后一次字节数
-
int d=0;
-
mythread *th;
-
};
-
#endif // WIDGET_H
-
-
-
mythread.cpp
-
#include "mythread.h"
-
-
mythread::mythread(QTextEdit *te1, QLineEdit *le,QTextEdit *te2)
-
:te1(te1),le(le),te2(te2)
-
{
-
tcp = new QTcpSocket;
-
connect(tcp,SIGNAL(readyRead()),this,SLOT(recv_data()));//接收信息
-
box = new QMessageBox;
-
}
-
-
void mythread::run()
-
{
-
int i = 0;
-
struct stu buf;
-
buf.type =1;
-
buf.a=0;
-
buf.b = 0;//三个标志置位,代表本次数据为文件内容,服务器无需判断直接存储。
-
QByteArray array;
-
array.resize(sizeof(struct stu));
-
int read_num1 = read_num;//传输次数
-
while(1)//文件内容打包好发出去
-
{
-
if(d!=0)
-
{
-
i ;
-
qDebug()<<"read"<<read_num<<"tail_num"<<tail_num;
-
file->read(buf.ar,sizeof(buf.ar));
-
qDebug()<<buf.ar;
-
memcpy(array.data(),&buf,sizeof(struct stu));
-
read_num--;
-
tcp->write(array);
-
usleep(3000);
-
tcp->waitForBytesWritten(30000);
-
d =0;
-
emit send_progress((i%read_num1)>0?(i*100/read_num1 1):(i*100/read_num1));//发送进度
-
}
-
if(!read_num)//传输完毕,退出循环
-
{
-
file->close();
-
break;
-
}
-
}
-
qDebug()<<"文件传输完毕";
-
}
-
-
void mythread::connect_server()//连接服务器
-
{
-
QString str = le->text();
-
tcp->connectToHost(str,6150);
-
if(tcp->waitForConnected(1000)){
-
te1->append("连接成功");
-
emit connect_sucess();//告诉界面连接成功
-
}else{
-
te1->append("连接失败");
-
emit connect_failed();
-
}
-
}
-
void mythread::discinnect()//断开连接
-
{
-
tcp->close();
-
te1->append("连接断开");
-
emit disconnect_success();
-
}
-
-
void mythread::send_data()//发送较长消息
-
{
-
int i =0;
-
int num =0;
-
struct stu buf;
-
buf.type =0;
-
QByteArray array;//发送数据载体
-
QByteArray array1 =te2->toPlainText().toStdString().c_str() ;//把数据从文本框取出来
-
QByteArray array2 ;//中转载体
-
-
array.resize(sizeof(struct stu));//重新设置数组大小
-
num = (array1.length())/(sizeof(buf.ar ));//本次发送整次数
-
buf.tail_num = (array1.length())%(sizeof( buf.ar)) ;//最后一次发送的字节数
-
if(buf.tail_num != 0){ num = 1;}//本次发送最终次数
-
buf.read_num = num;
-
qDebug()<< "num"<<buf.read_num;
-
while(num--)
-
{
-
if(num ==0 && buf.tail_num!= 0)//最后一次数据包字数
-
{
-
array2 = array1.mid(array1.length()-buf.tail_num,buf.tail_num);
-
}else{
-
array2 = array1.mid(i*MAX,MAX);
-
}
-
strncpy( buf.ar,array2.data(),sizeof(buf.ar));//数据存入结构体
-
memcpy(array.data(),&buf,sizeof(struct stu));//把结构体存入数组
-
qDebug()<< tcp->write(array);//把数组发出去
-
qDebug()<<buf.ar;
-
i ;
-
}
-
te1->append("my: " te2->toPlainText());
-
te2->clear();
-
qDebug()<<"sucess";
-
return;
-
}
-
-
void mythread::recv_data()//接收信息
-
{
-
QByteArray array;
-
struct stu buf;
-
array.resize(sizeof(struct stu));
-
array = tcp->readAll();
-
memcpy(&buf,array.data(),sizeof(struct stu));
-
if(-1 == buf.c)//文件已经存在
-
{
-
qDebug()<<"文件已经存在";
-
box->setText("文件名重名,重新改个名字试试吧。");
-
box->setInformativeText("注意:需要更改源文件名字,而不是直接在本软件中更改。");
-
box->setStandardButtons( QMessageBox::Discard | QMessageBox::Cancel);
-
box->setDefaultButton(QMessageBox::Save);
-
box->exec();
-
return;
-
}
-
if(-2 == buf.c)//文件打开失败
-
{
-
return;
-
}
-
if(buf.d ==1)//服务器每次接收确认
-
{
-
d =buf.d;
-
}else if(buf.d ==2)//服务器最后一次确认。
-
{
-
d =buf.d;
-
emit remind_send_file();//提醒界面可以启动线程run函数了
-
}
-
if(2 == buf.c)//服务器创建并文件打开成功
-
open_file();//打开本地文件
-
}
-
-
-
-
void mythread::choose_filename()//选择文件
-
{
-
QStringList list = QFileDialog::getOpenFileNames();
-
-
filepaths = new QStringList(list);
-
te2->clear();
-
for(int i=0; i<filepaths->length(); i )
-
{
-
te2->append((*filepaths)[i]);
-
}
-
}
-
-
void mythread::send_filename() //打包文件名,并发送
-
{
-
struct stu buf_filename;
-
QByteArray array;
-
array.resize(sizeof(struct stu));//给array分配空间
-
QByteArray array1= QString((*filepaths)[0]).toStdString().c_str();//将文件名转存在数组中
-
strncpy(buf_filename.filename,array1.data(),sizeof(buf_filename.filename));//数组内容打包到结构体中
-
buf_filename.type =1;//提示服务器,本次数据属于文件子路
-
buf_filename.a =1;//高速服务器本条消息为文件名
-
buf_filename.read_num =1;//本次仅传输一次
-
memcpy(array.data(),&buf_filename,sizeof(buf_filename));//把结构体存入数组
-
qDebug()<< tcp->write(array);//把数组发出去
-
// QString str( buf_filename.filename);
-
// QString filename = str.section('/',-1);//去除路径,获得文件名,
-
file = new QFile(buf_filename.filename);
-
qDebug()<<buf_filename.filename <<"文件名";
-
}
-
-
-
-
void mythread::open_file()//打开本地文件
-
{
-
struct stu buf;
-
QByteArray array;
-
buf.type=1;
-
buf.a=0;
-
array.resize(sizeof(struct stu));
-
if(file->open(QIODevice::ReadOnly))
-
{
-
buf.b=1;
-
if(!file->size())
-
{
-
buf.b=-1;
-
box->setText("源文件不能为空。");
-
box->setInformativeText("请查看源文件是否为空。");
-
box->setStandardButtons( QMessageBox::Discard | QMessageBox::Cancel);
-
box->setDefaultButton(QMessageBox::Save);
-
box->exec();
-
file->close();
-
memcpy(array.data(),&buf,sizeof(struct stu));
-
tcp->write(array);//告诉服务器,文件为空。
-
return ;
-
}
-
-
buf.tail_num = (file->size()%sizeof(buf.ar));
-
buf.read_num = file->size()/sizeof(buf.ar) ((buf.tail_num)>0?1:0);
-
read_num = buf.read_num;//计算出传输次数
-
tail_num = buf.tail_num;//最后一次字节数
-
qDebug()<<"readnum:"<<read_num<<"tailnum:"<<tail_num;
-
qDebug()<<"filenum"<<file->size();
-
-
memcpy(array.data(),&buf,sizeof(struct stu));
-
tcp->write(array);//告诉服务器,这边已经准备好了
-
tcp->waitForBytesWritten(3000);
-
}else{
-
qDebug()<<"sdsdesdwe";
-
buf.b=-1;
-
box->setText("文件打开失败");
-
box->setInformativeText("文件打开失败,请检查下文件。");
-
box->setStandardButtons( QMessageBox::Discard | QMessageBox::Cancel);
-
box->setDefaultButton(QMessageBox::Save);
-
box->exec();
-
memcpy(array.data(),&buf,sizeof(struct stu));
-
tcp->write(array);
-
return ;
-
}
-
return ;
-
}
-
mythread.h
-
#ifndef MYTHREAD_H
-
#define MYTHREAD_H
-
#define MAX (1024*13)
-
-
#include<QHostAddress>
-
#include<QByteArray>
-
#include<QFileDialog>
-
#include<QMessageBox>
-
#include<QProgressDialog>
-
#include<QThread>
-
#include<QTcpSocket>
-
#include<QDebug>
-
#include<QTextEdit>
-
#include<QLineEdit>
-
#include<QObject>
-
#include<QFile>
-
#include<QFileDialog>
-
#include<QMessageBox>
-
#include<QProgressBar>
-
struct stu{
-
char filename[128];//文件名
-
char ar[MAX];//数据体
-
int type;//传输的是否是文件
-
int read_num;//传输次数
-
int a;//告诉服务器创建文件
-
int b;//告诉服务器文件打开情况
-
int c;//服务器反馈信息
-
int d=1;
-
int tail_num;//最后一次字节数
-
};
-
-
class mythread : public QThread
-
{
-
Q_OBJECT
-
public:
-
mythread(QTextEdit *, QLineEdit *,QTextEdit *);
-
void run();
-
-
void open_file();//打开本地文件
-
void send_filedata();//发送文件
-
signals:
-
void connect_sucess();//发出信号,连接成功
-
void connect_failed();//发出信号连接失败
-
void disconnect_success();//发出信号已经断开连接
-
void remind_send_file();//提醒界面可以启动线程run函数发文件了
-
void send_progress(int);
-
-
public slots:
-
void connect_server();//执行连接
-
void discinnect();//断开连接
-
void choose_filename();//选择文件
-
void send_data();//发送较长消息
-
void send_filename(); //打包文件名,并发送
-
void recv_data();//接收信息
-
-
-
private:
-
QTcpSocket *tcp;
-
QTextEdit *te1;
-
QLineEdit *le;
-
QTextEdit *te2;
-
QStringList *filepaths;
-
QFile *file;
-
int read_num;//传输次数
-
int tail_num;//最后一次字节数
-
int d=1;
-
QMessageBox *box;
-
-
};
-
-
#endif // MYTHREAD_H
-
这篇好文章是转载于:学新通技术网
- 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
- 本站站名: 学新通技术网
- 本文地址: /boutique/detail/tanhgbejje
系列文章
更多
同类精品
更多
-
photoshop保存的图片太大微信发不了怎么办
PHP中文网 06-15 -
Android 11 保存文件到外部存储,并分享文件
Luke 10-12 -
《学习通》视频自动暂停处理方法
HelloWorld317 07-05 -
word里面弄一个表格后上面的标题会跑到下面怎么办
PHP中文网 06-20 -
photoshop扩展功能面板显示灰色怎么办
PHP中文网 06-14 -
微信公众号没有声音提示怎么办
PHP中文网 03-31 -
excel下划线不显示怎么办
PHP中文网 06-23 -
excel打印预览压线压字怎么办
PHP中文网 06-22 -
怎样阻止微信小程序自动打开
PHP中文网 06-13 -
TikTok加速器哪个好免费的TK加速器推荐
TK小达人 10-01