プログラミング Tips&メモ
ここではプログラムにおいて役に立つ(かも知れない)小技を紹介しています。
どちらかと言えば自分向けの覚え書きという内容です。


■自在な引数が使えるデバッグ出力処理

OutputDebugString 関数はアプリケーションのデバッガに文字列を出力します。
この関数は LPCTSTR 型の文字列を受け取るだけなので、int 型など数値を表示させたい場合は数値を文字列に変換してから OutputDebugString 関数をコールする必要があります。

数値を文字列に変換させて OutputDebugString 関数をコールする処理を何回も書くのは面倒なので以下の関数が便利です。例えば、

DebugMessage("%s の座標 %d , %d\n", szName, xx, yy);

という風に書けば変数が文字列に変換され、デバッガに出力されます。
//  デバッグ文出力
void DebugMessage(const char *pFormat, ...)
{
    char    szBuff[1024];
    va_list argptr;

    va_start(argptr, pFormat);
    vsprintf(szBuff, pFormat, argptr);
    va_end(argptr);

    OutputDebugString(szBuff);

    return;
}


カンマ区切り文字列はテキストで簡単にデータベース風の情報を扱えるのが利点です。
この便利なカンマ区切りテキスト、作るのは手軽な反面、プログラムで読み込む場合、一文字ずつカンマを探すなど少々面倒です。
任意の文字で分割できる strtok 関数を使う手もありますが、カンマ限定の場合、以下の関数で分割しています。

使い方は、分割したい文字列を引数にして CommaDivide 関数をコールします。2 つ目以降のカンマを区切る場合は引数に NULL を指定します。
この関数の戻り値が区切った文字列の先頭です。もし文字列が最後まで達していた場合は NULL が返ります。

なお、この関数は \0 を書き込みながら分割するため、元の文字列は破壊されるので注意してください。

//  カンマで区切った文字列を返す
//  (カンマ区切り文字列を格納したバッファは破壊されます)
LPSTR CommaDivide(LPSTR lpsz)
{
    static  LPSTR lpStart, lpFin;

    if(lpsz != NULL){
        lpStart = lpsz;
    }
    else{
        lpStart = lpFin + 1;
        lpsz = lpStart;
    }
    if(*lpStart == '\0') return NULL;   //  続きがない時は NULL

    while(*lpsz != '\0'){
        if(*lpsz == ','){
            *lpsz = '\0';
            lpFin = lpsz;
            return lpStart;
        }
        lpsz = CharNext(lpsz);
    }

    lpFin = lpsz - 1;
    return lpStart;
}

//  使う場合はこんな感じ
void Sample(void)
{
    LPSTR to;
    char sz[] = "abcde,fghij,klmno";

    to = CommaDivide(sz);   //  to は abcde を指す
    to = CommaDivide(NULL); //  to は fghij を指す
    to = CommaDivide(NULL); //  to は klmno を指す
    to = CommaDivide(NULL); //  to は NULL

    return;
}


リスト構造から要素を削除してゆく場合、削除と同時にそれを指していたポインタも無効になるため、delete する前にポインタを pNext に覚えさせておきます。
//  リスト構造の全削除
CObject *p, *pNext;

for(p = CObject::GetFirst(); p != NULL; p = pNext){
    pNext = p->GetNext();
    delete p;
}
for 文は引数の中に、左から順に、初期化式、条件式、ループ式を指定します。
初期化式は for 文が実行される際に一度だけ実行され、続いて条件式が評価されます。
ループ式は for 文の statement を実行し終えた時に実行され、その直後に再び条件式が評価されます。

よって上のように p を delete した場合 p は無効になりますが、ループ式( p=pNext )が実行されたあとに条件式( p!=NULL )が評価されるため、アクセス違反は起こりません。


以下の処理は矩形( prc1 )と矩形( prc2 )の重なり合っている部分の矩形を prcResult に返します。
重なり合っていない場合は FALSE を返します。
BOOL GetOverlapedRect(RECT* prc1, RECT* prc2, RECT* prcResult)
{
    //  領域が重なっていない場合は FALSE を返す
    if(prc1->right < prc2->left) return FALSE;
    if(prc1->bottom < prc2->top) return FALSE;
    if(prc2->right < prc1->left) return FALSE;
    if(prc2->bottom < prc1->top) return FALSE;

    //  重なり合う領域を得る
    prcResult->left = (prc1->left <= prc2->left) ? prc2->left : prc1->left;
    prcResult->top = (prc1->top <= prc2->top) ? prc2->top : prc1->top;
    prcResult->right = (prc1->right <= prc2->right) ? prc1->right : prc2->right;
    prcResult->bottom = (prc1->bottom <= prc2->bottom) ? prc1->bottom : prc2->bottom;

    return TRUE;
}


アプリケーションの中にはウィンドウが他のアプリの下に隠れず、常に表示されている物があります。
Windows 付属の電卓やメモ帳に常に手前に表示する機能があれば大変便利だと思うのですが……。

