CreateProcess で起動した explorer.exe が開いたウインドウの pid または hwnd を知りたい

[戻る]
yasuda 2011/02/05(土) 08:25:37
/* 皆様、こんにちは。

フォルダを開いて、その位置を整えるソフトの製作にトライしています。

CreateProcess をつかって explorer.exe を起動し、
開いたフォルダを EnumWindows を使ってプロセスIDを取得し、
同時に得られた HWND を使って SetWindowPos すればいいのではないか、という方針で
す。

ただ、CreateProcess 実行後の PROCESS_INFORMATION の dwProcessId 値と、
トップレベルウインドウの列挙で得られたプロセスIDの値をデバッガで確認しても
一致するものがなく、困っております。

CreateProcess が起動した先の explorer.exe のプロセスID として考えるならば、
当然、一致はしないとは思うのですが、なにかうまい方法はないでしょうか?


(Win32 コンソールアプリのソースをつけておきます)
*/

#include "stdafx.h"

#include <windows.h> // windows.h をインクルード.


// トップレベルウインドウの素性はこの値まで格納する.
#define MAX_NUM (1024)


// -------------------------------------------
// コールバック関数に渡すパラメータ構造体.

typedef struct tagMY_PRM_FOR_MY_CBK
{
    int counter;
    DWORD arr_prcid[MAX_NUM];
    HWND arr_hwnd[MAX_NUM];
}MY_PRM_FOR_MY_CBK;

// -------------------------------------------

// EnumWindows のコールバック関数.
BOOL CALLBACK my_callback_func_for_enum_windows( HWND hwnd, LPARAM lp )
{
    // この関数の中でつかうパラメータに向かうポインタ.
    MY_PRM_FOR_MY_CBK* p = ( MY_PRM_FOR_MY_CBK* )( lp );

    // コールバック中に使うカウンタ.
    int k = p->counter;

    // 列挙中のウインドウハンドルを配列に格納する.
    p->arr_hwnd[k] = hwnd;

    // 列挙中のウインドウハンドルからプロセスIDを取得して配列に格納する.
    DWORD pid;
    ::GetWindowThreadProcessId( hwnd, &pid );
    p->arr_prcid[k] = pid;

    // カウンタをインクリメントしてパラメータ構造体に戻す.
    k++;
    p->counter = k;

    // TRUE を返して列挙を続ける.
    return TRUE;

}

////// ここからメイン関数 //////////////////////////////////////////////////

int _tmain(int argc, _TCHAR* argv[])
{

    STARTUPINFO si;
    PROCESS_INFORMATION pi;

    ::memset( &si, 0x00, sizeof( si ) );
    ::memset( &pi, 0x00, sizeof( pi ) );
    
    TCHAR cmdline[MAX_PATH];
    ::memset( cmdline, 0x00, sizeof( cmdline ) );
    ::_stprintf( cmdline, _T( "explorer.exe \"c:\\Program Files\"" ) );

    // プロセスを起動する.
    si.cb = sizeof( si );
    BOOL bret = ::CreateProcess( NULL, cmdline, NULL, NULL, FALSE, 0, 
NULL, NULL, &si, &pi );

    if ( bret )
    {
        
        // コメントアウトしました.
        // Sleep( 1234 );

        // プロセスの起動が完了するまで待つ.
        DWORD timeout = INFINITE;
        ::WaitForSingleObject( pi.hProcess, timeout );

        // EnumWindows に与えるパラメータ構造体を初期化する.
        MY_PRM_FOR_MY_CBK prm;
        prm.counter = 0;
        for ( int n = 0; n < MAX_NUM; n++ )
        {
            prm.arr_hwnd[n] = NULL;
            prm.arr_prcid[n] = -1;
        }

        // トップレベルウインドウを列挙する.
        ::EnumWindows( my_callback_func_for_enum_windows, (LPARAM)
( &prm ) );

        // 列挙後に得られた値を評価してみる.
        for ( int n = 0; n < prm.counter; n++ )
        {
            // 列挙したウインドウのプロセスIDを評価する.
            if ( prm.arr_prcid[n] == pi.dwProcessId )
            {
                // 同じものがあったらウインドウをスクリ
ーン左上隅に寄せる.
                int x = 0;
                int y = 0;
                ::SetWindowPos( prm.arr_hwnd[n], NULL, 
x, y, 0, 0, SWP_NOSIZE | SWP_SHOWWINDOW );
                break;
            }
        }

        ::CloseHandle( pi.hProcess ); // 以降プロセスの利用はいらな
いのでClose.
        ::CloseHandle( pi.hThread ); // 以降プロセスの利用はいらな
いのでClose.
    
    }

    return 0;

}

