控制台应用程序中的进度条

106

我正在编写一个简单的C#控制台应用程序,用于将文件上传到SFTP服务器。然而,文件数量很大。我想要显示已上传的文件数量与待上传总文件数之比,或者仅显示已上传的文件数量。

首先,我获取所有文件和文件的总数。

string[] filePath = Directory.GetFiles(path, "*");
totalCount = filePath.Length;

然后我使用foreach循环逐个上传文件。

foreach(string file in filePath)
{
    string FileName = Path.GetFileName(file);
    //copy the files
    oSftp.Put(LocalDirectory + "/" + FileName, _ftpDirectory + "/" + FileName);
    //Console.WriteLine("Uploading file..." + FileName);
    drawTextProgressBar(0, totalCount);
}
在foreach循环中,我有一个进度条,但它存在问题,无法正确显示。
private static void drawTextProgressBar(int progress, int total)
{
    //draw empty progress bar
    Console.CursorLeft = 0;
    Console.Write("["); //start
    Console.CursorLeft = 32;
    Console.Write("]"); //end
    Console.CursorLeft = 1;
    float onechunk = 30.0f / total;

    //draw filled part
    int position = 1;
    for (int i = 0; i < onechunk * progress; i++)
    {
        Console.BackgroundColor = ConsoleColor.Gray;
        Console.CursorLeft = position++;
        Console.Write(" ");
    }

    //draw unfilled part
    for (int i = position; i <= 31 ; i++)
    {
        Console.BackgroundColor = ConsoleColor.Green;
        Console.CursorLeft = position++;
        Console.Write(" ");
    }

    //draw totals
    Console.CursorLeft = 35;
    Console.BackgroundColor = ConsoleColor.Black;
    Console.Write(progress.ToString() + " of " + total.ToString() + "    "); //blanks at the end remove any excess
}

输出只是[ ] 0/1943

我在这里做错了什么?

编辑:

我试图在加载和导出XML文件时显示进度条。 但它正在经过一个循环。 它完成第一轮后,就会进入第二轮,依此类推。

string[] xmlFilePath = Directory.GetFiles(xmlFullpath, "*.xml");
Console.WriteLine("Loading XML files...");
foreach (string file in xmlFilePath)
{
     for (int i = 0; i < xmlFilePath.Length; i++)
     {
          //ExportXml(file, styleSheet);
          drawTextProgressBar(i, xmlCount);
          count++;
     }
 }

它从未离开for循环...有任何建议吗?


xmlCount和count是什么? - eddie_cat
1
另外,为什么for循环在foreach循环内部?它似乎正在迭代相同的内容。保留foreach循环可能是不必要的。 - eddie_cat
xmlFilePath.Length给了我相同的输出。代码没有出错。目前文件夹中有1493个XML文件。一旦达到1493,i就会重新从0开始。 - smr5
1
你移除了外层的foreach循环吗?然后将注释掉的部分改为ExportXml(xmlFilePath[i]) - eddie_cat
1
就是这样。我只有一个for循环,它可以工作。 - smr5
显示剩余2条评论
11个回答

261

我也在寻找一个控制台进度条,但没有找到一个满足我的需求,所以我决定自己写一个。点击此处获取源代码(MIT许可证)。

动态进度条

功能:

  • 适用于重定向输出

    如果您重定向了控制台应用程序的输出(例如,Program.exe > myfile.txt),大多数实现都会崩溃并引发异常。这是因为 Console.CursorLeftConsole.SetCursorPosition() 不支持重定向输出。

  • 实现了 IProgress<double>

    这使您可以将进度条与异步操作一起使用,它们会报告 [0..1] 范围内的进度。

  • 线程安全

  • 快速

    Console 类因其慢得可怕的性能而臭名昭著。调用次数太多会导致应用程序变慢。这个类每秒只执行8次调用,无论您多频繁地报告进度更新。

使用方法:

Console.Write("Performing some task... ");
using (var progress = new ProgressBar()) {
    for (int i = 0; i <= 100; i++) {
        progress.Report((double) i / 100);
        Thread.Sleep(20);
    }
}
Console.WriteLine("Done.");

