独夜无伴守灯下,清风对面吹.............................................................................................
文章目录
前言
一、【行缓冲区的引入】
1、【问题提出】
2、【\r和\n】
3、【简易倒计时程序】
二、【简易进度条的实现】
process_bar.h代码:
process_bar.c代码:
主函数main.c 代码:
效果图:
颜色设置:
三、【不一样的进度条】
1、【进度条动态增长的本质】
2、【背景色实现动态增长】
process_bar.hpp代码:
process_bar.c代码:
主函数main.c 代码:
效果图:
总结
前言
本篇博客介绍了,如何在Linux下运行一个进度条小程序,以及该如何对一个进度条小程序进行美化,还包括行缓冲区的概念,以及 ” \r “ 的用法,请耐心观看!
一、【行缓冲区的引入】
1、【问题提出】
首先,我们来感受一下行缓冲区的存在,在Linux当中以下代码的运行结果是什么样的?
#include<stdio.h> #include<unistd.h> int main() {printf("this is a test file...\n");sleep(3);return 0; }
对于此代码,大家应该都没问题,当然是先输出对应字符串然后休眠3秒之后结束运行。
那么对于以下代码呢?
#include<stdio.h> #include<unistd.h> int main() {printf("this is a test file...");sleep(3);return 0; }
可以看到代码中仅仅删除了字符串后面的’\n’,那么代码的运行结果还与之前相同吗?
答案否定的,该代码的运行结果是:
先休眠3秒,然后打印字符串hello world之后结束运行。该现象就证明了行缓冲区的存在。
原因如下:
显示器对应的是行刷新,即当缓冲区当中遇到’\n’或是缓冲区被写满才会被打印出来,而在第二份代码当中并没有’\n’,所以字符串先被写到缓冲区当中去了,然后休眠3秒后,直到程序运行结束时才将字符串打印到显示器当中。
这里我们可能会有点懵,不知道缓冲区是什么,目前我们仅需知道缓冲区实际上就是一块内存空间,对于上面的字符串,就是被保存在这样的一块内存空间里,当程序运行结束时,会自动的刷新缓冲区里的内容到显示器上,所以无‘\n’才能看到上述的现象。
也就是说我们需要刷新缓冲区,才能将内容进行显示,那么我们该如何刷新缓冲区呢?
一般有下面3种方法:
①加上‘\n’,立即刷新
②等待缓冲区满或者程序结束,自动刷新
③强制刷新
实际上我们需要进行强制刷新,而我们强制刷新的方式就是使用C语言提供的一个的函数———fflush,这个函数就是用于强制刷新缓冲区的。
这里的文件流后面的文章有详细讲解,现在我们只需要知道程序在运行时默认会打开三种流:
- 标准输入,stdin
- 标准输出,stdout
- 标准错误,stderr
我们的显示器就是标准输出文件,Linux下一切皆文件!所以可以采用该函数强制刷新到对应的文件流上。
我们现在强制刷新看一下:
#include<stdio.h> #include<unistd.h> int main() {printf("this is a test file...");fflush(stdout);sleep(3);return 0; }
可以看到,即使程序没有‘\n’,也能直接显示字符串内容!
2、【\r和\n】
\r: 回车,使光标回到本行行首。
\n: 换行,使光标下移一格回车和换行对于我们而言就是一个简单的动作,可是对于机器来说这是两个独立的动作。
而我们键盘上的Enter键实际上就等价于\n + \r :
既然是\r是使光标回到本行行首,那么如果我们向显示器上写了一个数之后再让光标回到本行行首,然后再写一个数,不就相当于将前面一个数字覆盖了吗?
但这里有一个问题:不使用’\n’进行换行怎么将缓冲区当中数据打印出来?
我们就可以使用前面介绍的fflush函数,该函数可以刷新缓冲区,即将缓冲区当中的数据刷新当显示器当中。对此我们可以编写一个倒计时的程序。
3、【简易倒计时程序】
原理:实际就是对同一个位置进行覆盖,进而实现一个动态的效果。
每当显示一个数字之后就将光标挪动至最前面(回车),休眠,然后再显示下一个数字。回车配休眠需要强制刷新缓冲区。
#include<stdio.h> #include<unistd.h> int main() {int cnt = 10;while (cnt >= 0){printf("倒计时:%2d\r", cnt);fflush(stdout);//强制刷新到屏幕sleep(1);cnt--;}return 0; }
注意:为什么用%2d,是因为数字显示在屏幕上时,实际上是字符串的形式,相当于10是两个字符的字符串,需要覆盖两个字符。当然,大家感兴趣把2去掉看看!
知道了\r这个概念我们就可以实现一个简单的进度条了。
二、【简易进度条的实现】
这里我们使用Makefile来实现,可以方便我们管理文件。
进度条不可能单独存在,一般常见的场景就是下载这一场景,会根据网络带宽,文件大小等其他的要素来决定下载进度,这里我们不清楚具体的下载逻辑,因此我们仅仅是模拟一下下载过程。
代码:
process_bar.h代码:
#pragma once #include<stdio.h> #include <unistd.h> #include <string.h> #define SIZE 101 //定义数组大小为101,反映数组的长度 #define MAX_RATE 100//最大为百分比为100 #define STYLE '='//进度条填充物 #define STYLE_HEADER '>'//进度条尾部填充物 #define STIME 100000//休眠时间 #define TARGET_SIZE 1024*1024 // 1MB #define DSIZE 1024*10//单次下载速度 typedef void (*callback_t)(double);//函数指针,方便实现回调函数 void process_bar(double);//函数声明
process_bar.c代码:
#include "process_bar.h" const char* str = "|/-\\";//旋转光标的数组 void process_bar(double rate) {static char bar[SIZE] = { 0 };static int cnt = 0;//旋转光标数组下标,用来变化旋转光标int num = strlen(str);//旋转光标长度if (rate <= MAX_RATE && rate >= 0)//判断比率是否合法{cnt++;cnt = (cnt >= num ? 0 : cnt); //cnt %= num;//防止越界//[\033[30;47m\033[0m]是为其设置颜色,30是字体颜色,47是背景颜色 printf("[\033[30;47m % -100s\033[0m][% .1f % %][% c]\r",bar, rate, str[cnt]);fflush(stdout);if (rate < MAX_RATE){bar[(int)rate] = STYLE; //'='bar[(int)rate + 1] = STYLE_HEADER; //'>'}else{bar[(int)rate] = STYLE;}} }
代码解释:
①进度条实际上是放在一个数组的中,所以定义一个长度为101的字符数组。数组按字节全部初始化为'\0'!
②因为要带来动态的效果所以运用上面提到的倒计时程序类似。回车('\r')配延时,强制刷新缓冲区
③%-100s:表示左对齐
④%.1f%%:最后的双百分号,是为了取字面值%,以实现100%的效果。因为单独一个%在C语言中有着特殊含义,并且一般进度条都带小数,所以采用double。
⑤const char* str:字符串,同样最后的”\\“,也是为了取字面值\,防止转义,单独的\在C语言有着特殊含义。
主函数main.c 代码:
#include "process_bar.h" void download(callback_t cb) {int testcnt = 100;int target = TARGET_SIZE;int total = 0;while (total <= target){usleep(STIME); // 用简单的休眠时间,模拟本轮下载花费的时间total += DSIZE;double rate = total * 100.0/ target;//模拟卡顿/* if (rate > 50.0 && testcnt) {total = target / 2;testcnt--;}*/cb(rate); // 回调函数}cb(MAX_RATE); // 回调函数printf("\n"); }int main() {download(process_bar);//模拟下载函数return 0; }
注意:这里声明了一个callback_t函数指针类型,目的就是实现函数回调。因为未来可能还有更多版本的进度条功能代码, 采用函数指针的方式,只需要传入对应的函数名,就会去调用对应的功能代码。
效果图:
颜色设置:
printf("\033[0m \033[属性代码1;属性代码2;属性代码3...m字符串,\033[0m",占位符填充变量);
三、【不一样的进度条】
当我们向上面那样实现进度条会发现与我们日常见到的进度条不太一样,我们日常见到的大多是,类似于一整个填充满的矩形块,而不是上面那样用字符等号,箭头来填充。一般像下面这样:
我们来实现一个类似于这种矩形的进度条,由于能力有限,还是实现一个简易的没有图片中这么炫酷。
1、【进度条动态增长的本质】
我们先来分析一下,我们上面版本的进度条是如何实现进度条的动态增长的,我们先来看单纯打印进度条的代码:
void process_bar(int rate) {int rate = 0;char crr[101];memset(crr, '\0', sizeof(crr));const char* str = "\\|/—";//char str[4] = {'\\','|','/','--'};while (rate <= 100){printf("[%-100s][%d%% %c]\r", crr, rate, str[rate % 4]);crr[rate] = '=';Sleep(50);rate++;}printf("\n"); }
我们主要打印的是crr数组里所有的内容,并且随着rate的自增,渐渐的crr数组会被‘ = ’,填满,由于我们每次在打印之前都会使用/r来清除上次打印留下的结果,所以随着crr中的 ‘ = ’随着rate的增长越来越多,就实现了动态增长的效果。
那么是不是意味着,我们可以通过将字符等号替换为某个矩形块来实现我们的目的呢?
比如下面的这些矩形块:
static const char* ponit[] = { "\x20\x20", "\xA8\x87", "\xA8\x86", "\xA8\x84", "\xA8\x83", "\xA8\x80" }; // ▏▎▍▊█
用这些字符实际上是不能达到目的的,本质原因是因为它们不是字符而是字符串,我们是通过·%s来打印crr这个字符数组的所有内容,本质上crr是一个会自动变化的字符串,它其中的 ‘ = ’字符越来越多,而%s只有遇到 ‘ \0 ’才会停止打印,而又因为那些矩形块本质上都是字符串,所以无论crr里存放再多的字符串,%s也只会打印一个,因为字符串是以 ‘ \0 ’结尾的。
所以我们应该重新实现一个打印的逻辑。这里还是采用简单的办法。
2、【背景色实现动态增长】
背景色是打印对象的背景,也就是说只要打印对象是自增的,那么背景也会跟着自增,但是要注意,要将背景只对应填充物,这就意味着我们不能在使用100那个占位符(”%-100s“)了,如果提前占100个位置,背景也会一直是100个长度,恒定不变,所以我们不进行占位,至于填充物选择空格就好。
那么我们就可以像下面这样书写(vs2019下运行):
process_bar.hpp代码:
#pragma once #include <stdio.h> #include <string.h> #include <windows.h> #define Length 101 //进度条长度 #define Style ' ' //进度条样式 typedef void(*Call_back)(double, double);//定义函数指针类型,为了方便回调 void processbar(double total, double current);
注意:这里声明了一个Call_back函数指针类型,目的就是实现函数回调。因为未来可能还有更多版本的进度条功能代码, 采用函数指针的方式,只需要传入对应的函数名,就会去调用对应的功能代码。
process_bar.c代码:
#include "process_bar.hpp" const char* lab = "|/-\\";//加载旋转标志 void processbar(double total, double current) {char bar[Length];memset(bar, '\0', sizeof(bar));//初始化为\0int len = strlen(lab);int cnt = 0;double rate = (current * 100.0) / total;//计算比率int load_top = (int)rate;while (cnt < load_top){bar[cnt++] = Style;//字符追加至数组中}printf("\033[0m \033[42;1m[%c]%s%.1lf%% \r\033[0m", lab[cnt % len], bar, rate);fflush(stdout); //强制刷新 }
解释一下:
①因为主函数的下载功能是一个循环,会不断的调用该功能函数,每次的下载进度都不一样,因此需要计算每次的比率,再根据比率去打印进度条。。
②这里是先将字符一次性放进数组中,最后在一起刷新出来。如果不这样的话每次都会显示从0开始打印,并不是我们想看到的结果。
主函数main.c 代码:
double bandwith = 1024 * 1024 * 1.0;//下载速度1MB/s void download(double filesize, Call_back cb)//函数指针做参数 {double cnt = 0.0;printf("download begin....,bandwith is:%.1lf\n", bandwith);while (cnt <= filesize){cb(filesize, cnt);Sleep(50);cnt += bandwith;}printf("\ndownload finishi......,filesize is:%.1lf\n", filesize);printf("\n"); } int main() {download(50.0 * 1024 * 1024, processbar);//50MB download(10.0 * 1024 * 1024, processbar);//10MB download(100.0 * 1024 * 1024, processbar);//100MB return 0; }
我们在主函数中分别模拟下载了3个不同大小的文件,还有下载功能存在一个函数指针类型的参数,只需传入函数名(函数地址),就能找到对应函数的功能实现。
效果图:
可以看到,进度条会根据文件大小的不同,下载的速度也不一样,同一带宽情况下,文件越大,那必然越慢,如上图的10MB比100MB快多了。
总结
本篇博客到这里就结束了,感谢观看!
...................................................................................................花开当折直须摘,青春最可爱
————《望春风》