yasuda 2011/02/05(土) 08:31:47
/* すいません、ソースが横に長すぎて改行がおかしくなったので、
もう一度同じものを整形してはります。もうしわけありません。
*/



#include "stdafx.h"

#include <windows.h> // windows.h をインクルード.

// トップレベルウインドウの素性はこの値まで格納する.
#define MAX_NUM (1024)


// -------------------------------------------
// コールバック関数に渡すパラメータ構造体.

typedef struct tagMY_PRM_FOR_MY_CBK
{
    int counter;
    DWORD arr_prcid[MAX_NUM];
    HWND arr_hwnd[MAX_NUM];
}MY_PRM_FOR_MY_CBK;

// -------------------------------------------

// EnumWindows のコールバック関数.
BOOL CALLBACK my_cbk_func_for_enumwnds( HWND hwnd, LPARAM lp )
{
    // この関数の中でつかうパラメータに向かうポインタ.
    MY_PRM_FOR_MY_CBK* p = ( MY_PRM_FOR_MY_CBK* )( lp );

    // コールバック中に使うカウンタ.
    int k = p->counter;

    // 列挙中のウインドウハンドルを配列に格納する.
    p->arr_hwnd[k] = hwnd;

    // 列挙中のウインドウハンドルからプロセスIDを取得して配列に格納する.
    DWORD pid;
    ::GetWindowThreadProcessId( hwnd, &pid );
    p->arr_prcid[k] = pid;

    // カウンタをインクリメントしてパラメータ構造体に戻す.
    k++;
    p->counter = k;

    // TRUE を返して列挙を続ける.
    return TRUE;

}

////// ここからメイン関数 //////////////////////////////////////////////////

int _tmain(int argc, _TCHAR* argv[])
{

    STARTUPINFO si;
    PROCESS_INFORMATION pi;

    ::memset( &si, 0x00, sizeof( si ) );
    ::memset( &pi, 0x00, sizeof( pi ) );
    
    TCHAR cmdline[MAX_PATH];
    ::memset( cmdline, 0x00, sizeof( cmdline ) );
    ::_stprintf( cmdline, _T( "explorer.exe \"c:\\Program Files\"" ) );

    // プロセスを起動する.
    si.cb = sizeof( si );
    BOOL bret = ::CreateProcess(
        NULL,
        cmdline,
        NULL,
        NULL,
        FALSE,
        0,
        NULL,
        NULL,
        &si,
        &pi
        );

    if ( bret )
    {
        
        // コメントアウトしました.
        // Sleep( 1234 );

        // プロセスの起動が完了するまで待つ.
        DWORD timeout = INFINITE;
        ::WaitForSingleObject( pi.hProcess, timeout );

        // EnumWindows に与えるパラメータ構造体を初期化する.
        MY_PRM_FOR_MY_CBK prm;
        prm.counter = 0;
        for ( int n = 0; n < MAX_NUM; n++ )
        {
            prm.arr_hwnd[n] = NULL;
            prm.arr_prcid[n] = -1;
        }

        // トップレベルウインドウを列挙する.
        ::EnumWindows( my_cbk_func_for_enumwnds, (LPARAM)( &prm ) );

        // 列挙後に得られた値を評価してみる.
        for ( int n = 0; n < prm.counter; n++ )
        {
            // 列挙したウインドウのプロセスIDを評価する.
            if ( prm.arr_prcid[n] == pi.dwProcessId )
            {
                // ウインドウをスクリーン左上隅に寄せる.
                int x = 0;
                int y = 0;
                ::SetWindowPos(
                    prm.arr_hwnd[n],
                    NULL,
                    x,
                    y,
                    0,
                    0,
                    SWP_NOSIZE | SWP_SHOWWINDOW
                    );

                break;
            }
        }

        // 以降プロセスの利用はいらないのでClose.
        ::CloseHandle( pi.hProcess );
        ::CloseHandle( pi.hThread );
    
    }

    return 0;

}

