一个简单的工具开发:从学生端更新程序部署工具说起,浅谈qt中ui的使用和TCP协议下文件的收发、以及可执行文件的打包

写在前面,Qt Designer是一个非常操蛋的页面编辑器,它非常的...怎么说呢,生硬,也可能是我现在用的这个Qt Designer的版本比较老的原因。有很多点,如果要我吐槽我都不知道从哪里开始吐槽起,不过今天写到这里了,就先来吐槽一下这个布局的使用。

先上文件:

文件部署工具_教师端

文件部署工具_学生端

首先我们知道布局,在Qt里面这个布局是非常好用的一个工具,它可以自适应地给你布置好一些位置的控件,这样就不用你在resizeEvent里面去单独每一次修改窗体的大小或者对应位置,但是代价是什么呢?代价就是你真的不会想用这个做来做Qt的可视化预览界面的,用起来真的非常的一言难尽。

首先我们谈谈Qt的布局我们怎么用。常规的布局是垂直、水平布局,如果你玩的花一点,那么你可能会用到一个叫做栅格布局的,但是不管是一个什么样的布局,只要你不是一步步用布局来布置的,而是直接简单粗暴的对一个充满了空间的widget直接来一波布局,那么这个控件就会直接变形,整个widget可能直接就化为了废墟。布局确实是一个非常好用的东西,但是这个东西在具体使用上真的非常容易失控

那就从一次实战来讲解一下布局工具究竟应该怎么使用,顺便记一下笔记,聊聊这个学生端更新程序部署工具怎么使用

首先来看看工具

没错只有两个裸的exe文件,因为比较轻量化,所以依赖dll被我封装进了exe内部,随取随用就行

打开的话操作逻辑也比较简单,如下:

教师端:

学生端:

如果是从零开始,这个小工具的设计主要有两个:

1.如何做到文件收发,这个是最基本的问题,不管界面上怎么做,至少这个文件收发要能够做到

2.教师端右边的学生是如何动态的插入的,而且可以让用户自定义上面的按钮和功能。

1.浅谈文件收发

关于传输文件的问题,我在上一篇文章中有提到,详情可以看Qt网络编程-书接上文,浅谈TCP文件收发,以及心跳包,这篇文章简单聊了一下如何进行tcp消息的消息传输,也提供了一个tcp传输的类,在这里就进行一个简单的实战,通过小工具来传输文件:

发送端:

首先做好发送端的准备,在教师端选中文件,然后点击发送之后,会做如下动作:

void StudentFileTransfer::on_btn_sendfile_clicked()
{
if (!this->ui.line_file->text().isEmpty()) {
QFile temp_file(this->ui.line_file->text())
if (temp_file.exists()) {
//此时需要缓存文件了,将文件转为二进制码流,同时开始进行传输工作
QByteArray file_array;
QFileInfo file_info(temp_file);
temp_file.open(QIODevice::ReadOnly);
file_array = file_title + "|" + file_info.fileName().toLocal8Bit() + "|" + temp_file.readAll();
//准备发送文件,发送给全体成员,等待消息回复,并重置整个列表
this->s_tcp->SendMsg(file_array, "", 0); this->addMessage("Try SendFile to Client:" + file_info.fileName()); }
}
}

我们可以把文件发送端的行为简单的分为三步:找到文件->拆分文件->发送流

首先读取到指定文件

QFile temp_file(this->ui.line_file->text())

然后读取文件的字节,通过.readAll()方法 (注意,想要readAll需要先open该文件,如果文件并未open,则会提示QIODevice not open)然后将发送的字符串以:NewFile|文件名|文件二进制流的形式发送出去。

这样我们就把发送端做好了,然后来看看接收端

接收端:

接收端的话,稍微复杂一点,但是也不会有多复杂,我们可以来看

void FramelessWidget::RecvTCP(const QByteArray& bytes)
{
//接到发送消息,进行解析
if (bytes.contains(file_title)) {
//如果当前发送的字符串是发送的文件,则此时开始保存文件
//接到这段消息之后将字符串向右移动,将抬头移除
QByteArray temp_qba = bytes;
//将左侧的NewFile|移除,再输入
temp_qba = temp_qba.remove(0, 8);
this->FileReceiver(temp_qba);
}
}

