すべてのウィンドウ情報を取得する方法。Scripting Bridge vs AX*

in

OSXである時点の表示されているすべてのウィンドウ位置を取得するのはScripting Bridgeで簡単にできると思っていたのですがいざやってみたらうまくできませんでした。やり方は、起動中のすべてのプロセス取得。そのプロセスが持っているウィンドウ情報を引き出すという手法でできると思ったですが...

まず、起動中のプロセス取得。これはシステムイベントから取得できます。

  SystemEventsApplication *systemEvents   = 
  [SBApplication applicationWithBundleIdentifier:@"com.apple.systemevents"];
  SBElementArray *procs = [systemEvents processes];

でこのプロセスからウィンドウ情報を取得します。

for(SystemEventsProcess* proc in procs)
{
   for (SystemEventsWindow *w in [proc windows])
   {
      NSLog(@"%@ :%@ (%.1f:%.1f,%.1f:%.1f),%d,%@,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d",
            [proc name],
            [w name],
            w.bounds.origin.x, w.bounds.origin.y,
            w.bounds.size.width,w.bounds.size.height,
            w.closeable,
            [[w document] name],
            w.floating,
            w.id,
            w.index,
            w.miniaturizable,
            w.miniaturized,
            w.modal,
            w.resizable,
            w.titled,
            w.visible,
            w.zoomable,
            w.zoomed
         );
     }
  }

これで完璧だと思ったのですが、取得できたのはw.nameだけ。あとは皆NULLかゼロという悲惨な状況でした。プロパティはあるのになんで!

というわけでいろいろ調べた結果アクセサビリティ系の関数でリトライ。面倒だったので現在のプロセスを全部取るところまでは同じで、その後取得したプロセスのpidを使ってアプリケーションの情報、そのアプリが持っているウィンドウ情報の取得と辿っていきます。

AXUIElementRef appElement = AXUIElementCreateApplication(proc.unixId);
id             windows    = nil;
NSArray       *proc_attrs;
CFStringRef    attribute  = CFSTR("AXWindows");

if (AXUIElementCopyAttributeNames(appElement,(CFArrayRef *)&proc_attrs) 
  != kAXErrorSuccess)
{
  continue; //next process
}
if ([proc_attrs indexOfObject:(NSString *)attribute] == NSNotFound)
{
   [proc_attrs release];
  continue; //next process
}

実際にはウィンドウを持たないプロセスもヒットするわけで最初にウィンドウ情報を取得できたかどうかチェックします。一つのアプリケーションは複数のウィンドウを持つ可能性があるのでリファレンスはAXUIElementRef の配列で返ります。念のため型をチェックしてループしながらウィンドウ情報を取得します。

if (AXUIElementCopyAttributeValue(appElement,attribute,(CFTypeRef *)&windows)
  ==kAXErrorSuccess)
{
  if (CFGetTypeID(windows) == CFArrayGetTypeID())
  {
    NSArray       *window_attrs = (NSArray *)windows;
    NSEnumerator  *enumerator   = [window_attrs objectEnumerator];
    AXUIElementRef aref;
    while ((aref = (AXUIElementRef)[enumerator nextObject]))
    {
      (ウィンドウ情報の取得処理)
    }
 }
}

後は、個々のウィンドウ属性を一つ一つ取得するだけです。今回は位置情報が欲しかったので、こんな感じになります。

if (AXUIElementCopyAttributeValue(aref,CFSTR("AXPosition"),(CFTypeRef *)&w_pos)
  == kAXErrorSuccess)
{
  if (AXValueGetType((AXValueRef)w_pos) == kAXValueCGPointType)
  {
    AXValueGetValue((AXValueRef)w_pos, kAXValueCGPointType, &pos);
    wi.wpos_x      = [NSNumber numberWithDouble:pos.x];
    wi.wpos_y      = [NSNumber numberWithDouble:pos.y];
  }
  [w_pos release];
}

この手でいけばウィンドウ情報に限らずテキストフィールドとか細かいエレメントの情報も皆取れるようですが、それにしてもいかにも面倒。最初に書いたようにScripting Bridgeで何故サクっと情報取得できないのでしょうかね?後、大事な事。これは環境設定-ユニバーサルアクセスで「補助装置にアクセスできるようにする」にチェックが入ってないとダメみたいです。補助装置がなんのことかわかりませんがこれも仕様ですから...また、CoreGraphicのundocumentな関数を使えばこれを回避できるみたいですが、そこまでする必要がないので調べてません。Scripting Bridgeを使わずProcessを取得する方法はまだ模索中です。

winfo

実験用に作成したサンプルアプリは http://github.com/hippos/winfo置いときました。ご存分に。

この記事のトラックバックURL:

http://hippos-lab.com/blog/trackback/345

Comments