‘Win32’のエントリ

土日はずっとタスクトレイ用のアイコンを作っていた。アイコン作るたびに思うけど,16×16は表現の限界がある。特にタスクトレイの場合,Windows2000ではシステム16色しか使えないので大変。

最終的にタスクマネージャのようなアニメーションをしたいのだが,アイコンを何枚も用意するのはカッコ悪いので,動的に作る仕組みを考えてみる。アイコンのフォーマットはMSDNに公開されているので,その通りにリソース-GRPICONDIR-GRPICONDIRENTRY-ICONDIRENTRYの順番で探す。最終的にBMPと同じ形式になるのでどうにでも編集できて,CreateIconFromResourceExでアイコンを作成。

久しぶりにGDIプログラミングをしてみる。最後に本格的にやったのはWAP用エディタ作った時だから,5年以上前ってことに。それ以降はずっとWebアプリとDirectXとかだから,MFCとかデバイスコンテキストとかだいぶ忘れてる。

#とは書いたものの,ロビーは結構ガシガシとウィンドウプログラミングしてたか。

とりあえず作りたいのはVisual Studioにあるようなプロパティ編集用グリッドコントロール。.NETならPropertyGridViewというそのまんまのクラスがあるのだが,C++ネイティブコードからは簡単には使えない(多分)ので,完全自作しかないかなあ,てな感じで。

松4氏が書いたC++コード。ちょっと変えてあるけど。

class A
{
public:
    void f();
};

void A::f()
{
    std::cout << "hello." << std::endl;
}

int main()
{
    A a = NULL;
    a->f();

 return 0;
}

一見 NULL Pointer Exception が発生しそうだが,VC でも GCC でも BCC でも通る。アセンブリレベルで見ると絶対アドレスで関数を呼んでいる。なので仮想関数だったりするとアウト。コンパイラ依存ぽいけど。

ロビー,スクロールバーが最下部になければスクロールしない機能がようやく実装完了。MFCを使用している場合,下のようなコードで OK と思われる。

// 元々選択されていた部分を保存
long org_start, org_end;
richEdit.GetSel(org_start, org_end);

// スタイルを変更するかどうか
bool modifyStyle = false;

// 変更前のスタイル
LONG style = ::GetWindowLong(richEdit.GetSafeHwnd(), GWL_STYLE);

// スクロールバーが存在
if((style & WS_VSCROLL) != 0)
{
    // スクロール情報を取得
    SCROLLINFO scrollInfo;
    if(richEdit.GetScrollInfo(SB_VERT, &scrollInfo))
    {
        // 選択されているか,最終行でなければスタイルを変更する
        if((org_start != org_end)
            || (scrollInfo.nPos < (scrollInfo.nMax - static_cast<int>(scrollInfo.nPage))))
        {
            modifyStyle = true;
        }
    }
}

if(modifyStyle)
{
    // ECO_AUTOVSCROLL スタイルを解除する
    richEdit.SetOptions(ECOOP_XOR, ECO_AUTOVSCROLL);
}

// カーソルを最後に持っていく
richEdit.SetSel(0x7ffffffe,0x7fffffff);

// 追加部分の開始位置を保存
long start_start, start_end;
richEdit.GetSel(start_start, start_end);

// メッセージを追加
richEdit.ReplaceSel(message);

// 追加部分の終了位置を保存
long end_start, end_end;
richEdit.GetSel(end_start, end_end);

// RichEdit 用フォントフォーマットを作成
CHARFORMAT2 format;
format.cbSize = sizeof(CHARFORMAT);
format.dwMask = CFM_COLOR | CFM_SIZE | CFM_FACE | CFM_BOLD | CFM_ITALIC;
format.yHeight = font_size;
format.dwEffects = (font_bold ? CFE_BOLD : 0) | (font_italic ? CFE_ITALIC : 0);
strcpy(format.szFaceName, font_name.c_str());
format.crTextColor = font_color;

// 追加文字を選択
richEdit.SetSel(start_start, end_start);

// フォントを設定
richEdit.SetSelectionCharFormat(format);

