C++BuilderからWMIを呼び出す

概説

Microsoft Windowsには、システムの情報を管理するためにWMI(Windows Management Instrumentation)というものがあり、システム構成やログ等をWMIで取得することができます。
ネットのサンプルではVBやWSH(Windows Script Host)が多いのですが、C++BuilderからWMIを呼び出すこともできます。
方法はいくつかあると思いますが、ここでは次の4例を挙げます。

  1. C++Builderのコンポーネントとしてインポート
  2. Variantを使う
  3. WindowsのAPIを使う
  4. WSHをインポートし、スクリプトを動かす

wbemtest.exe

プログラムの例を挙げる前に、WMIの情報を目で見ることのできるツールを紹介します。
Windowsには、WMIをGUIで操作できるアプリケーションとして「wbemtest.exe」があります。Windowsのメニューから[スタート]→[ファイル名を指定して実行] で起動します。このアプリでどんな名前でどんな情報が得れるのかインタラクティブに調べることができます。詳しくは以下のマイクロソフトのサイトを御覧下さい。
「WBEMとは」 - http://technet.microsoft.com/ja-jp/scriptcenter/ff575750

wbemtest



サンプルプログラムの構成

サンプルプログラムは以下のように1つのフォームとその上に4つのボタンと1つの RichEdit を配置したものです。

サンプルプログラムの外観


WMIをC++Builderのコンポーネントとしてインポート

C++BuilderからWMIを呼び出す方法のひとつは、WMIをC++Builderのコンポーネントとしてインポートして呼び出す方法です。サンプルプログラムは次のようになります。

void __fastcall TForm1::Button1Click(TObject *Sender)
{
// インポートしたコンポーネントを使う
AnsiString s;
ISWbemServices *srv;
ISWbemObjectSet *objset;
VARIANT vi;

TSWbemLocator *obj = new TSWbemLocator(this);
srv = obj->ConnectServer(
L".", //BSTR strServer/*[in,def,opt]*/,
L"root\\cimv2", //BSTR strNamespace/*[in,def,opt]*/,
L"", //BSTR strUser/*[in,def,opt]*/,
L"", //BSTR strPassword/*[in,def,opt]*/,
L"", //BSTR strLocale/*[in,def,opt]*/,
L"", //BSTR strAuthority/*[in,def,opt]*/,
0, //long iSecurityFlags/*[in,def,opt]*/,
NULL //LPDISPATCH objWbemNamedValueSet/*[in,def,opt]*/
);
objset = srv->ExecQuery(
L"SELECT * FROM Win32_DiskDrive", // strQuery
L"wql", // strQueryLanguage
0, // iFlags
NULL // objWbemNamedValueSet
);
if (objset != NULL) {
long n = objset->Count;
for (int i = 0; i < n; i++) {
VariantInit(&vi);
ISWbemObject *wo = objset->ItemIndex(i);
ISWbemProperty *wp = wo->Properties_->Item(L"Caption", 0);
wp->get_Value(&vi);
if (vi.vt == VT_BSTR) {
RichEdit1->Lines->Add(AnsiString(vi.bstrVal));
}
VariantClear(&vi);
}
}

delete obj;
}

これに先立って、WMIをC++Builderにインポートしておきます。
インポートはC++Builderのメニューから[コンポーネント] → [コンポーネントのインストール] を選ぶと表示される「コンポーネントのインポート」ダイアログ・ウィンドウで次のように操作します。

  1. 「タイプライブラリの取り込み」を選択
  2. 「Microsoft WMI Scriptiong V.xx Library」を選択
  3. ユニットディレクトリ名を適当(プログラムを作成するディレクトリ)に指定
  4. 「現在のプロジェクトにユニットを追加」を選択
  5. 「完了」ボタンをクリック

以上の操作で WbemScripting_OCX.[cpp,h] と WbemScripting_TLB.[cpp,h] が作成されるので、プログラムでは WbemScripting_OCX.h をインクルードします。

使い方は、ヘッダーファイルを見てトライ&エラーで行いました。良いマニュアルを知っていたら教えて下さい。



Variantを使う

色々な型の値を保持できるクラスとしてVariantクラスがあります。Variantは、COM(IDispatch*)オブジェクトも保持でき、Variantな変数にCOMオブジェクトを代入し、OleFunction や OleProperGet 等のメソッドをコールして関数の実行やプロパティの取得ができます。サンプルプログラムは次のようになります。

