LoginSignup
11
6

More than 3 years have passed since last update.

CLIプログレスバーを作る(複数行対応/色付き)[Node.js/C#/VB.Net/Python3]

Last updated at Posted at 2017-02-11

コンソールで一行用のプログレスバーを作る時は"\r"(Cr、キャリッジリターン)が使用されるのは割と有名ですが、これでは一行を過ぎると戻らなくなってしまいます。
ここでは、1行目にゲージを、2行目以降にメッセージを表現するプログレスバーの例を記述します。
progress1.gif
progress3.gif
progress2.gif

Node.js

ProgressBar.js
"use strict";
const readline=require("readline");

const ProgressBar=(()=>{
    const PB={};
    const lenB=Symbol();

    //モノクロ版プログレスバー
    PB.Progress=class{
        constructor(width,parMax){
            //最大の桁数
            this.columns=process.stdout.columns;
            //プログレスバーの長さ
            this.width=width;
            //進捗度
            this.par=0;
            //目標進捗度
            this.parMax=parMax;
        }

        //プログレスバーの更新
        update(message){
            const parcent=this.par/this.parMax;
            const widthNow=Math.floor(this.width*parcent);
            const rowCnt=Math.floor(this[lenB](message)/this.columns)+2;

            const gauge=">".repeat(widthNow)+" ".repeat(this.width-widthNow);
            const status=`(${(parcent*100).toFixed(1)}%<-${this.par}/${this.parMax})`;

            console.error(`#[${gauge}]#${status}`);
            readline.clearScreenDown(process.stdout);

            console.error(message);
            readline.moveCursor(process.stdout,0,-rowCnt);
            this.par++;
        }

        //プログレスバーの完了
        done(doneAlert){
            const sideLen=Math.floor((this.width-doneAlert.length)/2);

            var gauge="=".repeat(sideLen)+doneAlert;
            gauge+="=".repeat(this.width-gauge.length);
            const status=`(100%<-${this.parMax}/${this.parMax})`;

            readline.clearScreenDown(process.stdout);
            console.error(`#[${gauge}]#${status}`);
        }
    };
    //バイト(文字幅)数計算
    PB.Progress.prototype[lenB]=function(message){
        var len=0;
        for(let val of message){
            let cc=val.charCodeAt(0);
            if( 0x0000<=cc && cc<=0x024F &&
                cc!=0x0085 && cc!=0x089 && cc!=0x00A7 && cc!=0x00B0 &&
                cc!=0x00B1 && cc!=0x00D7 && cc!=0x00F7 ||
                cc==0xA5 || cc==0x203E || cc==0xF8F0 ||
                0xFF61<=cc && cc<=0xFFDC ||
                0xFFE8<=cc && cc<=0xFFEE
            ){
                len=0|len+1;
            }
            else{
                len=0|len+2;
            }
        }
        return len;
    };

    //カラー版プログレスバー
    PB.ProgressColor=class extends PB.Progress{
        //プログレスバーの更新
        update(message){
            const parcent=this.par/this.parMax;
            const widthNow=Math.floor(this.width*parcent);
            const rowCnt=Math.floor(this[lenB](message)/this.columns)+2;

            const status=`(${(parcent*100).toFixed(1)}%<-${this.par}/${this.parMax})`;

            readline.clearScreenDown(process.stderr);
            console.error(
                "\u001b[43m\u001b[5m"+  /*BackLightYellow*/
                "\u001b[33m"+           /*ForeDarkYellow*/
                "{"+
                "\u001b[46m"+           /*BackLightCyan*/
                " ".repeat(widthNow)+
                "\u001b[25m"+           /*BackDarkCyan*/
                " ".repeat(this.width-widthNow)+
                "\u001b[43m\u001b[5m"+  /*BackLightYellow*/
                "}"+
                "\u001b[0m"+            /*ResetColor*/
                status+
                "\n"+
                message
            );
            readline.moveCursor(process.stderr,0,-rowCnt);
            this.par=0|this.par+1
        }

        //プログレスバーの完了
        done(doneAlert){
            const sideLen=Math.floor((this.width-doneAlert.length)/2);

            var gauge=" ".repeat(sideLen)+doneAlert;
            gauge+=" ".repeat(this.width-gauge.length);
            const status=`(100%<-${this.parMax}/${this.parMax})`;

            readline.clearScreenDown(process.stderr);

            console.error(
                "\u001b[43m\u001b[5m"+  /*BackLightYellow*/
                "\u001b[33m"+           /*ForeDarkYellow*/
                "{"+
                "\u001b[42m"+           /*BackLightGreen*/
                "\u001b[31m\u001b[1m"+  /*ForeLightRed*/
                gauge+
                "\u001b[43m"+           /*BackLightYellow*/
                "\u001b[33m\u001b[22m"+ /*ForeDarkYellow*/
                "}"+
                "\u001b[0m"+            /*ColorReset*/
                status
            );
        }
    };

    return PB;
})();
Object.freeze(ProgressBar);
module.exports=ProgressBar;
main.js
"use strict";
const readline=require("readline");
readline.emitKeypressEvents(process.stdin);
const rl=readline.createInterface({input:process.stdin,output:process.stdout,terminal:false});
process.stdin.setRawMode(true);

const {Progress,ProgressColor}=require("./ProgressBar");

const g=(function*(){
    const firstMsg="1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ";
    const secondMsg="2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ";
    const thirdMsg="3rdステップ 3rdステップ 3rdステップ 3rdステップ 3rdステップ";
    process.stdout.write("READY...");
    yield process.stdin.once("keypress",key=>g.next(key));
    console.log("\rSTART!  ");

    const width=55;
    const works=270;
    //モノクロ版
    //const prg=new Progress(width,works);
    //カラー版
    const prg=new ProgressColor(width,works);

    for(let i=0;i<=works;i++){
        yield setTimeout(()=>g.next(),20);
        if(i<130){
            prg.update(firstMsg);
        }
        else if(i<210){
            prg.update(secondMsg);
        }
        else{
            prg.update(thirdMsg);
        }
    }
    prg.done("Done!");
    console.log("終了しました!");
    process.stdin.setRawMode(false);
    yield rl.once("line",val=>g.next(val));
    rl.pause();
})();
g.next();

C#

ProgressBar.cs
using System;
using static System.Console;

namespace ProgressBar{
    class Progress{
        //最大の桁数
        public int columns;
        //プログレスバーの長さ
        public int width;
        //進捗度
        public int par=0;
        //目標進捗度
        public int parMax;
        //最後に出力したカーソルの行
        protected int rowLate=CursorTop;

        //モノクロ版プログレスバー
        public Progress(int width,int parMax){
            this.columns=WindowWidth;
            this.width=width;
            this.parMax=parMax;
        }

        //プログレスバーの更新
        public virtual void update(string message){
            int row0=CursorTop;

            float parcent=(float)par/parMax;
            int widthNow=(int)Math.Floor(width*parcent);

            string gauge=new string('>',widthNow)+new string(' ',width-widthNow);
            string status=$"({(parcent*100).ToString("f1")}%<-{par}/{parMax})";

            Error.WriteLine($"#[{gauge}]#{status}");
            clearScreenDown();

            Error.WriteLine(message);
            rowLate=CursorTop;
            SetCursorPosition(0,row0);
            par++;
        }

        //プログレスバーの完了
        public virtual void done(string doneAlert){
            int sideLen=(int)Math.Floor((float)(width-doneAlert.Length)/2);

            string gauge=new string('=',sideLen)+doneAlert;
            gauge+=new string('=',width-gauge.Length);
            string status=$"(100%<-{parMax}/{parMax})";

            clearScreenDown();
            Error.WriteLine($"#[{gauge}]#{status}");
        }

        //コンソール表示の掃除
        protected void clearScreenDown(){
            int clearRange=rowLate-(CursorTop-1);
            Error.Write(new string(' ',columns*clearRange));
            SetCursorPosition(CursorLeft,CursorTop-clearRange);
        }
    }

    //カラー版プログレスバー
    class ProgressColor:Progress{
        public ProgressColor(int width,int parMax):base(width,parMax){}

        //プログレスバーの更新
        public override void update(string message){
            int row0=CursorTop;
            float parcent=(float)par/parMax;
            int widthNow=(int)Math.Floor(width*parcent);

            string status=$"({(parcent*100).ToString("f1")}%<-{par}/{parMax})";

            BackgroundColor=ConsoleColor.Yellow;
            ForegroundColor=ConsoleColor.DarkYellow;
            Error.Write("{");
            BackgroundColor=ConsoleColor.Cyan;
            Error.Write(new string(' ',widthNow));
            BackgroundColor=ConsoleColor.DarkCyan;
            Error.Write(new string(' ',width-widthNow));
            BackgroundColor=ConsoleColor.Yellow;
            Error.Write("}");
            ResetColor();
            Error.WriteLine(status);
            clearScreenDown();

            Error.WriteLine(message);
            rowLate=CursorTop;
            SetCursorPosition(0,row0);
            par++;
        }

        //プログレスバーの完了
        public override void done(string doneAlert){
            int sideLen=(int)Math.Floor((float)(width-doneAlert.Length)/2);

            string gauge=new string(' ',sideLen)+doneAlert;
            gauge+=new string(' ',width-gauge.Length);
            string status=$"(100%<-{parMax}/{parMax})";

            clearScreenDown();

            BackgroundColor=ConsoleColor.Yellow;
            ForegroundColor=ConsoleColor.DarkYellow;
            Error.Write("{");
            BackgroundColor=ConsoleColor.Green;
            ForegroundColor=ConsoleColor.Red;
            Error.Write(gauge);
            BackgroundColor=ConsoleColor.Yellow;
            ForegroundColor=ConsoleColor.DarkYellow;
            Error.Write("}");
            ResetColor();
            Error.WriteLine(status);
        }
    }
}
main.cs
using System;
using System.Threading;
using static System.Console;
using ProgressBar;

class Program{
    static void Main(){
        const string firstMsg="1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ";
        const string secondMsg="2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ";
        const string thirdMsg="3rdステップ 3rdステップ 3rdステップ 3rdステップ 3rdステップ";
        Write("READY...");
        ReadKey();
        WriteLine("\rSTART!  ");

        const int width=55;
        const int works=270;
        //モノクロ版
        //var prg=new Progress(width,works);
        //カラー版
        var prg=new ProgressColor(width,works);

        for(var i=0;i<=works;i++){
            Thread.Sleep(20);
            if(i<130){
                prg.update(firstMsg);
            }
            else if(i<210){
                prg.update(secondMsg);
            }
            else{
                prg.update(thirdMsg);
            }
        }
        prg.done("Done!");
        WriteLine("終了しました!");
        ReadLine();
    }
}

VB.Net

ProgressBar.vb
Option Strict On
Option Infer On
Imports System.Console
Imports CL=System.Console

Namespace ProgressBar
    'モノクロ版プログレスバー
    Class Progress
        '最大の桁数
        Public columns As Integer
        'プログレスバーの長さ
        Public width As Integer
        '進捗度
        Public par As Integer=0
        '目標進捗度
        Public parMax As Integer
        '最後に出力したカーソルの行
        Protected rowLate As Integer=CursorTop

        Sub New(width As Integer,parMax As Integer)
            Me.columns=WindowWidth
            Me.width=width
            Me.parMax=parMax
        End Sub

        'プログレスバーの更新
        Overridable Sub update(message As String)
            Dim row0=CursorTop

            Dim parcent=CSng(par/parMax)
            Dim widthNow=CInt(Math.Floor(width*parcent))

            Dim gauge=New String(">"c,widthNow) & New String(" "c,width-widthNow)
            Dim status=$"({(parcent*100).ToString("f1")}%<-{par}/{parMax})"

            CL.Error.WriteLine($"#[{gauge}]#{status}")
            clearScreenDown()

            CL.Error.WriteLine(message)
            rowLate=CursorTop
            SetCursorPosition(0,row0)
            par+=1
        End Sub

        'プログレスバーの完了
        Overridable Sub done(doneAlert As String)
            Dim sideLen=(width-doneAlert.Length)\2

            Dim gauge=New String("="c,sideLen) & doneAlert
            gauge &=New String("="c,width-gauge.Length)
            Dim status=$"(100%<-{parMax}/{parMax})"

            clearScreenDown()
            CL.Error.WriteLine($"#[{gauge}]#{status}")
        End Sub

        'コンソール表示の掃除
        Protected Sub clearScreenDown()
            Dim clearRange=rowLate-(CursorTop-1)
            CL.Error.Write(New String(" "c,columns*clearRange))
            SetCursorPosition(0,CursorTop-clearRange)
        End Sub
    End Class

    'カラー版プログレスバー
    Class ProgressColor: Inherits Progress
        Sub New(width As Integer,parMax As Integer)
            MyBase.New(width,parMax)
        End Sub

        'プログレスバーの更新
        Overrides Sub update(message As String)
            Dim row0=CursorTop
            Dim parcent=CSng(par/parMax)
            Dim widthNow=CInt(Math.Floor(width*parcent))

            Dim status=$"({(parcent*100).ToString("f1")}%<-{par}/{parMax})"

            BackgroundColor=ConsoleColor.Yellow
            ForegroundColor=ConsoleColor.DarkYellow
            CL.Error.Write("{")
            BackgroundColor=ConsoleColor.Cyan
            CL.Error.Write(New String(" "c,widthNow))
            BackgroundColor=ConsoleColor.DarkCyan
            CL.Error.Write(New String(" "c,width-widthNow))
            BackgroundColor=ConsoleColor.Yellow
            CL.Error.Write("}")
            ResetColor()
            CL.Error.WriteLine(status)
            clearScreenDown()

            CL.Error.WriteLine(message)
            rowLate=CursorTop
            SetCursorPosition(0,row0)
            par+=1
        End Sub

        'プログレスバーの完了
        Overrides Sub done(doneAlert As String)
            Dim sideLen=(width-doneAlert.Length)\2

            Dim gauge=New String(" "c,sideLen) & doneAlert
            gauge &=New String(" "c,width-gauge.Length)
            Dim status=$"(100%<-{parMax}/{parMax})"

            clearScreenDown()

            BackgroundColor=ConsoleColor.Yellow
            ForegroundColor=ConsoleColor.DarkYellow
            CL.Error.Write("{")
            BackgroundColor=ConsoleColor.Green
            ForegroundColor=ConsoleColor.Red
            CL.Error.Write(gauge)
            BackgroundColor=ConsoleColor.Yellow
            ForegroundColor=ConsoleColor.DarkYellow
            CL.Error.Write("}")
            ResetColor()
            CL.Error.WriteLine(status)
        End Sub
    End Class
End Namespace
main.vb
Option Strict On
Imports System.Console
Imports System.Threading
Imports ProgressBar

Module Program
    Sub Main()
        Const firstMsg="1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ"
        Const secondMsg="2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ"
        Const thirdMsg="3rdステップ 3rdステップ 3rdステップ 3rdステップ 3rdステップ"
        Write("READY...")
        ReadKey()
        WriteLine(vbCr & "START!  ")

        Const width=CInt(55)
        Const works=CInt(270)
        'モノクロ版
        'Dim prg As New Progress(width,works)
        'カラー版
        Dim prg As New ProgressColor(width,works)

        For i=0 To works
            Thread.Sleep(20)
            If i<130 Then
                prg.update(firstMsg)
            ElseIf i<210 Then
                prg.update(secondMsg)
            Else
                prg.update(thirdMsg)
            End If
        Next
        prg.done("Done!")
        WriteLine("終了しました!")
        ReadLine()
    End Sub
End Module

Python3

ProgressBar.py
import sys
import math
import os
import unicodedata
import colorama
from colorama import Fore,Back,Style
colorama.init()

#モノクロ版プログレスバー
class Progress:
    def __init__(self,width,parMax):
        #最大の行数
        self.columns=os.get_terminal_size().columns
        #プログレスバーの長さ
        self.width=width
        #進捗度
        self.par=0
        #目標進捗度
        self.parMax=parMax

    #プログレスバーの更新
    def update(self,message):
        parcent=self.par/self.parMax
        widthNow=math.floor(self.width*parcent)
        rowCnt=math.floor(self.__lenB(message)/self.columns)+2

        gauge=">"*widthNow+" "*(self.width-widthNow)
        status=f"({round(parcent*100,1)}%<-{self.par}/{self.parMax})"

        sys.stderr.write(
            f"#[{gauge}]#{status}\n"+
            "\u001b[0J"+            #clearScreenDown
            message+
            f"\u001b[{rowCnt}A\r\n" #moveCursorUp
        )
        self.par=self.par+1

    #プログレスバーの完了
    def done(self,doneAlert):
        sideLen=math.floor((self.width-len(doneAlert))/2)
        gauge="="*sideLen+doneAlert
        gauge+="="*(self.width-len(gauge))

        status=f"(100%<-{self.parMax}/{self.parMax})"

        sys.stderr.write(
            "\u001b[0J"+    #clearScreenDown
            f"#[{gauge}]#{status}\n"
        )

    #バイト(文字幅)数計算
    def __lenB(self,str):
        len=0
        for val in str:
            cw=unicodedata.east_asian_width(val)
            if cw in u"WFA":
                len=len+2
            else:
                len=len+1
        return len

#カラー版プログレスバー
class ProgressColor(Progress):
    #プログレスバーの更新
    def update(self,message):
        parcent=self.par/self.parMax
        widthNow=math.floor(self.width*parcent)
        rowCnt=math.floor(self._Progress__lenB(message)/self.columns)+2

        status=f"({round(parcent*100,1)}%<-{self.par}/{self.parMax})"

        sys.stderr.write(
            Style.BRIGHT+
            Back.YELLOW+
            Fore.YELLOW+
            "{"+
            Back.CYAN+
            Fore.CYAN+
            "▤"*widthNow+
            " "*(self.width-widthNow)+
            Back.YELLOW+
            Fore.YELLOW+
            "}"+
            Style.RESET_ALL+
            status+
            "\n"+
            "\u001b[0J"+            #clearScreenDown
            message+
            f"\u001b[{rowCnt}A\r\n" #moveCursorUp
        )
        self.par=self.par+1

    #プログレスバーの完了
    def done(self,doneAlert):
        sideLen=math.floor((self.width-len(doneAlert))/2)
        doneAlert=" "+doneAlert+" "
        gauge=(
            Fore.GREEN+
            "▤"*sideLen+
            Fore.RED+
            doneAlert+
            Fore.GREEN+
            "▤"*(self.width-len("#"*sideLen+doneAlert))
        )
        status=f"(100%<-{self.parMax}/{self.parMax})"

        sys.stderr.write(
            "\u001b[0J"+    #clearScreenDown
            Style.BRIGHT+
            Back.YELLOW+
            Fore.YELLOW+
            "{"+
            Back.GREEN+
            gauge+
            Back.YELLOW+
            Fore.YELLOW+
            "}"+
            Style.RESET_ALL+
            status+
            "\n"
        )
main.py
import os
from time import sleep
from ProgressBar import Progress,ProgressColor

if __name__ in "__main__":
    firstMsg="1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ"
    secondMsg="2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ"
    thirdMsg="3rdステップ 3rdステップ 3rdステップ 3rdステップ 3rdステップ"
    print("READY...",end="")
    if os.name=="nt":
        import msvcrt
        msvcrt.getch()
    else:
        input()
    print("\rSTART!  ")

    width=55
    works=270
    #モノクロ版
    #prg=Progress(width,works)
    #カラー版
    prg=ProgressColor(width,works)

    for i in range(0,works+1):
        sleep(0.02)
        if i<130:
            prg.update(firstMsg)
        elif i<210:
            prg.update(secondMsg)
        else:
            prg.update(thirdMsg)
    prg.done("Done!")
    print("終了しました!")
    input()

何をやってるの?

コンソールのカーソルの位置を記憶して、標準出力後に戻してます。
Node.jsではカーソルの位置の取得ができなかったので、コンソールの最大桁数と出力する文字バイト幅から何行分に相当するか計算して、相対座標で移動しています。
Node.jsのバイト数の計算はこちらのUnicode表に基づいて、半角全角判別するメソッドを書いて使用しています。(とりあえず、半角英数記号に加えてラテンBまでと半角カナ、半角ハングルまでを半角(1バイト)として計算。この区分分けについてベターと言えそうなものがなかったので自分で実装。)
Pythonの方は文字のコードから文字種の判別できるようなので、そレを利用しています。

なお、Python3については標準じゃ厳しかったのでcoloramaを使用しています。
インストールはpipから次のように行っています。

pip3.6 install colorama

色つき版補足

Node.jsではWindowsでもANSIエスケープコードが使えるみたいですね。
流石replやnpmがやたらカラフルなだけあって感心したのですが、拡張色選ぼうとしたらバグったので使えるのは7色だけのようです。

なお、Python3でだけcolorama使用しても何故か背景色が変わりませんでした...
他の言語と見た目が若干変わるのは妥協の証。

11
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
6