// 選択部分を元に戻す
if(org_start == org_end)
{
    // 選択範囲がなければカレットを最後に置く
    org_start = 0x7ffffffe;
    org_end = 0x7fffffff;
}
richEdit.SetSel(org_start, org_end);

if(modifyStyle)
{
    // ECO_AUTOVSCROLL スタイルを元に戻す
    richEdit.SetOptions(ECOOP_OR, ECO_AUTOVSCROLL);
}

というところでロビークライアントをアップ。変更点は,

  • 発言エディットボックスの自動水平スクロールを外し,俗に言う右端で折り返すようにする
  • スクロールバーが一番下にない,もしくは選択がある場合は自動スクロールしないようにする
  • ロビーに入るときのメッセージを表示
  • 部屋入室メッセージを指定,表示できるようにする
  • 新しいロビークライアントがあった場合,メッセージが出るようにする
  • ゲームクライアントのバージョンを表示するようにする

ただし,上の2つ以外は明日辺りにロビーサーバを再起動した後有効になる。また,旧ロビークライアントでも接続出来るが,メッセージは表示されない。

ウェザタイ,実行ファイルの削減について。STL を使っていた部分

map.insert(a)
map.insert(b)
map.insert(c)
…

MapInsert(a)
MapInsert(b)
MapInsert(c)
…

のようにラッピングし,インライン展開を抑制することで約 500KB 削減することができた。テーブルのコンパイルだも 30 分から 10 秒程度に減少。STL は使い方次第でコード量をかなり抑えることができるようだ。

ファイルダイアログ。最初のディレクトリがカレントディレクトリの時とそうでないときがある。これは,*.bmp 等のフィルタをかけてある場合,実際に *.bmp のファイルがある場合はカレントディレクトリ,ない場合はそれ以外になるらしい。

CFileDialog::m_ofn::lpstrInitialDir にディレクトリを指定すればそのディレクトリで表示される。

VC。F8 で選択(Shift)をロック,アンロックできることを発見。ってこれはどうでもいいか。

乱数。srand を使って乱数を初期化していたが,いつも同じ系列になる。原因は,乱数を初期化する方と使う方が別のスレッドであったため(だと思う…)。

XML4C を使って XML を出力することを試みる。が,XMLPlatformUtils::Initialize で,「XML4C Panic Error」というエラーで終了してしまう。で,トレースしてみると,エラーコード U_FILE_ACCESS_ERROR というエラーを返している。

結局,Unicode 変換用データベース icudata.dll がない,ということだった。何か,VC のデバッグ用作業ディレクトリに入れといただけじゃダメで実行ファイルのあるディレクトリに置かないと認識してくれなかった。まあいいや。

VC++ MFC で作ったアプリケーション。Form View でコントロールをはると,Ctrl+C などのショートカットを何故か受け付けてくれない。以下のように自分でメッセージをとばして一応解決はした。

void CXXXView::OnEditCopy() 
{
    GetFocus()->PostMessage(WM_COPY);
}

VS に VSS のプロジェクトを挿入するときに,VSS と関連づけてくれない場合がある。その時はプロジェクトの挿入ダイアログの,「ソースコード管理のプロジェクトを開く」で追加すればいい。

はっぱ氏作の Unix/Win 用ソケットラッパーを使う。FTP オープンができずに困ったが,アプリケーションの最初で AfxSocketInit() を行うことで解決。

STL。ある vector に他の型の vector の要素を insert しようする。

vector<A> a;
vector<B> b;
b.insert(b.end(), a.begin(), a.end());

こんな感じだが,VC++ ではコンパイルエラーになった。これは多分メンバ関数テンプレートを完全にサポートしてないからなワケだが,なんとかならないのか。

VC で Resource View を追加する方法。

リソースを追加し,.rc を保存。で,[プロジェクト]-[プロジェクトへ追加]-[ファイル] で .rc と resource.h を追加する。

static なコールバック関数を呼ぶ。が,動作がおかしい。いろいろ調べてみたところ,基底クラスと派生クラスで同じ名前のメソッドを作っていた。

