Linux上基于C++文本的选择菜单的行为很奇怪

C++ text-based selection menu on Linux behaving strangely?

本文关键字:菜单 选择 C++ 文本 Linux      更新时间:2023-10-16

代码有点长,但这只是因为我评论了所有,所以很容易阅读。基本上,这是一个简单的基于文本的选择菜单,我正在工作。您需要在Linux上,并且有一个C++11编译器才能正常运行。以下是代码(功能齐全的示例,准备编译):

#include <string>
#include <vector>
#include <iostream>
#include <unistd.h>
#include <sys/ioctl.h>
#include "raw_terminal.h" // for setting the terminal to raw mode
using namespace std;
/* Simple escape sequences to control the cursor and colors on the screen */
#define CLI_HIDE_CUR            "33[?25l"
#define CLI_SHOW_CUR            "33[?25h"
#define CLI_SAVE_CUR_POS        "33[s"
#define CLI_REST_CUR_POS        "33[u"
#define CLI_CLR_LAST_LINE       "33[A33[2K"
#define CLI_MV_CUR_UP           "33[A"
#define CLI_MV_CUR_DN           "33[B"
#define CLI_DEFAULT_COLOR       "33[0m"
#define CLI_FGROUND_BLACK       "33[0;30m"
#define CLI_FGROUND_RED         "33[0;31m"
#define CLI_FGROUND_GREEN       "33[0;32m"
#define CLI_FGROUND_BROWN       "33[0;33m"
#define CLI_FGROUND_BLUE        "33[0;34m"
#define CLI_FGROUND_MAGENTA     "33[0;35m"
#define CLI_FGROUND_CYAN        "33[0;36m"
#define CLI_FGROUND_LIGHTGREY   "33[0;37m"
#define CLI_BOLD                "33[0;1m"
#define CLI_BGROUND_BLACK       "33[7;30m"
#define CLI_BGROUND_RED         "33[7;31m"
#define CLI_BGROUND_GREEN       "33[7;32m"
#define CLI_BGROUND_BROWN       "33[7;33m"
#define CLI_BGROUND_BLUE        "33[7;34m"
#define CLI_BGROUND_MAGENTA     "33[7;35m"
#define CLI_BGROUND_CYAN        "33[7;36m"
#define CLI_BGROUND_LIGHTGREY   "33[7;37m"

/* Centers a string in a 'width' character wide terminal
 by appending spaces before and after the string */