这里我们接到消息后,如果消息是带有我们的新文件接收事件的抬头的,则执行接收文件的方法,也就是FileReceiver,我这里图省事就直接把这个抬头去掉了,当然你不去掉也没什么关系,不影响的。注意这里是const类型的参数,所以不能通过replace和remove该,但是可以新申请一块内存来处理。

void FramelessWidget::FileReceiver(QByteArray strValue)
{ QString file_title;
QString SeatID;
QString file_path;
QDir folder;
qint32 parse_index;
QByteArray title_bytes; //名称信息
QByteArray file_bytes; //文件二进制流 parse_index = strValue.indexOf("|",1); title_bytes = strValue.left(parse_index);
file_title = QString::fromLocal8Bit(title_bytes); file_path = this->file_path +"/"+ file_title; //不管怎么样,只要没占用就得重新写入
QFile received_file(file_path);
file_bytes = strValue.mid(parse_index + 1, -1);
file_bytes = file_bytes + "\0"; received_file.open(QIODevice::Truncate | QIODevice::WriteOnly); if (received_file.isOpen()) {
received_file.write(file_bytes, file_bytes.length());
}
else {
folder.mkpath(this->file_path);
received_file.open(QIODevice::Truncate | QIODevice::WriteOnly);
received_file.write(file_bytes, file_bytes.length());
}
//接收文件结束之后需要通知服务端当前文件已接收,同时需要改变置顶的文字提示
ui.lab_title->setText(QString("文件%1已接受!").arg(file_path));
c_tcp->SendMsg("FILERECIVED");
} 这里我们就把这个二进制流拆分为两部分,第一个|的前半部分是文件名称,后半部分是文件内容,将二者分别取出然后再写成文件,这样就完成了一个文件的接收 以上就是关于在tcp协议中传输文件的收发,除此之外还有几点需要注意的。

以下这部分是在我完成这篇博客之后才发现的。

1.tcp传输文件的效率根据网速而定,而且即使网速能处,实际上tcp的效率也可能非常感人,我的这个tcp协议本身是单线程而且是主线程的,也就是说这个工具在发送文件的时候是阻塞的,所以传输大文件可能会...你懂的。

2.文件传输的时候,如果当前文件被占用了,比如程序正在运行,那么这个时候的文件是没法写入的,当然了tcp可不会管你那么多,同样会显示写入成功了,但文件肯定是没有办法写入的。这个时候可以尝试对指定QFile temp_file(file_path),然后检查这个temp_file 的 isOpen,来试试看这个文件是否可以被写入。当然了,即使不可以写入也做不了什么,不过可以返回当前写入失败的情况给发送端。当然,最好的情况就是直接把该指定程序的占用全部解除...如果能做到这点...当然是可以的,但是非常麻烦,所以考虑到运行的场景,只让运行着的进程结束掉,且循环检测当前这个进程是否存在,如果不在了在进行更换。

3. 1 和 2两个问题结合起来,有的进程有守护,临时关闭一下,那可能还来不及接收到文件,被占用的进程就被调用起来了,这样文件又被占用了,导致文件再次无法写入。所以可以让文件在本地先缓存,文件接收完毕之后,再执行2中的步骤,本地之间的文件交换就快了,不用考虑网络的问题。

以上几点大概就是关于文件传输的一些观察,接下来讲一下自定义Ui控件如何编写和调用。

2.浅谈自定义ui的使用和编写

有很多地方可能我要动态地插入一些控件,比如我现在的这个工具的右边,有一个地方专门用来放一些用户的数据。当然了这个地方的控件肯定没有一个QtDesigner内标准的控件来表示,肯定只能让我自己来定义,但是该怎么做呢?

当前我们写的程序,都是直接在QtDesigner内编写的,但是实际上,QtDesigner的可视化编辑工具编辑出来的文件是什么呢?就是头文件ui_xxx.h,我们可以进去看一下里面是什么内容

看到了吗,也就是说这个可视化的工具编辑出来的结果其实还是一堆堆的文本,走进去看看就知道了,实际上还是代码来编辑的。我们可以看到里面的各种控件,其实还是一个个的QPushButton QLabel等指针和一大堆的setGeometry函数等等,那么我们实际上也可以直接通过代码的方式来编写界面,完全不依赖任何编辑器。

那么聪明的你肯定想到了,既然我们有QPushButton类,QLabel这些Qt自带的类,那么我们是不是可以自己定义一个类,来放我们的自定义控件,然后来操控呢?

