Displaying a detailed Progress bar in a C# Console Application

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 three custom areas or text blocks;

  1. The user modifiable text segments and can contain any number of lines
  2. The percentage progress complete text
  3. The progress bar

These sections all have customizable colors.

Details

There is one constructor needing three arguments:

  1. What line in the console that output should commence on i.e.. 7
  2. Number of user message lines i.e.. 8
  3. Whether to display the percentage complete text under the user message lines i.e.. true
ConsoleProgressBar pBar = new ConsoleProgressBar(7,8,true);

There are many custom features that can be set on initialization to customize the look of the progress bar.

Building on the previous example as follows

ConsoleProgressBar pBar = new ConsoleProgressBar(7, 8, true)
	{
	   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 the majority of variables in a single constructor call.

There are four functions allowing user interaction;

  1. SetMessage allows changing a specific line to different text
  2. RenderConsoleProgress updates the console with the current user modified data and the current percentage complete
  3. RenderConsoleProgress passing in a percentage updates the console as above and sets the current percentage
  4. EndConsoleOutput puts the cursor two lines under the outputted console text and doesn't disturb the current output

The getter and setter functions allowing user interaction;

  1. Progress allows getting the percentage complete of the task
  2. ProgressBarCharacter allows getting and setting the progress bar character used
  3. LineIndicator allows getting and setting the line indicator for output of user and percentage complete messages
  4. PercentageChaserText allows getting and setting the text following the percentage output above the progress bar
  5. PercentageVisible allows getting and setting whether to display the text progress percentage

I originally designed the class using an Enum to represent the lines of text minimizing 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 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.

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(4, 5, true);

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);
	   if (progress != pBar.Progress)
	   {
		   pBar.RenderConsoleProgress(progress);
	   }
	   mp.SaveMovie(movies[i], i);
	}
	

This just checks the current progression through the tasks and if it doesn't match up with our previously stored percentage from the progress bar class we call the RenderConsoleProgress function passing in the new percentage.


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("line2", "Searching themoviedb.org for data, Complete");

 

And that is as simple as it gets.

Note: When setting the number or rows the row for the percentage display and the row for the progress bar itself are not counted, i.e.. Number of rows = 4 would consume 6 rows, or 5 if the percentage display 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;
/*
 * This class renders multiple lines of text like so;
 * > Line 1
 * > Line 2
 * > Line 3
 * Then a 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
 * 
 * The lines of text start from Line1, Line0 is non existant to minimise confusion
 */
