009 进度条的实现

80 阅读14分钟

进度条的实现

进度条是用户界面中常见的元素,用于显示任务的完成进度。通过实现一个进度条,可以学习如何在 Linux 下进行简单的用户界面设计,同时也能理解文件链接和代码维护的重要性。结合之前讲的倒计时的代码,我们正式来实现我们 Linux 下第一个小程序 —— 进度条。结合之前讲解的 Linux 文件链接 和考虑到代码的 维护性,依旧采用 头文件 .h、函数体文件 .c、main 函数文件 .c 的三个文件进行讲解:

makefile 的配置

为了方便进度条的运行展示,这里给出我的 makefile 配置(第一种旨在理解编译流程,第二种全自动更方便):

# 目标文件 processBar 依赖于 processBar.o
processBar: processBar.o
	gcc processBar.o -o processBar

# 目标文件 processBar.o 依赖于 processBar.s
processBar.o: processBar.s
	gcc -c processBar.s -o processBar.o

# 目标文件 processBar.s 依赖于 processBar.i
processBar.s: processBar.i
	gcc -S processBar.i -o processBar.s

# 目标文件 processBar.i 依赖于 processBar.c
processBar.i: processBar.c
	gcc -E processBar.c -o processBar.i
    
# 清理生成的文件
.PHONY: clean
clean:
	rm -f processBar.i processBar.s processBar.o processBar

或者

# 定义目标和源文件
TARGET = processBar
SRCS = processBar.c processBarmain.c
OBJS = $(SRCS:.c=.o)

# 定义编译器和选项
CC = gcc
CFLAGS = -I. -Wall -O2

# 默认目标
all: $(TARGET)

# 生成目标文件
$(TARGET): $(OBJS)
	$(CC) $(OBJS) -o $(TARGET)

# 生成对象文件
%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@

# 清理生成的文件
.PHONY: clean
clean:
	rm -f processBar.i processBar.s processBar.o processBar

第一版:基础进度条

1. 单个循环打印

#include <stdio.h>
#include <unistd.h>

int main()
{
    for (int i = 0; i <= 100; ++i)
    {
        printf("\r[%3d%%]", i); // 使用\r回车,覆盖之前的输出。注意:在Linux下,我们更倾向于%%这种写法,使用转义符/可能会存在问题。
        fflush(stdout);         // 刷新输出缓冲区
        usleep(20000);          // 睡眠20ms(单位:微秒)
    }
    
    printf("\n");               // 最后换行
    return 0;
}

问题:

  1. 输出不直观,只是一个百分比数字。
  2. 缺少进度条的视觉表示。

2. 添加进度条显示

#include <stdio.h>
#include <unistd.h>

int main()
{
    char bar[101] = {0}; 		// 数组用于存储进度条内容

    for (int i = 0; i <= 100; ++i)
    {
        bar[i] = '#'; 			// 使用 '#' 或者 '-'表示进度
        printf("\r[%-100s][%3d%%]", bar, i);
        fflush(stdout);
        usleep(100000); 		// 调整速度
    }
    
    printf("\n");
    return 0;
}

问题:

  1. 进度条没有动态效果,看起来比较单调。
  2. 缺少加载动画提示。

3. 改进:添加加载动画

#include <stdio.h>
#include <unistd.h>

const char *label = "|-\\";		// 动画样式
int cnt = 0;

void processbar()
{
    printf("\r[%s] [%3d%%]", &label[cnt % 3], cnt); // 使用动画符号
    fflush(stdout);
    cnt++;
    usleep(100000);
}

int main()
{
    while (cnt <= 100)
    {
        processbar();
    }
    
    printf("\n");
    return 0;
}

问题:

  1. 动画符号和进度条没有良好结合。
  2. 缺少进度条长度控制。

4. 完善进度条长度与样式

#include <stdio.h>
#include <unistd.h>
#include <string.h>

const char *label = "|-\\";
char bar[101] = {0}; 			// 数组用于存储进度条内容

void processbar()
{
    static int cnt = 0;
    printf("\r[%-100s][%3d%%][%c]", bar, cnt, label[cnt % 3]);
    fflush(stdout);
    bar[cnt++] = '#';
    usleep(50000);
}

