VC++ 開発メモ mfc


ぽつぽつと作っているアプリのコーディングで気付いたことをこのページに随時追加していこうと思う
時代の流れに逆らってVCネイティブのウィンドウアプリケーション、しかもmfc雛形・・・理由はいろいろ
その成果:アプリ1



01.MDIタブを作成順に表示
02.CWinAppExで強制表示?
03.ツリービューのチェックボックスをMiddle(ThreeState, 3-state)状態にしたい怪
04.MDIタブの通知罠(わな)
05.縦型レバーコントロール(ReBarCtrl)めも
06.GetBinary、GetSectionBinaryで、メモリ開放
07.CMFCPopupMenuの開閉
08.MDI特定のドキュメント・ビューアクセス
09.起動用タスクバー登録アイコンの怪
10.ツリービューのチェックで全配下のチェック状態を変更
11.プロパティーシートのボタン処理
12.CFileDialogで32bitと64bitで見えるファイルが違う
13.ドッキングウィンドウのゴースト現象
14.ツリービューのチェックボックスを無効化(disable)
15.ツリービューの全件読み取り


2012年5月25日 01.MDIタブを作成順に表示

MFCのマルチドキュメントインターフェイスのタブ付きフレームをいじって見た。
使われているクラスは主に3つですね、
CMDIFrameWndEx (CMDIFrameWnd)、CMFCTabCtrl (CMFCBaseTabCtrl)
CMDIChildWndEx (CMDIChildWnd)

相変わらずドキュメントビューは使い難い。 愚痴はおいといて、ウィザードで雛形が全部できるので即実行してみると色々問題ありすぎな印象。 あくまで個人的印象なので世の中的には普通なのか、まったく問題にならないのかわかりませんが。

初期表示でウィンドウに収まり切らないタブがあるとタブが開いた順に並んでいない。
新規ドキュメントの作成で位置指定が無いので全部ADDで後ろに追加されるだけ+タブコントロールのアクティブタブの可視領域への自動スワップ機能でこんな事になるのでしょうか。
で、並べ替えてみました。 コード全部は無理ですが並べ替えている部分だけ。

その前に使用前、使用後を見てみましょう。
並べ替え前の起動直後、タブ10番が先頭で&アクティブ選択されています。
操作後ならともかく初期表示で7番の次が1、2です、後から開いたの方がいろんな意味で新しいのだから6,5では?と思いましたが、隠れタブの作成・アクティブ化は先頭にINSERTするだけなのでこうなってしまう。

並べ替えした後の起動直後

むりやりな感はあるかもですが(そうでもないか)、コード。


BOOL CTestApp::InitInstance()
{
...省略
	// テストにタブを10個作成
	pDocTemplate->OpenDocumentFile(NULL);
...省略×10個
	// m_bEnableTabSwap = FALSEでも全タブが初期表示領域内に
	// 収まっていないと勝手にスワップして順番が狂う
	((CMainFrame*)m_pMainWnd)->InitOrderByTabId();
	// メイン ウィンドウが初期化されたので、表示と更新を行います。
	pMainFrame->ShowWindow(m_nCmdShow);
	// 自動配色の再設定(ShowWindowの後でないと効果なし)
	pMainFrame->SetTabStyle();  // コードは省略
	pMainFrame->UpdateWindow();
	return TRUE;
}

///////////////////////////////////////////////////////////////////////
// CMainFrame の記述
///////////////////////////////////////////////////////////////////////
struct SortTabsId{
	int nTabId;
	int	nTabNo;
};
bool TabsIdComp(const SortTabsId& left, const SortTabsId& right)
{
	if (left.nTabId < right.nTabId) return 1;
	return 0;
}
// 初期表示用の整列、ドキュメント1番を先頭に順に並べる
// 整列後、隠れているタブをアクティブにすると再度順番は変わる。操作上それは仕方がなし。
// 呼べばいつでも戻る。アクティブ化するタブ番号を渡す引数を付けてもいい。
void CMainFrame::InitOrderByTabId()
{
	// MDIClient・TabCtrl取得は違う書き方もあります。
	// こちら「08.MDI特定のドキュメント・ビューアクセス」

	CWnd* pMdiCaWnd = FindWindowEx(m_hWnd, NULL, _T("MDIClient"), NULL);
	if (!pMdiCaWnd)
		return;

	CMFCTabCtrl* pTabCtrl = ((CMDIClientAreaWnd*)pMdiCaWnd)->GetFirstTabWnd();
	if (!pTabCtrl)
		return;
	
	int nTabsNum = pTabCtrl->GetTabsNum();
	if (1 >= nTabsNum)
		return;

	int i;
	std::vector<SortTabsId> vec1;
	vec1.resize(nTabsNum);

	for (i = 0; i < nTabsNum; i++)
	{
		AfxTrace(_T("Tabs ID: 0x%08X %d (%d)\n"),
			pTabCtrl->GetTabWnd(i)->GetSafeHwnd(), i, pTabCtrl->GetTabID(i));

		vec1.at(i).nTabId = pTabCtrl->GetTabID(i);
		vec1.at(i).nTabNo = i;
	}

	std::sort(vec1.begin(), vec1.end(), TabsIdComp);

	CArray<int,int> arOrder;
	for (i = 0; i < nTabsNum; i++){
		AfxTrace(_T("Order Tabs: %d (%d)\n"), vec1.at(i).nTabNo, vec1.at(i).nTabId);
		arOrder.Add( vec1.at(i).nTabNo );
	}
	pTabCtrl->SetTabsOrder(arOrder);

	// 並べ替え後のインデックスは先頭ゼロから再採番されてる。
	// 「ウィンドウ外の隠れタブがあれば」、指定番号が先頭に移動。
	// 8を指定すればタブは(1大きい)「9,1,2,3・・・,8,10」の順。
	// ウィンドウ内に全部あれば入れ換えは発生せずアクティブ化。
	// 今はソート直後の順なので先頭をアクティブ化してるのみ。
	pTabCtrl->ActivateMDITab( 0 ); 
	// 最終的にどこかでUpdateWindowは必要
}

MDIタブを作成すると自動でタスクバーにタブの数だけアイコンが登録されてしまい
 (アプリとしては1個、重なって表示)
マウスが通過するとすごい数のプレビューが出てしまうので
CMDIChildWndの方で以下の一文をオーバーライドした方がよいかと思います。
virtual BOOL CanShowOnTaskBarTabs();
BOOL CChildFrame::CanShowOnTaskBarTabs()
{
return FALSE;
}

ページ先頭に戻る


2012年5月26日 02.CWinAppExで強制表示?

CWinAppExの方だけなのかな?、CREATESTRUCTのstyleをいじっても非表示ならないのは
CREATEしている2箇所でいくら非表示にしても表示されてしまいます。
    BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
    int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)