答案当然是可以的,这也是接下来我要说的。

以服务端举例,可以看到界面大概是这样的

这个是我们的自定义控件,里面用来存放用户的信息和对应的操作,我们可以先把这个控件画出来。右键工程文件->Add Qt Class->Qt Widget Class,然后把这个界面先画出来。

内容就不展示了,详情可以直接看工程文件,只是说下思路。画完之后,这个类就可以当成一个普通控件来使用了。现在我们来写一个类,可以用以表示每一个连接上服务器的用户,这里我举一个例子:

struct Users {
QString sIp = "";
QString userName = "";
qint32 sPort = 0;
bool fileRecState = false;
userInfo *info; //用户窗口控件指针 QString current_file_name = "";
//一个带有指定用户信息的窗口 void static DeleteNode(QList<Users> user_list,QString sIp) {
for (int i = 0; i < user_list.size(); i++) {
if (user_list[i].sIp == sIp) {
user_list[i].Delete();
user_list.removeAt(i);
}
}
} void Delete() {
QString sIp = "";
QString userName = "";
qint32 sPort = 0;
bool fileRecState = false;
info = nullptr;
} bool isEmpty() {
if (sIp.isEmpty()) {
return true;
}
else {
return false;
}
}
};

构建一个结构体来表示一个用户,当然用类也是可以的,不过我是个C老嗨,对于比较轻量级的结构喜欢用结构体,这个看个人。

ok,现在我们就有了一个表示用户的结构体了,当我们新加入一个用户的时候,就可以申请一个Users的内存结点,然后给这个结点里面赋值,插入一个user_list,来代表我们总的用户的列表。

QList真的蛮好用的,一个很像数组的链表,用起来很方便,我一般用来表示全体用户的结构通常使用QList这种链表来表示,用的多了就知道有些结构怎么设计了。

然后现在就要向右下角的框体来插这些控件来,就和插入QPushButton 一样的,先声明,然后设置这个框体为控件的父控件,然后setGeometry,大概就这么简单,我这里只简单展示一下怎么把user_list内的所有用户的控件插入到右下角这个框体中。

框体我选用的是QScrollArea,因为用户可能有很多,可能需要一个滚轮上下滑动来找用户什么的,当然翻页什么的也可以的。

userInfo是我的自定义控件,user_list是存放所有用户的链表,其中的结点为Users

void StudentFileTransfer::UpdateUserList()
{
try {
//先清空,后添加
QList<userInfo*> temp = ui.scrollAreaWidgetContents->findChildren<userInfo*>(); for (int i = 0; i < temp.size(); i++) {
userInfo* si = static_cast<userInfo*>(temp[i]);
si->setParent(this);
si->hide();
//直接删除掉窗口,然后在添加
} for (int i = 0; i < this->user_list.size(); i++) {
this->ui.scrollAreaWidgetContents->setGeometry(0, 0, this->ui.scrollAreaWidgetContents->width(), 61 * (i+1));
this->user_list[i].info->setParent(this->ui.scrollAreaWidgetContents);
this->user_list[i].info->move(0, 60 * i);
this->user_list[i].info->show();
} this->addMessage("## Update User List!");
this->ui.lab_users->setText(QString("当前在线用户:%1人").arg(user_list.size()));
}
catch (exception& e) {
qDebug() << "UpdateUserList Failed ! :" << e.what();
}
}

因为直接在QScrollArea中清除所有的用户窗口不是很方便,好像只能把这个结点的窗口指针直接清理掉,但是这样的话就会影响到user_list内用户的窗口指针,这里的话让这些窗口指针直接设置总窗口为父窗口,然后再让他们消失就可以了,反正之后还会加回来。

添加的话,就像我说的,先设置scrollArea为父窗口,然后再根据实际情况设置自定义窗口的位置,我这里就是一个自定义控件60p的高,一个个堆叠下来就行。

还有就是每个控件的信号怎么和外部的槽函数connect?其实你在创建的时候connect就行了,发送一个自定义控件内部的信号,触发外部单例的槽函数,执行一些指定的功能,比如新加入一个用户如下:

    Users user_node;
