终端中的进度条是如何实现的(终端中动态输出进度条的原理、编码实现终端中动态输出进度条、自定义进度条的风格、在Java代码中如何优雅地换行)

1. 问题引入

先看以下代码

在这里插入图片描述

public class Loop {
    
    

    public static void main(String[] args) {
    
    
        int size = 500;
        for (int i = 0; i < size; i++) {
    
    
            doSth();
        }
    }

    private static void doSth() {
    
    
        try {
    
    
            Thread.sleep(10);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }

}

在开发过程中,我们经常会碰到在循环中执行耗时操作的情况,当代码进入到循环后,会进入漫长的等待。在等待过程中,我们并不知道运行到第几次循环了,整个等待过程将变得十分煎熬


你也许会想到这样做:在每次循环中输出当前的运行进度

在这里插入图片描述

public class Loop {
    
    

    public static void main(String[] args) {
    
    
        int size = 500;
        for (int i = 0; i < size; i++) {
    
    
            doSth();
            System.out.println("Current progress: " + (i + 1) + "/" + size);
        }
    }

    private static void doSth() {
    
    
        try {
    
    
            Thread.sleep(10);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }

}

这样确实能起到一个观测的作用,但终端中会出现大量重复的信息,这些重复的信息会干扰我们看其它内容


有没有办法让输出内容在同一行呢?我们先将System.out.println方法换成System.out.print方法,接着在每一次输出的内容后面加上\r

在这里插入图片描述

public class Loop {
    
    

    public static void main(String[] args) {
    
    
        int size = 500;
        for (int i = 0; i < size; i++) {
    
    
            doSth();
            System.out.print("Current progress: " + (i + 1) + "/" + size + "\r");
        }
    }

    private static void doSth() {
    
    
        try {
    
    
            Thread.sleep(10);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }

}

加上\r后的运行效果

在这里插入图片描述

这是怎么做到的呢,为什么加了一个\r就能让输出内容保持在同一行呢

2. 编码实现终端中动态输出进度条

2.1 终端中动态输出进度条的原理

相信做过开发的同学对\r\n都不陌生,当输出大段字符串,中间想要插入换行效果的时候,就会用到\r\n。其实,\r\n是两个字符,两者的含义是不同的

\r的学名是回车,作用是将光标移动到行首,起源于老式打字机的回车键。老式打字机有一个部位叫字车,每打一个字,字车都会向右移动一位,当移动到最右侧时,按下老式打字机的回车键,字车就会重新回到行首

在这里插入图片描述

\n的学名是换行,作用是将光标移动到下一行,起源于老式打字机的换行键。当按下老式打字机的换行键后,滚筒就会向下滚动一行,滚筒每向下滚动一行,纸张就会向上滚动一行

在这里插入图片描述

所以\r\n的效果就是将光标移动到下一行的行首,\r\n也是早期Windows系统标准的换行符组合

在Linux系统中,只需要\n就可以用来表示换行,后来大部分的Windows系统也支持用\n直接来表示换行

回到上面循环的例子,当第一次输出Current progress: 1/500的时候,后面加一个\r,就可以把光标移动到行首,第二次输出Current progress: 2/500的时候,就会把第一次输出的Current progress:1/500给覆盖掉

由于存在视觉暂留,我们肉眼看的感觉就是只有第一个数字变了,其余信息都没有变,这就是在同一行动态输出运行进度的原理

2.2 编码实现(基于Java语言)

基于终端中动态输出进度条的原理,我们可以实现一些更好玩的进度条效果,例如Linux系统中使用yum install安装软件时的进度条效果

我们自定义一个与进度条的有关的类,类中包含current、total、width、style四个属性,内置三种风格的进度条,其中print方法是整个类的核心

2.3.1 ProgressBar.java

在这里插入图片描述

public class ProgressBar {
    
    
    private int current;
    private final int total;
    private final int width;
    private final Style style;

    private final static Style STYLE_COMMENT = new Style("#", " ", "");
    private final static Style STYLE_HEADER = new Style("#", "=", "");
    private final static Style STYLE_ASSIGNMENT = new Style("=", " ", ">");