void __fastcall TForm1::Button2Click(TObject *Sender)
{
// Variant を使う
AnsiString s;
Variant vX, vY, vZ, vi;

vX = Variant::CreateObject("WbemScripting.SWbemLocator");
vY = vX.OleFunction("ConnectServer");
vZ = vY.OleFunction("ExecQuery", "SELECT * FROM Win32_DiskDrive");
int n = vZ.OlePropertyGet("Count");
s.SetLength(0);
for (int i = 0; i < n; i++) {
vi = vZ.OleFunction<int>("ItemIndex", i);
s = vi.OlePropertyGet("Caption");
RichEdit1->Lines->Add(s);
}
}

このプログラムを動かすだけならWMIをコンポーネントとしてインポートする必要はありません。
前のサンプルで使った全部大文字のVARIANTはWindowsで定義されたもので、ここで出てくるのは頭が大文字のVariantでC++Builderで定義されたものです。
Variantのヘルプを読んでも、こういう使い方は想像できませんが、皆様はどうですか?
動きを理解するためにはデバッガーでステップ実行するのが良い方法です。Variantのソースであるvariant.cppをコピーして、プロジェクトに追加してコンパイルするとVariantのメソッドの中まで追うことができます(試される時は自己責任で)。


WindowsのAPIを使う

次はWindowsのAPIを使った例です。エラー処理を入れると行数が増え見難くなるので省いてあります。エラー処理を含んだ例はこちらを御覧下さい。
プログラムのステップ数は多いのですが、やっていることは、元になるインスタンスを作成し、ConnectServer、ExecQury、メソッドの実行です。メソッドの実行時に、名前からディスパッチIDを取得し、VARIANT型で引数を用意し、Invoke で実行する、という手順を踏むので行数が多くなります。

void __fastcall TForm1::Button3Click(TObject *Sender)
{
// Windows の API

HRESULT hr;
VARIANT var;
VARIANT varResult;
DISPPARAMS dispParams;

CoInitialize(NULL);

// クラスIDを得る
CLSID clsid;
hr = CLSIDFromProgID(L"WbemScripting.SWbemLocator", &clsid);
if (FAILED(hr)) {
CoUninitialize();
RichEdit1->Lines->Add(ResultString(hr));
return;
}

// インスタンス作成
IDispatch *pLocator;
hr = CoCreateInstance(
clsid, // REFCLSID rclsid
NULL, // LPUNKNOWN pUnkOuter
CLSCTX_INPROC_SERVER, // DWORD dwClsContext
__uuidof(IDispatch), // __uuidof(変数名) はエラー
(void **)&pLocator
);
DISPID dispid;
OLECHAR *pNames[1];

// vY = vX.OleFunction("ConnectServer");

// "ConnectServer" のディスパッチIDを得る
pNames[0] = L"ConnectServer";
hr = pLocator->GetIDsOfNames(
IID_NULL, // REFIID riid ([in]Reserved)
pNames, // OLECHAR FAR* FAR* rgszNames,
1,
LOCALE_USER_DEFAULT,
&dispid
);
// 引数を用意
dispParams.cArgs = 0;
dispParams.rgvarg = NULL;
dispParams.cNamedArgs = 0;
dispParams.rgdispidNamedArgs = NULL;
VariantInit(&varResult);

// ConnectServer を実行
hr = pLocator->Invoke(
dispid, // dispatch ID
IID_NULL, // Reserved
LOCALE_SYSTEM_DEFAULT, // locale context
DISPATCH_METHOD, // _PROPERTYGET, _PROPERTYPUT, _PROPERTYREF
&dispParams, // pDispParams
&varResult, // result
NULL, // exception information
NULL // puArgErr
);
IDispatch *pServer = varResult.pdispVal;
// VariantClear(&varResult) をコールすると次の pServer->GetIdsOfNames() がエラーになる
// VariantClear() 内で pServer->Release() がコールされているのか?

// vZ = vY.OleFunction("ExecQuery", "SELECT * FROM Win32_DiskDrive");
pNames[0] = L"ExecQuery";
hr = pServer->GetIDsOfNames(
IID_NULL, // REFIID riid ([in]Reserved)
pNames, // OLECHAR FAR* FAR* rgszNames,
1,
LOCALE_USER_DEFAULT,
&dispid
);
var.vt = VT_BSTR;
var.bstrVal = SysAllocString(L"SELECT * FROM Win32_DiskDrive");
dispParams.cArgs = 1;
dispParams.rgvarg = &var;
dispParams.cNamedArgs = 0;
dispParams.rgdispidNamedArgs = NULL;
VariantInit(&varResult);
hr = pServer->Invoke(
dispid, // dispatch ID
IID_NULL, // Reserved
LOCALE_SYSTEM_DEFAULT, // locale context
DISPATCH_METHOD, // _PROPERTYGET, _PROPERTYPUT, _PROPERTYREF
&dispParams, // pDispParams
&varResult, // result
NULL, // exception information
NULL // puArgErr
);
SysFreeString(var.bstrVal);
IDispatch *pItems = varResult.pdispVal;
//VariantClear(&varResult);

pNames[0] = L"Count";
hr = pItems->GetIDsOfNames(
IID_NULL, // REFIID riid ([in]Reserved)
pNames, // OLECHAR FAR* FAR* rgszNames,
1,
LOCALE_USER_DEFAULT,
&dispid
);
dispParams.cArgs = 0;
dispParams.rgvarg = NULL;
dispParams.cNamedArgs = 0;
dispParams.rgdispidNamedArgs = NULL;
VariantInit(&varResult);
hr = pItems->Invoke(
dispid, // dispatch ID
IID_NULL, // Reserved
LOCALE_SYSTEM_DEFAULT, // locale context
DISPATCH_PROPERTYGET, // _PROPERTYGET, _PROPERTYPUT, _PROPERTYREF
&dispParams, // pDispParams
&varResult, // result
NULL, // exception information
NULL // puArgErr
);
long count = varResult.lVal;
VariantClear(&varResult);

pNames[0] = L"ItemIndex";
hr = pItems->GetIDsOfNames(
IID_NULL, // REFIID riid ([in]Reserved)
pNames, // OLECHAR FAR* FAR* rgszNames,
1,
LOCALE_USER_DEFAULT,
&dispid
);
for (long i = 0; i < count; i++) {
var.vt = VT_I4;
var.lVal = i;
dispParams.cArgs = 1;
dispParams.rgvarg = &var;
dispParams.cNamedArgs = 0;
dispParams.rgdispidNamedArgs = NULL;
VariantInit(&varResult);
hr = pItems->Invoke(dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT,
DISPATCH_METHOD, &dispParams, &varResult, NULL, NULL);
IDispatch *pOne = varResult.pdispVal;
//VariantClear(&varResult);

DISPID diCaption;
pNames[0] = L"Caption";
hr = pOne->GetIDsOfNames(IID_NULL,
pNames, 1, LOCALE_USER_DEFAULT, &diCaption);
dispParams.cArgs = 0;
dispParams.rgvarg = NULL;
dispParams.cNamedArgs = 0;
dispParams.rgdispidNamedArgs = NULL;
VariantInit(&varResult);
hr = pOne->Invoke(diCaption, IID_NULL, LOCALE_SYSTEM_DEFAULT,
DISPATCH_PROPERTYGET, &dispParams, &varResult, NULL, NULL);
if (SUCCEEDED(hr)) {
if (varResult.vt == VT_BSTR) {
RichEdit1->Lines->Add(AnsiString(varResult.bstrVal));
}
} else {
RichEdit1->Lines->Add(ResultString(hr));
}
VariantClear(&varResult);

pOne->Release();
}

pItems->Release();
pServer->Release();
pLocator->Release();
CoUninitialize();
}