int main()
{
    while (cnt <= 100)
    {
        processbar();
    }
    
    printf("\n");
    return 0;
}

问题:

  1. 进度条结束时,右箭头没有正确显示。
  2. 变量写“死”,不利于维护。
  3. 没有体现文件之间的链接。

5. 大改文件,将变量写活,添加尾部符号“>”,优化代码

// 头文件 processBar.h
#pragma once

#include <stdio.h>
#include <unistd.h>
#include <string.h>

#define NUM 102
#define TOP 100
#define STYLE '-'
#define RIGHT '>'

extern void processbar(int speed);

// 函数文件 processBar.c
#include "processBar.h"

const char *label = "|-\\";
void processbar()
{
    char bar[NUM];
    memset(bar, '\0', sizeof(bar));
    int len = strlen(label);

    int cnt = 0;
    while (cnt <= 100)
    {
        printf("[%-100s][%d%%][%c]\r", bar, cnt, label[cnt % len]);
        fflush(stdout);
        bar[cnt++] = STYLE;
        bar[cnt] = '>';  // 在尾部添加'>'
        usleep(50000);   // 加快速度
    }
}

// main函数文件 processBarmain.c
#include "processBar.h"

int main()
{
    processbar();
    return 0;
}

问题

  • 进度条完成后,尾部仍然显示“>”,显得不够美观。
  • 命令提示行会进行覆盖。

6. 修复尾部符号“>”和提示行覆盖的问题

const char *label = "|-\\";
void processbar()
{
    char bar[NUM];
    memset(bar, '\0', sizeof(bar));
    int len = strlen(label);

    int cnt = 0;
    while (cnt <= 100)
    {
        printf("[%-100s][%d%%][%c]\r", bar, cnt, label[cnt % len]);
        fflush(stdout);
        bar[cnt++] = STYLE;

        if (cnt < 100)
        {
            bar[cnt] = '>';		  // 仅在未完成时添加'>'
        }

        usleep(50000);
    }
    printf("\n");				  // 刷新行缓冲区
}

7. 优化代码,提高可维护性 —— 第一版进度条诞生!

// 头文件 processBar.h
#pragma once
#include <stdio.h>
#include <unistd.h>
#include <string.h>

#define NUM 102         				   // 进度条数组长度, 代表进度条最大容量
#define TOP 100         				   // 最大进度, 定义进度条的最大进度(100%)
#define BODY '-'        				   // 进度条主体符号, 用于表示进度的主要部分
#define RIGHT '>'      					   // 进度条尾部符号, 用于显示进度条的结束位置

extern void processbar(int speed);  	   // 函数声明, 声明一个更新进度条的函数,speed为速度参数

// 函数体文件 processBar.c
#include "processBar.h"

const char *label = "|-\\";           	   // 旋转动画符号, 用于表示进度条右侧的动态符号

// 更新进度条函数
void processbar(int speed)
{
    char bar[NUM];                         // 创建一个字符数组,用来存储进度条的状态
    memset(bar, '\0', sizeof(bar));        // 初始化进度条数组,填充空字符
    int len = strlen(label);               // 获取动画符号的长度,label长度用于循环动画符号

    int cnt = 0;                           // 进度条的当前进度,初始为0
    while (cnt <= TOP)                     // 当进度小于等于最大值(100%)时,持续循环
    {  
        // 在进度条的前面输出进度状态,格式:[进度条状态][百分比][动画符号]
        printf("[%-100s][%d%%][%c]\r", bar, cnt, label[cnt % len]);
        fflush(stdout);                    // 强制刷新输出缓冲区,以确保进度条实时更新显示
        bar[cnt++] = BODY;                 // 将进度条数组的当前进度位置更新为进度条主体符号

        if (cnt < TOP)                     // 如果当前进度小于最大值,则更新尾部符号
        {
            bar[cnt] = RIGHT;              // 在进度条的尾部添加'>'符号,表示当前进度
        }

        usleep(speed);                     // 使用usleep函数控制更新速度,单位为微秒
    }
    
    printf("\n");                          // 完成进度条后换行,避免覆盖命令行内容
}


