进度条的实现(配合make和makefile超详细)
前言:
这里不是为了引流,但是如果进度条和make命令写在同一篇会时人有种长篇大论的感觉,大家可以先去看之前的make讲解的文章(make命令和makefile文件详解-CSDN博客)。本篇我们要在Linux中实现一个比较常见的进度条,因为每当我们使用yum的时候都会看到这个进度条。
学完本篇你也可以自己实现!
一、前置知识
这里当然是使用C语言来实现,但是不可能从C语言开始讲起(默认大家都有该基础)。完成进度条需要有以下只是做铺垫。
1.回车和换行
回车和换行是一个概念吗?其实不是,看下图:
这里有一个知识,在C语言中,回车是/r,换行是/n。但是我们平时/n是回车+换行,是因为C语言做了特殊处理。
此时我们更改proc.c内容(注意在里面加入了sleep函数),并看运行结果:
因为sleep函数是一个API,不同系统下API函数不同,这里Windows中sleep函数要引入windows.h头文件;而我们目前是Linux,要引入unistd.h头文件,函数名还是sleep函数。
因为作者是个废物,不知道怎么搞动图(也是懒得去搞),这里先允许作者通过口头描述来形容执行过程。
以上注意我们printf语句中有"\n"语句,先打印之后睡眠两秒执行完毕。
此时我们把"\n"去掉,并再次运行观察结果:
当然执行顺序依然是从上到下执行的,但是为什么这里先睡眠2s了呢?
在这2s中,"hello Linux"已经被执行了,在输出缓冲区中,而/n是强制刷新,所以直接把缓冲区的内容打印了出来。
所以此时在文件中加上fflush(stdout)语句,执行并观察结果:
2.\r回车
此时我们再次编辑Count.c文件:
此时我们使用/r做回车,之后需要强制刷新一次屏幕才会输出内容:
此时就已经完成了在原地的倒计时程序。
但是此时我们无法看到结束的内容,此时我们可以在代码再进行修改,此时直接在尾部追加printf("\n");即可。
#include<stdio.h>
#include<unistd.h>int main()
{int count = 3;while (count >= 0){printf("%d\r", count);fflush(stdout);count--;sleep(1);}printf("\n");return 0;
}
3.格式化输出
此时我们把count的起始值改为10,并再次观察输出结果:
这是因为在显示器上比如int a = 123,打印出a的值123。你以为它是int型,其实计算机已经将每个123都变成了一个字符,并打印出来。
这也就是我们平时的格式化输出。 此时我们是否还记得,%2d的功能,当整形不超过2位,右对齐并打印一位,前面打印空格,超出2位还是正常输出。
但是我们应该使用左对齐,所以加上负号。
所以修改Count.c内容:
#include<stdio.h>
#include<unistd.h>int main()
{int count = 10;while (count >= 7){printf("%-2d\r", count);fflush(stdout);count--;sleep(1);}printf("\n");return 0;
}
此时就已经做好了知识的铺垫,我们已经可以正式开始进度条的小项目了。
二、简单进度条
对于一个项目,我们一般要有3个文件(test.c文件,process.c文件,process.h头文件)。
具体原因是方便修改,我们一般在自定义头文件中引入要使用的所有头文件,声明所有的函数,一些静态变量和各种宏定义;process.c文件中引入自定义的头文件process.h,之后实现对所有函数的定义;而test.c就是写入主函数,调用process.c中的函数。
此时我们新创建一个目录,并在在目录中写这几个文件:
此时编辑Makefile文件:
并分别向他们中写入以下内容(具体操作看vim编辑器和gcc编译器-CSDN博客):
此时我们先测试一下文件:
这里千万不要嫌麻烦,因为我们要写一个项目必须保证前面的小步骤没有问题,必须一步一个脚印。 OK这里没有问题,继续。
1.进度条样式预览
我们先来看我们其中的一个进度条样式:
2.process.c初步实现
嗯……大家看看注释,当然这是一个简单的实验版进度条:
#include"process.h"#define NUM 4
//此时[]只打印3个#
#define STYLE '#'void Process()
{char bar[NUM];//因为打印的是一个字符数组的进度条//我们先将内存设置为'\0'memset(bar, '\0', sizeof(bar)); int cnt = 0;while (cnt < NUM - 1) {printf("[%-3s]\r", bar);//这里要记得回车才能在原地显示bar[cnt] = STYLE;cnt++;//强制刷新fflush(stdout);sleep(1);}printf("\r[%s]\n", bar);
}
OK,这就是最初的进度条。我们将数组长度设置为4,因为最后要留一个'\0',所以进度条中最多显示3个#,最多生成3次,我们每次都要强制刷新一次,之后回车使光标回到首字符位置,之后重新打印一次bar数组。
3.usleep函数
因为sleep函数是以秒为单位睡眠的,太慢了,我们要以毫秒进行睡眠,就需要使用usleep函数,注意它的单位是微秒。
1秒 = 1000000微秒
4.显示百分比
我们要计算百分比,这里以整数为例,要计算当前打印多少个#并和一个多少个#进行相除,这里要注意类型转化。
#include"process.h"#define NUM 80
//此时[]只打印79个#
#define STYLE '#'void Process()
{char bar[NUM];//因为打印的是一个字符数组的进度条//我们先将内存设置为'\0'memset(bar, '\0', sizeof(bar)); int cnt = 0;int persent = 0;while (cnt < NUM - 1) {persent = (int)((float)(cnt + 1) / (NUM - 1) * 100);printf("[%-79s][%-3d%%]\r", bar, persent);//这里要记得回车才能在原地显示bar[cnt] = STYLE;cnt++;//强制刷新fflush(stdout);usleep(300);}printf("\r[%s]\n", bar);
}
5.旋转光标
我们可以有一个动态的加载过程,就好比一个小圈圈。
此时我们就可以用一个字符串数组来完成:
#include"process.h"#define NUM 80
//此时[]只打印79个#
#define STYLE '#'#define STYLE1 '-'
#define STYLE2 '\\'
#define STYLE3 '|'
#define STYLE4 '/'void Process()
{char bar[NUM];//因为打印的是一个字符数组的进度条//我们先将内存设置为'\0'memset(bar, '\0', sizeof(bar));char set[5] = {STYLE1, STYLE2, STYLE3, STYLE4, '\0'};int cnt = 0;int persent = 0;while (cnt < NUM - 1) {persent = (int)((float)(cnt + 1) / (NUM - 1) * 100);printf("[%-79s][%-3d%%][%c]\r", bar, persent, set[cnt % 4]);//这里要记得回车才能在原地显示bar[cnt] = STYLE;cnt++;//强制刷新fflush(stdout);usleep(1000);}printf("\r[%s]\n", bar);
}
当然也可以使用字符串指针来完成,就像下面:
#include"process.h"#define NUM 80
//此时[]只打印79个#
#define STYLE '#'void Process()
{char bar[NUM];//因为打印的是一个字符数组的进度条//我们先将内存设置为'\0'memset(bar, '\0', sizeof(bar));const char *lable = "|/-\\";int len = strlen(lable);int cnt = 0;int persent = 0;while (cnt < NUM - 1) {persent = (int)((float)(cnt + 1) / (NUM - 1) * 100);printf("[%-79s][%-3d%%][%c]\r", bar, persent, lable[cnt % len]);//这里要记得回车才能在原地显示bar[cnt] = STYLE;cnt++;//强制刷新fflush(stdout);usleep(1000);}printf("\r[%s]\n", bar);
}
6.其他风格进度条
这种风格的进度条我们就实现了,但是你肯定也见过以下形式的进度条:
[=======> ][20%][\]
此时我们还是通过修改process.c文件来实现:
#include"process.h"#define NUM 80
//此时[]只打印79个#
#define STYLE '='void Process()
{char bar[NUM];//因为打印的是一个字符数组的进度条//我们先将内存设置为'\0'memset(bar, '\0', sizeof(bar));const char *lable = "|/-\\";int len = strlen(lable);int cnt = 0;int persent = 0;while (cnt < NUM - 1) {persent = (int)((float)(cnt + 1) / (NUM - 1) * 100);printf("[%-79s][%-3d%%][%c]\r", bar, persent, lable[cnt % len]);//这里要记得回车才能在原地显示bar[cnt] = STYLE;cnt++;if (cnt < NUM - 1) {bar[cnt] = '>';}//强制刷新fflush(stdout);usleep(10000);}bar[--cnt] = '=';printf("\r[%s]\n", bar);
}
此时版本二就做好了(第一个字符从=开始)。
还有第三版(第一个字符从">"开始):
#include"process.h"#define NUM 80
//此时[]只打印79个#
#define STYLE '='
#define STYLE1 '>'void Process()
{char bar[NUM];//因为打印的是一个字符数组的进度条//我们先将内存设置为'\0'memset(bar, '\0', sizeof(bar));const char *lable = "|/-\\";int len = strlen(lable);int cnt = 0;int persent = 0;while (cnt < NUM - 1) {bar[cnt] = STYLE1;persent = (int)((float)(cnt + 1) / (NUM - 1) * 100);printf("[%-79s][%-3d%%][%c]\r", bar, persent, lable[cnt % len]);//这里要记得回车才能在原地显示//强制刷新fflush(stdout);usleep(50000);bar[cnt] = STYLE;printf("[%-79s][%-3d%%][%c]\r", bar, persent, lable[cnt % len]);fflush(stdout);cnt++;//if (cnt < NUM - 1) {// bar[cnt] = '>';//}//usleep(10000);}bar[--cnt] = '=';printf("\r[%s]\n", bar);
}
三、正式版进度条
但是此时功能依旧不完整,此时我们需要正式模拟下载过程,考虑的必须全面,包括网速,当前下载量,总下载量。
我们先在test.c中模拟一次。
#include"process.h"//调整网速
const int base = 100;//声明总下载量
double total = 2048.0; // 2048MB//但此下载
double once = 0.1;void download()
{//声明当前总下载量double current = 0.0;while(current < total) {//产生 [1 - 20] 的随机数int r = rand() % base + 1;//当前网速double speed = r * once;current += speed;//此时可能会产生问题,current + 网速下载量 > totalif (current >= total) {current = total;}printf("cur = %.1lf,total = %.1lf\r", current, total);fflush(stdout);usleep(5000);}printf("\n");
}int main()
{srand(time(NULL));//种下随机数种子download();//Process(); return 0;
}
接下来步骤其实就是和之前的进度条结合起来,直接上代码了。
注意:此时我们又换了一种风格,后面的旋转光标更换为. 。而且打印的时候要
1.全部代码
process.c源文件:
#include"process.h"#define NUM 101
#define STYLE '='
#define STYLE1 '>'
#define STYLE2 ' '
#define STYLE3 '.' const int pnum = 6 ;//version2:
void Process(double total, double current)
{//1.更新当前进度百分比double rate = current / total * 100;//printf("test:%.1lf%%\r", rate);//2.更新进度条主体//当前认为 1% 就是一个等号char bar[NUM];memset(bar, '\0', sizeof(bar));int i = 0;for(i = 0; i < (int)rate; ++i){bar[i] = STYLE;}//3.更新旋转光标或者其他风格//此时我们用一个延长声明周期的static修饰一个整数static int num = 0;//以便控制打印下标num++;//这里解释一下:num最多到5,之后生命周期延长,到6的时候执行下面语句会为0num %= pnum;//也就是说:num一直都在 [0 - 5]循环//这里声明一个字符串数组char points[pnum + 1];//此数组大小为7,最后是'\0'占据//注意:Linux中编译器使用的是C99版本以上,所以可以这样写memset(points, '\0', sizeof(points));//这里我们要打印这个字符数组for (i = 0; i < pnum; ++i) {//我们先打印. 之后将后面数组中的内容换为空格即可if (i < num) {//此时初始化为.points[i] = STYLE3;} else {//否则打印空格points[i] = STYLE2;}}//3.打印printf("[%-100s][%.1lf%%]%s\r", bar, rate, points);
}//void Process()
//{
// char bar[NUM];
// //因为打印的是一个字符数组的进度条
// //我们先将内存设置为'\0'
// memset(bar, '\0', sizeof(bar));
// const char *lable = "|/-\\";
// int len = strlen(lable);
// int cnt = 0;
// int persent = 0;
// while (cnt < NUM - 1) {
// persent = (int)((float)(cnt + 1) / (NUM - 1) * 100);
// printf("[%-79s][%-3d%%][%c]\r", bar, persent, lable[cnt % len]);
// //这里要记得回车才能在原地显示
//
// bar[cnt] = STYLE;
//
// /制刷新
// fflush(stdout);
// usleep(50000);
// cnt++;
// if (cnt < NUM - 1) {
// bar[cnt] = '>';
// }
//
//
// }
// bar[--cnt] = '=';
// printf("\r[%s]\n", bar);
//}
process.h头文件:
#pragma once#include<stdio.h>
#include<unistd.h>
#include<string.h>//引入随机数头文件
#include<time.h>
#include<stdlib.h>void Process(double total, double current);
test.c源文件:
#include"process.h"//调整网速
const int base = 100;//声明总下载量
double total = 2048.0; // 2048MB//但此下载
double once = 0.1;void download()
{//声明当前总下载量double current = 0.0;while(current < total) {//产生 [1 - 20] 的随机数int r = rand() % base + 1;//当前网速double speed = r * once;current += speed;//此时可能会产生问题,current + 网速下载量 > totalif (current >= total) {current = total;}Process(total, current);fflush(stdout);usleep(5000);}printf("\n");
}int main()
{srand(time(NULL));//种下随机数种子download();//Process(); return 0;
}
这个旋转光标的目的就是当网速很慢的时候,我们也可以看到并不是卡死了,而是一直在下载。
四、更改为函数指针(附加)
我们将Prosess重命名,之后主函数可以使用回调函数的方式调用。
1.使用函数指针的全部代码
process.h头文件:
#pragma once#include<stdio.h>
#include<unistd.h>
#include<string.h>//引入随机数头文件
#include<time.h>
#include<stdlib.h>void FlushProcess(double total, double current);
process.c源文件:
#include"process.h"#define NUM 101
#define STYLE '='
#define STYLE1 '>'
#define STYLE2 ' '
#define STYLE3 '.' const int pnum = 6 ;//version2:
void FlushProcess(double total, double current)
{//1.更新当前进度百分比double rate = current / total * 100;//printf("test:%.1lf%%\r", rate);//2.更新进度条主体//当前认为 1% 就是一个等号char bar[NUM];memset(bar, '\0', sizeof(bar));int i = 0;for(i = 0; i < (int)rate; ++i){bar[i] = STYLE;}//3.更新旋转光标或者其他风格//此时我们用一个延长声明周期的static修饰一个整数static int num = 0;//以便控制打印下标num++;//这里解释一下:num最多到5,之后生命周期延长,到6的时候执行下面语句会为0num %= pnum;//也就是说:num一直都在 [0 - 5]循环//这里声明一个字符串数组char points[pnum + 1];//此数组大小为7,最后是'\0'占据//注意:Linux中编译器使用的是C99版本以上,所以可以这样写memset(points, '\0', sizeof(points));//这里我们要打印这个字符数组for (i = 0; i < pnum; ++i) {//我们先打印. 之后将后面数组中的内容换为空格即可if (i < num) {//此时初始化为.points[i] = STYLE3;} else {//否则打印空格points[i] = STYLE2;}}//4.打印printf("[%-100s][%.1lf%%]%s\r", bar, rate, points);
}
test.c源文件:
#include"process.h"//调整网速
const int base = 100;//声明总下载量
double total = 2048.0; // 2048MB//但此下载
double once = 0.1;//此时重命名函数指针
typedef void(*flush_t)(double, double);void download(flush_t f)
{//声明当前总下载量double current = 0.0;while(current < total) {//产生 [1 - 20] 的随机数int r = rand() % base + 1;//当前网速double speed = r * once;current += speed;//此时可能会产生问题,current + 网速下载量 > totalif (current >= total) {current = total;}f(total, current);fflush(stdout);usleep(5000);}printf("\n");
}int main()
{srand(time(NULL));//种下随机数种子//因为参数类型为函数指针//此时只需要传入函数名即可download(FlushProcess);//Process(); return 0;
}
这里我们使用回调函数,可以使以后调用函数更加方便。
总结:
大家最好跟着完成一遍,即使是抄的,抄明白以后自己完成,就会很有成就感。这是我们第一个在Linux中实现的小项目,其实是很有意义的。