XML4C を使う。この中に XMLString をローカルな文字列に変換する関数,transcode というのがあるのだが,この関数は内部で new した文字列を返す。当然後で delete しなくてはならないのだが,ここで問題が発生。XML4C のライブラリは DLL になっているので,DLL で new したメモリを exe 側で delete しなくてはならず,デバッグ版(だけ?)ではメモリ管理の不整合が起きて assertion されてしまう。

結局こんな感じで無理矢理解決。

DOMString dom_str("aaa");
const XMLCh *xml_ch = dom_str.rawBuffer();
char local_str[255];
XMLString::transcode(xml_ch, local_str, dom_str.length());

STL。vector::back() でコンパイルエラー。これは begin(),end() と違って iterator じゃなく reference を返すのか。

ワーカースレッドからスレッドの作成元の CDialog::UpdateData() を呼び出したりすると Assert が発生する。これは CWnd のポインタが複数スレッドでの共有を許していないからで,PostMessage とかでウィンドウハンドルを使ったメッセージ経由でやりとりすれば大丈夫。

メモリリーク。

AAA *p = NULL;

try
{
    AAA *p = new AAA;

    // 例外を throw する関数呼び出し
    func_with_exception();

    delete p;
}
catch(exception &)
{
    if(p != NULL)
    {
        delete p
    }
}

つまり p というポインタを例外ハンドラで削除しようということだが,p が try ブロックで再定義されている。ので,例外ハンドラで delete しようとする p は当然外の p なので,必ず NULL って感じで。2 日くらい気付かなかった。

VC のデバッガのクイックウォッチ等に独自文字列を入れる方法。

例えば CString では中身の文字列が,CPoint では x と y が表示されるが,これらは実は visual studio ディレクトリ内にある autoexp.dat に指定されている。ヘルプにも載っているが,これを編集すれば自分で作ったクラス等で好きな文字列を表示させられる。

これが java だったら toString メソッドを実行させて表示,とかもっときれいにできるんだろうなぁ。

ダイアログ内でのエディットボックスでの改行。そのままだとリターンが押された時点でデフォルトボタンが押されてしまうので,エディットボックスプロパティの [スタイル]-[改行を許可] をチェック。

ユーザー定義メッセージの作り方

  1. メッセージを必要とするクラスに const static UINT のメッセージ用メンバ変数を用意。
  2. メッセージ番号を ::RegisterWindowMessage(“XXX”); で初期化。
  3. LRESULT OnXXX(WPARAM wParam, LPARAM lParam); を .h のメッセージマップに追加。
  4. ON_REGISTERED_MESSAGE(XXX, OnFilterStart); を .cpp のメッセージマップに追加。

GCC。string::compare だが,

int compare (const basic_string& str, size_type pos, size_type n) const;
int compare (const charT* s, size_type pos, size_type n) const;

の 2 つの挙動が違う。というか後者の場合うまく動かない。これも詳しく調べてないけど前者なら期待通りに動いてるのでまあいいか,とか。

GCC。B 様の調査によると,STL を使う場合,__STL_USE_NAMESPACES を指定した方がよい場合があるらしい。例えば iterator とかを単独で使う場合などに,これがないと iterator が定義されなくなってしまうらしいが…。

1999/09/14「コンソールアプリを,DOS 窓なしで GUI から呼び出す。」だが,この方法だとデータを得られない,という読者さんからのご指摘を頂く。読んでくれた人がいるというだけでもありがたいが,メールまで頂くと非常に嬉しい。

以下,頂いたサンプル。(見やすくするためと簡略化のため,改行や出力先等,多少変更してあります。)

// コマンドライン
LPTSTR lpCommandLine;
lpCommandLine = "xxx.exe";

// 標準出力用パイプ
HANDLE hReadPipe = NULL, hWritePipe = NULL;

// エラー出力用パイプ
HANDLE hErrReadPipe = NULL, hErrWritePipe = NULL;

// セキュリティ属性(ハンドル継承を指定)
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(SECURITY_ATTRIBUTES);
sa.lpSecurityDescriptor = NULL;
sa.bInheritHandle = TRUE;

