meta-author mattn.jp@gmail.com (mattn)

2010-12-20

こんにちわ。寒いですね。ネタも財布も寒いと噂の mattn です。
突然ですが Perl で電卓作れと言われれば、どう作りますか?
構文木つくって、括弧とか対応して...うざったいですよね。

せっかく Windows には最初から電卓が入っているのですから、これ 使っちゃいましょう。
どうやるか...

電卓を起動して、ボタンを押しちゃえばいいのです。
Win32::API を使って FindWindow でウィンドウを探し、数字ボタンのウィンドウ探し、SendMessage でボタンクリックのイベントを送り、最後に GetWindowText でテキストを取得してしまえばいいのです。

Win32::API は各 DLL と関数名、引数シグネチャ、戻り値シグネチャを指定すればエクスポートされている関数が取り出せます。

use Win32::API;
$function = Win32::API->new(
    'mydll, 'int sum_integers(int a, int b)',
);
$return = $function->Call(3, 2);

簡単ですね。シグネチャはPerldocによると

"I": value is an integer (int)
"N": value is a number (long)
"F": value is a floating point number (float)
"D": value is a double precision number (double)
"C": value is a char (char)
"P": value is a pointer (to a string, structure, etc...)
"S": value is a Win32::API::Struct object (see below)
"K": value is a Win32::API::Callback object (see Win32::API::Callback)

となっているので頑張ればコールバックも呼び出せますね。今日は FindWindow(FindWindowEx) と SendMessage 、GetWindowText だけ使うので

my $FindWindow    = Win32::API->new( "user32", "FindWindowA",    'PP',   'N' );
my $FindWindowEx  = Win32::API->new( "user32", "FindWindowExA",  'NNPP', 'N' );
my $SendMessage   = Win32::API->new( "user32", "SendMessageA",   'NNNP', 'N' );
my $GetWindowText = Win32::API->new( "user32", "GetWindowTextA", 'NPN',  'N' );

こうして準備しておきます。
起動は system を使いますが、ウィンドウが現れるまで FindWindow を投げても失敗してしまいます。
そこで

my $window;
my $timeout = 100;
while ( $timeout-- && !$window ) {
    $window = $FindWindow->Call( 0, '電卓' );
    usleep 10000;
}
die '404 電卓 Not Found' unless $window;

こうやって待機してあげましょう。そうですね!かっこ悪いですね。Window が見つかったら次は子ウィンドウハンドルを取得しましょう。

my $button = $FindWindowEx->Call( $window, 0, 0, '7' );
$SendMessage->Call( $button, BM_CLICK, 0, 0 );

これで「7」のボタンが押せました。
あとはこれをスクリプトに仕上げて

use strict;
use warnings;
use Win32::API;

use constant BM_CLICK   => hex('F5');
use constant WM_GETTEXT => hex('0D');
use constant WM_CLOSE   => hex('10');

my $FindWindow    = Win32::API->new( "user32", "FindWindowA",    'PP',   'N' );
my $FindWindowEx  = Win32::API->new( "user32", "FindWindowExA",  'NNPP', 'N' );
my $SendMessage   = Win32::API->new( "user32", "SendMessageA",   'NNNP', 'N' );
my $GetWindowText = Win32::API->new( "user32", "GetWindowTextA", 'NPN',  'N' );

sub push_button {
    my ( $hwnd, $caption ) = @_;
    my $button = $FindWindowEx->Call( $hwnd, 0, 0, $caption );
    $SendMessage->Call( $button, BM_CLICK, 0, 0 );
}

sub get_text {
    my ( $hwnd, $caption ) = @_;
    my $edit = $FindWindowEx->Call( $hwnd, 0, 'Edit', 0 );
    my $buf = "\0" x 1024;
    $SendMessage->Call( $edit, WM_GETTEXT, length($buf), $buf );
    $buf =~ s/\0.*$//;
    $buf;
}

system( 1, "calc.exe" );

my $window;
my $timeout = 100;
while ( $timeout-- && !$window ) {
    $window = $FindWindow->Call( 0, '電卓' );
    usleep 10000;
}
die '404 電卓 Not Found' unless $window;

for ( split //, 'CE' . (shift || '') . '=' ) {
    push_button( $window, $_ ) if $_ ne ' ';
}
my $result = get_text($window);
$result =~ s!\. $!!;

$SendMessage->Call( $window, WM_CLOSE, 0, 0 );

warn $result if defined $result;

こうですね。簡単ですね。これならば

C:\> perl calc.pl 1+2+3+4+5+6+7+8+9+10
55

なんて事も出来ますよね。一見中で calc.exe を使ってるなんて誰も気付かないですよね!
さて、このスクリプトを実行するよい子とのお約束

この2つだけ守れば君も明日から win32 perl hacker だよ!