N0ラボ(仮)


Ruby編

Win32APIを使ってマウスクリックを行わせる

ぐぐっても意外といい感じのコードが落ちてなかった。 そもそも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

Win32APIを使ってキーボード入力を行わせる

マウスクリックがあったら次はキーボードでしょ。 これもいいコードがなかなか見つからなかったが、 キーワードをあれこれ変えて検索してたら動いてるコードを見つけた。 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経由で任意の文字を入力できる。 クリップボードを扱うコードは普通に落ちてたからぐぐってくれ。

Win32APIを使って特定のウインドウをフォアグラウンドに出す、タイトルからウインドウを取得する

クリックだのキーボードだの操作する前提としてウインドウがフォアグラウンドに出てないといけないわけだが、 比較的安定して動くものを作るのが結構大変だった(自分がそもそも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移植版

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とかも変わってないことを明示するために放置している。

UTF-8, UTF-16, UTF-32相互変換(添付ライブラリ不使用)

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