Ruby/DL2 で Windows のクリップボードを取得

普段あまり Windows は使わない(正確にはブラウザとメーラと PuTTY しか使ってない)のですが、ちょっと思い立ってツールを作るために Ruby を使って Windowsクリップボードの内容を取得する方法を調べてみたところ、1.9 で使える方法が簡単に見つからなかったのでメモしておきます。Windows 上の Ruby は arton さんの ActiveScriptRuby 1.9.1-p378 を対象にしています。

まず 1.8 版での情報はすぐにいくつか見つかります。るびまの第0008号のWin32OLE 活用法の記事より。

http://jp.rubyist.net/magazine/?cmd=view&p=0008-Win32OLE

ここでは以下のような選択肢をあげたうえで独自に Win32API を使ってクリップボードを取得するサンプルコードが例示されています。

  • VisualuRuby の vr/clipboard
  • るびまの第0005回で紹介されていた Win32Utils(win32-clipboard という RubyGems パッケージがあります)
    • 依存しているパッケージに拡張ライブラリを含むものがあって gem install でインストールされるのは 1.8 版。1.9 版のバイナリパッケージを配布してないか探してみましたが見つからず。
  • cygwin 版であれば、`cat /dev/clipboard` を実行するという方法や `getclip` というコマンドを実行する方法
    • ASR なので対象外
  • Win32OLE を使って Internet Explorer の COM オブジェクトを経由する方法
    • 重いうえに毎回 IEクリップボードへのアクセスを許可するかどうかダイアログを出してしまいます。
  • Win32API を利用したサンプルコード
    • 1.9 では GetGlobalLock の返り値が Integer になってしまうので動作しない

というわけで ASR の 1.9 版で手軽にできる方法は全滅状態でした。

Win32API で動かす方法がなぜうまくいかなくなったのかなと調べてみると、1.9 から Win32API は Ruby/DL2 (1.9 からは dl として標準添付されている拡張ライブラリ)を利用したラッパとして定義されていて、これまでポインタを String として返していた(多分)ものがアドレスの整数がそのまま返るように変更されてしまったので GetGlobalLock が Integer を返すようになってうまくいかないみたいでした。

整数のアドレスをポインタとして扱うには DL::CPtr を使えばいいみたいだったので*1そこだけちょっと手を入れて以下のようにすると、クリップボードのテキストを Ruby から取得できました。

require "dl"
require "win32api"

module ClipBoard
  OpenClipboard = Win32API.new("user32", "OpenClipboard", ["I"], "I")
  CloseClipboard = Win32API.new("user32", "CloseClipboard", [], "I")
  GetClipboardData = Win32API.new("user32", "GetClipboardData", ["I"], "I")
  GlobalLock = Win32API.new("kernel32", "GlobalLock", ["I"], "P")
  GlobalUnlock = Win32API.new("kernel32", "GlobalUnlock", ["I"], "I")

  # Clipboard contents format
  CF_TEXT = 1

  def get_text
    data = nil
    while OpenClipboard.Call(0) == 0
      sleep 0.2
    end

    begin
      handle = GetClipboardData.Call(CF_TEXT)
      if handle != 0
	if ptr = GlobalLock.Call(handle)
	  begin
	    data = DL::CPtr.new(ptr).to_s
	  ensure
	    GlobalUnlock.Call(handle)
	  end
	end
      end
    ensure
      CloseClipboard.Call()
    end
    data
  end
  module_function :get_text
end

if $0 == __FILE__
  text = ClipBoard.get_text
  puts text
end

*1:というのを調べるのにソースを読まないといけなかったわけですが……