4
这看起来非常不错!您是否考虑为其添加MIT等开源软件许可证?http://choosealicense.com/ - Daniel Plaisted
2
好主意。已完成。 - Daniel Wolf
@DanielWolf 你是怎么通过改变光标位置来实现 Console.Write 的呢? - JJS
1
@knocte:在生产代码中,我肯定会这样做。这里的目标是尽可能简洁地保持示例,不要分散注意力。 - Daniel Wolf
12
这个 gif 很吸引人。 - Lei Yang
显示剩余8条评论

32
我知道这是一个旧的线程,抱歉自我推广,但我最近编写了一个开源控制台库,可在nuget Goblinfactory.Konsole 上使用,支持多线程安全的进度条,可能会帮助到需要不阻塞主线程的新用户。
与上面的答案有些不同,它允许您并行启动下载和任务,并继续进行其他任务;
祝福,希望这有所帮助。

A

var t1 = Task.Run(()=> {
   var p = new ProgressBar("downloading music",10);
   ... do stuff
});

var t2 = Task.Run(()=> {
   var p = new ProgressBar("downloading video",10);
   ... do stuff
});

var t3 = Task.Run(()=> {
   var p = new ProgressBar("starting server",10);
   ... do stuff .. calling p.Refresh(n);
});

Task.WaitAll(new [] { t1,t2,t3 }, 20000);
Console.WriteLine("all done.");

给出这种类型的输出

enter image description here

该NuGet包还包括一些实用程序,可用于在控制台的窗口化部分中进行编写,并提供完整的剪切和换行支持,以及PrintAt和其他各种有用的类。
我编写了这个NuGet包,因为每当我编写构建和操作控制台脚本和实用程序时,我总是不断地编写许多常见的控制台例程。
例如,如果我要下载几个文件,我过去会在每个线程上缓慢地Console.Write到屏幕上,并尝试各种技巧使读取屏幕上交错的输出更容易阅读,例如使用不同的颜色或数字。最终,我编写了窗口库,以便可以将来自不同线程的输出简单地打印到不同的窗口中,并且它减少了我的实用程序脚本中大量的样板代码。
例如,以下代码:
        var con = new Window(200,50);
        con.WriteLine("starting client server demo");
        var client = new Window(1, 4, 20, 20, ConsoleColor.Gray, ConsoleColor.DarkBlue, con);
        var server = new Window(25, 4, 20, 20, con);
        client.WriteLine("CLIENT");
        client.WriteLine("------");
        server.WriteLine("SERVER");
        server.WriteLine("------");
        client.WriteLine("<-- PUT some long text to show wrapping");
        server.WriteLine(ConsoleColor.DarkYellow, "--> PUT some long text to show wrapping");
        server.WriteLine(ConsoleColor.Red, "<-- 404|Not Found|some long text to show wrapping|");
        client.WriteLine(ConsoleColor.Red, "--> 404|Not Found|some long text to show wrapping|");

        con.WriteLine("starting names demo");
        // let's open a window with a box around it by using Window.Open
        var names = Window.Open(50, 4, 40, 10, "names");
        TestData.MakeNames(40).OrderByDescending(n => n).ToList()
             .ForEach(n => names.WriteLine(n));

        con.WriteLine("starting numbers demo");
        var numbers = Window.Open(50, 15, 40, 10, "numbers", 
              LineThickNess.Double,ConsoleColor.White,ConsoleColor.Blue);
        Enumerable.Range(1,200).ToList()
             .ForEach(i => numbers.WriteLine(i.ToString())); // shows scrolling

产生这个

enter image description here

你可以像写入窗口一样轻松地在窗口内创建进度条(混合和匹配)。

这只是最好的。 - Pratik

17

你可能希望尝试https://www.nuget.org/packages/ShellProgressBar/

我刚刚偶然发现了这个进度条实现 - 它是跨平台的,真的很容易使用,非常可配置,并且直接开箱即用。

只是分享一下,因为我非常喜欢它。


13

这一行是你的问题:

