Writing menus

I wanted to write an article on menus for a while now, and a post on Pete's QB Site finally gave me the motivation to do so. Thanks Knothead1008 (if that's your real name.)

Menus are essentially a safe environment designed to allow users to modify variables and access specific parts of your program. All in all, items contained within each menu can only do three things: switch to another menu, trigger functions, and modify variables. How do we do that?

The very best of types and arrays

If you know what both user-defined types and arrays are, skip ahead. If it's the first time you ever heard about these things, bear with me a moment.

A type is like a collection of variables within a variable. Let's take a customer file. For each customer, there is a group of information you need to keep track of: a name, a phone number, a unique shop id, and maybe their birthday. You COULD write something like this:

' Customer 1
DIM customer1name AS STRING * 12
DIM customer1phone AS STRING * 10
DIM customer1birth AS STRING * 8
DIM customer1shopId AS INTEGER

' Customer 2
DIM customer2name AS STRING * 12
DIM customer2phone AS STRING * 10
DIM customer2birth AS STRING * 8
DIM customer2shopId AS INTEGER

' Customer 3
DIM customer3name AS STRING * 12
DIM customer3phone AS STRING * 10
DIM customer3birth AS STRING * 8
DIM customer3shopId AS INTEGER

' Give me a call if you ever need more than 3 customers.
' Yours truly, Mike Hawk (programmer extraordinaire) - XOXO

But that'd be a terrible idea. The easiest way to store all of these fields into one package would be a user-defined type and it would look like this:

' Declaring our user-defined type. It's like a variable template if you will...
' Big downsides: they cannot contain variable-length strings or arrays.
TYPE customerType
  cName AS STRING * 12 ' Customer name
  phone AS STRING * 10 ' Phone number
  birth AS STRING * 8  ' Birthday
  shopId AS INTEGER    ' Unique id
END TYPE

' The DECLARE statement doesn't understand user-defined types, use "ANY" instead.
DECLARE SUB printClient(c AS ANY)

' Set variable "customer" as a "customerType" type
DIM customer AS customerType

' Initialize customer data. Fields (members) are accessed by simply placing
' a dot after the "customer" variable; members work exactly like any other variable.
customer.cName = "DARYL HALL"
customer.phone = "7192662837" ' or "719-26-OATES"
customer.birth = "10111946"
customer.shopId = 1

' You can pass user-defined types to routines
printClient customer

' And here's a custom routine to display variables of "customerType" type
SUB printClient(c AS customerType)
  DIM m AS STRING * 3

  PRINT " Name: " + c.cName
  PRINT "Phone: " + MID$(c.phone, 1, 3) + "-" + MID$(c.phone, 4, 3) + "-"+ MID$(c.phone, 7, 4)
  m = MID$("JANFEBMARAPRMAYJUNJULAUGSEPOCTNOVDEC", 1 + (VAL(MID$(c.birth, 1, 2) ) - 1) * 3, 3)
  PRINT "Birth: " + m + " " + MID$(c.birth, 3, 2) + ", " + MID$(c.birth, 5, 4)
  PRINT "   ID:"; c.shopId
END SUB

By the way, if you write a user-defined type into a file (or read it from a file,) all members will be filled at once: you don't have to go through each member manually:

' This:
OPEN "CUSTOMER.DAT" FOR BINARY AS #1
PUT #1, , customer
CLOSE #1

' Is the equivalent of that:
OPEN "CUSTOMER.DAT" FOR BINARY AS #1
PUT #1, , customer.cName
PUT #1, , customer.phone
PUT #1, , customer.birth
PUT #1, , customer.shopId
CLOSE #1

And you can copy two variables of the same type like you would any other variable:

DIM customer1 AS customerType
DIM customer2 AS customerType

' This:
customer2 = customer1

' Is the equivalent of that:
customer2.cName = customer1.cName
customer2.phone = customer1.phone
customer2.birth = customer1.birth
customer2.shopId = customer1.shopId

Now. If your shop attracts more customers, you may need to have more than one variable to store customer data:

' As requested over the phone, here are more customers.
' Try not to be so successful in the future - Mike Hawk.
DIM customer0 AS customerType
DIM customer1 AS customerType
DIM customer2 AS customerType
DIM customer3 AS customerType
DIM customer4 AS customerType
DIM customer5 AS customerType
DIM customer6 AS customerType
DIM customer7 AS customerType
DIM customer8 AS customerType
DIM customer9 AS customerType

