HTAからWScriptへアクセスしてみた2007年06月05日 20時15分12秒

とりあえずのテスト

6/1のエントリで紹介した、「Windows Script Programming」さんのWindowsアプリからWScript.exeのWScriptオブジェクトを利用するをとりあえず試してみた。

元々頭の中にあったのが「WScriptオブジェクトをHTAに渡せば、HTAからグローバルメンバにアクセスできるぜ。へへへ」てなことだったのだが、なんかイマイチうまくいかなかった。

よくよく考えてみると、単体のJSファイルでも

WScript == this
falseになるので無理だったのだが、それがわかるまで結構ハマった。

なので、IEを経由させるのはWScriptではなくthisにするようにしてみた。

他にも型のチェックとかで元のコードを単純に移行しても動かなかったのと、もらったWScriptからWshShellを作成してPopup叩いてもイマイチ面白くないので、HTAからWScript.EchoとWScript.StdIn.ReadLineを試してみるようにした。

で、作ってみたコード

まずjs側。名前を「wsh.js」とする。

function echo(s) {
	WScript.Echo( s );
}

function readln(msg) {
	if(msg) WScript.StdOut.Write(msg);
	return WScript.StdIn.ReadLine();
}
// 終了待ち合わせフラグ
var quit = false;

var sh = new ActiveXObject("WScript.Shell");
var shell = new ActiveXObject("Shell.Application"), ie;

for(var col = new Enumerator( shell.Windows() ); ! col.atEnd(); col.moveNext()) {
	if( col.item().hWnd == WScript.Arguments.Item(0) ) {
		ie = col.item();
		break;
	}
}

ie.PutProperty( "WScript", this );
// HTAからquitにtrueがセットされるまで待ち合わせ
while( ! quit ) {
	WScript.Sleep(1000);
}

sh.Popup( "completed." );

そんでこっちがHTA側。名前はなんでもいいけど「test.hta」とかにしておく。

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=Shift_JIS">
<meta http-equiv="Content-Script-Type" content="text/javascript">
<title>test</title>
</head>
<body>

</body>
<script>
var ie;
window.onbeforeunload = function() {
	ie.Quit();
}
setTimeout( function() {
	ie = new ActiveXObject("InternetExplorer.Application");
	var shell = new ActiveXObject("WScript.Shell");

	var ws;
	shell.Run( [ "cscript wsh.js ", ie.hWnd ].join("") );
	for(var k = 1; k < 11; k++) {
		ws = ie.GetProperty("WScript");
		// wsh.jsからプロパティがセットされるとundefinedではなくなる。型は見なくてもOK(のハズ)
		if( typeof( ws ) != "undefined" ) break;
		shell.Run( "ping localhost -n 2", 0, true );
	}
	for(var i = 0; i < 10; i++) {
		ws.echo( new Date() );
		ws.WScript.Sleep( 1000 );
	}
	
	alert( ws.readln("なんか入力すれ。 > ") );
	
	ws.quit = true;
	window.close();
}, 0 );
</script>
</html>

IEに設定するプロパティ名が「WScript」になっているが、wsh.jsでセットしているオブジェクトはWScriptではなく、グローバルコードでの「this」になるので注意。

で、test.htaとwsh.jsを同じところに配置してtest.htaを起動すると、

  1. コンソール(cscript.exe)が起動して、1秒間隔で10回時間を表示する
  2. コンソールで入力を求められる
  3. HTA側でコンソールで入力された文字をalertして終了
  4. コンソール側もメッセージを表示して終了
となる。

簡単なライブラリ化をしてjsからHTAをダイアログのように使うのも試してみたので次のエントリで。

WScript - HTA 相互通信ライブラリ2007年06月05日 20時40分45秒

ってほど大層なもんじゃないけど、まあこれも一種のIPCになるかなぁ、と思ったり。

scripthost.jsのソース

まずはライブラリコードから。これはwsh・htaの両方から共通で使用する。要:prototype.js

Object.extend( Enumerator.prototype, {
	_each : function(iterator) {
		this.moveFirst();
		var i = 0;
		try {
			for(; ! this.atEnd(); this.moveNext()) {
				try {
					iterator( this.item(), i++ );
				} catch(e) {
					if( e != $continue ) throw e;
				}
			}
		} catch(e) {
			if( e != $break ) throw e;
		}
	}
} );
Object.extend( Enumerator.prototype, Enumerable );

var WshHost = Class.create();
WshHost.prototype = {
	initialize : function(hWnd) {
		if( isNaN( hWnd ) ) {
			this._ie = new ActiveXObject("InternetExplorer.Application");
		} else {
			this._ie = new Enumerator( new ActiveXObject("Shell.Application").Windows() ).find( function(ie) {
				return ie.hWnd == hWnd;
			} );
			if( ! this._ie ) throw new Error( "ie not found" );
		}
		this.id = this._ie.hWnd;
	},
	setHost : function(host) {
		this._ie.PutProperty( "Host", host );
	},
	getHost : function() {
		return this._ie.GetProperty( "Host" );
	}
}