drawTextProgressBar(0, totalCount);
你在说每一轮迭代中进度都是零,应该把它增加。也许可以使用for循环代替。
for (int i = 0; i < filePath.length; i++)
{
    string FileName = Path.GetFileName(filePath[i]);
    //copy the files
    oSftp.Put(LocalDirectory + "/" + FileName, _ftpDirectory + "/" + FileName);
    //Console.WriteLine("Uploading file..." + FileName);
    drawTextProgressBar(i, totalCount);
}

第一次它运行正常,但是在同一个地方做同样的事情会导致循环,永远停不下来。我已经更新了我的帖子,请你看一下,谢谢。 - smr5
你更新了什么?对我来说看起来没有变化,我错过了什么吗? - eddie_cat

10

我很喜欢原始海报的进度条,但发现它在某些进度/总项目组合下无法正确显示进度。例如,以下情况无法正确绘制,导致进度条末尾多出一个灰色块:

drawTextProgressBar(4114, 4114)

我重新编写了一些绘图代码,去掉了不必要的循环,解决了上述问题,并且大大提高了速度:

public static void drawTextProgressBar(string stepDescription, int progress, int total)
{
    int totalChunks = 30;

    //draw empty progress bar
    Console.CursorLeft = 0;
    Console.Write("["); //start
    Console.CursorLeft = totalChunks + 1;
    Console.Write("]"); //end
    Console.CursorLeft = 1;

    double pctComplete = Convert.ToDouble(progress) / total;
    int numChunksComplete = Convert.ToInt16(totalChunks * pctComplete);

    //draw completed chunks
    Console.BackgroundColor = ConsoleColor.Green;
    Console.Write("".PadRight(numChunksComplete));

    //draw incomplete chunks
    Console.BackgroundColor = ConsoleColor.Gray;
    Console.Write("".PadRight(totalChunks - numChunksComplete));

    //draw totals
    Console.CursorLeft = totalChunks + 5;
    Console.BackgroundColor = ConsoleColor.Black;

    string output = progress.ToString() + " of " + total.ToString();
    Console.Write(output.PadRight(15) + stepDescription); //pad the output so when changing from 3 to 4 digits we avoid text shifting
}

这通常是有效的,但它会删除先前的控制台输出,例如在此之前的任何文本,并且在此之后不会换行.... - David Shnayder
1
我喜欢这个实现。有趣的事情是使用条件文本,让它更加有趣。所以如果进度%10为3,则输出“-”,如果是模6,则输出“|”。我还使用进度条唱歌,通过使用子字符串 $"{lyrics}{lyrics}".substring(progress % lyrics.length, 20)。这很容易,你不需要一个包或API,并且由于它如此简单,你可以玩得开心。非常好的贡献。 - John Harrison

7
C#控制台进度条(完整代码 - 只需复制并粘贴到您的IDE中)
class ConsoleUtility
{
    const char _block = '■';
    const string _back = "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b";
    const string _twirl = "-\\|/";

    public static void WriteProgressBar(int percent, bool update = false)
    {
        if (update)
            Console.Write(_back);
        Console.Write("[");
        var p = (int)((percent / 10f) + .5f);
        for (var i = 0; i < 10; ++i)
        {
            if (i >= p)
                Console.Write(' ');
            else
                Console.Write(_block);
        }
        Console.Write("] {0,3:##0}%", percent);
    }

    public static void WriteProgress(int progress, bool update = false)
    {
        if (update)
            Console.Write("\b");
        Console.Write(_twirl[progress % _twirl.Length]);
    }
}


//This is how to call in your main method
static void Main(string[] args)
{
    ConsoleUtility.WriteProgressBar(0);
    for (var i = 0; i <= 100; ++i)
    {
       ConsoleUtility.WriteProgressBar(i, true);
       Thread.Sleep(50);
    }

    Console.WriteLine();
    ConsoleUtility.WriteProgress(0);
    for (var i = 0; i <= 100; ++i)
    {
        ConsoleUtility.WriteProgress(i, true);
        Thread.Sleep(50);
    }
}

Output: C# Progress Bar


6

我复制粘贴了你的 ProgressBar 方法。因为你的错误在循环中,正如被接受的答案所提到的那样。但是 ProgressBar 方法也有一些语法错误。这是一个可工作的版本。稍作修改即可。

