-
Folder View にコンテキストメニューをつける
たいして難しい内容でもないのに、相変わらず間が空いてしまって申し訳ありません。気を取り直してコンテキストメニューの解説をします。コンテキストメニューは動的に作成することも、既存のメニューリソースを利用することもできますが、今回は簡単にリソースを作ってそれを使う方法を示します。
まず、リソースエディタで、メニューリソースを追加します。

やり方は簡単で IDE
のリソースビューを開き、メニューの挿入を選ぶとメニューリソースが追加されます。今回はメニューのリソースIDとして、 IDR_POPUP
という名前をつけました。
次にこのメニューに項目を追加します。今回は、選択されたファイルを Source View
に表示する「ソースをビューに表示」と、Source View に表示し、かつ変換まで行う「ソース全体を変換」の二つの項目を追加します。
このとき、メニューの親はポップアップを有効にして、Caption を入れません。またメニュー ID
はそれぞれ、
IDM_DISPLAY_SOURCEVIEW(ソースをビューに表示)
IDM_TRANSLATE_ALL(ソース全体を変換)
としました。
以上で、メニューの準備は完成したので、これをツリービュー(CFolderView)から表示するコードを書いていきます。
CFolder View
の任意のノードでユーザーが右クリックしたときにコンテキストメニュー(今作った IDR_POPUP)を表示したいので、CFolder
View で右クリックされたイベントを取得する必要があります。これには、通知メッセージを利用します。
CFolder View のメッセージマップに以下のように追加します。
BEGIN_MSG_MAP(CFolderView)
MESSAGE_HANDLER(WM_CREATE, OnCreate)
MESSAGE_HANDLER(WM_SIZE, OnSize)
NOTIFY_HANDLER(IDC_FOLDER_TREE, TVN_ITEMEXPANDING, OnItemExpanding)
NOTIFY_HANDLER(IDC_FOLDER_TREE, NM_RCLICK, OnRClick)
END_MSG_MAP()
通知メッセージは NOTIFY_HANDLER
で取得することに注意してください。同じく、このイベントを受けるハンドラを記述します。Handler Prototypes
として用意されている Notify Handler の部分をコピーして、関数名を上記 MSG_MAP で指定した OnRClick
に変更します(OnRClick の部分はユーザーが自由に決定できます)
変更したハンドラは以下のようになります。
// Handler prototypes (uncomment arguments if needed):
// LRESULT MessageHandler(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)
// LRESULT CommandHandler(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/)
// LRESULT NotifyHandler(int /*idCtrl*/, LPNMHDR /*pnmh*/, BOOL& /*bHandled*/)
LRESULT OnCreate(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/);
LRESULT OnSize(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/);
LRESULT OnItemExpanding(int /*idCtrl*/, LPNMHDR /*pnmh*/, BOOL& /*bHandled*/);
LRESULT OnRClick(int /*idCtrl*/, LPNMHDR /*pnmh*/, BOOL& /*bHandled*/);
次に、上記 NM_RCLICK
に対応するハンドラを記述する訳ですが、通常通知ハンドラのポインタが示す構造体に、有用な情報が入るわけですが、今回の場合特に有効な情報を取得できないので、この部分は自力で書いていく必要があります(詳しくは、MSDN
の NM_RCLICK の説明を参照してください)
幸いツリービューにはマウスクリックされた座標に該当するノードを返す関数が実装されているので、これを利用することにします。
LRESULT CFolderView::OnRClick(int /*idCtrl*/, LPNMHDR pnmh, BOOL& /*bHandled*/)
{
/// マウスカーソルの位置を取得
CPoint cPoint, cPoint2;
GetCursorPos(&cPoint);
cPoint2 = cPoint;
ScreenToClient(&cPoint2);
/// ツリービューのその位置のアイテムを選択
HTREEITEM hItem = m_pcTreeViewCtrl->HitTest(cPoint2, NULL);
if ( NULL != hItem ) {
m_pcTreeViewCtrl->SelectItem(hItem);
TCHAR tcBuf[1024];
m_pcTreeViewCtrl->GetItemText(hItem, tcBuf, 1024);
/// アイテムの属性を調べる
CIDL cIDL;
SHFILEINFO sfi = { 0 };
if ( TRUE == GetAbsIDLFromTree(hItem, cIDL)) {
SHGetFileInfo(reinterpret_cast<const char*>(cIDL.m_pIDL), 0, &sfi,
sizeof(SHFILEINFO), SHGFI_PIDL | SHGFI_ATTRIBUTES);
if ( sfi.dwAttributes & SFGAO_FOLDER || sfi.dwAttributes & SFGAO_LINK ) {
/// フォルダとショートカットは変換しない
} else {
/// その位置にポップアップメニューを表示
/// ただし選択後のメッセージは親ウィンドウへ通知
CMenu cPopup;
cPopup.LoadMenu(IDR_POPUP);
cPopup.GetSubMenu(0).TrackPopupMenu(TPM_LEFTALIGN | TPM_LEFTBUTTON, cPoint.x, cPoint.y, GetParent());
}
} else {
ATLTRACE(_T("%s の属性の取得に失敗しました。"), tcBuf);
}
}
return 0;
}
最初から解説していくと、まずユーザーが CFolder View 上で右クリックを押します。すると、この OnRClick
に飛んできます。そこで、まずカーソルの座標を取得します。この座標はスクリーン座標系で取得されるので、ScreenToClient
を使ってクライアント座標系に変換します。
次に CTreeView
のメンバ関数 HitTest
を利用して、クリックされた座標の位置にあるアイテムを取得します。取得できなかった場合は特に何もしません(たとえば、空白とか何も表示されていないところをクリックされたとき)
アイテムが取得できたら、ポップアップメニューを表示するわけですが、単純に実装してしまうと、すべての場合にポップアップメニューが表示されてしまいます。フォルダやショートカットの場合に表示されても仕方がないので(この場合)、以前取得した
IDL の情報を使って、それらの場合には何もしないようにします。
このツリービューを構築するときに、それぞれのアイテム(TreeItem)に対応する IDL
をセットしたことを思い出してください。クリックされた座標にあるアイテムを取得したら、以前作成した GetAbsIDLFromTree
関数を利用して、そのアイテムが持つ IDL を取得し、SHGetFileInfo 関数を使ってその属性を調べます。
今回はフォルダかどうかを調べたいので、SHGFI_ATTRIBUTES を指定してこの関数を呼びます。IDL
を渡しているので、SHGFI_PIDL を指定するのを忘れないでください。
フォルダでないことがわかったらポップアップメニューの表示対象になります。ポップアップメニューを表示するのは簡単で、CMenu
クラスのインスタンスを宣言し、LoadMenu でリソースからメニューをロードします。ここでは先ほどリソースエディタで作成した
IDR_POPUP を指定します。
そして、現在の座標(cPoint) に最初(0番目)のメニューを表示します。メニューは複数のサブメニューを持つことができますが、TrackPopupMenu
で表示することができるのは一つだけですので、何番目のメニューを表示するか指定する必要があります。
たとえば、S2H_Conv のメインメニューも File、Edit、View,・・・
という複数のサブメニューを持っています。0番目のサブメニューは File、1番目のサブメニューは Edit ということになります。今回は
IDR_POPUP を表示したいので、サブメニューは一つしかないですが、0番目を指定しています。ちょっと省略して書いているのですが、
CMenu cPopup、cSubMenu;
cPopup.LoadMenu(IDR_POPUP);
cSubMenu = cPopup.GetSubMenu(0);
cSubMenu.TrackPopupMenu(TPM_LEFTALIGN | TPM_LEFTBUTTON, cPoint.x, cPoint.y, GetParent());
と書くのと同じことです。
-
コンテキストメニューからのメッセージを処理する
コンテキストメニューがクリックされると、「ソースをビューに表示」の場合は、IDM_DISPLAY_SOURCEVIEW
が、「ソース全体を変換」の場合は、IDM_TRANSLATE_ALL が通知されます。
このメッセージを処理しましょう。メッセージは親ウィンドウに通知されますので、ここで処理します。せっかくですからメインメニューからもこれらが実行できるようにメニューを追加しておきましょう。
メインメニュー(IDR_MAINFRAME)に新しいサブメニュー「Translate」を追加し、ソース全体を選択「IDM_TRANSLATE_ALL」
とソースの選択部分を変換「IDM_TRANSLATE_SELECTED」を追加します。