namespace MovieDB
{
	class ConsoleProgressBar
	{
//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
private int startOnLine = 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;
private const string MessagePercentageText = "percentage";
//Percentage MessageIndex object used for efficency so we dont need to have checks in the render looking for type of messages
private MessageIndex MIPercentage = new MessageIndex("", -2, MessagePercentageText);
//Current percentage of task completed
private int percentage = 0;
//Character to use for progress bar
private char progressBarCharacter = 'u2592';
//Identifying character for lines output to console will be visible for all lines except progress bar line
private String lineIndicator = "> ";
//Text following the percentage completed
private String percentageChaserText = "% Complete";
//Implemented properties for percentage
public int Progress { get { return percentage; } private set { percentage = value; } }
//Implemented properties for progressBarCharacter
public char ProgressBarCharacter { get { return progressBarCharacter; } set { progressBarCharacter = value; } }
//Implemented properties for lineIndicator       
public String LineIndicator { get { return lineIndicator; } set { lineIndicator = value; } }
//Implemented properties for percentageChaserText
public String PercentageChaserText { get { return percentageChaserText; } set { percentageChaserText = value; RenderConsoleProgress(percentage); } }
//Internal indicator to whether the percentage indicator show be shown or not to save checking its value for valid data
public bool PercentageVisible { get; set; }

//Colors for various outputs, color of progress bar, color of text, color of percentage line
public ConsoleColor pBarColor = ConsoleColor.Green;
public ConsoleColor textColor = ConsoleColor.DarkCyan;
public ConsoleColor percentageColor = ConsoleColor.DarkYellow;


/*
 * ConsoleProgressBar(int firstLine, int linesAvailable, bool showPercentageText)
 * 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
 * Input
 * Int firstLine, the first line of the console to output to i.e. 7 to start output from the 7th line downwards
 * Int linesAvailable, how many lines should be usable by the program i.e. 5
 * bool showPercentageText, toggles percentage display on or off, this is only for a line following all other lines
 * showing percentage followed by % Complete i.e. 54% Complete
 */
public ConsoleProgressBar(int firstLine, int linesAvailable, bool showPercentageText)
{
for (int i = 0; i < linesAvailable; i++)
{
	messages.Add(new MessageIndex(lineIndicator, i + 1, null));
}
startOnLine = firstLine;
PercentageVisible = showPercentageText;
 
RenderConsoleProgress(percentage);
}
/*
 * void EndConsoleOutput()
 * Sets the console output to be below the percentage data generated from this class
 */
public void EndConsoleOutput()
{
int numberOfLines = messages.Count + 3;
for (int i = 0; i < numberOfLines; i++)
{
	Console.WriteLine("");
}
}
/*
 * bool SetMessage(String mKey, String message)
 * 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
 */
public bool SetMessage(String mKey, String message)
{
int key = GetMessageIndexValue(mKey);
if (key != MessageNOTFOUND)
{
	SetMessage(messages[key], message);
	return true;
}
return false;
}

/*
 * void RenderConsoleProgress(int nPercentage)
 * Sets the current task progress and rerenders to screen
 * if the percentage 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
 */
public void RenderConsoleProgress(int nPercentage)
{
percentage = nPercentage;
if (PercentageVisible)
{
	SetMessage(MIPercentage, percentage + percentageChaserText);
}
RenderConsoleProgress();
}
/*
 * void RenderConsoleProgress()
 * Rerenders the current message data to console
 */
public void RenderConsoleProgress()
{
Console.CursorVisible=false;
ConsoleColor originalColor = Console.ForegroundColor;
Console.ForegroundColor = textColor;
Console.CursorTop = startOnLine;
int i = 0;
for (; i < messages.Count; i++)
{
	OverwriteConsoleMessage(messages[i]._Message);
	Console.CursorTop++;
}
if (PercentageVisible)
{
	Console.ForegroundColor = percentageColor;
	OverwriteConsoleMessage(MIPercentage._Message);
	Console.CursorTop++;
}   
Console.ForegroundColor = pBarColor;
Console.CursorLeft = 0;
int width = Console.WindowWidth - 1;
int newWidth = (int)((width * percentage) / 100d);
string progBar = new string(progressBarCharacter, newWidth) + new string(' ', width - newWidth);
Console.Write(progBar); 
Console.CursorTop = startOnLine; 
Console.ForegroundColor = originalColor;
Console.CursorVisible = true;
}
/*
 * void SetMessage(MessageIndex mIndex, String message)
 * Sets the message for a given MessageIndex object then rerenders the messages to the console
 */
private void SetMessage(MessageIndex mIndex, String message)
{
mIndex._Message = lineIndicator + message;
RenderConsoleProgress();
}
/*
 * int GetMessageIndexValue(String key)
 * 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
 */
private int GetMessageIndexValue(String key)
{
key = key.ToLower(); 
if (key.Equals(MessagePercentageText))
{
	return MessagePERCENTAGE;
} 
for(int i = 0; i < messages.Count; i++)
{
	MessageIndex mi = messages[i];
	if (mi._Line.Equals(key))
	{
		return i;
	}
}
return MessageNOTFOUND;
}
/*
 * int GetMessageIndexValue(MessageIndex mi)
 * returns the messageIndex value from the MessageIndex object which should refer to its position
 * in the messages Container as this is set during initialisation or SetMessage to that value
 */
private int GetMessageIndexValue(MessageIndex miToFind)
{
if (miToFind == MIPercentage)
{
	return MessagePERCENTAGE;
}
for (int i = 0; i < messages.Count; i++)
{
	MessageIndex mi = messages[i];
	if (mi._Line.Equals(miToFind._Line) && mi._MessageIndex == miToFind._MessageIndex)
	{
		return i;
	}
}
return MessageNOTFOUND;
}
/*
 * void OverwriteConsoleMessage(string message)
 * Overwrites the text at the current console coordinaes to the passed in message data
 */
private void OverwriteConsoleMessage(string message)
{
Console.CursorLeft = 0;
int maxCharacterWidth = Console.WindowWidth - 1;
if (message.Length > maxCharacterWidth)
{
	message = message.Substring(0, maxCharacterWidth - 3) + "...";
}
message = message + new string(' ', maxCharacterWidth - message.Length);
Console.Write(message);
}
	}
	/* 
	 * 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 ;(
	*/
	public class MessageIndex
	{
public String _Message { get; set; }
public int _MessageIndex { get; set; }
public String _Line { get; private set; }

public MessageIndex(String msg, int index, String line)
{
_Message = msg;
_MessageIndex = index;
if (String.IsNullOrEmpty(line))
	_Line = "line" + index;
else
	_Line = line.ToLower();
}
	}
}

Posted by:

Share:

This page has been visited 3155 times!

comments powered by Disqus