最終更新:2008/05/24

〜 プログラミング Q & A 〜

ネタはあれども準備できず...


 

ツリーコントロール(CTreeCtrl)で高速な DeleteAllItems を実現する

Q 既存のツリーアイテムがある状態で、MFCのCTreeCtrl::DeleteAllItems(TVI_ROOT を指定したTVM_DELETEITEM
メッセージ)を実行すると、TVN_SELCHANGEDメッセージが大量に送信されてしまう。そして非常に遅い。
独自のフラグを立てるなどしてTVN_SELCHANGEDメッセージですぐreturnするようにしても、アイテムが減り行く
際にスクロールバーが短くなっていく様子が見える。
   
A ポイントは2つある。

1つ目はアイテム削除時の選択変更自体を起こさせない事である。
ツリーアイテムのうち、ルートとなるアイテム群の一番最後を選択した状態でDeleteAllItemsを実行させると
削除にかかる時間が明らかに短くなるはずだ。
これはおそらく全削除時の処理で先頭から順次削除をしているせいだと思われるが、この削除の際に選択
位置が共に移動いていくのが原因なのだろう。
そこで、ダミーのアイテムをルートの最後に追加&選択した状態にすることで、選択位置の移動、つまりは
TVN_SELCHANGEDメッセージの発生を抑止する。

2つ目は全削除中に内部で行われる順次削除による表示の更新を抑止する事だ。
アイテムが大量にあると分かりやすいが、削除と共にスクロールバーが順次長く描画されていっているのが
見てとれる。
1つ消える毎に表示を更新しようとして、ツリー表示自体は白いまま(更新されたツリーは表示されない)描画
作業が繰り返し実行されているのである。
この手の処理中状態での描画による重さの軽減はコントロールの非表示で対応できる。
リストコントロール(CListCtrl)への大量の行追加(例えばCSVを読み込みながら順次追加)にも応用できるので
覚えておくと良いだろう。

CTreeCtrl &rTree = GetTreeCtrl();

// ツリーコントロールを非表示にして、アイテムが1つ1つ消える毎の再描画を抑止する
rTree.ShowWindow(SW_HIDE);

// 削除中の選択変更メッセージを抑止するためにダミーを追加する
// ツリールートの最後に追加するところがポイントである(おそらく上から順に削除している)
HTREEITEM hDummy = rTree.InsertItem("");
rTree.SelectItem(hDummy);

// 全て削除する
rTree.DeleteAllItems();

// ダミーを削除する
rTree.DeleteItem(hDummy);

// ツリーを作成する
CreateTree();

// 非表示状態を戻す 
rTree.ShowWindow(SW_SHOW);

記:2008/05/24

 

マウスカーソルの形状をウインドウ外でも変更する

Q SetCaptureでマウスをキャプチャして WM_SETCURSOR メッセージでマウスカーソルを
変更するようにしても、ウインドウの外側にマウスが移動した場合に対応できない。
   
A システムカーソルを変更することでマウスカーソルそのものを変更する。

{
	// マウスキャプチャ開始
	SetCapture();

	// 設定したいカーソルをロードする
	HCURSOR hCursor = AfxGetApp()->LoadCursor(IDC_MY_CURSOR);

	// 形状ごとにシステムカーソルを変更する
	SetSystemCursor(CopyCursor(hCursor), OCR_NORMAL);    // 矢印
	SetSystemCursor(CopyCursor(hCursor), OCR_IBEAM);     // Iビーム
}

{		
	// マウスキャプチャを解放する
	ReleaseCapture();

	// カーソルを元に戻す
	SystemParametersInfo(SPI_SETCURSORS, 0, NULL, 0);
}

記:2003/06/04

 

キャプション無しでシステムメニューを表示する

Q キャプション無しのダイアログ等でアプリケーションを実行すると、タスクバー(スタート
メニューの右側の部分)上では空白のボタンとして表示されてしまう。
SetWindowTextを用いればタイトル文字列は表示できるが、右クリックしても、システム
メニューが表示されない。
   
A システムメニュー表示メッセージ(0x0313:define未定義)を起点に独自にポップアップ
メニューを表示する。

【方法】
1.DefWindowProc にて 0x0313 メッセージを処理できるようにする。
  0x0313 は右クリックでシステムメニューを表示する際に送信されるが、システム
  メニューは無いので何もおきない。