    public ProgressBar(int total, int width, int style) {
    
    
        this.total = total;
        this.width = width;
        this.current = 0;
        if (style == 1) {
    
    
            this.style = STYLE_COMMENT;
        } else if (style == 2) {
    
    
            this.style = STYLE_HEADER;
        } else {
    
    
            this.style = STYLE_ASSIGNMENT;
        }
    }

    public void start() {
    
    
        System.out.println("Start a task");
    }

    public void finish() {
    
    
        System.out.println(System.lineSeparator() + "Task executed successfully");
    }

    public void update(int progress) {
    
    
        this.current = progress;
        print();
    }

    private void print() {
    
    
        double percentage = (double) current / total;
        int progressMarks = (int) (percentage * width);

        StringBuilder bar = new StringBuilder();
        bar.append(String.format("Current progress: %3d%% [", (int) (percentage * 100)));
        boolean firstRight = true;
        for (int i = 0; i < width; i++) {
    
    
            if (i < progressMarks) {
    
    
                bar.append(style.leftString);
            } else {
    
    
                if (firstRight) {
    
    
                    bar.append(style.leftEndString);
                    firstRight = false;
                }
                bar.append(style.rightString);
            }
        }
        bar.append("]");

        System.out.print("\r" + bar);
        if (percentage >= 1) {
    
    
            finish();
        }
    }

    private static class Style {
    
    
        private String leftString;
        private String rightString;
        private String leftEndString;

        public String getLeftString() {
    
    
            return leftString;
        }

        public void setLeftString(String leftString) {
    
    
            this.leftString = leftString;
        }

        public String getRightString() {
    
    
            return rightString;
        }

        public void setRightString(String rightString) {
    
    
            this.rightString = rightString;
        }

        public String getLeftEndString() {
    
    
            return leftEndString;
        }

        public void setLeftEndString(String leftEndString) {
    
    
            this.leftEndString = leftEndString;
        }

        public Style(String leftString, String rightString, String leftEndString) {
    
    
            this.leftString = leftString;
            this.rightString = rightString;
            this.leftEndString = leftEndString;
        }
    }

}

2.3.2 ProgressBarDemo.java

public class ProgressBarDemo {
    
    

    public static void main(String[] args) {
    
    
        int size = 500;
        ProgressBar progressBar = new ProgressBar(size, 50, 3);

        progressBar.start();
        for (int i = 0; i < size; i++) {
    
    
            doSth();
            progressBar.update(i + 1);
        }
    }

    private static void doSth() {
    
    
        try {
    
    
            Thread.sleep(10);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }

}

2.3 各种进度条的实现效果

第一种风格的进度条(STYLE_COMMENT)

在这里插入图片描述

第二种风格的进度条(STYLE_HEADER)

在这里插入图片描述

第三种风格的进度条(STYLE_ASSIGNMENT)

在这里插入图片描述

2.4 其它语言实现终端中动态输出进度条

2.4.1 C/C++

#include <iostream>
#include <thread>
#include <chrono>

using namespace std;

class ProgressBar {
    
    
private:
    int current;
    const int total;
    const int width;
    const char* leftString;
    const char* rightString;
    const char* leftEndString;

public:
    ProgressBar(int total, int width, int style) : total(total), width(width), current(0) {
    
    
        if (style == 1) {
    
    
            leftString = "#";
            rightString = " ";
            leftEndString = "";
        } else if (style == 2) {
    
    
            leftString = "#";
            rightString = "=";
            leftEndString = "";
        } else {
    
    
            leftString = "=";
            rightString = " ";
            leftEndString = ">";
        }
    }

    void start() {
    
    
        cout << "Start a task" << endl;
    }

    void finish() {
    
    
        cout << endl << "Task executed successfully" << endl;
    }