// main函数文件 processBarmain.c
#include "processBar.h"

int main()
{
    processbar(50000);                     // 调用进度条函数,传入速度参数(单位:微秒)
    return 0;
}

第二版:支持单任务和多任务调用

1. 单任务调用

// 头文件 processBar.h
#pragma once  		 					// 防止头文件重复包含
#include <stdio.h>   					// 引入标准输入输出库,用于 printf()
#include <unistd.h>  					// 引入unistd.h库,用于 usleep()
#include <string.h>  					// 引入字符串处理库,用于 strlen()

// 宏定义常量
#define NUM 102      					// 进度条的长度
#define TOP 100      					// 进度条的最大值(100%)
#define BODY '-'     					// 进度条填充的字符
#define RIGHT '>'    					// 进度条末尾的字符,表示进度的指示符

extern void processbar(int rate);		// 声明外部函数 processbar(),用于更新进度条


// 函数体文件 processBar.c
#include "processBar.h"  				// 引入头文件,声明了 processbar() 和相关常量

const char *label = "|-\\";
char bar[NUM];  						// 存储进度条的字符数组,长度为 NUM(102)

// 更新进度条函数
void processbar(int rate)
{
    if (rate < 0 || rate > 100)
    {
        return;							// 如果进度值不在有效范围内(0 到 100),直接返回
    }
    
    int len = strlen(label);  			// 获取 label 字符串的长度,用于后续计算进度条的状态显示

    // 打印进度条信息
    // [%-100s] 打印进度条的当前状态(由 bar 数组表示)
    // [%d%%] 打印当前进度百分比
    // [%c] 打印动态变化的进度条符号(`|`, `-`, `\`, 或 `\\`)
    printf("[%-100s][%d%%][%c]\r", bar, rate, label[rate % len]);

    fflush(stdout);  					// 刷新标准输出,确保进度条立即显示

    // 更新进度条的当前状态
    bar[rate++] = BODY;  				// 填充进度条的当前位置(使用定义的 BODY 字符)
    
    if (rate < 100)
    {
        bar[rate] = RIGHT;				// 如果进度没有达到 100%,则在当前进度末尾添加一个 RIGHT 字符('>')
    }
}


// main函数文件 processBarmain.c
#include "processBar.h"  				// 引入头文件,声明了 processbar() 函数和其他常量

int main()
{
    int total = 1000;  					// 总进度,模拟下载的总大小
    int curr = 0;      					// 当前进度,从 0 开始

    // 模拟进度条更新
    while (curr <= total)
    {
        // 根据当前进度计算进度百分比
        int rate = (curr * 100) / total;// 进度百分比计算公式

        processbar(rate);				// 调用 processbar() 函数,更新进度条

        curr += 10;  					// 每次增加 10,模拟进度增加
        usleep(50000);  				// 延迟 50 毫秒(模拟下载延迟)
    }

    printf("\n");  						// 下载完成后换行,清理输出格式
    return 0;  							// 程序正常退出
}

2. 多任务调用:

// 定义函数指针类型 callback_t,这个指针类型指向一个接受 int 参数并返回 void 的函数。用于作为回调函数,更新和展示进度。
typedef void (*callback_t)(int);

// 下载函数,模拟一个下载过程,接受一个回调函数作为参数。该回调函数用于展示下载进度。
void download(callback_t cb)
{
    int total = 1000;  					// 总下载量,模拟为 1000MB
    int curr = 0;      					// 当前下载量,初始化为 0

    // 模拟下载过程,直到下载完成
    while (curr <= total)
    {
        usleep(50000);  				// 模拟下载过程中每次更新的延迟,50毫秒

        int rate = curr * 100 / total;	// 计算当前下载进度的百分比

        cb(rate);// 调用回调函数 cb,传入当前的进度百分比,例如,调用进度条更新函数 processbar(rate)

        curr += 10;  					// 每次模拟下载进度增加 10MB
    }

    printf("\n");						// 打印换行符,确保下载过程结束后格式正确
}