...it MAY work for a while if you're being very enthusiastic about programming, copy-pasting lines and lines of code, and if you don't need to sort and search your data or do anything too complex with it... thankfully, there's a much simpler and flexible solution: arrays!

Arrays are collections of identical variables, all grouped under the same name but with an index system to identify each entry (element.) So instead of "customer#" you have "customer(#)":

' You can send whole arrays into routines... if you really need to.
DECLARE FUNCTION findCustomer%(c() AS ANY, cName AS STRING)

' Declare variables
DIM customer(0 TO 99) AS customerType ' Here's an array of 100 elements.
DIM tmp AS INTEGER                    ' Your run-of-the-mill integer variable.

' Initialize the first two elements of the "customer()" array:
customer(0).cName = "DARYL HALL"
customer(0).phone = "7192662837"
customer(0).birth = "10111946"
customer(0).shopId = 1
customer(1).cName = "JOHN OATES"
customer(1).phone = customer(0).phone ' Same phone number...
customer(1).birth = "04071948"
customer(1).shopId = 2

' Where's JOHN OATES?
tmp = findCustomer%(customer(), "JOHN OATES")
IF (tmp = -1) THEN
  PRINT "Customer not found"
ELSE
  printCustomer customer(tmp)
END IF

' A function to find a customer by name (returns the element
' index in the passed array.)
FUNCTION findCustomer%(c() AS customerType, cName AS STRING)
  ' Assume we don't have that entry
  findCustomer% = -1

  ' Search the whole array (from lower to upper bound)
  FOR i% = LBOUND(c) TO UBOUND(c)
    IF (RTRIM$(c(i%).cName) = cName) THEN
      findCustomer% = i%
      EXIT FOR
    END IF
  NEXT i%
END FUNCTION

And that's it. See you next t-- oh. Right. The menu thing.

Building a simple menu

Let's start with something very simple: a cursor and a list of items. We want the user to remain in the same loop until he leaves the menu by pressing escape. It's very straightfoward:

' Menu variables
DIM item(7) AS STRING, itemCount AS INTEGER
DIM itemThis AS INTEGER, itemPrev AS INTEGER

' Force a menu redraw, see the "redraw menu" section at the end of the loop
itemPrev = -1

' Create a simple menu of 5 items
itemCount = 5
item(0) = "EXECUTE MULTIFLUX POLARIZATION!"
item(1) = "META VERTEX CONFIGURATION..."
item(2) = "ITERATIONS"
item(3) = "NUMBER OF BUNNIES"
item(4) = "QUIT TO DOS"

' Main loop
DO
  '' -- CATCH USER INPUT -- ''
  SELECT CASE INKEY$
  ' Press up
  CASE CHR$(0) + "H"
    itemThis = itemThis - 1 ' Move to previous item
    IF (itemThis < 0) THEN itemThis = 0 ' Cap

  ' Press down
  CASE CHR$(0) + "P"
    itemThis = itemThis + 1 ' Move to next item
    IF (itemThis >= itemCount) THEN itemThis = itemCount - 1 ' Cap

  ' Press escape
  CASE CHR$(27)
    EXIT DO ' Quit menu loop
  END SELECT

  '' -- REDRAW MENU -- ''
  IF (itemPrev <> itemThis) THEN
    ' Cancel redraw and clear screen
    itemPrev = itemThis
    CLS

    ' Draw menu
    FOR i% = 0 TO itemCount - 1
      LOCATE 4 + i%, 8: PRINT item(i%)
    NEXT i%

    ' Draw cursor
    LOCATE 4 + itemThis, 6: PRINT ">"
  END IF
LOOP

' ...and we're done.
END

What we have here is very convenient because if we add or remove items from the menu, we only have to change ONE variable (itemCount) and the code will process the new menu same as the old, without problem.

As a side note: it's possible to remove the line after changing itemThis by including the evaluation within the change: itemThis = itemThis + (itemThis > 0) will only decrease itemThis if it is greater than 0. Similarly, itemThis = itemThis - (itemThis < itemCount - 1) will only increase itemThis if it is lower than itemCount - 1. The evaluation of a variable (for instance x = y) will either return 0 (FALSE) or -1 (TRUE.) So instead of just adding or removing 1, we can use the result of the evaluation instead. Don't worry, it won't be on the test. It's just a fun way to make your code faster and less readable.

