Отловить сочетание клавиш в консоли

132
18 декабря 2020, 03:40

1) Код должен быть или кроссплатформенный, или 2а варианта кода (под Windows и Linux)

2) Мне нужно отследить, когда пользователь нажмет Shift+Enter

3) Программа консольная

Answer 1

Можно так:

#include <iostream>
#include <array>
#include <vector>
#include <set>
#include <functional>

template <typename T> void throw_if(T val, T err, const char* msg)
{
    if (val == err) { throw std::runtime_error(msg); }
}

template <typename T> void throw_if_not(T val, T ok, const char* msg)
{
    if (val != ok) { throw std::runtime_error(msg); }
}

enum class Key_t
{
    Unknown,
    Shift,
    Return,
    Esc
};

#if defined(_MSC_VER)
#include <windows.h>
Key_t to_Key_t(WORD vk)
{
    switch (vk) {
        case VK_SHIFT: return Key_t::Shift;
        case VK_RETURN: return Key_t::Return;
        case VK_ESCAPE: return Key_t::Esc;
        default: return Key_t::Unknown;
    }
}
void read_kb(std::function<bool(Key_t, bool)> key_handler)
{
    auto stdout_handle = GetStdHandle(STD_INPUT_HANDLE);
    throw_if(stdout_handle, INVALID_HANDLE_VALUE, "could not get STD_INPUT_HANDLE");
    DWORD orig_mode;
    throw_if(GetConsoleMode(stdout_handle, &orig_mode), FALSE, "could not get console mode");
    throw_if(SetConsoleMode(stdout_handle, orig_mode | ENABLE_WINDOW_INPUT), FALSE, "could not set console mode");
    try {
        for (bool done = false; !done; ) {
            std::array<INPUT_RECORD, 128> ir;
            DWORD count;
            throw_if(ReadConsoleInput(stdout_handle, ir.data(), ir.size(), &count), FALSE, "coud not read console input");
            std::for_each(std::cbegin(ir), std::cbegin(ir) + count, [&done, key_handler](const INPUT_RECORD& r) {
                if (r.EventType == KEY_EVENT) {
                    const auto& e = r.Event.KeyEvent;
                    done = key_handler(to_Key_t(e.wVirtualKeyCode), e.bKeyDown);
                }
            });
        }
    }
    catch (...) {
        SetConsoleMode(stdout_handle, orig_mode);
        throw;
    }
    SetConsoleMode(stdout_handle, orig_mode);
}
#else
#include <termios.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/kd.h>
bool is_a_console(int fd)
{
    char arg = 0;
    return (isatty(fd) && ioctl(fd, KDGKBTYPE, &arg) == 0 && ((arg == KB_101) || (arg == KB_84)));
}
int open_a_console(const char *fn)
{
    int fd = open(fn, O_RDWR);
    if (fd < 0) { fd = open(fn, O_WRONLY); }
    if (fd < 0) { fd = open(fn, O_RDONLY); }
    if (fd < 0) { return -1; }
    return fd;
}
int getfd()
{
    int fd;
    std::vector<const char*> fn = {
        "/proc/self/fd/0",
        "/dev/tty",
        "/dev/tty0",
        "/dev/vc/0",
        "/dev/systty",
        "/dev/console"
    };
    for (const auto& s : fn) {
        fd = open_a_console(s);
        if (is_a_console(fd)) { return fd; }
        close(fd);
    }
    for (fd = 0; fd < 3; fd +=1)
        if (is_a_console(fd)) { return fd; }
    throw std::runtime_error("could not get console fd");
}
Key_t to_Key_t(const char c)
{
    switch (c) {
    case 1: return Key_t::Esc;
    case 42:
    case 54: return Key_t::Shift;
    default: return Key_t::Unknown;
    }
}
void read_kb(std::function<bool(Key_t, bool)> key_handler)
{
    int fd = getfd();
    termios orig;
    throw_if_not(tcgetattr(fd, &orig), 0, "could not get attr");
    int kbmode_orig;
    throw_if_not(ioctl(fd, KDGKBMODE, &kbmode_orig), 0, "could not get KDGKBMODE");
    termios raw = orig;
    raw.c_lflag &= ~(ECHO | ICANON);
    raw.c_cc[VMIN] = 0;
    raw.c_cc[VTIME] = 1;
    tcsetattr(fd, TCSANOW, &raw);
    throw_if_not(ioctl(fd, KDSKBMODE, K_RAW), 0, "could not set KDSKBMODE");
    try {
        for (bool done = false; !done; ) {
            char c = 0;
            if (read(fd, &c, 1) == 1) {
                done = key_handler(to_Key_t(c & 0x7F), !(c & 0x80));
            }
        }
    }
    catch (...) {
        tcsetattr(fd, TCSANOW, &orig);
        ioctl(fd, KDSKBMODE, kbmode_orig);
        throw;
    }
    tcsetattr(fd, TCSANOW, &orig);
    ioctl(fd, KDSKBMODE, kbmode_orig);
}
#endif
bool is_Shift_Return(const std::set<Key_t>& keys)
{
    return keys.size() == 2 &&
           keys.find(Key_t::Shift) != std::end(keys) &&
           keys.find(Key_t::Return) != std::end(keys);
}
int main()
{
    try {
        std::set<Key_t> keys;
        read_kb([&keys](Key_t key, bool is_down) {
            if (is_down) { keys.insert(key); } else { keys.erase(key); }
            std::cout
                << (is_Shift_Return(keys) ? "Shift+Return" : "Unknown")
                << " key is "
                << (is_down ? "down" : "up")
                << std::endl;
            return (key == Key_t::Esc);
        });
    }
    catch (const std::exception& e) {
        std::cout << "Error: " << e.what() << std::endl;
        return 1;
    }
    return 0;
}

Проверено на Win 10, Ubintu 16.04.

В обычном режиме коды специальных клавиш (Shift, Alt, Ctrl, и т.п.) получить не удастся, т.к. ввод буферизируется и транслируется в символы (или строки). Поэтому консоль переводится в "сырой" режим (без буферизации и трансляции).

Коды клавиш: Windows - Virtula-Key Codes, Linux - одним из способов Keyboard input.

Работа с консолью в Windows - все достаточно тривиально, и описано в доках Reading Input Buffer Events. В Linux - см. ответ и ссылки из него - https://stackoverflow.com/a/29446193/2267114, плюс код утилиты showkey, плюс доступное описание кодов ioctl.

READ ALSO
Проблема с PlaySoundA

Проблема с PlaySoundA

Воспроизвожу звук так:

116
unsigned char* и char* при работе со строками

unsigned char* и char* при работе со строками

Могут ли возникнуть проблемы с работой со строками если использовать не char* /const char*, а unsigned char*/const unsigned char*? Если все правильно, к примеру, в UTF-8,...

118