aetos 2011/02/07(月) 10:58:42
explorer.exe は(設定にもよりますが)CreateProcess で新しく立ち上げたプロセス
は、既存のプロセスに仕事を任せてとっとと終了してしまいます。
フォルダーを開いているのは既存のプロセスになるので、プロセス ID が一致しないの
です。

こういう方法でウィンドウを探す必要があると思います。
http://eternalwindows.jp/shell/shellname/shellname07.html

念のためキーワードをば。
IShellWindows::FindWindowSW です。

あと、

> // プロセスの起動が完了するまで待つ.
> DWORD timeout = INFINITE;
> ::WaitForSingleObject( pi.hProcess, timeout );

これはプロセスの終了まで待つコードです。
前述のように explorer.exe の新しいプロセスはすぐ終了してしまうので、以降のコー
ドが走っていますが、他のプロセスでこれをやったら、そこで止まります。

Ban 2011/02/08(火) 01:04:27
> // プロセスの起動が完了するまで待つ.

これとの混同でしょうか。
WaitForInputIdle
http://msdn.microsoft.com/ja-jp/library/cc429415.aspx

yasuda 2011/02/09(水) 22:45:55
aetosさん、Banさん、ご返事ありがとうございます。

aetosさんに、ご紹介いただいたウェブページを見て調べているのですが、
私はSH系のオペレーションは SHBrowseForFolder ぐらいしか使ったことがなく、
だいぶ苦戦しておりまして、うまいソースができあがりません。

ご紹介のページのサンプルは、デスクトップなどの Special な Folder を
つかっているので、私のやりたい動作の寸前というところです。

C:\hoge\moge\ など絶対パス情報から、それにまつわる ITEMIDLIST を得られれば、
うまくいくとは思うのですが、その部分の調査がうまくいっておらず、
なかなかご返事をすることができません。

質問をしておいて、ずっとダンマリというわけにもいきませんので、
とりあえず書き込ませていただきました。

Banさん
そうですね、私のしたいことは、どうやら WaitForInputIdle ですね。
ご指摘、感謝いたします。

aetos 2011/02/10(木) 16:02:02
> C:\hoge\moge\ など絶対パス情報から、それにまつわる ITEMIDLIST を得られれば、
> うまくいくとは思うのですが、その部分の調査がうまくいっておらず、
> なかなかご返事をすることができません。

パスから IDList を得るには、IShellFolder::ParseDisplayName とか、
SHParseDisplayName とか、ILCreateFromPath などの API が使えます。

が、申し訳ないですが、俺にはうまく動くサンプルが作れませんでした。

yasuda 2011/02/11(金) 11:15:31
こんにちは。

ご紹介のURLやMSDNをいろいろ調べて
下記のようなソースコードを記述してみたのですが、
いったんシェルがデスクトップのフォルダを開いてから、
所望のフォルダに遷移してウインドウハンドルを取得するようなコードになってしまい
ました。

シェルの関係の手続きをしたり、デスクトップを開いたりして、
いろいろやってから、ようやく ParseDisplayName の引数が準備できるようで、
なんだかまどろっこしいコードになっています。

結果的にはフォルダを任意の位置に移動できるという所望の動作なのですが、
一瞬、ぜんぜん違うフォルダがチラッと見えてしまうのが気に入りません。

識者の皆様方、なんらかのご助言をいただけませんでしょうか。

なお、投稿文字数の関係で、2つに分けて投稿させていただきます。
皆様、管理人様、まことにもうしわけありません。

// ------------------------------------------------------
// VC++ 2010 Express コンソールアプリ

// ユニコードビルド.

#include "stdafx.h"

#include <windows.h>
#include <shlobj.h>
#include <shlwapi.h>
#pragma comment( lib, "shlwapi.lib" )


// 目的のフォルダ.
#define MY_TARGET_PATH "c:\\tmp\\xyz"

// 終了のためのキー入力待ち関数.
int my_press_enterkey_wait( void )
{
    ::_tprintf( _T( "press [Enter] key to exit > " ) );
    TCHAR str[BUFSIZ];
    ::memset( str, 0x00, sizeof( str ) );
    ::_fgetts( str, BUFSIZ, stdin );
    return 0;
}

////////// ここからメイン ////////////////////////////////

