Visual Basic: How to Draw a Border of ASCII Text in a Console Application
Introduction
The Console Application is a handy way to write simple utility programs that do not require much in the way of a user interface. Typically a Console Application uses a command-line interface where the user types commands into the window, presses enter, and the window reports back lines of text with the result of the command execution.
But before there were programs with windows and buttons and fancy widgets to click, there were programs with menus and key-maps and GUIs made of ASCII characters. You may even have seen such a screen recently when installing an operating system or configuring a BIOS. There may still be times today when it makes sense to create a modern application that uses a simple interface designed in ASCII which runs in a command prompt window.
There are a number of ways one might go about making such an interface today. A traditional approach would be to define a two-dimensional array of characters (essentially your own "buffer" for the console window) and then manually map out the series of ASCII characters making up the frame of the "window" and storing these characters in the array. The program then draws its own buffer to the window according to the characters assigned. Different window designs can be loaded into the buffer and then written to the console.
This article will take a slightly more advanced approach and will use an algorithm to layout the proper sequence of ASCII characters based on a series of defined rectangles representing the window frame. The easiest way to describe this might be with a picture:
Here you can see a double-line border around the inside of the window, with a box at the top and bottom. This frame would be described by the rectangles:
Dim border As New Rectangle(0, 0, Console.WindowWidth, Console.WindowHeight)
Dim title As New Rectangle(0, 0, Console.WindowWidth, 3)
Dim command As New Rectangle(0, Console.WindowHeight - 3, Console.WindowWidth, 3)
While the approach presented here may be "slightly more advanced", the algorithm used is straight-forward and easy to follow so an even more (or truly) advanced version could still be crafted.
Prerequisite Article
Before continuing, please read How to Write to a Console Window without Wrapping Text. You will need to complete the example shown in that article before you can use the code in this article.
Creating the FrameRenderer
In this example we will create a static class called FrameRenderer which will provide us with all of the features and functionality required to render a frame of ASCII characters based on Rectangles.
But before we can create the FrameRenderer itself, there are a couple of support objects that are going to be needed; namely the Rectangle and a "FrameCellType" to allow us to track the kind of ASCII character used in a given part of the frame.
Rectangle Structure
To keep from needing to add a reference to System.Drawing when all we need is a simple rectangle, we will define our own small structure to suit our purposes:
Public Structure Rectangle
Public Left As Integer
Public Height As Integer
Public Top As Integer
Public Width As Integer
Public Function Right() As Integer
Return Left + Width - 1
End Function
Public Function Bottom() As Integer
Return Top + Height - 1
End Function
Public Sub New(l As Integer, t As Integer, w As Integer, h As Integer)
Left = l
Top = t
Width = w
Height = h
End Sub
End Structure
This structure simple holds a Left, Top, Width and Height value set for us. It exposes a Right and Bottom method for convenience only.
FrameCellType Enum
Much as we would do using a traditional character map, we will define a two-dimensional array to track the characters that make up our frame. But instead of storing actual ASCII characters, we will store Enum values that allow us to refer to the characters in the frame without tying us to a particular character. We'll see shortly how this adds a lot of versatility to the FrameRenderer class.
The Enum will contain values which describe each possible portion of a frame:
Public Enum FrameCellType
Empty
Horizontal
Vertical
BottomLeft
BottomRight
TopLeft
TopRight
TeeBottom
TeeLeft
TeeRight
TeeTop
Cross
End Enum
So here we define characters for horizontal and vertical lines, the corners of a rectangle, and the intersection point (T's) along any side of a rectangle. With this information in hand we can now begin to write an algorithm which can combine rectangles into a single frame.
Preliminary Class Layout
With our support objects in place, we can now begin to design the FrameRenderer class itself. This will be a sealed class with all static (shared) members. Users will not create an instance of this class but rather will simply call shared methods to use the class functionality. To begin, we can declare the sealed class and define the cell-type array used to track the characters of the frame and the character array which will provide the individual characters to use:
Public NotInheritable Class FrameRenderer
Private Shared _Cells(,) As FrameCellType
Private Shared _Characters() As Char = "╚╝╬═╩╠╣╦╔╗║"
Protected Sub New()
End Sub
End Class
This example will use the ASCII characters for a double-line border by default, but you could set the _Characters() array to any series of eleven characters you want. For example, here is the single-line border character series: "└┘┼─┴├┤┬┌┐│"
To make the algorithm easy to write and follow, we'll add eleven properties to the class which allow us to access a character from the _Characters() array according to its frame type name:
Public Shared ReadOnly Property BottomLeft As Char
Get
Return _Characters(0)
End Get
End Property
Public Shared ReadOnly Property BottomRight As Char
Get
Return _Characters(1)
End Get
End Property
...
Public Shared ReadOnly Property Vertical As Char
Get
Return _Characters(10)
End Get
End Property
Now we can create the DrawFrame() method which will contain our algorithm and do the work of drawing the characters defined by the rectangles. The method will also take parameters to specify the colors to use, so the method signature and initial lines of code become:
Public Shared Sub DrawFrame(forecolor As ConsoleColor, backcolor As ConsoleColor, ParamArray bounds() As Rectangle)
Dim forecolorDelta As ConsoleColor = Console.ForegroundColor
Dim backcolorDelta As ConsoleColor = Console.BackgroundColor
Console.ForegroundColor = forecolor
Console.BackgroundColor = backcolor
ReDim _Cells(Console.WindowWidth - 1, Console.WindowHeight - 1)
End Sub
This gets everything ready to begin drawing the frame according to the rectangles.
The Algorithm
The overall algorithm then becomes a couple of nested loops with a lot of Select Case statements. The outer-most loop will need to iterate through each of the rectangles passed to the method. For each rectangle, the code will need to loop from the top of the rectangle to the bottom. For each line within the top-to-bottom loop, the code will need to loop from left to right. Within the left-to-right loop the code will set the cursor to the current position and then analyze the type of character at the current position, setting or updating the character based on what the character is and which character is required for the current rectangle.
For Each r As Rectangle In bounds
For y As Integer = r.Top To r.Bottom
For x As Integer = r.Left To r.Right
Console.SetCursorPosition(x, y)
Here is where the repetitive blocks of Select statements come into play. For instance, the algorithm first checks for the upper-left corner case:
If x = r.Left Then
If y = r.Top Then
Select Case _Cells(x, y)
Case FrameCellType.Empty
_Cells(x, y) = FrameCellType.TopLeft
Console.Write(TopLeft)
Case FrameCellType.Horizontal
_Cells(x, y) = FrameCellType.TeeTop
Console.Write(TeeTop)
Case FrameCellType.Vertical, FrameCellType.BottomLeft
_Cells(x, y) = FrameCellType.TeeLeft
Console.Write(TeeLeft)
Case FrameCellType.TopRight
_Cells(x, y) = FrameCellType.TeeTop
Console.Write(TeeTop)
Case FrameCellType.BottomRight, FrameCellType.TeeRight, FrameCellType.TeeBottom
_Cells(x, y) = FrameCellType.Cross
Console.Write(Cross)
End Select
When x = r.Left and y = r.Top the current cell needs to be a top-left corner character. So if the current cell is empty, it can simply be set to TopLeft. If the cell already contains the Horizontal character, then merging Horizontal with Top-Left would result in the Tee-Top character. This logic continues, transforming the existing character according to the character which needs to be written. There are seven more blocks like this one, but they all follow similar logic.
After completing the main work of the algorithm, all that remains is to restore the console colors:
ElseIf y = r.Bottom Then
Select Case _Cells(x, y)
Case FrameCellType.Empty
_Cells(x, y) = FrameCellType.Horizontal
Console.Write(Horizontal)
Case FrameCellType.Vertical, FrameCellType.TeeLeft, FrameCellType.TeeRight
_Cells(x, y) = FrameCellType.Cross
Console.Write(Cross)
Case FrameCellType.TopLeft, FrameCellType.TopRight
_Cells(x, y) = FrameCellType.TeeTop
Console.Write(TeeTop)
Case FrameCellType.BottomLeft, FrameCellType.BottomRight
_Cells(x, y) = FrameCellType.TeeBottom
Console.Write(TeeBottom)
End Select
End If
End If
Next
Next
Next
Console.ForegroundColor = forecolorDelta
Console.BackgroundColor = backcolorDelta
End Sub
To take advantage of the character versatility we can also add a couple of helper methods for specifying the character set to use:
Public Shared Sub SetCharacters(characters As String)
If characters.Length = 11 Then
_Characters = characters
Else
Throw New ArgumentException("Must supply exactly eleven characters.")
End If
End Sub
Public Shared Sub SetDoubleBar()
_Characters = "╚╝╬═╩╠╣╦╔╗║"
End Sub
Public Shared Sub SetSingleBar()
_Characters = "└┘┼─┴├┤┬┌┐│"
End Sub
Example Program
With the FrameRenderer ready to use, we can create the output in the screenshot above with the following simple program:
Module Module1
Sub Main()
NativeMethods.SetConsoleMode(NativeMethods.GetStdHandle(-11), 1)
Console.BufferWidth = Console.WindowWidth
Console.BufferHeight = Console.WindowHeight
Console.CursorVisible = False
Dim border As New Rectangle(0, 0, Console.WindowWidth, Console.WindowHeight)
Dim title As New Rectangle(0, 0, Console.WindowWidth, 3)
Dim command As New Rectangle(0, Console.WindowHeight - 3, Console.WindowWidth, 3)
FrameRenderer.DrawFrame(border, title, command)
Console.ReadKey()
End Sub
End Module
As you can see, the work we put into writing the behemoth algorithm pays off when we actually go to draw a frame in the console. And the more complex the layout, the greater the payoff.
Summary
It can be relatively easy to draw a series of interconnected rectangles out of ASCII characters for use as a window frame in a console application. By designing an algorithm based around an indirect character map it is possible to provide versatility for various character sets when drawing the frame.
The algorithm presented in this article could be rewritten to be more sophisticated and/or could be expanded with logic to combine single and double frame rectangles (cross over ASCII characters exist to do this).
Appendix A: Complete Code Sample
Public NotInheritable Class FrameRenderer
Public Shared ReadOnly Property BottomLeft As Char
Get
Return _Characters(0)
End Get
End Property
Public Shared ReadOnly Property BottomRight As Char
Get
Return _Characters(1)
End Get
End Property
Public Shared ReadOnly Property Cross As Char
Get
Return _Characters(2)
End Get
End Property
Public Shared ReadOnly Property Horizontal As Char
Get
Return _Characters(3)
End Get
End Property
Public Shared ReadOnly Property TeeBottom As Char
Get
Return _Characters(4)
End Get
End Property
Public Shared ReadOnly Property TeeLeft As Char
Get
Return _Characters(5)
End Get
End Property
Public Shared ReadOnly Property TeeRight As Char
Get
Return _Characters(6)
End Get
End Property
Public Shared ReadOnly Property TeeTop As Char
Get
Return _Characters(7)
End Get
End Property
Public Shared ReadOnly Property TopLeft As Char
Get
Return _Characters(8)
End Get
End Property
Public Shared ReadOnly Property TopRight As Char
Get
Return _Characters(9)
End Get
End Property
Public Shared ReadOnly Property Vertical As Char
Get
Return _Characters(10)
End Get
End Property
Private Shared _Cells(,) As FrameCellType
Private Shared _Characters() As Char = "╚╝╬═╩╠╣╦╔╗║"
Protected Sub New()
End Sub
Public Shared Sub DrawFrame(ParamArray bounds() As Rectangle)
DrawFrame(Console.ForegroundColor, Console.BackgroundColor, bounds)
End Sub
Public Shared Sub DrawFrame(forecolor As ConsoleColor, ParamArray bounds() As Rectangle)
DrawFrame(forecolor, Console.BackgroundColor, bounds)
End Sub
Public Shared Sub DrawFrame(forecolor As ConsoleColor, backcolor As ConsoleColor, ParamArray bounds() As Rectangle)
Dim forecolorDelta As ConsoleColor = Console.ForegroundColor
Dim backcolorDelta As ConsoleColor = Console.BackgroundColor
Console.ForegroundColor = forecolor
Console.BackgroundColor = backcolor
ReDim _Cells(Console.WindowWidth - 1, Console.WindowHeight - 1)
For Each r As Rectangle In bounds
For y As Integer = r.Top To r.Bottom
For x As Integer = r.Left To r.Right
Console.SetCursorPosition(x, y)
If x = r.Left Then
If y = r.Top Then
Select Case _Cells(x, y)
Case FrameCellType.Empty
_Cells(x, y) = FrameCellType.TopLeft
Console.Write(TopLeft)
Case FrameCellType.Horizontal
_Cells(x, y) = FrameCellType.TeeTop
Console.Write(TeeTop)
Case FrameCellType.Vertical, FrameCellType.BottomLeft
_Cells(x, y) = FrameCellType.TeeLeft
Console.Write(TeeLeft)
Case FrameCellType.TopRight
_Cells(x, y) = FrameCellType.TeeTop
Console.Write(TeeTop)
Case FrameCellType.BottomRight, FrameCellType.TeeRight, FrameCellType.TeeBottom
_Cells(x, y) = FrameCellType.Cross
Console.Write(Cross)
End Select
ElseIf y = r.Bottom Then
Select Case _Cells(x, y)
Case FrameCellType.Empty
_Cells(x, y) = FrameCellType.BottomLeft
Console.Write(BottomLeft)
Case FrameCellType.Horizontal, FrameCellType.BottomRight
_Cells(x, y) = FrameCellType.TeeBottom
Console.Write(TeeBottom)
Case FrameCellType.Vertical, FrameCellType.TopLeft
_Cells(x, y) = FrameCellType.TeeLeft
Console.Write(TeeLeft)
Case FrameCellType.TopRight, FrameCellType.TeeRight, FrameCellType.TeeTop
_Cells(x, y) = FrameCellType.Cross
Console.Write(Cross)
End Select
Else
Select Case _Cells(x, y)
Case FrameCellType.Empty
_Cells(x, y) = FrameCellType.Vertical
Console.Write(Vertical)
Case FrameCellType.Horizontal, FrameCellType.TeeTop, FrameCellType.TeeBottom
_Cells(x, y) = FrameCellType.Cross
Console.Write(Cross)
Case FrameCellType.TopLeft, FrameCellType.BottomLeft
_Cells(x, y) = FrameCellType.TeeLeft
Console.Write(TeeLeft)
Case FrameCellType.TopRight, FrameCellType.BottomRight
_Cells(x, y) = FrameCellType.TeeRight
Console.Write(TeeRight)
End Select
End If
ElseIf x = r.Right Then
If y = r.Top Then
Select Case _Cells(x, y)
Case FrameCellType.Empty
_Cells(x, y) = FrameCellType.TopRight
Console.Write(TopRight)
Case FrameCellType.Horizontal
_Cells(x, y) = FrameCellType.TeeTop
Console.Write(TeeTop)
Case FrameCellType.Vertical, FrameCellType.BottomRight
_Cells(x, y) = FrameCellType.TeeRight
Console.Write(TeeRight)
Case FrameCellType.TopLeft
_Cells(x, y) = FrameCellType.TeeTop
Console.Write(TeeTop)
Case FrameCellType.BottomLeft, FrameCellType.TeeLeft, FrameCellType.TeeBottom
_Cells(x, y) = FrameCellType.Cross
Console.Write(Cross)
End Select
ElseIf y = r.Bottom Then
Select Case _Cells(x, y)
Case FrameCellType.Empty
_Cells(x, y) = FrameCellType.BottomRight
Console.Write(BottomRight)
Case FrameCellType.Horizontal, FrameCellType.BottomLeft
_Cells(x, y) = FrameCellType.TeeBottom
Console.Write(TeeBottom)
Case FrameCellType.Vertical, FrameCellType.TopRight
_Cells(x, y) = FrameCellType.TeeRight
Console.Write(TeeRight)
Case FrameCellType.TopLeft, FrameCellType.TeeLeft, FrameCellType.TeeTop
_Cells(x, y) = FrameCellType.Cross
Console.Write(Cross)
End Select
Else
Select Case _Cells(x, y)
Case FrameCellType.Empty
_Cells(x, y) = FrameCellType.Vertical
Console.Write(Vertical)
Case FrameCellType.Horizontal, FrameCellType.TeeTop, FrameCellType.TeeBottom
_Cells(x, y) = FrameCellType.Cross
Console.Write(Cross)
Case FrameCellType.TopLeft, FrameCellType.BottomLeft
_Cells(x, y) = FrameCellType.TeeLeft
Console.Write(TeeLeft)
Case FrameCellType.TopRight, FrameCellType.BottomRight
_Cells(x, y) = FrameCellType.TeeRight
Console.Write(TeeRight)
End Select
End If
Else
If y = r.Top Then
Select Case _Cells(x, y)
Case FrameCellType.Empty
_Cells(x, y) = FrameCellType.Horizontal
Console.Write(Horizontal)
Case FrameCellType.Vertical, FrameCellType.TeeLeft, FrameCellType.TeeRight
_Cells(x, y) = FrameCellType.Cross
Console.Write(Cross)
Case FrameCellType.TopLeft, FrameCellType.TopRight
_Cells(x, y) = FrameCellType.TeeTop
Console.Write(TeeTop)
Case FrameCellType.BottomLeft, FrameCellType.BottomRight
_Cells(x, y) = FrameCellType.TeeBottom
Console.Write(TeeBottom)
End Select
ElseIf y = r.Bottom Then
Select Case _Cells(x, y)
Case FrameCellType.Empty
_Cells(x, y) = FrameCellType.Horizontal
Console.Write(Horizontal)
Case FrameCellType.Vertical, FrameCellType.TeeLeft, FrameCellType.TeeRight
_Cells(x, y) = FrameCellType.Cross
Console.Write(Cross)
Case FrameCellType.TopLeft, FrameCellType.TopRight
_Cells(x, y) = FrameCellType.TeeTop
Console.Write(TeeTop)
Case FrameCellType.BottomLeft, FrameCellType.BottomRight
_Cells(x, y) = FrameCellType.TeeBottom
Console.Write(TeeBottom)
End Select
End If
End If
Next
Next
Next
Console.ForegroundColor = forecolorDelta
Console.BackgroundColor = backcolorDelta
End Sub
Public Shared Sub SetCharacters(characters As String)
If characters.Length = 11 Then
_Characters = characters
Else
Throw New ArgumentException("Must supply exactly eleven characters.")
End If
End Sub
Public Shared Sub SetDoubleBar()
_Characters = "╚╝╬═╩╠╣╦╔╗║"
End Sub
Public Shared Sub SetSingleBar()
_Characters = "└┘┼─┴├┤┬┌┐│"
End Sub
End Class
Public Enum FrameCellType
Empty
Horizontal
Vertical
BottomLeft
BottomRight
TopLeft
TopRight
TeeBottom
TeeLeft
TeeRight
TeeTop
Cross
End Enum
Public Structure Rectangle
Public Left As Integer
Public Height As Integer
Public Top As Integer
Public Width As Integer
Public Function Right() As Integer
Return Left + Width - 1
End Function
Public Function Bottom() As Integer
Return Top + Height - 1
End Function
Public Sub New(l As Integer, t As Integer, w As Integer, h As Integer)
Left = l
Top = t
Width = w
Height = h
End Sub
End Structure
Other Languages
Visual Basic: Bir konsol uygulamasında ASCII metnin kenarlığını nasıl çizersiniz?(tr-TR)