自作エディターを作る! C言語でコンソールのカーソルを移動する

今回は予告通りキー移動について書いていきます。 前回までで矢印キーの入力検出はできているので、あとはコンソール上のカーソルを移動すればいいです。 あとはmessage_loop()関数でイベント処理をする仕組みについても紹介します。

前回の記事はこちらです。 r-mutax.hateblo.jp

カーソル移動実演

実際にカーソルを動かしているgifを貼っておきます。 まだ上限チェックをしていないので、空白にもガンガン動いていってしまいます。 f:id:sanseido:20201125000759g:plain

設計内容

まずはじめに今回のリビジョンを貼っておきます。 github.com

イベントメッセージの処理をする。

イベントを創出してメッセージキューに積む処理は以下の記事で出しているので、今回はそれを利用する側です。 r-mutax.hateblo.jp

メッセージループ

MMGetMessage()関数でイベントメッセージを受け取りながらmessage_procedure()でそれぞれのイベントハンドラに分配します。 ちなみにイベントメッセージを取得するMMGetMessage()関数はメッセージキューが空の場合は、内部でスリープするので帰ってきません。あとGetMessage()関数から名前を変えました。

void CEditControl::message_loop(){

    MESSAGE msg;
    // msg_id が 0(= MM_QUIT)になったらループを抜けて終了する
    while(int msg_id = MMGetMessage(msg) != MM_QUIT){
        if(msg_id == MM_ERROR){
            // エラー処理を入れる
        }
        message_procedure(msg);
    }
}

メッセージの分配

MESSAGE.idで分岐してメッセージごとのイベントハンドラを呼び出します。イベントハンドラOnxxxx()関数にしました。 気に入ってないところとしては、MM_CHANGE_WINSIZEのパラメータをここでfree()しているところですかね。これはちゃんと動いていないのでまた後で直します。

void CEditControl::message_procedure(MESSAGE& msg){
    
    switch(msg.id){
        case MM_KEYPRESS:
        {
            char input_key = (char)msg.lParam;
            OnKeyPress(input_key);
            break;
        }
        case MM_CHANGE_WINSIZE:
        {
            MSize ms = *(MSize*)msg.lParam;
            free((void*)msg.lParam);

            OnChangeWindowSize(ms);
           break;
        }
        case MM_PAINT:
            OnPaint();
            break;
        default:
            break;
    }
}

OnKeypress()

キープレスイベントのハンドラでの処理は下図のような感じになっています。 f:id:sanseido:20201123213127p:plain

編集モードによってキー処理を変えようと思っているので、まずkeygen()で現在のモードによる振り分けを行って、keygen_command_mode()などに処理を流します。 これらのモードで扱わない共通キー(方向キーなど)はkeygen_commonkey()で処理します。

今回は方向キー押下によって、カーソルを動かしたいので、以下にkeygen_commonkey()の処理を提示します。 やっていることは簡単で、引数で受け取った仮想キーによってswitchで処理を切り分けています。move_cursor()は仮想キーを受け取って、指定された方向にカーソルを動かして再表示します。

void CEditControl::keygen_commonkey(char key){
    switch (key)
    {
        case 'd':
            m_mode = DEBUG_MODE;
            break;
        case VK_UP:
            move_cursor(VK_UP);
            break;
        case VK_DOWN:
            move_cursor(VK_DOWN);
            break;
        case VK_RIGHT:
            move_cursor(VK_RIGHT);
            break;
        case VK_LEFT:
            move_cursor(VK_LEFT);
            break;
        case VK_ESCAPE:  // ESC
            m_mode = COMMAND_MODE;
            break;
        case QUIT_CHAR:
            m_mode = QUIT_OPERATION;
            break;
        case 0x35:  // PAGE_UP
            PageUp();
            break;
        case 0x36:  // PAGE_DOWM
            PageDown();
            break;
        default:
            break;
    }
}

move_cursor()

m_cursorは行と列をメンバに持つMSize型のCEditControlのメンバです。 move_cursor()ではm_cursorを変えてdisp_cursor()で表示しています。 これなんでデクリメント演算子使ってないんでしょうね。 disp_cursor()printf()している謎の文字列はターミナル制御文字で、カーソルをn行n列目に表示するためのものです。 詳細は以前紹介したこちらの記事が詳しいです。(参考させてもらいました。) dev.grapecity.co.jp

disp_cursor()で呼んでいるfflush()ですが、描画性能改善のためにstdoutをフルバッファリングしているので、カーソル描画文字を送るためにコールしています。 描画性能改善については別途記事にまとめます。

void CEditControl::move_cursor(char dir){
    switch(dir){
        case VK_UP: // up
            m_cursor.iRow -= 1;
            disp_cursor();
            break;
        case VK_DOWN: // down
            m_cursor.iRow++;
            disp_cursor();
            break;
        case VK_RIGHT: // right
            m_cursor.iColumn++;
            disp_cursor();
            break;
        case VK_LEFT: // left
            m_cursor.iColumn -= 1;
            disp_cursor();
            break;
        default:
            break;
    }
}

void CEditControl::disp_cursor(){
    printf("\033[%d;%dH", m_cursor.iRow, m_cursor.iColumn);
    fflush(stdout);
}

次回予告

move_cursor()の章でも触れましたが、現在作成しているテキストエディタは標準出力の頻度が多すぎて画面がちらつくという問題が発生していました。 次回は描画性能改善の取り組みについて語ります。