さて、この「常に手前に表示」の処理は
SetWindowPos(hWnd, (HWND)HWND_TOPMOST, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE);
です。これを書けばそのウィンドウ( hWnd )は他のウィンドウの下に隠れることはありません。

その常に手前の状態を元に戻すには
SetWindowPos(hWnd, (HWND)HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE);
となります。

ちなみに、電卓やメモ帳のウィンドウハンドルを得て、そこに向かってこの SetWindowPos 関数を送るとそのウィンドウが常に手前に表示されます。ちょっと強引ですが便利です。


普通、ウィンドウのタイトルバーをドラッグするとウィンドウが移動します。
ではタイトルバーのないアプリケーションやウィンドウのどこをドラッグしてもウィンドウを移動させたい場合はどうしたらよいのでしょうか。

ウィンドウプロシージャの中に
case WM_LBUTTONDOWN:
    PostMessage(hWnd, WM_SYSCOMMAND, SC_MOVE | 0x02, 0L);
    break;
と、書いてみてください。
WM_LBUTTONDOWN メッセージの送られる部分で左ドラッグをしたら時にはこれでウィンドウを移動させることが出来ます。

どこをドラッグしてもウィンドウを移動させたい場合は
case WM_NCHITTEST:
    return HTCAPTION;
と書けば、どこでボタンが押されようとタイトルバーで押されたこと(つまり移動する)になります。
ただし、こちらのルーチンは DefWindowProc 関数と関係するらしいので、ダイアログプロシージャでは動作しないようです。


DrawText 関数などを使用して文字列をセンタリング(中央寄せ)を行おうとすると文字列の幅(ドット数)を得る必要があります。
文字列の幅と高さを返す関数が、GetTextExtentPoint32 関数です。
HDC  hDC;
SIZE size;
char szString[256];

lstrcpy(szString, "文字列文字列");  //  サイズを調べたい文字列

hDC = GetDC(hWnd);
GetTextExtentPoint32(hDC, szString, lstrlen(szString), &size);
ReleaseDC(hWnd, hDC);
このやり方で文字列の幅と高さが SIZE 構造体に格納されます。
この例ではまったくフォントの指定をしていませんが、CreateFont 関数で作成したフォントを SelectObject 関数で結び付けてやれば、そのフォントでの幅と高さを得る事ができます。
ただし、イタリックフォント(斜体文字)を使うと多少のズレが生じるので補正が必要らしいです。


ある文字を指しているポインタ( char* 型 )をインクリメントするとポインタは次の文字を指します。
しかし、この方法では 1 バイト文字、2 バイト文字が混在した文字列では、2 バイト文字が来た時に正しい文字を指す事ができず文字化けを起こしてしまいます。これを回避するのが CharNext 関数です。

引数に文字列へのポインタを渡してやると、CharNext 関数は次の文字のポインタを返します。ポインタの指していた場所に 1 バイト文字があれば 1 バイト進んだアドレスを返し、2 バイト文字があれば 2 バイト進んだアドレスを返してくれます。

それだけといえばそうですが、これはなかなか便利な関数です。

逆に 1 文字手前にポインタを移動するには CharPrev 関数を使います。


ウィンドウ上部にあるタイトルバー。このバーを実行中に非表示にしたり、再び表示させる方法です。
DWORD dwStyle = GetWindowLong(hWnd, GWL_STYLE);  //  現在のスタイルを取得

if(bTitleBar){
    dwStyle |= WS_CAPTION | WS_SYSMENU | WS_BORDER;
}else{
    dwStyle &= ~WS_BORDER;
}
SetWindowLong(hWnd, GWL_STYLE, dwStyle);  //  新しいスタイルを設定

SetWindowPos(hWnd, NULL, 0, 0, 0, 0,
         SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
ShowWindow(hWndMain, SW_SHOW);
このサンプルでは bTitleBar を TRUE にすればバー表示、FALSE にすれば非表示になります。
最後の 2 行は何をしているのでしょうか?実は SetWindowLong 関数でスタイルを変えても画面には反映されません。そこで強制的に SetWindowPos 関数と ShowWindow 関数で再描画しています。


特定のフォルダにあるワイルドカードで指定した条件に合うファイルを列挙してみます。
やり方ですが、まず FindFirstFile 関数で目的のファイルを探します。
もしこの時点で見つからなければ INVALID_HANDLE_VALUE が返ってきます。
ファイルが見つかった場合(戻り値が INVALID_HANDLE_VALUE 以外)は続けて二つ目以降を探します。その場合には FindNextFile 関数を使います。

以下の例では、C ドライブ直下にあるテキストファイル( *.txt )を探します。
HANDLE           hFindFile;
WIN32_FIND_DATA  wfd;

char szSearch[] = "c:\\*.txt";

hFindFile = FindFirstFile(szSearch, &wfd);  //  ひとつめを検索
if(hFindFile != INVALID_HANDLE_VALUE){
    do{

        //  アプリケーション特有の処理を書く...

    }while(FindNextFile(hFindFile, &wfd));  //  二つ目以降を検索
}
FindClose(hFindFile);