さて、次は何を実装しよう。。。
が、vi コマンドを処理するためのクラスを導入する作業を先に行うことにする。
前稿までは、QPlainTextEdit の派生クラスに挿入・コマンドモード状態を持たせ、 keyEvent() をリインプリメントすることで vi コマンド処理を行っていた。 しかし、将来的にはビュークラスを変更するかもしれないし、:e 等の ex コマンドはビューが処理するより MainWindow のようなアプリ全体をコントロールするクラスが処理する方が適材適所で合理的である。
というわけで、vi/ex コマンドに関する処理を一手に引き受けるクラス:ViEngine を導入することにする。
1: #include <QObject> 2: class ViEngine : public QObject 3: { 4: Q_OBJECT 5: 6: public: 7: ViEngine(QObject *parent = 0); 8: ~ViEngine(); 9: };
ViEngine を QObject の派生クラスにし、最初に Q_OBJECT マクロを記述しているのは、 こうしておけば便利なシグナル・スロット機能を使えるようになるからである。
挿入・コマンドモード状態はViEngineが保持し、ViEngineのみが能動的に状態遷移させるものとする。
状態を定義した enum は ViEngine に移動する。
ただし、ViEditView::m_mode と、そのセッターゲッターは残しておき、セッターはスロット化し、
connect により状態を同期させるものとする。
ViEditView に送られたキーイベントは何らかの方法で ViEngine に送られ、処理される。
挿入モードの場合、Esc 以外はイベント処理を行わずリターンし、QPlainTextEdit::keyEvent() に処理させる。
vi コマンドはViEngineにより解析され、コマンドが確定した時点でそれぞれの処理を行う。
カーソル移動・編集操作の場合は ViEditView のメソッドを直接コールする。
現状、ビューはひとつだけだが、将来的にはMDIにするかもしれないので、 ViEngine はカレント ViEditView オブジェクトへのポインタを保持するものとする。 (※ 将来的には vi コマンドをサポートするビュークラスを追加するかもしれないが、 その時は ViEditView オブジェクトへのポインタではなく、viビューインタフェースを導入し、それへのポインタに変える)
とりあえず、ViEditView に ViEngine へのポインタを用意し、ViEditView::keyPressEvent(QKeyEvent *) から ViEngine::processKeyPressEvent(QKeyEvent *) をコールすることにしてみる。
1: class ViEngine; 2: class ViEditView : public QPlainTextEdit 3: { 4: ..... 5: public: 6: void setViEngine(ViEngine *viEngine) { m_viEngine = viEngine; } 7: ..... 8: private: 9: ViEngine *m_viEngine; 10: };
1: #include "ViEngine.h" 1: ..... 2: void ViEditView::keyPressEvent ( QKeyEvent * event ) 3: { 4: if( m_viEngine != 0 && m_viEngine->processKeyPressEvent(event) ) 5: return; 6: QPlainTextEdit::keyPressEvent(event); 7: }
1: bool ViEngine::processKeyPressEvent( QKeyEvent * event ) 2: { 3: switch( mode() ) { 4: case CMD: 5: return cmdModeKeyPressEvent(event); // コマンドモードの場合の処理 6: case INSERT: 7: return insModeKeyPressEvent(event); // 挿入モードの場合の処理 8: } 9: return false; 10: }
cmdModeKeyPressEvent(), insModeKeyPressEvent() は ViEditView で定義していたものを移動しただけ。
以上で、ViEngine クラスを定義し、vi コマンドに依存する部分を ViEditView から取り除くことができた。
このような実装でもそれほと悪いことは無いのだが、ViEngine と ViEditView が相互に依存しているのはいまいちである。
幸いなことに Qt にはイベントフィルターという機構がある。
イベントフィルターはイベントをフックする機構で、クラスをいっさい変更せずにクラスの挙動を変えることができる。
1: bool ViEngine::eventFilter(QObject *obj, QEvent *event) 2: { 3: if( obj == m_editor && event->type() == QEvent::KeyPress ) { 4: switch( mode() ) { 5: case CMD: 6: cmdModeKeyPressEvent(static_cast<QKeyEvent *>(event)); // コマンドモードの場合の処理 7: return true; 8: case INSERT: 9: return insModeKeyPressEvent(static_cast<QKeyEvent *>(event)); // 挿入モードの場合の処理 10: } 11: } 12: return false; 13: }
イベントフィルターのインストールは、ViEngine::setEditor(ViEditView *editor) で行えばおk。
void ViEngine::setEditor(ViEditView *editor) 1: { 2: m_editor = editor; 3: m_editor->installEventFilter(this); // イベントフィルターを設定 4: }
あとは、ViEditView::keyPressEvent() で ViEngine::processKeyPressEvent() をコールする部分を削除するだけだ。
1: //#include "ViEngine.h" 依存は無くなったので必要ない 1: ..... 2: void ViEditView::keyPressEvent ( QKeyEvent * event ) 3: { 4: //if( m_viEngine != 0 && m_viEngine->processKeyPressEvent(event) ) 5: // return; 6: QPlainTextEdit::keyPressEvent(event); 7: }
って言うか、もう ViEditView::keyPressEvent() をりインプリメントする必要性が無くなった。
こうしておけば、ViEditView から ViEngine への依存を完全に取り去ることができる。
下図に ViEditView が ViEditView のメソットをコールする場合(A)と、イベントフィルターを使用した場合(B)のシーケンス図を示す。
本稿のソースコードは以下からDLできます。
http://vivi.dyndns.org/dist2/qvi-003.zip
※ VS2008 でプロジェクトを作成しています。