// 主函数,演示如何调用 download 函数并传入回调函数。每次调用 download 时,都会传入相同的回调函数 processbar 来展示进度。
int main()
{
    printf("download 1:\n");			// 输出下载开始的提示信息
    download(processbar);				// 调用 download 函数,传入 processbar 作为回调函数,显示下载进度

    printf("download 2:\n");			// 输出第二次下载的提示信息
    download(processbar);				// 再次调用 download 函数,继续传入 processbar 作为回调函数

    
    printf("download 3:\n");			// 输出第三次下载的提示信息
    download(processbar);				// 再次调用 download 函数,继续传入 processbar 作为回调函数

    return 0;  							// 程序执行结束,返回 0
}

再次运行,会发现这样的多任务下载的进度条是同步的,不能达到真正的多任务下载,我们添加一个函数解决:

void initbar()
{
    memset(bar,'\0',sizeof(bar));
}

3. 优化代码 —— 第二版进度条诞生!

// 头文件 processBar.h
#pragma once
#include <stdio.h>
#include <unistd.h>
#include <string.h>

#define NUM 102        					// 进度条数组的长度,定义进度条的最大容量
#define TOP 100        					// 最大进度,定义进度条的最大进度(100%)
#define BODY '-'       					// 进度条主体符号,用于表示进度的主体部分
#define RIGHT '>'      					// 进度条尾部符号,用于表示当前进度位置的尾部

typedef void (*callback_t)(int);  		// 定义一个函数指针类型,用于回调进度更新的函数
void processbar(int rate);
extern void download(callback_t cb);	// 函数声明,模拟下载过程并调用回调函数
extern void initbar();                  // 函数声明,用于初始化进度条


// 函数体文件 processBar.c
#include "processBar.h"

const char *label = "|-\\";         	// 旋转动画符号,用于在进度条右侧展示动态符号
char bar[NUM];                      	// 全局进度条数组,用来存储当前的进度状态

// 初始化进度条,将进度条数组清空
void initbar()
{
    memset(bar, '\0', sizeof(bar));  	// 使用memset将进度条数组全部置为'\0',表示进度条清空
}

// 模拟下载过程,接受回调函数来更新进度条
void download(callback_t cb)
{
    int total = 1000;               	// 模拟总任务量(比如:1000MB)
    int curr = 0;                   	// 当前已完成的任务量

    while (curr <= total)           	// 当当前进度小于总进度时,继续更新
    {
        usleep(50000);              	// 模拟一个耗时操作,每次延时50ms
        int rate = curr * 100 / total;  // 根据已完成的任务量计算当前进度百分比
        cb(rate);                    	// 调用回调函数更新进度条,传入当前进度百分比
        curr += 10;                  	// 增量更新,假设每次下载增加10MB
    }
    
    printf("\n");                    	// 完成后换行,避免覆盖命令行内容
}

// 更新进度条的函数,根据传入的进度百分比显示进度
void processbar(int rate)
{
    // 如果进度值无效,输出错误信息并返回
    if (rate < 0 || rate > TOP)
    {
        fprintf(stderr, "Invalid rate: %d\n", rate);  // 错误输出
        return;
    }

    int len = strlen(label);  			// 获取动画符号的长度,用于循环显示
    // 打印当前进度条状态:[进度条状态][百分比][动画符号]
    printf("[%-100s][%d%%][%c]\r", bar, rate, label[rate % len]);
    fflush(stdout);  					// 强制刷新输出缓冲区,确保进度条实时更新

    bar[rate++] = BODY;   				// 在进度条数组的当前进度位置更新为主体符号('-')

    // 如果进度小于最大值,添加尾部符号('>')
    if (rate < TOP)
    {
        bar[rate] = RIGHT;
    }
}

// main函数文件 processBarmain.c
#include "processBar.h"

int main()
{
    printf("Download 1:\n");
    download(processbar);  				// 模拟第一个下载过程,并使用processbar回调更新进度条
    initbar();              			// 重置进度条

    printf("Download 2:\n");
    download(processbar);  				// 模拟第二个下载过程
    initbar();              			// 重置进度条

    printf("Download 3:\n");
    download(processbar);  				// 模拟第三个下载过程

    return 0;
}

