Visual Basic: How to Draw a Border of ASCII Text in a Console Application

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)
Sort by: Published Date | Most Recent | Most Useful
Comments
Page 1 of 1 (6 items)