時間のかかる処理を行うとき,「終了まであと?秒」なんていう表示がよくありますが,
それを簡単に実装できるクラスを作ってみました。残り時間推定クラスは
コレ です。
最近はPICマイコンで遊んでいて,PCと通信するときにあったらいいなぁと思って作りました。
使い方はソースを見てください。
あ,ただし,ダイアログやプログレスバーなどは各自実装してくださいね。あくまでも時間の計算だけです。
今どきのパソコンでアナログモデムなんて使わないしCOMポート(RS232C)はもう使わないかと思っていましたが,
携帯電話との通信や,USB-RS232C変換ケーブル,BluetoothのSPP接続と,COMポートの使用は増える一方です。
しかも動的に,歯抜けで増減したりするからプログラム中でどのCOMポートが存在するか調べなくちゃいけなくなってしまいました。
まあ,決め打ちでCOM1からCOM10くらいまでメニューに用意しておけばいいかもしれませんが…。
いい機会なので有効なCOMポートの一覧はどうやって取得するのか調べてみました。
が,レジストリを直接参照する方法しか見つかりませんでした。以下のコードです。
// シリアルポート一覧取得
// CStringArrayに"COM1"などのポート名称を格納する
// 戻り値はポート数(エラーの場合は0)
#include "atlbase.h"
INT_PTR GetSerialCommList( CStringArray &strListArray )
{
CRegKey Reg;
LONG iRet = Reg.Open( HKEY_LOCAL_MACHINE, "HARDWARE\\DEVICEMAP\\SERIALCOMM", KEY_READ );
if( iRet != ERROR_SUCCESS ){
return 0;
}
strListArray.RemoveAll();
DWORD dwIndex = 0;
TCHAR szName[256], szValue[256];
DWORD nNameLen, nValueLen, dwType;
for(;;){
nNameLen = nValueLen = 256;
iRet = RegEnumValue( Reg.m_hKey, dwIndex, szName, &nNameLen, 0, &dwType, (BYTE *)szValue, &nValueLen ); // なぜか CRegKey のメンバにRegEnumValue()がない!
if( iRet != ERROR_SUCCESS ){
break;
}
if( dwType == REG_SZ ){
strListArray.Add( szValue );
}
dwIndex++;
}
Reg.Close();
return strListArray.GetCount();
}
モードレスダイアログを使ったアプリをデバッグ実行した後,次のようなワーニングが出ることがありました。
Warning: calling DestroyWindow in CDialog::~CDialog --
OnDestroy or PostNcDestroy in derived class will not be called.
これは,派生ダイアログクラスで OnDestroy あるいは PostNcDestroy が呼び出されなかったというワーニングです。
どうやら,モードレスダイアログを Create( ) した後,用が済んだら明示的に DestroyWindow( ) を
呼び出してあげないといけないようです。
検索や置換のためのダイアログはコモンダイアログとして用意されています。
使用するための流れを以下に記載しました。
1. メインダイアログのメンバ変数に検索ダイアログのポインタと検索文字列を追加
CFindReplaceDialog *m_pFindDialog;
CString m_strFindString;
2. メインダイアログの初期化で上記ポインタと文字列を初期化
this->m_pFindDialog = NULL;
this->m_strFindString = "";
3. メインダイアログのメニューハンドラなどで検索ダイアログを作成
if( this->m_pFindDialog ){
return;
}
this->m_pFindDialog = new CFindReplaceDialog;
if( this->m_pFindDialog ){
BOOL bRet = this->m_pFindDialog->Create( TRUE, this->m_strFindString, NULL, (FR_DOWN|FR_HIDEMATCHCASE|FR_HIDEWHOLEWORD) );
if( !bRet ){
this->m_pFindDialog = NULL;
}
}
4. メインダイアログに検索ダイアログからのメッセージを受け取るための関数を追加(メンバ関数としてヘッダにも追加)
// 検索ダイアログからのメッセージを受信
LRESULT CMLOGVWDlg::OnFindCmd( WPARAM wParam, LPARAM lParam )
{
CFindReplaceDialog *pDlg = CFindReplaceDialog::GetNotifier( lParam );
if( pDlg == this->m_pFindDialog ){
// 終了
if( pDlg->IsTerminating() ){
pDlg->DestroyWindow();
this->m_pFindDialog = NULL;
}
// 次を検索
else if( pDlg->FindNext() ){
// 検索文字列の取得
this->m_strFindString = pDlg->GetFindString();
// 検索方向の取得
BOOL bSearchDown = pDlg->SearchDown();
// TODO: ここに独自の検索処理を入れる
}
}
return 0;
}
5. 検索ダイアログからのメッセージを受け取るためメインダイアログにマクロを追加
const UINT wm_Find = RegisterWindowMessage( FINDMSGSTRING ); // ← ★追加
BEGIN_MESSAGE_MAP(CxxxxDlg, CDialog)
…
ON_REGISTERED_MESSAGE(wm_Find, OnFindCmd) // ← ★追加
END_MESSAGE_MAP()
※備考
検索オプションを変更する場合や置換の場合は CFindReplaceDialog の Create( ) で引数を修正してください。
検索オプションがどのように設定されているか調べるには CFindReplaceDialog のメンバ関数を実行します。
詳細はヘルプを参照してください。
MFCのSDIやMDIアプリケーションではビューに描いたものを簡単に印刷できます。
方法は,ドキュメントとビュー の後半を参照してください。
定期的ではない処理を実行するときに,処理状況をダイアログ上のエディットコントロールに
表示しながら進めたい場合があると思います。
たとえばチェック処理。チェック開始ボタンを押したらダイアログが開いて
自動的にチェック処理が始まり,状況をダイアログ上に表示する。
チェックが終わってもクローズボタンを押すまではダイアログは閉じない。
このときすぐに思いつくのは新規作成したダイアログのOnInitDialog( )にやりたい処理を実装するということです。
しかしこの場合,ダイアログが表示されるのは処理が終わってからです。処理状況を画面上に表示できません。
今度はOnPaint( )ではどうでしょうか。OnPaint( )が終わってから実際の描画が行われるようなのでこれもダメです。
筆者は SetTimer( ) と OnTimer( ) を使ってこの仕様を以下のように実装してみました。
// 処理状況を表示する文字列とエディットコントロール(メンバ変数)
CString m_strLog;
CEdit m_EditLog;
OnInitDialog( )
// 開始メッセージ表示
m_strLog = "処理開始...\r\n"
this->m_EditLog.SetWindowText( m_strLog );
// 500msec後に処理開始
this->SetTimer( 1, 500, 0 );
OnTimer( )
// 処理は一度だけ
this->KillTimer( 1 );
// 処理をここに入れる
// 処理状況を表示
m_strLog += "処理中です...\r\n";
this->m_EditLog.SetWindowText( m_strLog );
// 終わったらエディットコントロールをスクロールさせて最終行を見せる
this->m_EditLog.LineScroll( this->m_EditLog.GetLineCount() );
◆ クラスウィザードが応答なしになってしまう(VC7)
Visual C++ .NET2003のVisual Studioでクラスを追加するときクラスウィザードが応答なしになってしまうことがありました。
よくわからないのですが,stdafx.h を開こうとして応答なしになっているようです。あらかじめ stdafx.h を開いておいてから,クラスを追加すると大丈夫でした。
stdafx.h に記述を追加しているからでしょうか?
デバッグの際,出力ウインドウに "MRU: open file (1)" とかいうメッセージがでることがあります。
何かのエラーかとびっくりしましたが,最近使ったファイルをオープンするとこのメッセージが出ることが分かりました。
Most Recently Used の略で,システムの機能が働いたことを通知するみたいです。もちろんエラーではありません。
リソースエディタでダイアログを新規作成し,それを子ダイアログとして親ウインドウからCreateしたとき,
ダイアログは表示されるけどその上に載っているコントロールを操作できないことがありました。
実はダイアログのプロパティで Disabled が True になっていただけでしたが,
気がつくまでにかなり時間がかかりました(子ダイアログなのでタイトルバーが表示されておらずグレイアウトされているのが分からないため)。
ダイアログリソースを追加する際に,プロパティページ型のダイアログを挿入するとデフォルトで Disable が True になっているので気をつけてください。
Microsoft Visual Studio .NET 2003 のリソースエディタで新規にダイアログを作成して,
さぁ OnInitDialog() をオーバーライドしようと思っても,どこをどうしたらいいんだか
さっぱり分かりません。しかたがないので基本クラスのソースをコピペして手動で
実装していましたが,やっぱりそういう機能がありました。
「表示」→「クラスビュー」でプロパティを表示,
「◆」ボタンを押してオーバーライドさせたい処理を選択する。
しかし,なんでリソースエディタのプロパティからは◆ボタンが出てこないかなぁ…。
こういう細かいところで使い勝手が悪くなってる気がする。
例えばマウスドラッグで領域を指定する場合などで,CRectの座標が,左(left)>右(right) または 上(top)>下(bottom) に
なってしまう場合も多々あると思います。そのまま幅や高さを計算しようとするとマイナス値になってしまいますが,
それぞれのメンバの大小を比較して入れ換える処理をベタで実装するのもスマートではありません。
そんな時はCRectのメンバ関数 NormalizeRect() を呼び出せば上記の大小入れ換えをやってくれます(正規化と呼ぶようです)。
◆ データ管理の味方 CArray と CList
SDIやMDIアプリケーションを作成するとき,ドキュメントファイルのフォーマットをどうしようか悩むと思います。
固定長で固定配置のデータなら別にどうってことないのですが,可変長のデータだとどうやってデータを管理しましょうか?
そんなとき強力な味方になってくれるのが CArray と CList です。いろいろ言われている MFC ですが,これがあるおかげで
可変長ドキュメントでもかなり楽チンにプログラミングできます。
簡単に説明すると,CArray と CList は既存の型はもちろん,独自に定義した構造体や共用体などを配列またはリストとして
動的に管理できるようになるものです。
例えば以下のような構造体と共用体を定義したとします。
typedef struct hoge_st {
int a, b, c, d;
} HOGE_ST;
typedef union fuga_un {
int a;
short b;
char c;
} FUGA_UN;
この型のデータを動的に管理するには,malloc() や new でメモリを確保して,そのアドレスをリンクリストで
管理するのが普通です。削除するときもメモリリークしないように気をつけなければいけませんし,
シリアライズするときも一度にはできません。とーっても面倒くさいです。
CArray を使うと,あたかも上記データを要素として持つ「配列」として扱うことができます。
同じように CList を使うと「リスト」として扱えます。また,CArray と CList はいずれもシリアライズ可能なので
メンバ関数である Serialize() を実行するだけでファイル入出力ができてしまいます!すばらしい!!
使用サンプルを以下に示します。
#include
typedef struct hoge_st {
int a, b, c, d;
} HOGE_ST;
typedef union fuga_un {
int a;
short b;
char c;
} FUGA_UN;
// オリジナル構造体の配列を確保
CArray HogeArray; // デフォルトでは配列の要素数は1ずつ拡張される
HogeArray.SetSize( 5, 10 ); // 要素数と拡張単位数を設定することもできる
// 要素にアクセス
HOGE_ST *p;
p = &HogeArray[0];
p->a = 0xAAAAAAAA;
// 途中で要素数の拡大可能
HOGE_ST hoge = {1,2,3,4};
HogeArray.Add( hoge );
INT_PTR n = HogeArray.GetSize();
p = &HogeArray[n-1];
// 全要素を削除
HogeArray.RemoveAll();
// オリジナル共用体のリストを確保
CList FugaList(16); // リスト要素の拡張単位数を最初に設定(デフォルト10)
// 要素を登録
FUGA_UN fuga;
fuga.a = 0xAAAAAAAA;
fuga.b = (short)0xBBBB;
fuga.c = (char)0xCC;
FugaList.AddTail( fuga );
INT_PTR m = FugaList.GetCount();
// リストの検索
POSITION pos;
FUGA_UN f;
pos = FugaList.GetHeadPosition();
for( int i=0; i
なお,ダイアログアプリケーションでは #include <afxtempl.h> しないと使えないようです。
ちなみに CArray<CString,CString&> は CStringArray と,CList<CString,CString&> は CStringList と同等です。
ウインドウのサイズではなくて,クライアント領域のサイズを設定したいときは
以下のようにしています。ダサいですか?(笑)
// クライアント領域サイズ設定
CRect WindowRect, ClientRect;
this->GetWindowRect( WindowRect );
this->GetClientRect( ClientRect );
int dw = WindowRect.Width() - ClientRect.Width();
int dh = WindowRect.Height() - ClientRect.Height();
this->SetWindowPos( 0, 0, 0, CLIENT_WIDTH+dw, CLIENT_HEIGHT+dh, SWP_NOMOVE );
this->CenterWindow();
◆ 動的に実装したコントロールのイベント処理方法(VC7)
動的に実装したコントロールのイベントを処理するには以下のようにする必要があります。
最初に,動的に実装するコントロール用に新しいクラスを作成します。
プロジェクト → クラスの追加 → MFC → MFCクラス で既存クラスを基本クラスに持つ
新規クラスを作成します。
クラスビューの該当クラスで 右クリック → プロパティ で,イベントやメッセージに
対する処理を実装します。
動的にコントロールを実装するときに,新しく作成したクラスのオブジェクトを実装します。
IDE(統合開発環境)のようなドッキングバー(ドッキングウインドウ)を簡単に実装するには
以下のサイトのソースコードを利用させてもらうのがいいでしょう。
http://www.datamekanix.com/sizecbar/
個人でも商用でもロイヤリティ無料で利用できるので(ただし著作権は放棄していない)重宝しています。
使用方法は上記のサイトでも解説があったり,アーカイブにデモソースがあったりするので割愛します。
以下はおぼえがき程度の実装メモです。
・sizecbar-v2.44 のソースとヘッダをプロジェクトに登録します。afxChNil が定義されていないという
エラーがコンパイル時に発生するので,sizecbar.cpp に以下の行を追加します。
/////////////////////////////////////////////////////////////////////////////
// "afxChNil" undeclared identifier in VC++.NET 2003
static TCHAR afxChNil = '\0';
/////////////////////////////////////////////////////////////////////////////
・stdafx.hに以下の行を挿入します。
// ドッキングウインドウの追加
#include "..\sizecbar-v2.44\src\sizecbar.h"
#include "..\sizecbar-v2.44\src\scbarg.h"
#include "..\sizecbar-v2.44\src\scbarcf.h"
・MainFrm.hでメンバ変数を追加します。
CSizingControlBarCF m_wndXXXXBar;
・MainFrm.cppで以下のように実装します。
// ドッキングウインドウの追加
if( !m_wndXXXXBar.Create( _T("Docking Bar"), this, IDR_CONTROLBAR ) ){
return -1; // 作成できませんでした。
}
m_wndXXXXBar.SetSCBStyle( m_wndXXXXBar.GetSCBStyle()|SCBS_SIZECHILD );
m_wndXXXXBar.SetBarStyle( m_wndXXXXBar.GetBarStyle()|CBRS_TOOLTIPS|CBRS_FLYBY|CBRS_SIZE_DYNAMIC );
m_wndXXXXBar.EnableDocking( CBRS_ALIGN_LEFT|CBRS_ALIGN_RIGHT );
// ドッキングを可能にする
EnableDocking(CBRS_ALIGN_ANY);
DockControlBar(&m_wndXXXXBar, AFX_IDW_DOCKBAR_LEFT);
あとは,ドッキングウインドウを親として各種コントロールを実装すればよいです。
ドッキングウインドウを親ウインドウとして持つオブジェクトのサイズを調整する場合は,
WM_SIZEメッセージハンドラを実装して,そこでMoveWindow( )すれば可能です。
GetClientRect( ) ではドッキングウインドウのサイズをうまく取れないようです。
印刷プレビューウインドウのボタンが英語になるのは MFC の DLL に原因があります。
デフォルトでは,プロジェクト → プロパティ → 全般 → MFCの使用 で,「スタティックライブラリでMFCを使用する」
となっていると英語に,「共有DLLでMFCを使用する」となっていると日本語になります。
スタティックライブラリでも日本語にしたい場合は,プロジェクト → プロパティ → リソース → 追加のインクルードディレクトリ に,
$(VCInstallDir)\atlmfc\include\l.jpn を追加すると良いです。
この際,プロジェクト → プロパティ での構成を「全ての構成」にすることを忘れないでください。
VC++.NET 2003 のリソースビューで Menu を新規追加したのはいいけど,
メニューのリソースIDを IDR_MENU1 からどうしても変更できない!
…と思ったときは,ツリー項目の IDR_MENU1 を左クリックしてからおもむろに Alt+Enter を押してみてください。
プロパティウインドウが開いて変更できるでしょう。表示 → プロパティウインドウ でも可。
コンテキストメニュー(右クリックポップアップメニュー)にプロパティが存在しないのは不親切です!!
VC++.NET 2003でもリソースビューでダイアログの挿入をして,ダイアログエディタでボタンなどの
コントロールを載せていきますが,そのままでは,そのコントロールに変数を追加できません。
前もって新規ダイアログのクラスを作成しておかなければなりません。
VC4だと自動的にクラスウィザードが起動されたのですが,VC7では手動のようです。なんとなく不親切なんだよな〜。
なお,ダイアログに配置したコントロールの初期化は OnInitDialog() を実装して行ないます。
ダイアログのコンストラクタや OnCreate() ではまだコントロールの実体が生成されていないので
エラーになってしまいます。
SDI や MDI アプリケーションで,ステータスバーにメッセージを表示する方法。
ステータスバーの表示領域を増やすには,MainFrm.cpp の以下の配列で ID_SEPARATOR の行を
単純に増やすだけでできます。
static UINT indicators[] =
{
ID_SEPARATOR, // status line indicator
ID_SEPARATOR, // status line indicator
ID_INDICATOR_CAPS,
ID_INDICATOR_NUM,
ID_INDICATOR_SCRL,
};
ステータスバーにメッセージを表示するには,
CMainFrame *pMainFrm = (CMainFrame *)AfxGetMainWnd();
CStatusBar *pStatusBar = (CStatusBar*)pMainFrm->GetMessageBar();
pStatusBar->SetPaneText( n, "message" );
ここで n は領域番号を示します。
クライアント領域に何かを描画して,それをスクロールさせたくなったら
ダイアログベースアプリケーションから卒業する時期です(笑)。
ダイアログベースはわりと簡単なのでウインドウプログラミングを始めるには
とっつきやすいですが,スクロールバーを付けて描画をしようとすると,
とたんに難しくなります。ボタンなどはスクロールバーについていきますが,
自分で描画したものは動きません。
ビットマップをメモリ上において,貼り付ける位置をスクロールバーの位置に応じて変える…
なんてことはしなくていいです。知らなかった私はしてしまいましたが。
長いものには巻かれろ,腐ってもMicrosoft,
そういう場合はSDIアプリケーションにすれば良かったんです。
最初のAppWizardでViewの基本クラスをCScrollViewにしておけば,
スクロールバーの制御を勝手にしてくれるし(単位と範囲の設定は必要),
印刷機能まで付けてくれます。
VC++4.0ではプロジェクトが存在するフォルダのパス名に半角スペースが入っていると
ブレークポイントを張っても止まらないようです。え?今時4.0なんて使っている
人はいない? う〜,今年こそパソコン買い換えて環境を最新にするぞ!!
リソースエディタで登録,編集できるデータはアイコンやメニューだけではありません。
WAVデータなど,その他全てのデータファイルを実行ファイル内に格納し,
リソースとして使用することができます。
まず,リソースエディタでリソースのインポートを実行して格納するデータファイルを
指定します。このときインポートするときに用途を「カスタム」にするとどんなファイルでも
リソースとすることができます。
次にプログラム中からリソースのアクセスですが,WAVファイルをリソースとした場合に
そのデータのアドレスを取得して再生するというサンプルを以下に示します。
#include
#include "resource.h"
#pragma comment( lib, "winmm.lib")
void main( void )
{
/* リソースからWAVデータを読み込む */
HRSRC hRes = FindResource( NULL, (const char *)IDR_WAVE1, "WAVE" );
HGLOBAL hSound = LoadResource( NULL, hRes );
/* 読み込んだデータのアドレスを取得 */
LPVOID pSound = LockResource( hSound );
/* WAVデータを再生 */
sndPlaySound( (const char *)pSound, SND_SYNC|SND_MEMORY );
// ロードしたリソースの解放
FreeResource( hRes );
}
リソースの解放は明示的に FreeResource( ) しなくてもプログラムの終了時にシステムが
自動で解放するようです。ヘルプにはWindows95では必ず FreeResource( ) しろと書いて
あるのに,デバッグでステップ実行してみると FreeResource( ) 自体は何もしていなかっ
たりするのですが,FreeResource( ) したからといって即座にメモリが解放されるかどうか
は95系かNT系かで実装が異なるみたいです。
【2007.02.02追記】
Visual Studio .NET 2003 (VC7) にはカスタムリソースのインポートが期待通りできない不具合があります。
SP1でも直ってません。ただ,以下のように操作をすれば可能です。
リソースビューを右クリックして「リソースの追加...」を選択する。
「カスタム...」を押して,リソースの種類は「WAVE」を入力する。
リソースビューに追加されたIDR_WAVE1などのシンボルをダブルクリックする。
バイナリエディタで適当な値を追加する。
ファイルメニューの「全てを保存」した後,該当リソースのプロパティでファイル名を登録したいものに変更する。
ファイルを読み込みますか?ときかれるので「はい」を選択する。
あと,リソースからサウンド再生する方法は次のようにすると簡単でした。VC++.NET 2003で確認しました。
#include "Mmsystem.h"
#pragma comment(lib,"winmm")
PlaySound( "IDR_WAVE1", AfxGetInstanceHandle(), SND_RESOURCE|SND_ASYNC );
sndPlaySound() は PlaySound() のサブセットで互換性のために残されている関数のようです。
ダイアログベースのアプリケーションにコマンドプロンプトからパラメータを指定して
実行できるようにしたとき,コンソールにメッセージを表示したいことがあります。
このとき単にprintf( )してもメッセージが全然表示されません。_cprintf( )でもダメダメです。
調べたところ,そのアプリケーション用に新しくコンソールを開かないといけないみたいです。
面倒くさいですが,次がそのサンプルコードです。出力コンソールを開いてメッセージを出力したあと,
すぐに終了しないで入力コンソールを開いてRETURNキーを待つということをしています。
なおサンプルコードを使用するにはStdAfx.hのVC_EXTRALEAN定義をコメントアウトしておく必要があります。
// 出力コンソールを開く
AllocConsole();
HANDLE hFile = CreateFile( "CONOUT$", GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL );
// コンソール出力
char szMessage[] = "コンソール出力サンプル\n";
DWORD dwConsole;
WriteFile( hFile, szMessage, strlen( szMessage ), &dwConsole, NULL );
// コンソールを閉じる確認
char szReturn[] = "\n終了するにはRETURNキーを押してください。\n";
WriteFile( hFile, szReturn, strlen( szReturn ), &dwConsole, NULL );
CloseHandle( hFile );
// 入力コンソールを開く(RETURNキーを待つ)
hFile = CreateFile( "CONIN$", GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL );
ReadFile( hFile, &dwConsole, 1, &dwConsole, NULL );
CloseHandle( hFile );
// コンソールを閉じる
FreeConsole();
メニュー項目などに「開く(O ) CTRL+O」などとあると,コントロールキーを押しながら
Oキーを押すとこの項目を実行できます。このCTRL+Oがアクセラレータキーと呼ばれるものです。
アクセラレータキーを実装すると使いやすいアプリケーションになるため,よく使う機能には
是非キーを割り当てたいものです。
MFC Appwizardを使用してSDIやMDIアプリケーションを作成するとアクセラレータ機能が標準で
実装されます。リソースのAcceleratorという項目にアクセラレータキーのリストがあるはずです。
ここにメニュー項目IDとキー操作を登録することで,アクセラレータキーの実装が済んでしまいます。
簡単ですね!メニュー項目のキャプションは 「開く(&O)...\tCTRL+O」というように記述しておきます。
ところがダイアログベースのアプリケーションではこの方法で実装できません!!できなくは
ないですが,アクセラレータキーの機能自身を実装しなくてはいけないので大変です。
そこで見かけ上アクセラレータキーと同じ動きになるような方法をとることにします。
メニューの項目のキャプションは同じように「開く(&O)...\tCTRL+O」というように記述します。
ダイアログにPreTranslateMessage( )ハンドラを実装して以下のようにコーディングすると簡単に
アクセラレータキーもどきが実装できます。
BOOL C????Dlg::PreTranslateMessage(MSG* pMsg)
{
if( pMsg->message == WM_KEYDOWN ){
// CTRL+O アクセラレータキーの代用
if( pMsg->wParam =='O' && (GetKeyState( VK_CONTROL )&0x8000) ) On????Open( );
}
return CDialog::PreTranslateMessage(pMsg);
}
なおOn????Open( )はメニュー項目のハンドラです。
ネットワークプログラミングは難しそうに見えますが,クライアント(要求者)とサーバ(提供者)とで
データをやりとりしているに過ぎません。そのやりとりがプロトコルというお約束で決められているだけです。
クライアントが「海」と言ったらサーバが「山」といって色々なデータがやりとりされているようなものです。
もちろん独自のプロトコルを作ってしまうことも可能です。ただ通信は相手がいることなので,その場合は
クライアントとサーバの両方のアプリケーションを作成する必要があります。
アプリケーションは通信する相手毎にソケットというものを用意します。複数のソケットを作成して複数の相手と
同時に通信することも可能です。全てのネットワークプログラミングはこのソケットを使用して行われます。
ソケットに関するインタフェースはwinsockと呼ばれるAPIが用意されています(wsock32.dll)。
簡単に言うと,WSAStartup( )でAPIを初期化して,socket( )でソケットを作成して,connect( )で接続して
send( )でデータを送信して,recv( )でデータを受信して,closesocket( )でソケットを削除してWSACleanup( )で
後始末をするという流れです。このようにネットワーク通信プログラム自身は大して難しくありません。
どのような手順でどんなデータをやりとりするかというプロトコルのプログラミングが面倒くさいだけです。
MFCにはソケットインタフェースをカプセル化したクラス CSocket があります。これはより詳細な制御を行える
CAsyncSocket からの派生クラスですが,次のような簡単なコードでネットワーク通信プログラムが可能となって
しまいます。
// ※プロジェクトを作成する際にWindowsソケットを有効にしておく
// ソケット オブジェクトの構築
CSocket sockClient;
// SOCKET の生成
sockClient.Create( );
// コネクションの検索
sockClient.Connect( strAddr, nPort ); // アドレスとポート番号
// ファイル オブジェクトの構築
CSocketFile file( &sockClient );
// アーカイブの構築
CArchive arIn( &file, CArchive::load );
CArchive arOut( &file, CArchive::store );
// 受信データがあるか調査
arIn.IsBufferEmpty( );
arIn >> dwValue; // 受信
arOut << dwValue; // 送信
CSocket の派生クラスを作成し,データを受信した時のイベントハンドラをオーバーライドすることに
よって受信データ待ちもスマートにプログラミング可能です。
// ※プロジェクトを作成する際にWindowsソケットを有効にしておく
#include "stdafx.h"
// CSocket派生クラス
class CSocketEx : public CSocket {
CSocketEx( ){} // コンストラクタ
~CSocketEx( ){} // デストラクタ
// データ受信ハンドラ
void OnReceive( int nErrorCode )
{
// Receive( )関数で受信
CSocket::OnReceive(nErrorCode);
}
};
// VC++4.0ではクラスウィザードでCSocketを基本クラスにした派生クラスを
// 新規作成できなかったので手動で派生しています。VC++6.0ではできた…。
イベントハンドラには OnReceive( ), OnSend( ), OnAccept( ), OnConnect( ), OnClose( ) というものが存在します。
詳細はそれぞれヘルプを見てください。「Windows ソケット」をキーワードにヘルプを参照するとソケット
プログラミングが良く分かります。
なお,CSocket は通信が完了するまでプログラムが待たされます(同期)。CAsyncSocketは通信が完了しなくても
プログラムが継続します(非同期)。いずれにしてもプログラミングの複雑さはプロトコルの複雑さで決まります。
MFCには日付と時刻を管理する便利なCTimeクラスがあります。
以下のように簡単に時刻と時間の処理を実装することが可能です。
// 現在時刻で作成
CTime now = CTime::GetCurrentTime( );
// 指定した時刻で作成
CTime t = CTime( 2003, 10, 19, 11, 42, 0, -1 );
// 大小比較
if( now < t ){ ... }
// 差分の取得
CTimeSpan span = t - now;
// 時間の計算
now += span;
// UTC時刻からローカル時刻への変換
#include
CTime utc = CTime( 2003, 10, 19, 2, 42, 0, -1 );
CTime local = utc;
local -= CTimeSpan( (time_t)_timezone );
詳細は CTime や CTimeSpan や「日付と時刻」や「時間管理」をキーワードにヘルプを参照してください。
これまでは主にダイアログベースのアプリを作成してきました。
テキストエディタやペイントツールの様に、データを参照するだけでなく
編集や保存を行いたい場合は SDI または MDI アプリケーションを作成する
必要があります。
そこで必要になってくるのが「ドキュメント」と「ビュー」の概念です。
「ドキュメント・ビューアーキテクチャ」なんて仰々しく呼ばれたりするこの考え方は、
「ファイルとして残すデータとアプリケーションでデータをどう見せるかを切り離して
考えた方が何かと便利ですよ」というものです。
「ドキュメント」は「アプリケーションで扱うデータ」を意味していて、テキストファイルだけ
でなく画像データや実行ファイルなど全てのファイルが対象になり得ます。
また、例えばテキストファイルという「ドキュメント」は人が読める文字列で表示させたり、
ダンプリストで表示させたりと、その表示方法はいくつも考えられます。しかし表示方法
によってドキュメントの内容は変わることはありません。
つまりこの二つを明確に分けて考えるとデータの管理や表示方法などのプログラミングが
きれいに効率よくできますよというのが、ドキュメントとビューの考え方なのです。
では実際にサンプルコードを作成してみます。AppWizard で SDI または MDI アプリケーションを
作成するとドキュメントクラス C????Doc と ビュークラス C????View が作成されます。
ドキュメントクラスではファイルとして保存されるデータの定義を行い、
メンバ変数として実装します。また必要に応じてそのデータにアクセスするメンバ関数も追加します。
ただしデータファイル形式はここでは決定されません。後述の Serialize( ) 関数で決定されます。
この作業は C????Doc.h を変更します。
// アトリビュート
public:
char m_Data[10]; // ドキュメントデータ
新規ドキュメントを開いた時、このドキュメントデータの初期値を設定するように
C????Doc.cpp 内の OnNewDocument( ) を変更します。
BOOL C????Doc::OnNewDocument( )
{
if (!CDocument::OnNewDocument( )) return FALSE;
for( int i=0; i<10; i++ ) m_Data[i] = 0; // 新規ドキュメントデータ
return TRUE;
}
またドキュメントを開いたり保存したりする場合に呼ばれる関数が Serialize( ) です。
現在のデータを一旦保存して、次に読み込んだとき作業を継続できるという意味で「シリアライズ」
という名称になっています。 C????Doc.cpp 内にあるこの関数にコードを追加します。
void C????Doc::Serialize(CArchive& ar)
{
if (ar.IsStoring( )) // 保存する時
{
for( int i=0; i<10; i++ ) ar << m_Data[i];
}
else // 読み込む時
{
for( int i=0; i<10; i++ ) ar >> m_Data[i];
}
}
ar は CArchve クラスのオブジェクトですが、自動的に生成されていて、
ファイルの入出力を適切に行ってくれるものです。ファイル名の決定やオープン、
クローズは自動的に行われるので、上記のようなたった数行でドキュメントデータと
ファイルのやりとりが簡単に実装できます。
上記サンプルでは基本データ型の char 変数を演算子 << と >> を使うことによって
順次保存したり読み込んだりしています。この順番がデータファイルの形式を決定します。
残念ながら ar は基本データ型以外の構造体やクラスオブジェクトを直接扱うことはできません。
<< や >> が有効なのは CObject の派生クラスで Serialize( ) が実装されているものに限られます。
ファイル形式がテキストでよければ,以下のようにもできます。
void CxxxxDoc::Serialize(CArchive& ar)
{
if (ar.IsStoring())
{
CString String;
String.Format( "VERSION: 2.2\r\n" );
ar.WriteString( String );
String.Format( "PROJECT: %s\r\n", "hoge" );
ar.WriteString( String );
}
else
{
CString VersionString, ProjectString;
ar.ReadString( VersionString );
ar.ReadString( ProjectString );
}
}
ar.ReadString() でどんどん読み込んでいってテキスト終端に到達したとき,
戻り値は FALSE なり NULL を返してくれるのですが,
それと同時にMFCは必ず以下のような例外も発生してくれるようです…。
CArchive exception: endOfFile.
XXXX.exe の 0x???????? で初回の例外が発生しました : Microsoft C++ exception: CArchiveException @ 0x0012edbc。
例外は終端以降を読んだときにしてほしいなぁ。
初回の例外
は実害がないようなので無視していいのかな?
次にビュークラスの実装を行います。
最初に表示する時も含めて再描画する際に実行される関数が C????View.cpp 内の OnDraw( ) 関数
です。以下のように実装してみます。
void C????View::OnDraw(CDC* pDC)
{
C????Doc* pDoc = GetDocument( );
ASSERT_VALID(pDoc);
for( int i=0; i<10; i++ ){
CString Str;
Str.Format( "m_Data[%d] = %d", i, pDoc->m_Data[i] );
pDC->TextOut( 0, i*20, Str );
}
}
ドキュメントへは GetDocument( ) 関数を使ってアクセスします。
ドキュメントを変更するインタフェースはビュークラスに実装します。ここでは
マウスをダブルクリックしたときにドキュメントを変更してみます。
クラスウィザードでビュークラスに WM_LBUTTONDBLCLK ハンドラを実装して以下のように
コーディングします。
void C????View::OnLButtonDblClk(UINT nFlags, CPoint point)
{
C????Doc* pDoc = GetDocument( );
// ドキュメントデータの変更
for( int i=9; i>0; i-- ) pDoc->m_Data[i] = pDoc->m_Data[i-1];
pDoc->m_Data[0] = pDoc->m_Data[1] + 1;
// ドキュメントが更新されたことを通知
pDoc->SetModifiedFlag( );
// 更新されたドキュメントを表示するビューを再描画させる
pDoc->UpdateAllViews( NULL );
CView::OnLButtonDblClk(nFlags, point);
}
これでドキュメントとビューの一連のコーディングが終了しました。
何たってファイルを直接操作しなくて良いのですから簡単すぎますね!(笑)
【ドキュメント・ビュー追加TIPS】
ドキュメントとビューに関するおぼえがきを以下に記載します。
ドキュメントにはデータ内容と,データの操作関数の両方を用意すると使い勝手が良くなります。
ドキュメントの内容を作成するのはOnNewDocument( )で,内容を空にするのはDeleteContents( )をオーバーライドして実装します。
ドキュメントは SetModifiedFlag( ) で変更フラグを更新し,IsModified( ) で変更があるかどうかを確認できます。
ビュー側でデータの操作をする場合は GetDocument( )->操作関数( ) を実行します。
ドキュメント側から全てのビューを再表示したいときは UpdateAllViews( NULL ) を実行します。
このときビュー側では OnDraw() ではなく OnUpdate() が実行されるので注意してください。
ドキュメント側からビューアドレスを取得したい場合は GetFirstViewPosition( ) と GetNextView( ) を使用します。
メインフレームからビューとドキュメントにアクセスするには次のようにします。
CView *pView = this->GetActiveView( );
CDocument *pDoc = this->GetActiveDocument( );
※MDIの場合は最初に this->GetActiveFrame( ) で子フレームを取得してから。
ドキュメントまたはビューからメインフレームにアクセスするには次のようにします。
CFrameWnd *pMainFrm = (CFrameWnd *)AfxGetMainWnd( );
ドキュメント名を変更するには SetTitle( ) を使います。
ドキュメントが変更された場合,メインフレームのタイトルバーに変更フラグ(*)を以下のようにするとつけることができます。自動では面倒見てくれないようです。
// ドキュメント変更フラグ設定のオーバーライド
void CxxxxDoc::SetModifiedFlag(BOOL bModified)
{
// デフォルト
CDocument::SetModifiedFlag( bModified );
// 変更状況に応じてメインフレームのタイトルの末尾に * を付ける
CMainFrame *pMainFrm = (CMainFrame *)AfxGetMainWnd();
if( pMainFrm == NULL ){
return;
}
CString String;
pMainFrm->GetWindowText( String );
// 変更がある場合
if( bModified == TRUE ){
// 末尾が * でなければ * を追加
if( String.Right(1) != "*" ){
String += "*";
pMainFrm->SetWindowText( String );
}
}
// 変更がない場合
else {
// 末尾が * であれば削除
if( String.Right(1) == "*" ){
String = String.Left( String.GetLength()-1 );
pMainFrm->SetWindowText( String );
}
}
}
スクロールビューにスクロールバーが付かないときはビュークラスのソースで以下の項目を確認してください。
OnInitialUpdate( ) で CScrollView::OnInitialUpdate( ) よりも前に SetScrollSizes( ) しているか?
OnUpdate( ) をオーバーライドして SetScrollSizes( ) と ResizeParentToFit( ) を実行しているか?
スクロールビューのマウスボタン操作やカーソル移動のハンドラに渡される引数 point は
スクロールされた分が入っていない(ビューウインドウの見かけ上の座標)ので,
point += this->GetScrollPosition();
として加算しないと,描画座標と一致しません。
★印刷倍率
MFCアプリケーションウィザードでMFCアプリケーションを作成するとき,「高度な機能」のところで
「印刷と印刷プレビュー(P)」をチェックすると,ビューに描いたものの印刷機能を自動で付けてくれてとても便利です。
が,実際に印刷するととても小さく印刷されてしまいます。画面に描画する場合も印刷する場合も
同じ OnDraw( ) が使われているため,Ondraw( ) 内で解像度に合わせて以下のようにスケールを変える必要があります。
// 印刷時のスケール設定
if( pDC->IsPrinting() == TRUE ){
pDC->SetMapMode( MM_ANISOTROPIC );
pDC->SetWindowExt( 100, 100 );
pDC->SetViewportExt( 400, 400 );
}
【2007.05.08追記】
OnDrow( )で pDC->SetPixel( ) を使用しているとき,ディスプレイでは期待通り描画されるのに
印刷するとスカスカの点になってしまう場合があります。
印刷時は以下のように pDC->SetPixel( ) ではなく,pDC->FillSolidRect( ) を使うといいみたいです。
if( pDC->IsPrinting() == TRUE ){
pDC->FillSolidRect( x, y, 1, 1, nColor );
}
else {
pDC->SetPixel( x, y, nColor );
}
ウインドウの終了時に取得したメモリなどのリソースを解放しなくてはなりませんが、
具体的にどこで行えば良いのでしょうか?
調査の結果、以下のような順番で処理が進むようです。
OnClose( )
右上の[X]ボタンやシステムメニューで終了を選択した時のみ実行されます。
ダイアログなどでESCキーが押された場合には実行されないので、後処理を行うには
不向きです。
DestroyWindow( )
ウインドウを閉じる(廃棄する)時に実行されます。デフォルトの処理が行われた時点で
ウインドウが廃棄されます。アプリケーションの終了前にウインドウサイズを保存するなど、
ウインドウに関わる後処理を行う場合はここのデフォルト処理の前で行うと良いでしょう。
またウインドウで使われる可能性のあるリソースの解放などはデフォルト処理の後で
行うと良いでしょう。
OnDestroy
ウインドウが廃棄されてから呼ばれます。
ウインドウで使われる可能性のあるリソースの解放はここで行うと良いでしょう。
デストラクタ
ウインドウクラスのデストラクタなので全ての終了処理が終わってから一番最後に実行されます。
ウインドウに関する情報は既に全て廃棄されているので、ここでの後処理はあまり必要ないかも
しれません。
バイナリファイル操作には CFile クラスを使うのが便利ですが、ファイルの作成日時やサイズを
知りたいだけの場合もこのクラスの静的メンバ関数を使用するとファイルをオープンしないで
可能になります。
CFileStatus rStatus;
if( CFile::GetStatus( "filename", rStatus ) ){
CTime Date = rStatus.m_mtime; // ファイル最終変更日時
int nSize = rStatus.m_size; // ファイルの論理サイズ
}
else // ファイルが存在しない
CFileStatus にはファイル属性などの情報もあるので詳しくはヘルプを見てください。
ダイアログを開いたとき、デフォルトではOKボタンにフォーカスがセットされています。
これを変更するにはリソースエディタのレイアウトでタブオーダーの設定をします。
一番若いスタティックでないコントロールにセットされるようです。
タブオーダーの設定の仕方は、ダブルクリックして1番をセットし、あとは順番にシングル
クリックしてセットしていきます。
プログラム上でフォーカスを変更するには次のようにします。
NextDlgCtrl( ); // 次のコントロールへ
PrevDlgCtrl( ); // 前のコントロールへ
GotoDlgCtrl( GetDlgItem( IDC_???? ) ); // IDC_???? のコントロールにフォーカスをセット
GetDlgItem( IDC_???? )->SetFocus( ); // IDC_???? のコントロールにフォーカスをセットする別の方法
またダイアログ上であたかもTABキーが押されたかのようにすることでも次のコントロールへ移動できます。
// 処理前メッセージハンドラ
BOOL CVCalEdDlg::PreTranslateMessage(MSG* pMsg)
{
if( pMsg->message == WM_KEYDOWN ){
// RETURNキーは次のコントロールを選択する(TABキーに変換)
if( pMsg->wParam == VK_RETURN ) pMsg->wParam = VK_TAB;
}
return CDialog::PreTranslateMessage(pMsg);
}
メディアプレーヤを自作プログラムに組み込むことができるようです。
以前はOLEコントロールと呼んでいて今はActiveXコントロールとかいう技術を利用するのですが、
現在の筆者のレベルではよくわかりません。使ってるのがVC++4.0だし…。(^^;
要するにメディアプレーヤがクラスとして定義され、ダイアログなどにオブジェクトとして実装できるということらしいです。
最初にプロジェクトを作成する時にOLEサポートの選択でOLEコントロールを有効にします。
挿入->コンポーネント->OLEコントロールでWindows Media Playerを追加します。
ダイアログなどにメンバ変数 CMediaPlayer2 m_Player; を追加します。#include "MediaPlayer2.h" するのを忘れずに。
ダイアログのOnInitDialogで、
// スタティックコントロール内にメディアプレーヤを組み込む
CRect Rect;
this->GetDlgItem( IDC_STATIC_AREA )->GetWindowRect( Rect );
this->ScreenToClient( Rect );
Rect.DeflateRect( 2, 2, 3, 3 );
RECT rect = Rect;
m_Player.Create( "Media Player", WS_CHILD|WS_VISIBLE, rect, this, 1 );
ロードするファイルの指定は m_Player.SetFileName( "c:\\windows\\デスクトップ\\movie.mpg" ); で行う。
再生開始は m_Player.Play( ); で 停止は m_Player.Stop( ); らしい。
他にもオートスタートを制御する m_Player.SetAutoStart( FALSE ); とか、オートサイズ?を設定する
m_Player.SetAutoSize( TRUE ); とか パネルを出すか出さないか設定する m_Player.SetShowControls( FALSE ); とか
色んなメンバ関数があるみたいですが、何せヘルプが無いのでよくわかりません。インターネットで調べてもあまり
使っている人はいないみたい…。暇ができたらもっと詳しく調べようと思います。[つづく]
デバグ実行終了時に以下のメッセージがでることがありました。
・例外処理 (初回) は xxxx.exe にあります: 0xC0000005: アクセス違反。
原因: NULLポインタ(0番地)やシステム予約領域または自分で確保した領域以外にアクセスした。
原因: CStringのメンバ関数Formatで自分自身を参照させると発生する。
// アクセス違反が発生する例
CString Str;
this->GetWindowText( Str );
Str.Format( "%s test", Str ); // << ここでアクセス違反!
// 改良版
CString Str;
this->GetWindowText( Str );
Str += " test";
・例外処理 (初回) は xxxx.exe (KERNEL32.DLL) にあります: 0xC0000005: アクセス違反。
ここ にもあるように「(初回)」のエラーは致命的では
なさそうなのでほおっておいても良さそうですが、(^^; 気持ち悪いので調べてみました。
原因:ダイアログベースのアプリケーションが終了するときに、
フォーカスを持つコントロールが無効になっていると発生する。
・例外処理 (初回) は xxxx.exe (MSONSEXT.DLL) にあります: 0x006D007E: (unknown)。
原因:CFileDialogを開いたときに存在しないファイル名を指定したときに発生する。
・CArchive exception: endOfFile.
XXXX.exe の 0x???????? で初回の例外が発生しました : Microsoft C++ exception: CArchiveException @ 0x0012edbc。
原因:CArchive ar.ReadString() でテキスト終端に到達したときに発生する。
他の場合もあると思いますが、とりあえず今回は以上。
タブコントロールはプロパティシートについているようなタブを制御するクラスです。
ダイアログをタブによって切り換えたい場合はプロパティシートそのものである
CPropertySheetクラスを使えばCTabCtrlクラスを使用しなくても簡単に実装できますが
(参照 ),タブによってツリーコントロールのような任意のウインドウを
切り換えたい場合はCTabCtrlを実装する必要があります。
ダイアログベースのアプリケーションにタブコントロールを実装する手順は次の通りです。
メインダイアログにタブによって切り換えられる子ウインドウ(複数)をメンバ登録する。
リソースエディタでメインダイアログにタブコントロールを貼り付ける。
クラスウィザードでタブコントロールにコントロール変数(例m_Tab)を割り付ける。
メインダイアログのOnInitDialogでm_Tab.InsertItem( )によってタブを追加する。
同じくOnInitDialogでm_Tabを親として全ての子ウインドウをCreateする。
クラスウィザードでタブのメッセージTCN_SELCHANGEにハンドラOnSelchangeTabを割り付ける。
タブが切り換わるとOnSelchangeTabハンドラが実行されるのでm_Tab.GetCurSel( )で選択されている
タブを取得し,子ウインドウのどれか一つをShowWindow( SW_SHOW )し,他をShowWindow( SW_HIDE )する。
2つのタブを持ちツリーコントロールとリストコントロールを切り換えるようにした
サンプルコードはこちら 。
【2007.08.08追記】
リソースエディタでダイアログの上にタブコントロールとリストコントロールを作成してから
リストコントロールをタブコントロールの子にしたい場合は,ダイアログの OnInitDialog( ) で
this->m_ListCtrl.SetParent( (CWnd *)&(this->m_TabCtrl) );
とすればできます。動的なリストコントロールを Create( ) しなくてよいので楽チンです。
また,そうやってタブコントロールの子にしたリストコントロールをタブコントロールの
領域内にぴったりあわせるには以下のようにします。
if( this->m_ListCtrl.m_hWnd ){
CRect TabRect;
this->m_TabCtrl.GetItemRect( 0, TabRect ); // 1個のタブサイズを取得
int nRow = this->m_TabCtrl.GetRowCount(); // タブの行数を取得
int nMargin = TabRect.left;
CRect ListRect;
ListRect.left = nMargin;
ListRect.top = TabRect.Height() * nRow + nMargin * 2;
ListRect.bottom = TabCtrlRect.bottom - nMargin;
ListRect.right = TabCtrlRect.right - nMargin;
this->m_ListCtrl.MoveWindow( ListRect );
}
上記コードを関数化して,親ダイアログの OnInitDialog( ) と OnSize( ) で呼べばOK!
ツリーコントロールとはエクスプローラの左側部分そのものです。階層的にアイテムを管理することができます。
使い方はメニューコントロールとリストビューコントロールのあいのこのようで、アイテムの追加には
InsertItem( )を使用し、アイテムが選択された時のハンドラは TVN_SELCHANGED を実装します。
具体的なサンプルは
これ
を見てください。
エクスプローラの左側のようにサブフォルダツリーを作成する関数を作ってみました。
上記サンプルでは一度に最下層サブフォルダまで検索してツリー表示するので
実際に使用してみるとストレスを感じることがありました。
そこでツリーを展開する時は直下のサブフォルダしか検索しないようにして動作を軽くしたのが
これ
です。ついでなので CTreeCtrl クラスの派生クラスとして作成してみました。
使用方法はソースを見てください。
なお、ツリーコントロールからフォーカスが外れると選択アイテムが分からなくなってしまうのが
いやな場合は、以下のようにするとフォーカスが外れても選択されたままになります。
m_TreeCtrl.ModifyStyle( NULL, TVS_SHOWSELALWAYS );
【ツリーコントロールの追加TIPS】
★項目の削除
項目を削除するには,現在選択されている項目を GetSelectedItem( ); で調査して,
DeleteItem( ); で削除します。
以下にDELETEキーが押された際に項目を削除するコードを記載します。
OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
{
if( nChar == VK_DELETE ){
HTREEITEM hItem = this->GetSelectedItem();
if( hItem ){
this->DeleteItem( hItem );
}
}
CTreeCtrl::OnKeyDown(nChar, nRepCnt, nFlags);
}
★項目ラベルの編集
項目ラベルを編集可能にするにはツリーコントロールのスタイルに TVS_EDITLABELS 属性をセットします。
変更イベントを受けるには,TVN_ENDLABELEDIT(WM_NOTIFY)メッセージハンドラを実装し,
そこで this->SetItem( &pTVDispInfo->item ); します。
★アイコンの表示
ツリーコントロールにアイコンを表示させるには,リソースエディタでアイコンを作成しイメージリストにセットして,
それをツリーコントロールに登録するという流れになります。
イメージリストはツリーコントロールにメンバ変数として以下のように追加します。
CImageList m_ImageList;
次に,イメージリストを初期化します。ツリーコントロールに OnCreate() を実装し,以下のコードを追加します。
// アイコンイメージリストの初期化
m_ImageList.Create( 16, 16, ILC_COLOR8, 2, 0 );
m_ImageList.Add( AfxGetApp()->LoadIcon( アイコン0のリソースID ) );
m_ImageList.Add( AfxGetApp()->LoadIcon( アイコン1のリソースID ) );
// ツリーコントロールにアイコンイメージを登録
this->SetImageList( &m_ImageList, TVSIL_NORMAL );
そして,InsertItem( "ITEM", 0, 1 ); のように非選択状態と選択状態でのそれぞれのイメージ番号を
イメージリストのインデックスで指定すればアイコン付きのツリーコントロールが実装できます。
★コンテキストメニュー
ツリーコントロールでコンテキストメニューを表示させるにはWM_RBUTTONDOWNメッセージハンドラを
以下のように実装します。
// 右クリックハンドラ
void CxxxxTree::OnRButtonDown(UINT nFlags, CPoint point)
{
// 該当座標に項目があれば選択してポップアップメニューを表示する
HTREEITEM hItem = this->HitTest( point );
if( hItem ){
this->SelectItem( hItem );
CMenu XxxxxMenu;
XxxxxMenu.LoadMenu( IDR_MENU_PRJ );
CMenu *pPopup = XxxxxMenu.GetSubMenu( 0 );
this->ClientToScreen( &point );
pPopup->TrackPopupMenu( TPM_LEFTALIGN|TPM_RIGHTBUTTON, point.x, point.y, this );
}
CTreeCtrl::OnRButtonDown(nFlags, point);
}
【2006.11.18追記】
CTreeCtrlクラスでドラッグ&ドロップを実装するのは難しそうですが,そんなには難しくありません。
ドラッグ開始ハンドラ OnTvnBegindrag( ),マウスカーソル移動ハンドラ OnMouseMove( ),
マウスボタン離上ハンドラ OnLButtonUp( ),破棄ハンドラ OnDestroy( ) の4つの関数を実装するだけで実現できます。
メンバ
BOOL m_bDragging;
HTREEITEM m_hItemDrag;
HTREEITEM m_hItemDrop;
CImageList *m_pDragImage;
BOOL IsChildNodeOf( HTREEITEM hItemChild, HTREEITEM hItemSuspectedParent );
OnCreate()
// ドラッグ&ドロップ情報の初期化
this->m_bDragging = FALSE;
this->m_hItemDrag = NULL;
this->m_hItemDrop = NULL;
this->m_pDragImage = NULL;
OnDestroy()
CImageList *pImageList;
pImageList = GetImageList(TVSIL_NORMAL);
pImageList->DeleteImageList();
delete pImageList;
OnTvnBegindrag()
LPNMTREEVIEW pNMTreeView = reinterpret_cast(pNMHDR);
// ドラッグ開始項目の取得
HTREEITEM hDragItem = this->HitTest( pNMTreeView->ptDrag );
if( hDragItem ){
this->m_bDragging = TRUE;
this->m_hItemDrag = hDragItem;
this->m_hItemDrop = NULL;
CPoint ptAction;
GetCursorPos( &ptAction );
ScreenToClient( &ptAction );
this->m_pDragImage = this->CreateDragImage( hDragItem );
this->m_pDragImage->DragShowNolock( TRUE );
this->m_pDragImage->SetDragCursorImage( 0, CPoint(0, 0) );
this->m_pDragImage->BeginDrag( 0, CPoint(0,0) );
this->m_pDragImage->DragMove( ptAction );
this->m_pDragImage->DragEnter( this, ptAction );
this->SetCapture();
}
OnMouseMove()
if( this->m_bDragging ){
if( this->m_pDragImage ){
this->m_pDragImage->DragMove( point );
HTREEITEM hItem;
hItem = this->HitTest( point );
if( hItem ){
this->m_pDragImage->DragLeave( this );
// ドロップ候補を保存
this->SelectDropTarget( hItem );
this->m_hItemDrop = hItem;
this->m_pDragImage->DragEnter( this, point );
}
}
}
OnLButtonUp()
if( this->m_bDragging ){
if( this->m_pDragImage ){
this->m_pDragImage->DragLeave( this );
this->m_pDragImage->EndDrag();
delete this->m_pDragImage;
this->m_pDragImage = NULL;
if( this->m_hItemDrag != this->m_hItemDrop
&& !this->IsChildNodeOf( this->m_hItemDrop, this->m_hItemDrag )
&& this->GetParentItem( this->m_hItemDrag ) != this->m_hItemDrop ){
// this->m_hItemDrag がドラッグ元アイテム
// this->m_hItemDrop がドロップ先アイテム
}
}
::ReleaseCapture();
this->m_bDragging = FALSE;
this->m_hItemDrag = NULL;
this->m_hItemDrop = NULL;
this->SelectDropTarget( NULL );
}
// 子項目か調査する
BOOL CxxxxTree::IsChildNodeOf( HTREEITEM hItemChild, HTREEITEM hItemSuspectedParent )
{
do {
if( hItemChild == hItemSuspectedParent ){
break;
}
}
while( (hItemChild = this->GetParentItem( hItemChild ) ) != NULL );
return ( hItemChild != NULL );
}
VC++.NET2003ではCTreeCtrlクラスのヘルプでクラスの概要のページに「MFC サンプル CMNCTRL1」として
サンプルコードを取得することができました。
◆ CListCtrl リストビューコントロールの実装
リストビューとはエクスプローラの右側部分そのものです。アイコンの一覧表示(アイコンビュー)や
詳細の列表示(レポートビュー)といった形態を持つことができます。
レポートビューは自作アプリケーションでも便利に使えるので、今回はリストビューコントロールの
レポートビュー形式での実装方法を解説します。
まずリソースエディタでリストビューコントロールを作成し、スタイルの表示をReportにしておきます。
これにメンバ変数をコントロールで割り付けます。ここでは m_ListCtrl という変数名とします。
次に、エクスプローラを詳細表示にすると右側に「名前」「サイズ」「ファイルの種類」などの列(カラム)が
ありますが、これをヘッダを指定して追加します。
// カラム作成
LV_COLUMN lvclm;
lvclm.mask = LVCF_TEXT|LVCF_WIDTH;
lvclm.pszText = "名前"; // ヘッダ
lvclm.cx = 80; // カラムのドット幅
m_ListCtrl.InsertColumn( 0, &lvclm ); // 追加
lvclm.pszText = "サイズ";
lvclm.cx = 80;
m_ListCtrl.InsertColumn( 1, &lvclm );
lvclm.pszText = "ファイルの種類";
lvclm.cx = 150;
m_ListCtrl.InsertColumn( 2, &lvclm );
そして実際の項目(アイテム)を登録します。
int i = 0;
m_ListCtrl.InsertItem( i, "NAME" );
m_ListCtrl.SetItemText( i, 1, "SIZE" );
m_ListCtrl.SetItemText( i, 2, "TYPE" );
// 列幅調整
// m_ListCtrl.SetColumnWidth( 0, LVSCW_AUTOSIZE );
// m_ListCtrl.SetColumnWidth( 1, LVSCW_AUTOSIZE );
// m_ListCtrl.SetColumnWidth( 2, LVSCW_AUTOSIZE );
上記例では i=0 として1個のアイテムしか登録しませんでしたが、
実際はループにして登録数だけi++していく必要があります。
リストからそれぞれの項目を取得するには以下のように行います。
CString Name[100], Size[100], Type[100];
int nListItem = m_ListCtrl.GetItemCount( ); // リスト登録数の取得
if( nListItem ){
for( int i=0; i
さらに、マウスでクリックした時にどのアイテムがクリックされたかを取得するには
CListCtrl の NM_CLICK ハンドラを実装します。
// リストビュークリックハンドラ (CListCtrlのNM_CLICKハンドラ)
void C????::OnClickList( NMHDR *pNMHDR, LRESULT *pResult )
{
NM_LISTVIEW *pNMListView = (NM_LISTVIEW *)pNMHDR;
// pNMListView->iItem にアイテム番号が入ります
// 先頭列をクリックされた時のみ反応させます(エクスプローラと同じ)
if( pNMListView->iSubItem == 0 ){
CString name = m_ListCtrl.GetItemText( pNMListView->iItem, 0 );
CString size = m_ListCtrl.GetItemText( pNMListView->iItem, 1 );
CString type = m_ListCtrl.GetItemText( pNMListView->iItem, 2 );
}
*pResult = 0;
}
選択されているアイテムを調査するにはこうします。
int nSelected = m_ListCtrl.GetNextItem( -1, LVNI_SELECTED );
if( nSelected < 0 ) // 選択されていない
さらにアイテムを選択状態にするにはこうします。
int i = 0; // アイテム番号
m_ListCtrl.SetFocus( );
m_ListCtrl.SetItemState( i, LVIS_SELECTED, LVIS_SELECTED );
// リストコントロール自身にフォーカスをセットしてからでないとダメ
同様にアイテムの属性を変えるにはこうします。
// 選択の解除
m_ListCtrl.SetItemState( i, (UINT)~LVIS_SELECTED, LVIS_SELECTED );
// フォーカスのセットと解除
m_ListCtrl.SetItemState( i, LVIS_FOCUSED, LVIS_FOCUSED );
m_ListCtrl.SetItemState( i, (UINT)~LVIS_FOCUSED, LVIS_FOCUSED );
ラベルを編集可能にするにはリソースエディタでスタイルのラベルの編集をチェックしておき
クラスウィザードでLVN_ENDLABELEDITメッセージのハンドラを実装し以下のように変更する。
void CEGZcDlg::OnEndlabeleditListviewDstdir(NMHDR* pNMHDR, LRESULT* pResult)
{
LV_DISPINFO* pDispInfo = (LV_DISPINFO*)pNMHDR;
// TRUEを返して変更を有効にする
*pResult = TRUE;
}
基本は以上です。各アイテムにはビットマップイメージなども添えられるようなので
後はヘルプを見るなり頑張ってください。
なお、リストコントロールからフォーカスが外れると選択アイテムが分からなくなってしまうのが
いやな場合は、以下のようにするとフォーカスが外れても選択されたままになります。
m_ListCtrl.ModifyStyle( NULL, LVS_SHOWSELALWAYS );
【2007.08.09追記】
アイテム数(行数)ではなく,カラム数(列数)を知りたい場合は次のようにすればOK!
int nColumn = this->m_ListCtrl.GetHeaderCtrl()->GetItemCount();
【2007.08.27追記】
リストコントロールのカラムを右配置にする方法
// 右寄せコラム設定
LVCOLUMN lvClm;
lvClm.mask = LVCF_FMT;
lvClm.fmt = LVCFMT_RIGHT;
this->m_ListCtrl.SetColumn( 1, &lvClm );
ただし,一番左のカラムは配置を変更できないようです。その場合はダミーカラムを入れておくことで回避します。
【2007.09.08追記】
選択時に一行全てを反転させたりグリッド線を引いたりするには拡張スタイルの設定を行います。
// 拡張スタイルの設定(選択時に1行全てを反転させる,グリッド線を引く)
this->m_ListCtrl.SetExtendedStyle( (LVS_EX_FULLROWSELECT|LVS_EX_GRIDLINES) );
【2008.12.04追記】 リストビューコントロールのフォントを変更する方法
親ダイアログなどで,CFont m_ListCtrlFont; などとしてメンバ変数を確保しておき,OnInitDialog( )で以下のようにすると,
フォントとともに,各行の高さも自動的に変更されます。
// リストコントロールのフォント設定
this->m_ListCtrlFont.CreatePointFont( 140, "Arial" );
this->m_ListCtrl.SetFont( &this->m_ListCtrlFont );
【2008.12.04追記】 リストビューコントロールのカスタムドロー
リストビューコントロールで,各行あるいは各列ごとに,テキストの色や背景の色,あるいはフォントそのもの,
さらには図形を描きたい場合は,カスタムドローという方法を使います。
すなわち,リストビューコントロールのプロパティで NM_CUSTOMDRAW ハンドラを追加して,以下のように処理を実装します。
void CtestDlg::OnNMCustomdrawList(NMHDR *pNMHDR, LRESULT *pResult)
{
// LPNMCUSTOMDRAW pNMCD = reinterpret_cast(pNMHDR);
// TODO : ここにコントロール通知ハンドラ コードを追加します。
LPNMLVCUSTOMDRAW pNMLVCD = (LPNMLVCUSTOMDRAW)pNMHDR;
switch( pNMLVCD->nmcd.dwDrawStage ){
case CDDS_PREPAINT:
*pResult = CDRF_NOTIFYSUBITEMDRAW;
break;
case CDDS_ITEMPREPAINT:
*pResult = CDRF_NOTIFYSUBITEMDRAW;
break;
case CDDS_ITEMPREPAINT|CDDS_SUBITEM:
{
// 行と列の取得
DWORD_PTR iRow = pNMLVCD->nmcd.dwItemSpec;
int iCol = pNMLVCD->iSubItem;
// 以下,行 iRow と 列 iCol に応じて,さまざまな処理を実装します。
// ★テキストおよび背景の色を変える場合
if( iRow == xxxx && iCol == xxxx ){
pNMLVCD->clrTextBk = RGB(0,0,0);
pNMLVCD->clrText = RGB(255,255,255);
*pResult = CDRF_NEWFONT;
}
// ★フォントを変える場合
else if( iRow == xxxx && iCol == xxxx ){
SelectObject( pNMLVCD->nmcd.hdc, this->m_ListCtrlFont2.m_hObject );
*pResult = CDRF_NEWFONT;
}
// ★図形を描く場合
else if( iRow == xxxx && iCol == xxxx ){
// 選択されているときの背景色
DWORD dwColorBk;
if( pNMLVCD->nmcd.uItemState & CDIS_SELECTED ){
dwColorBk = RGB(0,0,192);
}
// 選択されていないときの背景色
else {
dwColorBk = RGB(0,0,0);
}
// 背景領域の取得
CRect Rect;
this->m_ListCtrl.GetSubItemRect( (int)iRow, iCol, LVIR_BOUNDS, Rect );
// 描画準備
CDC CD;
CD.Attach( pNMLVCD->nmcd.hdc );
int iSaveDC = CD.SaveDC();
// 背景塗りつぶし
CD.FillSolidRect( &Rect, dwColorBk );
// ダミー図形
Rect.top = Rect.top + Rect.Height() / 2;
Rect.bottom = Rect.top+2;
CD.FillSolidRect( &Rect, RGB(0,255,0) );
// 描画終了
CD.RestoreDC( iSaveDC );
CD.Detach();
// デフォルト描画スキップ
*pResult = CDRF_SKIPDEFAULT;
}
else {
*pResult = CDRF_DODEFAULT;
}
}
break;
default:
*pResult = CDRF_DODEFAULT;
break;
}
// *pResult = 0;
}
時間のかかる処理などでよく進行状況を表示するダイアログが開いたりしますが、
あの棒グラフみたいのなのがプログレスバーです。今回はこれを実装してみます。
リソースビューの Dialog のところで右クリックし "dialogの挿入" を選択します。
作成されたダイアログのプロパティの一般で ID を IDD_PROGRESS_DIALOG に、
キャプションは "Now Loading..." とでもしておきます。
さらにプロパティのスタイルでシステムメニューのチェックボックスをクリアしてください。
もしタイトルバーがいらなければタイトルバーのチェックボックスをクリアしてください。
またその他のスタイルで、可視と無効をチェックしてください。
ダイアログ上のOKボタンとキャンセルボタンを削除します。
さらにスタティックテキストを作成し、後からこれを変更できるようにプロパティで
IDを IDC_LABEL に、またキャプションを "Now Loading..." にでもしておきます。
プログレスバーコントロールをダイアログに置いて、全体のレイアウトを整えてください。
プログレスバーのIDは IDC_PROGRESS とでもしておきます。
クラスウィザードを起動します。
クラスの追加 "IDD_PROGRESS_DIALOGは新規リソースです…" と聞いてくるので
新規クラスの作成を選択します。
新規クラスの作成では、クラス名を CProgressDialog にして後はそのままで作成ボタンを押します。
クラスウィザードのメンバ変数で IDC_LABEL に m_Label というカテゴリが値の CString 変数と、
IDC_PROGRESS に m_ProgressBar というカテゴリがコントロールの CProgressCtrl 変数を割り当てます。
後は実際にこのプログレスバーのついたダイアログを使用するだけです。
使用するプログラムに #include "ProgressDialog.h" しておき
プログレスバーを出したい部分で以下のようなコーディングをします。
プログレスバーダイアログを開くときはモーダルの DoModal( ) ではなく
モードレスの Create( ) で行います。モーダルだとそこから先に処理が行かないからです。
// 親ウインドウを無効にする
EnableWindow( FALSE );
// プログレスバーダイアログの作成
CProgressDialog pd;
pd.m_Label = "プログレスバーをテスト中...";
pd.Create( IDD_PROGRESS_DIALOG );
pd.ShowWindow( SW_SHOW ); // 2007.08.27追記(VC7)
pd.m_ProgressBar.SetRange( 0, 10 );
pd.EnableWindow( FALSE );
// プログレスバーを更新する
Sleep( 1000 ); pd.m_ProgressBar.SetPos( 1 );
Sleep( 1000 ); pd.m_ProgressBar.SetPos( 2 );
(略)
Sleep( 1000 ); pd.m_ProgressBar.SetPos( 9 );
Sleep( 1000 ); pd.m_ProgressBar.SetPos( 10 );
// プログレスダイアログを消去する
pd.DestroyWindow( );
// 親ウインドウを有効にする
EnableWindow( TRUE );
実際は、プログレスバーダイアログの作成部分と、プログレスバーの更新部分は別々の関数のはずで
長い処理に入る前にダイアログを作成し、長い処理の中からコールバック関数の形でプログレスバーの
更新を行う形になります。
上記で m_Label を動作に応じて変更したい場合は,最初に CStatic にしておいて,
随時 m_Label.SetWindowText( ) すればいいでしょう。
リソースビューを見ているとVS_VERSION_INFOとかいうVersion管理情報があったりして、
さもここでバージョンを管理してくださいってなっているのですが、同じリソースビューにある
Aboutダイアログにはスタティックテキストでバージョンが書かれていたりして、
ここまでするなら自動的に連動してくれよなんて思うのですが、自分でやらなければ
ならないようです。
バージョンリソースにアクセスするには
GetFileVersionInfoSize( )
GetFileVersionInfo( )
VerQueryValue( )
を組み合わせて使えばいいことがわかりました。
これを元にバージョンを取得するクラスなんかを作成すれば便利なのですが、
これが結構面倒くさいのです。
VC++の解説サイトを見てまわっていると
おパパさん
という方が作られた大変すばらしい
バージョンリソースクラスのサンプル
を見つけてしまいました。マクロの使い方が凄いです。
でもマクロでメンバ関数が記述されているがゆえにデバグの時ちょっと不便だったのと
何をやっているか自分なりにかみくだいてコーディングしてみたかったので
書き換えたクラスが これ です。
ほとんどパクリです。スイマセン(^^;;;
タスクトレイにアイコンを表示し、そのアイコンからのイベントを受け取るには以下のようにします。
・ダイアログまたはメインフレームの定義追加
#define MY_NOTIFYICON (WM_APP+100) // 通知メッセージ
・ダイアログまたはメインフレームのメンバ変数として追加
NOTIFYICONDATA nIcon;
・ダイアログなら OnInitDialog( ) で、メインフレームの場合は OnCreate( ) に
次のコードを追加
// タスクトレイアイコンの初期化
nIcon.cbSize = sizeof( NOTIFYICONDATA );
nIcon.uID = 1;
nIcon.hWnd = this->m_hWnd;
nIcon.uFlags = NIF_MESSAGE|NIF_ICON|NIF_TIP;
nIcon.hIcon = AfxGetApp( )->LoadIcon( IDR_MAINFRAME );
nIcon.uCallbackMessage = MY_NOTIFYICON;
// タスクトレイにマウスポインタを置いたときに表示される文字列
lstrcpy( nIcon.szTip, "TEST" );
・タスクトレイにアイコンを表示する
Shell_NotifyIcon( NIM_ADD, &nIcon );
・終了時にアイコンを消去するために DestroyWindow( ) を
クラスウィザードでオーバライドする。
void C????::DestroyWindow( )
{
Shell_NotifyIcon( NIM_DELETE, &nIcon );
return CDialog::DestroyWindow( );
}
・アイコンからのイベントを受け取るためにクラスウィザードで
WindowProc( ) をハンドルし以下のように実装する。
LRESULT C????::WindowProc(UINT message, WPARAM wParam, LPARAM lParam)
{
if( message == MY_NOTIFYICON ){
if( lParam == WM_LBUTTONDBLCLK ){
// アイコンが左ダブルクリックされたときの処理
}
}
return CDialog::WindowProc(message, wParam, lParam);
}
またタスクトレイにアイコンを表示している間、メインウインドウを隠す場合は以下のように
するよ良いでしょう。
// メインウインドウを隠す
ShowWindow( SW_HIDE );
// メインウインドウを復帰させる
ShowWindow( SW_RESTORE );
SetForegroundWindow98( this->m_hWnd );
SetForegroundWindow98( ) についてはここ を参照してください。
タスクトレイアイコンをクラス化してみました。これ です。
キーが押されたり離されたりするイベントを受けるには、クラスウィザードで WM_KEYDOWN および
WM_KEYUP メッセージを実装します。メッセージハンドラは OnKeyDown( ) および OnKeyUp( ) で、
何のキーが押されたか引数 nChar でわかるようになっています。
アルファベットのキーであれば'A'などの値と比較することで何のキーが押されたか判定できます。
矢印キーなど特殊なものは仮想キーコードとして扱われ、その内容は msdev\include\winuser.h に
VK_???? と定義されているので確認してみてください。以下にサンプルを示します。
// キー離上ハンドラ
void C????Dlg::OnKeyUp(UINT nChar, UINT nRepCnt, UINT nFlags)
{
switch( nChar ){
case VK_PRIOR:
// PageUp
break;
case VK_NEXT:
// PageDown
break;
case VK_HOME:
// Home
break;
case VK_END:
// End
break;
case VK_F5:
// F5
break;
case VK_LEFT:
// ←
break;
case VK_RIGHT:
// →
break;
case VK_UP:
// ↑
break;
case VK_DOWN:
// ↓
break;
case 'C':
if( GetKeyState( VK_CONTROL ) & 0x8000 ){
// Ctrl + C
}
break;
}
}
なおダイアログでボタンなどが実装されている場合、ダイアログ自身にはキーイベントが届きません。
OnKeyDown( ) および OnKeyUp( ) を実装しても実行されないのです。
ダイアログ自身でキーを受けたい場合は、その上に載っているコントロールを全て無効にするか、
以下のようにメッセージハンドラ PreTranslateMessage( ) を実装してメッセージが処理される前に
直接制御を行います。
BOOL C????Dlg::PreTranslateMessage(MSG* pMsg)
{
// F5キーの離上に反応する
if( pMsg->message == WM_KEYUP ){
if( pMsg->wParam == VK_F5 ){
// 最新の状態に更新など…
return( TRUE );
}
}
// ESCキーの押下を無効にする
if( pMsg->message == WM_KEYDOWN ){
if( pMsg->wParam == VK_ESCAPE ) return( TRUE );
}
return CDialog::PreTranslateMessage(pMsg);
}
PreTranslateMessage( )内部でメッセージの処理を行った際は、必ず TRUE で戻ることを
忘れないようにしてください。動作がおかしくなる場合があります。
なお,CTRLキーやSHIFTキーおよびALTキーなどの特殊なキーについては,GetKeyState( ),
OnSyskeyDown / OnSyskeyUp, WM_SYSKEYDOWN / WM_SYSKEYUP あたりを調査してみてください。
デスクトップの大きさは以下のようにして取得できます。
CRect DesktopRect;
GetDesktopWindow( )->GetClientRect( DesktopRect );
ただしこれはタスクバーサイズも含まれています。タスクバーの含まれないデスクトップサイズを
取得するにはこうします。
RECT rect;
SystemParametersInfo( SPI_GETWORKAREA, 0, &rect, 0 );
以上。
ウインドウの位置と大きさを取得したり設定するには以下のようにします。
// ウインドウの位置と大きさを取得する
CRect rect;
GetWindowRect( rect );
// ウインドウのクライアント領域(描画領域)の大きさを取得する
GetClientRect( rect );
// ウインドウを親ウインドウの中央に配置する。
CenterWindow( );
// ※引数として基準にしたいウインドウへのポインタを指定できる。
// デスクトップを基準にしたい場合は GetDesktopWindow( ) の戻値を使用する。
// ウインドウの位置のみを変更する
SetWindowPos( &wndTop, 100, 100, 0, 0, SWP_NOSIZE );
// ウインドウの位置と大きさを変更する
SetWindowPos( &wndTop, 100, 100, 640, 480, SWP_DRAWFRAME|SWP_SHOWWINDOW );
// ウインドウを常に手前に表示する
SetWindowPos( &wndTopMost, 0, 0, 0, 0, SWP_NOMOVE|SWP_NOSIZE );
// 常に手前に表示するのを解除する
SetWindowPos( &wndNoTopMost, 0, 0, 0, 0, SWP_NOMOVE|SWP_NOSIZE );
// ウインドウを最大化する
ShowWindow( SW_SHOWMAXIMIZED );
// ※リソースエディタでダイアログのプロパティのシステムメニューと
// 最大化を有効にしておく
// ウインドウを最小化(アイコン化)する
ShowWindow( SW_MINIMIZE );
// ※リソースエディタでダイアログのプロパティのシステムメニューと
// 最小化を有効にしておく
// ウインドウが最大化されているか確認する
BOOL bRet = IsZoomed( );
// ウインドウが最小化されているか確認する
BOOL bRet = IsIconic( );
// ウインドウを非表示にする
ShowWindow( SW_HIDE );
// ウインドウを最大化・最小化・非表示から元に戻す
ShowWindow( SW_RESTORE );
テキストをクリップボードにコピーする方法
// テキストをクリップボードにコピー
char *pText = "copy_text";
HGLOBAL hGlobal = GlobalAlloc( GHND, strlen( pText ) + 1 );
char *pBuf = (char *)GlobalLock( hGlobal );
strcpy( pBuf, pText );
GlobalUnlock( hGlobal );
if( OpenClipboard( ) == TRUE ){
EmptyClipboard( );
SetClipboardData( CF_TEXT, hGlobal );
CloseClipboard( );
}
// [注意] クリップボードにコピーしたメモリは
// OSの管理下になるので解放してはいけない
クリップボードからテキストを取得する方法
// クリップボードからテキストを取得
char szText[256] = { 0 };
if( OpenClipboard( ) == TRUE ){
HGLOBAL hGlobal = GetClipboardData( CF_TEXT );
CloseClipboard( );
if( hGlobal ){
char *pBuf = (char *)GlobalLock( hGlobal );
if( pBuf ){
strcpy( szText, pBuf );
GlobalUnlock( hGlobal );
}
}
}
// [注意] 取得を受けるバッファサイズが足りていることを確認すること
ビットマップをクリップボードへコピーする方法
// ピクチャーコントロールに描画されたビットマップを
// クリップボードへコピー
CWnd *pWnd = GetDlgItem( IDC_PICTURECTRL );
CRect Rect;
pWnd->GetClientRect( Rect );
CClientDC DCsrc( pWnd );
CDC DCdst;
DCdst.CreateCompatibleDC( &DCsrc );
CBitmap Bitmap;
Bitmap.CreateCompatibleBitmap( &DCsrc, Rect.Width( ), Rect.Height( ) );
DCdst.SelectObject( &Bitmap );
DCdst.BitBlt( 0, 0, Rect.Width( ), Rect.Height( ), &DCsrc, 0, 0, SRCCOPY );
if( OpenClipboard( ) == TRUE ){
EmptyClipboard( );
SetClipboardData( CF_BITMAP, (HBITMAP)Bitmap );
CloseClipboard( );
}
// [注意] 上の例ではDCsrcとDCdstのデストラクタでデバイスコンテキストの
// 解放が行われるのでわざわざ DCdst.DeleteDC( ) および
// DCsrc.DeleteDC( ) しなくてもよい
// またSelectObject( )で戻るオブジェクトを復帰させるとうまく動作しない
クリップボードからビットマップを取得して描画する方法
// クリップボードからビットマップを取得して描画
if( OpenClipboard( ) == TRUE ){
HBITMAP hBitmap = (HBITMAP)GetClipboardData( CF_BITMAP );
CloseClipboard( );
if( hBitmap ){
BITMAP bm;
if( GetObject( hBitmap, sizeof( BITMAP ), &bm ) ){
// ピクチャーコントロールに描画
CWnd *pWnd = GetDlgItem( IDC_PICTURECTRL );
CClientDC DCdst( pWnd );
CDC DCsrc;
DCsrc.CreateCompatibleDC( &DCdst );
HBITMAP hOld = (HBITMAP)SelectObject( DCsrc.GetSafeHdc( ), hBitmap );
// 等倍で描画する場合
DCdst.BitBlt( 0, 0, bm.bmWidth, bm.bmHeight, &DCsrc, 0, 0, SRCCOPY );
// 領域内に収めて描画する場合
CRect Rect;
pWnd->GetClientRect( Rect );
DCdst.StretchBlt( 0, 0, Rect.Width( ), Rect.Height( ), &DCsrc, 0, 0,
bm.bmWidth, bm.bmHeight, SRCCOPY );
SelectObject( DCsrc.GetSafeHdc( ), hOld );
}
}
}
// [注意] 上の例ではDCdstとDCsrcのデストラクタでデバイスコンテキストの
// 解放が行われるのでわざわざ DCdst.DeleteDC( ) および
// DCsrc.DeleteDC( ) しなくてもよい
◆ リリースモードDLLとでバグモードDLLで発生する問題
Win95+VC4.0の環境では気づかなかったのですが、NT+VC4.0ではデバッグ実行した後に
HEAP[アプリ名]: Invalid Address specified to RtlFreeHeap( 130000, ef0568 )
と表示されることがありました。googleで調べてみるとメモリを確保するようなDLL(例えばSUSIEプラグイン)
を使用し、そのメモリの解放を本体で行う場合に発生することがわかりました。
参考 http://homepage2.nifty.com/tulip-an/soft/scrwrpprblm.html
要するにデバグモードではメモリの確保と解放の際、本来の領域に加えてデバグ用の領域も確保・
解放を行いますが、リリースモードでコンパイルされたDLLでメモリを確保し、デバグモードで
コンパイルされた本体でそのメモリを解放しようとすると、確保されていないデバグ用領域を
解放しようとするために発生するそうです。
解決方法は無いようなのでこのエラーメッセージは無視する以外ありませんね…。
ダイアログ一つで済むアプリケーションならともかく、比較的規模が大きくなると
アプリケーション全体からアクセスする必要のある設定値などの変数をどこに確保したら良いかが
問題となってきます。これらは複数のオブジェクトのメンバ変数としてバラバラに確保されたり
すると管理も大変になるので構造体などで一元管理しておきたいものです。
でも複数のオブジェクトからそれぞれどうやって一元管理したデータにアクセスすれば
いいでしょうか?
アプリケーションを作成した場合、アプリケーション本体のオブジェクトとして
theAppが作成されます。とにかくこれが最初のオブジェクトで、ダイアログベースのアプリでは
このオブジェクトがダイアログを生成し、DoModal( )します。
したがってアプリケーション全体からアクセスされる設定値やワーク変数は、アプリケーション本体
であるこのtheAppオブジェクトが持つべきです。すなわちtheAppオブジェクトのメンバとして
ワーク構造体を持たせるといいでしょう。この構造体は他のオブジェクトから当然アクセスするので
パブリックメンバにしておきます。
次に他のオブジェクトからtheAppのメンバ変数である構造体にアクセスするにはtheAppのアドレスを
取得する必要があります。これは関数AfxGetApp( )で可能です。ただしこの関数はtheAppの
基本クラスであるCWinAppクラス型のポインタを返すので、そのままでは派生クラスオブジェクトで
あるtheAppで追加されたメンバに直接アクセスできません。したがってそのアドレスを派生クラスの
ポインタとしてキャストする必要があります。つまり ((CTestApp*)AfxGetApp( ))->member; とする
ことによりメンバ変数にアクセス可能となります※。
※ただしここではアプリケーションクラス名をCTestAppとしています。
後はこのメンバ変数の構造体の内容をレジストリなりINIファイルから読み込んだり書き出したり
好きにしてください(笑)。
リソースエディタでダイアログにスクロールバーを追加できますが、その制御はコントロール
変数を割り当てて行うのではなく、ダイアログ自身の(正確には親クラスの)メンバ関数で行います。
詳細はヘルプの「CWnd スクロール関数」を確認してください。
下記にスクロールバーの設定例を示します。
SCROLLINFO ScrollInfo;
ScrollInfo.cbSize = sizeof( SCROLLINFO );
ScrollInfo.fMask = SIF_ALL|SIF_DISABLENOSCROLL;
ScrollInfo.nMin = min; // 最小値
ScrollInfo.nMax = max; // 最大値
ScrollInfo.nPage = page; // スクロールバーのサイズ
ScrollInfo.nPos = 0; // スクロールバーの位置
ScrollInfo.nTrackPos = NULL; // お約束
// 水平スクロールバーへの設定の場合
this->SetScrollInfo( SB_HORZ, &ScrollInfo );
// 垂直スクロールバーへの設定の場合
this->SetScrollInfo( SB_VERT, &ScrollInfo );
// 両方のスクロールバーへの設定の場合
this->SetScrollInfo( SB_BOTH, &ScrollInfo );
スクロールバーが無効になる条件の場合、デフォルトでは非表示になりますが上記のように
SIF_DISABLENOSCROLLオプションを付けておくと非表示の代わりに淡色化されます。
またスクロールバーが操作された時のハンドラはクラスウィザードのメッセージマップで
WM_HSCROOLおよびWM_VSCROOLコマンドを実装することで可能です。ただスクロールバーは
自動的に位置を変えて再描画してはくれず、同ハンドラの引数でどの様に動かされたかを
判定し、自分でスクロールバーの位置を変更しなければなりません。
ここ にサンプルを示します。
コンパイル時に、
「fatal error C1010: プリコンパイル済みヘッダーの検索中に予期しない EOF を検出しました。」
と表示されエラーになる場合は、そのソースファイルの先頭で
#include "stdafx.h" する必要があります。
メニューバーやコンテキストメニュー(ポップアップメニュー)を動的に変更する方法を解説します。
クラスウィザードでは実現できないので、マニュアルでソースを変更しないとできません。
まず、メニューを実装するオブジェクトのヘッダファイルを変更します。
以下に示す★マークが追加が必要な部分です。なお下のコードは例なので必ずしも一致するとは
限りません。
// インプリメンテーション
protected:
HICON m_hIcon;
// 生成されたメッセージ マップ関数
//{{AFX_MSG(CMenutestDlg)
virtual BOOL OnInitDialog( );
afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
afx_msg void OnPaint( );
afx_msg HCURSOR OnQueryDragIcon( );
afx_msg void OnContextMenu(CWnd* pWnd, CPoint point);
★ afx_msg void OnInitMenu(CMenu* pMenu); // メニュー初期化ハンドラ追加
//}}AFX_MSG
★ afx_msg void OnExecMenu( UINT uID ); // メニュー項目実行ハンドラ追加
DECLARE_MESSAGE_MAP( )
メニュー項目実行ハンドラの追加位置が、//{{AFX_MSG 〜 //}}AFX_MSG の外側に書くのは、
このハンドラ一つで動的に追加された複数のメニュー項目からのメッセージコマンドを受け付ける
ことによるからです。ClassWizardは複数のメッセージコマンドを一つの関数で受け付けるのに
対応していないので、ClassWizardの編集対象外の位置に追加します。なお動的に追加するメニュー
の個数が少なく既定の場合は、個別にハンドラを作成し //{{AFX_MSG 〜 //}}AFX_MSG 内に記述する
こともできると思います。
次にメニューを実装するオブジェクトのソースファイルを変更します。
BEGIN_MESSAGE_MAP(CMenutestDlg, CDialog)
//{{AFX_MSG_MAP(CMenutestDlg)
ON_WM_SYSCOMMAND( )
ON_WM_PAINT( )
ON_WM_QUERYDRAGICON( )
ON_WM_CONTEXTMENU( )
ON_WM_INITMENU( ) // ★メニュー初期化メッセージコマンド追加
//}}AFX_MSG_MAP
ON_COMMAND_RANGE( 1000, 1099, OnExecMenu ) // ★メッセージコマンド追加
END_MESSAGE_MAP( )
上記変更によってメニューオブジェクトが初期化される際と動的に追加されたメニュー項目が
選択実行された場合に、これから用意するハンドラOnInitMenu( ), OnExecMenu( )が実行されるよう
になります。メッセージコマンド追加部分の1000, 1099というのはメニュー項目を動的に追加する
時に指定する、その項目が選択実行された場合に発行されるメッセージコマンドIDです。
なお動的に追加するメニューが少なく既定の場合で個別にハンドラを作成する時は
//{{AFX_MSG_MAP 〜 //}}AFX_MSG_MAP の内側にON_COMMANDマクロで記述します。
メニューオブジェクトが初期化される際に実行されるハンドラを追加します。
// メニュー初期化ハンドラ
void CMenutestDlg::OnInitMenu(CMenu* pMenu)
{
CDialog::OnInitMenu(pMenu);
// ここでメニューの以前の項目の削除と新しい項目の追加を行う
CString str;
pMenu->GetMenuString( 0, str, MF_BYPOSITION );
if( str == "TEST0" ){
pMenu = pMenu->GetSubMenu( 0 );
pMenu->RemoveMenu( 1, MF_BYPOSITION );
pMenu->AppendMenu( MF_STRING, 1000, "MENU APPEND" );
DrawMenuBar( );
}
else if( str == "POPUP0" ){
pMenu->AppendMenu( MF_STRING, 1010, "POPUP APPEND" );
}
}
上記の例では先頭項目が"TEST0"というメニューバーに「最近使ったファイル」のような感じで
"MENU APPEND"という項目を追加しているのと、先頭項目が"POPUP0"というコンテキストメニューに
"POPUP APPEND"という項目を追加しています。
このハンドラはメニューが開かれる度に毎回実行されるので、メニューバーのように削除されずに
残っているものは、重複して追加されないように前もって追加する位置の項目を削除してから追加
しています。
また上記例では追加した項目が選択実行された場合にメッセージコマンドIDである1000と1010が次に
実装されるハンドラに渡されます。
// メニュー項目実行ハンドラ
void CMenutestDlg::OnExecMenu( UINT uID )
{
afxDump << uID << "\n";
}
uIDの値を見て、どのメニュー項目が選択実行されたかを知るわけです。このハンドラはIDが
1000〜1099までのものを渡されるので、メニュー項目を追加する場合にそれらのIDを用いる
ようにします。なおIDの値には特に制限はないようですが、シンボルブラウザで重複しないように
定義しておくのが気持ちがいいかしれません。
最後に
これ
はお気に入りフォルダ以下を検索し階層メニューを作成するサンプルです。
複数のCStringオブジェクトを管理したい場合にCStringListオブジェクトが便利です。
自分で作ろうかと思ってたら既にありました。ぐはー。
CStringList StrList;
CString str = "TEST0";
StrList.AddHead( str );
StrList.AddHead( "TEST1" );
StrList.AddHead( "TEST2" );
StrList.AddHead( "TEST3" );
afxDump << StrList.GetCount( ) << "\n";
POSITION pos = StrList.GetHeadPosition( );
while( pos ){
str = StrList.GetNext( pos );
afxDump << "[" << pos << "]: " << str << "\n";
}
注意するべき点はリスト内部で管理されたCStringオブジェクトの位置はPOSITION型だと
いうことです。連続して読み出す場合などは上記例のwhileループ内部を参考にしてください。
詳細はCStringListのヘルプ参照のこと。
とか言ってたら配列のように管理できるクラスがありました。ズガビーン!!!!
CStringArrayがそうです。以下にサンプルを示します。
CStringArray StrArray;
CString str = "TEST0";
StrArray.Add( str ); if( StrArray.GetSize( ) > 5 ) StrArray.RemoveAt( 0 );
StrArray.Add( "TEST1" ); if( StrArray.GetSize( ) > 5 ) StrArray.RemoveAt( 0 );
StrArray.Add( "TEST2" ); if( StrArray.GetSize( ) > 5 ) StrArray.RemoveAt( 0 );
for( int i=0; i
上の例では配列要素数を最大5個に制限しています。
なんだーこっちの方が簡単じゃん!!
【2008.10.28追記】ソート可能な CStringArray
CStringArray にソート機能を追加した派生クラス CSortStringArray です。
Microsoftサポートオンラインに載っていました。
[HOWTO] MFC では、CStringArray 並べ替え方法
以下のコードはヘッダ定義形式に改造して,ついでに力技でソート順を指定できるようにしたものです。(^^;
class CSortStringArray : public CStringArray {
public:
void Sort(int dir = 0)
{
BOOL bNotDone = TRUE;
while (bNotDone)
{
bNotDone = FALSE;
for(int pos = 0;pos < GetUpperBound();pos++)
bNotDone |= CompareAndSwap(pos, dir);
}
}
private:
BOOL CompareAndSwap(int pos, int dir)
{
CString temp;
int posFirst = pos;
int posNext = pos + 1;
if( dir == 0 ){
if (GetAt(posFirst).CompareNoCase(GetAt(posNext)) > 0){
temp = GetAt(posFirst);
SetAt(posFirst, GetAt(posNext));
SetAt(posNext, temp);
return TRUE;
}
}
else {
if (GetAt(posFirst).CompareNoCase(GetAt(posNext)) <= 0){
temp = GetAt(posFirst);
SetAt(posFirst, GetAt(posNext));
SetAt(posNext, temp);
return TRUE;
}
}
return FALSE;
}
};
ファイル名を指定するのに良く使われる「ファイルを開く」ダイアログはコモンダイアログと
呼ばれており以下のようにして簡単に使用できます。
下の例では取得したファイル名をエディットボックスのコントロール変数にセットしています。
/* TRUEだと「ファイルを開く」,FALSEだと「ファイルを名前を付けて保存」 */
CFileDialog dlg( TRUE );
long nResponse = dlg.DoModal( );
if( nResponse == IDOK ){
/* エディットボックスにセット */
m_Filename.SetWindowText( dlg.GetPathName( ) );
}
また拡張子を選択するような場合は,以下のように行います。
CFileDialog dlg( TRUE, ".jpg", NULL, OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT,
"読込み可能ファイル(*.jpg;*.bmp)|*.jpg;*.bmp|全てのファイル(*.*)|*.*||" );
long ret = dlg.DoModal( );
if( ret == IDOK ){
m_FileName.SetWindowText( dlg.GetPathName( ) );
}
フォルダの初期位置やキャプションを指定するにはDoModal( )を行う前にダイアログのメンバ変数を変更します。
CFileDialog dlg( FALSE );
dlg.m_ofn.lpstrTitle = "お気に入りに追加"; // キャプションの変更
dlg.m_ofn.lpstrInitialDir = "C:\\WINDOWS\\FAVORITE"; // フォルダの初期位置の変更
dlg.m_ofn.lpstrFile = "filename.dat"; // デフォルトファイル名の設定
dlg.m_ofn.nMaxFile = MAX_PATH; // ファイル名の最大文字数
long nResponse = dlg.DoModal( );
lpstrFileをフルパスでセットした場合はlpstrInitialDirを設定しなくてもそのフォルダが開きます。
(VC7)2006.10.20追記
VC++.NET2003ではデフォルトファイル名を書き換え可能な領域としなければエラーになりました。
CFileDialog dlg( FALSE );
char szFilename[MAX_PATH];
strcpy( szFilename, "filename.dat" );
dlg.m_ofn.lpstrTitle = "お気に入りに追加"; // キャプションの変更
dlg.m_ofn.lpstrInitialDir = "C:\\WINDOWS\\FAVORITE"; // フォルダの初期位置の変更
dlg.m_ofn.lpstrFile = szFilename; // デフォルトファイル名の設定
dlg.m_ofn.nMaxFile = MAX_PATH; // ファイル名の最大文字数
INT_PTR nRet = dlg.DoModal();
if( nRet == IDOK ){
this->GetDlgItem(IDC_EDIT_FILE)->SetWindowText( dlg.GetPathName() );
}
またフォルダを選択するダイアログも見たことがあるかと思いますが、
残念ながらコモンダイアログでは用意されていません。
実装するにはシェル関数を使う必要がありますが、気が向いたのでフォルダダイアログという
クラスを作ってみました。
これ です。
作成にはかぶ さんという方の
サイトを参考にしました。
ここ の
シェルエクステンションという項目です。
以下のようにして使用します。
CFolderDialog dlg( "フォルダを選択してください" );
if( dlg.DoModal( this->GetSafeHwnd( ) ) == IDOK ){
CString path = dlg.GetPathName( );
}
ダイアログベースのアプリケーションにメニューを追加したり,マウス右ボタンのクリックで
表示されるコンテキストメニュー(ポップアップメニュー)を実装する方法を解説します。
リソースエディタのリソース一覧表示部分で右クリックし「挿入...」を選択,Menuを挿入する。
新規追加されたメニューリソースのプロパティで適当なIDをつける。例"IDR_MENU"
メニューアイテムを作成する。このときアイテムのプロパティで「ポップアップ」をチェックすると,
子メニューを作成することができる。
リソースエディタでメニューのプロパティからクラスウィザードを起動すると「クラスの追加」という
問い合わせてくるので「既存のクラスを選択」しメニューを表示するダイアログを選択する。
起動したクラスウィザードで追加したメニューアイテムのIDを選択しメッセージからCOMMANDを
ダブルクリックすると,メニューアイテムを選択したときに実行されるハンドラ関数名を聞いてくるので設定する。この操作を各アイテム毎に行う。
それぞれのハンドラ関数にやりたいことを実装する。
ダイアログにメニューを追加する場合はリソースエディタでダイアログ自身のプロパティを変更します。
一般タブの「メニュー」に追加するメニューIDを設定します。メニューアイテムを有効・無効にしたり
チェックマークを付けたりするにはダイアログオブジェクト内で以下のようにします。
CMenu *pMenu = GetMenu( );
pMenu->EnableMenuItem( IDM_?, MF_GRAYED|MF_BYCOMMAND ); // 無効(淡色)にする
pMenu->EnableMenuItem( IDM_?, MF_ENABLED|MF_BYCOMMAND ); // 有効にする
pMenu->CheckMenuItem( IDM_?, MF_CHECKED|MF_BYCOMMAND ); //チェックマークを付ける
pMenu->CheckMenuItem( IDM_?, MF_UNCHECKED|MF_BYCOMMAND ); //チェックマークを外す
DrawMenuBar( ); // メニューの再描画
コンテキストメニューを作成する場合はクラスウィザードを起動してダイアログのメッセージから
WM_CONTEXTMENUを選択して以下のようにハンドラ関数を実装します。
void CTmpDlg::OnContextMenu(CWnd* pWnd, CPoint point)
{
CMenu menu;
menu.LoadMenu( IDR_MENU ); // IDR_MENU はメニューリソースID
CMenu *pPopup = menu.GetSubMenu( 0 );
// コンテキスト(ポップアップ)メニュー表示
pPopup->TrackPopupMenu( TPM_LEFTALIGN|TPM_RIGHTBUTTON, point.x, point.y, this );
}
【メニュー追記】
★動作が変?
ダイアログでないものからコンテキストメニューを表示して,ある項目を選択したらダイアログを
開くという実装をしようと,マウスの右クリックハンドラでコンテキストメニューを実装したら
,開いたダイアログ上でマウス操作ができないという現象が発生しました。
開いたダイアログ上で一度右クリックするとその後は期待通りマウス操作できるようになるのですが,
これは TrackPopupMenu( ) の仕様がからんでいるようです(詳細は同関数のヘルプを参照)。
マウス右クリックハンドラでは,TrackPopupMenu( ) を呼び出す前にデフォルトハンドラの
OnRButtonDown( ) などを呼び出しておけば上記のような現象は発生しないようです。
★メニューでのトグル動作を簡単に実装する
メニュー項目の選択で,チェックマークを付けたり外したりしてトグル的な操作をしたい
場合はメニュー項目のイベントハンドラ ON_COMMAND と ON_UPDATE_COMMAND_UI を実装して
以下のような処理を追加すると簡単にできます。
// メニュー項目の ON_COMMAND メッセージハンドラ
BOOL bShow = m_wndXXXX.IsVisible();
ShowControlBar(&m_wndXXXX, !bShow, FALSE);
// メニュー項目の ON_UPDATE_COMMAND_UI メッセージハンドラ
pCmdUI->Enable();
pCmdUI->SetCheck(m_wndXXXX.IsVisible());
上記の例ではウインドウ m_wndXXXX の表示,非表示をメニューでトグル的に操作しています。
【2008.10.28追記】
メニュー項目にチェックマーク「レ」を入れるのは pMenu->CheckMenuItem( ) でできますが,
どれかひとつのメニュー項目を選択するラジオマーク「●」を入れることもできます。
pMenu->CheckMenuRadioItem( ) を使います。
また,メニュー項目のそれぞれについて,有効・無効,チェックあり・なしを設定する場合は,
その項目に UPDATE_COMMAND_UI メッセージに対するハンドラを実装すると簡単に行えます。
void CMainFrame::OnUpdateXXXX(CCmdUI *pCmdUI)
{
// 有効にする場合
pCmdUI->Enable(TRUE);
// 無効にする場合
pCmdUI->Enable(FALSE);
// チェックを入れる場合
pCmdUI->SetCheck(TRUE);
// チェックを外す場合
pCmdUI->SetCheck(FALSE);
pCmdUI->xxxx のメンバ関数で,メニュー項目の表示前に色々できるようです。
オプション設定などでよく用いられるタブがいくつかついたダイアログはプロパティシートで作成できます。
以下はメニューからオプションを選択するとプロパティシートが開かれるコードを実装する手順です。
挿入→コンポーネントでプロパティシートを追加する(アクセス権はベースとなるダイアログに設定する)
ウィザードによりプロパティシートクラスと各シートのクラス,およびリソースが自動的に作成される。クラス名はわかりやすい名前に変更した方がいいでしょう。
リソースエディタで追加された各シートのプロパティのキャプションを変更しタブ文字列とする。
同じくリソースエディタで追加された各シートに好きなコントロールをひょいひょいと載せる。ID設定も気の利いたものを。
搭載したコントロールにメンバ変数を「値」で割り付ける。
リソースビューで「挿入...」しMenuを追加する。
メニューバーについてクラスウィザードを起動すると「クラスの追加」ウインドウが開くので
「既存のクラスを選択」にし,メニューを表示させる元ダイアログやフレームを選択する。
メニューバーを表示させるダイアログやフレームについてリソースエディタでプロパティを表示し
「一般」タブ内にある「メニュー」をさっき追加したメニューのIDを設定する。
メニューバーに項目「オプション」を追加する。IDはIDM_OPTION等。
追加した項目についてクラスウィザードを起動し,メッセージのCOMMANDをダブルクリックして、
メニュー選択ハンドラを実装する。
メニュー選択ハンドラから、プロパティシートを追加したウィザードによって自動追加されたOnProperties( )を呼び出す。
OnProperties( )内で適宜以下のような変更を行う。
CMyPropertySheet propSheet;
// 適用ボタンを消す
propSheet.m_psh.dwFlags |= PSH_NOAPPLYNOW;
// ヘルプボタンを消す
propSheet.m_psh.dwFlags &= ~PSH_HASHELP;
propSheet.m_Page1.m_psp.dwFlags &= ~PSP_HASHELP;
propSheet.m_Page2.m_psp.dwFlags &= ~PSP_HASHELP;
propSheet.m_Page3.m_psp.dwFlags &= ~PSP_HASHELP;
//現在の設定値をメンバ変数にセットする。
m_Static1.GetWindowText( propSheet.m_Page1.m_Edit );
m_Static2.GetWindowText( propSheet.m_Page2.m_Edit );
m_Static3.GetWindowText( propSheet.m_Page3.m_Edit );
if( propSheet.DoModal( ) == IDOK ){
// OKが押されたときにメンバ変数を読み取って設定に反映させる。
m_Static1.SetWindowText( propSheet.m_Page1.m_Edit );
m_Static2.SetWindowText( propSheet.m_Page2.m_Edit );
m_Static3.SetWindowText( propSheet.m_Page3.m_Edit );
}
とする。
プロパティシートの大きさも各シートの大きさに自動的にあわせてくれるし,うぉー結構簡単だったぜぃ!
プロパティシートのキャプションを変更するにはリソースビューのストリングテーブルに登録された
データを変更する必要があります。
適用ボタンを有効にする場合、プロパティページで SetModified( ) を実行します。適用ボタンが押された
場合、めくられたプロパティページの各 OnOK( ) ハンドラが呼ばれるので処理が必要な場合は
クラスウィザードで実装しておいてください。
プロパティページを増やす場合はリソースエディタでダイアログを追加作成(プロパティは他の
プロパティページのものに合わせる)し、基本クラスをCPropertyPageとした新規クラスを作成します。
後は他のページと同じようにプロパティシートに追加すればOKです。
後から追加したプロパティページは別ファイルになってしまうので #include など手動で追加する
必要があります。
デバグ実行させた後、
「例外処理(初回)はxxx.exe(COMCTL32.DLL)にあります:0xC0000005:アクセス違反」
と表示されることがあります。
本家 Microsoft の
解説 によると…
メモ
対応するダイアログ リソースから最初にプロパティ ページが作成されるとき、
初回例外が発生する可能性があります。これは、プロパティ ページによって、
ページの作成前にダイアログ リソースのスタイルが必要なスタイルに変更される
ためです。通常、リソースは書き込み禁止であるため、例外が発生する可能性が
あります。この例外はシステムによって処理され、システムによって、更新された
リソースのコピーが自動的に作成されます。このような処理により、初回例外は
無視できるようになります。
だそうです。
無視しても構わないようですが、気持ち悪ければリソースファイル(*.rc)をテキストエディタで
開き、各プロパティページで FONT 9, "MS Pゴシック" という項目を削除すれば、
例外は発生しなくなります。リソースを変更したらまた元に戻っちゃいますが。
またプロパティページに拡張ダイアログがあるとデバグ実行した時に (X) Debug Assertion Failed! など
とエラーウインドウが表示されます。原因を追ってみると Msdev\Mfc\src\Dlgprop.cpp の834行目に
こんな記述がありました。
// WINBUG: Windows currently does not support DIALOGEX resources!
// Assert that the template is *not* a DIALOGEX template.
// DIALOGEX templates are not supported by the PropertySheet API.
// To change a DIALOGEX template back to a DIALOG template,
// remove the following:
// 1. Extended styles on the dialog
// 2. Help IDs on any control in the dialog
// 3. Control IDs that are DWORDs
// 4. Weight, italic, or charset attributes on the dialog's font
ようするにバグというか仕様らしいです。
プロパティページに載せるダイアログアイテムのプロパティの拡張スタイルは全て設定しないように
すれば大丈夫みたいです。
2006.10.20追記
【プロパティシートの作成方法(VC7)】
VC++4.0では上記のように「挿入」→「コンポーネント」でプロパティシートを自動的に追加してくれましたが,
VC++.NET2003ではやってくれないようです。しょうがないので以下のように手作業します。
プロパティシートのタブで開く各ページをダイアログで新規に作成します。
「プロジェクト」→「リソースの追加」でDialogのIDD_PROPPAGE_LARGEを選択して「新規作成」。
該当ダイアログを右クリックして「プロパティ」を選択し,IDとCaptionを適切に設定。
プロパティシートのタブの数だけ上記のようにダイアログを作成。
それぞれのダイアログにコントロールを載せて適切にIDなどを設定する。
それぞれのダイアログのクラスを新規作成する。
該当ダイアログを右クリックして「クラスの追加」を選択し,基本クラスを「CPropertyPage」にする。クラス名を適切に命名して「完了」。
※このときなぜかクラスウィザードが応答なしになってしまう場合はあらかじめ stdafx.h を開いておくとよいかも。
これらのページをまとまるプロパティシートを新規に作成します。プロジェクト→「クラスの追加」で Visual C++ のツリーから MFC を選択し,「MFCクラス」を選んで「開く」。
基本クラスを「CPropertySheet」にしてクラス名を適切に命名して「完了」。
新たに作成されたプロパティシートのヘッダファイルに全てのプロパティページのヘッダファイルを#includeするとともに,パブリックメンバとしてプロパティページの実体を追加する(以下参照)。
#include "PropertyPageXXXX.h"
#include "PropertyPageYYYY.h"
#include "PropertyPageZZZZ.h"
.
.
.
public:
CPropertyPageXXXX m_PropertyPageXXXX;
CPropertyPageYYYY m_PropertyPageYYYY;
CPropertyPageZZZZ m_PropertyPageZZZZ;
さらにプロパティシートのソースファイルのコンストラクタ(2つある)の部分に以下のようにページを追加する処理を実装する。
IMPLEMENT_DYNAMIC(CPropertySheetOption, CPropertySheet)
CPropertySheetAAAA::CPropertySheetAAAA(UINT nIDCaption, CWnd* pParentWnd, UINT iSelectPage)
:CPropertySheet(nIDCaption, pParentWnd, iSelectPage)
{
this->AddPage( &this->m_PropertyPageXXXX );
this->AddPage( &this->m_PropertyPageYYYY );
this->AddPage( &this->m_PropertyPageZZZZ );
}
CPropertySheetAAAA::CPropertySheetAAAA(LPCTSTR pszCaption, CWnd* pParentWnd, UINT iSelectPage)
:CPropertySheet(pszCaption, pParentWnd, iSelectPage)
{
this->AddPage( &this->m_PropertyPageXXXX );
this->AddPage( &this->m_PropertyPageYYYY );
this->AddPage( &this->m_PropertyPageZZZZ );
}
あとはプロパティシートを表示させたいハンドラでプロパティシートをDoModal()するだけです。
ただ,実体を宣言するときは VC4++ の時と異なり,ちゃんとコンストラクタに引数を渡さなければならないようです。
具体例は以下を見てください。
CPropertySheetAAAA PropSheet( "AAAA", this, 0 );
// ;;;; 現在の設定をセット
INT_PTR nRet = PropSheet.DoModal();
if( nRet == IDOK ){
// ;;;; 設定を変更
}
CFile クラスを使用するのが簡単です。
CFile File;
BYTE *pFileImage;
/* ファイルダイアログによるファイル名の取得 */
CFileDialog dlg( TRUE );
INT_PTR nRet = dlg.DoModal();
if( nRet == IDOK ){
// ファイル読み込み
if( File.Open( dlg.GetPathName(), CFile::modeRead ) == TRUE ){
UINT nLen = (UINT)File.GetLength();
pFileImage = new BYTE [nLen];
File.Read( pFileImage, nLen );
File.Close();
}
/* ファイル新規作成(上書き) */
if( File.Open( dlg.GetPathName(), CFile::modeCreate|CFile::modeWrite ) == TRUE ){
unsigned int nData;
File.Write( &nData, sizeof(unsigned int) );
File.Close( );
}
}
1行ずつテキストを読み書きするだけなら以下のように CStdioFile を使うのが一番簡単です。
筆者はこのクラスを最近まで知りませんでした(苦笑)。
ファイルのクローズもデストラクタでやってくれるようです。
CStdioFile File( pszFilename, CFile::modeReadWrite|CFile::typeText );
CString String;
File.ReadString( String );
File.WriteString( String );
// 描画領域としてリソースエディタでピクチャーコントロールを作成し
// コントロール変数を付加
CStatic m_Area;
/* ペン形式を設定する変数を確保しておく */
CPen Pen;
/* 初期化関数でペン形式を設定しておく */
Pen.CreatePen( PS_SOLID, 1, RGB( 0x00, 0xff, 0x00 ) );
/* 描画が必要な場所で以下のように描画を行う */
long x0, y0, x1, y1;
CPen *pPenOld;
CDC *pDC;
pDC = m_Area.GetDC( );
pPenOld = pDC->SelectObject( &Pen );
/* もちろん座標の設定が必要(x0,y0,x1,y1) */
pDC->MoveTo( x0, y0 ); /* 始点 */
pDC->LineTo( x1, y1 ); /* 終点 */
pDC->SelectObject( pPenOld );
/* デバイスコンテキストの開放を忘れないこと */
ReleaseDC( pDC );
例えば1秒毎に何か処理をしたいというプログラムは「タイマー」を利用すると可能です。
タイマーの実装方法は,クラスウィザードのメッセージマップで WM_TIMER メッセージを追加して,
作成された OnTimer ハンドラに好きな処理を実装するだけです。
タイマーの初期化はOnInitDialogなど初期化処理の中で SetTimer( 1, 1000, NULL ); などとします。
タイマーの起動周期はmsec単位で指定できますが,あまり早くするとシステムの負担が大きくなります。
またある処理の中で1秒間だけ休止したいという場合は Sleep( 1000 ); とすると1000msecだけ休止します。
休止期間中は他のスレッドが動くのでシステムに負担をかけません。
さらにその時々の詳細な時間を調査したい場合は GetTickCount( ); を使用します。
この関数はWindowsが起動してからの時間がmsec単位で戻ります。戻り値がDWORDなので
49日位すると0に戻ってしまいますので注意してください。
【2010.04.27追記】
さらにさらに,1msec以下の経過時間を知りたい場合は QueryPerformanceCounter() を使って
高分解能パフォーマンスカウンタを取得します。このカウンタの更新周波数は
QueryPerformanceFrequency()で調べられます。以下に0.2msec以上※待機するサンプルを掲載します。
※Windowsはリアルタイムの保証がないので0.2msec「以上」になる場合が大いにあるためこのような表現をしています。
// 高分解能待機関数
void WaitPerformance( LONGLONG llCount )
{
LARGE_INTEGER liCountStart, liCountNew;
LONGLONG llDiff;
BOOL bRet = QueryPerformanceCounter( &liCountStart );
// 高分解能パフォーマンスカウンタ非対応なら1msecスリープ
if( bRet == 0 || llCount == 0 ){
Sleep( 1 );
}
else {
for(;;){
QueryPerformanceCounter( &liCountNew );
llDiff = liCountNew.QuadPart - liCountStart.QuadPart;
if( llDiff >= llCount ){
break;
}
}
}
}
// 0.2msec以上待つ
LARGE_INTEGER liFrequency;
BOOL bRet = QueryPerformanceFrequency( &liFrequency );
if( bRet ){
LONGLONG llWaitCount;
// 0.2msec = 0.0002sec = 1/5000sec
llWaitCount = liFrequency.QuadPart / 5000;
WaitPerformance( llWaitCount );
}
else {
Sleep( 1 );
}
メニューからオプションを選択、何々の設定…とかやる方法。
リソースビューでDialogを右クリックし「Dialog」の挿入を行う。
追加したダイアログに好きなコントロールをひょいひょいと載せる。ID設定も気の利いたものを。
搭載したコントロールにメンバ変数を「値」で割り付ける。
追加したダイアログについてクラスウィザードを起動すると「クラスの追加」ウインドウが開くので
で「新規クラスの作成」を選択する。
クラス名はCOption等,基本クラスはCDialogにする。
リソースビューで「挿入...」しMenuを追加する。
メニューバーについてクラスウィザードを起動すると「クラスの追加」ウインドウが開くので
「既存のクラスを選択」にし,メニューを表示させる元ダイアログやフレームを選択する。
メニューバーを表示させるダイアログやフレームについてリソースエディタでプロパティを表示し
「一般」タブ内にある「メニュー」をさっき追加したメニューのIDを設定する。
メニューバーに項目を追加する。IDは何でもいいらしい。ID_MENU等。
さらに追加した項目についてクラスウィザードを起動し,メッセージのCOMMANDをダブルクリックして、
ハンドラ関数名を決定する。
そのハンドラ内で
// TODO: この位置にコマンド ハンドラ用のコードを追加してください
COption dlg;
// 現在の設定値をメンバ変数にセットする。
dlg.m_data = data;
if( dlg.DoModal( ) == IDOK ){
// OKが押されたときにメンバ変数を読み取って設定に反映させる。
data = dlg.m_data;
}
とする。
上記ハンドラが記述されるファイルでCOptionクラスの定義ヘッダファイルを#includeする。
エクスプローラからドラッグ&ドロップしてきたファイルを開かずに、
そのファイル名だけ受け取る方法。
・ダイアログベースの場合
デフォルトではファイルのドロップイベントを受け取れない。
クラスウィザードでもイベントを追加できないので、手作業で以下のことを行う。
(★マークがついている個所が追加部分です)
1. ダイアログ.h にメッセージマップ関数を追加定義
// 生成されたメッセージ マップ関数
//{{AFX_MSG(C????Dlg)
virtual BOOL OnInitDialog( );
afx_msg void OnPaint( );
afx_msg HCURSOR OnQueryDragIcon( );
afx_msg void OnDropFiles( HDROP hDropInfo ); // ★ ドロップハンドラ追加
virtual void OnOK( );
//}}AFX_MSG
DECLARE_MESSAGE_MAP( )
2. ダイアログ.cpp にメッセージマップ関数を追加
BEGIN_MESSAGE_MAP(C????Dlg, CDialog)
//{{AFX_MSG_MAP(C????Dlg)
ON_WM_PAINT( )
ON_WM_QUERYDRAGICON( )
ON_WM_DROPFILES( ) // ★ ドロップハンドラ追加
//}}AFX_MSG_MAP
END_MESSAGE_MAP( )
3. 同じくダイアログ.cpp の OnInitDialog( ) にドロップイベントを許可する旨を追加
// TODO: 特別な初期化を行う時はこの場所に追加してください。
DragAcceptFiles( ); // ★ ファイルドロップを許可
4. ドロップイベントハンドラを追加
★// ファイルドロップイベントハンドラ
★void C????Dlg::OnDropFiles( HDROP hDropInfo )
★{
★ long files;
★ char filename[256];
★
★ /* ドロップされたファイル数を取得 */
★ files = DragQueryFile( hDropInfo, 0xFFFFFFFF, filename, 256 );
★
★ /* ドロップされた一番目のファイル名文字列を取得 */
★ DragQueryFile( hDropInfo, 0, filename, 256 );
★
★ CEdit *pEdit = (CEdit*)GetDlgItem( IDC_???? );
★ pEdit->SetWindowText( filename );
★
★ // CWnd::OnDropFiles( hDropInfo );
★}
上記の例ではエディットボックスに取得したファイル名をセットしています。
・SDI(MDI)の場合
デフォルトではファイルを開こうとしてしまう。
1. クラスウィザードで MainFrame に OnDropFiles イベントを追加する。
2. OnDropFiles イベントハンドラを以下のように修正。
void CMainFrame::OnDropFiles(HDROP hDropInfo)
{
// TODO: この位置にメッセージ ハンドラ用のコードを追加するか
// またはデフォルトの処理を呼び出してください
★ CM3UView *pView;
★ pView = (CM3UView *)GetActiveView( ); // これが勝利の鍵だ!!
★ pView->OnDropFiles( hDropInfo );
★
★ DragFinish( hDropInfo );
★ // CFrameWnd::OnDropFiles(hDropInfo);
}
デフォルトのイベントハンドラをコメントアウトして、
ビューの OnDropFiles を呼び出すようにする。
3. MainFrm.cpp にドキュメントクラスとビュークラスの .h ファイルを #include する。
4. クラスウィザードでビューに OnDropFiles イベントを追加する。
5. ビューの OnDropFiles イベントハンドラを以下のように修正。
void CM3UView::OnDropFiles(HDROP hDropInfo)
{
// TODO: この位置にメッセージ ハンドラ用のコードを追加するか
// またはデフォルトの処理を呼び出してください
★ long files;
★ char filename[256];
★
★ /* ドロップされたファイル数を取得 */
★ files = DragQueryFile( hDropInfo, 0xFFFFFFFF, filename, 256 );
★
★ /* ドロップされた一番目のファイル名文字列を取得 */
★ DragQueryFile( hDropInfo, 0, filename, 256 );
★
★ CEdit *pEdit = &GetEditCtrl( );
★ CString string;
★ pEdit->GetWindowText( string );
★ if( string.GetLength( ) > 0 ) string += "\r\n";
★ string += filename;
★ pEdit->SetWindowText( string );
★
★ // CEditView::OnDropFiles(hDropInfo);
}
この例では CEditView に取得したファイル名をどんどん追加していってます。
ドロップするファイルは1つとは限らないので、ファイル数の分だけループする
と良いでしょう。
エクスプローラなどでは一つのウインドウが左と右に別れていますが、
これがウインドウのスプリットです。
PowerWaveも上の方には波形全体を表示するウインドウ、
下の方には拡大できるエディットウインドウを作成しようと思っています。
さて、MDI(マルチドキュメントインタフェース)で子ウインドウをスプリットするには
以下のような方法が有効でした。
1. AppWizardでMDIを選択しプロジェクトを作成する。
2. 途中ステップ6/6で、ビュークラスを適当な名前に変更する。
(プロジェクト名と同じ名前のビューだとよくわからなくなるので)
3. ClassWizardで新規ビュークラスを作成する。
4. ChildFrm.h のCChildFrameに
public:
CSplitterWnd m_wndSplitter;
を追加する。
5. ClassWizardで ChildFrame::OnCreateClient のイベントハンドラを実装する。
6. そのハンドラに以下のコードを追加する。
if( !m_wndSplitter.CreateStatic( this, 2, 1 ) ||
!m_wndSplitter.CreateView( 0, 0, RUNTIME_CLASS( CWholeView ),
CSize( 100, 100 ), pContext ) ||
!m_wndSplitter.CreateView( 1, 0, RUNTIME_CLASS( CPartialView ),
CSize( 0, 0 ), pContext ) ){
MessageBox( "スプリットウインドウの作成に失敗しました", NULL, NULL );
return( FALSE );
}
上の例ではCWholeViewとCPartialViewがスプリットされたウインドウの
2つのビュークラスです。
7. ChildFrm.cppにビュークラスのヘッダファイルを#includeする。
こんな感じです。関数の詳しい情報などはヘルプを読めばわかると思います。
マウスを置いてしばらくすると、漫画のセリフのような小さなヘルプがでるヤツが
ツールヒントです。これを出すのは凄く簡単!!
挿入→コンポーネントでツールヒントを選択し、導入して下さい。
自動的にコードにインプリメントされます。
やらなくちゃいけないのは、どのオブジェクトでどの文字列を出すかという設定だけ。
以下のようなコードが自動的に挿入されているはずなので、適切に設定します。
// TODO: コントロールの追加には以下のどちらかの形式を使用してください:
// m_tooltip.AddTool(GetDlgItem(IDC_), );
// m_tooltip.AddTool(GetDlgItem(IDC_), "");
// 実際の設定例
m_tooltip.AddTool(GetDlgItem(IDC_EDIT), IDS_TIP_EDIT );
これこそストリングテーブルで用意した方が後々らくですね。;-)
これでそれっぽいアプリになりました。
そうそうこれは、ボタンとかエディットボックスとか具体的に動作するオブジェクトでしか
ヒントが出ないようです。CStaticには出ないみたい…。
[後日談]
挿入→コンポーネントではなんとプロパティページにはツールヒントを導入できないことが発覚!!
おいおい一番必要なところなのに…。
しょうがないのでツールヒントを手動でインプリメントする方法を調べました。
まずツールヒントを表示したいダイアログ(プロパティページ)クラスの protected: メンバに
コントロール変数を追加します。
protected:
CToolTipCtrl m_tooltip;
次にダイアログの OnInitDialog( ) で以下のコードを追加します。
m_tooltip.Create( this );
m_tooltip.Activate( TRUE );
// 以下のどちらかの形式でヒントを表示したいダイアログアイテムに
// 表示するテキストを割り当てます
// m_tooltip.AddTool( GetDlgItem(IDC_), );
// m_tooltip.AddTool( GetDlgItem(IDC_), "");
さらにダイアログに PreTranslateMessage ハンドラをクラスウィザードで実装し以下のように
コーディングします。
BOOL C????Dlg::PreTranslateMessage(MSG* pMsg)
{
// ツールヒント処理
m_tooltip.RelayEvent(pMsg);
return CDialog::PreTranslateMessage(pMsg);
}
以上でツールヒントの実装は終了です。
[後々日談]
なんとVC++4.0でのツールヒントは1行テキストしか対応していないことが判明。ズガビーン!!
調べによるとVC++6.0では複数行に対応しているみたいだけど、そんなの買う金があったら未だに
VC++4.0なんて使ってねーよ! ということで、CToolTipCtrlから複数行対応の派生クラス作成に挑戦!!
OnPaintあたりでもオーバーライドすれば楽勝だろうなんて思っていたら、なんとOnPaintが
実行されないことが分かってあきらめかけましたが、WindowProcを丸ごとオーバーライドして
なんとかできました。
これ が
複数行対応のCToolTipsCtrlです。色とフォントも変えられます。
CToolTipCtrlと差し替えてそのまま使用できます。
スピンボタンのコントロールと同じように、クラスウィザードでオブジェクトへの
コントロール変数を割り当てます。CStaticだったりCButtonだったりCEditだったり
しますが、これらは全てCWndの派生クラスです。つまりメンバ関数である
GetWindowTextでそのオブジェクトのキャプションを取得でき、SetWindowTextで
キャプションを設定(変更)することが可能です。
これでCEditの場合はエディットボックスの中身を参照・変更できます。
管理上、メッセージなんかはストリングテーブルで
リソースとして一元管理したいところ。
リソースエディタのツリーにて右クリックし、挿入、String Tableを選択すると
ストリングテーブルを作成することができます。
このテーブルにIDS_からはじまるIDとそれに定義するメッセージを追加し、
ソース中でこの文字列を使いたくなったら、以下のようにしてロードして使用します。
CString str; // ロード用CString変数が必要
str.LoadString( IDS_MESSAGE ); // リソースから文字列をロード
pDC->TextOut( 0, 0, str ); // などとして描画
これで多言語化もバッチリ?! (^^
ウインドウに何らかの描画を行う場合デバイスコンテキストを取得しますがここでまとめておきます。
デバイスコンテキストとはWindowsシステムの中の画家さん達です。GDIオブジェクトという
ビットマップやフォントやペンが画材です。画家さん達は全部で5人しかいません。つまりシステムで
GetDC( )できるのは同時に5回までです。これを越えると予期しない領域に描画が行われたりして
Windowsの中の人も大変です。したがってデバイスコンテキストは必要なときに取得し、用が済んだら
解放します。アプリのメンバ変数などで抱え込んではいけません。
デバイスコンテキストをGetDC( )したのにReleaseDC( )しなくてもエラーにはなりません。
せめてデバイスコンテキストが足りなくなったらメッセージ出してもいいのに…
またCDCオブジェクトを自分で生成し、CreateDC( ), CreateIC( ), CreateCompatibleDC( ) で作成された
デバイスコンテキストはデストラクタで自動的に後処理(DeleteDC( ))が行われるので解放を特に意識
する必要はありません。これはCClientDCやCWindowDCで生成した場合も同じです。
以下の2つのサンプルは同じ効果を得られます。
// ピクチャーコントロールのアドレスを取得
CWnd *pWnd = GetDlgItem( IDC_PICTURE );
// 描画領域座標を取得
CRect rect;
pWnd->GetClientRect( rect );
// ピクチャーコントロールのデバイスコンテキストを取得
CDC *pDC = pWnd->GetDC( );
// 塗る
pDC->FillSolidRect( rect, RGB(0,255,0) );
// フレームの再描画(必要であれば)
pWnd->Invalidate( );
// デバイスコンテキストの開放を忘れないこと
pWnd->ReleaseDC( pDC );
// ピクチャーコントロールのアドレスを取得
CWnd *pWnd = GetDlgItem( IDC_PICTURE );
// ピクチャーコントロール用のデバイスコンテキストを生成
CClientDC dc( pWnd );
// 描画領域座標を取得
CRect rect;
pWnd->GetClientRect( rect );
// 塗る
dc.FillSolidRect( rect, RGB(0,255,0) );
// フレームの再描画(必要であれば)
pWnd->Invalidate( );
後者では自分で生成したデバイスコンテキストでピクチャーコントロールのコンテキストを
コンストラクタ内部で取得していますが、変数が破棄される際にデストラクタが解放を
自動的に行っているのでアプリケーションで明示的に解放する必要はありません。
状況にもよりますが、簡単な描画を行うのであれば後者の方が楽だと思います。
ところでデバイスコンテキストに関してフォントや色を変更した後,元に戻さないといけないのですが,
ひとつひとつ元に戻すのはおっくうです。と思っていたら一度に復帰させる便利な関数がありました!
// デバイスコンテキストの保存
int SavedDC = pDC->SaveDC( );
// 背景モードを透明に設定
pDC->SetBkMode( TRANSPARENT );
// テキスト色を設定
pDC->SetTextColor( RGB( 0, 0, 0 ) );
// 塗り塗り書き書き
// デバイスコンテキストの復帰
pDC->RestoreDC( SavedDC );
どうやらシステムに保存するスタックがあるようです。
以下ちょっとした描画ルーチン集
CClientDC DC(this);
// 背景の塗りつぶし
CRect Rect;
this->GetClientRect( Rect );
DC.FillSolidRect( Rect, RGB(0,0,0) );
// 枠の描画
DC.FrameRect( CRect(100,100,200,200), &CBrush( RGB(255,0,0) ) );
// 文字の描画
DC.SetBkMode( TRANSPARENT );
DC.SetTextColor( RGB(255,255,255) );
DC.TextOut( 0, 0, "ふがふが" );
ある決められた領域を塗りたい場合、その領域の座標x,y,w,hを#defineしておいて
pDC->FillSolidRect( x, y, w, h, color );とするのはちょっと鬱です。
あらかじめリソースエディタでそこにピクチャーオブジェクトを作成しておきましょう。
ピクチャーオブジェクトはCStaticクラスオブジェクトですがCWndの派生クラスなので
領域情報を得る関数GetWindowRect( )やデバイスコンテキストを取得する関数GetDC( )を
使用でき、以下のように座標を意識しなくても、この領域を塗ることができます。
リソースエディタで作成したピクチャーコントロールはタイプを「フレーム」にし、
IDを例えばIDC_PICTUREとしておき、ダイアログのOnPaint( )などで以下のように記述します。
// ピクチャーコントロールのアドレスを取得
CWnd *pWnd = GetDlgItem( IDC_PICTURE );
// 描画領域座標を取得
CRect rect;
pWnd->GetClientRect( rect );
// ピクチャーコントロールのデバイスコンテキストを取得
CDC *pDC = pWnd->GetDC( );
// 塗る
pDC->FillSolidRect( rect, RGB(0,255,0) );
// フレームの再描画(必要であれば)
pWnd->Invalidate( );
// デバイスコンテキストの開放を忘れないこと
pWnd->ReleaseDC( pDC );
レイアウトが気に入らなくなったらピクチャーコントロールの位置や大きさを
リソースエディタで変更するだけでいいのでとっても楽チンですね。
上記の応用で、ピクチャーコントロールで位置と大きさを指定した子ウインドウを作成することもできます。
ピクチャーコントロールは非可視にしておき以下のようにします。
CRect Rect;
GetDlgItem( IDC_PICTURE )->GetWindowRect( Rect );
this->ScreenToClient( &Rect );
CWnd Window;
Window.Create( NULL, NULL, WS_VISIBLE, Rect, this, IDC_PICTURE );
Window.ModifyStyleEx( NULL, WS_EX_STATICEDGE, SWP_DRAWFRAME );
ウインドウのタイトルバーにアイコンを追加・変更する場合は以下のようにします。
// アイコン変更
HICON hIcon = AfxGetApp()->LoadIcon( アイコンのリソースID );
this->SetIcon( hIcon, FALSE );
ボタンにアイコンを貼り付けるにも SetIcon( ) が使えますが,その前にひと手間あります。
リソースエディタのプロパティのスタイルで「アイコン」にチェックを入れておきます。
アイコンは同じくリソースエディタで,挿入-リソース-Iconで新規作成します。大きさは標準の32×32です。
それ以外だとリサイズされて品質が落ちてしまいます。貼り付けの位置はボタンの中央とアイコンの中央を
合わせるので図柄は真ん中辺に描いてください。
ボタンの ID が IDC_BUTTON ,アイコンの ID が IDI_ICON の場合,OnInitDialog で以下のようにすると
貼り付きます。
((CButton *)GetDlgItem(IDC_BUTTON))->SetIcon(AfxGetApp( )->LoadIcon(IDI_ICON));
マウスカーソルの座標を取得および設定するには以下のようにします。
POINT Pos;
GetCursorPos( &Pos ); // 取得
SetCursorPos( Pos.x, Pos.y ); // 設定
マウスカーソルが動いたときは、WM_MOUSEMOVE メッセージハンドラで受けられますが
タイトルバーなど非クライアント領域でのイベントは得られません。
非クライアント領域でのイベントは WM_NCMOUSEMOVE で受けられますが、クラスウィザードでは
出てこないので手作業で実装する必要があります。
※VC++4.0以降の環境では選択できるかも。
VC++.NET 2003ではリソースプロパティのメッセージ一覧から選択できました。
作業は「ドロップしたファイルの名前を受け取る」 で OnDropFiles( ) を
実装した方法と同じです。ヘッダファイルとソースファイルにメッセージマップとハンドラの登録
を行います。
ヘッダファイル.h に
afx_msg void OnNcMouseMove( UINT nHitTest, CPoint point );
を追加
ソースファイル.cpp にメッセージマップ
ON_WM_NCMOUSEMOVE( )
とハンドラ
void C????Dlg::OnNcMouseMove( UINT nHitTest, CPoint point )
{
}
を追加
なおウインドウ外のマウスイベントを取得するには SetCapture( ) すると得られます。
ただし受けるウインドウがアクティブである必要があります。
マウスカーソルのアイコンを変更する場合は以下のようにします。
// マウスカーソルを砂時計にする。
SetCapture( );
SetCursor( AfxGetApp( )->LoadStandardCursor( IDC_WAIT ) );
// マウスカーソルを元に戻す
ReleaseCapture( );
砂時計などの標準のカーソルは上記のように LoadStandardCursor( ) でロードしますが、
自分で作ったカーソルを表示するにはその代わりに LoadCursor( IDC_MYCURSOR ) を使い、
リソースエディタで作成したカーソルのIDを指定します。
マウスカーソルを SetCapture( ) しておかないと変わらないので注意してください。
また ReleaseCapture( ) も忘れずに。
ラジオボタンは複数の選択肢から一つを選択するためのボタン群です。
実装方法を以下に示します。
リソースエディタで複数のラジオボタンを並べて,最初のボタンのプロパティの一般でグループに
チェックを入れます。その他のボタンはグループのチェックを入れないようにします。
さらにプロパティのスタイルを自動にしてチェックマークが自動でどれか一つに入るようにします。
最後にレイアウトメニューのタブオーダーでラジオボタン群が順番になるようにします。
タブオーダーの設定の仕方は、ダブルクリックして1番をセットし、あとは順番にシングル
クリックしてセットしていきます。
各ボタンのクラスウィザードでBN_CLICKEDハンドラを実装すると,各ボタンが押されたときの
処理を実装することができます。
OnInitDialogなど初期化時にどれか一つを選択するには
((CButton *)GetDlgItem( IDC_RADIO_???? ))->SetCheck( 1 );
などとしてチェックマークを付けることができます。
リソースエディタでエディットボックスとスピンボタンを関連づけて作成するには
それぞれのコントロールを配置した後、レイアウトメニューのタブオーダーでスピンボタンの直前に
エディットボックスがナンバリングされるようにします。タブオーダーの設定の仕方は、
ダブルクリックして1番をセットし、あとは順番にシングルクリックしてセットしていきます。
またスピンボタンのプロパティでスタイルの自動関連付けと数値の自動表示を設定しておきます。
こうしてデフォルトでは0から100までしか変更できない、しかも増減が逆な感のあるヘンテコなものが
できあがります。
最小と最大の値を変更するには、クラスウィザードのメンバ変数で、
スピンボタンオブジェクトにスピンボタンコントロール変数を割り付けます。
その後、初期化処理の中でこのコントロール変数を例えば
CSpinButtonCtrl m_SpinButton; // クラスウィザードが自動で*.hに追加
…
m_SpinButton.SetRange( 0, 255 ); // OnInitDialog( )などで最小・最大を設定
とすると、期待通りの動作になります。
最大と最小が入れ替わっていると、増減の向きが逆になるようです。
ボタンなど、リソースオブジェクトのコントロールは、
このようにコントロール変数を割り付け、それのメンバ関数を呼ぶことによって
行うみたいです。
例えばエディットボックス内の数値が変更されたら、画面も更新したいと思って
エディットボックスのEN_UPDATEハンドラで画面更新処理Invalidate(FALSE)等を
呼んだりすると、エディットボックスでのカーソルの動きが変になります。
カーソルが進んだと思っても書き直しでまた先頭に戻ってしまう。
この場合、エディットボックスの変更はリターンキーで確定という仕様に割り切り、
EN_UPDATEハンドラでは画面更新処理は行わず、リターンキーを押すと実行される
OnOKハンドラでInvalidate(FALSE)を実行して画面を書き直すという風に妥協しました。(^^;
[追記]
Invalidate( )はダイアログ全体を無効にする関数(その結果再描画される)なので、
エディットボックスのカレット(カーソル)も初期化されてしまうのは当たり前です。
上を書いた当時はそんなことも分からなかったのですね。(^^;
上記の場合の解決方法としては画面を更新する関数を起こして、EN_UPDATE ハンドラから
呼べばいいだけなのに、何故そうしなかったのでしょう?全部 OnPaint( ) でやろうとしてたの
でしょうか?きっと C++ がよく分からなくてメンバ関数とかの追加ができなかったのでしょう。(^^;;;
[ちょっと高度?]
例えばエディットボックスに時刻を表示するとします。表示されている時刻を変更しようと
入力操作をした時,2桁入力したら自動で":"を挿入するような便利な補完機能を簡単に
実装するにはどうすればいいでしょうか?
EN_CHANGEメッセージのハンドラでエディットボックス内の文字列の前の値をstaticでとっておいて
前の文字列の長さが1で新しい文字列の長さが2のときのみ補完を実行すれば可能です。
以下にサンプルコードを示します。
// エディットボックス更新ハンドラ
void C????Dlg::OnChangeEditBox( )
{
// 更新前の文字列
static CString StringOld;
// 更新後の文字列を取得
CString StringNew;
m_EditBox.GetWindowText( StringNew );
// 自動補完する条件に当てはまった場合
if( StringNew.GetLength( ) == 2 && StringOld.GetLength( ) == 1 ){
// 補完して更新
StringNew += ":";
m_EditBox.SetWindowText( StringNew );
// 更新後のカレットを末尾に移動
m_EditBox.SendMessage( WM_KEYDOWN, (WPARAM)VK_END, (LPARAM)0 );
}
// 更新前の文字列を更新
StringOld = StringNew;
}
デスクトップのデバイスコンテキストを取得することができます。
CDC dc_root;
dc_root.CreateDC( "DISPLAY", NULL, NULL, NULL );
これで取得したdc_rootは他の時と同じようにCDCのメンバ関数を使うことが可能です。
テキスト出力を行うと壁紙の部分にテキストが描画されます(笑)。
なおこの場合、dc_rootのデストラクタでコンテキストの解放が自動的に行われるので
ReleaseDC( )やDeleteDC( )を実行する必要はありません。
テキスト描画の際に、テキストの色と、背景の色を以下のようにすると変更できます。
// pDC デバイスコンテキストへのポインタ
pDC->SetBkMode( TRANSPARENT ); // 背景モードを透明に設定
pDC->SetTextColor( RGB( 255, 0, 0 ) ); // テキスト色を設定
pDC->TextOut( 0, 80, "背景色透明 テキスト色を赤に変えた" );
pDC->SetBkMode( OPAQUE ); // 背景モードを塗に設定
pDC->SetBkColor( RGB( 0, 0, 255 ) ); // 背景色を設定
pDC->SetTextColor( RGB( 255, 255, 255 ) ); // テキスト色を設定
pDC->TextOut( 0, 120, "背景色青 テキスト色を白に変えた" );
DC(デバイスコンテキスト)を取得した後、そのDCでテキストを描画するときに
以下のようにするとフォントを選択できます。
// フォントサイズをポイントで指定する場合
CFont Font;
Font.CreatePointFont( 90, "MS Pゴシック" );
CFont *pFontOld = pDC->SelectObject( &Font );
pDC->TextOut( 0, 0, "フォントを変えてみた" );
pDC->SelectObject( pFontOld );
1ポイントは1/72インチなので,1ポイントが何ピクセル(ドット)に相当するかは以下のように計算できます。
(ピクセル数) = (ポイント数) / 72 * (dpi値)
dpi値とは画面のプロパティで設定されている値で1インチ(2.54cm)あたりを何ピクセルで
画面表示するかを表したものです。通常ディスプレイは96dpiとされているので計算してみると
9ポイントのフォントは12ピクセルで表示されます。なおWindowsはテキトーなので
15インチ1024×768のディスプレイでも17インチ1024×768のディスプレイでも
96dpiが使用されているので注意が必要です。
また文字列の幅と高さをピクセル数で取得したい場合は以下のようにします。
CString String = "サンプル文字列";
CSize Size = pDC->GetTextExtent( String );
// Size.cx に幅,Size.cyに高さが格納されます。
なお斜体や太字など,より詳しくフォントの設定をしたい場合は、以下のようにします。
CFont Font;
Font.CreateFont( 48, 0, 0, 0, FW_REGULAR, FALSE, FALSE, FALSE,
SHIFTJIS_CHARSET, OUT_TT_PRECIS, CLIP_DEFAULT_PRECIS,
DEFAULT_QUALITY, DEFAULT_PITCH, "MS Pゴシック" );
CFont *pFontOld = pDC->SelectObject( &Font );
pDC->TextOut( 0, 0, "フォントを変えてみた" );
pDC->SelectObject( pFontOld );
詳細はヘルプを見てください。
またエディットボックス内のフォントを変更する場合は CFont m_Font などとして
メンバ変数を確保しておき,OnInitDialog( )で
m_Font.CreatePointFont( 240, "MS Pゴシック" );
m_EditCtrl.SetFont( &m_Font );
とすればそのエディットボックスは常に変更したフォントが使用されます。
MFCのAppWizardでダイアログベースのアプリケーションを作ると、
OKとCANCELのボタンがあるだけのスケルトンが作成されます。
大抵このボタンは必要無いのでリソースエディタで削除しちゃいますが、
にも関わらずリターンキーを押したりすると勝手に終了しちゃうのが
気に食わないですね。だからといってOKとCANCELが押されたときのハンドラを
削除すると、今度は永遠に終らなくなってしまいます。
簡単な方法。リソースエディタでOKボタンを削除する前にクラスウィザードでOKボタンが
押されたときのハンドラを実装(オブジェクトIDがIDOKでBN_CLICKEDメッセージを追加)し,
以下のように変更する。
void C????Dlg::OnOK( )
{
// ダイアログが閉じないように小細工
// Dialog::OnOK( );
}
その後でボタンを削除してください。あいかわらずESCキーを押すと終了しちゃうけど…。(^^;
[追記]
こういった特殊なキーは OnKeyDown( ) より前で処理されてしまうので、全て制御したい場合は
以下のようにメッセージハンドラ PreTranslateMessage( ) を実装し、
メッセージが処理される前に制御を行う必要があります。
BOOL C????Dlg::PreTranslateMessage(MSG* pMsg)
{
// ESCキーを無効にする
if( pMsg->message == WM_KEYDOWN ){
if( pMsg->wParam == VK_ESCAPE ) return( TRUE );
}
return CDialog::PreTranslateMessage(pMsg);
}
メッセージボックスは確認用のダイアログを表示したりデバグ用に文字列を
表示したりするのに使用できます。ボタンの数やアイコンの種類も変更できます。
方法はヘルプを参照してください。
// CWndクラスにはメンバ関数としてメッセージボックスを表示する関数が
// あるのでその派生クラスのオブジェクトからは以下のように使えます。
this->MessageBox( "ほげほげ" );
// CWndクラス以外のオブジェクトでは次のような関数が使用できます。
AfxMessageBox( "ふがふが" );
::MessageBox( NULL, "へろへろ", NULL, NULL );
ヘルプを見るとアプリケーションは基本的に AfxMessageBox( ) 関数を使えと書いてあります。
AfxMessageBox( ) はメッセージボックスのウインドウタイトル文字列(キャプション)が
自動的にアプリケーション名になります。ただしアプリケーションのコンストラクタ実行中は
まだアプリケーション名(CWinApp.m_pszAppName)がセットされていないようで,デフォルトである
「エラー」という文字列が表示されてしまいます。これを避けるにはAPIである ::MessageBox( )
を使用するしかないようです。
文字列を確保,操作するには CString クラスが便利である。これは文字列のポインタというより
各種編集関数を備えた文字列自身と捕らえればよい。以下のようにするだけで
エディットボックス内の文字列を変更できる。
CString str;
int i = 2;
str.Format( "TEST: 1+1=%d", i );
this->m_edit_ctrl.SetWindowText( str ); // エディットボックスに表示
またCString型のデータをNULLで終わる文字列に変換したい場合は、
CString型のデータを(LPCTSTR)でキャストすれば可能である。その逆は直接代入可能。
// CString型のデータを文字列に変換
CString str;
char *buf[256];
sprintf( buf, "%s", (LPCTSTR)str );
// 文字列からCString型に変換
CString str;
str = "TEST";
リソースエディタで作成したエディットボックスなどのオブジェクトをプログラム中で
アクセスするには,クラスウィザードにより当該オブジェクトへカテゴリがコントロールの変数を追加
すれば,その変数を使用することにより可能である。
MFCが作ってくれたスケルトンソースですが、コードをインプリメントする時は
TODOとコメントされた場所に記述するようにしましょう
(AppWizardでコメント出力を許可する必要があります)。
追加したメンバ変数の初期化などはOnInit****( )の中のTODOで行わないと、
正常に動作しなくなることがしょっちゅうでした。
ただカーソルやアイコンをロードするのはTODOのない所(クラスのコンストラクタ)
でないとダメみたいなんだよなぁ(謎)。
まだダイアログアプリしかやってないのでホントは違うかもしれません。(^^;
…というのはC++とVC++を分かっていなかった頃に書いた文章です(笑)。
©Yutaka Wada(ana53), AirparkLab ALL RIGHTS RESERVED.
海外旅行