Tip Menu module

Grayda

Member
Joined
May 20, 2010
Messages
16
Programming Experience
10+
Good afternoon everyone,

While working on my first console app, I had the need to create an interactive menu. There were some solutions about, but most of them required the screen to be cleared with each key press. This wasn't a good solution for me so I came up with my own. You pass it two string arrays -- one for the text to display, one for the values to return and optionally a welcome message to display. The result is a vertical menu that you can use the up / down arrows on to select an item. The screen is never cleared during the process, so you can preserve previously entered text.

Usage:
VB.NET:
menu(text(), values(), [message])

Example:
VB.NET:
Sub Main()
        Dim selection As String

        selection = menu({"Option 1", "Option 2", "Option 3", "Option 4", "Option 5"}, {"1", "2", "3", "4", "5"}, "")
        Console.WriteLine()
        Console.WriteLine("You have selected: " & selection)
        Console.ReadKey()
    End Sub

Returns:
VB.NET:
The selection as string (1, 2, 3, 4 or 5 in the example above)

Output:
VB.NET:
Please select an option:

>> 1
   2
   3
   4
   5

The code:

VB.NET:
Function menu(ByVal text() As String, ByVal values() As String, Optional ByVal welcometext As String = "Choose one of the following:")
        ' The currently selected item
        Dim curIndex As Integer

        ' For our loop
        Dim i As Integer

        ' The original position of our cursor BEFORE the menu is written
        ' (This lets us overwrite our menu without having to clear the screen)
        Dim t As Integer
        t = Console.CursorTop + 1

        ' Gotos? Bad! But for brevity's sake .. :B
writemenu:
        ' Re-set our cursor position
        Console.CursorTop = t

        ' Spit out our welcome text, then a blank line for neatness
        Console.WriteLine(welcometext)
        Console.WriteLine()

        ' Loop through the values()
        For i = 0 To values.Length - 1
            ' If i = our currently selected item
            If i = curIndex Then
                ' Highlight it
                Console.ForegroundColor = ConsoleColor.Black
                Console.BackgroundColor = ConsoleColor.Gray
                ' Put >> in front to denote it's the selected item
                Console.Write(">> " & text(i).ToString)
                ' Reset our console colours
                Console.ForegroundColor = ConsoleColor.Gray
                Console.BackgroundColor = ConsoleColor.Black
                ' Pad the rest out with spaces (to cover up any menu artifacts
                ' that come with mismatched line lengths)
                Console.WriteLine(New String(" ", 40))
            Else ' If this isn't our selected item, then
                ' Spit out the text (indented with three spaces)
                ' then pad the rest with spaces to clean up
                Console.WriteLine("   " & text(i).ToString & New String(" ", 40))
            End If
        Next
        ' Uh oh, 2 gotos?
selectagain:
        ' Read one key from the console and return it as the variable called "key"
        Dim key As ConsoleKeyInfo = Console.ReadKey(True)

        Select Case key.Key.ToString
            Case "UpArrow"
                ' If we have room to move (that is, if we're not at the start
                ' of the menu)
                If curIndex > 0 Then
                    ' Increment the selected index by one.
                    ' When the menu is redrawn, the new item appears to be selected
                    curIndex -= 1
                Else ' If we're at the start of the menu and we're trying to go up
                    ' "Scroll" to the bottom menu option
                    curIndex = text.Count - 1
                End If

            Case "DownArrow"
                If curIndex < text.Length - 1 Then
                    curIndex += 1
                Else
                    ' Same as before, but if we're at the bottom, go to the top
                    curIndex = 0
                End If

                ' We've decided
            Case "Enter"
                ' Return the value that corresponds with curIndex
                Return values(curIndex)

            Case Else ' Otherwise
                ' Go back, read another key and do it all again
                GoTo selectagain
        End Select

        ' Just in case
        GoTo writemenu

    End Function

It should be ready to copy and paste into your app. There might be some bugs that I'm not sure about (e.g. titles longer than 40 characters etc.) but this is something you should be able to work with.

It also needs some Try .. Catch statements in there, because if you mismatch the number of text / values items, it'll chuck up an error but my deadline doesn't really give me a whole lot of time for that :p

Enjoy!
 
It also needs some Try .. Catch statements in there, because if you mismatch the number of text / values items
Function should validate that each array has same length, and if not throw an ArgumentException. You can avoid that trouble by using a dictionary, since all keys here must have a value.
VB.NET:
                If curIndex > 0 Then
                    ' Increment the selected index by one.
                    ' When the menu is redrawn, the new item appears to be selected
                    curIndex -= 1
                Else ' If we're at the start of the menu and we're trying to go up
                    ' "Scroll" to the bottom menu option
                    curIndex = text.Count - 1
                End If
A shorter version of this is to use the If operator:
curIndex = If(curIndex > 0, curIndex - 1, text.Count - 1)

' Gotos? Bad! But for brevity
Goto will in most cases be replaced with a Do or While loop or putting code in their own method that can be called repeatedly, or both. For example in your code you have this main structure (where Do-Loop is a Goto in your code):
VB.NET:
Do 
   'write menu
   'get navigation key
Loop
In the 'get navigation key' part you have another Goto that can be replaced with a function that loops until it gets a correct nagivation key, for example like this:
    Function GetNavigationKey() As ConsoleKey
        Do
            Dim info As ConsoleKeyInfo = Console.ReadKey(True)
            Select Case info.Key
                Case ConsoleKey.UpArrow, ConsoleKey.DownArrow, ConsoleKey.Enter
                    Return info.Key
            End Select
        Loop
    End Function

Then in main menu loop you can do this, and notice here ConsoleKey enumeration is used rather than some hardcoded string values:
            Select Case GetNavigationKey()
                Case ConsoleKey.UpArrow
                    curIndex = If(curIndex > 0, curIndex - 1, text.Count - 1)
                Case ConsoleKey.DownArrow
                    curIndex = If(curIndex < text.Count - 1, curIndex + 1, 0)
                Case ConsoleKey.Enter
                    Return values(curIndex)
            End Select

These two last code fragments also shows separation of concerns (getting the key and using the key are two different operations), which is a good OOP coding practise.
 
Back
Top