Submenus!

We have a moving cursor. It's great and all but a little limited. What if we want sub menus? Each item should have a behavior attached to it, so when the cursor is next to a specific item and the user presses enter, the code should process the attached action. For instance, an item could have a "switch to menu" behavior and an extra value would tell us which menu to display. While we're at it, let's also include an "exit to DOS" behavior.

The code is fairly similar to the previous one except it now has a dedicated block to redefine items according to the current menu, the "catch user input" section can process enter, and we've got a user-defined type for our items:

' Item behaviors
CONST itemSwitchTo = 1
CONST itemSystem = 2

' Custom "menu item" type
TYPE itemType
  label AS STRING * 32
  behavior AS INTEGER
  index AS INTEGER
END TYPE

' Menu variables
DIM item(7) AS itemType, itemCount AS INTEGER
DIM itemThis AS INTEGER, itemPrev AS INTEGER
DIM menuThis AS INTEGER

' Select and setup menu index 0. Yes, we're going to
' use a GOSUB for that, you pearl-clutching elitist.
menuThis = 0
GOSUB initMenu

' Main loop
DO
  '' -- CATCH USER INPUT -- ''
  SELECT CASE INKEY$
  ' Press up
  CASE CHR$(0) + "H"
    itemThis = itemThis - 1 ' Move to previous item
    IF (itemThis < 0) THEN itemThis = 0 ' Cap

  ' Press down
  CASE CHR$(0) + "P"
    itemThis = itemThis + 1 ' Move to next item
    IF (itemThis >= itemCount) THEN itemThis = itemCount - 1 ' Cap

  ' Press enter
  CASE CHR$(13)
    SELECT CASE item(itemThis).behavior
    CASE itemSwitchTo
      menuThis = item(itemThis).index
      GOSUB initMenu
    CASE itemSystem
      EXIT DO
    END SELECT

  ' Press escape
  CASE CHR$(27)
    EXIT DO ' Quit menu loop
  END SELECT

  '' -- REDRAW MENU -- ''
  IF (itemPrev <> itemThis) THEN
    ' Cancel redraw and clear screen
    itemPrev = itemThis
    CLS

    ' Draw menu
    FOR i% = 0 TO itemCount - 1
      LOCATE 4 + i%, 8: PRINT item(i%).label
    NEXT i%

    ' Draw cursor
    LOCATE 4 + itemThis, 6: PRINT ">"
  END IF
LOOP

' ...and we're done.
END


'' -- MENU INITIALIZATION -- ''
initMenu:
  ' Make sure we reset behaviors!
  FOR i% = LBOUND(item) TO UBOUND(item)
    item(i%).behavior = 0
  NEXT i%

  ' Force redraw menu, place cursor
  itemPrev = -1
  itemThis = 0

  ' Initialize the item() array for the current menu
  SELECT CASE menuThis
  CASE 0
    itemCount = 5
    item(0).label = "EXECUTE MULTIFLUX POLARIZATION!"
    item(1).label = "META VERTEX CONFIGURATION..."
    item(1).behavior = itemSwitchTo: item(1).index = 1
    item(2).label = "ITERATIONS"
    item(3).label = "NUMBER OF BUNNIES"
    item(4).label = "QUIT TO DOS"
    item(4).behavior = itemSwitchTo: item(4).index = 2

  CASE 1
    itemCount = 3
    item(0).label = "VERTEX SIZE"
    item(1).label = "TECHNOBABBLE THING"
    item(2).label = "GO BACK"
    item(2).behavior = itemSwitchTo: item(2).index = 0

  CASE 2
    itemCount = 2
    item(0).label = "YES"
    item(0).behavior = itemSystem
    item(1).label = "NO"
    item(1).behavior = itemSwitchTo: item(1).index = 0
  END SELECT
RETURN

The main advantage of this type of design is that you can add or remove items easily, move them from one menu to another, switch their places, change their label, and you won't have to modify the main code: everything's processed the same way...