2.このメッセージ処理内で TrackPopupMenu を用いてメニュー表示を行うと、メニュー
  項目選択後になぜか無いはずのシステムメニューも表示されてしまうので、メッセー
  ジを PostMesgage で中継する。

3.中継されたユーザ定義のメッセージ(例えば WM_USER + 100)にてメニューを表示
  する。

4.各メニュー項目の状態などは表示する際に調整しておくこと

5.ユーザ定義のコマンドやメニューIDを追加する場合は 0xF000 以上を
  WM_SYSCOMMAND とし、それ以外を WM_COMMAND として PostMesgage するか
  OnSysCommand で処理するようにする。

  ※ 標準のバージョン情報表示処理は OnSysCommand に実装されているので注意。

LRESULT CMyDlg::DefWindowProc(UINT message, WPARAM wParam, LPARAM lParam) 
{
	if(0x0313 == message)
	{
		// lParam はクリック位置を表すPOINTデータ
		PostMesgage(WM_USER + 100, wParam, lParam);
	}

	if(WM_USER + 100 == message)
	{
		// クリック位置(スクリーン座標なので変換の必要なし)
		CPoint point = lParam;

		// メニューを作成する(項目左横の絵は非対応)
		CMenu menu;
		menu.CreatePopupMenu();

		// この位置でメニューの各項目を調整します
		menu.AppendMenu(MF_STRING, SC_RESTORE, "元のサイズに戻す(&M)");
		menu.AppendMenu(MF_STRING, SC_MOVE, "移動(&M)");
		menu.AppendMenu(MF_STRING, SC_SIZE, "サイズ変更(&S)");
		menu.AppendMenu(MF_STRING, SC_MINIMIZE, "最小化(&N)");
		menu.AppendMenu(MF_STRING, SC_MAXIMIZE, "最大化(&X)");
		menu.AppendMenu(MF_SEPARATOR);
		menu.AppendMenu(MF_STRING, SC_CLOSE, "閉じる(&C)\tAlt+F4");

		// メニューを表示して選択されたコマンドIDを得る
		UINT nIDItem
		nIDItem = menu.TrackPopupMenu(TPM_LEFTALIGN | TPM_LEFTBUTTON |
		                              TPM_RETURNCMD,
		                              point.x, point.y, this, NULL);

		// コマンドを実行させる
		PostMessage(WM_SYSCOMMAND, nIDItem, NULL);
		return 0;
	}
}

ダミー画面を用いる方法

LRESULT CMyDlg::DefWindowProc(UINT message, WPARAM wParam, LPARAM lParam) 
{
	if(0x0313 == message)
	{
		// lParam はクリック位置を表すPOINTデータ
		PostMesgage(WM_USER + 100, wParam, lParam);
	}

	if(WM_USER + 100 == message)
	{
		// クリック位置(スクリーン座標なので変換の必要なし)
		CPoint point = lParam;

		// メニューを作成する(項目左横の絵に対応)
		// ダミーの画面を一時的に作成してシステムメニューを借りる
		CWnd wnd;
		wnd.Create(NULL, NULL, WS_OVERLAPPEDWINDOW, CRect(0, 0, 0, 0),
		           GetDesktopWindow(), 0);
		CMenu *pSysMenu = wnd.GetSystemMenu(FALSE);

		// この位置でメニューの各項目を調整します

		// メニューを表示して選択されたコマンドIDを得る
		UINT nIDItem;
		nIDItem = pSysMenu->TrackPopupMenu(TPM_LEFTALIGN |
		                                   TPM_LEFTBUTTON |
		                                   TPM_RETURNCMD,
		                                   point.x, point.y,
		                                   this, NULL);

		// ダミーウィンドウを破棄する
		wnd.DestroyWindow();

		// コマンドを実行させる
		PostMessage(WM_SYSCOMMAND, nIDItem, NULL);
		return 0;
	}
}

記:2003/04/27

 

キー押下をエミュレートする

Q コントロールの操作など OnKeyDown に仮想キーを設定して直接呼び出しても動作しない
がどうすればよいか?
   
A 引数に値を設定しても、基本クラスはその値を見ない。
また、実際にはキー押下後にどう処理すべきかを経てから OnKeyDown などが呼び出さ
れるので、直接呼び出しても意味が無い。
そこで、キー押下のエミュレートAPIを用いる。

