◆ショートカットキーの処理 - VC++ 2005 MFC

[ ホーム ] [ 上へ ]

ショートカットキーはメニュー上のコマンドと同じ処理をCtrl + 'A'や Alt + 1などの キーの組み合わせで一発で処理できる便利な機能です。 今回はVC++ 2005でこの機能の実装をメモします。

ショートカットキーはメニュー上のコマンドの場合が多いですが、メニューが関わると ステップ数も多くなるので、ダイアログ上にコマンドボタンを作成して、そのクリックイベントを Ctrl + Aで処理するショートカットとして実装します。

・概要

ショートカットをコマンドとして処理するには押されたキーに対してメッセージが ディスパッチされる前に目的のWM_COMMANDメッセージに変換する必要があります。その変換用のAPIが

TransrateAccelerator(HWND hWnd, HACCEL hAccTable, LPMSG lpMsg) です。

このAPIは押されたショートカットキーのメッセージ(WM_KEYDOWNなど)をlpMsgに受取り、 hAccTableで指定したアクセラレータテーブルに従ってWM_COMMANDなどに変換し、直接hWndの ウィンドウへメッセージを送ります。

TranslateAcceleratorの第二引数に指定するのは アクセラレータテーブルのハンドルであるHACCEL型ですが、このハンドルを取得するには

  1. LoadAccelerators(HINSTANCE hInstance, LPCTSTR lpTableName)
  2. CreateAcceleratorTable(LPACCEL lpaccl, int cEntries)

の2つがあります。

どちらもTranslateAcceleratorの第二引数で使用する アクセラレータテーブルのハンドルを返しますが、

  1. はIDEのリソースエディタで作成されたアクセラレータテーブルを使うときに用いられ、
  2. はACCEL構造体(MSDNを参照)を自分で作成してアクセラレータテーブルのハンドルを取得します。

今回は2.の方法で実装してみますが、より簡単でより多く使われている?と思う 1.の概略だけは書いておきます。

(LoadAcceleratorsの場合)

まずは、アクセラレータテーブルの作成から。
プロジェクトを開いてメニューの表示(V)からリソースビューをアクティブにします。 編集(E)からリソースの追加(A)を選択してリソースの種類でAcceleratorを新規作成します。 Accelerator用のリソースエディタがオープンするので、ショートカットキーが押されたとき実行する コマンドID(ID)、ショートカットキー(修飾子)、キー、キータイプを入力して保存します。

アクセラレータテーブルのリソースエディタ例 (Ctrl + AでIDC_BUTTON1コマンドを実行)

リソースビューで見るとこのアクセラレータテーブルのリソースIDが「IDR_ACCELERATOR1」 などとなっていると思うので確認しておいてください。 (このリソースIDをLoadAcceleratorsd関数で使用します)

あとは適当なタイミング(CWnd::OnCreateとかダイアログの時は CDialog::OnInitDialogなど)で
LoadAccelerators(AfxGetInstanceHandle(), MAKEINTRESOURCE(IDR_ACCELELATOR1));
とすればアクセラレータテーブルのハンドルが取得できます。

(CreateAcceleratorTableの場合)

LoadAcceleratorsを使ってアクセラレータテーブルのハンドルを取得する場合、 GUIで簡単に設定できますが、動的にアクセラレータキーを変更したい場合など複雑なことをしたい時は 2.のCreateAcceleratorTableを使用します。

このAPIを使用するときはアクセラレータの情報 (どのショートカットキーの時どのコマンドを実行するか)をACCEL構造体に設定して CreateAcceleratorTableを実行します。1つのACCEL構造体の情報が上記リソースエディタの アクセラレータテーブルの1行に相当します。ですから、複数のショートカットキーを設定する場合は ACCEL構造体の配列を作成してそのポインタをCreateAcceleratorTableの第一引数に 指定します。第二引数にはACCEL構造体のインスタンスの数(配列の要素数)を指定します。
ACCEL構造体はwinuser.hに次のように定義されています。

typedef struct tagACCEL {
    BYTE fVirt;
    WORD key;
    WORD cmd;
} ACCEL, *LPACCEL;

詳しくはMSDNで確認してください。

もうひとつCreateAcceleratorTableを使う場合に気をつけなければならないことは、 LoadAcceleratorsとことなり、アプリケーション終了前に、DestroyAcceleratorTable で明示的にアクセラレータテーブルを破棄しないといけない点です。 (LoadAcceleratorsは自動的に破棄してくれます。)

これでアクセラレータテーブルへのハンドルが取得できたので TranslateAcceleratorでコマンドメッセージに変換してウィンドウへ送ることができますが、 どこでこのAPIを使うかという問題があります。

