Qtアプリでキーカスタマイズを行う方法


ご挨拶

Qt Advent Calendar 2018での2回目の投稿となりますKATO Kanryuです。
よろしくお願いします。

まず宣伝

  • QActionManager
    • アプリにQt Creatorと同等のキーボード/マウスショートカットのカスタマイズ機能を提供します(本稿)
    • マウスカスタマイズ機能はオリジナル
  • 世界最速の画像ビューアー、QuickViewerをQtで作りました。
  • QLanguageSelector
    • 言語切り替えUI(メニュー)を自動生成
    • Qtアプリを再ビルドなしに翻訳言語を増やせるようになります
    • Qtアプリをテキストエディタだけでリアルタイムに翻訳できるようにします
  • QFullscreenFrame
    • アプリをフルスクリーン表示させたときに、メインメニューやツールバーをスライド表示したいときがあると思います。それを実現するやつです。イベントハンドラを細かく設定することになるのでC++11推奨
  • QNamedPipe
    • アプリケーションを複数起動防止しつつ、2つ目以降のプロセスの起動オプションを1つ目のプロセスに引き渡したりする処理って単純ながら実装が面倒なものです。この問題を各OSのNamedPipeを使ってシンプルに解決するライブラリです。
    • 本家のQNamedPipeと異なり、QNetworkなどのコンポーネントは不要です。

GUIにおけるキーボードショートカット

大抵のアプリの場合、何らかの形でキーボードショートカットが設定されていることが多いです。例えばウェブブラウザで Ctrl+N を押せば新しいウィンドウが表示されますし、テキストエディタで Ctrl+V を押せばクリップボードに入っている文字列が画面のテキストに追加されるでしょう。

Qtアプリにおけるキーボードショートカット

Qtアプリの場合、アプリに対する各操作は基本的にQActionで定義していきます。QActionはGUI要素を兼ねており、QMenuにQActionのインスタンスを追加すると、そのままメニュー上の1項目として画面上に表示されます。

QActionにはname、shortcuts、各種Signals等があり、デザインパターンで言うところのCommandパターンを実現しています。QActionのインスタンスのtoggled Signalに何らかのイベントハンドラを登録しておけば、そのインスタンスがtriggered()されるごとにイベントを実行できる形ですね。

shortcutsプロパティにQKeySequenceのインスタンスを設定してメインメニューに登録しておくと、アプリを起動した状態でキーボードでそのショートカットを入力するだけで、メニューの選択なしにtriggered()が発動するようになります。

Note: この方式はわかりやすくてよくできているのですが、残念ながらメインメニュー自体を非表示にすると発動しなくなるようですね

キーボードカスタマイズを実現するには

QActionのインスタンスのshortcutsプロパティを書き換えればいいので、アプリのどこかにQMap<action名, QKeySequence>を保持しておき、mapが書き換わるごとにそれぞれのQActionのshortcutsを書き換えればいいことになりますね。

QKeySequenceとは

キーボード入力をデータモデル化したもので、シリアライズ可能になっているので文字列として書き出し、復元することができます。文字列化できるならiniファイルやレジストリなどに保存できますね。

QKeySequenceはDWORDと相互変換可能になっていて、1インスタンスごとに合計4キー入力までを表現可能です。

この場合の4キーというのは Ctrl+C のようなキー2個の同時押しを数えるわけではなく、例えばVisual Studioの行コメントのショートカットキーである Ctrl+K, Ctrl+C のような連続シーケンス(Emacs Style Key Sequencesと言うらしい)で2個と数えます。理屈上、4回キーを叩かないと発動しないようなショートカットキーが作れるわけですね。そんなもの作らないと思いますが。

Note: この2つのインスタンスは同じ値
QKeySequence("Ctrl+X, Ctrl+C");
QKeySequence(Qt::CTRL + Qt::Key_X, Qt::CTRL + Qt::Key_C);

複数のショートカットを登録したい場合

しかし、通常1つのQActionに複数のショートカットを登録したい場合、上記のような連続シーケンスを登録したいというよりも、例えばカーソルLeftキーとHキーを両方登録して、どちらかを押されたら左に動く動作をさせたい、というように、複数のショートカットキーを受け付けたい場合が圧倒的ではないでしょうか。そういうケースの場合、それぞれのショートカットキーを別々のQKeySequenceのインスタンスとし、QListに登録した上で、

