'Qbasic Highlighted Menu

Does anyone have an example code or instructions for making this work? I've just never been able to accomplish a highlighted menu that uses the arrow keys and enter for selections. Thanks in advance!

I anticipate this working by drawing boxes for each option, and redrawing the box in color while coloring the text when an option is selected. I'm just unsure how to design a loop to accomplish this. I'm pretty comfortable with the INKEY$ function and the SELECT CASE statement, but I don't know how to factor them in.



Solution 1:[1]

A highlighted menu will draw the menu and wait for a key press in a loop or using SLEEP. A common alternative was to simply change the text color of one of the first few letters, informing the user to press the corresponding key to select the corresponding menu option. For example, the letters Q in "Quit" and N in "New Game" would be a different color from the rest of the text in the line.

However, you're asking to use the arrow keys, so clearly you don't want to do it that way. How you highlight the current menu item depends on the screen mode in use. Screen modes 11, 12, and 13 don't allow you to specify a background color, and I can't get DOSBox to render a background with modes 7, 8, and 9. As a workaround to this issue, you could instead simply draw a box next to the current selection and erase the box (draw a black one or whatever your screen's background color is). Or you could just use an asterisk to avoid graphics/text size issues and simplify the code. Here's an example of the box style with arrow keys, WASD keys, and Vim-style keys (H=Left, J=Down, K=Up, L=Right) all supported, assuming a US-QWERTY keyboard is used. If you only wanted arrow keys, then you'd merely need to change the first (outer) SELECT CASE...END SELECT block to simply IF LEFT$(k$, 1) = CHR$(0) THEN...END IF while preserving the inner SELECT CASE...END SELECT block that works with extended keys.

'size% is used in the selIncDec subroutine.
DIM text$(0 TO 3)
DIM SHARED size%
size% = UBOUND(text$) - LBOUND(text$) + 1

selected% = 0

text$(0) = "Example 1"
text$(1) = "Example 2"
text$(2) = "Example 3"
text$(3) = "Example 4"

SCREEN 12

' Width and height of a text cell in pixels.
' I use 8x8 text cells for max screen compatibility, despite 8x16 looking better.
xpxText% = 8
ypxText% = 8

' See the documentation for SCREEN to determine which screen sizes are
' available with the screen mode you want to use.
' 80x60 for mode 12 results in 8x8 text cells. 80x30 results in 8x16 text cells.
WIDTH 80, 60

DO
    LOCATE 1, 1
    FOR i% = LBOUND(text$) TO UBOUND(text$)
        PRINT TAB(3); text$(i%)
        '    selected% = i%
        ' is an equality comparison, resulting in -1 for true and 0 for false.
        ' If false, -(0) * 2 = 0; if true, -(-1) * 2 = 2.
        LINE (0, i% * ypxText%)-STEP(xpxText% - 1, xpxText% - 1), -(selected% = i%) * 2, BF
    NEXT i%

    SLEEP
    k$ = INKEY$
    SELECT CASE UCASE$(LEFT$(k$, 1))
      ' Left -- does nothing
      CASE "H", "A"

      ' Right -- does nothing
      CASE "L", "D"

      ' Up
      CASE "K", "W"
        CALL selIncDec(selected%, -1)

      ' Down
      CASE "J", "S"
        CALL selIncDec(selected%, 1)

      ' Enter key
      CASE CHR$(13)
        EXIT DO

      ' Extended key, such as arrows.
      CASE CHR$(0)
        SELECT CASE RIGHT$(k$, 1)
          ' Left
          CASE "K"

          ' Right
          CASE "M"

          ' Up
          CASE "H"
            CALL selIncDec(selected%, -1)

          ' Down
          CASE "P"
            CALL selIncDec(selected%, 1)
        END SELECT
    END SELECT
LOOP

PRINT USING "You selected option #"; selected% + 1

END

SUB selIncDec (selected%, amtInc%)
    selected% = selected% + amtInc%
    IF selected% >= size% THEN
        selected% = selected% - size%
    ELSEIF selected% < 0 THEN
        selected% = selected% + size%
    END IF
END SUB

If you were using a screen mode that supports background colors or highlighting in some form, such as screen 0, you might be able to get away with simply "highlighting" the entire line's background in text mode. You need not specify the width of the screen to have a "reverse video" effect acting as highlighting, but it looks better when you have an entire line highlighted rather than just the text. After that menu item is printed, just change the colors back to the default and continue printing as usual. Below shows a few changes to the above code (screen mode, screen width setting, and menu display code), but it remains the same otherwise:

SCREEN 0
'8x8 text cells in SCREEN 0 for VGA adapters.
WIDTH 80, 43

...

    FOR i% = LBOUND(text$) TO UBOUND(text$)
        ' "Reverse video" highlighting.
        IF selected% = i% THEN COLOR 0, 7 ELSE COLOR 7, 0
        PRINT TAB(3); text$(i%); SPACE$(78 - LEN(text$(i%)))
    NEXT i%

    ' The screen will turn "white" when the last menu item is selected.
    ' This fixes the issue.
    COLOR 7, 0

    SLEEP
...

Note that I assumed a VGA adapter with a color display for all of the code above, which has long since been superseded by various other display adapter standards that are in use even on devices as small as smart watches.

You should be able to adapt the code to fit your needs. I designed it such that you could simply add menu items as you wished. Also, the display code itself is contained entirely in the FOR...NEXT loop with the functionality immediately following, so all you'd need to change is the stuff inside the FOR...NEXT loop to change how things are displayed.

Solution 2:[2]

I'm in the same boat, wanting to use SCREEN 12 for a menu, with background colour. As per http://www.qb64.net/wiki/index_title_COLOR/

SCREEN modes 12 and 13 can use the foreground parameter only in QB 4.5! Background color 0 can be changed using OUT.

I also came across this SUB:

SCREEN 12

ColourPrint "Hello", 4, 9
PRINT
ColourPrint "bob", 3, 10


SUB ColourPrint (t$, fg%, bg%)
    ' t$ = printing text
    ' fg% = foreground colour
    ' bg% = background colour
    DIM h%(1 + 32 * LEN(t$))
    x1% = 8 * (POS(0) - 1)
    y1% = 16 * (CSRLIN - 1)
    x2% = x1% + 8 * LEN(t$) - 1
    y2% = y1% + 15
    LINE (x1%, y1%)-(x2%, y2%), bg%, BF
    GET (x1%, y1%)-(x2%, y2%), h%()
    COLOR fg% XOR bg%
    PRINT t$;
    PUT (x1%, y1%), h%(), XOR
    ERASE h%
END SUB

enter image description here

This site has a bunch of menu examples that might help too:

http://www.brisray.com/qbasic/qmenu.htm

enter image description here

enter image description here

enter image description here

Solution 3:[3]

Basically you need a loop that:

  1. Keeps track of the current menu item selected (usually an index of the menu item: 1, 2, 3, ...) within a variable - let's call it SELECTED
  2. Draws the menu on the screen with a highlight for the selected item (you'll need to iterate over all menu items and highlight when SELECTED = current menu item index)
  3. Wait for a key to be pressed:
    1. If the key is UP then select previous menu item (SELECTED = SELECTED - 1)
    2. If the key is DOWN then select next menu item (SELECTED = SELECTED + 1)
    3. If the key is ENTER then exit menu and return the selected item

I have a full example here but for a text-based menu. You can adapt it to your problem. The main functionality is on the "menu" function:

DECLARE FUNCTION countItems% (items$)
DECLARE FUNCTION menu% (row%, col%, items$)
DECLARE FUNCTION widestItemLength% (items$)

DEFINT A-Z

CONST SEPARATOR = ";"

CLS
selectedMenuOption = menu(1, 1, "APPLE;BANANA;ORANGE;EXIT")

PRINT
PRINT
PRINT "Selected menu option:"; selectedMenuOption

' Returns how much items we have in the menu
'
FUNCTION countItems (items$)

count = 1

DO
    i = INSTR(i + 1, items$, SEPARATOR)
    IF i > 0 THEN count = count + 1
LOOP UNTIL i = 0

countItems = count

END FUNCTION

' Main menu functionality
' 1. shows the menu highlighting the selected item
' 2. wait for a key to be pressed
'   a. UP -> select previous item
'   b. DOWN -> select next
'   c. ENTER -> exit and returns the index of the current selected item
'
FUNCTION menu (row%, col%, items$)

widestLength = widestItemLength(items$)
itemCount = countItems(items$)
selected = 1    ' menu item index starting by 1

DO
    ' Prints the menu on the screen
    itemEnd = 0
    LOCATE row%, col%
    FOR i = 1 TO itemCount
        itemStart = itemEnd + 1
        itemEnd = INSTR(itemStart, items$, SEPARATOR)
        IF itemEnd = 0 THEN itemEnd = LEN(items$) + 1
        item$ = MID$(items$, itemStart, itemEnd - itemStart)
        item$ = " " + item$ + SPACE$(widestLength - LEN(item$)) + " "
        IF selected = i THEN COLOR 0, 7 ELSE COLOR 7, 0
        LOCATE CSRLIN + 1, col: PRINT item$;
    NEXT

    WHILE INKEY$ <> "": WEND            ' Clears the keyboard buffer
    DO: k$ = INKEY$: LOOP WHILE k$ = "" ' Waits for a key to be pressed

    SELECT CASE k$
        ' Up key pressed - select previous menu item
        CASE CHR$(0) + "H": IF selected > 1 THEN selected = selected - 1
        ' Down key pressed - select next menu item
        CASE CHR$(0) + "P": IF selected < itemCount THEN selected = selected + 1
    END SELECT

LOOP UNTIL k$ = CHR$(13)    ' Loops until the key pressed is enter

menu = selected ' returns the selected menu item index

COLOR 7, 0

END FUNCTION

' Returns the size of the widest menu item
'
FUNCTION widestItemLength (items$)

widestLen = 0
itemEnd = 0

DO
    itemStart = itemEnd + 1
    itemEnd = INSTR(itemStart, items$, SEPARATOR)
    IF itemEnd = 0 THEN itemEnd = LEN(items$) + 1
    itemLen = itemEnd - itemStart
    IF itemLen > widestLen THEN widestLen = itemLen
LOOP UNTIL itemEnd = LEN(items$) + 1

widestItemLength = widestLen

END FUNCTION

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2
Solution 3 Hiro