{
	// キー押下のエミュレート(例ではスクリーンショット用のキー押下)
	keybd_event(VK_SNAPSHOT, 0, 0, 0);    // 全体「PintScreen」
	keybd_event(VK_SNAPSHOT, 1, 0, 0);    // アクティブ「Alt」+「PintScreen」
}

注意:Microsoftより正常に機能させる方法についてのコメントが出ています。
    詳しくはこちらをご覧下さい。

記:2003/06/03

 

リストビュー(レポート)でアイテムの高さを変更する

Q CListCtrlにはアイテムの高さを変更するメンバ関数は無いが、アイテムの高さを変更したい。
   
A フォントを変更することで高さは変わるが、字のサイズも当然変わってしまう。
CImageListを用いて任意のイメージを設定することで高さを変更する。

イメージリストのソースはアイコンでもビットマップでも良い。
この例では、イメージデータを持たない空のイメージリストを用いている。

class CMyDialog : public CDialog
{
public:
	//(略)

	CImageList m_image;    // 高さを調整する為のイメージリスト
};


CMyDialog::PreSubclassWindow()
{
	// 幅1、高さ40のイメージを持つ
	m_image.Create(1, 40, ILC_COLOR, 0, 0);
}

BOOL CMyDialog::OnInitDialog()
{
	//(略)

	// TODO: 初期化をここに追加します。

	// 高さ調整用のイメージリストを設定する
	m_List.SetImageList(&m_image, LVSIL_STATE);

	// サンプル表示用のコード
	m_list.InsertColumn(0, "ヘッダー", LVCFMT_LEFT, 100);
	m_list.InsertItem(0, "アイテム1");
	m_list.InsertItem(1, "アイテム2");
	m_list.InsertItem(2, "アイテム3");
}


図1 アイテム高さを変更したレポートビュー

記:2003/02/08

 

ダイアログでアクセラレータテーブルを使用する

Q ダイアログにはメニューを簡単に追加できるが、メニューにあわせてアクセラレータテーブルも追加したい。
   
A PreTranslateMessage をオーバーライドしてキーボードアクセラレータを処理するようにする。

{
	// アクセラレータテーブルをロードする
	m_hAccelerator = ::LoadAccelerators(AfxGetInstanceHandle(),
	                                    MAKEINTRESOURCE(IDR_ACCELERATOR1));
}

BOOL CMyDialog::PreTranslateMessage(MSG* pMsg) 
{
	// TODO: この位置に固有の処理を追加するか、または基本クラスを呼び出してください

	// キーボードアクセラレータを処理する
	if(0 != ::TranslateAccelerator(m_hWnd, m_hAccelerator, pMsg))
	{
		return TRUE;
	}

	return CDialog::PreTranslateMessage(pMsg);
}

記:2002/06/15

 

フレームウィンドウのアクセラレータテーブルを状況によって切り替える

Q 表示するビューの切り替えにあわせて、使用するアクセラレータテーブルを変更するにはどうすれば良いか。
   
A CDocument::GetDefaultAccelerator または CFrameWnd::GetDefaultAccelerator をオーバーライドしてアクセラレータテーブルを切り替える。

<注意>
サンプルでは GetDefaultAccelerator 内でアクセラレータテーブルをロードしているが、GetDefaultAccelerator は Ctrl キー押下時などの度に呼び出されるので、あらかじめメンバ変数にロードしておくと良い。

HACCEL CMainFrame::GetDefaultAccelerator()
{
	// 別のアクセラレータテーブルを使用する場合
	if(TRUE == m_bOtherAccelerator)
	{
		// 別のアクセラレータテーブルを返す
		return ::LoadAccelerators(AfxGetInstanceHandle(),
		                          MAKEINTRESOURCE(IDR_ACCELERATOR1));
	}

	// 標準のアクセラレータテーブルを返す
	return ::LoadAccelerators(AfxGetInstanceHandle(),
	                          MAKEINTRESOURCE(IDR_MAINFRAME));
}

記:2002/04/27

 

固定幅フォントを使用する

Q1 フォントの「MS 明朝」や「MS ゴシック」は固定幅のフォントであるが、ダイアログのプロパティで設定したり CFont::CreatePointFont で作成したフォントを設定しても正しく表示されない。
   