WSHをインポートし、スクリプトを動かす

WSHをC++Builderのコンポーネントとしてインポートし、VBSでWMIを呼び出す方法もあります。VBSのサンプルはネットで沢山見つかるので、それらを参考にできる利点があります。
難しいのはスクリプトの実行結果をC++Builderに返す方法かと思います。このサンプルではスクリプトでの処理を関数にし、関数の戻り値として結果を返すようにしました。

void __fastcall TForm1::Button4Click(TObject *Sender)
{
// wsh を使う
static char *f_x =
"Function f_x()\n"
"rtn = \"\"\n"
"Set WMI = GetObject(\"winmgmts:\")\n"
"Set ObjSet = WMI.ExecQuery(\"Select * From Win32_DiskDrive\")\n"
"For Each Item In ObjSet\n"
"rtn = rtn & Item.Caption & vbCrLf\n"
"Next\n"
"f_x = rtn\n"
"End Function";
AnsiString s;

TScriptControl *scri = new TScriptControl(this);
scri->Language = L"VBScript";

scri->AddCode(WideString(f_x).c_bstr());
s = VarToStr(scri->Eval(WideString("f_x()").c_bstr()));

delete scri;
RichEdit1->Lines->Add(s);
}

WSHのインポートは、WMIの時と同様でタイプライブラリは「Microsoft Script Control x.x」を選択します。
プログラムからは MSScriptControl_OCX.h をインクルードします。


ソースファイル

サンプルプログラムはC++Builder 2007で作成しました。サンプルプログラムのソースを以下のファイルにまとめました。

TryWMI.zip


プロジェクト名は TryA.cbproj です。
WMIとWSHのコンポーネントは含まれていませんので、そのままコンパイルするとエラーになります。TryA.cbproj を開いてから、上で説明した手順でコンポーネントをインストールして下さい。

ご意見、ご要望、バグ、等ございましたらメールでご連絡ください。