なんだろうとソースをさかのぼって行くと・・・LoadFrameの中で強制表示されています。
以下のコメント記述があるので、それ以前に表示される場面があるとは思っていなかったのだ。
    // メイン ウィンドウが初期化されたので、表示と更新を行います。
    pMainFrame->ShowWindow(m_nCmdShow);
やられた、改めてCWinAppExのドキュメントを見ると、LoadFrameの前に1行入れるとよさそうだ。

// LoadFrameの中で前回終了時の状態に戻すか戻さないか選択できるらしい。
// 以下の1行をいれればOK、まあ便利といえば便利機能かな
EnableLoadWindowPlacement(FALSE);

// メイン MDI フレーム ウィンドウを作成します。
CMainFrame* pMainFrame = new CMainFrame;
if (!pMainFrame || !pMainFrame->LoadFrame(IDR_MAINFRAME))

ページ先頭に戻る


2012年6月6日 03.ツリービューのチェックボックスをMiddle(ThreeState, 3-state)状態にしたい

まずTreeCtrlのSetCheck()を使っている限りチェック有り・無しの2つの状態にしかならない、半選択状態のMiddleにするには、SetItemState()関数を使わないといけない。 間違っているかもしれないが

SetItemState(hItem, INDEXTOSTATEIMAGEMASK(3), TVIS_USERMASK);

と記述してみる。

もう気付いたでしょうが、INDEXTOSTATEIMAGEMASK(1)と(2)がチェック無しと有りの時ですね。数字はイメージリストの1からの連番なのでゼロは使いません。中身は数字を左に12シフトさせているだけなので 1=0x1000, 2=0x2000, 3=0x3000 ~ 15=0xF000まで15個指定できます。3-stateどころか15-stateボタンまで可能です。

しかーしここで重大な問題がります。標準ではチェック有り・無しの2個しかイメージを持っていないので(3)以上を指定させても何も表示されません、Checkbox部分の幅もゼロで表示されます。 .netなら初めからイメージを持っているのかもしれませんが真偽はしりません。Web検索ではプロパティ指定だけで出来るような感じで書いてある?ので自力で確かめてください。

無い物は仕方が無いので自分でイメージを作ります。 画像 こんな感じで1枡16×16(今だと24bit/32bitかな)でBMP内に4個のボタンイメージを作成します。4個なのでインデックスは0、1、2、3です、要するに先頭の1個はダミーで使われません。 気を付ける事はCheckboxをMiddl状態までにしたい場合は作るイメージは4個です。それ以上作るとCheckboxをクリックする度にItemState値がボタン数分繰り上がっていきます。CImageListで調整できそうだけどとりあえず。 で、作ったBMPイメージをCTreeCtrlに登録します。
:ダウンロード用BMPイメージファイル
幅はリソースエディタープのロパティで調整、ペイントを2個開いて新規へ切り貼りでもいいです
※ゼロ番は表示されませんが、あえて何も無い状態(Nothing)としてなら使えるかも。
※利用可能イメージのZIPファイルが\Microsoft Visual Studio 10.0\Common7\2010ImageLibrary\の下にあります、ほかのバージョンでもCommonの下かDVDに付属しています(Express版にはない)。