第三版:彩色进度条(C 语言输出颜色

我们以绿色为例,添加以下内容:

#define GREEN "\033[0;32;32m"
#define NONE "\033[m"

将颜色渲染到进度条,就得到了最终的彩色进度条:

// 头文件 processBar.h
#pragma once
#include <stdio.h>
#include <unistd.h>
#include <string.h>

#define NUM 102            			// 进度条数组的长度,定义进度条的最大容量
#define TOP 100            			// 最大进度,定义进度条的最大值(100%)
#define BODY '-'           			// 进度条主体符号,用于表示进度的主体部分
#define RIGHT '>'          			// 进度条尾部符号,用于表示当前进度的尾部
#define GREEN "\033[0;32;32m"  		// 设置绿色字体颜色
#define NONE "\033[m"          		// 恢复默认字体颜色

typedef void (*callback_t)(int);    // 定义函数指针类型,用于回调进度更新的函数
void processbar(int rate);
extern void download(callback_t cb);// 函数声明,模拟下载过程并调用回调函数更新进度
extern void initbar();              // 函数声明,用于初始化进度条


// 函数体文件 processBar.c
#include "processBar.h"

const char *label = "|-\\";         // 动画符号,用于显示进度条右侧的动态效果
char bar[NUM];                      // 全局进度条数组,用于存储进度条的当前状态

// 初始化进度条,将进度条数组清空
void initbar()
{
    memset(bar, '\0', sizeof(bar)); // 使用memset将进度条数组全部置为'\0',表示进度条清空
}

// 模拟下载过程,接受回调函数来更新进度条
void download(callback_t cb)
{
    int total = 1000;               // 模拟总任务量(比如:1000MB)
    int curr = 0;                   // 当前已完成的任务量

    while (curr <= total)           // 当当前进度小于总进度时,继续更新
    {
        usleep(50000);              // 模拟一个耗时操作,每次延时50ms
        int rate = curr * 100 / total;  // 根据已完成的任务量计算当前进度百分比
        cb(rate);                   // 调用回调函数更新进度条,传入当前进度百分比
        curr += 10;                 // 增量更新,假设每次下载增加10MB
    }
    printf("\n");                   // 下载完成后输出换行,避免覆盖命令行内容
}

// 更新进度条的函数,根据传入的进度百分比显示进度
void processbar(int rate)
{
    if (rate < 0 || rate > TOP)     // 如果传入的进度值无效(小于0或大于100),直接返回
    {
        return;
    }

    int len = strlen(label);  		// 获取动画符号的长度,用于循环显示旋转符号
    
    // 打印当前进度条状态:[进度条状态][百分比][动画符号]
    // 使用绿色显示进度条部分,恢复默认颜色后显示进度百分比和动画符号
    printf(GREEN "[%-100s]" NONE "[%d%%][%c]\r", bar, rate, label[rate % len]);
    fflush(stdout);  			    // 强制刷新输出缓冲区,确保进度条实时更新

    bar[rate++] = BODY;   			// 在进度条数组的当前进度位置更新为主体符号('-')

    if (rate < TOP)
    {
        bar[rate] = RIGHT;			// 如果进度小于最大值,则在尾部添加'>'符号,表示当前进度位置
    }
}

// main函数文件 processBarmain.c
#include "processBar.h"

int main()
{
    // 模拟下载1,调用download函数进行下载并更新进度条
    printf("Download 1:\n");
    download(processbar);    		// 调用download函数,并将processbar作为回调函数传入
    initbar();                		// 下载完成后重置进度条

    // 模拟下载2,重复下载过程并重置进度条
    printf("Download 2:\n");
    download(processbar);    		// 第二次下载
    initbar();                		// 重置进度条

    // 模拟下载3,最后一次下载
    printf("Download 3:\n");
    download(processbar);    		// 第三次下载

    return 0;                 		// 程序结束
}

通过本文的实现,我们逐步完成了从基础进度条到支持多任务和彩色进度条的功能扩展。未来,可以进一步优化代码结构,支持更多功能,例如动态调整进度条长度、支持多种颜色等。