使い方

コンストラクタは役割によって使い分ける。先に起動しているほうは引数なしで実行、後で呼び出された側では、コマンドライン引数かなにかでIEのhWndを受け取って、それを引数として実行する。

次にホストオブジェクトを設定するほう(=wsh側だな)がsetHostメソッドを実行する。引数はHTA側に渡したいオブジェクトならなんでもよいが、「HTAからWScriptのグローバルコードへアクセスする」のであればグローバルな「this」を渡す。

受け取り側(HTA側)ではgetHostメソッドでホストオブジェクトを受け取る。

後は工夫次第でお互いに通信が可能になる。

サンプルコード(HTA)

WSHからダイアログ扱いされるHTAのサンプルを以下に示す。ファイル名は「test.hta」とする。

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=Shift_JIS">
<meta http-equiv="Content-Style-Type" content="text/css">
<meta http-equiv="Content-Script-Type" content="text/javascript">
<title>test</title>
<script src="prototype.js"></script>
<script src="scripthost.js"></script>
<script>
window.resizeTo( 450, 120 );
</script>
<hta:application id="hta"></hta>
<script>
// trimメソッド追加
String.prototype.trim = function() {
	return this.replace( /^\s*/, "" ).replace( /\s$$/, "" );
}

// コマンドライン引数
this.Arguments = (function(src) {
	var result = [];
	var buf = {
		_buf : [],
		mode : false,
		push : function(c) {
			this._buf.push( c );
			if( /["']/.test( c ) ) {
				if( ! this.mode ) {
					this.mode = true;
				} else {
					this.value = this._buf.slice(1, this._buf.length - 1).join("");
					this.mode = false;
					this._buf = [];
					return false;
				}
			}
			return true;
		},
		finish : function() {
			if( this._buf.length == 0 ) return null;
			return this._buf.join("");
		}
	};
	for(var i = 0; i < src.length; i++) {
		if( ! buf.push( src.charAt(i) ) ) {
			result.push( buf.value );
		}
	}
	result.push( buf.finish() );
	return result.compact();
})( hta.commandLine.trim() );
</script>
<style>
</style>
<body>
<input type="file" id="file1" size="60">
<button id="btn1" disabled>確定</button>
</body>
<script>
// test.jsから渡されたhWndを引数にWshHostを初期化
var wshHost = new WshHost( this.Arguments[1] );
// ホストオブジェクト(=test.js内の'this')を取得
var host = wshHost.getHost();

Event.observe( $("file1"), "change", function() {
	$("btn1").disabled = false;
}, false );

Event.observe( $("btn1"), "click", function() {
	// test.jsのfileNameにパスをセット
	host.fileName = $("file1").value;
	// test.jsに終了を通知
	host.quit = true;
	// HTA自体も終了
	window.close();
}, false );
</script>
</html>
コマンドライン引数の解析のコードがちょっとうざいが我慢して欲しい。

コードはそれほど複雑ではなく、input type=fileでファイルを選択して「確定」をクリックすると、呼び出し元のWSHにファイルパスと終了を通知して終了する。

サンプルコード(JS)

ちょっと長いので、ライブラリロードだの、echoだのreadlnだのの定義は省略し、肝心の部分のみ記載する。ロードするライブラリは「dummy.js」、「prototype.js」および今回の「scripthost.js」の3つ。dummy.jsはここらあたりのものを拾って使って欲しい。

ファイル名はこちらも単純に「test.js」としておこう。

var shell = new ActiveXObject("WScript.Shell");

var wshHost = new WshHost();
wshHost.setHost( this );

// HTAの終了待ち合わせフラグ
var quit = false;
// HTAで選択したファイルパスを受け取る変数
var fileName = null;

echo( "ファイルを選択してください。" );
shell.Run( [ "test.hta ", wshHost.id ].join("") );
while( ! quit ) {
	WSH.Sleep( 100 );
}

echo( "選択されたファイル:", fileName );

で、実行すると

  1. test.jsを起動すると自動的にtest.htaが呼び出される
  2. test.htaでファイルを選択し、「確定」をクリックするとtest.htaは終了
  3. test.jsのコンソールにはhtaで選択したファイルのパスが表示される
と、たったこれだけ。だが、この方法ならcscriptから呼び出し可能なカスタムダイアログが作れると思う。IEにHTAのwindowをセットするとかできそうだし。

サンプル一式

今回のサンプルコード一式を以下のリンクからダウンロードできるようにした。

prototype.jsも1.5.1を同梱している。prototype.js以外のjs、htaはnyslとする。