在C#中构建Windows控制台应用程序时,是否可以在不扩展当前行或转到新行的情况下写入控制台?例如,如果我想显示一个表示进程完成程度有多接近的百分比,我只想在光标所在的同一行上更新值,而不必将每个百分比放在新行上。
使用“标准”的C#控制台应用程序能否实现这一点?
在C#中构建Windows控制台应用程序时,是否可以在不扩展当前行或转到新行的情况下写入控制台?例如,如果我想显示一个表示进程完成程度有多接近的百分比,我只想在光标所在的同一行上更新值,而不必将每个百分比放在新行上。
使用“标准”的C#控制台应用程序能否实现这一点?
如果你只打印"\r"
到控制台,光标将返回当前行的开头,然后你就可以重写它。下面这个代码应该可以解决问题:
for(int i = 0; i < 100; ++i)
{
Console.Write("\r{0}% ", i);
}
注意数字后面的几个空格,以确保之前的内容被擦除。
同时注意使用Write()
而不是WriteLine()
,因为您不想在行末添加"\n"。
PadRight
的第一个参数输入(当然,首先要保存未填充的字符串或长度)。 - Jesper MatthiesenConsole.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);
}
}
请注意,您需要确保用新输出或空白覆盖任何现有输出。Console.SetCursorPosition(0, Console.CursorTop);
将光标设置在当前行的开头(或者您可以直接使用Console.CursorLeft = 0
)。
SetCursorPosition
(或 CursorLeft
)可以更灵活地解决问题,例如不只在行的开头写入,向窗口上方移动等等。因此,这是一种更通用的方法,可以用于输出自定义进度条或 ASCII 图形等内容。 - Dirk Vollmar4
之后将计数器重置为 0
可能是一个好主意,这样数字就永远不会溢出并导致意外行为。 - SeinopSys到目前为止,我们有三种竞争性的替代方案来完成这个任务:
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);
}
在我的机器上,我得到了以下结果:
此外,SetCursorPosition
会导致明显的闪烁,而我在其他两种方法中没有观察到。因此,教训是尽可能使用退格键或回车键,并且感谢Stack Overflow教给我一种更快的方法!
更新: 在评论中,Joel表明,相对于移动距离,SetCursorPosition是恒定的,而其他方法是线性的。进一步测试证实了这一点,但是恒定时间和缓慢仍然是缓慢的。在我的测试中,向控制台写入一长串退格键比SetCursorPosition快,直到大约60个字符左右。所以,对于替换长度小于60个字符(左右)的行部分,使用退格键更快,而且不会闪烁,因此我仍然支持使用\b而非\r和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
将光标返回到行首。
我刚才只是需要使用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)
的循环,并且我想同时允许多个旋转器,因此我不得不生成一个单独的线程来处理旋转。这意味着代码必须更加复杂。
另外,我并没有做过太多的多线程编程,所以其中可能会留下一些微妙的 bug。但到目前为止它似乎运行得相当不错:
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();
}
}
public void Update(string data)
{
Console.Write(string.Format("\r{0}", "".PadLeft(Console.CursorLeft, ' ')));
Console.Write(string.Format("\r{0}", data));
}
如果你想要在行的开头显式地使用回车符 (\r) 而不是(隐式或显式地)使用换行符 (\n),那么这样做应该可以得到你想要的结果。例如:
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"; 将标准错误输出流的行终止字符串设置为两个回车和换行序列。然后,您可以显式调用错误输出流对象的 WriteLine 方法,如 C# 语句 Console.Error.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);
}
}