ところで、メッセージの処理がどうなっているかということですが、 例えばコマンドボタンが押されるとWM_COMMANDがスレッドのメッセージキューに置かれます。 すると、スレッドの無限ループ(メッセージループと呼ばれMFCではCWinThread::Run() で実装されています) 内でまず、

  1. メッセージキューに置かれているメッセージをPeekMessageGetMessage でMSG構造体にメッセージを取得します。
  2. 取得したメッセージをTranslateMessageで仮想キーメッセージを文字メッセージに変換した後、
  3. DispatchMessageで目的の(この場合はボタンがあるウィンドウプロシージャへ送られます。
  4. 最後にウィンドウプロシージャでボタンのクリックに対する処理がされます。 (MFCではメッセージマップ機構で処理します。)

実際にはアイドル処理など複雑になっていますが、たいへん荒っぽく書くとこんな感じになります。

そこで本題に戻り、どこでTranslateAcceleratorを実行するかということですが、 上記のメッセージ処理の流れのなかで、1.と2.の間だろうと思われる方も多いかと思います。

MFCのソース(thrccore.cpp)をご覧になると分かりますが、メッセージループ CWinThread::Run内で CWinThread::PumpMessageが呼ばれています。 この関数がAfxInternalPumpMessageを呼び、この中で上記2.、3.を呼び出していますが、 2.の前にAfxPreTranslateMessage関数が呼ばれています。この後 CWinThread::PreTranslateMessageCWnd::PreTranslateMessageなど が呼ばれるのですがこれらは仮想関数として定義されているのでアプリケーションでこの関数を オーバーライドしてそこでTranslateAcceleratorを実行することになります。
では実際にやってみます。

・準備

Visual Studioからファイル(F)→新規作成(N)→ プロジェクト(P)から「MFCアプリケーション」を選択してください。アプリケーションウィザードが 起動するので次のようにしてください。

(プロジェクト名)            ・・・testAccelerator
(アプリケーションの種類)    ・・・ダイアログベース(他はデフォルト)
(ユーザインターフェイス機能)・・・すべてのチェックをはずす

あとはデフォルトで最後に完了を押してください。

※ダイアログベースのアプリでは前記の概要とはすこしメッセージの処理の流れが 変わるのですが、理屈としては同様で単純なのでこちらでプロジェクトを作成します。

ツールバーの表示(V)からリソースビューを アクティブにしてメインウィンドウのダイアログをリソースエディタに表示して任意の位置に コマンドボタンを配置してください。配置したコマンドボタンをアクティブにして右クリックから プロパティシートを表示して以下を確認してください。

(プロパティ)
ID      ・・・IDC_BUTTON1  (デフォルト)
Caption ・・・空白にする

(管理イベント)
BN_CLICKED ・・・ メッセージハンドラを作成(OnBnClickButton1)

最後にツールバーの表示(V)からクラスビューをアクティブにしてCtestAcceleratorDlgを クリックしてプロパティを表示後、PreTranslateMessageをオーバーライドしてください。

・実装

クラスビューをみるとCWinAppの派生クラスCtestAcceleratorAppと、 CDialogの派生クラスCtestAcceleratorDlgの2つができているとおもいますが実装は後者のみです。
実装内容は

  1. CtestAcceleratorDlgにアクセラレータテーブルのハンドル保持用HACCEL型変数作成。
  2. CtestAcceleratorDlg::OnInitDialogでアクセラレータテーブルの作成 & ハンドルの取得。
  3. コマンドボタンのクリックハンドラ実装。
  4. CtestAcceleratorDlg::PreTranslateMessageのオーバーライド。
  5. アクセラレータテーブル破棄のためデストラクタの実装。

です。

testAcceleratorDlg.hファイルの内容

class CtestAcceleratorDlg : public CDialog
{
//・
//・
protected:
    HACCEL  m_hAccel;               //アクセラレータテーブルハンドル

public:
    virtual ~CtestAcceleratorDlg(); //デストラクタ
    afx_msg void OnBnClickedButton1();
    virtual BOOL PreTranslateMessage(MSG* pMsg);
};

※OnBnClickedButton1とPreTranslateMessageは既にできてると思います。

//メッセージマップ
BEGIN_MESSAGE_MAP(CtestAcceleratorDlg, CDialog)
// ・
// ・
    ON_BN_CLICKED(IDC_BUTTON1, &CtestAcceleratorDlg::OnBnClickedButton1)
END_MESSAGE_MAP()


//コンストラクタ
CtestAcceleratorDlg::CtestAcceleratorDlg(CWnd* pParent /*=NULL*/)
: CDialog(CtestAcceleratorDlg::IDD, pParent)
, m_hAccel(NULL)
{
//・
//・
}

//ダイアログ初期化
BOOL CtestAcceleratorDlg::OnInitDialog()
{
// ・
// ・
    ACCEL   accel[1];
    accel[0].fVirt = FCONTROL | FVIRTKEY;
    accel[0].key = 'A'; //0x41
    accel[0].cmd = IDC_BUTTON1;

    m_hAccel = ::CreateAcceleratorTable(accel, sizeof accel / sizeof accel[0]);
// ・
// ・
}

//Button1クリック時
void CtestAcceleratorDlg::OnBnClickedButton1()
{
    static int i = 0;
    CString    str;
    str.Format(_T("クリック%d 回"), ++i);
    GetDlgItem(IDC_BUTTON1)->SetWindowText(str);
}

//メッセージ変換用
BOOL CtestAcceleratorDlg::PreTranslateMessage(MSG* pMsg)
{
    if (m_hAccel)
        if ( ::TranslateAccelerator(m_hWnd, m_hAccel, pMsg) )
            return TRUE;

    return CDialog::PreTranslateMessage(pMsg);
}

//デストラクタ
CtestAcceleratorDlg::~CtestAcceleratorDlg()
{
    if (m_hAccel)
        ::DestroyAcceleratorTable(m_hAccel));
}

ビルドして実行すると下記のような画面がでてくるのでボタンをクリックまたはCtrl + Aで 表題のクリック回数表示がカウントアップするのを確認してください。