记录一下通过C语言实现在终端中展示进度条的动画效果,代码同时支持 Linux 和 Windows。

头文件 pbar.h 内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#ifndef PBAR_H
#define PBAR_H
#ifdef __cplusplus
extern "C" {
#endif

struct pbar *pbar_create(int style); // style: 0,1,2,3

void pbar_update(struct pbar *pb, double pct);

double pbar_time_cost(struct pbar *pb);

#ifdef __cplusplus
}
#endif
#endif // PBAR_H

这里导出的接口同时支持 C 和 Cpp。

具体实现的源文件 pbar.c 内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
#include "pbar.h"

#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#ifdef _WIN32
#include <windows.h>
static LARGE_INTEGER freq; // time frequency
#else
#include <time.h>
#include <unistd.h>
#endif

struct pbar_tp {
#ifdef _WIN32
long long counter;
#else
struct timespec ts;
#endif
};

static void pbar_get_time(struct pbar_tp *tp) {
#ifdef _WIN32
LARGE_INTEGER now;
QueryPerformanceCounter(&now);
tp->counter = now.QuadPart;
#else
clock_gettime(CLOCK_MONOTONIC, &(tp->ts));
#endif
}

static double pbar_time_diff(struct pbar_tp *tp_start, struct pbar_tp *tp_end) {
#ifdef _WIN32
return (double)(tp_end->counter - tp_start->counter)
/ (double)freq.QuadPart; // seconds
#else
return (double)(tp_end->ts.tv_sec - tp_start->ts.tv_sec)
+ (double)(tp_end->ts.tv_nsec - tp_start->ts.tv_nsec)
/ 1e9; // seconds
#endif
}

static void pbar_time_str(double dt, char *time_buffer,
size_t time_buffer_len) {
if (!time_buffer || time_buffer_len == 0) { return; }

if (dt < 3600) { // mm:ss
int minutes = (int)(dt / 60);
int seconds = (int)(dt) % 60;
snprintf(time_buffer, time_buffer_len, "%02d:%02d", minutes, seconds);
}
else if (dt < 86400) { // hh:mm:ss
int hours = (int)(dt / 3600);
int minutes = ((int)(dt) % 3600) / 60;
int seconds = (int)(dt) % 60;
snprintf(time_buffer, time_buffer_len, "%02d:%02d:%02d", hours, minutes,
seconds);
}
else { // >xxh
int hours = (int)(dt / 3600);
snprintf(time_buffer, time_buffer_len, ">%dh", hours);
}
}

struct pbar {
int style;
int initialized;
double last_pct;
double last_rate;
struct pbar_tp start_time;
struct pbar_tp last_time;
};

struct pbar *pbar_create(int style) { // style: 0,1,2,3
#ifdef _WIN32
QueryPerformanceFrequency(&freq);
#endif

struct pbar *pb = (struct pbar *)malloc(sizeof(struct pbar));
memset(pb, 0, sizeof(struct pbar));

pbar_get_time(&pb->start_time);
pb->last_time = pb->start_time;
pb->style = style;
return pb;
}

double pbar_time_cost(struct pbar *pb) {
if (pb == NULL) { return 0; }

return pbar_time_diff(&pb->start_time, &pb->last_time);
}

void pbar_update(struct pbar *pb, double pct) {
if (pb == NULL || pct < pb->last_pct) { return; }

struct pbar_tp time_now;
pbar_get_time(&time_now);
double elapsed_time = pbar_time_diff(&pb->last_time, &time_now);

double cur_rate = (pct - pb->last_pct) / elapsed_time;
if (!pb->initialized || isnan(pb->last_rate) || isinf(pb->last_rate)) {
pb->last_rate = cur_rate;
pb->initialized = 1;
}
else {
const double alpha = 0.4;
pb->last_rate = alpha * cur_rate + (1 - alpha) * pb->last_rate;
}

pb->last_time = time_now;
pb->last_pct = pct;

char color_label_buf[20] = {0};
if (pct < 0.5) {
snprintf(color_label_buf, 20, "\033[38;2;%d;%d;%dm", 255,
(int)(255 * (pct / 0.5)), 0);
}
else {
snprintf(color_label_buf, 20, "\033[38;2;%d;%d;%dm",
(int)(255 * (1 - (pct - 0.5) / 0.5)), 255, 0);
}

double cost_time = pbar_time_cost(pb);
char cost_time_buf[20] = {0};
pbar_time_str(cost_time, cost_time_buf, 20);

double eta_time = (1.0 - pct) / pb->last_rate;
char eta_time_buf[20] = {0};
pbar_time_str(eta_time, eta_time_buf, 20);

char bar_buf[21] = {0};
int filled_num = (int)(pct * 20);
for (int i = 0; i < 20; ++i) { bar_buf[i] = (i < filled_num) ? '#' : ' '; }
bar_buf[20] = '\0';

switch (pb->style) {
case 1: printf("\r %s%6.2f%%\033[0m", color_label_buf, pct * 100); break;
case 2:
printf("\r %6.2f%% |%s| [%s<%s]", pct * 100, bar_buf, cost_time_buf,
eta_time_buf);
break;
case 3:
printf("\r \033[91m%6.2f%%\033[0m %s|%s|\033[0m \033[93m[%s<%s]\033[0m",
pct * 100, color_label_buf, bar_buf, cost_time_buf,
eta_time_buf);
break;
case 0: // default
default: printf("\r %6.2f%%", pct * 100); break;
}

fflush(stdout);
}

这里提供了四种进度条样式:

  • 数字百分比
  • 彩色数字百分比(颜色会随着进度逐渐变化)
  • 数字百分比 + 进度条
  • 数字百分比 + 彩色进度条

几种进度条的具体效果如下

测试文件 test.c 内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#include "pbar.h"

#include <stdio.h>
#include <stdlib.h>

#ifdef _WIN32
#include <windows.h>
#else
#include <time.h>
#include <unistd.h>
#endif

void common_sleep(int ms) {
if (ms <= 0) { return; }

#ifdef _WIN32
Sleep((DWORD)ms);
#else
struct timespec ts;
ts.tv_sec = ms / 1000;
ts.tv_nsec = (ms % 1000) * 1000000L;
nanosleep(&ts, NULL);
#endif
}

int main(void) {
// style 0
{
struct pbar *demo1 = pbar_create(0);
for (int i = 1; i <= 100; i++) {
pbar_update(demo1, i / 100.0);
common_sleep(i / 5);
}
printf("\ntime_cost: %.2f\n", pbar_time_cost(demo1));
free(demo1);
}

// style 1
{
struct pbar *demo1 = pbar_create(1);
for (int i = 1; i <= 100; i++) {
pbar_update(demo1, i / 100.0);
common_sleep(i / 5);
}
printf("\ntime_cost: %.2f\n", pbar_time_cost(demo1));
free(demo1);
}

// style 2
{
struct pbar *demo1 = pbar_create(2);
for (int i = 1; i <= 100; i++) {
pbar_update(demo1, i / 100.0);
common_sleep(i);
}
printf("\ntime_cost: %.2f\n", pbar_time_cost(demo1));
free(demo1);
}

// style 3
{
struct pbar *demo1 = pbar_create(3);
for (int i = 1; i <= 100; i++) {
pbar_update(demo1, i / 100.0);
common_sleep(i);
}
printf("\ntime_cost: %.2f\n", pbar_time_cost(demo1));
free(demo1);
}

return 0;
}