コンソールアプリケーションにアクティビティインジケータを表示


概要

コンソールアプリケーションにおいて、重たい処理を実行中であることをユーザーに伝えるためのアクティビティインジケータ(ビジーマーク)を表示するプログラムをC#とF#で作成しました。

一定時間毎に...を表示するためにRx(Observable.Timer)を利用しました。

  • 処理中
  • 処理中.
  • 処理中..
  • 処理中...
  • 処理が終わるまで上記の表示の繰り返し、処理が完了したら下記の表示で上書き
  • 処理完了

C#版

nugetでSystem.Reactiveをインストールしておきます。

using System;
using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;

class Program {

  // アクティビティインジケータを出力する関数
  static void IndicationPrinter(string msg1, string msg2, int i) {
    Console.SetCursorPosition(0, Console.CursorTop);
    if (i < 0) {
      Console.WriteLine(msg2);
    } else {
      var n = i % 4;
      Console.Write($"{msg1}{new string('.', n)}{new string(' ', 3 - n)}");
    }
  }

  // インジケータを出力しながら関数funcをする非同期で実行する関数
  static Task<T> RunWithActivityIndicator<S, T>(string msg1, string msg2, Func<S, T> func, S arg) {

    var sTimer = Observable.Timer(TimeSpan.FromSeconds(0), TimeSpan.FromSeconds(0.2));
    var timerSubscription = sTimer.Subscribe(i =>
      IndicationPrinter(msg1, msg2, (int)i));

    return Task.Run(() => {
      return func(arg);
    }).ContinueWith((Task<T> t) => {
      timerSubscription.Dispose();
      IndicationPrinter(msg1, msg2, -1);
      return t.Result;
    });
  }

  static void Main(string[] args) {

    Console.WriteLine("C# Ver.");
    //Console.CursorVisible = false; //カーソルを消したい場合

    Func<string, string> heavyWeightFunc1 = (string arg) => {
      Thread.Sleep(2500);
      return $"=={arg}==";
    };

    // 非常に時間がかかる関数1 (Func<int>) 
    Func<int> heavyWeightFunc2 = () => {
      Thread.Sleep(1500);
      return 0;
    };

    var msg1 = "処理中";
    var msg2 = "処理完了";

    var task1 = RunWithActivityIndicator(msg1, msg2, heavyWeightFunc1, "hoge");
    Console.WriteLine($"処理結果:{task1.Result}");

    Console.ReadKey();
  }
}

F#版

nugetでFSharp.Control.Reactiveをインストールしておきます。

open System
open System.Reactive.Linq
open FSharp.Control.Reactive
open System.Threading
open System.Threading.Tasks

// アクティビティインジケータを出力する関数
let indicationPrinter msg1 msg2 i = 
  Console.SetCursorPosition(0, Console.CursorTop) |> ignore
  if i < 0 then
    printfn "%s" msg2
  else
    match i%4 with
    | 0 -> ignore( printf "%s" (msg1+"   "))
    | 1 -> ignore( printf "%s" (msg1+".  "))
    | 2 -> ignore( printf "%s" (msg1+".. "))
    | _ -> ignore( printf "%s" (msg1+"..."))

// インジケータを出力しながら関数funcをする非同期で実行する関数
let runWithActivityIndicator (msg1,msg2) (func:'S->'T) arg =

  let indicationPrinter = indicationPrinter msg1 msg2

  let sTimer = Observable.Timer(TimeSpan.FromSeconds(0.),TimeSpan.FromSeconds(0.2))
  let timerSubscription = sTimer.Subscribe( fun i -> int(i) |> indicationPrinter  )

  let task = async { return func arg } |> Async.StartAsTask
  task.ContinueWith( fun (t:Task<'T>) -> 
    timerSubscription.Dispose()
    indicationPrinter -1
    t.Result ) 

[<EntryPoint>]
let main argv =

  printfn "F# Ver."
  //Console.CursorVisible <- false; // カーソルを消したい場合

  // 非常に時間がかかる関数1 (string->string) 
  let heavyWeightFunc1 arg =
    Thread.Sleep(1500)
    "==" + arg + "=="

  // 非常に時間がかかる関数2 (unit->string) 
  let heavyWeightFunc2 () =
    Thread.Sleep(2500)
    0

  // 非常に時間がかかる関数3 (unit->unit) 
  let heavyWeightFunc3 () =
    Thread.Sleep(1000)

  let msg = ("処理中","処理完了")  
  let task1 = runWithActivityIndicator msg heavyWeightFunc1 "hoge"
  printfn "%s" ("処理結果 : " + task1.Result )

  let task2 = runWithActivityIndicator msg heavyWeightFunc2 ()
  printfn "%s" ("処理結果 : " + (task2.Result.ToString()) )

  let task3 = runWithActivityIndicator msg heavyWeightFunc3 ()
  task3.Wait()
  printfn "." 

  Console.ReadKey() |> ignore
  0

参考資料