コンピューター:C言語講座:1バイト単位での文字入力について
概要
最近はGUI(グラフィカル・ユーザー・インターフェース)が主流となり、ターミナルモードでキー入力などを行うアプリケーションの開発は少なくなってきていますが、簡単なテストプログラムなど、いちいち重たいGUIをつけるのが面倒な場合意外と多いのではないでしょうか。
その場合に問題になるのが、キー入力で、テストプログラムくらいなら1行単位の読み込みで十分だと思いますが、もう少し細かい入力制御を行いたい場合にリターンキーを押されてはじめてアプリケーションに渡されるのでは困ることもよくあります。MS-DOS時代のCの処理系にはgetch(),kbhit()と言うようないわゆるシステムコールを直接行って1文字直接読み込みやキー入力状態をチェックすることができましたが、UNIXではそのような関数・システムコールは準備されていません。
UNIXの場合はキー入力もファイル入力もすべて同じ扱いで、デバイスからの入力となります。デバイスの中でも特に端末からの入力という分類で、端末モードを設定することにより入力処理の方法を設定します。おなじgetchar()でも端末モードにより見掛け上の動きが変わってきます。端末モードの設定はコマンドとしてsttyコマンドがあり、コマンドラインで直接sttyコマンドを使用して端末モードを変更することももちろん可能ですが、tcshなどのシェルはプロンプトを出す際にシェル自身で端末モードを毎回設定しているのでsttyコマンドで設定しても次のプロンプトが出たときには変更されてしまっています。shなどでは毎回端末モードを設定していないようなので実験できますが変な設定にするとそのままログアウトしたりするので気をつけましょう。
話がそれてしまいましたが、Cで端末モードを設定するにはioctl(),fcntl()などを使用していたのですが、最近はtermios関数を使用することが多いようで、この方が簡単に使用できるようです。そこで、今回はtermios関数を使って端末モードの設定を行ってみます。
サンプル
1バイト単位で入力を受け取る実験をしてみます。標準では端末でリターンキーを入力するまでアプリケーション側では何も入力を受け取れず、入力されたキーも勝手に画面にエコー(表示)されますが、端末モードを変更し、1バイト単位でアプリケーションが受け取り、画面表示もアプリケーションが行うようにしてみます。
#include <stdio.h> #include <termios.h> #include <unistd.h> #include <sys/filio.h> void main() { unsigned char c; /* 1バイト単位入力モードへ */ StdinRaw(1); while(1){ if(ioctl(0,FIONREAD,&bytes)==-1||bytes==0){ /* キー入力無し */ continue; } c=getchar(); if(c==0x04){ /* ^D */ break; } if(isascii(c)&&isprint(c)){ putchar(c); } else{ printf("[%02x]",c); } } /* 元のモードに戻す */ StdinRaw(0); } int StdinRaw(int flag) { static struct termios tio_save; struct termios tio; if(flag==1){ if(tcgetattr(0,&tio)==-1){ perror("tcgetattr"); return(-1); } tio_save=tio; tio.c_iflag&=~(BRKINT|ISTRIP|IXON); /* 「入力モード」 ブレークにシグナルを送らない 入力文字を 7 ビットにストリップしない START/STOP 出力制御を不可能にする */ tio.c_lflag&=~(ICANON|IEXTEN|ECHO|ECHOE|ECHOK|ECHONL); /* 「ローカルモード」 標準的な入力 (ERASEおよびKILL処理)を不可能にする VEOLZ,VSWTCH,VREPRINT,VDISCARD,VDSUSP,VWERASE,VLNEXT特殊制御文字を無効にする 入力されたすべての文字を表示しない ERASE文字をバックスペース-空白文字-バックスペースの文字列として表示しない KILL文字の後のNLを表示しない NLをエコーしない */ tio.c_cc[VMIN]=1; /* 受信するべき文字の最小数 */ tio.c_cc[VTIME]=0; /* バーストで短期のデータ伝送をタイムアウトするために使用する0.10秒単位のタイマ */ if(tcsetattr(0,TCSANOW,&tio)==-1){ perror("tcsetattr"); return(-1); } return(0); } else{ if(tcsetattr(0,TCSANOW,&tio_save)==-1){ perror("tcsetattr"); return(-1); } return(0); } }
StdinRaw()関数に1を渡すと、端末モードを1バイト単位の入力に設定します。ソース中にコメントで何を設定しているのかを記入しておきました。アプリケーション終了時に元の状態に戻すため、はじめにtcgetattr()で設定を得て、それをスタティックに保持しています。得た情報に対し、必要なフラグの操作を行い、tcsetattr()で設定します。
main()関数は端末モードの設定を行い、標準出力のバッファリングを無くし、無限ループに入ります。はじめにioctl()で入力待ちを調べていますが、これはMS-DOS版のkbhit()に相当するものとして入れてみました。サンプルでは特に意味はありませんが、入力が無い場合に別の処理を行いたい場合には有効です。getchar()で1バイト受取り、コントロールDで終了、それ以外はプリント可能であればそのまま表示し、制御文字などは16進表示しています。このサンプルでは漢字も16進表示となります。リターンキーやバックスペース・タブなども16進表示されるので、実行してみれば普段と違うことがわかるでしょう。
StdinRaw()の端末モードの設定で、c_cc[]に対する設定で動きがいろいろと変わります。VMINを0にすると1バイトも無くてもリターンしたり、VTIMEを0より大きくすると入力がない場合のタイムアウトを設定できたりします。詳細はtermioのオンラインマニュアルなどに詳しく書かれていますので参照してください。
まとめ
viなどでESCキーや、その他のキーをどのようにアプリケーションで受け取っているかはわかったと思います。ただ、まじめにエディターを作ろうとするともう少し細かい制御が必要で、ESCキーとファンクションキーの区別が難しかったりします。ファンクションキーはESC+[+Aなど、エスケープコード+数バイトで1キーを表し、単純にESCキーが押されたのか、ファンクションキーが押されたかの判断が必要になったりします。アラームを使ったタイマーで実現したりしますが、興味のある方は調べてみてください。