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をダイアログのように使うのも試してみたので次のエントリで。

Apolloが盛り上がってきているが2007年05月28日 22時25分18秒

こんなエントリこんなエントリを読んで漠然と思った。

XULRunnerのときも思ったが「これってHTAじゃね?」という感じがする。たぶん、Ajaxではなく「DHTML」といっていたころ(いまも言うのか)にJ(ava)Scriptにハマっていた人はおんなじようなことを感じてるんじゃなかろうか。

もちろん後発だけあって、セキュリティ周りとか低レベルのアクセスを行う機能とかはまともな設計をしている(と思いたい)だろうけど、実行可能な機能としてApolloでできることは大体HTAでもできるような気がする。もちろんWin限定ってのがアレだもんで、マルチプラットフォームなXULやApolloと比べるのはお門違いなんだけどさ。(あー、なによりSilverlightがあったか)

でもWindowsならわざわざランタイムインストールしなくても動作するってのは大きいと思う。ファイルIOだってDBアクセスだって大体のコンポーネントはOSで持ってるしね。あとは関連ファイルを1つのアーカイブにまとめて実行できる機能でもあればいいなぁ。.zipをmshtaに渡すとアーカイブ中の「main.hta」をキックして、とかね。(Win32リソースとしてexeに埋め込めばmshta.exe res://~ってできるんだけど、Win32アプリをビルドできる環境が必要になるしなぁ)

あと、MSがHTA(というかIE)向けの高度なUIコンポーネント(HTCベースでかまわない)でもOSに入れといてくれれば結構しっかりしたアプリケーションができるんだけど、もはやテコ入れはないだろうなぁ。すこし残念な気もするが。

そんな取りとめもないことを思ったり思わなかったり。

あー、ApolloでDBアクセスって話でてるけど、ひょっとしてASで直接ソケット接続するクライアントを作るってのかな?ODBCサポートでもすればいいのに。

位置とサイズを記憶するHTA2007年05月07日 01時20分20秒

アプリっぽくなる

以前HTAコネタで起動時に位置とサイズを設定するTIPS(てほどでもないが)を紹介したが、それにちょっとひねりを加えて、自動的に位置とサイズを記憶、次回自動時にそれを再現するサンプルを書いてみる。なんとなく、デスクトップアプリっぽいしょ?

なお、サンプルでは、矩形情報の読み込み・保存にjson.orgのJSONライブラリを利用しているが、単純なシリアライズ・デシリアライズなので自前でメソッドを用意できるなら特にjson.jsは必要ない。

処理のタイミング

まず、前のエントリでも書いたが、ウィンドウの位置とサイズを設定するタイミングは、HTA:APPLICATIONタグの出現前にしないと気になるちらつきが発生する。なので、json.jsをロード(または同等の機能の定義・ロード)してからhta:applicationが出現するまでに

  • 保存したウィンドウ矩形情報のロード
  • ロードした矩形情報を元にリサイズと位置の確定
の処理をする必要がある。

また、最終的な矩形情報の保存のタイミングだが、後述の通りdocument.bodyのプロパティを参照するので、window.onbeforeunloadにする。onunloadではdocument.bodyへアクセスできないためエラーになる。

ウィンドウの位置とサイズの取得方法

一番面倒なのは、ウィンドウの位置とサイズの正確な取得方法だ。

まず位置情報。IEにはwindow.screenLeft/screenTopというプロパティがあるが、これはウィンドウボーダーとタイトルバーのサイズを除いた情報しか取得できない。

また、サイズについてはdocument.bodyのoffsetWidth/offsetHeightが取得可能なもっとも外側のサイズのようで、これまたタイトルバーやらとの差がある。

で、どうするかというと

起動直後に位置とサイズを調整するのだからその情報を元に、document.bodyのレンダリングがすんだ時点で差分を取得しておく。で、保存時はその差分を加えた情報を保存する。
という、ややベタな方法をとることにする。 具体的には、window.onload時に
// offsetRectはleft/top/width/heightの
//   プロパティに、測定サイズと実サイズの差を格納する。
// initialRectはwindow.resizeTo/moveToに使用した値を
//   格納している。
with( offsetRect ) {
	left = initialRect.left - window.screenLeft;
	top = initialRect.top - window.screenTop;
	width = initialRect.width - document.body.offsetWidth;
	height = initialRect.height - document.body.offsetHeight;
}
としてオフセット情報を取得し、window.onbeforeunload時に
// savePrefは指定オブジェクトのJSONデータを保存する関数。
// window.screenLeft/Topとdocument.body.offsetWidht/Heightに
//   offsetRectの補正値を加える。
savePref( {
	left : window.screenLeft + offsetRect.left,
	top : window.screenTop + offsetRect.top,
	width : document.body.offsetWidth + offsetRect.width,
	height: document.body.offsetHeight + offsetRect.height
} );
の要領で補正した矩形情報を保存する。

サンプルコード

実際に位置とサイズを自動保存する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>テスト</title>
<script src="json.js"></script>
<script>
// 最後に保存した矩形情報をロードし、矩形オブジェクトとして返す
function loadPref() {
	var fso = new ActiveXObject("Scripting.FileSystemObject");
	var stream;
	try {
		// 起動ディレクトリの「preference.json」に決めうち
		stream = fso.OpenTextFile( "preference.json", 1 );
		return stream.ReadAll().parseJSON();
	} catch(e) {
		with( { w : 320, h : 240 } ) {
			return {
				width : w,
				height : h,
				left : parseInt( ( screen.width - w ) / 2 ),
				top : parseInt( ( screen.height - h ) / 2 )
			}
		}
	} finally {
		if( stream ) stream.Close();
	}
}

function savePref(obj) {
	var fso = new ActiveXObject("Scripting.FileSystemObject");
	var stream;
	try {
		// 起動ディレクトリの「preference.json」に決めうち
		stream = fso.CreateTextFile( "preference.json" );
		stream.Write( obj.toJSONString() );
	} finally {
		if( stream ) stream.Close();
	}
}

// 設定ロード直後の実ウィンドウサイズ
var initialRect = loadPref();
// 実サイズとdocument.bodyの矩形とのオフセット
var offsetRect = {
	left : 0,
	top : 0,
	width : 0,
	height : 0
};

with( { rect : initialRect } ) {
	window.moveTo( rect.left, rect.top );
	window.resizeTo( rect.width, rect.height );
}
</script>
<hta:application innerBorder="no" scroll="no"></hta:application>
</head>
<body>
</body>
<script>
window.onload = function() {
	// document.bodyのレンダリングが完了したのでoffsetRectを算出
	with( offsetRect ) {
		left = initialRect.left - window.screenLeft;
		top = initialRect.top - window.screenTop;
		width = initialRect.width - document.body.offsetWidth;
		height = initialRect.height - document.body.offsetHeight;
	}
}
window.onbeforeunload = function() {
	// offsetRectを加えた矩形情報を保存
	savePref( {
		left : window.screenLeft + offsetRect.left,
		top : window.screenTop + offsetRect.top,
		width : document.body.offsetWidth + offsetRect.width,
		height: document.body.offsetHeight + offsetRect.height
	} );
}
</script>
</html>
まあ、こんな機能なくてもいいんだけど、ちょっとデスクトップアプリ気分に浸れるので。

HTAコネタ2007年04月26日 09時15分28秒

起動時に位置とサイズを指定

HTAのウィンドウが表示されるタイミングはhta:application要素が出現した時なので、これより前にwindow.resizeTo/moveToで位置と要素を指定できる。

サンプル

<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>テスト</title>
<style>
body {
	background-color: threedface;
}
</style>
<script>
// 画面中央に320x240で表示
with( { w : 320, h : 240 } ) {
	window.resizeTo( w, h );
	window.moveTo( ( screen.width - w ) / 2, ( screen.height - h ) / 2 );
}
</script>
<hta:application innerBorder="no" scroll="no"></hta:application>
</head>
<body>
</body>
</html>
上のコードで、hta:applicationをscriptより前に記述すると、一瞬表示された後にセンタリングされる。

もっともhta:applicationを使用しなければこんなことに気を使わなくてもいいんだけど。

.htaにファイルをドラッグドロップできるようにする

割と定番だとおもうが、htafileにDropHandlerを登録すると、.htaのアイコンにファイルをドラッグドロップできるようになる。

DropHandlerの登録は

HKEY_CLASSES_ROOT\htafile\ShellEx\DropHandler
の標準の値に、文字列で
{60254CA5-953B-11CF-8C96-00AA00B8708C}
を設定する。以下の内容を.regファイルとして保存すれば登録できる(ハズ)。
Windows Registry Editor Version 5.00

[HKEY_CLASSES_ROOT\htafile\ShellEx\DropHandler]
@="{60254CA5-953B-11CF-8C96-00AA00B8708C}"

ドラッグドロップされたパラメータを受け取るには、htaオブジェクトのcommandLineプロパティを使用する。 commandLineプロパティには、mshta.exeを実行した際のコマンドライン引数がそのまま格納されているので、パースする必要がある。

<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>テスト</title>
<hta:application id="hta1" innerBorder="no" scroll="no"></hta:application>
</head>
<body>
<textarea id="text1" style="width: 100%; height: 100%;"></textarea>
</body>
<script>
document.getElementById("text1").innerHTML =
	document.getElementById("hta1").commandLine;
</script>
</html>
この例では、textareaにこのHTA自身の絶対パスが表示される。 .htaにファイルをドラッグドロップして起動した場合はこの後ろにドロップされたファイルのパスが続くことになる。 HTA自身またはドロップされたファイルのパスに空白が含まれている場合は、パスはダブルクォートされた状態で渡されるので、単純にcommandLine.split(" ")というわけにはいかないので注意。