We could use the "itemSwitchTo" behavior with "index" -1 to replace behavior "itemSystem" if we wanted, but it's not important. The goal here is to find a way to process any menu with the same code. I could easily code a routine to execute the multiflux polarization, but I'll let you do it as even a toddler knows how to execute a multiflux polarization. Seriously, it's well documented and obvious. Just do the search.

The part where it gets a little dirtier

Very simple menus can get away with a case-per-case processing of variables (especially if the menu is known at run-time like it is now.) In practice, we're going to create a new "itemVariable" behavior and use the "index" member to identify the variable we want to modify. This behavior will also tell us what needs to be displayed when we draw the menu.

Again, the code is roughly identical to what we've been doing so far, with only a handful of exceptions: a few more constants, proper processing of the left and right keys, and a tiny change in the display block:

' Item behaviors
CONST itemSwitchTo = 1
CONST itemSystem = 2
CONST itemVariable = 3

' Variables indices
CONST vIteration = 0
CONST vBunnies = 1
CONST vVertSize = 2
CONST vBabble = 3

' Custom "menu item" type
TYPE itemType
  label AS STRING * 32
  behavior AS INTEGER
  index AS INTEGER
END TYPE

' Menu variables
DIM item(7) AS itemType, itemCount AS INTEGER
DIM itemThis AS INTEGER, itemPrev AS INTEGER
DIM menuThis AS INTEGER

' Technobabble variables
DIM junkIteration AS INTEGER, junkBunnies AS INTEGER
DIM junkVertexSize AS INTEGER, junkBabble AS INTEGER

' Select and setup menu index 0. Yes, we're going to
' use a GOSUB for that, you pearl-clutching elitist.
menuThis = 0
GOSUB initMenu

' Main loop
DO
  '' -- CATCH USER INPUT -- ''
  SELECT CASE INKEY$
  ' Press up
  CASE CHR$(0) + "H"
    itemThis = itemThis - 1 ' Move to previous item
    IF (itemThis < 0) THEN itemThis = 0 ' Cap

  ' Press down
  CASE CHR$(0) + "P"
    itemThis = itemThis + 1 ' Move to next item
    IF (itemThis >= itemCount) THEN itemThis = itemCount - 1 ' Cap

  ' Press enter
  CASE CHR$(13)
    SELECT CASE item(itemThis).behavior
    CASE itemSwitchTo
      menuThis = item(itemThis).index
      GOSUB initMenu
    CASE itemSystem
      EXIT DO
    END SELECT

  ' Press left
  CASE CHR$(0) + "K"
    IF (item(itemThis).behavior = itemVariable) THEN
      SELECT CASE item(itemThis).index
      CASE vIteration
        junkIteration = junkIteration + (junkIteration > 0)
      CASE vBunnies
        junkBunnies = junkBunnies + (junkBunnies > 0)
      CASE vVertSize
        junkVertexSize = junkVertexSize + (junkVertexSize > 0)
      CASE vBabble
        junkBabble = junkBabble + (junkBabble > 0)
      END SELECT
      itemPrev = -1
    END IF

  ' Press right
  CASE CHR$(0) + "M"
    IF (item(itemThis).behavior = itemVariable) THEN
      SELECT CASE item(itemThis).index
      CASE vIteration
        junkIteration = junkIteration - (junkIteration < 15)
      CASE vBunnies
        junkBunnies = junkBunnies - (junkBunnies < 3)
      CASE vVertSize
        junkVertexSize = junkVertexSize - (junkVertexSize < 10)
      CASE vBabble
        junkBabble = junkBabble - (junkBabble < 2)
      END SELECT
      itemPrev = -1
    END IF

  ' Press escape
  CASE CHR$(27)
    EXIT DO ' Quit menu loop
  END SELECT

  '' -- REDRAW MENU -- ''
  IF (itemPrev <> itemThis) THEN
    ' Cancel redraw and clear screen
    itemPrev = itemThis
    CLS

    ' Draw menu
    FOR i% = 0 TO itemCount - 1
      LOCATE 4 + i%, 8: PRINT RTRIM$(item(i%).label);
      IF (item(i%).behavior = itemVariable) THEN
        SELECT CASE item(i%).index
        CASE vIteration
          PRINT ":"; junkIteration
        CASE vBunnies
          PRINT ": " + MID$("NONEFEW MANYALL ", 1 + junkBunnies * 4, 4)
        CASE vVertSize
          PRINT ":"; junkVertexSize
        CASE vBabble
          PRINT ": " + MID$("NORMAL AVERAGEEXTREME", 1 + junkBabble * 7, 7)
        END SELECT
      ELSE
        PRINT ""
      END IF
    NEXT i%

    ' Draw cursor
    LOCATE 4 + itemThis, 6: PRINT ">"
  END IF
