C#でWindowsコンソールアプリを構築する場合、現在の行を拡張したり新しい行に移動したりすることなく、コンソールに書き込むことはできますか?たとえば、プロセスがどれだけ完了しているかを表すパーセンテージを表示したい場合は、カーソルと同じ行の値を更新するだけで、各パーセンテージを新しい行に置く必要はありません。
これは「標準」のC#コンソールアプリで実行できますか?
C#でWindowsコンソールアプリを構築する場合、現在の行を拡張したり新しい行に移動したりすることなく、コンソールに書き込むことはできますか?たとえば、プロセスがどれだけ完了しているかを表すパーセンテージを表示したい場合は、カーソルと同じ行の値を更新するだけで、各パーセンテージを新しい行に置く必要はありません。
これは「標準」のC#コンソールアプリで実行できますか?
回答:
"\r"
コンソールのみに出力する場合、カーソルは現在の行の先頭に戻り、それを書き直すことができます。これでうまくいくはずです:
for(int i = 0; i < 100; ++i)
{
Console.Write("\r{0}% ", i);
}
番号の後ろのいくつかのスペースに注目して、以前にあったものがすべて消去されることを確認してください。
また、行の終わりに「\ n」を追加したくないので、Write()
代わりにを使用していることに注意しWriteLine()
てください。
PadRight
、パディングされていない文字列または長さを最初に保存します)。
を使用Console.SetCursorPosition
してカーソルの位置を設定し、現在の位置に書き込むことができます。
以下は、単純な「スピナー」を示す例です。
static void Main(string[] args)
{
var spin = new ConsoleSpinner();
Console.Write("Working....");
while (true)
{
spin.Turn();
}
}
public class ConsoleSpinner
{
int counter;
public void Turn()
{
counter++;
switch (counter % 4)
{
case 0: Console.Write("/"); counter = 0; break;
case 1: Console.Write("-"); break;
case 2: Console.Write("\\"); break;
case 3: Console.Write("|"); break;
}
Thread.Sleep(100);
Console.SetCursorPosition(Console.CursorLeft - 1, Console.CursorTop);
}
}
既存の出力を新しい出力または空白で必ず上書きする必要があることに注意してください。
更新:この例ではカーソルを1文字だけ戻すと批判されているので、わかりやすくするためにこれを追加しSetCursorPosition
ます。を使用して、カーソルをコンソールウィンドウの任意の位置に設定できます。
Console.SetCursorPosition(0, Console.CursorTop);
カーソルを現在の行の先頭に設定します(またはConsole.CursorLeft = 0
直接使用できます)。
SetCursorPosition
(またはCursorLeft
)を使用すると、たとえば行の先頭に書き込まない、ウィンドウ内で上に移動するなど、より柔軟に使用できるため、出力などに使用できるより一般的なアプローチです。カスタムプログレスバーまたはASCIIグラフィック。
これまでのところ、これを行う方法について3つの競合する選択肢があります。
Console.Write("\r{0} ", value); // Option 1: carriage return
Console.Write("\b\b\b\b\b{0}", value); // Option 2: backspace
{ // Option 3 in two parts:
Console.SetCursorPosition(0, Console.CursorTop); // - Move cursor
Console.Write(value); // - Rewrite
}
私は常にConsole.CursorLeft = 0
3番目のオプションのバリエーションであるを使用してきたため、いくつかのテストを行うことにしました。これが私が使ったコードです:
public static void CursorTest()
{
int testsize = 1000000;
Console.WriteLine("Testing cursor position");
Stopwatch sw = new Stopwatch();
sw.Start();
for (int i = 0; i < testsize; i++)
{
Console.Write("\rCounting: {0} ", i);
}
sw.Stop();
Console.WriteLine("\nTime using \\r: {0}", sw.ElapsedMilliseconds);
sw.Reset();
sw.Start();
int top = Console.CursorTop;
for (int i = 0; i < testsize; i++)
{
Console.SetCursorPosition(0, top);
Console.Write("Counting: {0} ", i);
}
sw.Stop();
Console.WriteLine("\nTime using CursorLeft: {0}", sw.ElapsedMilliseconds);
sw.Reset();
sw.Start();
Console.Write("Counting: ");
for (int i = 0; i < testsize; i++)
{
Console.Write("\b\b\b\b\b\b\b\b{0,8}", i);
}
sw.Stop();
Console.WriteLine("\nTime using \\b: {0}", sw.ElapsedMilliseconds);
}
私のマシンでは、次の結果が得られます。
さらに、SetCursorPosition
どちらの方法でも見られなかった顕著なちらつきを引き起こしました。したがって、道徳は可能な限りバックスペースまたは改行を使用することであり、これを行うためのより速い方法を教えてくれてありがとう、SO!
更新:コメントで、JoelはSetCursorPositionが移動距離に対して一定であり、他のメソッドは線形であると提案しています。これが事実である、ことをさらに試験確認が遅い時定数とは依然として遅いです。私のテストでは、バックスペースの長い文字列をコンソールに書き込む方が、約60文字になるまでSetCursorPositionよりも高速です。60文字(程度)よりも短いラインの一部を置き換えるために高速です、バックスペースはそうと、それがちらつきませんので、私はR \の上にB \の私の最初の裏書に立つつもりだとSetCursorPosition
。
\ b(バックスペース)エスケープシーケンスを使用して、現在の行の特定の数の文字をバックアップできます。これは現在の場所を移動するだけで、文字は削除されません。
例えば:
string line="";
for(int i=0; i<100; i++)
{
string backup=new string('\b',line.Length);
Console.Write(backup);
line=string.Format("{0}%",i);
Console.Write(line);
}
ここで、lineはコンソールに書き込むパーセント行です。トリックは、前の出力に対して正しい数の\ b文字を生成することです。
\ rアプローチに対するこれの利点は、パーセンテージ出力が行の先頭にない場合でも機能することです。
\r
これらのシナリオに使用されます。
\r
キャリッジリターンを表します。これは、カーソルが行の先頭に戻ることを意味します。
これが、Windowsが\n\r
新しい行マーカーとして使用する理由です。
\n
行を下に移動し、行\r
の先頭に戻ります。
私は歌手のConsoleSpinner
クラスで遊んだだけでした。Mineはそれほど簡潔ではありませんが、そのクラスのユーザーが独自のwhile(true)
ループを記述しなければならないというのは、私とうまく調和していませんでした。私はこのような経験のために撮影しています:
static void Main(string[] args)
{
Console.Write("Working....");
ConsoleSpinner spin = new ConsoleSpinner();
spin.Start();
// Do some work...
spin.Stop();
}
そして、私はそれを以下のコードで実現しました。私はしたくないので、Start()
ブロックにする方法を、私は、ユーザーが書き込みを心配する必要はしたくないwhile(spinFlag)
様ループを、と私は私が処理するために別のスレッドを生成しなければならなかった、同時に複数のスピナーを許可します紡糸。そして、それはコードがはるかに複雑でなければならないことを意味します。
また、私はそれほど多くのマルチスレッド処理を行っていないため、微妙なバグを1つまたは3つ残した可能性があります(たぶん均等です)。しかし、これまでのところかなりうまくいくようです:
public class ConsoleSpinner : IDisposable
{
public ConsoleSpinner()
{
CursorLeft = Console.CursorLeft;
CursorTop = Console.CursorTop;
}
public ConsoleSpinner(bool start)
: this()
{
if (start) Start();
}
public void Start()
{
// prevent two conflicting Start() calls ot the same instance
lock (instanceLocker)
{
if (!running )
{
running = true;
turner = new Thread(Turn);
turner.Start();
}
}
}
public void StartHere()
{
SetPosition();
Start();
}
public void Stop()
{
lock (instanceLocker)
{
if (!running) return;
running = false;
if (! turner.Join(250))
turner.Abort();
}
}
public void SetPosition()
{
SetPosition(Console.CursorLeft, Console.CursorTop);
}
public void SetPosition(int left, int top)
{
bool wasRunning;
//prevent other start/stops during move
lock (instanceLocker)
{
wasRunning = running;
Stop();
CursorLeft = left;
CursorTop = top;
if (wasRunning) Start();
}
}
public bool IsSpinning { get { return running;} }
/* --- PRIVATE --- */
private int counter=-1;
private Thread turner;
private bool running = false;
private int rate = 100;
private int CursorLeft;
private int CursorTop;
private Object instanceLocker = new Object();
private static Object console = new Object();
private void Turn()
{
while (running)
{
counter++;
// prevent two instances from overlapping cursor position updates
// weird things can still happen if the main ui thread moves the cursor during an update and context switch
lock (console)
{
int OldLeft = Console.CursorLeft;
int OldTop = Console.CursorTop;
Console.SetCursorPosition(CursorLeft, CursorTop);
switch (counter)
{
case 0: Console.Write("/"); break;
case 1: Console.Write("-"); break;
case 2: Console.Write("\\"); break;
case 3: Console.Write("|"); counter = -1; break;
}
Console.SetCursorPosition(OldLeft, OldTop);
}
Thread.Sleep(rate);
}
lock (console)
{ // clean up
int OldLeft = Console.CursorLeft;
int OldTop = Console.CursorTop;
Console.SetCursorPosition(CursorLeft, CursorTop);
Console.Write(' ');
Console.SetCursorPosition(OldLeft, OldTop);
}
}
public void Dispose()
{
Stop();
}
}
改行(\ n)を最後に(暗黙的または明示的に)使用するのではなく、行の先頭に明示的にCarrage Return(\ r)を使用すると、必要な結果が得られます。例えば:
void demoPercentDone() {
for(int i = 0; i < 100; i++) {
System.Console.Write( "\rProcessing {0}%...", i );
System.Threading.Thread.Sleep( 1000 );
}
System.Console.WriteLine();
}
MSDNのコンソールドキュメントから:
この問題を解決するには、OutプロパティまたはErrorプロパティのTextWriter.NewLineプロパティを別の行終端文字列に設定します。たとえば、C#ステートメントConsole.Error.NewLine = "\ r \ n \ r \ n";は、標準エラー出力ストリームの行終端文字列を2つのキャリッジリターンとラインフィードシーケンスに設定します。次に、C#ステートメントのConsole.Error.WriteLine();のように、エラー出力ストリームオブジェクトのWriteLineメソッドを明示的に呼び出すことができます。
だから-私はこれをしました:
Console.Out.Newline = String.Empty;
その後、自分で出力を制御できます。
Console.WriteLine("Starting item 1:");
Item1();
Console.WriteLine("OK.\nStarting Item2:");
そこに到達する別の方法。
これは、生成ファイルをクールに見せたい場合に機能します。
int num = 1;
var spin = new ConsoleSpinner();
Console.ForegroundColor = ConsoleColor.Green;
Console.Write("");
while (true)
{
spin.Turn();
Console.Write("\r{0} Generating Files ", num);
num++;
}
そして、これは私が以下のいくつかの答えから得てそれを変更した方法です
public class ConsoleSpinner
{
int counter;
public void Turn()
{
counter++;
switch (counter % 4)
{
case 0: Console.Write("."); counter = 0; break;
case 1: Console.Write(".."); break;
case 2: Console.Write("..."); break;
case 3: Console.Write("...."); break;
case 4: Console.Write("\r"); break;
}
Thread.Sleep(100);
Console.SetCursorPosition(23, Console.CursorTop);
}
}
1行を更新したいが、情報が長すぎて1行に表示できない場合は、新しい行が必要になることがあります。私はこの問題に遭遇しました。以下はこれを解決する1つの方法です。
public class DumpOutPutInforInSameLine
{
//content show in how many lines
int TotalLine = 0;
//start cursor line
int cursorTop = 0;
// use to set character number show in one line
int OneLineCharNum = 75;
public void DumpInformation(string content)
{
OutPutInSameLine(content);
SetBackSpace();
}
static void backspace(int n)
{
for (var i = 0; i < n; ++i)
Console.Write("\b \b");
}
public void SetBackSpace()
{
if (TotalLine == 0)
{
backspace(OneLineCharNum);
}
else
{
TotalLine--;
while (TotalLine >= 0)
{
backspace(OneLineCharNum);
TotalLine--;
if (TotalLine >= 0)
{
Console.SetCursorPosition(OneLineCharNum, cursorTop + TotalLine);
}
}
}
}
private void OutPutInSameLine(string content)
{
//Console.WriteLine(TotalNum);
cursorTop = Console.CursorTop;
TotalLine = content.Length / OneLineCharNum;
if (content.Length % OneLineCharNum > 0)
{
TotalLine++;
}
if (TotalLine == 0)
{
Console.Write("{0}", content);
return;
}
int i = 0;
while (i < TotalLine)
{
int cNum = i * OneLineCharNum;
if (i < TotalLine - 1)
{
Console.WriteLine("{0}", content.Substring(cNum, OneLineCharNum));
}
else
{
Console.Write("{0}", content.Substring(cNum, content.Length - cNum));
}
i++;
}
}
}
class Program
{
static void Main(string[] args)
{
DumpOutPutInforInSameLine outPutInSameLine = new DumpOutPutInforInSameLine();
outPutInSameLine.DumpInformation("");
outPutInSameLine.DumpInformation("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
outPutInSameLine.DumpInformation("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
outPutInSameLine.DumpInformation("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
//need several lines
outPutInSameLine.DumpInformation("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
outPutInSameLine.DumpInformation("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
outPutInSameLine.DumpInformation("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
outPutInSameLine.DumpInformation("bbbbbbbbbbbbbbbbbbbbbbbbbbb");
}
}
私はvb.netで同じ解決策を探していましたが、これを見つけて素晴らしいです。
ただし、@ JohnOdomは、前のスペースが現在のスペースよりも大きい場合に空白スペースを処理するためのより良い方法を提案しました。
私はvb.netで関数を作成し、誰かが助けてもらえると思った。
これが私のコードです:
Private Sub sPrintStatus(strTextToPrint As String, Optional boolIsNewLine As Boolean = False)
REM intLastLength is declared as public variable on global scope like below
REM intLastLength As Integer
If boolIsNewLine = True Then
intLastLength = 0
End If
If intLastLength > strTextToPrint.Length Then
Console.Write(Convert.ToChar(13) & strTextToPrint.PadRight(strTextToPrint.Length + (intLastLength - strTextToPrint.Length), Convert.ToChar(" ")))
Else
Console.Write(Convert.ToChar(13) & strTextToPrint)
End If
intLastLength = strTextToPrint.Length
End Sub
Static intLastLength As Integer
。
私はこれを検索して、私が書いたソリューションが速度を最適化できるかどうかを確認しました。私が欲しかったのは、現在のラインを更新するだけではなく、カウントダウンタイマーでした。これが私が思いついたものです。誰かに役立つかもしれません
int sleepTime = 5 * 60; // 5 minutes
for (int secondsRemaining = sleepTime; secondsRemaining > 0; secondsRemaining --)
{
double minutesPrecise = secondsRemaining / 60;
double minutesRounded = Math.Round(minutesPrecise, 0);
int seconds = Convert.ToInt32((minutesRounded * 60) - secondsRemaining);
Console.Write($"\rProcess will resume in {minutesRounded}:{String.Format("{0:D2}", -seconds)} ");
Thread.Sleep(1000);
}
Console.WriteLine("");
@ E.Lahu Solutionに触発され、パーセンテージでバーの進行状況が実装されます。
public class ConsoleSpinner
{
private int _counter;
public void Turn(Color color, int max, string prefix = "Completed", string symbol = "■",int position = 0)
{
Console.SetCursorPosition(0, position);
Console.Write($"{prefix} {ComputeSpinner(_counter, max, symbol)}", color);
_counter = _counter == max ? 0 : _counter + 1;
}
public string ComputeSpinner(int nmb, int max, string symbol)
{
var spinner = new StringBuilder();
if (nmb == 0)
return "\r ";
spinner.Append($"[{nmb}%] [");
for (var i = 0; i < max; i++)
{
spinner.Append(i < nmb ? symbol : ".");
}
spinner.Append("]");
return spinner.ToString();
}
}
public static void Main(string[] args)
{
var progressBar= new ConsoleSpinner();
for (int i = 0; i < 1000; i++)
{
progressBar.Turn(Color.Aqua,100);
Thread.Sleep(1000);
}
}
これが、s soshと0xA3の答えに対する私の見解です。スピナーの更新中にコンソールをユーザーメッセージで更新でき、経過時間インジケーターも表示されます。
public class ConsoleSpiner : IDisposable
{
private static readonly string INDICATOR = "/-\\|";
private static readonly string MASK = "\r{0} {1:c} {2}";
int counter;
Timer timer;
string message;
public ConsoleSpiner() {
counter = 0;
timer = new Timer(200);
timer.Elapsed += TimerTick;
}
public void Start() {
timer.Start();
}
public void Stop() {
timer.Stop();
counter = 0;
}
public string Message {
get { return message; }
set { message = value; }
}
private void TimerTick(object sender, ElapsedEventArgs e) {
Turn();
}
private void Turn() {
counter++;
var elapsed = TimeSpan.FromMilliseconds(counter * 200);
Console.Write(MASK, INDICATOR[counter % 4], elapsed, this.Message);
}
public void Dispose() {
Stop();
timer.Elapsed -= TimerTick;
this.timer.Dispose();
}
}
使用法は次のようなものです:
class Program
{
static void Main(string[] args)
{
using (var spinner = new ConsoleSpiner())
{
spinner.Start();
spinner.Message = "About to do some heavy staff :-)"
DoWork();
spinner.Message = "Now processing other staff".
OtherWork();
spinner.Stop();
}
Console.WriteLine("COMPLETED!!!!!\nPress any key to exit.");
}
}