进度条最终效果:
目录
进度条最终效果:
一:两个须知
1:缓冲区
①:C语言自带缓冲区
②:缓冲区的刷新策略
2:回车和换行的区别
二:倒计时程序
三:入门板进度条的实现
四:升级版进度条的实现
1:预留中括号
2:箭头
3:动态百分比
4:旋转光标
五:模拟下载场景
1:对进度条的封装
2:下载程序的编写
3:串联两个函数:
a:调用函数
b:函数回调
六:低速下载场景代码的问题
七:高速下载进度条代码
八:总代码:
①:不上色版本
②:上色版本
一:两个须知
实现进度条之前,我们要先理解两个点
1:缓冲区
例子1:
#include<stdio.h>
#include<unistd.h>int main()
{printf("you can see me!\n");sleep(3);return 0;
}
我们预期是先打印出"you can see me!"然后换行,3秒后再显式提示符:
运行结果:
解释:与我们预期一致
例子2:
#include<stdio.h>
#include<unistd.h>int main()
{printf("you can see me!");sleep(3);return 0;
}
我们预期是先打印出"you can see me!",3秒后再同一行显式提示符
运行结果如下:
解释:与我们预期不符,它是3秒后,把"you can see me!"和提示符在一行打印出来
首先我们能确定的是,打印语句无论是哪一种,一定都是第一句就被执行的了,所以肯定是某个东西影响到了打印语句的效果是先显式到屏幕上,还是后显示到屏幕上罢了,而我们的区别仅仅只有换行'\n',所以缓冲区及其刷新策略如下:
①:C语言自带缓冲区
在C语言中,每个文件都有自己的缓冲区,代码本质也是在一个文件里面编写,所以每个代码,比如上面的test.c就有自己的缓冲区,缓冲区就像一个装快递的车子,装满了才发车,这样效率更高,本质就是用空间换时间!
②:缓冲区的刷新策略
C语言缓冲区的三种刷新策略:
①:无刷新 没有缓冲区
②:行刷新->遇见\n刷新(显示器)
③:全刷新->缓冲区满了才刷新(普通文件)
两种特殊情况:
①:强制刷新(fflush()函数)
②:进程退出的时候(进度条不加\n显示不出来 程序退出显示出来的原因)
刷新要结合普通的刷新策略和特殊情况来判断,现在我们再来理解一下例子1和2的现象原因
例子1:因为我们是对显示器写入,所以其遇见\n才会刷新,这就是为什么在sleep函数之前,就已经把"you can see me!"打印了出来,因为缓冲区满足被刷新的条件
例子2:虽然我们是对显示器写入,但是我们没有\n,所以我们要么使用特殊情况中的fflush函数,或者是进程退出的时候,这就是为什么进程退出的时候,"you can see me!"和提示符一起打印了
建议看看博主的这篇文章:Linux->基础IO-CSDN博客,你会对缓冲区和刷新策略理解更透彻
2:回车和换行的区别
我们平时键盘上的回车,其实是有两个功能,换行+移动到新行首,这就是为什么按一下就能到下一行的开始位置:
所以我们代码中的'\n',就是键盘上的回车(换行+移动到新行首),这就是为什么我们连续的printf包含'\n'会呈现清晰的打印结果
我们的进度条很明显是在同一行不同的打印,所以'\n'是不行的!要用'\r'!
但是呢,\r 需要一些例子来说明其的特性,才能为我们之后的进度条实现打好基础,所以,现在实现一个倒计时程序的代码中体会\r的特性
二:倒计时程序
例子1:
期望是实现一个建议的10秒倒数到1秒的效果
#include<stdio.h>
#include<unistd.h>int main()
{int count = 9;while(count){printf("%d\r",count);count--;sleep(1);}return 0;
}
运行效果:
解释:什么都没打印,最后打印了提示符?!因为我们知道\r 只是把光标移动到同一行的起始为止,所以打印的结果9会被8封盖,8会被7覆盖.....最后提示符会覆盖掉最后一次的1,然后程序结束时,缓冲区刷新,刷新除了提示符!
本质就是我们向显示屏打印,没有换行,只能缓冲区满了才刷新,而这么点内容根本不会满,所以最后程序退出,强制刷新了缓冲区!所以\r一定要配合强制刷新函数fflush使用,才能起到效果!
修改后的代码和效果如下:
#include<stdio.h>
#include<unistd.h>int main()
{int count = 9;while(count){printf("%d\r",count);fflush(stdout);count--;sleep(1);}//printf("\n");return 0;
}
运行效果:
解释:虽然达到了效果,但是最后一次不应该被提示符覆盖,所以我们要在代码循环的外面加上打印\n(将上面代码的注释放开即可),如下效果
例子2:
唯一的区别就是count从10开始倒数
#include<stdio.h>
#include<unistd.h>int main()
{int count = 10;while(count){printf("%d\r",count);fflush(stdout);count--;sleep(1);}printf("\n");return 0;
}
运行结果:
解释:为什么是10到90到80....
因为9去覆盖10,只能覆盖到第一个字符,以此类推,就产生了这种效果!所以我们使用\r要预留空间,也就是设置为%-2d即可!
修改后的代码和效果如下:
#include<stdio.h>
#include<unistd.h>int main()
{int count = 10;while(count){printf("%-2d\r",count);fflush(stdout);count--;sleep(1);}printf("\n");return 0;
}
运行效果:
最后我们实现出了一个倒计时程序,也总结出了使用\r的三条性质:
总结:
1:\r之后一定要紧接fflush
2:最后一次打印完后 要\n 避免提示符覆盖打印内容
3:\r 对%d的预留很重要务必牢记这三点 其会贯彻整个进度条代码
三:入门板进度条的实现
所以我们就可以不断的'\r'的方式去打印一个递增的字符串,不就能够实现进度条了吗?该字符串的字符类型我们可以自己设置,你想要一串*号,一串=号都可以
所以我们需要一个101大小的字符数组,一开始全都是'\0',我们打印一次,就往该数组内部增加一个字符=,即可!
Q:为什么数组是101大小,不是100?
A:因为预留最后那一个位置给'\0',才能安全的进行最后一次的打印!
代码如下:
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#define MAX 101//定义数组的大小
#define S '='//定义进度条的字符类型int main()
{char buffer[MAX];memset(buffer,'\0',sizeof(buffer));//把数组全设置为'\0'int i = 0;while(i<=100){printf("%s\r",bar);fflush(stdout);//强制刷新buffer[i++] = S;usleep(100000);//0.1秒更新数组且打印一次,可以比sleep函数更快} printf("\n");//避免提示符覆盖进度条return 0;
}
效果如下:
解释:=号刚好有100个,符合预期
Q1:为什么是“连续增加的等号”?而不是把上一次z字符数组清空然后再打印新的字符数组?
A1:因为\r 不会清除之前的内容,只是把移动光标到行首,然后进行下一次的字符数组打印,而字符数组一次比一次多,所以就会呈现出连续递增的感觉?
为什么称这个代码是入门级呢,因为其有缺陷:
Q2:数组的最终状态是怎么样的?
A2:整个数组全是(buffer[0]
到 [100]
)全是 '='(
共 101 个),我们给最后一个位置预留存放\0,根本没有存放,因为当i为99的时候,此时进来打印后,buffer[99]='=',然后i变成了100,再次进入循环打印数组,此时的数组[0]~[99]都是'=',共100个,所以上面GIF图是100个,此时打印后,buffer[100] =‘=',也就是第101个元素也被变成了'=',此时i++为101,跳出循环!
验证:在最外面再打印一次数组:
符合我们的分析,数组的确是有101个'='
Q3:既然 bar 数组的所有元素(bar[0]~bar[100])都被赋值为 '=',没有 '\0',为什么最外面 printf("%s", bar) 仍然能正确停止,并且每次都能打印 101 个 '='?按照 C 字符串的规则,printf 应该一直往后读,直到随机遇到 '\0' 才停止才对啊?
A3:这在VS下运行的确是随机的,但是在Linux下就不是了,因为涉及到了Linux的内存布局,可能是bar
的相邻内存可能是 '\0'等原因,不必在意
所以入门级别的代码,为了安全,应该一开始设置MAX为102才对:
如下:
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#define MAX 102//定义数组的大小
#define S '='//定义进度条的字符类型int main()
{char buffer[MAX];memset(buffer,'\0',sizeof(buffer));//把数组全设置为'\0'int i = 0;while(i<=100){printf("%s\r",bar);fflush(stdout);//强制刷新buffer[i++] = S;usleep(100000);//0.1秒更新数组且打印一次,可以比sleep函数更快} printf("\n");//避免提示符覆盖进度条return 0;
}
如果你非要保持101 ,修改一下while的条件和内部执行顺序也行:
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#define MAX 101
#define type '='int main()
{char bar[MAX];memset(bar,'\0',sizeof(bar));int i = 0;while(i<=99){bar[i++] = type;printf("%s\r",bar);fflush(stdout);usleep(100000);} printf("\n");printf("%s\n",bar);return 0;
}
注:博主在下面的实现中 会全程使用这个类型的代码 !
最后的这两种写法,都可以让安全得打印100个'='
四:升级版进度条的实现
有入门进度条的效果,我们能看出缺了一些细节:
①:预留中括号
②:箭头的设置
③:动态百分比
④:旋转光标
1:预留中括号
也就是说,进度条应该是被一个中括号括起来的,而且中括号,一开始就再最开始和最末尾:
代码如下:
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#define MAX 101
#define type '-'int main()
{char buffer[MAX];memset(buffer,'\0',sizeof(buffer));int i = 0;while(i<=99){buffer[i++] = type;printf("[%-100s]\r",buffer);//预留中括号 %-100s的形式fflush(stdout);usleep(100000);} printf("\n");return 0;
}
运行效果:
2:箭头
也就是说,不要只是一条线,在线的最前面加一个箭头,更有指向性一点,所以我们就要在每次让i下标对应的元素变成'-'的同时,还要让i++对应的元素变成'>',代码如下:
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#define MAX 101
#define body '-'
#define head '>'int main()
{char buffer[MAX];memset(buffer,'\0',sizeof(buffer));int i = 0;buffer[0]=head;while(i<=99){buffer[i++] = body; buffer[i] = head;printf("[%-100s]\r",buffer);fflush(stdout);usleep(100000);} printf("\n");return 0;
}
效果如下:
解释:乍一看好像没什么问题,但是其实如果你数一下,会发现'-'和'>'的数目为101,这意味着我们只有101个空间,却全被使用了,没有给'\0'预留,无法保证被安全得打印;且进度条加载满了之后,箭头应该消失才对,综合这两点来看,我们不用改变数组的大小,而是让i=99的时候呢,我们让下标99对应的第100个元素'>'变成-即可(buffer[i++] = body; ),而不在进行i++后,让下标100对应的第101个元素再变成>"(buffer[i] = head;),所以我们在赋值'>'处加一个判断即可如下:
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#define MAX 101
#define body '-'
#define head '>'int main()
{char buffer[MAX];memset(buffer,'\0',sizeof(buffer));int i = 0;buffer[0]=head;while(i<=99){buffer[i++] = body;if(i < 99) buffer[i] = head;printf("[%-100s]\r",buffer);fflush(stdout);usleep(100000);} printf("\n");return 0;
}
运行效果:
解释:和最上面的原来效果相比,不仅的确变短了1个字符,总数为100,其次最后加载满的时候,箭头也消失了
3:动态百分比
用一个动态百分比来反映进度条的进展,这是进度条必不可少的一部分,但是非常简单,只需要把i打印出来不就可以了吗?代码如下:
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#define MAX 101
#define body '-'
#define head '>'int main()
{char buffer[MAX];memset(buffer,'\0',sizeof(buffer));int i = 0;buffer[0]=head;while(i<=99){buffer[i++] = body;if(i < 99) buffer[i] = head;printf("[%-100s][%d%%]\r",buffer,i);fflush(stdout);usleep(100000);} printf("\n");return 0;
}
运行效果:
Q1:i不是最大只有99吗?为什么最后打印出来能到100%?
A1:因为i进入循环,对i下标赋值body后会后置++的
Q2:[%d%%]是什么意思?
A2:在格式化字符串中,%
是特殊符号(如 %d
、%f
),如果想直接输出 %
,必须用 %%
转义。
4:旋转光标
日常中的进度条的旁边都有一个圆圈一直再转,或者一个竖线在顺时针旋转,这样即显得不那么单调,当我们进度条由于网速等原因卡住的时候,正在旋转的旋转光标也会告诉我们进度条只是由于网速被卡住了,而不是程序卡了
所以我选择使用 |
, /
, -
, \
四个符号循环,模拟顺时针旋转,实现旋转光标,代码如下:
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#define MAX 101
const char *rotate="|/-\\";//旋转光标数组
#define body '-'
#define head '>'int main()
{char buffer[MAX];memset(buffer,'\0',sizeof(buffer));int n = strlen(rotate);//旋转光标数组的个数n 用来后面打印的时候取模int i = 0;buffer[0]=head;while(i<=99){buffer[i++] = body;if(i < 99) buffer[i] = head;printf("[%-100s][%d%%][%c]\r",buffer,i,rotate[i%n]);fflush(stdout);usleep(100000);} printf("\n");return 0;
}
运行效果:
解释:打印的时候,取旋转光标数组的下标,正好可以用一直改变的i来模上旋转光标数组的元素个数n,这样就能一直取到[0,3] ;注意,"|-/\\"中的\\依旧是转义!
其他光标推荐:
用"..oo00OO00oo.."
效果如下:
所以,升级版进度条的最终代码如下:
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#define MAX 101
const char *rotate="..oo00OO00oo..";
#define body '-'
#define head '>'int main()
{char buffer[MAX];memset(buffer,'\0',sizeof(buffer));int n = strlen(rotate);//旋转光标数组的个数n 用来后面打印的时候取模int i = 0;buffer[0]=head;while(i<=99){buffer[i++] = body;if(i < 99) buffer[i] = head;printf("[%-100s][%d%%][%c]\r",buffer,i,rotate[i%n]);fflush(stdout);usleep(100000);} printf("\n");return 0;
}
五:模拟下载场景
但是,进度条怎么可能是一个独立的程序,其往往是用来反映其他东西的,比如下载的进度?所以,下面手动得模拟一个反映下载进度的场景:
我们用1024*1024*1024 也就是1GB来表示我们需要下载的总量,然后我们每次(间隔时间自己设定)下载一个随机数,其范围在0kb ~ 1mb之间,然后当总量被减到零的时候,我们就下载完成!
1:对进度条的封装
所以我们要对我们之前写的代码进行管理一下,我们把之前的程序封装成一个函数,这样我们再写一个下载函数,然后就可以在main中完成函数的调用了
封装如下:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#define MAX 101
const char *rotate = "..oo00OO00oo..";
#define body '-'
#define head '>'void processbar()
{char buffer[MAX];memset(buffer, '\0', sizeof(buffer));int n = strlen(rotate); // 旋转光标数组的个数n 用来后面打印的时候取模int i = 0;buffer[0] = head;while (i <= 99){buffer[i++] = body;if (i < 99)buffer[i] = head;printf("[%-100s][%d%%][%c]\r", buffer, i, rotate[i % n]);fflush(stdout);usleep(100000);}printf("\n");
}int main()
{rocessbar();return 0;
}
2:下载程序的编写
首先我们定义一个下载的总量为1个G:
#define FILESIZE 1024*1024*1024
以及下载函数的框架:
void download()
{}
①:种随机数种子
下载的速率不是固定的,所以用随机数来代替正好可以模拟:
void download()
{srand(time(NULL)*11);//乘11只是为了让结果更随机int one = rand()%(1024*1024);//让每次下载的速度在0kb~1mb之间
}
②:让每次下载循环起来,得到余量
//模拟一种场景
void download()
{srand(time(NULL)*11);int total = FILESIZE;//需要下载的总大小while(total){usleep(5000); //下载动作int one = rand()%(1024*1024);//一次下载的大小total -= one;//剩余的大小if(total < 0) total = 0;//不一定刚好减到0 所以当其<0 直接让其为0}
}
解释:每次下载的大小one,用total减等one,total就是动态的余量,但是余量有可能最后一次减到负数,所以当其是负数的时候,我们将其赋值0,方便退出循环
③:得到下载的进度
下载的进度,不就是已经下载的大小比上总大小吗;而为什么要求下载的进度呢?因为我们要将这个数传给进度条程序,让其能够更具速度打印进度条,所以必然的我们在后面还要对我们的进度条函数进行改造的,下载进度rate如下:
void download()
{srand(time(NULL)*11);int total = FILESIZE;while(total){usleep(5000); //下载动作int one = rand()%(1024*1024);total -= one;if(total < 0) total = 0;// 当前的进度是多少?int download = FILESIZE - total;double rate = (download*1.0/(FILESIZE))*100; // 0 23.4 35.6, 56.6printf("download: %f\n", rate);}
}
解释:
double rate = (download*1.0/(FILESIZE))*100;
我们的速度肯定是要double类型才真实一点,所以两个整数相除,怎么获得浮点数呢?download*1.0/FILESIZE即可,但是为什么要乘100呢?因为不乘就是0~1之间的double类型,而我我们平时生活中下载都是56.7%这种0~100之间的double类型,所以我们给其乘100
下面我们运行一下这个下载函数:
重点:要明白的是,我们的下载跨度最多0.1%,因为最大下载1mb,而总大小1个G,所以你单次下载最多也才占到总大小的%0.1,为什么我要这么写,这意味着,我们的processbar函数就可以无脑的套用之前的部分代码,比如无脑的将buffer数组进行递增写入字符,因为如果前一次是1.5 后一次是1.6,其实数组根本不用变,因为两次作为下标取整之后都是1,只有当其加到二点几的时候,才会取整得到2,才能改变数组,才会影响进度条的效果!所以即使下载速录在扩大10被,也就波动到1%,依旧可以复用前面的代码!
这样写的好处是,复用前面的代码,坏处是,生活中的下载可能跨度远远大于了1%,所以在后面我们还要对总代码升级一次,让其可以应对跨度大的下载场景!
至此我们的下载函数就写完了,下一步就是把下载函数和进度条函数关联起来,有两种常见方法:
3:串联两个函数:
a:调用函数
所以我们的processbar函数要改造一下,参数要接受每次循环产生的rate:
改造如下:
#define MAX 101
char buffer[MAX] = {'\0'};
const char *rotate = "|-/\\";
#define body '-'
#define head '>'
int count = 0;void processbar(double rate)
{int n = strlen(rotate); // 旋转光标数组的个数n 用来后面打印的时候取模// 该句不能放在全局 因为其要在编译时运行if (rate <= 1.0) buffer[0] = head; // 第一次下载在1%以内 第一个符号就是箭头printf("[%-100s][%.1f%%][%c]\r", buffer, rate, rotate[(count++) % n]);fflush(stdout);buffer[(int)(rate)] = body;if (rate < 99)buffer[(int)rate + 1] = head;if (rate >= 100.0)printf("\n");
}
解释:我们的 processbar函数 中肯定不会自己循环了,而是当下载函数传过来一个参数,则其在打印对饮的进度条,而我们不用担心两次传过来的数据,波动较大,因为我们在上文说过,你传过来的两个double类型差值不会超过1,这就意味着,大部分代码不用改变,依旧是递增式的往buffer数组里面插入数据,但有几个点需要说明一下:
①:
rotate[(count++) % n]
我们的旋转光标的下标,不能再用i 了,因为没i了,所以我们需要自建一个全局变量count,来作为其下标,其要++,才能让光标动起来,当然你也可以用static修饰count写进函数内,一样的
②:
if (rate < 99)buffer[(int)rate + 1] = head;
因为作为数组buffer的下标需要整形,所以我们进行强转为整形后再作为下标,而至于+1,是因为我们要把下一个字符变成箭头'>';而当其取整后为99的时候,代表其在上行代码已经将下标为99也就是对应的第100个元素变成'-'了,所以我们的箭头不要了,即美观又安全
③:
if (rate >= 100.0)printf("\n");
最后下载完成后,我们要打印一次换行,避免被提示符覆盖
所以总代码如下:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <time.h>
#include <stdlib.h>
#include <unistd.h>#define MAX 101
#define body '-'
#define head '>'
#define FILESIZE 1024 * 1024 * 1024char buffer[MAX] = {'\0'};
const char *rotate = "|-/\\";
int count = 0;void processbar(double rate)
{int n = strlen(rotate); // 旋转光标数组的个数n 用来后面打印的时候取模// 该句不能放在全局 因为其要在编译时运行if (rate <= 1.0)buffer[0] = head; // 第一次下载在1%以内 第一个符号就是箭头printf("[%-100s][%.1f%%][%c]\r", buffer, rate, rotate[(count++) % n]);fflush(stdout);buffer[(int)(rate)] = body;if (rate < 99.0)buffer[(int)rate + 1] = head;if (rate >= 100.0)printf("\n");
}// 模拟一种场景
void download()
{srand(time(NULL) ^ 1023);int total = FILESIZE;while (total){usleep(10000); // 下载动作int one = rand() % (1024 * 1024);total -= one;if (total < 0)total = 0;// 获取当前下载比率(已经下载大小/总大小)int download = FILESIZE - total;double rate = (download * 1.0 / (FILESIZE)) * 100;processbar(rate);}
}int main()
{download();return 0;
}
运行效果:
b:函数回调
我们定义一个processbar函数指针的类型,然后让download函数参数使用这个类型,就可以在download函数内部使用了,其实感觉没什么必要哈哈哈,但还是讲一下吧,代码如下:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <time.h>
#include <stdlib.h>
#include <unistd.h>#define MAX 101
#define body '-'
#define head '>'
#define FILESIZE 1024 * 1024 * 1024char buffer[MAX] = {'\0'};
const char *rotate = "|-/\\";
int count = 0;void processbar(double rate)
{int n = strlen(rotate); // 旋转光标数组的个数n 用来后面打印的时候取模// 该句不能放在全局 因为其要在编译时运行if (rate <= 1.0)buffer[0] = head; // 第一次下载在1%以内 第一个符号就是箭头printf("[%-100s][%.1f%%][%c]\r", buffer, rate, rotate[(count++) % n]);fflush(stdout);buffer[(int)(rate)] = body;if (rate < 99.0)buffer[(int)rate + 1] = head;if (rate >= 100.0)printf("\n");
}typedef void((*pro_bar)(double));
// 模拟一种场景
void download(pro_bar pb)
{srand(time(NULL) ^ 1023);int total = FILESIZE;while (total){usleep(10000); // 下载动作int one = rand() % (1024 * 1024);total -= one;if (total < 0)total = 0;// 获取当前下载比率(已经下载大小/总大小)int download = FILESIZE - total;double rate = (download * 1.0 / (FILESIZE)) * 100;pb(rate);}
}int main()
{download(processbar);return 0;
}
六:低速下载场景代码的问题
六作为拓展讲解,想要了解可以看一下
上面的这个代码代码用进度条反映了一个伪下载的场景,但是其实不完美的!因为其对下载较快的场景会出错!
例子:将download中的单次one的下载速度上限提高至原来的15倍,此时单次最高占到了1.5%,也就是意味着如果你上次是0.9,下次正好单次下载最快,达到了2.4,也就是第一个的取整下标是0,第二次的取整下标是2,这会导致下标1对应的元素没被赋值为'-',所以我们觉得其应该是保留了原本的'\0',这会导致,哪里第一次被跳过了没被赋值'-',其就应该停留在那个地方,那真的是这样吗?
首先我们先单独使用download函数来验证高速率下传递给processbar函数的(int)rate是否会跳跃:
所以我们把单词下载one的速率提高20倍,然后打印所有的(int)rate查看是否会跳跃:
void download(pro_bar pb)
{srand(time(NULL) ^ 1023);int total = FILESIZE;while (total){usleep(10000); // 下载动作int one = rand() % (1024 * 1024*15);total -= one;if (total < 0)total = 0;// 获取当前下载比率(已经下载大小/总大小)int download = FILESIZE - total;double rate = (download * 1.0 / (FILESIZE)) * 100;printf("%d ",(int)rate);//pb(rate);}
}
打印结果:
解释:随便就能找到两个下标被跳过了,所以按照我们的预想,4下标仍然为'\0',所以往后的每次打印遇到'\0'都会停止,也就是你的光标虽然在旋转,百分比虽然在增加,但是你每次\r打印的应该都一样!也就是我认为 buffer[4]
应保留初始值 \0
(即空白),导致进度条在 4%
位置中断。
但是效果如下:
原因如下:
这与 VS Code 终端的渲染机制 和 字符串输出的特性,不作深究,所以我们的想法是对的,但是受到了终端的影响,所以看起来的结果很奇怪
所以我们可以把buffer数组全部设置为空格字符' ',然后最后一次设置为'\0'(为了安全打印),这样我们就可以看到断裂的进度条了:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <time.h>
#include <stdlib.h>
#include <unistd.h>#define MAX 101
#define body '-'
#define head '>'
#define FILESIZE 1024 * 1024 * 1024char buffer[MAX];
const char *rotate = "|-/\\";
int count = 0;void processbar(double rate)
{int n = strlen(rotate);if (rate <= 1.0)buffer[0] = head;printf("[%-100s][%.1f%%][%c]\r", buffer, rate, rotate[(count++) % n]);fflush(stdout);buffer[(int)(rate)] = body;if (rate < 99.0)buffer[(int)rate + 1] = head;if (rate >= 100.0)printf("\n");
}typedef void((*pro_bar)(double));
// 模拟一种场景
void download(pro_bar pb)
{// srand(time(NULL) ^ 1023);int total = FILESIZE;while (total){usleep(100000); // 下载动作int one = rand() % (1024 * 1024 * 50);//50倍更好观察结果total -= one;if (total < 0)total = 0;// 获取当前下载比率(已经下载大小/总大小)int download = FILESIZE - total;double rate = (download * 1.0 / (FILESIZE)) * 100;pb(rate);}
}int main()
{memset(buffer, ' ', sizeof(buffer));//buffer数组全部设置为空格字符' 'buffer[101] = '\0';//最后一个设置为'\0' 安全打印download(processbar);return 0;
}
Q:进度条断裂我理解了,但是为什么会有这么多的箭头,而不是"---- ---- ---- ---->"这样?
A:
由图可知就能知道为什么箭头这么多,本质就是跳跃会导致覆盖箭头失败!
七:高速下载进度条代码
其实非常简单,我们在processbar函数中接收到一个下标,我们就把数组一开始到这个下标的内容全部置为'-',然后再把下标对应的内容置为'>'即可,其实一开始就可以讲这个,为什么不讲呢,本质是通过低级的代码引出更多的问题,不然我们不懂为什么不断裂,不知道可以用空格字符来展现断裂,甚至有的人都不知道低速代码的弊端,所以,讲下还是有必要的!
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <time.h>
#include <stdlib.h>#define MAX 101
char buffer[MAX] = {'\0'};
const char *rotate = "|-/\\";
#define body '-'
#define head '>'
int count = 0;void processbar(double rate) {int n = strlen(rotate);memset(buffer, body, (int)rate); // 直接填充'-'到当前下标buffer[(int)rate] = head; // 再把头部换成箭头printf("[%-100s][%.1f%%][%c]\r", buffer, rate, rotate[(count++) % n]);fflush(stdout);if (rate >= 100) printf("\n");
}#define FILESIZE (1024 * 1024 * 1024)void download() {srand(time(NULL) ^ 1023);int total = FILESIZE;while (total) {usleep(10000);int one = rand() % (50 * 1024 * 1024); // 扩大随机范围(最大5MB/次,却依旧不会断裂)total -= one;if (total < 0) total = 0;double rate = ((FILESIZE - total) * 100.0) / FILESIZE;processbar(rate);}
}int main() {download();return 0;
}
50倍:
15倍:
注:高速代码可完全替代低速代码,而不是仅仅只可用于高速下载!
八:总代码:
我们对高速下载代码,上个色吧~
①:不上色版本
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <time.h>
#include <stdlib.h>#define MAX 101
char buffer[MAX] = {'\0'};
const char *rotate = "|-/\\";
#define body '-'
#define head '>'
int count = 0;void processbar(double rate)
{int n = strlen(rotate);memset(buffer, body, (int)rate); // 直接填充'-'到当前下标buffer[(int)rate] = head; // 再把头部换成箭头printf("[%-100s][%.1f%%][%c]\r", buffer, rate, rotate[(count++) % n]);fflush(stdout);if (rate >= 100)printf("\n");
}#define FILESIZE (1024 * 1024 * 1024)void download()
{srand(time(NULL) ^ 1023);int total = FILESIZE;while (total){usleep(10000);int one = rand() % (50 * 1024 * 1024); // 扩大随机范围(最大5MB/次,却依旧不会断裂)total -= one;if (total < 0)total = 0;double rate = ((FILESIZE - total) * 100.0) / FILESIZE;processbar(rate);}
}int main()
{download();return 0;
}
②:上色版本
让所有元素(进度条 百分比 光标)都能根据进度改变颜色:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <time.h>
#include <stdlib.h>#define MAX 101
char buffer[MAX] = {'\0'};
const char *rotate = "|-/\\";
#define body '-'
#define head '>'
int count = 0;// ANSI 颜色代码
#define COLOR_RED "\033[31m"
#define COLOR_YELLOW "\033[33m"
#define COLOR_GREEN "\033[32m"
#define COLOR_CYAN "\033[36m"
#define COLOR_RESET "\033[0m"void processbar(double rate) {int n = strlen(rotate);memset(buffer, body, (int)rate);buffer[(int)rate] = head;// 根据进度选择颜色(红→黄→绿)const char *color;if (rate < 30) color = COLOR_RED;else if (rate < 70) color = COLOR_YELLOW;else color = COLOR_GREEN;// 旋转光标单独用青色const char *spin_color = COLOR_CYAN;// 打印带颜色的所有元素printf("[%s%-100s%s][%s%.1f%%%s][%s%c%s]\r", color, buffer, COLOR_RESET, // 进度条部分color, rate, COLOR_RESET, // 百分比部分spin_color, rotate[(count++) % n], COLOR_RESET); // 旋转光标fflush(stdout);if (rate >= 100) printf("\n");
}#define FILESIZE (1024 * 1024 * 1024)void download() {srand(time(NULL) ^ 1023);int total = FILESIZE;while (total) {usleep(10000);int one = rand() % (10 * 1024 * 1024); // 单次最多下载10MBtotal -= one;if (total < 0) total = 0;double rate = ((FILESIZE - total) * 100.0) / FILESIZE;processbar(rate);}
}int main() {download();return 0;
}
效果:
上色原理:
通过插入ANSI转义码(如\033[31m
表示红色)临时修改终端文本颜色。代码中动态选择颜色后,用\033[0m
重置样式,使进度条、百分比和光标按进度变色(如红→黄→绿),所有颜色变化均由终端实时解析实现。