LOOP

' ...and we're done.
END


'' -- MENU INITIALIZATION -- ''
initMenu:
  ' Make sure we reset behaviors!
  FOR i% = LBOUND(item) TO UBOUND(item)
    item(i%).behavior = 0
  NEXT i%

  ' Force redraw menu, place cursor
  itemPrev = -1
  itemThis = 0

  ' Initialize the item() array for the current menu
  SELECT CASE menuThis
  CASE 0
    itemCount = 5
    item(0).label = "EXECUTE MULTIFLUX POLARIZATION!"
    item(1).label = "META VERTEX CONFIGURATION..."
    item(1).behavior = itemSwitchTo: item(1).index = 1
    item(2).label = "ITERATIONS"
    item(2).behavior = itemVariable: item(2).index = vIteration
    item(3).label = "NUMBER OF BUNNIES"
    item(3).behavior = itemVariable: item(3).index = vBunnies
    item(4).label = "QUIT TO DOS"
    item(4).behavior = itemSwitchTo: item(4).index = 2

  CASE 1
    itemCount = 3
    item(0).label = "VERTEX SIZE"
    item(0).behavior = itemVariable: item(0).index = vVertSize
    item(1).label = "TECHNOBABBLE THING"
    item(1).behavior = itemVariable: item(1).index = vBabble
    item(2).label = "GO BACK"
    item(2).behavior = itemSwitchTo: item(2).index = 0

  CASE 2
    itemCount = 2
    item(0).label = "YES"
    item(0).behavior = itemSystem
    item(1).label = "NO"
    item(1).behavior = itemSwitchTo: item(1).index = 0
  END SELECT
RETURN

While it's easier to maintain than a "everything-is-hard-coded" type of menu, the current design suffers from the variable handling system: there are three blocks full of conditional steps and it's going to be maintenance hell. If you think this could work for you, stick to it...

The alternative is to have temporary variables attached to each item. This would require a block to copy the actual variables to the temporary variables, and another to do the opposite. It's either that or getting dirty with pointers (which is my go-to solution in C, but with QuickBASIC it's a little harder to pull off - but possible, I did it in previous projects - more about that later.)

Indirect variables

To keep things simple, we'll only stick to integer variables with two caps (low and high.) The tricky part is the display (so we can have strings like we've used for junkBunnies and junkBabble in the previous example.

' Item behaviors
CONST itemSwitchTo = 1
CONST itemSystem = 2
CONST itemVariable = 3
CONST itemSaveChanges = 4

' Variables indices
CONST vIteration = 0
CONST vBunnies = 1
CONST vVertSize = 2
CONST vBabble = 3

' Custom "menu item" type
TYPE itemType
  label AS STRING * 32
  behavior AS INTEGER
  index AS INTEGER
END TYPE

' Temporary variables for menus
TYPE menuVarType
  labels AS STRING * 58
  value AS INTEGER
  min AS INTEGER
  max AS INTEGER
END TYPE

' Menu variables
DIM item(7) AS itemType, menuVar(3) AS menuVarType, itemCount AS INTEGER
DIM itemThis AS INTEGER, itemPrev AS INTEGER, menuThis AS INTEGER

' Extra common variables
DIM ofs1 AS INTEGER, ofs2 AS INTEGER, tmp AS INTEGER

' Technobabble variables
DIM junkIteration AS INTEGER, junkBunnies AS INTEGER
DIM junkVertexSize AS INTEGER, junkBabble AS INTEGER

' Select and setup menu index 0. Yes, we're going to
' use a GOSUB for that, you pearl-clutching elitist.
menuThis = 0
GOSUB initMenu

