さて今度はメニューキャプチャにマウスカーソルを合成する。メニューキャプチャでは以前紹介した Windowクラスは使わずベタで CGWindowListCopyWindowInfo( )を使っていた。これに手をいれて Windowベースの実装へ変更する。こうすると CaptureController側のマウスカーソル画像合成はウィンドウほかの共通処理が使える。
早速修正処理に入るがコードがちょっとわかりづらい(自分で書いたのだが。。)。そこで一旦メニュー処理を整理してみた。
まずコードは何をやっているのかを追ってみた。
おおざっぱすぎてよくわからない。もう少し詳細に追って見る。
なんとなくやりたいことが見えてきた。つまり(当時自分で考えたことは)こういうこと。
メニューの種類によって、メインメニューバーやステータスバーを追加する処理を加えている。
例えばこんなのや、
こんなの、
そしてこんな感じ。
その為に処理が若干わかりづらくなっている。今回 Window対応するのに合わせてここをリファクタリングしてしまおう。
(続く)
2008年8月27日水曜日
マウスカーソルのキャプチャ (13)
2008年8月26日火曜日
マウスカーソルのキャプチャ (12)
マウスカーソルのキャプチャを入れるにあたってはキャプチャ処理のコントローラ CaptureController に手をいれてある。SimpleCapの中でこのコントローラの位置づけは次のようになっている。
メニュー選択(イベント発生)
|(1)
↓
AppController
|(2)
↓
CaptureContoller
|(3) ↑
↓ |(4)
キャプチャ処理(WindowHandler, ScreenHandler, ...)
各Handlerはキャプチャ結果を CGImage へ落とした後、その保存は CaptureControllerへ任せる(4)。
マウスカーソル合成はこの (4)のタイミングで行うようにしてある。各Handlerはマウスカーソル合成に必要なキャプチャ画像とその座標だけを容易すれば良いことになる。マウスカーソルの有無も後でここを制御するだけで済む。
以下は (4)の処理の一部。
CaptureController.m
- (void)saveImage:(CGImageRef)cgimage withMouseCursorInRect:(NSRect)rect
{
if (cgimage) {
// composite with mouse cursor
#1# MouseCursor* cursor = [MouseCursor mouseCursor];
NSBitmapImageRep *bitmap_rep = [[[NSBitmapImageRep alloc] initWithCGImage:cgimage] autorelease];
CGImageRelease(cgimage);
if ([cursor isIntersectsRect:rect]) {
NSPoint p = [cursor pointForDrawing];
p.x -= rect.origin.x;
p.y -= rect.origin.y;
NSImage *image = [[[NSImage alloc] init] autorelease];
[image addRepresentation:bitmap_rep];
p.y = [image size].height - p.y;
[image lockFocus];
#2# [[cursor image] compositeToPoint:p operation:NSCompositeSourceOver];
[image unlockFocus];
// re-creation bitmap rep
bitmap_rep = [[[NSBitmapImageRep alloc] initWithData:[image TIFFRepresentation]] autorelease];
}
// save to file
#3# [_file_manager saveImage:bitmap_rep];
}
}
#1# まず MouseCursorのインスタンスを作成する
#2# 位置補正を行った後、キャプチャ画像とマウスカーソル画像を合成する
#3# 最後にその画像を(ファイル管理クラスによって)保存する
2008年8月25日月曜日
マウスカーソルのキャプチャ (11)
さて今回はウィンドウ(Windows)。
まず結果から。
元々ウィンドウでは Window というクラスを用意し CGWindowListCopyWindowInfo( ) から取得した情報を管理していた。CGWindowListCopyWindowInfo( )の結果は CFDictionary 形式で値の取り出しは関数を使う必要があり、扱いづらいというのがその理由。これを拡張して座標管理も行なうようにした。
Window.h
@interface Window : NSObject {
int _order;
CGWindowID _window_id;
int _owner_pid;
NSString* _window_name;
NSString* _owner_name;
int _layer;
NSRect _rect;
NSImage* _image;
}
その結果、ほとんどの情報を Windowへ載せ替えるようなラッパーコードに近いものとなった(*)。ただその分実装は楽になった。
(*)実際にはラッパー以外の、例えばウィンドウ画像を扱うような機能、並び順管理などの機能も持たせている。
こんなコードだったのが
CFArrayRef window_list = CGWindowListCopyWindowInfo((kCGWindowListOptionOnScreenOnly|kCGWindowListOptionOnScreenBelowWindow|kCGWindowListExcludeDesktopElements), window_id);
for (i=0; i < CFArrayGetCount(window_list); i++) {
window = CFArrayGetValueAtIndex(window_list, i);
CFNumberGetValue(CFDictionaryGetValue(window, kCGWindowNumber),
kCGWindowIDCFNumberType, &_window_id);
CFNumberGetValue(CFDictionaryGetValue(window, kCGWindowOwnerPID),
kCFNumberIntType, &_owner_pid);
CFNumberGetValue(CFDictionaryGetValue(window, kCGWindowLayer),
kCFNumberIntType, &_layer);
:
:
ずいぶんとスッキリ書ける様になった。
NSArray *list = [self getWindowAllList];
for (Window* window in list) {
window_id = [window windowID];
owner_pid = [window ownerPID];
layer = [window layer];
:
:
これによって最後にキャプチャ対象の Windowの _rect を合成し、キャプチャ画像の最終座標を求めることができるようになった。この最終座標を使ってマウスカーソルを合成してやる。
2008年8月24日日曜日
2008年8月22日金曜日
マウスカーソルのキャプチャ (8)
一通り検証ができたので SimpleCap へ組み込んでみる。
SimpleCapではタイマー機能でのみマウスカーソルのキャプチャを行なうことにする。対象は次の通り。
・ウィンドウタイマー(Windows)
・範囲選択タイマー(Selection)
・メニュータイマー(Menu)
・スクリーンタイマー (Screen)
・アプリケーションタイマー(Application)
・ ウィジェットタイマー(Widget)
結構あるな。
前回までの通り、マウスカーソルはキャプチャ画像とは別に用意して合成する方法を取る。
画像の用意そのものは前回までの MouseCursorクラスで一元的に対応できる。やっかいなのはキャプチャ画像との合成処理。キャプチャ画像のどこへカーソルを描画(合成)すべきかを正確に計算する必要がある。スクリーンタイマーや範囲選択タイマーは元々座標を管理しているので容易にできるが、ウィンドウタイマーやメニュータイマーは複数のウィンドウを扱うため少々面倒である。今まで気にしていなかった各ウィンドウの座標をきちんと管理してやる必要がある。特にタイマーはメインメニューを合成するなどの特別な処理を行っているのでちょっとやっかい。
次回からすこしずつ実装していこう。
2008年8月21日木曜日
マウスカーソルのキャプチャ (7)
前回作成した MouseCursor を書き換えて CGSGetGlobalCursorData( ) を使ってマウスカーソル画像を用意する。
ソース:MousePointer-5.zip
CGSGetGlobalCursorData ( ) を使ったコードは以前紹介したものをそのまま使っているので、解説は割愛する。
するとこんな感じ。
なかなかいい。
- - - -
でも CGSGetGlobalCursorData( ) はプライベート関数。使うべきではないと分かっていても他に代替手段が無いと。。
例えば、デフォルトはグラブ (Grab.app)方式として、プリファレンスでこちらに切り替えられるようにするのはどうだろうか。
分かるユーザにだけ使ってもらう。また将来突然使えなくなったときの応急処置としてグラブ方式に切り替えてもらうことができる。
2008年8月20日水曜日
マウスカーソルのキャプチャ (6)
マウスカーソルの画像や位置情報などをまとめるクラスを追加する。名前は MouseCursor(そのまんま)。
MouseCursor.h
@interface MouseCursor : NSObject {
NSImage* _image;
NSPoint _location;
NSPoint _hot_spot;
}
+ (MouseCursor*)mouseCursor;
- (NSImage*)image;
- (NSSize)size;
- (NSPoint)location;
- (NSPoint)hotSpot;
- (NSPoint)pointForDrawing;
MouseCursor.m
@implementation MouseCursor
- (id)init
{
self = [super init];
if (self) {
NSCursor* cursor = [NSCursor arrowCursor];
_image = [[cursor image] retain];
_location = [NSEvent mouseLocation];
_hot_spot = [cursor hotSpot];
}
return self;
}
- (void) dealloc
{
[_image release];
[super dealloc];
}
+ (MouseCursor*)mouseCursor
{
return [[[MouseCursor alloc] init] autorelease];
}
- (NSImage*)image
{
return _image;
}
- (NSSize)size
{
return [_image size];
}
- (NSPoint)location
{
return _location;
}
- (NSPoint)hotSpot
{
return _hot_spot;
}
- (NSPoint)pointForDrawing
{
NSPoint p = _location;
p.y -= [self size].height;
p.x -= _hot_spot.x;
p.y += _hot_spot.y;
return p;
}
前回のソースコードへ組み込み動作を確認する。
ちゃんと動作している。
- - - -
処理内容は前回までに紹介したコードとほぼ同じで、特に大したことはおこなっていない。重要なのはクラスとして切り出されていること。後でマウスカーソル画像の取得処理を差し替えることができる。次回はプライベート関数 CGSGetGlobalCursorData( ) に置き換えてみる。
2008年8月19日火曜日
マウスカーソルのキャプチャ (5)
スクリーンキャプチャした画像にマウスカーソル画像を合成してみる。
サンプル:MousePointer-3.zip
まずは全画面キャプチャに、NSCursorから取得した矢印カーソルを合成してみる。タイマーを開始すると5秒後に screen.tiff をデスクトップへ作成する。
キャプチャには CGWindowListCreateImage を使い、矢印カーソルは NSCursor#arrowCursor を使う。
- (NSImage*)cursorImage
{
return [[NSCursor arrowCursor] image];
}
- (NSImage*)screenImage
{
CGImageRef cgimage = CGWindowListCreateImage(CGRectInfinite,
kCGWindowListOptionAll,
kCGNullWindowID,
kCGWindowImageDefault);
NSBitmapImageRep *bitmap_rep = [[NSBitmapImageRep alloc] initWithCGImage:cgimage];
NSImage *image = [[[NSImage alloc] init] autorelease];
[image addRepresentation:bitmap_rep];
[bitmap_rep release];
CGImageRelease(cgimage);
return image;
}
スクリーンキャプチャ画像は後の合成処理がしやすい様に CGImageRef から NSImage へ変換しておく。
先の2つのメソッドから NSImage を取得した後、#compositeToPoint:operation: を使い合成処理を行う。カーソル位置は、キャプチャ直後の座標を NSEvent#mouseLocation から取得して利用している。
- (void)capture
{
NSImage* screen_image = [self screenImage];
NSImage* cursor_image = [self cursorImage];
NSPoint p = [NSEvent mouseLocation];
NSSize s = [cursor_image size];
[screen_image lockFocus];
[cursor_image compositeToPoint:p operation:NSCompositeSourceOver];
[screen_image unlockFocus];
:
:
するとこうなる。以下、みやすいようにカーソルの周辺だけに手作業で切り取ってある。
実はマウスカーソルは Safariアイコンの磁石の中心を指していた。合成の結果、マウスカーソルの位置がずれている。これは左下が原点の座標系を使っていることによる。
そこで補正してやる。マウスカーソルの高さの分だけ修正すると次の様になる。
NSPoint p = [NSEvent mouseLocation];
NSSize s = [cursor_image size];
p.y -= s.height;
いい感じだがまだ少しづれている。今度の原因はマウスカーソルのポイント位置が画像の左上からずれていること。これはマウスカーソル毎に hot spot と呼ばれるオフセット(NSPoint)として定義されている。例えば NSCursor#arrowCursor の場合、画像の左上からカーソルが指す点は (4, 4)だけずれている。
hot spot は NSCursor#hotSpot で取得することができる。
そこでこの (4, 4)を補正してやると:
うまくいった。
2008年8月18日月曜日
マウスカーソルのキャプチャ (4)
次はグラブ(Grab.app)方式を考えてみる。NSCursorを使うとシステムが使用しているマウスカーソルの画像が簡単に取得できる。
NSCursor Class Reference
カーソルを取得するためのクラスメソッド:
+ currentCursor
+ arrowCursor
+ closedHandCursor
+ crosshairCursor
+ disappearingItemCursor
+ IBeamCursor
+ openHandCursor
+ pointingHandCursor
+ resizeDownCursor
+ resizeLeftCursor
+ resizeLeftRightCursor
+ resizeRightCursor
+ resizeUpCursor
+ resizeUpDownCursor
メソッドで取得できる画像を一覧するサンプルを作ってみた。NSCollectionViewを使うと簡単に一覧表示できる(NSCollectionViewの使い方は過去のブログを参照)。
画像とメソッド名称、ホットスポット位置(hotSpotの戻り値: NSPoint)を表示している。
ソース:MousePointer-2.zip
AppController で配列 (NSMutableArray)を用意し、そこへ個々のカーソル毎の情報を用意した NSMutableDictionaryを入れていく。NSMutableDictionaryのキーは @"image", @"name", @"hotSpot"としている。
AppController.m
- (id)init
{
self = [super init];
if (self) {
cursors = [[NSMutableArray alloc] init];
NSCursor* cursor;
cursor = [NSCursor arrowCursor];
[cursors addObject:[NSMutableDictionary dictionaryWithObjectsAndKeys:[cursor image], @"image", @"arrowCursor", @"name", NSStringFromPoint([cursor hotSpot]), @"hotSpot", nil]];
:
:
↑↑ メソッドの分だけベタに記述しているが、 vi などのマクロを使えばメソッドの数が多くとも簡単に記述できる。
一旦データが用意できれば、後は InterfaceBuilder上のバインディング設定だけで表示できる。本当に良くできている。ただ欲を言えばOutlet/Action同様にマウスドラッグでバインディングが設定できると便利なのだが。
- - - -
この数回の検証で、プライベート関数利用など問題はあるが画像を取る方法はわかった。次はこれをどうスクリーンキャプチャへ合成するかだ。これは意外と面倒な予感がする。
2008年8月17日日曜日
マウスカーソルのキャプチャ (3)
前回の続き。CGSGetGlobalCursorData( ) を使ったマウスカーソルのキャプチャのソース解説。サンプルでは AppController.m に実装されている。
まずボタンを押すと #start: が呼ばれ、ここでタイマーを仕掛ける。
-(IBAction)start:(id)sender
{
NSTimer* timer = [NSTimer timerWithTimeInterval:1.0
target:self
selector:@selector(fire:)
userInfo:nil
repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}
5秒経つと #fire: が呼ばれる。タイマーを無効化した後、マウスカーソルのキャプチャ処理 #saveCursorImage を呼出す。
- (void)fire:(NSTimer*)theTimer
{
static int t = 5;
if (t < 0) {
t = 5;
}
t--;
if (t < 0) {
[theTimer invalidate];
[self saveCursorImage];
}
}
#saveCursorImage での流れは次の通り。
(1) CGSNewConnection( ) でコネクションを確立
(2) コネクションを使い、マウスカーソルの画像を取得する。この時、CGSGetGlobalCursorDataSize( ) と CGSGetGlobalCursorData( ) を使う
(3) 得られたデータを CGImageRef へ変換する
(4) さらに CGImageRef を NSBitmapImagerep へ変換する
(5) 最後に NSData を経てファイル (PNG)へ書き出す
以下、具体的に追ってみよう。
まずは宣言から。プライベート関数を使うので自前でプロトタイプ宣言を行っておく。
typedef int CGSConnectionRef;
static CGSConnectionRef conn = 0;
extern CGError CGSNewConnection(void*, CGSConnectionRef*);
extern CGError CGSReleaseConnection(CGSConnectionRef);
extern CGError CGSGetGlobalCursorDataSize(CGSConnectionRef, int*);
extern CGError CGSGetGlobalCursorDa
必要な変数も定義しておこう。
-(void)saveCursorImage
{
int cursorDataSize;
int cursorRowBytes, colorDepth, components, cursorBitsPerComponent;
unsigned char *cursorData;
CGRect cursorRect;
CGPoint hotspot;
:
:
(1) CGSNewConnection( ) でコネクションを確立
if (CGSNewConnection(NULL, &conn) != kCGErrorSuccess) {
NSLog(@"CGSNewConnection error.\n");
return;
}
(2) コネクションを使い、マウスカーソルの画像を取得する。
if (CGSGetGlobalCursorDataSize(conn, &cursorDataSize) != kCGErrorSuccess) {
NSLog(@"CGSGetGlobalCursorDataSize error\n");
return;
}
cursorData = (unsigned char*) malloc(cursorDataSize);
CGError err = CGSGetGlobalCursorData(conn, cursorData, &cursorDataSize, &cursorRowBytes,
&cursorRect, &hotspot, &colorDepth, &components, &cursorBitsPerComponent);
if (err != kCGErrorSuccess) {
NSLog(@"CGSGetGlobalCursorData error\n");
return;
}
(3) 得られたデータを CGImageRef へ変換する
一旦、CGDataProvidreRef を経由させる。またCGImageRef を生成するのに カラースペースが必要なので用意する(これは最後に解放してやる)。
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGDataProviderRef providerRef = CGDataProviderCreateWithData(self, cursorData, cursorDataSize, nil);
// http://lists.apple.com/archives/quartz-dev//2008/Apr/msg00025.html
CGImageRef cgImageRef = CGImageCreate(cursorRect.size.width,
cursorRect.size.height,
cursorBitsPerComponent,
cursorBitsPerComponent * components,
cursorRowBytes,
colorSpace,
kCGImageAlphaPremultipliedLast,
providerRef,
nil,
NO,
kCGRenderingIntentDefault);
(4) さらに CGImageRef を NSBitmapImagerep へ変換する
これは簡単。CGImageRefなら、イニシャライザ一発でNSBitmapImageRepを作れる。
NSBitmapImageRep* bitmap = [[NSBitmapImageRep alloc] initWithCGImage:cgImageRef];
(5) 最後に NSData を経てファイル (PNG)へ書き出す
デスクトップへ cursor.png という名前(固定)で書き出す。
NSData* data = [bitmap representationUsingType:NSPNGFileType
properties:[NSDictionary dictionary]];
NSString* path =
[NSSearchPathForDirectoriesInDomains(NSDesktopDirectory, NSUserDomainMask, YES) objectAtIndex:0];
NSString* filename = [NSString stringWithFormat:@"%@/cursor.png", path];
[data writeToFile:filename atomically:YES];
-- - - - -
プラベート関数なのが惜しい!
なおこれが使えても、最終的には別途キャプチャした画像と合成する作業が残っている。座標を合わせるのが面倒そうだが。。
2008年8月16日土曜日
マウスカーソルのキャプチャ (2)
プライベート関数 CGSGetGlobalCursorData( ) を使い、表示中のマウスカーソルの画像をキャプチャしてみる。前回掲載した各種URLの情報を元にサンプルアプリを作ってみた。
様々なマウス形状を試せるように5秒間のタイマーを仕込んである。時間が来るとその時点でのマウスカーソルをキャプチャしてPNG画像として保存する。
いくつかマウスカーソルをキャプチャしてみた。
サンプルソース:MousePointer-1.zip
- - - -
いいかんじだ。しかしプライベート関数なのが惜しい。
コード解説は次回。
2008年8月15日金曜日
マウスカーソルのキャプチャ (1)
キャプチャにマウスカーソルを含めたい。ドキュメントを眺めたが見つからないので Googleでしたいので調べてみた。以下、分かったこと。
(1) 「これだ!」といううまい方法は無い
(2) CGSGetGlobalCursorData( ) を使う ※プライベート関数
(3) QDGetCursorDataを使う ※10.4 でdeprecated
(4) 表示されているマウスカーソルとは無関係に、カーソル画像を用意してキャプチャ画像に合成する
(2)(3)が有力だが、片や非公開のプライベート関数、もう一方は 10.4で廃止扱いの関数ときて決定的な方法が見つからなかった。ちょっとユニークだったのが (4)の方法。これは標準のグラブ(Grab.app)が採用している。
情報源:
(2)(3)関連
Apple Mailing List (Carbon.dev) - Getting the system cursor (マウスカーソルのキャプチャ方法について)
http://lists.apple.com/archives/Carbon-dev/2008/Feb/msg00211.html
Apple Mailing List (Quartz-dev)- Going round the bend with CGImageRef 16x16 (CGDataProviderCreateWithDataの利用例)
http://lists.apple.com/archives/quartz-dev//2008/Apr/msg00023.html
Member "x11vnc-0.9.4/x11vnc/macosxCG.c" of archive x11vnc-0.9.4.tar.gz (CGSGetGlobalCursorDataの利用例)
http://www.sfr-fresh.com/unix/privat/x11vnc-0.9.4.tar.gz:a/x11vnc-0.9.4/x11vnc/macosxCG.c
Google グループ cocoa-dev-japan - 在のマウスカーソル画像の取得 オプション
http://groups.google.com/group/cocoa-dev-japan/browse_thread/thread/6596d9a1f4d4ef86
(4)関連
Macの手書き説明書 - スクリーンショットにマウスポインタを含める方法
http://veadardiary.blog29.fc2.com/blog-entry-1290.html
- - - -
とりあえずあちこちで使われている(?)プライベート関数 CGSGetGlobalCursorData() を試してみよう。
2008年6月19日木曜日
メニューのキャプチャ(その12)Dockのメニュー
griffin-stewie さんより、Dockのメニューがあるとの指摘を受けた。早速 WindowListを調査してみる。
WindowListのタイマーを仕掛けてDockのメニューを開いておく。
出た。layer=101で普通のメニューであることがわかる。特徴としては Owner nameが"Dock"になっていること。
前回のサンプルでキャプチャしてみる。案の定、メニューバーが入っている。
処理としてはコンテキストメニューと同じなので、OwnerName=="Dock"を利用してフラグを立てた後、処理を合流させてやる。
AppController.m
// (2) search normal menus
for (i=0; i < CFArrayGetCount(window_list); i++) {
:
NSString *owner_name = (NSString*)CFDictionaryGetValue(window, kCGWindowOwnerName);
if ([owner_name isEqualToString:@"Dock"]) {
is_dockmenu = YES;
}
:
// (3) calcurate rect
if (is_contextmenu || is_dockmenu) {
rect_all = CGRectNull;
:
さて再チャレンジしてみよう。
できた。
ソース:MenuCapture-08.zip
- - - -
今度こそメニューが制覇できただろうか。
メニューのキャプチャ(その11)タイトルバーのメニュー
2008年6月18日水曜日
メニューのキャプチャ(その10)Spotlight
前回まででメニューキャプチャも完成と思いきや Spotlight のキャプチャがうまくできない。またもや WindowList で原因を調査してみよう。
下図のように Spotlight を使っている状態でウィンドウ情報を取って見る。
Spotlight は2つ現れていて、表の中の一番上のウィンドウがメニューバーのアイコンを表している。表の下から2番目の Spotlightが結果表示のウィンドウを表している。これを見ると Layerは23で通常のメニュー(kCGPopUpMenuWindowLevel=101)と異なっている。
なるほど。だから今までのコードではうまくキャプチャできないのか。Layer=23がヘッダファイルに無いか調べてみる。
CGWindowLevel.h
#define kCGBaseWindowLevel CGWindowLevelForKey(kCGBaseWindowLevelKey) /* INT32_MIN */
#define kCGMinimumWindowLevel CGWindowLevelForKey(kCGMinimumWindowLevelKey) /* (kCGBaseWindowLevel + 1) */
#define kCGDesktopWindowLevel CGWindowLevelForKey(kCGDesktopWindowLevelKey) /* kCGMinimumWindowLevel */
#define kCGDesktopIconWindowLevel CGWindowLevelForKey(kCGDesktopIconWindowLevelKey) /* kCGMinimumWindowLevel + 20 */
#define kCGBackstopMenuLevel CGWindowLevelForKey(kCGBackstopMenuLevelKey) /* -20 */
#define kCGNormalWindowLevel CGWindowLevelForKey(kCGNormalWindowLevelKey) /* 0 */
#define kCGFloatingWindowLevel CGWindowLevelForKey(kCGFloatingWindowLevelKey) /* 3 */
#define kCGTornOffMenuWindowLevel CGWindowLevelForKey(kCGTornOffMenuWindowLevelKey) /* 3 */
#define kCGDockWindowLevel CGWindowLevelForKey(kCGDockWindowLevelKey) /* 20 */
#define kCGMainMenuWindowLevel CGWindowLevelForKey(kCGMainMenuWindowLevelKey) /* 24 */
#define kCGStatusWindowLevel CGWindowLevelForKey(kCGStatusWindowLevelKey) /* 25 */
#define kCGModalPanelWindowLevel CGWindowLevelForKey(kCGModalPanelWindowLevelKey) /* 8 */
#define kCGPopUpMenuWindowLevel CGWindowLevelForKey(kCGPopUpMenuWindowLevelKey) /* 101 */
#define kCGDraggingWindowLevel CGWindowLevelForKey(kCGDraggingWindowLevelKey) /* 500 */
#define kCGScreenSaverWindowLevel CGWindowLevelForKey(kCGScreenSaverWindowLevelKey) /* 1000 */
#define kCGCursorWindowLevel CGWindowLevelForKey(kCGCursorWindowLevelKey) /* 2000 */
#define kCGOverlayWindowLevel CGWindowLevelForKey(kCGOverlayWindowLevelKey) /* 102 */
#define kCGHelpWindowLevel CGWindowLevelForKey(kCGHelpWindowLevelKey) /* 200 */
#define kCGUtilityWindowLevel CGWindowLevelForKey(kCGUtilityWindowLevelKey) /* 19 */
#define kCGAssistiveTechHighWindowLevel CGWindowLevelForKey(kCGAssistiveTechHighWindowLevelKey) /* 1500 */
あ、23が見当たらない。。
まあいいか。定義は無いが Layer=23を狙い撃ちしてみよう。下記コードを追加する。
} else if (layer == 23) {
[menu_windows addObject:[NSNumber numberWithInt:window_id]];
CGRectMakeWithDictionaryRepresentation(CFDictionaryGetValue(window, kCGWindowBounds), &rect);
if (CGRectEqualToRect(rect_all,CGRectZero)) {
rect_all = rect;
} else {
rect_all = CGRectUnion(rect, rect_all);
}
is_spotlight = YES;
}
layer=23 の場合もキャプチャ対象とした上でフラグ is_spotlight を立てておく。
その上でその後のステータスバーの処理へ合流させる。
} else if (is_status_bar || is_spotlight) {
:
これだけでOK。結果はこんな感じ。
ソース:MenuCapture-07.zip
- - - -
これでメニューは制覇できただろうか。
2008年6月17日火曜日
メニューのキャプチャ(その9)ステータスバー
前回までのサンプルでステータスバーのメニューをキャプチャするとメニュー全体が含まれてしまう。
この場合は右上のステータスバーだけをキャプチャに含めたい。さてどうするか。
下図のようにステータスバーのメニューを出した状態で WindowListを調べてみた。
上2つが開いているメニューとサブメニューを表す。ステータスバーは Layer=25 ( kCGStatusWindowLevel ) で表される、ここでは4つのウィンドウが該当する。上から順番に次のようになる(名称は OwnerName)。
SystemUIServer ステータスバーのアイコン群を表す(図では Spaces から「湖」※ユーザ名までが範囲)
SimpleCap これはステータスバー左のカメラのアイコンを表す
Spotlight これは一番右の Spotlightアイコンを表す
SystemUIServer 最後のものは透明なウィンドウでキャプチャには無関係。
開いているウィンドウがステータスバーに所属するかどうかを判断する材料がないか、WindowListをじっと眺めて共通項を探してみる。
すると OwnerPIDが使えそうなのがわかった。今回の場合、メニュー2つと、ステータスバー(SystemUIServer)の OwnerPID が一致しているのがわかる。これを判断に使ってみよう。
で、結果から先に話すとうまくいった。こんな感じ。
悪くない。
サンプル:MenuCapture-06.zip
コードが長いので今回は細かい解説を割愛するが、アルゴリズムは次のようになっている。番号はソースコード上のコメントに対応している。
(1) ステータスーバーのウィンドウ情報を最初に専用の配列へ保管しておく。
(2) メニューウィンドウをすべて配列に保管する。
この時、(1)のウィンドウと OnwerPIDが同じものがあるかどうか調べておく。
(3) 次の3通りのケースについてキャプチャ範囲を調整する
[a] Context Menu
[b] ステータスバーのメニュー
[c] 通常のメニュー
(4) (3)のサイズがスクリーンをはみ出した場合、スクリーンに収まるように補正する
最後にキャプチャ画像を作成する。
アルゴリズムが泥臭いせいもあってコードはわかりづらいかもしれない。
念のため通常メニューの動作確認。問題なし。
コンテキストメニューも大丈夫。
- - - -
それにしても WindowListは役に立つ。これはこれでちゃんとしたツールに仕上げておきたいな。
2008年6月16日月曜日
メニューのキャプチャ(その8)メニューバーを含める(2)
前回メニューバーを入れるようにしたがコンテキストメニューのキャプチャでも入るようになってしまった。コンテキストメニューの場合はメニューバーを入れないようにする。
コンテキストメニューかどうかはウィンドウ名でしか判別できないようなのでこれで判断する。ちょっと泥臭いが手を加える。
AppController.m
int menubar_window_id;
for (i=0; i < CFArrayGetCount(list); i++) {
w = CFArrayGetValueAtIndex(list, i);
CFNumberGetValue(CFDictionaryGetValue(w, kCGWindowLayer),
kCFNumberIntType, &layer);
CFNumberGetValue(CFDictionaryGetValue(w, kCGWindowNumber),
kCGWindowIDCFNumberType, &window_id);
if (layer == kCGPopUpMenuWindowLevel) {
NSString *window_name = (NSString*)CFDictionaryGetValue(w, kCGWindowName);
if ([window_name isEqualToString:@"ContextMenu"]) {
is_contextmenu = YES;
}
CGRectMakeWithDictionaryRepresentation(CFDictionaryGetValue(w, kCGWindowBounds), &rect);
if (CGRectEqualToRect(rect_all,CGRectZero)) {
rect_all = rect;
} else {
rect_all = CGRectUnion(rect, rect_all);
}
windowIDs[widx++] = window_id;
} else if (layer == kCGMainMenuWindowLevel) {
menubar_window_id = window_id;
}
}
if (!is_contextmenu) {
windowIDs[widx++] = menubar_window_id;
rect_all = CGRectUnion(CGRectZero, rect_all);
rect_all.size.width += 20;
rect_all.size.height += 25;
} else {
rect_all = CGRectNull;
}
これでOK。
ソース:MenuCapture-05.zip
2008年6月15日日曜日
メニューのキャプチャ(その7)メニューバーを含める
メニューのキャプチャはできたが、画面一番上のメニューバー部分が入っていない。せっかくなのでメニューバーもキャプチャ画像へ入れたい。やてみよう。
まずメニューバーがどんなウィンドウかを WindowList で調べる。
この中の "Shared Menubar" が目的のウィンドウのようだ。layer=24を CGWindowLevel.h で調べると定義が見つかった。
CGWindowLevel.h
#define kCGMainMenuWindowLevel CGWindowLevelForKey(kCGMainMenuWindowLevelKey) /* 24 */
これを狙い撃ちしてキャプチャ画像へ入れてみる。前回のコードに kCGMainMenuWindowLevel のケースも window_id に含めるコードを加える。
AppController.m
if (layer == kCGMainMenuWindowLevel) {
windowIDs[widx++] = window_id;
}
ただメニューバーはスクリーンと同じ横幅を持っているので大きすぎる。できれば目的のメニューの幅だけ切り取れるようにしたい。そこでメニューの大きさを取っておく。サブメニューを含めることを考えてメニューの大きさを CGRectUnion で加えてすべて含む領域を作成する。
CGRect rect = CGRectZero;
CGRect rect_all = CGRectZero;
:
if (layer == kCGPopUpMenuWindowLevel) {
CGRectMakeWithDictionaryRepresentation(CFDictionaryGetValue(w, kCGWindowBounds), &rect);
rect_all = CGRectUnion(rect, rect_all);
windowIDs[widx++] = window_id;
}
初期値を CGRectZero にしておくとうまい具合にメニューバーも含まれる。
結果はこう。うまくいった。
深いサブメニューもこの通り。
なお kCGWindowBounds で取得できる CGRect はメニューの影を含んでいない。この為、サイズを最後に補正してある。
rect_all.size.width += 20;
rect_all.size.height += 25;
安易な方法だが実用上はこれで十分だろう。
ソース:MenuCapture-04.zip
2008年6月14日土曜日
メニューのキャプチャ(その6)サブメニュー(2)
サブメニューのキャプチャ画像を作ってみる。複数のウィンドウのキャプチャには CGWindowListCreateImageFromArray( ) が使える。使い方は以前画面キャプチャその18で紹介した。
主要なコードを下記へ示す。
AppController.m
CGWindowID *windowIDs = calloc(20, sizeof(CGWindowID));
for (i=0; i < CFArrayGetCount(list); i++) {
w = CFArrayGetValueAtIndex(list, i);
CFNumberGetValue(CFDictionaryGetValue(w, kCGWindowLayer),
kCFNumberIntType, &layer);
CFNumberGetValue(CFDictionaryGetValue(w, kCGWindowNumber),
kCGWindowIDCFNumberType, &window_id);
if (layer == kCGPopUpMenuWindowLevel) {
windowIDs[widx++] = window_id;
}
}
if (widx > 0) {
NSString* path = [NSSearchPathForDirectoriesInDomains(NSDesktopDirectory, NSUserDomainMask, YES) objectAtIndex:0];
NSString* filename = [path stringByAppendingPathComponent:@"cocoa_days_menu.png"];
CFArrayRef windowIDsArray = CFArrayCreate(kCFAllocatorDefault, (const void**)windowIDs, widx, NULL);
CGImageRef cgimage = CGWindowListCreateImageFromArray(CGRectNull, windowIDsArray, kCGWindowImageDefault);
NSBitmapImageRep *bitmap_rep = [[[NSBitmapImageRep alloc] initWithCGImage:cgimage] autorelease];
NSImage *image = [[[NSImage alloc] init] autorelease];
[image addRepresentation:bitmap_rep];
NSData* data = [bitmap_rep representationUsingType:NSPNGFileType
properties:[NSDictionary dictionary]];
[data writeToFile:filename atomically:YES];
}
free(windowIDs);
タイマー発火直後に layer==kCGPopUpMenuWindowLevel (101) のウィンドウを探し配列 windowIDs へ格納しておく。そしてこの配列を CFArrayRef に変換し、CGWindowListCreateImageFromArray ( )へ渡す。
実行結果。うまくいった。
3階層でもOK。
ソース:MenuCapture-03.zip
2008年6月9日月曜日
メニューのキャプチャ(その1)
SimpleCapではメニューを簡単にキャプチャしたい。その為の調査を少しずつ行っていく。
まずはメニューがそのように扱われているのか、以前作成したウィンドウリストアプリ(WindowList)で調べてみる。WindowList は画面に表示されているウィンドウのリストを表示するもの。
この以前のプログラムにタイマー機能を持たせてメニューを開いている時のウィンドウリストを見てみよう。
タイマーを仕掛け、メニューを開いてみる。Finder のメニューを開いてみた。
すると WindowList に Layer=101 でメニューが現れた。OwnerNameから Finderのメニューであることがわかる。実際(画像の右側が切れているが)PIDは Finderと同じものを示している。Nameはアップルマークになっている。
この Layer=101 は NSWindowのレベルを示していると思われ、実際ヘッダファイルを見ると定義があった。
CGWindow.h
/* Definitions of older constant values as calls */
#define kCGBaseWindowLevel CGWindowLevelForKey(kCGBaseWindowLevelKey) /* INT32_MIN */
#define kCGMinimumWindowLevel CGWindowLevelForKey(kCGMinimumWindowLevelKey) /* (kCGBaseWindowLevel + 1) */
#define kCGDesktopWindowLevel CGWindowLevelForKey(kCGDesktopWindowLevelKey) /* kCGMinimumWindowLevel */
#define kCGDesktopIconWindowLevel CGWindowLevelForKey(kCGDesktopIconWindowLevelKey) /* kCGMinimumWindowLevel + 20 */
#define kCGBackstopMenuLevel CGWindowLevelForKey(kCGBackstopMenuLevelKey) /* -20 */
#define kCGNormalWindowLevel CGWindowLevelForKey(kCGNormalWindowLevelKey) /* 0 */
#define kCGFloatingWindowLevel CGWindowLevelForKey(kCGFloatingWindowLevelKey) /* 3 */
#define kCGTornOffMenuWindowLevel CGWindowLevelForKey(kCGTornOffMenuWindowLevelKey) /* 3 */
#define kCGDockWindowLevel CGWindowLevelForKey(kCGDockWindowLevelKey) /* 20 */
#define kCGMainMenuWindowLevel CGWindowLevelForKey(kCGMainMenuWindowLevelKey) /* 24 */
#define kCGStatusWindowLevel CGWindowLevelForKey(kCGStatusWindowLevelKey) /* 25 */
#define kCGModalPanelWindowLevel CGWindowLevelForKey(kCGModalPanelWindowLevelKey) /* 8 */
#define kCGPopUpMenuWindowLevel CGWindowLevelForKey(kCGPopUpMenuWindowLevelKey) /* 101 */
#define kCGDraggingWindowLevel CGWindowLevelForKey(kCGDraggingWindowLevelKey) /* 500 */
#define kCGScreenSaverWindowLevel CGWindowLevelForKey(kCGScreenSaverWindowLevelKey) /* 1000 */
#define kCGCursorWindowLevel CGWindowLevelForKey(kCGCursorWindowLevelKey) /* 2000 */
#define kCGOverlayWindowLevel CGWindowLevelForKey(kCGOverlayWindowLevelKey) /* 102 */
#define kCGHelpWindowLevel CGWindowLevelForKey(kCGHelpWindowLevelKey) /* 200 */
#define kCGUtilityWindowLevel CGWindowLevelForKey(kCGUtilityWindowLevelKey) /* 19 */
メニューのレベルは、kCGPopUpMenuWindowLevel (101) で定義されている。ということは Layer=101 (kCGPopUpMenuWindowLevel) を狙い撃ちすればメニューがキャプチャできるかもしれない。
ついでにコンテキストメニューも調べておこう。
タイマーを仕掛けコンテキストメニューを出して見る。
出た。Layer=101で名前は「ContextMenu」。
ソース:WindowList-5.zip