Linuxで kbhit() と getch()

やりたいこと

  • WindowsC言語では kbhit() と getch() を使ってキーボード入力を即時に取得できる。
  • LinuxC言語ではこれらの関数は用意されておらず、キーボード入力は1行ごとにバッファされ、Enterキーが入力されるまで取得できない。
  • Linuxでも kbhit() と getch() 相当の機能を実現したい。

ちなみに、Windowsでも現在では kbhit() と getch() は非推奨の関数名で、代わって _kbhit() と _getch() を使う。

#include <stdio.h>
#include <conio.h>

int main()
{
    while (1) {
        while (!_kbhit()) {;}
        int ch = _getch();
        printf("kbhit: %c\n", ch);
        if (ch == 'q') break;
    }
    printf("\nQuit\n");
    return 0;
}

方法

  • 方法はいろいろあるが、基本的には tcgetattr() / tcsetattr() でターミナルの設定を変更する。
  • 後述の参考サイトのソースを参考に実装した。
  • 処理の無駄を省いたかわり、最初に kb_begin()、最後に kb_end() を実行する。
  • 無駄を省いた作りにしたので使い方を間違えないように注意。
  • スレッドセーフでない作りだが、そもそもキーボード入力を複数のスレッドから受けることはないはず。

ソース

kbhit.h

#pragma once

void kb_begin(void);
void kb_end(void);
int  kbhit(void);
char getch(void);

kbhit.c

#include <stdio.h>
#include <termios.h>
#include <unistd.h>

#include "kbhit.h"

static struct termios g_old_set; // ターミナルの元の設定
static int g_read_char = -1;     // 読みだした文字 (-1のとき無効)

// 開始する
// (標準入力を、即時、エコーバックなし、ノンブロッキングに設定)
void kb_begin(void)
{
    // 現在の標準入力の設定を取得
    tcgetattr(STDIN_FILENO, &g_old_set);
    // 標準入力の設定を変更
    struct termios new_set = g_old_set;
    new_set.c_lflag &= ~ICANON; // 非カノニカル(改行を待たず即時入力)
    new_set.c_lflag &= ~ECHO;   // エコーバックなし
//  new_set.c_lflag &= ~ISIG;   // シグナル発生なし (Ctrl-Cなどの)
    new_set.c_cc[VMIN] = 0;     // 待ち受けしない (待ち受け文字数ゼロ)
    new_set.c_cc[VTIME] = 0;    // 待ち受けしない (待ち受け時間ゼロ)
    tcsetattr(STDIN_FILENO, TCSANOW, &new_set);
}

// 終了する
// (キーボード入力の設定を元に戻す)
void kb_end(void)
{
    // ターミナルの設定を戻す
    tcsetattr(STDIN_FILENO, TCSANOW, &g_old_set);
}

// キーボード入力があったかチェック
// return: 0:なし / 1:あり
int  kbhit(void)
{
    if(g_read_char != -1) return 1; // すでに入力があった
    
    // 標準入力から1文字読み出し
    char ch;
    int num = read(STDIN_FILENO, &ch, 1);
    // 読み出せたか?
    if(num == 1) {
        g_read_char = ch;
        return 1;
    }else{
        return 0;
    }
}

// キーボード入力を1文字取得
char getch(void)
{
    // すでに読みだした文字があればそれを返す
    if(g_read_char != -1) {
       char ch = g_read_char;
       g_read_char = -1;
       return (ch);
    }
    // なければ1文字読み出す (読み出せなければゼロを返す)
    else{
        char ch = 0;
        read(STDIN_FILENO, &ch, 1);
        return(ch);
    }
}

使い方

main.c

#include <stdio.h>
#include "kbhit.h"

int main()
{
    kb_begin();

    while (1) {
        while (!kbhit()) {;}
        char ch = getch();
        printf("kbhit: %c\n", ch);
        if (ch == 'q') break;
    }
    kb_end();

    printf("\nQuit\n");
    return 0;
}

CMakeLists.txt

cmake_minimum_required(VERSION 3.13)
project(kbhit_test C)
add_executable(kbhit_test main.c kbhit.c)

参考