CTreeCtrl   TVCtrl;
TVCtrl.Create(TVS_CHECKBOXES    ・・・雛形通りなので以下色々省略
CBitmap bmp1;    
bmp1.LoadBitmap(IDB_3STATE_CHECKBOX);
BITMAP bmpObj1;    
bmp1.GetBitmap(&bmpObj1);
CImageList  ImageState;    
ImageState.Create(16, bmpObj1.bmHeight, nFlags, 0, 0);
TVCtrl.SetImageList(&ImageState, TVSIL_STATE);  // ステータス用として追加登録
TVCtrl.SetImageList(&m_ImageNormal, TVSIL_NORMAL); // 雛形の通常の奴はそのまま使用

これでチェックボックスをクリックすると自動でイメージの1,2,3戻って1,2,3と順に切り替わります、当然GetItemState(hItem, TVIS_USERMASK);で取れる値も0x1000、0x2000、0x3000と連動して変わります。

 結局3-stateにするのにコードでやった事は準備を除けば次の1コマンドを発行するだけ。
 SetImageList(&ImageState, TVSIL_STATE);

結果は左端がTVSIL_STATEのチェックボックス状態イメージ、
右隣りがTVSIL_NORMALのツリー開閉(選択?)状態イメージ、
で文字列の順に表示されました。

ItemState値にはSELECTEDか何かの値も付いてる事があるので==ではなく&しましょう。
※重要:SetItemState()したら「CTreeCtrlをUpdateWindow()」 で表示更新しましょう。
では次にCheckboxのクリックイベントを検出しましょう。

#define IDTV_UNCHECKED  0x1000
#define IDTV_CHECKED    0x2000
#define IDTV_MIDCHECKED  0x3000
BOOL Class::OnNotify(WPARAM wParam, LPARAM lParam, LRESULT* pResult)
{ 
    LPNMHDR pNMHdr = reinterpret_cast<LPNMHDR>(lParam);
    switch (wParam) {
    case ID_CTRL:
    {
        LPNMTREEVIEW pNMTV = reinterpret_cast<LPNMTREEVIEW>(lParam);
        switch (pNMHdr->code){
        case TVN_ITEMCHANGED: // CheckBox Click, InsertItem, SetItemState
        {
            NMTVITEMCHANGE* pNMTVChg = reinterpret_cast<NMTVITEMCHANGE*>(lParam);
            if (!m_bInsert &&
                (TVIS_USERMASK & pNMTVChg->uStateOld) != 
                (TVIS_USERMASK & pNMTVChg->uStateNew))
            {
                //チェックボックスクリック発生!なにかの処理。
                switch (TVIS_USERMASK & pNMTVChg->uStateNew){
                case IDTV_UNCHECKED: break; 
                case IDTV_CHECKED: break; 
                case IDTV_MIDCHECKED: break; 
                }
            }
        }
        return TRUE;
    }
    break;  //以下略

省略して最低限にしました。注意点は起動時のデータセットでInsertItemを連続発行してSetItemStateで初期値を付けると思いますがその際もイベント発生するのでここではm_bInsertというフラグを1個作って除外しています。
親ノードのチェックオンオフで全子ノードのチェック状態を変更する場合も連続でイベント発生するので同フラグで除外しています。TVN_ITEMCHANGED処理中にSetItemStateで同じTVN_ITEMCHANGEDが発生するので何かで抑止しないと1秒で終わる処理が1分以上かかったりします。

更にマウス1クリックでTVN_ITEMCHANGEDが複数回発生する(Selected状態とかの他ステータスが付加されるとか)場合もあるのでTVIS_USERMASKでマスクしてチェック状態だけを取り出して最終的にNEWとOLDを比べています。NMTVITEMCHANGEの中にフラグがあったかも知れないのでフラグを見てもいいかも。

ちょっとした疑問 CTreeCtrlの拡張スタイルでTVS_EX_PARTIALCHECKBOXESと部分的チェックボックスというのがありSetExtendedStyleで使ってみたのですがSpy++で見ても設定されないのですよね(表には出ない内部的なスタイル値か)。結局なくてもMiddle状態に出来て動いているのでTVS_EX_PARTIALCHECKBOXESは何か他の意味なのかなーSetImageListでボタン数とか自動設定されちゃうし。
それとヘッダー定義で #define TVSIL_NORMAL 0、#define TVSIL_STATE 2となっていて1が抜けているんだけど、どっかにイメージリソースを隠し持っているのかもと疑ってみたりしてます。 今日はこの辺で。

ページ先頭に戻る


2012年6月9日 04.MDIタブの通知罠

MDI内のタブのどれかを選択するとMDIメインフレームに「選択状態が変わった」とメッセージが来ますよね、
正確には選択というよりもアクティブ状態ですが。MFCだと具体的には

ヘッダー定義:afx_msg LRESULT OnChangeActiveTab(WPARAM wp, LPARAM lp);
ソース宣言
BEGIN_MESSAGE_MAP(CMainFrame, CMDIFrameWndEx)
    ON_REGISTERED_MESSAGE(AFX_WM_CHANGE_ACTIVE_TAB, OnChangingActiveTab)
END_MESSAGE_MAP()
LRESULT CMainFrame::OnChangeActiveTab(WPARAM wp, LPARAM /*lp*/){
	AfxTrace(_T("CHANG_ACT_TAB: %d\n"), (int)wp);
	return 0;
}

ここまでならへーそうか。で終わるのですが、タブ変更で何かアクションを起こす場合にMDIフレーム内に他種類のタブコントロールが存在すると何やら困った事になるようです。 何が困るかって言うとですね渡って来る引数を見て下さい、 WPARAMで選択されたタブのインデックスが1個来るだけです。 で、その際に下の図の様な画面構成(雛形)だとMDIクライアント(緑枠)以外にも2箇所にタブコントロールが紫と赤枠に存在します。この2箇所のタブ変更もMDIクライアント領域と同じようにメインフレームに通知されます。 要するに来る引数がINT型のインデックスが1個なので3箇所のタブコントロールのうちのどれの変更なのかまったく不明だと言う事です。 緑枠のタブの選択によって例えばプロパティグリッドの項目を変えるよな記述をすると赤枠のタブの変更でも同じタブインデックス値が渡って来るので反応してしまいます。 タブインデックスはタブ自身が管理しているゼロからの連番なのでユーザーで設定するのは不可能です。 とりあえずそういう事象です。

ではどうやって「どこのタブコントロール」の変更か区別したらいいのか?たぶん決まった答えは無いと考えます、他の要素とも絡んでその時々でベストの方法を取るって感じでしょうか。 この雛形は全タブがどこにでもドッキング可能な機能付きなので簡単にはウィンドウ構成が変えられないし、MFC以外だとこの場合どうなっているのかは判りません。 私がやった解決方法は少しごちゃごちゃと書いて最終的にウィンドウクラス名が目的のMDIクライアント領域の物(場合によっては親から孫まで複数あるかも)か判定するって感じです。

LRESULT CMainFrame::OnChangeActiveTab(WPARAM wp, LPARAM /*lp*/)
{
    // …省略
	TCHAR szWndClass[MAX_PATH];
	TCHAR szWndTitle[MAX_PATH];
	int nNum1 = 0, nNum2 = 0;
	DWORD nCtrlId = 0;
	CWnd* pFocusWnd = GetFocus();
	if (pFocusWnd)
	{
		nNum1 = ::GetClassName(pFocusWnd->m_hWnd, szWndClass, MAX_PATH);
		nNum2 = ::GetWindowText(pFocusWnd->m_hWnd, szWndTitle, MAX_PATH);
		nCtrlId = ::GetWindowLongPtr(pFocusWnd->m_hWnd, GWL_ID);
		AfxTrace(_T("Focus: %s [%d](%s)\n"), szWndClass, nCtrlId, szWndTitle);
		// AfxTraceではszWndTitleが日本語だと出力できません。
	}
    // あとは一致を調べるだけ。
    // MDI/SDIがアクティブになった時に通常、フォーカスを持つの
    // はビューなのでビューのクラス名と比較すればいいと思う。
    // ※含まれるコントロールにSetFocusとかしてなければ。

ページ先頭に戻る


2012年6月12日 05.縦型レバーコントロール(ReBarCtrl)めも

レバーコントロール(ReBarCtrl)を縦にスライドさせたくて悪戦苦闘した結果。 以前も少しやってあきらめた事が・・・ ちょっとした事なのにこういうのって割と面倒です、どのスタイルの組み合わせでどんな動きになるのかはっきり書いてないから時間が掛かる。

int Class::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
	if (Class::OnCreate(lpCreateStruct) == -1)
		return -1;

	CRect rectDummy;
	rectDummy.SetRectEmpty();

	// ここのスタイル単独ではなく、バンドスタイルと組み合わせで結果的にそうなる物もあるかな
	DWORD dwViewStyle;
	dwViewStyle = WS_CHILD | WS_VISIBLE |  
		RBS_DBLCLKTOGGLE |	// 今回は機能しないようだ(調べてない)
		RBS_BANDBORDERS |	// RBS_FIXEDORDERで先頭バンドの上にもグリップを出す時
		RBS_FIXEDORDER |	// 追加したバンド順を変えない
		CCS_VERT |		// 縦型、CCS_LEFTでもCCS_RIGHTでもいい
		CCS_ADJUSTABLE;	// BREAK禁止(無いとD&DでX軸移動,見えなくなる)

	// CReBarCtrl  m_ReBar;  //ヘッダーの方で宣言済み。

	if (!m_ReBar.Create(dwViewStyle, rectDummy, this, AFX_IDW_REBAR))
		return -1;   // 作成できない場合

	CRect rectClient;
	GetWindowRect(&rectClient);
	ScreenToClient(&rectClient);

	REBARBANDINFO	rbbi;
	ZeroMemory(&rbbi, sizeof(REBARBANDINFO));
	rbbi.cbSize = sizeof(REBARBANDINFO);

	rbbi.fMask	= RBBIM_TEXT | RBBIM_STYLE | RBBIM_ID | RBBIM_SIZE | RBBIM_CHILDSIZE;
	rbbi.fStyle	= RBBS_GRIPPERALWAYS | RBBS_TOPALIGN | RBBS_VARIABLEHEIGHT;
	// 縦でcx, cyが逆でVARIABLEHEIGHTが機能するか不明(調べてない)、まあ要らない

	// サイズ初期値、実際のサイズはWM_SIZE時に計算してSetBandInfo等で設定
	// WM_SIZEルーチンがまだ無いとか、デバッグ用にある程度見えてる方がいい
	rbbi.cx = (rectClient.Height() / 4);	// 高さ
	rbbi.cxMinChild = 9;
	rbbi.cyChild	= rectClient.Width();	// 幅
	rbbi.cyMinChild = rectClient.Width();
	rbbi.cyMaxChild = rectClient.Width();
	rbbi.cyIntegral = rectClient.Width();
		
	TCHAR szBandA[] = _T("バンドA");
	TCHAR szBandB[] = _T("バンドB");
	TCHAR szBandC[] = _T("バンドC");

	rbbi.wID = 1;
	//rbbi.hwndChild = m_Wnd1.m_hWnd;
	rbbi.lpText = szBandA;
	rbbi.cch = sizeof(szBandA);
	if (!m_ReBar.InsertBand(0, &rbbi)) return -1;

	rbbi.wID = 2;
	//rbbi.hwndChild = m_Wnd2.m_hWnd;
	rbbi.lpText = szBandB;
	rbbi.cch = sizeof(szBandB);
	if (!m_ReBar.InsertBand(1, &rbbi)) return -1;

	rbbi.wID = 3;
	//rbbi.hwndChild = m_Wnd3.m_hWnd;
	rbbi.lpText = szBandC;
	rbbi.cch = sizeof(szBandC);
	if (!m_ReBar.InsertBand(2, &rbbi)) return -1;

	// オーナー変更 (RBBIM_CHILD ・ rbbi.hwndChild = で設定した場合)
	    // 省略、メッセージ送信先を明示的に再設定にしないと受け取れない場合あり。
	    // CWndObjectXXX.SetOwner(this);
	// ReBarCtrlを親にする物を作成
	    // 省略、ReBarCtrlを親にしないと表示されない物等作成。単独のCToolTipCtrlとか。
	    // CWndObjectXXX.SetParent(&m_ReBar);では対応出来ない物。
	return 0;
}

WM_SIZEメッセージ時の計算でSetBandInfoする時は、先にReBarCtrl全体の位置・サイズ設定をSetWindowPosでやるのが確実かと思います。次にGetBandCountで回して先頭バンドから順に設定する、SetBandInfoで位置が変動するので後ろや途中のみだとうまく描画計算されないかも?。バンド順が変わっていても先頭からで問題はなし。  おおよそ次のような感じのコードになっている。

void Class::AdjustLayout()
{
	if (!GetSafeHwnd() || !m_ReBar.GetSafeHwnd())
		return;

	// バンドが3つあると仮定して Child1, Child2, Child3 とする
	REBARBANDINFO	put_rbbi, get_rbbi;
	MARGINS			nMargins;
	CRect			rcClient, rcBord;

	GetWindowRect(&rcClient);
	ScreenToClient(&rcClient);

	int cxWnd  = rcClient.Width();
	int cyWnd  = rcClient.Height();
	int cyChild1, cyChild2, cyChild3, cyLast;

	ZeroMemory(&put_rbbi, sizeof(REBARBANDINFO));
	put_rbbi.cbSize		= sizeof(REBARBANDINFO);
	put_rbbi.fMask		= RBBIM_SIZE | RBBIM_CHILDSIZE;
	put_rbbi.cxMinChild = 9;
	put_rbbi.cyChild	= cxWnd;
	put_rbbi.cyMinChild = cxWnd;
	put_rbbi.cyMaxChild = cxWnd;
	put_rbbi.cyIntegral = cxWnd;

	ZeroMemory(&get_rbbi, sizeof(REBARBANDINFO));
	get_rbbi.cbSize = sizeof(REBARBANDINFO);
	get_rbbi.fMask	= RBBIM_HEADERSIZE;

	cyChild1 = cyWnd / 4;  // 何かしら計算で必要な高さを求める
	cyChild2 = cyWnd / 4;  // 何かしら計算で必要な高さを求める
	// ここでは余った部分を全部Child3に割り当てるようにしている

	m_ReBar.SetWindowPos(NULL, 0, 0, cxWnd, cyWnd, SWP_NOACTIVATE | SWP_NOZORDER);
	m_ReBar.GetBandMargins(&nMargins);
	// 各Childの.SetWindowPosは設定しない、勝手に合わさる

	// RBS_FIXEDORDERでない場合IDを使う。IDは1から連番で付けている
	// まあRBS_FIXEDORDERありの時でもこのままでいいけど。
	for (int i, id = 1; id < ((int)m_ReBar.GetBandCount() + 1); id++)
	{
		i = m_ReBar.IDToIndex( id );
		
		rcBord.SetRectEmpty();
		m_ReBar.GetBandBorders(i, &rcBord);  // 

		get_rbbi.cxHeader = 0;
		m_ReBar.GetBandInfo(i, &get_rbbi);

		// cxHeader値を取得する理由はスタイルの設定により最上段(i=0)だけ
		// 「つまみ」が存在しない場合があるためです

		cyLast = nMargins.cxLeftWidth + nMargins.cxRightWidth + get_rbbi.cxHeader;
		// または
		cyLast = rcBord.top; //RBS_BANDBORDERSに関係なく上辺つまみ全高さが来た

		//説明ではRBS_BANDBORDERS スタイルが設定されていない場合は、
		//バンドの左辺に相当する構造体メンバーの情報だけが有効です。 となっているので、
		//縦型はX,Yが逆だから、TOP=左辺で.topメンバーは常に有効な値らしい。
		
		switch ( id ){
		case 1:
				cyChild1 += cyLast;
				put_rbbi.cx = cyChild1;
				break;
		case 2:
				cyChild2 += cyLast;
				put_rbbi.cx = cyChild2;
				break;
		case 3:
				cyChild3 = cyWnd - (cyChild1 + cyChild2 + cyLast);
				put_rbbi.cx = cyChild3;
 				break;
 		}
		m_ReBar.SetBandInfo(i, &put_rbbi);
	}
}

それで、このままでは通常のウィンドウサイズ変更でも各バンドが初期位置・サイズに戻ってしまい使い難いので、取っ手(つまみ)操作でユーザーが変更した幅(高さ)を保存して同じサイズを維持するように変更します。 窓サイズが変わると末端バンドにシワ寄せが行って伸長しますが比率でやっても同じく伸長する事に変わりはないですね、特定バンドか全てを固定幅(高さ)にしたい時は素直に.cxと.cxMinChildに同じ固定値を入れればよいかと思います(予想)。


CRect m_BarSize[6];	// ReBarの幅(高さ)、どっかで宣言・初期化して下さい。

BOOL Class::OnNotify(WPARAM wParam, LPARAM lParam, LRESULT* pResult)
{
	// TODO: ここに特定なコードを追加するか、もしくは基本クラスを呼び出してください。
	LPNMHDR pNMHdr = reinterpret_cast<LPNMHDR>(lParam);

	switch (wParam)
	{
	case AFX_IDW_REBAR:
		{
			//if ((NM_FIRST-24) <= pNMHdr->code && pNMHdr->code <= (NM_FIRST-1))
			//	break;
			//LPNMREBAR pNMRB = reinterpret_cast<LPNMREBAR>(lParam);
			//LPNMREBARCHILDSIZE pNMRBCS = reinterpret_cast<LPNMREBARCHILDSIZE>(lParam);
			/******
			switch (pNMHdr->code){
			case RBN_AUTOBREAK:		break;
			case RBN_BEGINDRAG:		break;
			case RBN_ENDDRAG:		break;
			case RBN_SPLITTERDRAG:	break;
			case RBN_LAYOUTCHANGED:	break;
			case RBN_GETOBJECT:		break;
			case RBN_CHILDSIZE:		break;
			*******/
			// 使えそうなメッセージはRBN_CHILDSIZE, RBN_LAYOUTCHANGED, RBN_ENDDRAGです。
			// RBN_CHILDSIZEはSetBandInfoとかユーザー操作以外でも発生するのでパス
			// 今回は最終的に発生するRBN_ENDDRAGを使います
			// 正式にはLAYOUTCHANGEDの方だと思う、まあセットで発生するので。
			
			if (RBN_ENDDRAG == pNMHdr->code) // ユーザー操作の変更を保存
			{
				REBARBANDINFO get_rbbi;
				ZeroMemory(&get_rbbi, sizeof(REBARBANDINFO));
				get_rbbi.cbSize = sizeof(REBARBANDINFO);
				get_rbbi.fMask	= RBBIM_SIZE | RBBIM_CHILDSIZE;

				for (int i, id = 1; id < ((int)m_ReBar.GetBandCount() + 1); id++)
				{
					i = m_ReBar.IDToIndex( id );
					get_rbbi.cx = 0;
					get_rbbi.cyChild = 0;
					m_ReBar.GetBandInfo(i, &get_rbbi);
					m_BarSize[i].SetRectEmpty();
					m_BarSize[i].right = get_rbbi.cx;
					m_BarSize[i].bottom = get_rbbi.cyChild;
				}
			}
		}
		break;
	// 以下省略

// さっきのサイズ設定関数を変更します
void Class::AdjustLayout()
{
	// 省略・・・
	
	ZeroMemory(&put_rbbi, sizeof(REBARBANDINFO));
	put_rbbi.cbSize		= sizeof(REBARBANDINFO);
	put_rbbi.fMask		= RBBIM_SIZE | RBBIM_CHILDSIZE;
	put_rbbi.cxMinChild = 9;
	put_rbbi.cyChild	= cxWnd;
	put_rbbi.cyMinChild = cxWnd;
	put_rbbi.cyMaxChild = cxWnd;
	put_rbbi.cyIntegral = cxWnd;

	// この辺にSetWindowPosを持ってきて
	m_ReBar.SetWindowPos(NULL, 0, 0, cxWnd, cyWnd, SWP_NOACTIVATE | SWP_NOZORDER);

	// ユーザー操作を保存していればEmptyは有り得ないので
	if (!m_BarSize[0].IsRectEmpty())
	{
		for (int i, cyTotal = 0, id = 1, EndId = (int)m_ReBar.GetBandCount(); id <= EndId; id++)
		{
			i = m_ReBar.IDToIndex( id );
			if (id < EndId)
				put_rbbi.cx = m_BarSize[i].Width();
			else
				put_rbbi.cx = cyWnd - cyTotal;
			// どこか1つを余り(残り領域)に設定しないと全体サイズの変更でずれる

			m_ReBar.SetBandInfo(i, &put_rbbi);
			cyTotal += put_rbbi.cx;
		}
		return;  // 処理完了
	}

	ZeroMemory(&get_rbbi, sizeof(REBARBANDINFO));
	// 以下先ほどと同じ

ページ先頭に戻る


2012年6月15日 06.GetBinary、GetSectionBinaryで、メモリ開放

小さなことですが。CWinAppExのGetBinary、GetSectionBinaryの説明が足りませんね。
BOOL GetBinary( LPCTSTR lpszEntry, LPBYTE* ppData, UINT* pBytes );
[出力] ppData メソッドがバイナリ データを格納するバッファーへのポインター。
第2パラメータがポインター型のアドレスを要求しているので、要は中身で領域確保して
くれるって事ですね。 でも説明には終わったら開放しろとは一切書いてありません。

LPBYTE  pData = NULL;
UINT nBytes = 0;
if (GetBinary(  "lpszEntry",  &pData,  &nBytes  ))
{
	if (sizeof(RECT) == nBytes){
		LPRECT prc = (LPRECT)pData;
		AfxTrace(_T("%d, %d, %d, %d\n"), prc->left, prc->top, prc->right, prc->bottom);
	}
	if (pData) delete [] pData;  	// Or free( pData );
}

ところでバイナリーで書き込む物の宣言は生BYTE配列か構造体か両方をUNIONした物か
単に(LPBYTE)キャストした物かでしょうが、構造体の場合はアライメント境界の問題があるので
WriteBinary、WriteSectionBinaryで書き込む時に配置サイズを確認したりしましょうー。
ビルドオプション見ても「既定」ってなってるのでその既定っていくつだよって言いたくなる。
 AfxTrace(_T(“MyStruct _alignof = %d\n”), _alignof(MyStruct));
みたいな感じかな?

ページ先頭に戻る


2012年6月26日 07.CMFCPopupMenuの開閉

何かの事情で開いているポップアップメニューを強制的に閉じたい時があります。
基本ポップアップメニューはアプリ上で同時に複数は存在していないから(※閉じるまでの時間差で複数存在する場合がある)メインフレームの中で以下の2つをオーバーライドしてOnShowPopupMenuでポインターを保存、OnClosePopupMenuで初期化しておけば今開閉しているかどうか、また保存したポインターを使ってWM_CLOSE等を送って閉じる事が可能です。メニューを作成したウィンドウ(メニューの送り先)はCMFCPopupMenuのGetMessageWnd()->GetSafeHwnd()でたぶん判ります。

class CMainFrame : public CMDIFrameWndEx (or SDI...)
	virtual BOOL OnShowPopupMenu(CMFCPopupMenu* pMenuPopup);
	virtual void OnClosePopupMenu(CMFCPopupMenu* pMenuPopup);

ほぼこれで管理できそうな感じです。まあ通常のアプリの動きではポップアップメニューなんて管理する必要すらないと思いますけど。
では通常ではない場合はどうでしょうか。 メインフレーム配下ではないツールウィンドウ的の物の上でポップアップメニューを開くとメインフレームのOnShowPopupMenuもOnClosePopupMenuも通らない場合があります。表示するときに直接CMainFrame::OnShowPopupMenuを呼べば開いた事はわかります、でもいつ閉じたかまではわからないので管理できません。この場合CMainFrameに頼らずになんとかするしかありません。 ポップアップメニューは他のウィンドウがアクティブになると自動で閉じます、そこでその動作をシュミレートすることで閉じることが可能です。
無関係なウィンドウをアクティブにするのも芸がないのでCMFCPopupMenuをCreateしたときのスクリーン座標を保存しておいてCreateしたウィンドウの親(構成により親でなくても良いかも)にWM_LBUTTONDOWNを送って閉じます。他のメッセージでも可能なのかは試してないのでわかりません。※調べてたらMFCのソースコードの中でも同じような事をしてる箇所がありました、ただしLBUTTONでは無かった(場所は失念)。
通常ではない場合はおそらく親枠が存在していると仮定。CWndのShowOwnedPopups(FALSE)は私の場合効果が無かったです。

ページ先頭に戻る


2012年7月09日 08.MDI特定のドキュメント・ビューアクセス

すこし基本にもどってMDIの操作はどうやったらいいのか考えたいと思います。
MDI・SDIを問わずMFCのドキュメントとビューの関係、ウィンドウ間のやりとり方法がいまいち不明瞭なのでその時点でMFCを投げた人は多いと思います。はっきり言ってシリアライズ機能を使わないならドキュメントは未使用で全機能をビューの中に書いても問題なく動きますけど。 

とりあえず今日はプログラムの任意の箇所から特定のビューへのアクセス方法を考察してみます。 MDIプログラムの場合ウィンドウが複数開いていてユーザー操作によっては既に閉じられていて目的のウィンドウが存在しなくなっているかもしれないのを考慮しないといけません。 その前に前提としてCWinApp以外にビューやドキュメントのヘッダーを#include しては駄目です、なぜ駄目かというと説明は難しいけどオブジェクト指向的・アーキテクチャー的に駄目?な感じでしょうか。もちろん例外はあるでしょうけど。 

さてビューを含むウィンドウにアクセスするにはすべてを包むCMainFrameを利用するのは誰でもわかると思います、その次にどうやるのかが問題です、なぜかマイクロソフトはその辺の説明はしていないので。 普通に考えるとCMainFrameの基本クラスのCMDIFrameWndExや子フレームのCMDIChildWndExにGetCount系やGetAt(First,Next,Prev)系のビューへのアクセス関数があってもよさそうですが一切ありません、唯一アクティブの設定と取得があるだけです。 通常はアクティブタブの内容に対する操作を行うのでCFrameWnd::GetActiveView()、CFrameWnd::GetActiveDocument()だけあれば事たりるのでしょうが、特定のビューとなると特に関数は用意されていません。 そして実際にMDIのタブ機能を実装しているMDIClientウィンドウ(CMDIClientAreaWndクラス)とタブ自身のCMFCTabCtrlはフレームの奥に隠れていて表には出てきていません。

やり方ですが大きく別けて能動的にやるか受動的にやるかの2つの手法があると思います。能動的とはCMainFrameからTabCtrlを使って自力でビューを列挙して目的の物をさがす方法で、受動的とはビュー自身に自分はここに居ますと手を上げて貰う方法です。
書くコードも仕組みも簡単安全確実安心な方がいいに決まっているので、そうなると後者の受動的な手段になります。 やり方はいたって簡単で、CMainFrameからSendMessageToDescendantsを送信するだけです。ただし既に閉じられていて存在しないと何もおきません。それでよしなら簡単ですが、すべてを完全に管理しないと仕様上問題あるなら両方の方法を使う必要があります。

ソースコードを乗せようとしたら全体は長いし(機能実装は各数行程度)、断片的説明ではどうしようも無いので以下からZIPを落として下さい、VC2010のプロジェクトZIPです(SDK7.1)。
https://cid-569556ae05c3462e.skydrive.live.com/self.aspx/.Public/Samples/MDISam.Zip
コードでは能動的・受動的の両方でビューにアクセスしています。 ビルド出来なくても実行EXEが付いているのでコードを見ながら動かせばわかると思います。
おおざっぱに流れだけ説明します。目的のビューを特定する為の値をドキュメントにm_DocNumberという変数で持たせています。その値を持つビューはメッセージを受信すると動き出す、という流れです。

ページ先頭に戻る


2012年7月18日 09.起動用タスクバー登録アイコンの怪

アイコンオーバーレイとは別の事で、アプリ起動用のアイコンを単純にタスクバーに表示させたときの話なのですが
普通タスクバーに登録したアイコンからアプリを起動するとそのアイコンに枠が付きます。でも付かない時があります。

付かないパターンは以下です。 エクスプローラからアプリを選んで右ボタンで登録します。
タスクバー登録アイコンとそこから起動したアプリのアイコンが別々になってしまいます。

アプリ起動後もタスクバーに登録したアイコンを利用するように設定するには、
起動後のアイコンからタスクバーに登録する先程とはちょうど逆のオペレーションが必要です。
先にアプリを起動しておいてからタスクバーのアプリアイコンを右ボタンで選んでピン留め。

以下の登録先フォルダーの中を見てもどこが違うのかよくわかりません。
C:\Users\USR\AppData\Roaming\Microsoft\Internet Explorer\Quick Launch\User Pinned\TaskBar
※登録したアイコンが変・または登録や解除が出来ない件は以前別ページの一番下で少し触れています
オペレーションで解決するので深く調べる気にならないのですが使っているAPIが違うからでしょうか
ITaskBarなんとかかんとか1から4まであるやつでしたっけ、MFCとは関係ないけど気になったので。

ページ先頭に戻る


2012年8月25日 10.ツリービューのチェックで全配下のチェック状態を変更

以前ツリービューに関して書きました、それの続きです。
親ノードのチェックボックスのオンオフに合わせて全子ノードのチェック状態も同じように変更したいと思います
以前のコードにそのまま追加します。

BOOL Class::OnNotify(WPARAM wParam, LPARAM lParam, LRESULT* pResult)
{ 
・・・省略
	case TVN_ITEMCHANGED: // CheckBox Click, InsertItem, SetItemState
	{
		NMTVITEMCHANGE* pNMTVChg = reinterpret_cast<NMTVITEMCHANGE*>(lParam);
		if (!m_bInsert && 
			(TVIS_USERMASK & pNMTVChg->uStateOld) != 
			(TVIS_USERMASK & pNMTVChg->uStateNew))
		{
			// 子ノードの状態を変更する関数をあらたに追加。
			m_bInsert = TRUE;	// ステータスを変更中です(フラグ必須)
			SetChildItemState(pNMTVChg->hItem, (TVIS_USERMASK & pNMTVChg->uStateNew));
			TVCtrl.UpdateWindow();
			m_bInsert = FALSE;
		}
	}
	return TRUE; // 処理完了。
・・・省略

// 開始地点となる親のhItemStart自体はクリック等で既に変更されている。
// 配下のみ変更(再帰呼び出し)
void Class::SetChildItemState(HTREEITEM hItemStart, UINT newState)
{
	HTREEITEM hItem = TVCtrl.GetNextItem(hItemStart, TVGN_CHILD);
	while (NULL != hItem)
	{
		TVCtrl.SetItemState(hItem, newState, TVIS_USERMASK);
		SetChildItemState(hItem, newState);
		hItem = TVCtrl.GetNextItem(hItem, TVGN_NEXT);
	}
}

※SetItemState()の再帰ループを抜けた後「CTreeCtrlをUpdateWindow()」 で表示更新しましょう。

ページ先頭に戻る


2012年9月5日 11.プロパティーシートのボタン処理

ウィザードじゃない場合のサンプルです。
プロパティーシートのOKと適用を押した時の保存処理はなにかと複雑な動きになっていて非常にわかり難いです、普通のダイアログの感覚だとシート全体の土台クラス(CPropertySheet)でボタン処理を一括で行うと考えますがプロパティーシートでは各ページ(CPropertyPage)のOnApply、OnOkが処理を行い、Applyは閉じない、Okは閉じる、しかもページ(タブ)を表示したかしてないかで呼ばれたり呼ばれなかったりでその複雑な動きがわかり難い理由です。そこでシートのボタン動作を見える化しました。ほぼどんな保存動作にも対応出来ると思います。
データはページ単位ではなくシート全体を1つのデータとして保存するようにしています。もちろんページ単位に別けて保存してもかまいません。サンプルなのでデータは簡略化。

プロパティーシートは開いてないタブの画面は作成すらされないので単純にアクセスして何かをしようとするとエラーになります。プロパティーページ用の関数を調べるとページにメッセージを送信するQuerySiblingsというのがありますのでこれで各ページにメッセージを送りページを操作すると綺麗に動かすことができます。 もっと機能を増やす場合はQuerySiblingsで送るメッセージ自体を追加するかLPARAMで追加データを与えます。

見える化で通常のDoModalのダイアログスタイルではなくモードレスでビューの子として動作しています
そのため作成と終了処理だけ通常と違います。MainFrameで管理したいのでNcDestroyとdelete thisはつかっていません、終わらせたらメニューから再作成します。 シート自体のコードはDoModalでもモードレスでも違いはありません。
モードレスの場合はシートのボタンが非表示になっているので自分で表示させます今回は通常のボタンに加えて追加でリセットボタンも付けました。

サンプルコードは複数ファイルなので掲載不能です。VC++2010 Expressプロジェクト丸ごとのZIPを落として下さい。
https://cid-569556ae05c3462e.skydrive.live.com/self.aspx/.Public/Samples/PropSam.Zip

ページ先頭に戻る


2012年12月12日 12.CFileDialogで32bitと64bitで見えるファイルが違う

久しぶりの投稿となります。
今日の今日までまったく気付かなかったのですが、タイトル通りです・・・同じソースコードを32ビットと64ビットでビルドした結果、CFileDialog で覗いたフォルダーのファイル表示で見えるファイルと見えないファイルがあり見える時のサイズと日時も違います。CFileDialogと言うよりはコモンダイアログ(コントロール)のファイルを開くと言うべきでしょうか

ビルド環境はVC2010(sp1)、VC2012(update1)の両方、ビルドはUnicodeです。OSはWin7(sp1)64bit
UACはオフ、エクスプローラーの設定は隠しファイル表示する・システムファイル表示する
※エクスプローラー本体から見た場合はファイルは存在しますし検索でもヒットします
因みにCFileDialog ではなくてGetShellManager()->BrowseForFolder でも同じでした
誤解のないように言い直すとネイティブ32ビットではなくてWoW64環境上の32ビットです。

すこし大きなイメージですが、以下がその結果です。
「defrag.exe」が32ビットでは表示されません、右上の検索ボックスから検索しても見つかりません
見えてる同じファイルのサイズ・更新日時が食い違っています。
CFileDialog1
以下がそのCFileDialogを呼び出しているコードです (良し悪しは横においといて下さい)

	TCHAR	strBuffer[MAX_PATH+1] ;
	TCHAR	strDlgTitle[] = _T("出力DATファイルの場所と名前の入力");
	TCHAR	strExtFilter[] = _T("DATファイル(*.dat)\0*.dat\0すべて(*.*)\0*.*\0\0");
	TCHAR	strDefExt[] = _T("dat");
	_tcscpy_s(strBuffer, MAX_PATH, (LPCTSTR)OrgFileName);
	CFileDialog dlg(0);
	dlg.GetOFN().lpstrTitle		= strDlgTitle;
	dlg.GetOFN().lpstrFile		= strBuffer;
	dlg.GetOFN().nMaxFile		= MAX_PATH;
	dlg.GetOFN().lpstrFilter	= strExtFilter;
	dlg.GetOFN().lpstrDefExt	= strDefExt;
	if (IDOK == dlg.DoModal())

さて、いったいどうすればいいのでしょうか?仕様と言われればそれまでですが、ファイルが見つかるのと見つからないのではかな~り大きな差があると思うのですが・・・ちょっとよくわからない。 何かどっかで勘違いをしているのか、あるいはSystem32という特別扱いのフォルダーだからなのか。
ダイアログを開いてる最中にデバッガーにC:\Windows\SysWOW64\comdlg32.dllのロードと即アンロードが出ますので何か別の物で表示されているのか、DLLが沢山使われているので複雑です。

解決:実際の読み取り先はSysWOW64に変更されている。
%windir%\SysWOW64を読みに行くのはWOW64下で32ビットモジュールを起動した時だけ検索パスが変更されて実行に必要なEXEやDLLを読み取りに行くのだとばかり思っていたのですが、起動後の行為も変更されていたわけです。 てっきりどっかの変換トンネルを通って指定先のファイルシステムをちゃんと取得しているのだと考えていました。 と言うことで今回は完全に馬鹿な投稿をしましたって落ちです。

回避策:WOW64用APIで Wow64DisableWow64FsRedirection、Wow64RevertWow64FsRedirection、Wow64EnableWow64FsRedirection
 というのがあるのでファイル参照・作成時に一時的にリダイレクトされるのを回避できるらしい、システム設定ではなくてAPIを呼んだスレッドのみで有効。
試した限りではコモンダイアログ・ファイル開くのような複雑な物には効果無いようなんですが?、fopenとか低Level API用なんでしょうか。

ページ先頭に戻る


2012年12月25日 13.ドッキングウィンドウのゴースト現象

CDockablePane クラスから派生したウィンドウは任意の場所にドッキングできてウィザードで生成された状態のままでも手を加えずに問題なく動きます。でも1点だけ難を言うと色々操作してあっちこっちと合体やフロートさせていると合体後に自動生成されるCTabbedPane クラスやCMiniFrameWnd クラスの残骸が(生成後に他と合体したので未使用になった)プログラムの終了まで残ってしまいます。「04.MDIタブの通知罠(わな)」で使ったウィザード生成のMDI雛形プログラムで色々合体させてみます。 操作的にはCTabbedPaneのウィンドウを1つ1つ分離させてメインフレームにじか付けしてから新たなタブパネルに戻してやるかんじです。
以下がその状態のスナップショットです。同じ名前のドッキングウィンドウとミニフレームが非表示で残ってしまいます。
Dockzonbi1
Dockzonbi2
残骸のウィンドウテキストは生成された時のパネルのキャプションなので、この状態でウィンドウ一覧とかを取得して何かをしようとすると複数みつかったりします。オリジナルのウィンドウも非表示だった場合は判別に少しあれこれと記述しないといけなくなります。
何にしろウィンドウマネージャが汚い状態になってしまうのはあまり好ましくないのでメインフレーム中のCDockingManager オブジェクトと協力して自分で掃除ロジックを入れるしかないのかと思います。私の実験ではある程度は掃除できたのですが、ドッキングウィンドウの横にあるCSliderCtrl オブジェクト(自動生成)の処理が難儀ですね。
一応MFCクラスにも自動掃除機能はついてるのですが、ユーザーが適当に行う操作の全パターンには対応しきれないのではないでしょうか。次がそれらしい関数です
CPaneContainer::ReleaseEmptyPaneContainer
CPaneContainer::RemoveNonValidPanes
CPaneContainerManager::CheckAndRemoveNonValidPane
CPaneContainerManager::ReleaseEmptyPaneContainers
CPaneContainerManager::RemoveNonValidPanes
CPaneDivider::ReleaseEmptyPaneContainers
CBaseTabbedPane::AllowDestroyEmptyTabbedPane
CDockingManager::ReleaseEmptyPaneContainers
で、DockingManagerからリストを取得して全ウィンドウハンドルをプリントしてみたところ、ゴースト化したものと一致するハンドルはありませんでした。対応できないオペレーションの結果、もう既にマネージャの管理外になってるようです(これでは掃除されないはずだ)。なので普通にウィンドウ一覧を作成して自分で回しながら探せって事になるようです。

	CObList Dlist;
	GetDockingManager()->GetPaneList(Dlist, 0, NULL, 0); //パラメータは全パターン試した
	for (POSITION pos = Dlist.GetHeadPosition(); pos != NULL;){
		pBasePane = (CBasePane*)Dlist.GetNext(pos);
		プリント pBasePane->m_hWnd ・・・以下略
ミニフレームの方は動きが単純なせいかGetDockingManager()->GetMiniFrames();でゴースト化したものも一緒に正常取得できます。

ページ先頭に戻る


2012年12月27日 14.ツリービューのチェックボックスを無効化

標準ツリービューのチェックボックスに無効状態はないので専用関数もありません、あるのは状態を表すステータス値だけです、いえミス、値も無いです。 元々チェックボックス自体が実在していなく、絵(bmp)を表示してそこにチェックボックスがあるように見せかける事で機能を実現しているのですから。
なので無効化状態のBMPイメージを追加する(それでステータス値が使えるようになる)のと、ステータス値が無効を表す値の時にクリックを無視するようにコードを修正する作業になります。
気をつける点は通常状態でクリックが発生すると自動でステータス値が巡回するので、勝手に無効状態にしたくない場合は「この場合の次のステータス値はこれ」とOnNotify通知関数の中でSetItemStateを使い明示的に設定してやるひつようがあると思います。
ロジックはそんな感じになるので「03.ツリービューのチェックボックスをMiddle・・・」のコードと大して変わりありませんからそちらを参考にしてください、その際該当イベントはクリック後(TVN_ITEMCHANGED)ではなくてクリック中(TVN_ITEMCHANGING、かNM_CLICKあたり)にかえましょう。

ページ先頭に戻る


2014年12月23日 15.ツリービューの全件読み取り

作成アプリのTreeCtrlの全件読み取りにバグがあったのでその修正内容を投稿する、MFC的にはCTreeViewになるのでしょうか。
以下のようなTree構造をルートから最後まで読み取るコードです
treectrl

// CTreeCtrl clsTree;  // この名前でどこかで宣言されている
CString strWork
HTREEITEM hItem = clsTree.GetRootItem();	// まずルートから始める
HTREEITEM hItemTmp;
int nCount = 1;

while (NULL != hItem)
{
	// 何か処理をするならここでする
	strWork.Format(_T("%03d;%s\n"), nCount, clsTree.GetItemText(hItem));
	OutputDebugString((LPCTSTR)strWork);
	nCount++;

	// 下への読み取り1:必ず子があるか先に調べる(優先)
	// 優先の意味はツリーは見たまんま親子関係を表すので子は親に対して何かしら関連性を持つデータ
	// と考えられるます、先に兄弟を処理すると論理的に破綻すると思います
	hItemTmp = clsTree.GetNextItem(hItem, TVGN_CHILD);

	// 下への読み取り2:子が無い場合のみ兄弟を探す
	if (NULL == hItemTmp)
		hItemTmp = clsTree.GetNextItem(hItem, TVGN_NEXT);

	// それでも無ければこれ以上下にツリーは無い

	// 上への読み取り:何も無い場合のみ親階層の兄弟を探す
	while (NULL == hItemTmp)
	{
		// 初めてこのループ内に来た時は図でいう'01','02','03','04'は処理済みです
		// なので'03'まで戻って同じ階層の次の兄弟、'05'を得なければなりません
		// その後は同じ処理の繰り返し、下への読み取りで'06'まで処理したら
		// このループに入って来て'05'の次の兄弟'07'を得る事になります
		
		if (NULL == (hItemTmp = clsTree.GetParentItem(hItem)))
			break;

		hItem = hItemTmp;	// ここ重要:求めた親の次の兄弟を探す設定

		if (NULL != (hItemTmp = clsTree.GetNextItem(hItem, TVGN_NEXT)))
			break;

		// 兄弟が無ければループ先頭に戻り1階層上の親を得ます
	}
	
	hItem = hItemTmp;
}

以上です。
再帰呼び出しで処理する場合は以下のリンクのSetChildItemState(HTREEITEM hItemStart, UINT newState)関数の呼び出しと同じになります。 初回呼び出しで第一引数にGetRootItem();を渡す、第二引数は未使用。
10.ツリービューのチェックで全配下のチェック状態を変更

ページ先頭に戻る

投稿日: 2012/06/06 | カテゴリー: ソフトウェア、ハードウェア, テスト, 疑問 | パーマリンク コメントする.

コメントを残す