Duindain
Displaying a detailed Progress bar in a C# Console Application
Update
I've refactored the code in 2021 with the following changes;
- Properties object to collect all the configurable properties with default values so minimal to no setup is possible
- Cleaning up and optimised the code
- Adding more comments
- Removing some public methods and changing others to private, removing some unneeded code, properties and values
- Changing the interaction with the class to just SetProgress for percentage complete and SetMessage for changing the text on the lines above the percentage and an optional call to ShowProgressBar if showing before percentage is above 0 is needed, though the same behaviour is possible for SetProgress with an additional optional argument
- Added a more complete example of how to use the class here in the article
- Updated this article with the new options and features
- Added a video of an example project using the progress bar
See a video of the example project in action
The code here will show how to create and alter a progress bar to your own requirements with very little effort.
The progress bar color or character can be altered as desired as well as the text above it and the number of lines needed for your own application.
Background
I was writing a movie catalog application that searches for movies you have then queries via TheMovieDB.org and saves to a Sqlite database. It was a console application and could take some time to finish on large collections so I needed a progress bar to keep the user in touch with what was happening. More importantly I wanted to show text to the user that could be updated in place without having to make any complex calls as miles of streaming console text wasn't helping get any messages across.
A quick search on Google brought up this tutorial that gave me a decent skeleton to flesh out.
I thought I would post my code to show how to use this in a real world situation and I'll always know where to find the code now when I need it again.
Design Considerations
I have chosen not to make this a static class or to use the singleton design pattern as I think it is unnecessary for me, if someone needs to access the class in many locations then a singleton functionality is pretty easy to add in. In my case I needed access in two classes and I create the object in one and pass it to the other when needed.
I have defined several custom areas or text blocks;
- The user modifiable text segments and can contain any number of lines
- Whether to add a space between the progress text or bar and the lines above
- The percentage progress complete text
- The progress bar
These sections all have customisable colors apart from the space between the user messages and percentage text or progress bar.
Details
There is one constructor with an optional CPBProperties object:
- CPBProperties can be null or just not provided to the constructor, otherwise these are the available options;
- int StartOnLine -- what line in the console to start output at, default of 0, increase this number to allow for text before you start the progress output
- int LinesAvailable -- how many user set lines available for output, default of 5, how much text about the progress operation do you need
- ConsoleColor PBarColor -- color of the progress bar, default of Green
- ConsoleColor TextColor -- color of user set messages above the progress bar, default of DarkCyan
- ConsoleColor PercentageColor -- color of the text output for percentage, default of DarkYellow, if enabled shows [0-9]% Complete above the progress bar
- string LineIndicator -- indicator for user set messages, default of "> ", this will be added to any user set messages
- char ProgressBarCharacter -- character to use for 1 increment of progress in the progress bar, default of 'u2592' (in html ΓΆ–’)
- bool PercentageVisible -- whether to show the percentage text, default of true
- bool ShowEmptyLineAbovePercentage -- whether to add a blank line between user messages and percentage text if visible or progress bar, default of true
- string PercentageChaserText -- text to use following percentage text, default of "% Complete"
- string TruncatedIndicator -- truncation indicator if a user message is too long for the console, default of "..."
ConsoleProgressBar pBar = new ConsoleProgressBar();
or overriding some default properties from CPBProperties
ConsoleProgressBar pBar = new ConsoleProgressBar(new CPBProperties { StartOnLine = 5, LinesAvailable = 3 });
There are many custom features that can be set on initialization to customize the look of the progress bar.
Building on the previous example for an example
ConsoleProgressBar pBar = new ConsoleProgressBar(new CPBProperties { LineIndicator = "} ",//Change the default line indicater from "> " to "} " ProgressBarCharacter = 'o',//Change the default progress bar char from 'u2592' to 'o' PBarColor = ConsoleColor.Cyan,//Change the progress bar color TextColor = ConsoleColor.DarkMagenta,//Change the user message text color PercentageChaserText = "% Till We Eat!!!"//Change the percentage text string });
This allows you to set any configurable property in the constructor call, hopefully for most purposes no configuration will be necessary.
There are five functions allowing user interaction;
- ShowProgressBar renders the messages and progress bar with existing values i.e. ShowProgressBar();
- This is only needed if you want to show immediately before progress has incremented past 0 as setting 0 wouldn't render to the console as 0 is the default value
- This is not super useful as SetProgress(0, true); would accomplish the same thing but its easier to call and an easier pattern to remember
- SetProgress sets the progress and rerenders everything to the console if progress has changed i.e. SetProgress(25);
- This should be a number between 0 - 100 but it is checked and clamped to that minimum and maximum value
- There is an optional argument forceUpdate to force rendering even if the value of progress hasn't changed
- SetMessage allows changing a specific line to different text i.e. SetMessage("line2", "Almost finished article updates")
- EndConsoleOutput increments console output line to be below the progress bar with no extra spacing so normal console output can continue for the calling program
- Optional method if normal console output is required after the console progress bar rendering is finished
- Dispose if EndConsoleOutput hasn't been called calls EndConsoleOutput
- Optional method to end console output instead of calling EndConsoleOutput
There are two public getter properties;
- Progress allows getting the percentage complete of the task
- CPBProperties allows getting the current configurable properties
- You can change the properties of this object during runtime and it would reflect on next render
I originally designed the class using an Enum to represent the lines of text minimising the chance of input error and greatly simplifying searching for Messages.
enum Messages { LINE1, LINE2, LINE3, PERCENTAGE }
Unfortunately I could not find a way to dynamically increase or decrease this Enum to allow the user to specify how many lines of text there should be and that seemed more important than the input errors possible through passing string identifiers ("line1", "line2", "line3") around. If anyone can think of a way to make this more efficient or cleaner please contact or post a reply. I also didn't want to specify a huge number of lines within the enum and then not use most of them as it would clutter up the code and could be not enough anyway.
Messages lines are starting from 1 to avoid confusion so "line1" is the first line of output through to "line[n+1]", the SetMessage won't through exceptions if you specify a non existing line it will return false but nothing will break.
Example
I recently decided to try this class again and had to write a bit of code to get it to run so I thought I might provide a more fleshed out example here. This was written in a new console project for .Net 6 but not using any new features other than the new Main class format.
The main processing loop of the example, initially shows the progress bar with no data provided then as it goes through each task in the list with an initial simulated delay then when complete sets progress to 100 just incase the number of tasks didnt finish at exactly 100% then disposes of the progress bar class
//Show the ConsoleProgressBar, normal console output will not work correctly as it will get overwritten during the render //cycle so make sure you have output the required text before showing the progress bar pBar.ShowProgressBar(); //Wait a moment to show the progressbar with default values and no text added yet await Task.Delay(random.Next(1500, 2500)); var movies = CreateMovieList(); for (var i = 0; i < movies.Count; i++) { var progress = Convert.ToInt32((Convert.ToDouble(i) / Convert.ToDouble(movies.Count) * 100.0) + 0.5); pBar.SetProgress(progress); //Run a task that takes time to execute await SaveMovie(movies[i], i); } //Force final 100% incase we didn't hit it perfectly pBar.SetProgress(100); //We can call dispose or EndConsoleOutput() to end console progress output and put the cursor on the next available line pBar.Dispose();
SaveMovie is a simulated task where we update the user messages to indicate what we are working on and when complete update again to indicate this state
- line1 is what we are working on right now i.e. working on task 1 of 200 for {name of task 1}
- line2 is something about that task i.e. Searching database for relevant entries
- line3 is what happened when complete i.e. Found entry in database, updated record for {name of task 1}
As line3 is only updated after the individual task is complete it will trail the current processing by 1 task and show the result of the previous operation or an empty line if we are on the first task
//Before executing the task tell the user what is happening pBar.SetMessage("line1", $"Processing movie {index + 1} of {totalNumberOfMovies} "{guid}""); pBar.SetMessage("line2", "Searching myonlinedatasource.com for data..."); //Execute the task await Task.Delay(random.Next(500,1500)); //Tell the user that this incremental task is complete //If there were many quick tasks to execute that the text would change too frequently we could do something like //if(index % 500 == 0) then output an update pBar.SetMessage("line3", $"Completed processing for {guid}");
You can download the full example class here
or Create a new class file and copy and paste in the following
using Lazinator; int totalNumberOfMovies = 27; Random random = new Random(); Console.WriteLine(@"Console Progress Bar example We are using the default values for most properties so we have 5 lines to play with before the console progress will be displayed Then the progress bar will have 3 lines available to it"); //This wont render to the screen until ShowProgressBar is called or ConsoleProgressBar pBar = new ConsoleProgressBar(new CPBProperties { StartOnLine = 5, LinesAvailable = 3 }); await MainProcessing(); Console.WriteLine(@"By calling EndConsoleOutput or Disposing of the pBar object we can now output normally to the console after finishing the progress reporting Otherwise the render loop always resets the console line back to the first line and we would be overwriting the text on each line in the progress bar one by one"); async Task MainProcessing() { //Show the ConsoleProgressBar, normal console output will not work correctly as it will get overwritten during the render //cycle so make sure you have output the required text before showing the progress bar pBar.ShowProgressBar(); //Wait a moment to show the progressbar with default values and no text added yet await Task.Delay(random.Next(1500, 2500)); var movies = CreateMovieList(); for (var i = 0; i < movies.Count; i++) { var progress = Convert.ToInt32((Convert.ToDouble(i) / Convert.ToDouble(movies.Count) * 100.0) + 0.5); pBar.SetProgress(progress); //Run a task that takes time to execute await SaveMovie(movies[i], i); } //Force final 100% incase we didn't hit it perfectly pBar.SetProgress(100); //We can call dispose or EndConsoleOutput() to end console progress output and put the cursor on the next available line pBar.Dispose(); }
My Use Case Example
My use case involves fetching data about movies from an online database, writing that data to a local database then outputting the result and the time taken to complete each step. I output some basic task text before I begin using the progress bar i.e.. informing the user that my program has found 42 movie titles to fetch data about. I then create the progress bar and initialize it starting on the fourth line (because I have already used 3 lines of output previously and we don't want to overwrite them) with five lines of text and allowing the percentage text to be displayed as follows.
ConsoleProgressBar pBar = new ConsoleProgressBar(new CPBProperties { StartOnLine = 4, LinesAvailable = 3 });
In my processing loop I check the percentage of the task and if it has changed I update the progress bar
for (int i = 0; i < movies.Count; i++) { int progress = Convert.ToInt32((Convert.ToDouble(i) / Convert.ToDouble(movies.Count) * 100.0) + 0.5); pBar.SetProgress(progress); mp.SaveMovie(movies[i], i); }
This just sets the percentage on every SaveMovie call allowing the progress bar class render when the percentage has changed.
So far this is fine it will display the correct lines of user text and a moving percentage of the task as we progress along until it is completed, however it wont change the text displayed on the user message lines.
To do that we need to call the SetMessage function with some new text and refer to a line we want to place it on.
At the start of my SaveMovie function we update the progress bar with the current task
pBar.SetMessage("line1", $"Processing movie {movieNumber + 1} of {totalNumberOfMovies + 1} "{moviename}""); pBar.SetMessage("line2", "Searching myonlinedatasource.com for data...");
I can then query the online database and retrieve the result. When completed I can overwrite the original text to update the user on the current task as follows:
pBar.SetMessage("line3", $"Searching themoviedb.org for data, Complete for {moviename}");
And that is as simple as it gets.
Note: When setting the number or rows you are specifying how many user messages to display this won't include the empty row for spacing (If enabled), the row for the percentage display (If enabled) and the row for the progress bar itself, i.e.. Number of rows = 4 would consume 7 rows, or 6 if the percentage display is disabled and 5 if the empty row is disabled.
You can download the class here or Create a new class file and copy and paste in the following
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Reflection; namespace Lazinator { ////// Combined property model for the ConsoleProgressBar /// This contains defaults for all properties and is an optional argument to the ctor for the /// ConsoleProgressBar /// It is nested in the same class for brievity otherwise would be in a seperate file /// public class CPBProperties { ////// How far down the console window to start output ie if 5 then at line 5 will be Line1, line 6 with be Line2 of messages /// You would use this by outputting a known set of lines prior to any progress output like /// /// Console.WriteLine(@"Program counts the number of turles attracted to mushrooms /// Accepts fish flakes and checks with supplier for current best flavours."); /// /// You would then want to start the progress bar output on line 3 if no space required or line 4 to make it a bit nicer /// Default value of 0 /// public int StartOnLine { get; set; } = 0; ////// how many lines should be usable by the program /// This is for text related to the processing that the progress bar is doing /// line1: Checking for available titles /// line2: Found title /// line3: Processing /// line4: Completed title /// line5: empty space to seperate between percentage complete text /// Default value of 5 /// public int LinesAvailable { get; set; } = 5; ////// Color of progress bar character /// public ConsoleColor PBarColor { get; set; } = ConsoleColor.Green; ////// Color of normal text i.e. > Processing item 12 of 400 /// public ConsoleColor TextColor { get; set; } = ConsoleColor.DarkCyan; ////// Color of percentage progress text i.e. 56% Complete above the progress bar /// public ConsoleColor PercentageColor { get; set; } = ConsoleColor.DarkYellow; ////// Identifying character for lines output to console will be visible for all lines except progress bar line /// public string LineIndicator { get; set; } = "> "; ////// Single character or unicode value to display as a single unit of progress /// public char ProgressBarCharacter { get; set; } = 'u2592'; ////// Show Percentage completed text above the progress bar /// public bool PercentageVisible { get; set; } = true; ////// Shows an empty line under the last text section and before the Percentage text /// if PercentageVisible = false this will still add an extra line before the progress bar /// public bool ShowEmptyLineAbovePercentage { get; set; } = true; ////// Text following the percentage completed, used if PercentageVisible = true /// public string PercentageChaserText { get; set; } = "% Complete"; ////// If a message is too long to fit on one line in the console (Console.WindowWidth - 1) it will be reduced to console width - TruncatedIndicator.Length /// with the TruncatedIndicator added to indicate that the line has been shortened /// public string TruncatedIndicator { get; set; } = "..."; } ////// This class renders multiple lines of text like so; /// > Line 1 /// > Line 2 /// > Line 3 /// An optional space between the message lines and the percentage lines /// Then an optional percentage of task completed /// > 54% Complete /// Then a progress bar composed of the character 'u2592' or another user specified character /// That fills up as the percentage value is changed over the course of the program /// /// The class works downwards from a passed in argument in the constructor to render each line /// using Console.CursorTop by incrementing it.This means after the class has been finished with /// the console will be rather confusing as output will echo out on the first line passed in with /// the constructor to prevent this call EndConsoleOutput or dispose of the progress bar class /// after it is no longer needed /// /// The lines of text start from Line1, Line0 is non existant to minimise confusion /// public class ConsoleProgressBar { ////// Consolidated properties for progress bar with default values preset /// public CPBProperties CPBProperties { get; private set; } ////// Current percentage of task completed between 0 to 100 /// public int Progress { get; private set; } = 0; //Message container, each line is refered to by the enum private List_messages = new List (); //Some constants to simplify error message checking and string comparison private const int MessagePERCENTAGE = -2; private const int MessageNOTFOUND = -1; //Percentage MessageIndex object used for efficency so we dont need to have checks in the render looking for type of messages private readonly MessageIndex _MIPercentage = new MessageIndex (string.Empty, MessagePERCENTAGE); //Check for Dispose vs calling EndConsoleOutput() private bool _disposed = false; /// /// Initialises and begins rendering a console progress bar /// This is composed of MessageIndex Objects that are created either in this constructor or /// by passing an invalid object to the SetMessage function /// /// Progress bar properties, optional public ConsoleProgressBar(CPBProperties cPBProperties = null) { CPBProperties = cPBProperties ?? new CPBProperties(); CreateInitialMessages(); } ////// Convienence method to start the initial display of the progress bar and assosisated lines /// This is optional, the render is automatically called when progress is changed but this method /// allows immediate display /// public void ShowProgressBar() { //Call this method to force evaluate the progress text property SetProgress(Progress, true); } ////// Checks if the passed in progress is different from existing progress if not nothing happens /// otherwise sets the current task progress and Renders to screen /// if the percentage text is set to not visible this will still update the current task completed status /// and update the progress bar however it will not update the progress text otherwise it will update /// the text as well /// If the progress bar isnt currently rendered this will make it appear /// /// progress percentage to update to /// forces an update regardless of if progress is changed or not public void SetProgress(int progress, bool forceUpdate = false) { if(progress < 0) { progress = 0; } else if(progress > 100) { progress = 100; } if(Progress != progress || forceUpdate) { Progress = progress; if (CPBProperties.PercentageVisible) { SetMessage(_MIPercentage, $"{Progress}{CPBProperties.PercentageChaserText}"); } RenderConsoleProgress(); } } ////// Returns true if the key was found as a valid MessageIndex object and the message was altered to the passed in value /// Also renders the new data to the console /// /// message key to edit i.e. line2 /// text to change for the line ///public bool SetMessage(string mKey, string message) { var key = GetMessageIndexValue(mKey); if (key != MessageNOTFOUND) { SetMessage(_messages[key], message); return true; } return false; } /// /// Sets the console output to be below the percentage data generated from this class /// This is used to allow the caller to output normal console text after the progressbar is no longer needed /// public void EndConsoleOutput() { //Work out how many lines we have been using var numberOfLines = _messages.Count; if (CPBProperties.PercentageVisible) { numberOfLines++; } if (CPBProperties.ShowEmptyLineAbovePercentage) { numberOfLines++; } //Add a line for the progress bar numberOfLines++; for (int i = 0; i < numberOfLines; i++) { Console.WriteLine(string.Empty); } } ////// Optional dispose calls EndConsoleOutput if it hasent been called already /// Doesn't actually clear any data so should be safe to restart using the progress bar /// if needed after resetting Progress to 0 /// public void Dispose() { if(_disposed == false) { EndConsoleOutput(); _disposed = true; } } ////// Renders the current message data and progress to console, this is called from public methods /// SetMessage, SetProgress and ShowProgressBar /// private void RenderConsoleProgress() { //Preserve the existing foreground color of the console and hide the cursor Console.CursorVisible = false; var originalColor = Console.ForegroundColor; Console.ForegroundColor = CPBProperties.TextColor; //Set the cursor the first line of our progress output Console.CursorTop = CPBProperties.StartOnLine; //User line messages before progress for (var i = 0; i < _messages.Count; i++) { OverwriteConsoleMessage(_messages[i].Message); Console.CursorTop++; } //Add an empty line before percentage text or bar if(CPBProperties.ShowEmptyLineAbovePercentage) { Console.CursorTop++; } //Progress text output if (CPBProperties.PercentageVisible) { Console.ForegroundColor = CPBProperties.PercentageColor; OverwriteConsoleMessage(_MIPercentage.Message); Console.CursorTop++; } //Progress bar Console.ForegroundColor = CPBProperties.PBarColor; Console.CursorLeft = 0; var width = Console.WindowWidth - 1; var newWidth = (int)((width * Progress) / 100d); var progBar = new string(CPBProperties.ProgressBarCharacter, newWidth) + new string(' ', width - newWidth); Console.Write(progBar); //Reset to start of progress bar console Console.CursorTop = CPBProperties.StartOnLine; //restore the original foreground color and restore the cursor Console.ForegroundColor = originalColor; Console.CursorVisible = true; } ////// Sets the message for a given MessageIndex object then calls RenderConsoleProgress to render the messages to the console /// /// message object to update /// new message text to update private void SetMessage(MessageIndex messageIndex, string message) { messageIndex.Message = $"{CPBProperties.LineIndicator}{message}"; RenderConsoleProgress(); } ////// Returns a Integer index value for the MessageIndex list that matches the given key value /// Keys are forced to lower case to match the MessageIndex key setting function /// Returns -1 if the key is not found within the list /// /// line to look for i.e. line2 ///index of message if found or MessagePERCENTAGE or MessageNOTFOUND consts private int GetMessageIndexValue(string key) { key = key.ToLower(); for (int index = 0; index < _messages.Count; index++) { var messageIndex = _messages[index]; if (messageIndex.Line.Equals(key)) { return index; } } return MessageNOTFOUND; } ////// Overwrites the text at the current console coordinaes to the passed in message data /// called from the main render method and expects the console to be at the correct line to be overriden /// Checks for out of bounds text and subsitutes the truncated indicator where needed /// /// text to display private void OverwriteConsoleMessage(string message) { Console.CursorLeft = 0; var maxCharacterWidth = Console.WindowWidth - 1; if (message.Length > maxCharacterWidth) { message = $"{message.Substring(0, maxCharacterWidth - CPBProperties.TruncatedIndicator.Length)}{CPBProperties.TruncatedIndicator}"; } //Pads the message out to reach the maximum character width of the console message = $"{message}{new string(' ', maxCharacterWidth - message.Length)}"; Console.Write(message); } ////// Helper to create the right number of MessageIndex objects for the required number of lines /// private void CreateInitialMessages() { for (int i = 0; i < CPBProperties.LinesAvailable; i++) { _messages.Add(new MessageIndex(CPBProperties.LineIndicator, i + 1)); } } } ////// Simple class to contain message details and a string representation of an enum /// Unfortunately i couldnt find a way to build enums dynamically so I'll have to go /// forward using strings ;( /// Could be replaced with an arbitrary number of lines in an enum but would be an assumtion /// for how many to create at least the strings are unlimited /// public class MessageIndex { ////// Message to display in console for this line /// public string Message { get; set; } ////// Identifier we use to match against, pattern is just line{index} /// so line1, line2 and so on starts at 1 so easier for consumers to work with /// public string Line { get; private set; } public MessageIndex(string msg, int index) { Message = msg; Line = $"line{index}"; } } }
Note: there seems to be a strange artifact getting injected into the last line of the code above </messageindex></messageindex> Please remove that if you copy and paste it out it isnt in the actual file or the article but its getting corrupted somehow, I'll fix it soon.