void QAction::setShortcuts(const QList<QKeySequence> &shortcuts)

の方を呼び出す必要があります。この辺、しばらくわからなくてハマりました。

ともあれこのへんまでは説明されればそうなんだ、で終わりそうな話だと思います。

キーボード入力をカスタマイズする機能を作る

どうでしょう?

  • ショートカットキー入力をユーザーが実際にキー入力する形でも登録できるようにしたい
  • もちろんショートカットのテキスト表現をキーボード入力できるようにしたい

QtのダイアログってEscキーを押したら普通閉じますよね?
そういった通常のキー入力を適宜キャンセルして専用のUIを構築するってめんどくさくないですか?

そう思ったのでQActionManagerを作りました。キーボードカスタマイズ用のダイアログをuiファイルごと収録してるので、そのままあなたのアプリケーションのプロジェクトで参照していただければすぐに使える状態です。

画面はこんな感じです。

デフォルトのキー配置の初期設定、同じショートカットが複数のQActionにかぶらないようにする重複チェック、登録、削除、デフォルトに戻す機能など、シンプルですが一通り用意してあります。

マウスのショートカット機能

ではマウスのショートカット機能ってどうなるでしょう? 例えばウェブブラウザやテキストエディタでホイールを上下に回せばウィンドウ内が上下にスクロールするでしょう。動画プレーヤーならボリュームの増減につながっていたりするかもしれません。当然こういうものもカスタマイズしたいですよね。

マウス入力のモデル化

まず、マウス入力を起点にショートカットっぽいことを実現しようとしたら、それはどういう入力状態なのかを規定しないといけません。残念ながらQtにはQKeySequenceのような実装済みのコンポーネントは見当たらないようなので、QActionManagerではQMouseSequenceクラスを自作しています。

QMouseSequenceクラスはQKeySequenceと同様に文字列へのシリアライズが可能で、このような表現になります。

actionZoomIn="+::RightButton+WheelUp, Ctrl+::WheelUp"

上記の場合、右クリックをしながらホイール上回し、またはCtrlキーを押しながらホイール上回しでactionZoomInのQActionが発動します。

QuickViewerの設定用iniファイルから転記しましたが、左辺値がQActionのnameで、右辺の文字列がQMouseSequenceのシリアライズ表現となります。いろいろ検討した結果キーボードを押しながらマウス入力する可能性があり、キーボードショートカットを含めた表現方法が必要となりました。

するとどこかでキーボード入力とマウス入力を区切る必要があるので、そのセパレータとして +:: を採用しています。単に :: としない理由はキーボード側でNUM+ (テンキーの『+』)や『:』キーを押す可能性があり、その場合にうまくparseできなくなるためです。

ユーザー向けのカスタマイズ画面はこのようにしています。

QActionへのマウスショートカットの結びつけは自前で

ところが、残念ながらQAction側にマウスショートカットの登録機能はないので、アプリケーション側のイベントハンドラ上で

bool QWindow::eventFilter(QObject *obj, QEvent *event)

あたりから自前で拾ってくるしか無いようです。しょうがないのでこの辺は自力で作りましょう。

QuickViewerではこんな感じです。
https://github.com/kanryu/quickviewer/blob/master/QuickViewer/src/mainwindow.cpp

基本的な考え方としては、こんな感じです。

  1. ウィンドウ自体のイベントハンドラもしくはeventFilterでイベントを横取りする
  2. 現在のキーボード、マウスの入力状態をデータ化する。qApp->keyboardModifiers()、mouseEvent->buttons()などからQMouseValueクラスのインスタンスを作る
  3. QActionManager.getActionByKey(QMouseValue)でQActionのインスタンスが返ってきた場合、その発動をもって元のイベントハンドラを終了させる
  4. QActionがnullptrだった場合、ウィンドウ自体のイベントに処理を戻す

まとめ

Qt製の自作アプリに自由なショートカットカスタマイズ機能を実現するのは、地味ですが結構面倒な作業が必要です。今回紹介したライブラリを使えば少しは楽になるかもしれません。よろしければどうぞ。