如何在c# Windows控制台应用程序中更新当前行?

当在c#中构建Windows控制台应用程序时,是否有可能写入控制台而不必扩展当前行或去到新行?例如,如果我想显示一个百分比,表示一个流程离完成有多近,我只需要在游标所在的同一行上更新该值,而不必将每个百分比放在新行上。

这可以用“标准”c#控制台应用程序来实现吗?

269512 次浏览

您可以使用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);
}
}

请注意,您必须确保用新的输出或空白覆盖任何现有输出。

更新:有人批评这个例子只把光标向后移动了一个字符,我将添加这个来澄清:使用SetCursorPosition你可以将光标设置到控制台窗口的任何位置。

Console.SetCursorPosition(0, Console.CursorTop);

将光标设置到当前行的开头(或者你可以直接使用Console.CursorLeft = 0)。

如果你只在控制台打印"\r",光标就会回到当前行的开头,然后你就可以重写它了。这应该可以达到目的:

for(int i = 0; i < 100; ++i)
{
Console.Write("\r{0}%   ", i);
}

注意数字后面的几个空格,以确保之前的任何内容都被擦除 还要注意使用Write()而不是WriteLine(),因为您不想在行末添加“\n”

\r用于这些场景 # EYZ0 # EYZ1 < br > 这就是为什么Windows使用\n\r作为它的新行标记 \n向下移动一行,\r返回行开始

在行首显式地使用Carrage Return (\r),而不是(隐式或显式地)在行尾使用New line (\n),应该得到你想要的结果。例如:

void demoPercentDone() {
for(int i = 0; i < 100; i++) {
System.Console.Write( "\rProcessing {0}%...", i );
System.Threading.Thread.Sleep( 1000 );
}
System.Console.WriteLine();
}

您可以使用\ b (backspace)转义序列来备份当前行上特定数量的字符。这只是移动当前位置,并不移除字符。

例如:

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);
}

这里,是写入控制台的百分比线。诀窍是为前面的输出生成正确数量的\ b字符。

r \方法相比,这种方法的优点是即使输出百分比不在行开头,它也可以工作。

到目前为止,我们有三个相互竞争的替代方案:

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,第三个选项的变体,所以我决定做一些测试。下面是我使用的代码:

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);
}

在我的机器上,我得到了以下结果:

  • 退格:# EYZ0
  • 回车:28.7秒
  • SetCursorPosition: # EYZ0

此外,SetCursorPosition引起了明显的闪烁,我没有观察到任何一个替代方案。所以,寓意是尽可能使用退格或回车谢谢你教我是一种更快的方法,所以!


更新:在评论中,Joel建议SetCursorPosition相对于移动的距离是常数,而其他方法是线性的。进一步的测试证实了这一点,然而常数时间和慢仍然慢。在我的测试中,在控制台写入一长串退格符比SetCursorPosition快,直到大约60个字符。因此,退格在替换小于60个字符(或左右)的行部分时更快,而且它不会闪烁,所以我将坚持我最初对\b而不是\r和SetCursorPosition的支持。

我只需要玩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)类循环,并且我希望同时允许多个旋转器,我必须生成一个单独的线程来处理旋转。这意味着代码要复杂得多。

此外,我没有做那么多多线程,所以有可能(甚至可能)我在那里留下了一个或三个微妙的错误。但到目前为止,它似乎运行得很好:

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();
}
}

来自MSDN的控制台文档:

可以通过设置 TextWriter。属性的NewLine 属性转移到另一行 终止的字符串。例如, c#语句,Console.Error.NewLine = “\r\n\r\n”;,设置行终止 字符串用于标准错误输出 流到两车厢返回并行 饲料序列。然后你就可以 显式调用WriteLine方法 错误输出流对象的 在c#语句中, < / p > Console.Error.WriteLine ();

所以,我这样做了:

Console.Out.Newline = String.Empty;

然后我就可以自己控制输出了;

Console.WriteLine("Starting item 1:");
Item1();
Console.WriteLine("OK.\nStarting Item2:");

另一种方法。

如果要更新一行,但信息太长而不能在一行上显示,则可能需要一些新行。我遇到过这个问题,下面是解决这个问题的一种方法。

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");


}
}
    public void Update(string data)
{
Console.Write(string.Format("\r{0}", "".PadLeft(Console.CursorLeft, ' ')));
Console.Write(string.Format("\r{0}", data));
}
以下是我对s soosh和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.");


}
}

这是另一个选项:D

class Program
{
static void Main(string[] args)
{
Console.Write("Working... ");
int spinIndex = 0;
while (true)
{
// obfuscate FTW! Let's hope overflow is disabled or testers are impatient
Console.Write("\b" + @"/-\|"[(spinIndex++) & 3]);
}
}
}

SetCursorPosition方法适用于多线程场景,而其他两个方法则不能

我在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("");

如果你想让生成的文件看起来很酷,这是可行的。

                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);
}
}

灵感来自@E。拉湖解决方案,执行进度条用百分比。

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);
}
}