auto centerText(string& text, int width) -> void;
/* Creates a selection menu on the screen */
auto selectionMenu(std::vector<std::string> items) -> int;
int main() {
    std::vector<string> items;
    items.push_back("Menu item one");
    items.push_back("Menu item two");
    items.push_back("Menu item three");
    int selection = selectionMenu(items);
    if (selection == -1) return 0;
    cout << "You selected: " << items[selection] << endl;
    cin.get();
    return 0;
}
auto centerText(string& text, int width) -> void {
    size_t len = text.length();
    if (width <= len+1) return;
    for (int i=0; i<(width - len)/2; i++) text.insert(text.begin(), ' ');
    for (int i=0; i<(width - len)/2+len%2; i++) text.push_back(' ');
    return;
}
auto selectionMenu(std::vector<std::string> items) -> int {
    /* This stuff is required to get the width of the terminal
     in order to center the text on the screen with centerText() */
    struct winsize w;
    ioctl(0, TIOCGWINSZ, &w);
    /* Hide the cursor, initialize some variables */
    cout << CLI_HIDE_CUR << CLI_DEFAULT_COLOR << endl;
    int selection = 0, prevSelection, key;
    /* Center the menu items on the screen */
    for (const auto& s : items) centerText(s, w.ws_col);
    /* Print out the menu items */
    for (const auto& s : items) cout << s << endl;
    /* Highlight the first item */
    for (int i=0; i<items.size(); i++) cout << CLI_MV_CUR_UP;
    cout << CLI_BGROUND_BROWN << items[selection] << endl;
    /* Configure stuff so that we're able to retrieve raw keystrokes from stdin */
    raw_terminal::setRawTerminal();
    /* If the enter key is down, wait until it's released.
     This prevents the user from accidentally selecting
     an item after hitting enter in a previous menu. */
    while (getchar() == 'n') { usleep(1000); }
    /* Main loop */
    while (1) {
        key = getchar();
        /* We're only interested in escape sequences (starting with '33') */
        if (key == '33') {
            /* If nothing comes after the escape character, then esc was pressed, so we quit. */
            if (getchar() == -1) {
                selection = -1;
                goto MENU_END;
            }
            /* Get the next character in the received sequence */
            key = getchar();
            /* up arrow */
            if (key == 65) {
                prevSelection = selection;
                selection--;
            }
            /* down arrow */
            else if (key == 66) {
                prevSelection = selection;
                selection++;
            }
            /* If (first item - 1) or (last item + 1) is selected, loop around */
            if (selection < 0) selection = items.size()+selection;
            if (selection > items.size()-1) selection -= items.size();
            /* Draw the previously selected line with the default colors */
            cout << CLI_MV_CUR_UP << CLI_DEFAULT_COLOR << items[prevSelection] << endl;
            cout << CLI_MV_CUR_UP;
            /* Move the cursor to the new selection */
            if (selection < prevSelection) for (int i=0; i<(prevSelection-selection); i++) cout << CLI_MV_CUR_UP;
            if (selection > prevSelection) for (int i=0; i<(selection-prevSelection); i++) cout << CLI_MV_CUR_DN;
            /* Draw the newly selected line with the highlighting color */
            cout << CLI_BGROUND_BROWN << items[selection] << endl;
        }
        /* If the retrieved key is not an escape sequence, check whether it's the enter key.
         If so, break the main loop and return the selected item's number. */
        else if (key == 'n') break;
    }
MENU_END:
    cout << CLI_DEFAULT_COLOR;
    /* Position the cursor below the menu to continue */
    for (int i=0; i<items.size()-selection; i++) cout << CLI_MV_CUR_DN;
    /* Unhide the cursor, and set the terminal back to normal mode */
    cout << CLI_SHOW_CUR << endl;
    raw_terminal::restoreTerminal();
    /* Return selected item's number */
    return selection;
}

这是raw_terminal.h:

#ifndef _RAW_TERMINAL_H_
#define _RAW_TERMINAL_H_
#include <cstring>
#include <iostream>
#include <termios.h>
class raw_terminal {
public:
    static void setRawTerminal() {
        /* set the terminal to raw mode */
        if (isRaw) return;
        tcgetattr(fileno(stdin), &orig_term_attr);
        memcpy(&new_term_attr, &orig_term_attr, sizeof(struct termios));
        new_term_attr.c_lflag &= ~(ECHO|ICANON);
        new_term_attr.c_cc[VTIME] = 0;
        new_term_attr.c_cc[VMIN] = 0;
        tcsetattr(fileno(stdin), TCSANOW, &new_term_attr);
        isRaw = true;
    }
    static void restoreTerminal() {
        tcsetattr(fileno(stdin), TCSANOW, &orig_term_attr);
        isRaw = false;
    }
private:
    static struct termios orig_term_attr;
    static struct termios new_term_attr;
    static bool isRaw;
};
struct termios raw_terminal::orig_term_attr;
struct termios raw_terminal::new_term_attr;
bool raw_terminal::isRaw = false;
#endif

如果只使用向上/向下箭头,效果会很好。但如果你向左或向右按(这不应该有任何作用),它会完全扰乱屏幕,复制菜单项和其他东西。我想问题不在代码中,因为它完全忽略了左右键。我想是终端在按下这些键时会做一些事情。那么我该如何防止这种情况发生呢?

如有任何帮助,我们将不胜感激。谢谢

哇,这让我有点震惊。我通过在程序中包含左右箭头来解决问题,如下所示:

if (key == 65 || key == 68) // ...
if (key == 66 || key == 67) // ...

通过这种方式,左右键可以正常更改选择。我本来想忽略这些键,只使用向上/向下键,但这仍然比让屏幕疯狂要好。