private static void ProgressBar(int progress, int tot)
{
    //draw empty progress bar
    Console.CursorLeft = 0;
    Console.Write("["); //start
    Console.CursorLeft = 32;
    Console.Write("]"); //end
    Console.CursorLeft = 1;
    float onechunk = 30.0f / tot;

    //draw filled part
    int position = 1;
    for (int i = 0; i < onechunk * progress; i++)
    {
        Console.BackgroundColor = ConsoleColor.Green;
        Console.CursorLeft = position++;
        Console.Write(" ");
    }

    //draw unfilled part
    for (int i = position; i <= 31; i++)
    {
        Console.BackgroundColor = ConsoleColor.Gray;
        Console.CursorLeft = position++;
        Console.Write(" ");
    }

    //draw totals
    Console.CursorLeft = 35;
    Console.BackgroundColor = ConsoleColor.Black;
    Console.Write(progress.ToString() + " of " + tot.ToString() + "    "); //blanks at the end remove any excess
}

请注意,@Daniel-wolf有更好的方法:https://dev59.com/1mAf5IYBdhLWcg3wMwTX#31193455

5
我已经创建了一个方便的类,可以与System.Reactive一起使用。希望你会喜欢它。
public class ConsoleDisplayUpdater : IDisposable
{
    private readonly IDisposable progressUpdater;

    public ConsoleDisplayUpdater(IObservable<double> progress)
    {
        progressUpdater = progress.Subscribe(DisplayProgress);
    }

    public int Width { get; set; } = 50;

    private void DisplayProgress(double progress)
    {
        if (double.IsNaN(progress))
        {
            return;
        }

        var progressBarLenght = progress * Width;
        System.Console.CursorLeft = 0;
        System.Console.Write("[");
        var bar = new string(Enumerable.Range(1, (int) progressBarLenght).Select(_ => '=').ToArray());

        System.Console.Write(bar);

        var label = $@"{progress:P0}";
        System.Console.CursorLeft = (Width -label.Length) / 2;
        System.Console.Write(label);
        System.Console.CursorLeft = Width;
        System.Console.Write("]");
    }

    public void Dispose()
    {
        progressUpdater?.Dispose();
    }
}

0

我刚好在寻找其他东西时偶然发现了这个线程,所以想分享一下我编写的代码,使用DownloadProgressChanged下载文件列表。我觉得这非常有帮助,因为我不仅可以看到进度条,还可以看到文件正在传输时的实际大小。希望对某些人有所帮助!

public static bool DownloadFile(List<string> files, string host, string username, string password, string savePath)
    {
        try
        {
            //setup FTP client

            foreach (string f in files)
            {
                FILENAME = f.Split('\\').Last();
                wc.DownloadFileCompleted += new AsyncCompletedEventHandler(Completed);
                wc.DownloadProgressChanged += new DownloadProgressChangedEventHandler(ProgressChanged);
                wc.DownloadFileAsync(new Uri(host + f), savePath + f);
                while (wc.IsBusy)
                    System.Threading.Thread.Sleep(1000);
                Console.Write("  COMPLETED!");
                Console.WriteLine();
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.ToString());
            return false;
        }
        return true;
    }

    private static void ProgressChanged(object obj, System.Net.DownloadProgressChangedEventArgs e)
    {
        Console.Write("\r --> Downloading " + FILENAME +": " + string.Format("{0:n0}", e.BytesReceived / 1000) + " kb");
    }

    private static void Completed(object obj, AsyncCompletedEventArgs e)
    {
    }

这是一个输出的例子: 输入图像描述

希望能对某些人有所帮助!

2
@regisbsb 那些不是进度条,看起来像是他对文件名的部分进行了审查 :) 我知道,我一开始也被愚弄了。 - silkfire

0

我对 C# 还有点新,但我相信下面的内容可能会有所帮助。

string[] xmlFilePath = Directory.GetFiles(xmlFullpath, "*.xml");
Console.WriteLine("Loading XML files...");
int count = 0;
foreach (string file in xmlFilePath)
{
    //ExportXml(file, styleSheet);
    drawTextProgressBar(count, xmlCount);
    count++;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接