A1 これらの設定方法では、フォントは以下のようになる。
  • 半角として固定幅("A" と "W" の幅が同じ)
  • 全角として固定幅
  • 全角は半角の2倍幅ではない(全角の方が狭い)

全角が半角の2倍幅となるようにするには、CFont::CreateFontIndirect、CFont::CreateFont、CFont::CreatePointFontIndirect などを使用して FIXED_PITCH を指定する必要がある。

{
	LOGFONT logfont;
	CFont font;

	// LOGFONT構造体を設定する
	memset(&logfont, NULL, sizeof(logfont));
	logfont.lfHeight         = -80;                    // 8ポイント
	logfont.lfWeight         = FW_NORMAL;
	logfont.lfCharSet        = SHIFTJIS_CHARSET;       // Shift-JIS
	logfont.lfOutPrecision   = OUT_DEFAULT_PRECIS;
	logfont.lfClipPrecision  = CLIP_DEFAULT_PRECIS;
	logfont.lfQuality        = DEFAULT_QUALITY;
	logfont.lfPitchAndFamily = FIXED_PITCH;            // 固定幅指定
	strcpy(logfont.lfFaceName, "MS ゴシック");       // フォント名

	// フォントを作成する
	font.CreateFontIndirect(&logfont);
}

 

Q2 印刷の場合はどうすれば良いか。
   
A2 以下のようにすれば正しく印刷される。
(ただし、プレビュー表示は「中」以外では等幅で表示されない)

void CMyView::OnPrint(CDC* pDC, CPrintInfo* pInfo) 
{
	pDC->SetMapMode(MM_ISOTROPIC);

	// Window, Viewport左上を( 0, 0 )に設定する
	pDC->SetWindowOrg(0, 0);
	pDC->SetViewportOrg(0, 0);

	// View領域を取得、Windowに設定する
	CRect rectClient;
	GetClientRect(&rectClient);
	pDC->SetWindowExt( rectClient.Width(), rectClient.Height() );

	// 用紙サイズをViewportに設定する
	pDC->SetViewportExt( pDC->GetDeviceCaps(HORZRES),
	pDC->GetDeviceCaps(VERTRES) );

	// LOGFONT構造体を設定する
	LOGFONT logfont;
	memset(&logfont, NULL, sizeof(logfont));
	logfont.lfHeight         = -80;                    // 8ポイント
	logfont.lfWeight         = FW_NORMAL;
	logfont.lfCharSet        = SHIFTJIS_CHARSET;       // Shift-JIS
	logfont.lfOutPrecision   = OUT_DEFAULT_PRECIS;
	logfont.lfClipPrecision  = CLIP_DEFAULT_PRECIS;
	logfont.lfQuality        = DEFAULT_QUALITY;
	logfont.lfPitchAndFamily = FIXED_PITCH;            // 固定幅指定
	strcpy(logfont.lfFaceName, "MS ゴシック");       // フォント名

	// フォントを作成する
	CFont font;
	font.CreatePointFontIndirect(&logfont, pDC);

	// フォントを選択して描画(印刷)する
	CFont *pOldFont = (CFont*)pDC->SelectObject(&font);
	OnDraw(pDC);
	pDC->SelectObject(pOldFont);
}

記:2002/06/15

 

デフォルトの印刷方向を変更する

Q ファイルメニューの「プリンタの設定」は通常、縦がデフォルトになっている。デフォルトを変えたい場合はどうすれば良いか。
   
A 初期化処理などを行う場所で、デバイスの情報を変更する。

{
	PRINTDLG stPrintdlg;
	CWinApp* pApp =AfxGetApp();

	// デフォルトの設定を取得する
	pApp->GetPrinterDeviceDefaults(&stPrintdlg);

	// デバイスモードハンドルから構造体を取り出す
	DEVMODE FAR* pstDevMode = (DEVMODE FAR*)::GlobalLock(stPrintdlg.hDevMode);

	// 値を設定する
	pstDevMode->dmOrientation = DMORIENT_LANDSCAPE;    // 横向き

	// ロックを解除する
	::GlobalUnlock(stPrintdlg.hDevMode);
}

記:2002/06/15

 

〜する

Q どうすれば良いか。
   
A こうする。

サンプルコード

記:2002/04/27

 


Copyright(C) RainyLain 2000, 2003