ぐぐっても意外といい感じのコードが落ちてなかった。 そもそもWin32APIについての知識があんまりなくてやろうとしてるから、 自分のやりたいことに合致した関数がどれかわかんないし、 他の言語(C#とかVBとか)で書かれてるのも参考にしたけどRuby特有の部分の書き方がアテにならないし、 それらしい断片をかき集めてどうにか動いたコード。
要件的には、別のアプリケーションをクリックしたいんだけど、 ボタンとかがあるわけじゃない。 具体的に言うとInternet Explorerで普通にリンクとかクリックするのを自動化したい。
require 'Win32API'
SetForegroundWindow = Win32API.new('user32', 'SetForegroundWindow', %w(i), 'i')
ClientToScreen = Win32API.new('user32', 'ClientToScreen', %w(i p), 'i')
SetCursorPos = Win32API.new('user32', 'SetCursorPos', %w(i i), 'i')
SendInput = Win32API.new("user32", "SendInput", %w(i p i), 'i')
INPUT_MOUSE = 0
MOUSEEVENTF_LEFTDOWN = 0x0002
MOUSEEVENTF_LEFTUP = 0x0004
#
# 指定した座標にマウスを動かし、クリックする。
# top_hwndはクリックしたいウインドウ(前面に出す必要がある)のハンドル。
# (前述の例ならIEのウインドウのハンドル。)
# base_hwndはクリックする座標の基準となる(Windows用語で言う)ウインドウのハンドル。
# (前述の例ならIEのレンダリング領域のハンドル。)
# left, topはそれぞれbase_hwndのウインドウの左上を原点にしたときの座標。
#
def click(top_hwnd, base_hwnd, left, top)
SetForegroundWindow.call(top_hwnd)
sleep(1)
pointstruct = [0,0].pack("l!2")
ClientToScreen.call(base_hwnd, pointstruct)
baseleft, basetop = pointstruct.unpack("l!2")
SetCursorPos.call(left + baseleft, top + basetop)
sleep(0.1)
mouseinput1 = [INPUT_MOUSE, 0, 0, 0, MOUSEEVENTF_LEFTDOWN, 0, 0].pack('LllLLLL')
mouseinput2 = [INPUT_MOUSE, 0, 0, 0, MOUSEEVENTF_LEFTUP, 0, 0].pack('LllLLLL')
input = mouseinput1 + mouseinput2
SendInput.call(2, input, mouseinput1.length)
end
マウスクリックがあったら次はキーボードでしょ。 これもいいコードがなかなか見つからなかったが、 キーワードをあれこれ変えて検索してたら動いてるコードを見つけた。 lircwin.rb内のKeySenderというモジュールを参考に自分なりに改造してみた。
require 'Win32API'
MapVirtualKey = Win32API.new('user32.dll', 'MapVirtualKey', %w(i i), 'i')
SendInput = Win32API.new("user32", "SendInput", %w(i p i), 'i')
INPUT_KEYBOARD = 1
KEYEVENTF_KEYUP = 2
#
# キーボードの指定したキーを叩く動作を行わせる。
# vkeyは同時に叩くキーのキーコードの配列を指定する。
# 例えばtを入力したければ
# kbdinput(['T'[0]])
# Tを入力したければ
# VK_SHIFT=0x10; kbdinput([VK_SHIFT, 'T'[0]])
# と使う。
#
def kbdinput(keys)
wscans = keys.map{|vkey| MapVirtualKey.call(vkey, 0)}
vkeywscan = keys.zip(wscans)
keydowns = vkeywscan.map{|vkey, wscan|
[INPUT_KEYBOARD, vkey, wscan, 0, 0, 0, 0, 0].pack('LSSLLLLL')
}
keyups = vkeywscan.map{|vkey, wscan|
[INPUT_KEYBOARD, vkey, wscan, KEYEVENTF_KEYUP, 0, 0, 0, 0].pack('LSSLLLLL')
}.reverse
SendInput.call(keys.length*2, keydowns.join('')+keyups.join(''), keydowns[0].length)
end
あとはクリップボードを扱えればCtrl+V経由で任意の文字を入力できる。 クリップボードを扱うコードは普通に落ちてたからぐぐってくれ。
クリックだのキーボードだの操作する前提としてウインドウがフォアグラウンドに出てないといけないわけだが、 比較的安定して動くものを作るのが結構大変だった(自分がそもそもAPIをよく知らないのが問題)。 対象のウインドウが他のウインドウに隠れてようと最小化されてようととりあえずひっぱり出す、ということをしたかった。 それと、ウインドウを指定するのに、ウインドウタイトルの文字列で識別したかった。 という2つの機能しかないのだが、なぜだかコードがでかくなった。
コード自体はC++だのVBだので書かれたものをネットで探して移植したものの寄せ集め。 一部のAPIの使い方はRubyで書かれた実例を必死で探した。 所詮APIに詳しくない素人が書いた寄せ集めなので、不要なことを書いてる可能性も多々あり。
2/24追記:$KCODE="NONE"時に正規表現で警告が出る、または正規表現でエラーになる可能性があるのを修正しました。
require 'dl/win32'
require 'Win32API'
class WindowHandler
GetWindowText = Win32API.new('user32.dll', 'GetWindowText', %w(i p i), 'i')
SetForegroundWindow = Win32API.new('user32.dll', 'SetForegroundWindow', %w(i), 'i')
GetForegroundWindow = Win32API.new('user32.dll', 'GetForegroundWindow', [], 'i')
BringWindowToTop = Win32API.new('user32.dll', 'BringWindowToTop', %w(i), 'i')
GetWindowThreadProcessId = Win32API.new('user32.dll', 'GetWindowThreadProcessId', %w(i p), 'i')
AttachThreadInput = Win32API.new('user32.dll', 'AttachThreadInput', %w(i i i), 'i')
SystemParametersInfo = Win32API.new('user32.dll', 'SystemParametersInfo', %w(i i p i), 'i')
IsIconic = Win32API.new('user32.dll', 'IsIconic', %w(i), 'i')
OpenIcon = Win32API.new('user32.dll', 'OpenIcon', %w(i), 'i')
IsWindowEnabled = Win32API.new('user32.dll', 'IsWindowEnabled', %w(i), 'i')
SPI_GETFOREGROUNDLOCKTIMEOUT = 0x2000
SPI_SETFOREGROUNDLOCKTIMEOUT = 0x2001
SLEEP_LONG = 2
#
# 指定した文字列または正規表現すべてをタイトルに含むウインドウを扱うWindowHandlerインスタンスを作成する。
# 実行時に該当するウインドウを1つだけ開いておくこと。
# raise: 該当するウインドウが1つもない場合
#
def initialize(*window_title)
@title = window_title
@hwnd = find_hwnd
end
#
# ウインドウをアクティブ化する。すでにアクティブならば何もしない。
# 最小化されていた場合は復元してから。
# どういう順序でアクティブ化してもうまくいくわけではないようなので、
# アクティブ化されていなかった場合は、まずRuby起動中のプロンプトをアクティブ化してから、
# この画面のをアクティブ化する。
# なお、Ruby起動中のプロンプトは、Regexp.new("(cmd.exe|コマンド プロンプト) \\- ruby.+#{$0}")で
# 表される正規表現としている。
# raise: ウインドウが見つからなかったとき、アクティブ化に失敗したとき
#
def activate
targ_hwnd = get_hwnd
return if GetForegroundWindow.call == targ_hwnd #正常終了
unminimize
result = SetForegroundWindow.call(targ_hwnd)
if result == 1
sleep(SLEEP_LONG)
return #正常終了
end
wintitle_re = Regexp.new("(cmd.exe|#{Regexp.escape('コマンド プロンプト')}) \\- ruby.+#{Regexp.escape($0)}")
@rubywin = WindowHandler.new(wintitle_re) unless @rubywin
@rubywin.activate unless @rubywin.get_hwnd == targ_hwnd
unminimize
cur_hwnd = GetForegroundWindow.call
s = "\0" * 4
threadID1 = GetWindowThreadProcessId.call(cur_hwnd, s)
threadID2 = GetWindowThreadProcessId.call(targ_hwnd, s)
AttachThreadInput.call(threadID2, threadID1, 1)
cur = "\0" * 4
SystemParametersInfo.call(SPI_GETFOREGROUNDLOCKTIMEOUT, 0, cur, 0)
void = "\0" * 4
SystemParametersInfo.call(SPI_SETFOREGROUNDLOCKTIMEOUT, 0, void, 0)
result = SetForegroundWindow.call(targ_hwnd)
result = BringWindowToTop.call(targ_hwnd)
SystemParametersInfo.call(SPI_SETFOREGROUNDLOCKTIMEOUT, 0, cur, 0)
AttachThreadInput.call(threadID2, threadID1, 0)
sleep(SLEEP_LONG)
if result == 0 #異常終了
raise ""
end
#正常終了
end
#
# @hwndのウインドウが存在すれば@hwndを、存在しなければ@titleからハンドルを探す。
# raise:@hwndのウインドウが存在せず、@titleのすべてをタイトルに含むウインドウもない場合
#
def get_hwnd
if IsWindowEnabled.call(@hwnd) == 0
@hwnd = find_hwnd
end
@hwnd
end
private
#
# この@titleをタイトルに含むウインドウのhwndを取得する。
# 複数あった場合、取得した順の最初のものを返す。
# raise:該当するウインドウが1つもない場合
#
def find_hwnd
handles = self.class.find_window(*@title)
raise "" if handles.length == 0
handles[0]
end
#
# 最小化を解除する
#
def unminimize
targ_hwnd = get_hwnd
return if IsIconic.call(targ_hwnd) == 0 #最小化されていない
OpenIcon.call(targ_hwnd)
sleep(SLEEP_LONG)
end
#
# handleのウインドウのタイトルを取得する
#
def self.windowTitle(handle)
buf = "\0" * 1000
titlelength = GetWindowText.call(handle, buf, buf.size)
title = buf.unpack("A*").first
end
Enum_windows_proc = DL.callback('IL') do |hdl|
stack(hdl)
-1
end
#
# strsに指定したすべての文字列または正規表現を含むウインドウのハンドルの配列を返す。
#
def self.find_window(*strs)
@@stack = []
user32 = DL.dlopen("user32")
ew= user32['EnumWindows', '0PL']
ew.call(Enum_windows_proc, 0)
@@stack.find_all{|hdl| strs.all?{|str| str.kind_of?(Regexp) ? str=~windowTitle(hdl) : windowTitle(hdl).include?(str)} }
end
def self.stack(hdl)
@@stack.push(hdl)
end
end
AlphamericHTMLのエンコード・デコードをRubyでもしたくて移植してみた。 JavaScriptとRuby CGIの間をXMLHTTPRequestでやりとりするときのエンコード方式として、 安全だし圧縮率も高めだしコード自体の行数も短いのでいろいろ都合が良かった。
条件としては、エンコードの入力、デコードの出力の文字列はUTF-16(ビッグエンディアン、BOMなし、UCS-2範囲)である。必要に応じて他の文字コードに変換が必要。 Rubyで文字コードをUTF-16をデフォルトにして開発する人なんてまずいないだろうけど、JavaScriptの仕様に合わせてるからしょうがない。 あと、Ruby 1.8.6でしか動作確認しておらず、Ruby 1.9のM17Nは考えてない。
module AlphamericHTML
#
# UTF16文字列sを、0-9A-Za-z_のみからなるAlphamericStringにエンコードする。
#
def self.encode(s)
a=""
t=("\000 "*1024)+s
l=-1
i=1024
ll=nil
aa="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_".split("")
while (p=t[i*2, 64*2]) != "" && p
j=2
while j <= p.length/2
break unless k=t[(i-819)*2...(i+j-1)*2].rindex(p[0...j*2])
kk=k/2
j+=1
end
if(2==j||3==j&&ll==l)
ll=l
c=(t[i*2]<<8)+t[i*2+1]
i+=1
if c<128
a += aa[l-32] if ll != (l=(c-(c%=32))/32+64)
a+=aa[c]
elsif 12288 <= c && c < 12544
a+=aa[l-32] if ll!=(l=((c-=12288)-(c%=32))/32+68)
a+=aa[c]
elsif 65280 <= c && c < 65440
a+=aa[l-32] if ll != (l=((c-=65280)-(c%=32))/32+76)
a+=aa[c]
else
a+="n"+aa[l] if ll != (l=(c-(c%=1984))/1984)
a+=aa[(c-(c%=62))/62]+aa[c]
end
else
a+=aa[(kk-(kk%=63))/63+50]+aa[kk]+aa[j-3];
i+=j-1
end
end
a
end
#
# 0-9A-Za-z_のみからなるAlphamericString aをUTF16文字列にデコードする。
#
def self.decode(a)
cc = {}
m=false
i=l=0
63.times{|d| cc["0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_"[d,1]]=d}
s="\000 "*1024
while true
c=cc[a[i,1]]
i+=1
break unless c
if c<32
val = m ? l*32+c : (l*32+c)*62+cc[a[i,1]]
s+=[val>>8, val&255].pack("c*")
i+=1 unless m
elsif c<49
l=(c<36) ? c-32 : (c<44) ? c+348 : c+1996
m=true
elsif c<50
l=cc[a[i,1]]
i+=1
m=false
else
w=s[-819*2..-1]
k=(c-50)*63+cc[a[i,1]]
i+=1
j=k+cc[a[i,1]]+2
i+=1
p=w[k*2...j*2]
if p && p != ""
while w.length<j*2
w+=p
end
end
s+=w[k*2...j*2]
end
end
s[1024*2..-1]
end
end
ほんとにそのまま移植で、何もロジックは変更していない。チューニングもしてない。1024*2とかも変わってないことを明示するために放置している。
RubyにはNKF, Kconv, iconvなど、文字コード変換ライブラリは添付されていて、相互変換ぐらいできるんだけど、 細かいところで気に入らない挙動があった。 全角チルダ(~)がいつのまにか波ダッシュ(〜)になってたりとか。
なので、機械的にただ変換するだけのものを作ってみた。 変換方法に忠実に作った。それだけ。
これもRuby 1.8.6でしか動作確認しておらず、Ruby 1.9のM17Nは考えてない。 あと、UTF-16, UTF-32については、BOMなしビッグエンディアンである。BOMありや、リトルエンディアンには対応してない。 (リトルエンディアンにしたければ、"n*"→"v*"、"N*"→"V*"でいいはずだけど。)
module UTFConverter
def self.utf16to32(s)
buffer = nil
values = []
s.unpack("n*").each do |bits|
if bits & 0b1111100000000000 == 0b1101100000000000
if bits & 0b10000000000 == 0
values.push(buffer) if buffer
buffer = byte & 0b1111111111
else
buffer = buffer<<10 | (byte & 0b1111111111)
end
else
if buffer
values.push(buffer)
buffer = nil
end
values.push(bits)
end
end
values.push(buffer) if buffer
values.pack("N*")
end
def self.utf32to16(s)
tv = []
s.unpack("N*").each do |value|
if value < 0xffff
tv.push(value)
else
tv.push(0b1101100000000000 | value>>10, 0b1101110000000000 | value & 0b1111111111)
end
end
tv.pack("n*")
end
def self.utf8to32(s)
buffer = nil
values = []
s.each_byte do |byte|
if byte & 0b10000000 == 0
if buffer
values.push(buffer)
buffer = nil
end
values.push(byte)
elsif byte & 0b01000000 == 0
buffer = buffer<<6 | (byte & 0b00111111)
elsif byte & 0b00100000 == 0
values.push(buffer) if buffer
buffer = byte & 0b00011111
elsif byte & 0b00010000 == 0
values.push(buffer) if buffer
buffer = byte & 0b00001111
elsif byte & 0b00001000 == 0
values.push(buffer) if buffer
buffer = byte & 0b00000111
elsif byte & 0b00000100 == 0
values.push(buffer) if buffer
buffer = byte & 0b00000011
else
values.push(buffer) if buffer
buffer = byte & 0b00000001
end
end
values.push(buffer) if buffer
values.pack("N*")
end
def self.utf32to8(s)
tv = []
s.unpack("N*").each do |value|
if value < 0x80
tv.push(value)
elsif value < 0x800
tv.push(0b11000000 | value>>6, 0b10000000 | value & 0b111111)
elsif value < 0x10000
tv.push(0b11100000 | value>>12)
tv.concat(Array.new(2){res = 0b10000000 | value & 0b111111; value>>=6; res}.reverse!)
elsif value < 0x200000
tv.push(0b11110000 | value>>18)
tv.concat(Array.new(3){res = 0b10000000 | value & 0b111111; value>>=6; res}.reverse!)
elsif value < 0x4000000
tv.push(0b11111000 | value>>24)
tv.concat(Array.new(4){res = 0b10000000 | value & 0b111111; value>>=6; res}.reverse!)
else
tv.push(0b11111100 | value>>30)
tv.concat(Array.new(5){res = 0b10000000 | value & 0b111111; value>>=6; res}.reverse!)
end
end
tv.pack("C*")
end
def self.utf8to16(s)
utf32to16(utf8to32(s))
end
def self.utf16to8(s)
utf32to8(utf16to32(s))
end
end