    void update(int progress) {
    
    
        current = progress;
        print();
    }

private:
    void print() {
    
    
        double percentage = static_cast<double>(current) / total;
        int progressMarks = static_cast<int>(percentage * width);

        cout << "\rCurrent progress: " 
             << static_cast<int>(percentage * 100) 
             << "% [";

        for (int i = 0; i < width; i++) {
    
    
            if (i < progressMarks) {
    
    
                cout << leftString;
            } else if (i == progressMarks) {
    
    
                cout << leftEndString;
            } else {
    
    
                cout << rightString;
            }
        }
        cout << "]";

        if (percentage >= 1) {
    
    
            finish();
        }
    }
};

void doSth() {
    
    
    this_thread::sleep_for(chrono::milliseconds(10));
}

int main() {
    
    
    int size = 500;
    ProgressBar progressBar(size, 50, 3);

    progressBar.start();
    for (int i = 0; i < size; i++) {
    
    
        doSth();
        progressBar.update(i + 1);
    }

    return 0;
}

2.4.2 Python

import time
import sys


class ProgressBar:
    def __init__(self, total, width, style):
        self.total = total
        self.width = width
        self.current = 0
        if style == 1:
            self.left_string = "#"
            self.right_string = " "
            self.left_end_string = ""
        elif style == 2:
            self.left_string = "#"
            self.right_string = "="
            self.left_end_string = ""
        else:
            self.left_string = "="
            self.right_string = " "
            self.left_end_string = ">"

    def start(self):
        print("Start a task")

    def finish(self):
        print("\nTask executed successfully")

    def update(self, progress):
        self.current = progress
        self.print()

    def print(self):
        percentage = self.current / self.total
        progress_marks = int(percentage * self.width)

        bar = f"\rCurrent progress: {
      
      int(percentage * 100)}% ["
        for i in range(self.width):
            if i < progress_marks:
                bar += self.left_string
            elif i == progress_marks:
                bar += self.left_end_string
            else:
                bar += self.right_string
        bar += "]"

        sys.stdout.write(bar)
        sys.stdout.flush()

        if percentage >= 1:
            self.finish()


def do_sth():
    time.sleep(0.01)


def main():
    size = 500
    progress_bar = ProgressBar(size, 50, 3)

    progress_bar.start()
    for i in range(size):
        do_sth()
        progress_bar.update(i + 1)


if __name__ == "__main__":
    main()

2.4.3 bash

#!/bin/bash

# Function to simulate task execution
do_something() {
    
    
    sleep 0.01
}

# Function to display progress bar
progress_bar() {
    
    
    local total=$1
    local width=$2
    local style=$3

    local current=0
    local left_string
    local right_string
    local left_end_string

    if [ "$style" -eq 1 ]; then
        left_string="#"
        right_string=" "
        left_end_string=""
    elif [ "$style" -eq 2 ]; then
        left_string="#"
        right_string="="
        left_end_string=""
    else
        left_string="="
        right_string=" "
        left_end_string=">"
    fi

    echo "Start a task"

    while [ "$current" -lt "$total" ]; do
        ((current++))
        do_something

        percentage=$((current * 100 / total))
        progress_marks=$((percentage * width / 100))

        bar="["
        for ((i = 0; i < width; i++)); do
            if [ "$i" -lt "$progress_marks" ]; then
                bar+="$left_string"
            elif [ "$i" -eq "$progress_marks" ]; then
                bar+="$left_end_string"
            else
                bar+="$right_string"
            fi
        done
        bar+="]"

        echo -ne "\rCurrent progress: $percentage% $bar"
    done

    echo -e "\nTask executed successfully"
}

# Main function
main() {
    
    
    local size=500
    local width=50
    local style=3

    progress_bar "$size" "$width" "$style"
}

main

3. 扩展:在Java代码中如何优雅地换行

虽然在Java代码中可以直接用\n来换行,但这不是一个好的做法,为什么呢?

因为不同的操作系统使用不同的字符来表示文本文件的换行符,这有可能会导致在不同操作系统之间交换文本文件时出现问题

例如在 Windows 系统创建的文本文件,如果在不进行转换的情况下直接在 Unix/Linux 系统中打开,就可能会出现换行不正确的问题


因为不同的操作系统使用不同的换行符类似而造成的问题我还真遇到一个,具体可以参考我的另一篇博文:docker容器中sh脚本明明存在,启动容器时却一直报错:no such file or directory


Java的设计者早已想到了这个问题,那就是使用System类提供的lineSeparator方法来换行,该方法帮我们屏蔽了不同操作系统之间换行符的区别,提高了Java程序的可移植性

在这里插入图片描述


其实System.out.println方法也调用了lineSeparator方法,我们可以在源码中找到答案

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

4. 参考视频

Java也能手搓进度条了?