다음을 통해 공유


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)