追加したメニュー(Translate)
メニューを追加したらいよいよハンドラの部分を書いていきます。
BEGIN_MSG_MAP(CMainFrame)
MESSAGE_HANDLER(WM_CREATE, OnCreate)
COMMAND_ID_HANDLER(ID_APP_EXIT, OnFileExit)
・
・
COMMAND_ID_HANDLER(IDM_DISPLAY_SOURCEVIEW, OnDisplaySourceView)
COMMAND_ID_HANDLER(IDM_TRANSLATE_ALL, OnTranslateAll)
COMMAND_ID_HANDLER(IDM_TRANSLATE_SELECTED, OnTranslateSelected)
・
・
END_MSG_MAP()
IDM_DISPLAY_SOURCEVIEW はコンテキストメニューから、IDM_TRANSLATE_ALL
はコンテキストメニューとポップアップメニューから、IDM_TRANSLATE_SELECTED
はメインメニューから呼ばれます。簡単に共通化できる(IDM_TRANSLATE_ALL の部分)のも便利ですね。
LRESULT OnDisplaySourceView(WORD /*wNotifyCode*/, WORD wID, HWND /*hWndCtl*/, BOOL& /*bHandled*/);
LRESULT OnTranslateAll(WORD /*wNotifyCode*/, WORD wID, HWND /*hWndCtl*/, BOOL& /*bHandled*/);
LRESULT OnTranslateSelected(WORD /*wNotifyCode*/, WORD wID, HWND /*hWndCtl*/, BOOL& /*bHandled*/);
上記がハンドラの宣言になります。
次に各関数の中身を記述していきます。
OnDisplaySourceView
ですが、この関数ではクリックされたアイテムが表すファイルの内容を取得してビューに表示する必要があります。View
はまだ作っていないので、とりあえず現在のパスを取得する関数を CFolderView に追加します。
CString CFolderView::GetCurrentPath()
{
HTREEITEM hItem = m_pcTreeViewCtrl->GetSelectedItem();
return GetFullPath(hItem);
}
CString CFolderView::GetFullPath(HTREEITEM hItem)
{
CString sPath;
CIDL cIDL;
SHFILEINFO sfi = { 0 };
GetAbsIDLFromTree(hItem, cIDL);
TCHAR tcBuf[1024];
if ( FALSE == SHGetPathFromIDList(cIDL.m_pIDL, tcBuf)) {
/// IDL の示すものがファイルオブジェクトではないときに失敗
/// たとえば、マイネットワーク>ネットワークプレイスの追加 とか
/// もしここでなにか別のアクションをしてもいい
}
sPath = tcBuf;
return sPath;
}
やっていることは簡単で、現在選択されているアイテムが示す IDL を取得して、その IDL
から現在のパスを取得しているだけです。この処理を使って後で作る View
にロードすべきファイルのパスを渡します。実装は以下のようになります。
LRESULT CMainFrame::OnDisplaySourceView
(WORD /*wNotifyCode*/, WORD wID, HWND /*hWndCtl*/, BOOL& /*bHandled*/)
{
CString sPath = m_cFolderView.GetCurrentPath();
if ( FALSE == sPath.IsEmpty()) {
m_cSourceView.LoadFile(sPath); <- ここはまだ実装してないけど、こんな風になるということで。
}
return 0;
}
また、残り二つのハンドラですが、実際の変換処理はまだ作っていないので、以下のような関数を使うことにしておきます。
private:
void Translate(BOOL bAll = TRUE);
Translate
関数は BOOL 型の引数をとり、TRUE のときは全範囲を変換、FALSE のときは選択範囲を変換する関数ということにしておきます。
LRESULT CMainFrame::OnTranslateAll(WORD /*wNotifyCode*/, WORD wID, HWND /*hWndCtl*/, BOOL& /*bHandled*/)
{
Translate(TRUE);
return 0;
}
LRESULT CMainFrame::OnTranslateSelected
(WORD /*wNotifyCode*/, WORD wID, HWND /*hWndCtl*/, BOOL& /*bHandled*/)
{
Translate(FALSE);
return 0;
}
-
課題
以上で、コンテキストメニューから実際の処理を呼び出す段取りができました。実際にはファイルすべてが有効なソースを持っているわけではないので、拡張子で判断して、.cpp
とか .java とかのみを対象としたりすると便利だと思います(余力があったらやります)
次回はファイルの内容を表示する SourceView と HTML の内容を表示する HtmlSourceView
の実装と、大まかな変換エンジンの実装部分をやります。
ソースは次回分まで一緒になってるので、次回を待ってください。
第8回へ