user_node.sIp = clnAddr;
user_node.userName = strValues[1]; //反正新加入的用户不管说什么都不可能已经接到文件了不是吗
user_node.fileRecState = false; userInfo *temp_info = new userInfo();
temp_info->SetName(user_node.userName);
temp_info->SetSip(user_node.sIp);
temp_info->SetState(user_node.fileRecState); user_node.info = temp_info;
user_list.append(user_node); connect(temp_info, SIGNAL(KickUserInfo(QString)), this, SLOT(RemoveUser(QString)));
connect(temp_info, SIGNAL(Retry(QString)), this, SLOT(ReSendingFile(QString)));
this->UpdateUserList();

创建的时候进行一下connect,那么这个控件的信号函数就会发出来给外部的槽,如果你要辨识是哪个自定义控件发出来的信号,那你就需要一个控件内的自定义表示,比如我这里的temp_info传递的参数是对应用户的ip地址,是唯一的而且是可以比对的。

先写这些吧,反正也是简单谈谈使用。

3.可执行文件打包

就是qt写的可执行文件一般都会有一大堆的依赖dll,但是这里有些dll没有找到并不会报错,而是某些功能变残疾,比如qgif.dll,这个是QLabel加载gif图片的关键组件,如果这个组件缺失了会导致所有QLabel上的Gif都无法加载,但是,但是,但是程序并不会报错。这就很操蛋了,因为你永远不可能知道自己所有的dll占用,即使你知道,我的天,那么多dll谁记得过来呢?

qt的开发者也预示到了这个问题,知道一般的开发者难以忍受这样的折磨可能会爆体而亡,所以提供了一个qt依赖补全工具:windeployqt (wind depoly qt,这样好背一点)

在菜单栏内找到工具:

Qt 5.10.1 for Desktop (MinGW 5.3.0 32 bit)

打开进入控制台,cd转到指定路径后,输入windployqt xxx.exe ,在对应文件夹内获取该程序的完整依赖

详情可以见教你使用windeployqt工具来进行Qt的打包发布,我要说的重点不是在如何获取这些依赖,而是如何将依赖打包成一个单独的可执行文件。

就比如我现在这个小工具,我当然不希望别人还要通过依赖来运行我的程序,这样会显得非常冗长,而且用起来也非常麻烦,能打包是最好的

详情见Qt程序打包(使用Enigma Virtual Box)

其他的想到什么说什么吧,class dismiss