// 標準出力パイプの作成
::CreatePipe(&hReadPipe, &hWritePipe, &sa, 8192);

// エラー出力パイプの作成
::CreatePipe(&hErrReadPipe, &hErrWritePipe, &sa, 8192);

STARTUPINFO StartupInfo;
PROCESS_INFORMATION ProcessInfo;

::ZeroMemory(&StartupInfo, sizeof(STARTUPINFO));
StartupInfo.cb = sizeof(STARTUPINFO);

// ハンドルの継承を指定
StartupInfo.dwFlags = STARTF_USESTDHANDLES;

// DOS窓を表示しない
StartupInfo.wShowWindow = SW_HIDE;

// 標準出力ハンドルとエラー出力ハンドルを設定
StartupInfo.hStdOutput = hWritePipe;
StartupInfo.hStdError  = hErrWritePipe;

// コンソールアプリ起動
if (::CreateProcess(
    NULL,
    lpCommandLine,
    NULL,
    NULL,
    TRUE,    // ハンドルの継承
    DETACHED_PROCESS, // DOS窓を表示しないための指定
    NULL,
    NULL,
    &StartupInfo, &ProcessInfo))
{
    // パイプ内容受け取り用バッファ
    char bufStdOut[8192], bufErrOut[8192];
    DWORD dwStdOut = 0, dwErrOut = 0;
    DWORD dwRet;

    // プロセス起動中のパイプ内容受け取り処理
    while ( (dwRet = ::WaitForSingleObject(ProcessInfo.hProcess, 0)) !=
        WAIT_ABANDONED)
    {
        memset(bufStdOut, 0, sizeof(bufStdOut));
        memset(bufErrOut, 0, sizeof(bufErrOut));

        // 標準出力パイプの内容を調べる
        ::PeekNamedPipe(hReadPipe, NULL, 0, NULL, &dwStdOut, NULL);
        if (dwStdOut > 0)
        {
             // 内容が存在すれば、読み取る
             ::ReadFile(hReadPipe, bufStdOut, sizeof(bufStdOut) - 1, &dwStdOut,
                NULL);
             // bufStdOut にパイプ出力が入る
        }

        // 同様にエラー出力の処理
        ::PeekNamedPipe(hErrReadPipe, NULL, 0, NULL, &dwErrOut, NULL);
        if (dwErrOut > 0)
        {
             ::ReadFile(hErrReadPipe, bufErrOut, sizeof(bufErrOut) - 1, &dwErrOut,
                NULL);
             // bufErrOut にエラー出力が入る
        }

        // メッセージキューを取得し、存在すれば、処理を促す
        MSG msg;
        if (::PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
        {
             ::TranslateMessage(&msg);
             ::DispatchMessage(&msg);
        }
        // プロセス終了なら、ループを抜ける
        if (dwRet == WAIT_OBJECT_0)
             break;
    }

    // プロセスハンドルとスレッドハンドルを閉じる
    DWORD res;
    ::GetExitCodeProcess(ProcessInfo.hProcess, &res);
    CloseHandle(ProcessInfo.hProcess);
    CloseHandle(ProcessInfo.hThread);
}

// すべてのパイプを閉じる
::CloseHandle(hWritePipe);
::CloseHandle(hReadPipe);
::CloseHandle(hErrWritePipe);
::CloseHandle(hErrReadPipe);

修正個所としては,

  • CreateProcess の bInheritHandles,dwCreationFlags の値。
  • 全体的なエラーチェック。

というところ。

# ちなみに 1999/09/14 では Win98 で動いているのを確認しているので,多分 NT 等では動かなかったのでは…と思う。いずれにしろ上のコードならば大丈夫でしょう。

# 2006/07/10 CreateProcess第3引数にsaを渡さないようにした。将来プロセスハンドルを子に継承させるならsaを渡すのだが,この目的では不要。


その後読者さんとは話がスクロールバーの話題へ。スクロールバーを MFC で使っていると,OnVScroll() の SB_THUMBPOSITION,SB_THUMBTRACK において,nPos が 16bit を越えると負の値となる。また,MFC のヘルプには

nMinPos と nMaxPos で指定された値の差は、32、767以下でなければいけません。

ということが書いてあったので,てっきりスクロールバーは 16bit しか扱えないんだと思っていたが,これは WM_VSCROLL が nPos を 16bit しか渡してくれないから,ということだ。(nPos = (short int) HIWORD(wParam); となっている。)

読者さんの指摘通り,OnVScroll() 内で GetScrollInfo() を直接呼び出し,スクロール位置を得ることで 32bit の値をとることができた。


いや,いつも身内だけでやってるのでこういうのはいいですね。上記以外でも,もしこの日記を読んだ方がいて,何か変だと思うことがあったら是非知らせて欲しいです。

Visual Studio のエディタ。左余白をクリックすると 1 行選択されるが,そのまま Shift を押しながら上の行の左余白をクリックして 1 行選択していくと下にも 1 行ずつ選択範囲が拡張されていく。

ってそんな使い方する人いないか。


ステータスバー。前に使ったことはあるが,日記には書いてなくてどうやるか悩んだのでここに書いておく。

  • String リソースに indicators の ID を追加
  • CMainFrame の indicators に ID を追加
  • View 等に コマンドのハンドラを作成

IME の使用。

void CXXXView::OnImeStartComposition(WPARAM wParam, LPARAM lParam)
{
    COMPOSITIONFORM cpf;
    HIMC hIMC;

    // ウィンドウハンドルを取得
    HWND hWnd = GetSafeHwnd();
    if(!hWnd)
    {
        return;
    }

    // IME のコンテキストを得る
    hIMC = ::ImmGetContext(hWnd);
    if(!hIMC)
    {
        return;
    }

    // CompositionWinow
    cpf.dwStyle = CFS_POINT;
    cpf.ptCurrentPos.x = GetCaretPos().x;
    cpf.ptCurrentPos.y = GetCaretPos().y;

    // フォント
    LOGFONT logFont;
    m_font.GetLogFont(&logFont);

    ::ImmSetCompositionFont(hIMC, &logFont);
    ::ImmSetCompositionWindow(hIMC, &cpf);

    // IME のコンテキストを解放
    ::ImmReleaseContext(hWnd, hIMC);

    // デフォルトプロシージャ
    DefWindowProc(WM_IME_STARTCOMPOSITION, wParam, lParam);
}

void CXXXView::OnImeChar(WPARAM wParam, LPARAM lParam)
{
    // 何もしない
}

void CXXXView::OnImeComposition(WPARAM wParam, LPARAM lParam)
{
    if(lParam & GCS_RESULTSTR)
    {
        COMPOSITIONFORM cpf;
        HIMC hIMC;

        // ウィンドウハンドルを取得
        HWND hWnd = GetSafeHwnd();
        if(!hWnd)
        {
            return;
        }

        // IME のコンテキストを得る
        hIMC = ::ImmGetContext(hWnd);
        if(!hIMC)
        {
            return;
        }

        // 文字列の取得
        int nSize = ::ImmGetCompositionString(hIMC, GCS_RESULTSTR, NULL, 0);
        char *buf = new char[nSize + 1];
        ::ImmGetCompositionString(hIMC, GCS_RESULTSTR, buf, nSize);
        buf[nSize] = '';

        // IME のコンテキストを解放
        ::ImmReleaseContext(hWnd, hIMC);

        // 入力
        Input(buf);
        delete [] buf;

        // ウィンドウの移動
        cpf.dwStyle = CFS_POINT;
        cpf.ptCurrentPos.x = GetCaretPos().x;
        cpf.ptCurrentPos.y = GetCaretPos().y;

        ::ImmSetCompositionWindow(hIMC, &cpf);
    }

    // デフォルトプロシージャ
    DefWindowProc(WM_IME_COMPOSITION, wParam, lParam);
}

ドラッグアンドドロップ。前エクスプローラへのドロップ,ショートカットへのドロップができなかったが,できるようになった。

エクスプローラへのドロップは,DROPFILES にパラメータとファイル名を設定すればよい。(以前うまくいかなったんだが,今回はできた。)

// DROPFILES のサイズを計算
int nSize = sizeof(DROPFILES);
nSize += 1000;  // 適当に

// DROPFILES を作成
HGLOBAL hData = ::GlobalAlloc(GPTR, nSize);
if(hData == NULL)
{
throw cant_cache();
}
DROPFILES *pDropFiles = (DROPFILES *)::GlobalLock(hData);
memset((void *)pDropFiles, NULL, nSize);
pDropFiles->pFiles = sizeof(DROPFILES);
pDropFiles->pt = CPoint(0, 0);
pDropFiles->fNC = FALSE;
pDropFiles->fWide = FALSE;

// ファイル名をセット
char *pFileName = (char *)pDropFiles + sizeof(DROPFILES);
// ( 区切りでコピー)
*pFileName = '';

// キャッシュ
::GlobalUnlock(hData);                                                 
pDataSource->CacheGlobalData(CF_HDROP, hData);

ショートカットへのドロップは Shell IDList Array 形式をサポートすればいいのだが,ちょっと長いし私も完全に理解しているわけではないので手順だけ。

  • デスクトップフォルダの IShellFolder へのポインタを得る
  • ITEMIDLIST を作成。その際最初のデータがフォルダへの ITEMIDLIST で,次のデータからがファイルへの ITEMIDLIST。
  • CIDA を作る。
  • CIDA,ITEMIDLIST をキャッシュ。

CButton にビットマップを貼り付けていたのだが,Windows2000 で見ると背景色がダイアログの背景と違う。Win98 と Win2000 で微妙に色が変わっていた訳だが,結局

CBitmap::LoadBitmap() で CButton に CButton::SetBitmap()

していたのを

CBitmap::LoadMappedBitmap() (引数はデフォルト)

に変えることで背景がきちんと対応するシステムカラーに置き換えられた。

(とりあえず 16 色ビットマップだけしか試してないが。)


CWnd 派生クラスで WM_CHAR が送られてこないで困る。実際には BackSpase とかを押すと警告が鳴る。詳しくは調べてないが,結局 MFC が WM_CHAR をとっているからで,PreTranslateMessage で WM_CHAR を横取ることで解決。

でも昔 CView でやったときは大丈夫だったのだが。

プリコンパイル済みヘッダ。自分で作ったヘッダをプリコンパイル済みヘッダに指定すると,ビルド中に「プリコンパイル済みヘッダー ファイルではありません。」と言われる。

で,StdAfx.h を使う版のものを参考にして,プロジェクト自体の設定も,「プリコンパイル済みヘッダファイル(.pch)を使用」にすることでコンパイルできた。

参考までにプリコンパイル済みヘッダの使用方法。

  • 変更されることが少ないヘッダを選び,そのヘッダを a.h,それをインクルードしてるファイルの代表 1 つを a.cpp とする。
  • プロジェクトの設定で,プロジェクトの [c/c++]-[プリコンパイル済みヘッダ] の設定を「プリコンパイル済みヘッダファイル(.pch)を使用」にし,a.h を指定。
  • a.cpp の設定を「プリコンパイル済みヘッダファイル(.pch)を作成」とし,a.h を指定。
  • a.cpp 以外で a.h をインクルードしている cpp ファイルの設定を「プリコンパイル済みヘッダファイル(.pch)を使用」とし,a.h を指定。

以上で完了し,a.cpp 以外のコンパイルが高速になる。

デバッグのアウトプットに例外が表示される。そこで

  • デバッグ中に [デバッグ]-[例外処理] でダイアログを出す
  • Microsoft C++ Exception を常に停止させる

として例外で停止させる。ここで原因は判明(単純な配列範囲例外)したのだが,その後のコードが

catch(...)

となっていた。結局,せっかく処理系が例外を発生させているのに全ての例外が catch されてしまって分からなくなっていたのだ。やはりめんどうでも exception の派生クラスを定義するなどした方がいいようだ。