WTLのCListViewCtrlクラスは、右の図のようなリスト表示を行うコントロールを実現します。 コントロールは、EditLabelsプロパティをTrueに設定することで図のように項目を 編集することができるようになります。しかし、編集ができるのは左端の項目のみで、 サブアイテムは編集できないことが知られています。 したがって、サブアイテムを編集できるようにするためには、CListViewCtrlクラスを 自前でカスタマイズする必要があります。サブアイテムが編集できるCListViewCtrlクラスは いろいろなページで公開されていますが、今回はWTLの勉強もかねて自作してみました。
リソースエディタでダイアログを作成し、そこにList Controlを配置します。 ダイアログおよびリストコントロールについて以下のように設定を変更しました。
コントロール | Caption | ID |
---|---|---|
ダイアログ | ID | IDD_MAINDIALOG |
リストコントロール | ID | IDC_LISTVIEW |
リストコントロール | Edit Labels | True |
リストコントロール | View | レポート |
実際の項目編集機能は、 リストコントロールの上にエディットコントロールを表示することで実現されています。 エディットの生成と表示はWindowsが行っていますが、 CListViewCtrlクラスのGetEditControl()関数で生成されたエディットを取得することができます。 サブアイテムの編集は、GetEditControl()で取得したエディットをサブアイテム上に移動させることで、 編集しているように見せかけることで実現します。
GetEditControl()は、エディットコントロールをカプセル化したCEditクラスを返します。 しかしながら、取得したエディットは少々特殊らしく、MoveWindow() を実行しても元の位置に戻ってしまいます。そこで、取得したエディットをサブクラス化して、 WM_WINDOWPOSCHANGINGメッセージで強制的に位置を戻すようにします。 強制移動エディットクラスを以下に示します。 クラスはCEditの拡張クラスになります。
//--------------------------------------------------------------------------- // 強制移動エディット //--------------------------------------------------------------------------- class CSubEdit : public CWindowImpl<CSubEdit, CEdit> { private: CPoint mPos; public: //メッセージマップ BEGIN_MSG_MAP(CSubEdit) MSG_WM_WINDOWPOSCHANGING(OnWindowPosChanging) END_MSG_MAP() //アクセスメソッド void SetPos(const CPoint& pos) { mPos = pos; } //移動したら強制的に戻します void OnWindowPosChanging(LPWINDOWPOS lpWndPos) { lpWndPos->x = mPos.x; lpWndPos->y = mPos.y; SetMsgHandled(FALSE); } };
次にCListViewCtrlをサブクラス化したCSubEditListクラスを作成します。 リストコントロールのEditLabelsプロパティをTrueに設定すると、 編集開始時にLVN_BEGINLABELEDIT通知メッセージが、 編集終了時にLVN_ENDLABELEDIT通知メッセージが、発行されるようになります。 そこで、これらのメッセージを捕捉して編集開始時にエディットを移動、 編集終了時にエディットの内容をサブアイテムに設定するようにします。
以下にサブアイテムが編集可能なリストビュークラスのコードを示します。 通知メッセージをコントロール側で処理するために、メッセージリフレクションを実装します。
//--------------------------------------------------------------------------- // サブアイテム編集リストビュー //--------------------------------------------------------------------------- class CSubEditList : public CWindowImpl<CSubEditList, CListViewCtrl> { private: int mItem; int mSubItem; //編集用エディット CSubEdit mLabelEdit; public: //メッセージマップ BEGIN_MSG_MAP(CSubEditList) REFLECTED_NOTIFY_CODE_HANDLER_EX(LVN_BEGINLABELEDIT, OnBeginLabelEdit) REFLECTED_NOTIFY_CODE_HANDLER_EX(LVN_ENDLABELEDIT, OnEndLabelEdit) DEFAULT_REFLECTION_HANDLER() END_MSG_MAP() // 各種イベントハンドラ LRESULT OnBeginLabelEdit(LPNMHDR pnmh); LRESULT OnEndLabelEdit(LPNMHDR pnmh); };
CSubEditListクラスは、LVN_BEGINLABELEDITメッセージでエディットの表示位置とテキストを変更します。 以下にコードを示します。OnBeginLabelEdit関数でTRUEを返せば編集をキャンセルできます。
//リストアイテム編集開始イベント LRESULT CSubEditList::OnBeginLabelEdit(LPNMHDR pnmh) { WTL::CString str; LVHITTESTINFO lvhit; CRect rect; //カーソル位置からアイテム位置を取得します ::GetCursorPos(&lvhit.pt); ScreenToClient(&lvhit.pt); mItem = SubItemHitTest(&lvhit); mSubItem = lvhit.iSubItem; if ( mItem<0 || mSubItem<0 ) return TRUE; //エディットの表示位置を取得します if ( mSubItem==0 ) GetItemRect(mItem, &rect, LVIR_LABEL); else GetSubItemRect(mItem, mSubItem, LVIR_LABEL, &rect); //アイテムのテキストを取得します GetItemText(mItem, mSubItem, str); //位置を微調整して移動、表示します rect.top -= 2; rect.left += (mSubItem==0) ? 0 : 4; mLabelEdit.SetPos( rect.TopLeft() ); mLabelEdit.SubclassWindow( GetEditControl().m_hWnd ); mLabelEdit.SetWindowText(str); return FALSE; } //---------------------------------------------------------------------------
編集終了時にサブアイテムのテキストを更新するために、 LVN_ENDLABELEDITメッセージでエディットの内容をサブアイテムに適用します。 OSによるテキストの更新を防ぐためにOnEndLabelEdit関数ではFALSEを返すようにします。
//リストアイテム編集終了イベント LRESULT CSubEditList::OnEndLabelEdit(LPNMHDR pnmh) { CAtlString str; //テキストを取得して、サブアイテムに設定します mLabelEdit.GetWindowText(str); SetItemText(mItem, mSubItem, str); mLabelEdit.UnsubclassWindow(); return FALSE; } //---------------------------------------------------------------------------
以下にダイアログのクラスを示します。メッセージリフレクションを使用しているので、 メッセージマップにREFLECTION_NOTIFICATION_EX()を追加します。 またエディット編集の際、キーボード入力によってダイアログが終了するのを防ぐために、 IOOKとIOCANCELメッセージは通知コードをBN_CLICKEDに限定しました。
//--------------------------------------------------------------------------- // メインダイアログクラス //--------------------------------------------------------------------------- class CMainForm : public CDialogImpl<CMainForm>, public CMessageFilter { public: enum { IDD = IDD_MAINDIALOG }; // リストコントロール CSubEditList mListView; // WTLのメッセージマップ BEGIN_MSG_MAP(CMainWindow) MSG_WM_INITDIALOG(OnInitDialog) MSG_WM_DESTROY(OnDestroy) COMMAND_HANDLER_EX(IDOK, BN_CLICKED, OnOk) COMMAND_HANDLER_EX(IDCANCEL, BN_CLICKED, OnCancel) REFLECT_NOTIFICATIONS_EX() END_MSG_MAP() // 各種イベントハンドラ void OnOk(UINT uNotifyCode, int nID, CWindow wndCtl); void OnCancel(UINT uNotifyCode, int nID, CWindow wndCtl); BOOL PreTranslateMessage(MSG *pMsg) { return CWindow::IsDialogMessage(pMsg); } BOOL OnInitDialog(CWindow wndFocus, LPARAM lInitParam); void OnDestroy(); };
作成したCSubEditListクラスを使って、 ダイアログの初期化時に配置したリストコントロールをサブクラス化します。 また、リストの内容も適当に初期化しています。
//ウインドウ開始イベント BOOL CMainForm::OnInitDialog(CWindow wndFocus, LPARAM lInitParam) { CRect rect; CMessageLoop* loop = _Module.GetMessageLoop(); loop->AddMessageFilter(this); // リストビューを初期化します mListView.SubclassWindow( GetDlgItem(IDC_LISTVIEW) ); mListView.GetClientRect(&rect); mListView.SetExtendedListViewStyle(LVS_EX_FULLROWSELECT); mListView.InsertColumn(0, _T("項目"), LVCFMT_LEFT, rect.Width()/3); mListView.InsertColumn(1, _T("値1"), LVCFMT_LEFT, rect.Width()/3); mListView.InsertColumn(2, _T("値2"), LVCFMT_LEFT, rect.Width()/3); mListView.AddItem(0, 0, _T("てすと1")); mListView.AddItem(0, 1, _T("てすと2")); mListView.AddItem(0, 2, _T("てすと3")); mListView.AddItem(1, 0, _T("てすと4")); mListView.AddItem(1, 1, _T("てすと5")); mListView.AddItem(1, 2, _T("てすと6")); return 0; } //---------------------------------------------------------------------------
以上のプログラムを実行すると、右のようになります。 一応、サブアイテムが編集できているように見えますが、1列目の項目が消えてしまっています。 これは、エディットを移動した後、隠れていた部分が再描画されずに残ってしまっているためです。 隠れていた部分を再描画するためには、リストコントロールをオーナードローすればいいのですが...。 今回使用したプロジェクトをおいておきます。