一个简单的工具开发:从学生端更新程序部署工具说起,浅谈qt中自定义控件制作和调用、TCP协议下文件的收发 、以及可执行文件的打包的更多相关文章

  1. 利用 Docker 构建一个简单的 java 开发编译环境

    目前 Java 语言的版本很多,除了常用的 Java 8,有一些遗留项目可能使用了 Java 7,也可能有一些比较新的的项目使用了 Java 10 以上的版本.如果想切换自己本地的 Java 开发环境 ...

  2. Spark1.0.0 应用程序部署工具spark-submit

    原文链接:http://blog.csdn.net/book_mmicky/article/details/25714545 随着Spark的应用越来越广泛,对支持多资源管理器应用程序部署工具的需求也 ...

  3. TCP协议下的服务端并发,GIL全局解释器锁,死锁,信号量,event事件,线程q

    TCP协议下的服务端并发,GIL全局解释器锁,死锁,信号量,event事件,线程q 一.TCP协议下的服务端并发 ''' 将不同的功能尽量拆分成不同的函数,拆分出来的功能可以被多个地方使用 TCP服务 ...

  4. 用c++开发基于tcp协议的文件上传功能

    用c++开发基于tcp协议的文件上传功能 2005我正在一家游戏公司做程序员,当时一直在看<Windows网络编程> 这本书,把里面提到的每种IO模型都试了一次,强烈推荐学习网络编程的同学 ...

  5. Netty实现一个简单聊天系统(点对点及服务端推送)

    Netty是一个基于NIO,异步的,事件驱动的网络通信框架.由于使用Java提供 的NIO包中的API开发网络服务器代码量大,复杂,难保证稳定性.netty这类的网络框架应运而生.通过使用netty框 ...

  6. iBatis第二章:搭建一个简单的iBatis开发环境

    使用 iBatis 框架开发的基本步骤如下:1.新建项目(iBatis是持久层框架,可以运用到java工程或者web工程都可以) 这里我们建立一个 web 工程测试. 2.导入相应的框架 jar 包 ...

  7. ABP教程(四)- 开始一个简单的任务管理系统 - 实现UI端的增删改查

    接上一篇:ABP教程(三)- 开始一个简单的任务管理系统 – 后端编码 1.实现UI端的增删改查 1.1添加增删改查代码 打开SimpleTaskSystem.sln解决方案,添加一个“包含视图的MV ...

  8. 浅谈android中只使用一个TextView实现高仿京东,淘宝各种倒计时

    今天给大家带来的是只使用一个TextView实现一个高仿京东.淘宝.唯品会等各种电商APP的活动倒计时.近期公司一直加班也没来得及时间去整理,今天难得歇息想把这个分享给大家.只求共同学习,以及自己兴许 ...

  9. 【Unity游戏开发】浅谈 NGUI 中的 UIRoot、UIPanel、UICamera 组件

    简介 马三最近换到了一家新的公司撸码,新的公司 UI 部分采用的是 NGUI 插件,而之前的公司用的一直是 Unity 自带的 UGUI,因此马三利用业余时间学习了一下 NGUI 插件的使用,并把知识 ...

  10. 重温WCF之构建一个简单的WCF(一)(2)通过Windows Service寄宿服务和WCF中实现操作重载

    参考地址:http://www.cnblogs.com/zhili/p/4039111.html 一.如何在Windows Services中寄宿WCF服务 第一步:创建Windows 服务项目,具体 ...

随机推荐

  1. Docker安装部署Rancher

    # 一.Rancher简介 [Rancher](https://www.cnrancher.com/rancher/)是一个开源的企业级容器管理平台.通过Rancher,企业再也不必自己使用一系列的开 ...

  2. Spring Boot 项目转容器化 K8S 部署实用经验分享

    转载自:https://cloud.tencent.com/developer/article/1477003 我们知道 Kubernetes 是 Google 开源的容器集群管理系统,它构建在目前流 ...

  3. 复现CVE-2022-10270(向日葵远程代码执行漏洞)

    警告 请勿使用本文提到的内容违反法律.本文不提供任何担保. 漏洞描述 向日葵是一款免费的,集远程控制电脑手机.远程桌面连接.远程开机.远程管理.支持内网穿透的一体化远程控制管理工具软件.CNVD披露了 ...

  4. HashMap底层原理及jdk1.8源码解读

    一.前言 写在前面:小编码字收集资料花了一天的时间整理出来,对你有帮助一键三连走一波哈,谢谢啦!! HashMap在我们日常开发中可谓经常遇到,HashMap 源码和底层原理在现在面试中是必问的.所以 ...

  5. 如何在linux下检测(自身)IP冲突

    最近遇到一个需求,或者说是一个用户现场问题. 我们设备先安装,设置dhcp模式获取ip进行联网,后来又安装了其他设备,但该设备是手动设置的静态ip地址,正好与我们设备冲突,造成网络故障. 那我们就需要 ...

  6. C++ 自学笔记 对象的初始化

    数组的初始化: 在 C++中  struct ≈ Class:struct里面可以有函数. 默认构造函数: 没有参数的构造函数就是默认构造函数

  7. HM VNISEdit2.0.3修正版

    HM VNISEdit,曾经是NSIS最强最佳开源免费编辑器/IDE,但2003年至今原作者已经接近20年未再更新,随着NSIS3.X版本的普及,NIS Edit不可避免的出现了大大小小的各种BUG, ...

  8. 痞子衡嵌入式:浅谈i.MXRT10xx系列MCU外接24MHz晶振的作用

    大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家介绍的是i.MXRT10xx系列MCU外接24MHz晶振的作用. 痞子衡之前写过一篇关于时钟引脚的文章 <i.MXRT1xxx系列MCU时 ...

  9. 追求性能极致:Redis6.0的多线程模型

    Redis系列1:深刻理解高性能Redis的本质 Redis系列2:数据持久化提高可用性 Redis系列3:高可用之主从架构 Redis系列4:高可用之Sentinel(哨兵模式) Redis系列5: ...

  10. 微信小程序中视频的显示与隐藏

    在微信小程序中实现视频的播放与暂停 需求: 视频列表中只能有一个视频在播放 点击视频实现播放与暂停功能 加载完成显示图片,点击后变为视频播放 从上次播放的位置进行播放 思路: 定义一个标记变量,控制视 ...