int _tmain(int argc, _TCHAR* argv[])
{

    HRESULT hr;

    IDispatch* dsp;

    IShellWindows* sh_windows;
    IShellBrowser* sh_browser;

    LPITEMIDLIST idlist_target;
    LPITEMIDLIST idlist_desktop;

    // COM開始の必須コード.
    hr = ::CoInitialize( NULL );
    if ( hr != S_OK )
    {
        ::_tprintf( _T( "error: CoInitialize().\n" ) );
        
        ::my_press_enterkey_wait();
        return -1;
    }

    idlist_desktop = (LPITEMIDLIST)::CoTaskMemAlloc( sizeof( 
ITEMIDLIST ) );
    idlist_target = (LPITEMIDLIST)::CoTaskMemAlloc( sizeof( 
ITEMIDLIST ) );

    ::SHGetSpecialFolderLocation( NULL, CSIDL_DESKTOP, &idlist_desktop );
    ::SHOpenFolderAndSelectItems( idlist_desktop, 0, NULL, 0 );

    hr = ::CoCreateInstance(
            CLSID_ShellWindows,
            NULL,
            CLSCTX_ALL,
            IID_PPV_ARGS( &sh_windows )
            );

    if ( hr != S_OK )
    {
        // COM終了の必須コード.
        ::CoUninitialize();

        ::my_press_enterkey_wait();
        return -1;
    }
    else
    {

        // IShellWindows がうまく作られたときはこちら.

        HWND hwnd;

        VARIANT var;
        var.vt = VT_BYREF | VT_VARIANT;
        var.pvarVal = (VARIANT*)( idlist_desktop );

        VARIANT var_root;
        ::VariantInit( &var_root );

        hr = sh_windows->FindWindowSW(
                            &var,
                            &var_root,
                            SWC_BROWSER,
                            (LONG*)( 
&hwnd ),
                        
    SWFO_NEEDDISPATCH,
                            &dsp
                            );

        if ( hr != S_OK )
        {
            ::_tprintf( _T( "" ) );

            ::CoTaskMemFree( idlist_desktop );
            ::CoTaskMemFree( idlist_target );
            sh_windows->Release();
            ::CoUninitialize();

            ::my_press_enterkey_wait();
            return -1;
        }
        else
        {
            
            if ( hwnd != INVALID_HANDLE_VALUE )
            {
                // とりあえずウインドウを移動する.
                int swp_x = 256;
                int swp_y = 256;
                UINT flags_swp = SWP_NOSIZE | 
SWP_SHOWWINDOW;
                ::SetWindowPos(
                    hwnd,
                    NULL,
                    swp_x,
                    swp_y,
                    0,
                    0,
                    flags_swp
                    );

            }

        }
    
    }
    
    hr = ::IUnknown_QueryService(
                dsp,
                SID_STopLevelBrowser,
                IID_PPV_ARGS( &sh_browser )
                );

    if ( hr != S_OK )
    {
        ::_tprintf( _T( "error: IUnKnown_QueryService().\n" ) );
        
        ::CoTaskMemFree( idlist_desktop );
        ::CoTaskMemFree( idlist_target );
        dsp->Release();
        sh_windows->Release();
        ::CoUninitialize();

        ::my_press_enterkey_wait();
        return -1;
    }
    
    // ----------------------------------
    // デバッグ用フォルダ切り替え時間.
    ::Sleep( 1234 );
    // ----------------------------------

yasuda 2011/02/11(金) 11:16:03
    // ----------------------------------
    // デバッグ用フォルダ切り替え時間.
    ::Sleep( 1234 );
    // ----------------------------------

    LPSHELLFOLDER desktop_folder;
    hr = ::SHGetDesktopFolder( &desktop_folder );
    if ( hr == S_OK )
    {

        // ユニコードでビルド(第3引数は必ずLPWSTR).
        // アイテムIDリストの取得.

        ULONG eaten;
        ULONG attr;
        hr = desktop_folder->ParseDisplayName(
                                NULL,
                                NULL,
                                _T( 
MY_TARGET_PATH ), // LPWSTR.
                            
    &eaten,
                            
    &idlist_target, // 取得.
                                &attr
                                );
    
        if ( hr == S_OK )
        {

            hr = sh_browser->BrowseObject(
                            
    idlist_target,
                            
    SBSP_SAMEBROWSER | SBSP_ABSOLUTE
                                );

            if ( hr == S_OK )
            {

                // 開いたフォルダのハンドルを取得する.
                HWND hwnd;
                sh_browser->GetWindow( &hwnd );

                // ウインドウをスクリーン左上隅に移動する.
                int swp_x = 0;
                int swp_y = 0;
                UINT flags_swp = SWP_NOSIZE | 
SWP_SHOWWINDOW;
                ::SetWindowPos(
                    hwnd,
                    NULL,
                    swp_x,
                    swp_y,
                    0,
                    0,
                    flags_swp
                    );

            }
            else
            {
                ::_tprintf( _T( "error: BrowseObject
().\n" ) );
            }
        }
        else
        {
            ::_tprintf( _T( "error: ParseDisplayName().\n" ) );
        }
    }
    else
    {
        ::_tprintf( _T( "error: SHGetDesktopFolder().\n" ) );
    }

    ::CoTaskMemFree( idlist_desktop );
    ::CoTaskMemFree( idlist_target );

    dsp->Release();
    sh_windows->Release();
    sh_browser->Release();

    // COM終了の必須コード.
    ::CoUninitialize();

    // めでたくコードの最後まできたらコンソール出力してキー入力待ち.
    ::_tprintf( _T( "finish.\n" ) );
    ::my_press_enterkey_wait();


    return 0;

}

yasuda 2011/02/11(金) 11:26:11
ガーン、改行が....すいません。

ソースはりつけたバチがあたってしまいました。

aetos 2011/02/14(月) 12:29:10
> idlist_desktop = (LPITEMIDLIST)::CoTaskMemAlloc(sizeof(ITEMIDLIST));
> idlist_target = (LPITEMIDLIST)::CoTaskMemAlloc(sizeof(ITEMIDLIST));

これはやっちゃダメです。メモリリークします。

あと、ウィンドウの移動は2回やる必要はないと思います。

IShellFolder::ParseDisplayName は SHParseDisplayName とか ILCreateFromPath で置
き換えると IShellFolder * を扱う必要がなくて楽ちんです。

で、根本の話。

FindWindowSW の第一引数に直接 idlist_target を格納した VARIANT を渡して失敗する
原因が全く分かりません。
お手上げです。すいません。

subaru 2011/02/14(月) 16:54:52
IShellWindowsからブラウザを探す方向で話が進んでいますが、
新しくエクスプローラーを開くのであればCLSID_InternetExplorerで作成して
フォルダ移動や位置変更はIWebBrowser2インターフェースで操作した方が
手っ取り早いんじゃないかと思います。

gak 2011/02/14(月) 18:00:03
> IShellWindowsからブラウザを探す方向で話が進んでいますが
こっちの方向で行く場合の一例。

xxxxxxxx {
    CComPtr<IShellWindows> sw;
    sw.CoCreateInstance(CLSID_ShellWindows);

    ITEMIDLIST* pidl = ::ILCreateFromPath(_T("C:\\Windows\\System32"));
    ::SHOpenFolderAndSelectItems(pidl, 0, NULL, 0);

    // SHOpenFolderAndSelectItems() で「c:\windows\」を開いて「system32」を選択
している状態(仕様)なので「system32」を切る
    ::ILRemoveLastID(pidl);

    COleVariant loc(pidl); // VT_ARRAY | VT_UI1 型のデータ作成(MFC 使いたく無い
場合は自力で頑張って作る)
    VARIANT var;
    var.vt = VT_BYREF | VT_VARIANT;
    var.pvarVal = loc;
    VARIANT root;
    ::VariantInit(&root);
    HWND wnd;
    CComPtr<IDispatch> out;
    if (sw->FindWindowSW(&var, &root, SWC_BROWSER | SWC_EXPLORER,
reinterpret_cast<long*>(&wnd), SWFO_NEEDDISPATCH, &out) == S_OK) {
        ::SetWindowPos(::GetAncestor(wnd, GA_ROOT), NULL, 100, 100, 200, 200,
SWP_NOZORDER | SWP_NOACTIVATE);
    }
    ::ILFree(pidl);
}

> http://eternalwindows.jp/shell/shellname/shellname07.html
FindWindowSW() の引数の渡し方が間違っているかも。
というか、VT_BYREF | VT_VARIANT 指定でデータが渡されさえすれば、中身がおかしい場
合でもエラーにせず「desktop」を探しに行っちゃう FindWindowSW() の動作?のせいで
気付かなかったのかもしれないが。

gak 2011/02/14(月) 18:11:02
というか、VT_BYREF | VT_VARIANT 指定で渡さなくても動いたな…良くわからん。

xxxxxxxx {
      :
    COleVariant loc(pidl); // VT_ARRAY | VT_UI1 型のデータ作成(MFC 使いたく無い
場合は自力で頑張って作る)
    VARIANT root;
    ::VariantInit(&root);
    HWND wnd;
    CComPtr<IDispatch> out;
    if (sw->FindWindowSW(loc, &root, SWC_BROWSER | SWC_EXPLORER,
reinterpret_cast<long*>(&wnd), SWFO_NEEDDISPATCH, &out) == S_OK) {
      :
}

yasuda 2011/02/17(木) 20:33:41
aetosさん、subaruさん、gakさん、ご回答ありがとうございます。

いろいろお知らせいただいているのですが、
確実にわたくしの能力からはオーバーフローしています。

ただ、COMを扱う上でよくわからないのが、
どれがメモリ確保しなくてはいけなくて、
どれがポインタの宣言だけでよくて、みたいなところです。

aetos さん、ご指摘の
idlist_desktop = (LPITEMIDLIST)::CoTaskMemAlloc(sizeof(ITEMIDLIST));
idlist_target = (LPITEMIDLIST)::CoTaskMemAlloc(sizeof(ITEMIDLIST));
はリークしますよ、というのはまさにこのことです。

絶対パスから作ったID類を入れてもらうんだから、
アイテムIDリストの器を用意しとかないといけないよな、
みたいな思考だったんです。

アプリ終了時(_DEBUGビルド)において、
VisualStudio の出力ウインドウで MemoryLeak の Dump を吐かなかったので、
リークしてないと自分勝手に判断していたのですが、COM はよくわかりません〜。

もう、ギブアップで解決のチェックを入れる寸前です。

subaru 2011/02/18(金) 11:06:39
>ただ、COMを扱う上でよくわからないのが、
>どれがメモリ確保しなくてはいけなくて、
>どれがポインタの宣言だけでよくて、みたいなところです。
>
>aetos さん、ご指摘の
>idlist_desktop = (LPITEMIDLIST)::CoTaskMemAlloc(sizeof(ITEMIDLIST));
>idlist_target = (LPITEMIDLIST)::CoTaskMemAlloc(sizeof(ITEMIDLIST));
>はリークしますよ、というのはまさにこのことです。

SHGetSpecialFolderLocation の関数内でメモリ確保が行われるので
idlist_desktopにあらかじめCoTaskMemAllocしておいても上書きされてしまいます。
自分でCoTaskMemAllocする必要があるのであれば
SHGetSpecialFolderLocationの第3引数はITEMIDLISTのポインタのポインタではなく
ITEMIDLISTのポインタになっているでしょう。
(実際にCoTaskMemAllocで確保するサイズは複数のITEMIDLIST分必要なので事前にはわか
りません)

ちなみにIWebBrowser2を使う一例を手抜きで書くとこんな感じになります。
#include <tchar.h>
#include <windows.h>
#include <shlobj.h>
#include <comdef.h>

int _tmain()
{
    CoInitialize(NULL);
    {
        IWebBrowser2Ptr wb;
        wb.CreateInstance(CLSID_InternetExplorer);
        wb->put_Visible(-1);

        //C:\\Windows\\System32に移動
        variant_t url(L"C:\\Windows\\System32");
        variant_t v;
        wb->Navigate2(&url, &v, &v, &v, &v);

        //ウインドウをデスクトップの左上に移動
        wb->put_Left(0);
        wb->put_Top(0);
    }
    CoUninitialize();
    return 0;
}

yasuda 2011/02/20(日) 23:10:21
[[解決]]
subaruさん、ありがとうございます。
この週末で少し試してみました。

wb->put_Visible(-1); だと IE がチラっと見えてしまうので、
wb->put_Visible( VARIANT_FALSE ); のほうがいいみたいです。

このコード効かないんですよね。
wb->put_Left(0);
wb->put_Top(0);
なぜか...

COMのメモリのこともお教えいただきありがとうございます。
いわれてみると CreateDIBSection() のように関数にメモリをとらせて、
そこでもらったポインタを操作する場合は、void** みたいに
2重ポインタでわたしますね。

explorer.exe の起動時オプションが
twmなみに用意されていればいいのになと思ってしまいました。

どうも皆様ありがとうございました。

古いスレッドには追加発言できません。