【ツール作成】プログレスバークラスを作る
前置き
プログレスバーのクラスを作ります。
どうやって作る?
作りはとっても単純です。
- 全体、実行済みの値から、進捗度を計算
- 進捗度からバーの状態を生成
- キャリッジリターン
\r
を使って、コンソール行を上書きする。
行全体を上書きしますが、一部だけ変わっているので、その行でバーが動いているように見えます。
バーのフォーマットと動き
バーは、メモリをつけた形をベースにして、src_dir
ディレクトリの処理が進んでいくと、=>
の記号が伸びていき、100% で完了します。
src_dir |---------+---------+---------+---------| 0% ( done / total ) copying_filename ↓ src_dir |===================>---------+---------| 50% ( done / total ) copying_filename ↓ src_dir |=======================================| 100% ( done / total ) copying_filename 【完了】
バーの部分は、ベースの文字列(|--+--| の部分)を作っておいて、進捗度を計算し、進捗部分(==> の部分)を作って、ベース文字列の部分を差し替えるように作っています。
今回は、コピー元ディレクトリを複数登録できるようにしていて、コピー元ディレクトリ毎に、プログレスバーオブジェクトを生成するようにしました。流れとしては、
- コピー元ディレクトリの総ファイルサイズを
total
の値として生成 - ファイルを一つコピーするごとに、コピーしたファイルのファイルサイズを
done
に加算 - 都度、進捗割合を計算し、プログレスバー文字列を作成
- 前に表示した内容を消して、新しいプログレスバー文字列を表示
といった感じです。
実装コード
class ProgressBar # 定数定義 EDGE = "|".freeze # バーの両端 SCALE = ("---------+" * 4).freeze # バーの目盛り部分 BAR = ((EDGE + SCALE).chop + EDGE).freeze # バーのベース文字列 DONE = "=".freeze # 済み記号 ARROW = ">".freeze # 済み記号の先端 # # initialize 初期化 # 引数 # title : プログレスバーのタイトル名 # total : プログレスバーの 100%の値 # def initialize(title, total) # インスタンス変数 定義 @title = title # 進捗表示する処理名(フォルダー名とか渡す) @total = total # 100% 時の値(単位は何でもいい。) @done = 0 # 完了した値 @showed_length = 0 # 直前に表示した文字列の長さ(上書きの際に使用する。) end # # show # プログレスバーの表示更新 # 引数 # info : バーに表示する情報文字列 # size : 進捗する値 # def show(info, size) # 処理したファイルサイズを加算し、進捗割合を計算 @done += size progress = get_progress(@total, @done) bar = BAR.dup # BAR の内容を複製(こうしないと BAR の値が変わってしまう。。) if progress >= 100 then # 進捗が 100% 以上の場合 bar[1..-2] = DONE*(BAR.length-2) # 両端を除く全てを DONE 記号で埋める。 else tmp = EDGE + (DONE*(progress*(SCALE.length)/100).floor).chop + ARROW # 進捗部分を生成して、 bar[0..tmp.length-1] = tmp # 進捗部分を置き換える。 end # 前回表示した行を消去 print "\r".ljust(@showed_length) # 情報表示 line = "\r" + sprintf("%s %s %s%% (%s/%s) %s...", @title[0,10], bar, progress, get_size_str(@done), get_size_str(@total), info[0,10]) print line @showed_length = line.length end # # reset # 進捗をリセットする。 # def reset @done = 0 @showed_length = 0 end private # # get_progress # 進捗度を割合計算し、100分率(%)で返す。 # 引数 # total : 100% となるサイズ # done : 実行済みのサイズ # 戻り値 # 進捗度(100分率の整数値。端数は四捨五入) # def get_progress(total, done) if done <= 0 then return 0 end # done が 0 以下の場合は、0 を返す。 if total <= 0 then return 100 end # total が 0 以下 : 100% として返す。 if total < done then return 101 end # total が done よりも小さい : 101% として返す。 return ((done*100)/total).round # 計算して返す。値は四捨五入で返す。 end # # get_size_str バイト数を KiB, MiB, GiB のうち、最適な単位に変換して返す。 # 引数 # bytes : バイト数 # 戻り値 # 最適な単位にしたバイト数と単位を付与した文字列 # def get_size_str(bytes) unit = ["Bytes", "KiB", "MiB", "GiB"] block = 1024 # KiB, MiB, GiB かどうかを判定する。 for n in 1..3 do if bytes.between?(block**n, block**(n+1)) then return ((bytes/block**n).floor).to_s + unit[n] end end # どの単位にも合致しない場合は、そのまま Bytes で返す。 return bytes.to_s + unit[0] end end
実装のポイント
コメントを多めに入れてるので、読んでいただければそのまま分かると思いますが、、、
ベースとなる 0%状態のバー文字列は、定数で最初に宣言しています。そして、加工のためにbar
変数に文字列の内容を複製しているのですが、理解されている方には分かり切ったことですが、代入では値の複製にならない、必ずdup
メソッドで値の複製をすること!です。
dup
メソッドの後、進捗部分の文字列を置き換える処理で、bar[1..-2]
のような書き方をしていますが、これが破壊的メソッドなので、line = BAR
のような代入をしてしまうと、BAR の値も同時に書き換わってしまいます。
また、新しいバーをそのまま上書きしてしまうと、もし前回表示した行の方が長かった場合、長かった分の文字列が残ってしまいます。ですので、@showed_length
変数を定義して表示した文字列の長さを取っておき、新たに表示するときにその長さの空白で上書きして、確実に消えるようにしています。(もちろん、\r
付きで上書きです)
後は、バイトの表示単位を変換するget_size_str
メソッドを作ったりしてるくらいですか。
例外処理については、ワザと変な引数(nil
とか型違いとか)を入れない限り、ロジック上で例外が発生する可能性がないため、このクラスでは行っていません。
次は・・・設定関係のクラスかな・・・