C#でWindowsコンソールアプリケーションを構築する場合、現在の行を延長したり新しい行に移動したりせずにコンソールに書き込むことは可能ですか?たとえば、プロセスがどの程度完了しているかを表すパーセンテージを表示したい場合は、カーソルと同じ行の値を更新するだけでよく、各パーセンテージを新しい行に置く必要はありません。
これは「標準の」C#コンソールアプリでも可能ですか?
コンソールに"\r"
だけを表示すると、カーソルは現在の行の先頭に戻り、その後それを書き換えることができます。これでうまくいくはずです。
for(int i = 0; i < 100; ++i)
{
Console.Write("\r{0}% ", i);
}
番号の後の数個のスペースに注目して、前にあったものがすべて消去されていることを確認してください。
また、行末に "\ n"を付けたくないので、Write()
の代わりにWriteLine()
を使うことに注意してください。
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
を直接使用できます)。
これまでのところ、これを実行する方法について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
は、私がどちらの方法でも観察できなかった顕著なちらつきを引き起こしました。それで、道徳的には可能であればバックスペースまたはキャリッジリターンを使う、そして教えてくれてありがとうこれを行うには、そう!
Update:コメントの中で、JoelはSetCursorPositionは他のメソッドが線形である間は移動距離に関して一定であると示唆しています。さらなるテストはこれが事実であることを確認します、しかし一定時間そして遅いはまだ遅いです。私のテストでは、コンソールに長い文字列のバックスペースを書くことは、SetCursorPositionより60文字くらいまで速くなりました。そのため、バックスペースは60文字より短い行の部分を置換するのにはより速くなります(またはその程度)、およびそれはちらつきません。\rとSetCursorPosition
の上の\ bの支持。
現在の行の特定の数の文字をバックアップするには、\ 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
はあなたを一行の先頭に戻します。
私はただdivoのConsoleSpinner
クラスで遊ばなければなりませんでした。私の言うところはあまり簡潔ではありませんが、そのクラスのユーザーが独自の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)を使用するのではなく、行頭で明示的にキャリッジリターン(\ r)を使用すると、必要なものが得られます。例えば:
void demoPercentDone() {
for(int i = 0; i < 100; i++) {
System.Console.Write( "\rProcessing {0}%...", i );
System.Threading.Thread.Sleep( 1000 );
}
System.Console.WriteLine();
}
public void Update(string data)
{
Console.Write(string.Format("\r{0}", "".PadLeft(Console.CursorLeft, ' ')));
Console.Write(string.Format("\r{0}", data));
}
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:");
もう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
私が書いた解決策がスピードのために最適化されることができるかどうか見るために私はこれを捜していました。私が欲しかったのは、現在の行を更新するだけではなく、カウントダウンタイマーでした。これが私が思いついたものです。誰かに役立つかもしれません
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("");
これが私の話題と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();
}
}
使い方はこんな感じです。クラス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.");
}