' Main loop
DO
  '' -- CATCH USER INPUT -- ''
  SELECT CASE INKEY$
  ' Press up
  CASE CHR$(0) + "H"
    itemThis = itemThis - 1 ' Move to previous item
    IF (itemThis < 0) THEN itemThis = 0 ' Cap

  ' Press down
  CASE CHR$(0) + "P"
    itemThis = itemThis + 1 ' Move to next item
    IF (itemThis "= itemCount) THEN itemThis = itemCount - 1 ' Cap

  ' Press enter
  CASE CHR$(13)
    SELECT CASE item(itemThis).behavior
    CASE itemSwitchTo
      menuThis = item(itemThis).index
      GOSUB initMenu
    CASE itemSystem
      EXIT DO
    CASE itemSaveChanges
      FOR i% = 0 TO itemCount - 1
        IF (item(i%).behavior = itemVariable) THEN
          tmp = menuVar(item(i%).index).value
          SELECT CASE item(i%).index
          CASE vIteration
            junkIteration = tmp
          CASE vBunnies
            junkBunnies = tmp
          CASE vVertSize
            junkVertexSize = tmp
          CASE vBabble
            junkBabble = tmp
          END SELECT
        END IF
      NEXT i%
    END SELECT

  ' Press left
  CASE CHR$(0) + "K"
    IF (item(itemThis).behavior = itemVariable) THEN
      tmp = menuVar(item(itemThis).index).value             ' copy value from temporary variable
      tmp = tmp + (tmp > menuVar(item(itemThis).index).min) ' decrease value
      menuVar(item(itemThis).index).value = tmp             ' copy value back to temporary variable
      itemPrev = -1
    END IF

  ' Press right
  CASE CHR$(0) + "M"
    IF (item(itemThis).behavior = itemVariable) THEN
      tmp = menuVar(item(itemThis).index).value             ' copy value from temporary variable
      tmp = tmp - (tmp < menuVar(item(itemThis).index).max) ' increase value
      menuVar(item(itemThis).index).value = tmp             ' copy value back to temporary variable
      itemPrev = -1
    END IF

  ' Press escape
  CASE CHR$(27)
    EXIT DO ' Quit menu loop
  END SELECT

  '' -- REDRAW MENU -- ''
  IF (itemPrev <> itemThis) THEN
    ' Cancel redraw and clear screen
    itemPrev = itemThis
    CLS

    ' Draw menu
    FOR i% = 0 TO itemCount - 1
      LOCATE 4 + i%, 8: PRINT RTRIM$(item(i%).label);
      IF (item(i%).behavior = itemVariable) THEN
        ofs1 = 0
        ofs2 = 0
        tmp = item(i%).index
        IF (ASC(LEFT$(menuVar(tmp).labels, 1)) = 32) THEN
          PRINT ":"; menuVar(tmp).value
        ELSE
          FOR j% = menuVar(tmp).min TO menuVar(tmp).value
            ofs1 = ofs2 + 1
            ofs2 = INSTR(ofs1, menuVar(tmp).labels, " ")
          NEXT j%
          PRINT ": " + MID$(menuVar(tmp).labels, ofs1, ofs2 - ofs1)
        END IF
      ELSE
        PRINT ""
      END IF
    NEXT i%

    ' Draw cursor
    LOCATE 4 + itemThis, 6: PRINT ">"
  END IF
LOOP

' ...and we're done.
END


'' -- MENU AND VARIABLES INITIALIZATION -- ''
initMenu:
  ' Reset temporary variables so they match actual variables
  menuVar(vIteration).value = junkIteration
  menuVar(vIteration).min = 0
  menuVar(vIteration).max = 15
  menuVar(vIteration).labels = ""

  menuVar(vBunnies).value = junkBunnies
  menuVar(vBunnies).min = 0
  menuVar(vBunnies).max = 3
  menuVar(vBunnies).labels = "NONE FEW MANY ALL"

  menuVar(vVertSize).value = junkVertexSize
  menuVar(vVertSize).min = 0
  menuVar(vVertSize).max = 10
  menuVar(vVertSize).labels = ""

  menuVar(vBabble).value = junkBabble
  menuVar(vBabble).min = 0
  menuVar(vBabble).max = 2
  menuVar(vBabble).labels = "NORMAL AVERAGE EXTREME"

  ' Make sure we reset behaviors!
  FOR i% = LBOUND(item) TO UBOUND(item)
    item(i%).behavior = 0
  NEXT i%

  ' Force redraw menu, place cursor
  itemPrev = -1
  itemThis = 0

  ' Initialize the item() array for the current menu
  SELECT CASE menuThis
  CASE 0
    itemCount = 5
    item(0).label = "EXECUTE MULTIFLUX POLARIZATION!"
    item(1).label = "META VERTEX CONFIGURATION..."
    item(1).behavior = itemSwitchTo: item(1).index = 1
    item(2).label = "ITERATIONS"
    item(2).behavior = itemVariable: item(2).index = vIteration
    item(3).label = "NUMBER OF BUNNIES"
    item(3).behavior = itemVariable: item(3).index = vBunnies
    item(4).label = "QUIT TO DOS"
    item(4).behavior = itemSwitchTo: item(4).index = 2

  CASE 1
    itemCount = 4
    item(0).label = "VERTEX SIZE"
    item(0).behavior = itemVariable: item(0).index = vVertSize
    item(1).label = "TECHNOBABBLE THING"
    item(1).behavior = itemVariable: item(1).index = vBabble
    item(2).label = "SAVE CHANGES"
    item(2).behavior = itemSaveChanges
    item(3).label = "GO BACK"
    item(3).behavior = itemSwitchTo: item(3).index = 0

  CASE 2
    itemCount = 2
    item(0).label = "YES"
    item(0).behavior = itemSystem
    item(1).label = "NO"
    item(1).behavior = itemSwitchTo: item(1).index = 0
  END SELECT
RETURN

With this design, you only have two sections to check: the temporary variable initialization (before the items initialization) and in the handler for itemSaveChanges. Speaking of which, it becomes your responsibility to preserve the content of the temporary variables whenever necessary as changing menus will reset the temporary variables to the actual values. Depending on how you organize your menus, this might be the best option. Of course, this is only a suggestion.

Pointers mumbo-jumbo

If you feel courageous enough to read this section, you're probably courageous enough to implement it within the menu system on your own (the following code only demonstrates how to read and write variables via pointers.) If not, it's still a neat trick.

Essentially, this technique will completely ignore the variables and go straight to the memory address where the content resides, which means you don't need to code specific blocks to manipulate pre-determined variables:

CLS
RANDOMIZE TIMER

TYPE settingsType   ' OFS SZE  The value under the OFS column is the offset in
  x AS INTEGER      ' 000 002  bytes, SZE is the size in bytes. When an array
  y AS INTEGER      ' 002 002  or user-defined type is created, all elements
  z AS INTEGER      ' 004 002  and members are stored contiguously in memory,
  s AS STRING * 10  ' 006 010  forming a whole buffer. We need that offset!
END TYPE            ' 16 BYTES - Dummy settings

' And here's the variable that will contain all our settings. It's easier to
' stuff everything into one user-defined type because we only need to find the
' memory segment of that variable, rather than hunt for multiple variables.
DIM settings AS settingsType

' Let's assign random values to .x, .y, .z, and .s
settings.x = INT(RND * 1000)
settings.y = INT(RND * 1000)
settings.z = INT(RND * 1000)
settings.s = "myString"

' Show us
PRINT : PRINT "SETTINGS"
PRINT "  x ="; settings.x
PRINT "  y ="; settings.y
PRINT "  z ="; settings.z
PRINT "  s = "; CHR$(34); settings.s; CHR$(34)

' Now we're going to create a type that will contain the information we need
' to retrieve members within the settingsType structure:
TYPE varType
  label AS STRING * 10 ' Variable name, up to 10 characters.
  offset AS INTEGER     ' Starting offset within the user-defined type.
  strLen AS INTEGER    ' Size of STRING member. 0 for an INTEGER.
END TYPE

' This table will tell us where every member is located and how long it is. It
' can easily be extended to support other data type: curently it only works
' with INTEGERS (if .strLen is 0) and STRINGS (if .strLen is at least 1.) In C
' and some other languages, the offset can easily be obtained with OFFSETOF().
DIM v(3) AS varType

v(0).label = "x": v(0).offset = 0: v(0).strLen = 0
v(1).label = "y": v(1).offset = 2: v(1).strLen = 0
v(2).label = "z": v(2).offset = 4: v(2).strLen = 0
v(3).label = "s": v(3).offset = 6: v(3).strLen = 10

' Alright, now let's ask the user to select a variable. vId will contain the
' index within the v() array;
DIM vId AS INTEGER, user AS STRING

' Let's check in the table for any variable that has the name provided by the
' user (enter nothing to quit the program.)
DO
  PRINT : INPUT "Select a variable by name: ", user
  IF (LEN(user) = 0) THEN SYSTEM
  FOR vId = 0 TO UBOUND(v)
    IF (LCASE$(user) = RTRIM$(LCASE$(v(vId).label))) THEN EXIT DO
  NEXT vId
  PRINT "This variable doesn't exist. Try another."
LOOP

' Alright, now we can retrieve the data straight from memory with some voodoo.
' First, we need to locate the settings variable in memory.
DIM vSeg AS INTEGER, vOfs AS INTEGER

vSeg = VARSEG(settings) ' get segment
vOfs = VARPTR(settings) ' get offset
DEF SEG = vSeg          ' jump to segment

' The member we're looking for starts .offset away from the beginning of the
' settings variable... so we just add that value to vOfs:
vOfs = vOfs + v(vId).offset

' Now, if we PEEK at vOfs, we can obtain the first byte of the member. But how
' many bytes do we need to read? If .strLen is under 1, it is an INTEGER (only
' 2-byte long.) If .strLen is equal or bigger than 1, it's a STRING (the size
' in bytes is .strLEN.)
DIM vInt AS INTEGER, vStr AS STRING

PRINT : PRINT "The current value of "; CHR$(34); user; CHR$(34); " is";
IF (v(vId).strLen < 1) THEN
  vInt = PEEK(vOfs + 1) * &H100 + PEEK(vOfs)
  PRINT vInt
ELSE
  vStr = SPACE$(v(vId).strLen)
  FOR i% = 0 TO v(vId).strLen - 1
    MID$(vStr, 1 + i%, 1) = CHR$(PEEK(vOfs + i%))
  NEXT i%
  PRINT " "; CHR$(34); vStr; CHR$(34)
END IF

' That's neat. But can we modify the content of the variable?
PRINT: INPUT "Set a new value: ", user

' We need to process the input a little bit so it doesn't completely break the
' limits of the target member: if we overflow (write too many bytes) we will
' modify the next member in the user-defined type!
IF (v(vId).strLen < 1) THEN
  vInt = cint(val(user))
  POKE (vOfs + 1), vInt \ &h100
  POKE (vOfs ), vInt AND &hFF
ELSE
  IF (LEN(user) > v(vId).strLen) THEN ' remove excess characters
    user = LEFT$(user, v(vId).strLen)
  ELSEIF (LEN(user) < v(vId).strLen) THEN ' pad with spaces
    user = user + SPACE$(v(vId).strLen - LEN(user))
  END IF
  FOR i% = 0 TO v(vId).strLen - 1
    POKE vOfs + i%, ASC(MID$(user, 1 + i%, 1))
  NEXT i%
END IF

' Show us
PRINT : PRINT "NEW SETTINGS"
PRINT "  x ="; settings.x
PRINT "  y ="; settings.y
PRINT "  z ="; settings.z
PRINT "  s = "; CHR$(34); settings.s; CHR$(34)

Be careful though: QuickBASIC is known to constantly move variables in memory to ensure that a large free block is available for new variables. I would recommend requesting the variable segment of the setting variable before reading and writing, just in case it moved.

Conclusion

There you have it, a working menu system. It's not optimized nor is it fool-proof, but it covers enough things to give you an idea of what to do next (optimize redrawing time, creating scrolling menus when too many items are being displayed at once, adding an actual graphic interface,... it really depends on your needs).

I would recommend placing the menu code inside its own function, so it can return values that could be processed by the parent loop. This way, you HAVE to quit the menu in some shape or form before proceeding to another part of your code rather than nesting everything on top of the menu loop:

DO
  SELECT CASE menuFunction%
  CASE 0
    EXIT DO ' terminate program
  CASE 1
    gameLoop ' execute main game loop - when we're done, go back to the menu
  END SELECT
LOOP

END

The possibilities are endless and I'm sure you can come up with creative ways (also download this thing) to write menus! Come on! Be wild! Be creative! Drink detergent! Wait no. Don't drink detergent please. Thanks.