Как обновить текущую строку в консольном приложении C# Windows?
при создании консольного приложения Windows на C# можно ли писать на консоль без необходимости расширения текущей строки или перехода на новую строку? Например, если я хочу показать процент, представляющий, насколько близок процесс к завершению, я просто хотел бы обновить значение в той же строке, что и курсор, и не нужно помещать каждый процент в новую строку.
можно ли это сделать с помощью" стандартного " консольного приложения C#?
14 ответов
если вы печатаете только "\r"
в консоли курсор возвращается к началу текущей строки, а затем вы можете переписать его. Это должно сделать трюк:
for(int i = 0; i < 100; ++i)
{
Console.Write("\r{0}% ", i);
}
обратите внимание на несколько пробелов после номера, чтобы убедиться, что все, что было раньше стирается.
Также обратите внимание на использование Write()
вместо WriteLine()
так как вы не хотите добавлять "\n" в конце строки.
можно использовать 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);
}
}
обратите внимание, что вам нужно будет обязательно перезаписать любой существующий вывод новыми выводами или пробелами.
Update: поскольку критикуется, что пример перемещает курсор только на один символ, я добавлю это для уточнения: использование SetCursorPosition
вы можете установить курсор на любое положение в окне консоли.
Console.SetCursorPosition(0, Console.CursorTop);
устанавливает курсор в начало текущей строки (или вы можете использовать Console.CursorLeft = 0
напрямую).
до сих пор у нас есть три конкурирующих Альтернативы для того, как это сделать:
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);
}
на моей машине, я получаю следующие результаты:
- Backspaces: 25.0 секунд
- Возвраты Каретки: 28.7 секунд
- SetCursorPosition: 49.7 секунды!--14-->
кроме того, SetCursorPosition
вызвало заметное мерцание, которое я не наблюдал ни с одной из альтернатив. Итак, мораль в том, чтобы используйте backspaces или возврат каретки, когда это возможно и спасибо, что научил меня более быстрый способ сделать это, так!
обновление: в комментариях Джоэл предполагает, что SetCursorPosition постоянна относительно расстояния, перемещаемого, в то время как другие методы линейный. Дальнейшее тестирование подтверждает, что это так, постоянное время и медленно по-прежнему медленно. В моих тестах запись длинной строки обратных пространств на консоль происходит быстрее, чем SetCursorPosition, пока где-то около 60 символов. Таким образом, backspace быстрее для замены частей строки короче 60 символов (или около того), и он не мерцает, поэтому я буду стоять на своем первоначальном одобрении \b над \r и SetCursorPosition
.
можно использовать \b (backspace) escape-последовательность для резервного копирования определенного количества символов в текущей строке. Это просто перемещает текущее местоположение, оно не удаляет символы.
например:
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 подход заключается в том, что если работает даже если ваш процент вывода не в начале строки.
\r
используется для этих сценариев.\r
представляет возврат каретки, что означает, что курсор возвращается в начало строки.
Вот почему windows использует \n\r
как новый маркер линии.\n
перемещает вас вниз по линии, и \r
возвращает вас к началу строки.
мне просто пришлось играть с диво 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)
-like loop, и я хочу разрешить несколько прядильщиков одновременно, я должен был spawn отдельная нить для обработки спиннинга. А это значит, что код должен быть намного сложнее.
кроме того, я не сделал так много многопоточности, поэтому возможно (вероятно, даже), что я оставил там тонкую ошибку или три. Но, похоже, до сих пор это работает довольно хорошо:
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();
}
}
явное использование возврата Carrage (\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:
Вы можете решить эту проблему путем установки для textwriter.Свойство NewLine Свойство Out или Error для другой строки строка завершения. Например, С заявлением о#, консоли.Ошибка.новая строка= "\r\n\r\n";, устанавливает окончание строки строка для вывода стандартной ошибки поток к возвращению и линии экипажа 2 последовательности подачи. Тогда вы можете явно вызывать метод WriteLine объекта потока вывода ошибок, как В С# , Приставка.Ошибка.WriteLine();
Так я сделал это:
Console.Out.Newline = String.Empty;
тогда я могу контролировать выход сам;
Console.WriteLine("Starting item 1:");
Item1();
Console.WriteLine("OK.\nStarting Item2:");
другой способ добраться туда.
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();
}
}
использование-это что-то вроде этого. программа класса {
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.");
}
Если вы хотите обновить одну строку, но информация слишком длинная, чтобы показать на одной строке, может потребоваться несколько новых строк. Я столкнулся с этой проблемой, и ниже приведен один из способов ее решения.
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
вот еще один :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]);
}
}
}