追加情報。
作る前にさらに調査。
Toolbar Programming Topics for Cocoa
http://developer.apple.com/documentation/Cocoa/Conceptual/Toolbars/Toolbars.html
ADCにツールバーの作り方が載っていた。これは参考になりそう。
Cocoaアプリケーションのための ソフトウェア・パターン
http://hmdt-web.net/seminar/CocoaSoftwarePattern/CocoaSoftwarePattern.pdf
HMDT木下さんの資料。環境設定のパターンの中でツールバーパターンについて具体的に取り上げている。P.93の「ツールバーパターンのクラス構成」は実際にアプリに組み込む上でとても参考になる。
2008年8月31日日曜日
ツールバー(その2)
2008年8月30日土曜日
ツールバー(その1)
さて、そろそろアプリケーションとしての体裁を整える事にする。機能は一通りできたので設定ダイアログ(プリファレンス)を用意する。プリファレンスと言えばツールバー。そこでツールバーの実装に入る。
さっそく情報集め。Googleで検索するといくつかヒットした。
How to Use Toolbars - Cocoa Dev Centrale
http://cocoadevcentral.com/articles/000037.php
Creating a Custom Cocoa Toolbar - Subjective C
http://subjectivec.wordpress.com/2008/07/07/creating-a-custom-toolbar/
ツールバー - Cocoaはやっぱり!#出張版
http://www.big.or.jp/~crane/cocoa/9000_mdo/MDOnline_2001.12.pdf
古い情報が多い。
まずはサンプルを作ってみよう。
2008年8月29日金曜日
2008年8月28日木曜日
2008年8月27日水曜日
マウスカーソルのキャプチャ (13)
さて今度はメニューキャプチャにマウスカーソルを合成する。メニューキャプチャでは以前紹介した Windowクラスは使わずベタで CGWindowListCopyWindowInfo( )を使っていた。これに手をいれて Windowベースの実装へ変更する。こうすると CaptureController側のマウスカーソル画像合成はウィンドウほかの共通処理が使える。
早速修正処理に入るがコードがちょっとわかりづらい(自分で書いたのだが。。)。そこで一旦メニュー処理を整理してみた。
まずコードは何をやっているのかを追ってみた。
おおざっぱすぎてよくわからない。もう少し詳細に追って見る。
なんとなくやりたいことが見えてきた。つまり(当時自分で考えたことは)こういうこと。
メニューの種類によって、メインメニューバーやステータスバーを追加する処理を加えている。
例えばこんなのや、
こんなの、
そしてこんな感じ。
その為に処理が若干わかりづらくなっている。今回 Window対応するのに合わせてここをリファクタリングしてしまおう。
(続く)
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月23日土曜日
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年8月14日木曜日
アプリケーション起動完了を捉える (2)
NSWorkspace の メソッドを眺めていると他にもアプリケーション起動のメソッドが見つかった。
Launch Options
enum {
NSWorkspaceLaunchAndPrint = 0x00000002,
NSWorkspaceLaunchInhibitingBackgroundOnly = 0x00000080,
NSWorkspaceLaunchWithoutAddingToRecents = 0x00000100,
NSWorkspaceLaunchWithoutActivation = 0x00000200,
NSWorkspaceLaunchAsync = 0x00010000,
NSWorkspaceLaunchAllowingClassicStartup = 0x00020000,
NSWorkspaceLaunchPreferringClassic = 0x00040000,
NSWorkspaceLaunchNewInstance = 0x00080000,
NSWorkspaceLaunchAndHide = 0x00100000,
NSWorkspaceLaunchAndHideOthers = 0x00200000,
NSWorkspaceLaunchDefault = NSWorkspaceLaunchAsync | NSWorkspaceLaunchAllowingClassicStartup
};これを見るとデフォルト(NSWorkspaceLaunchDefault)に非同期起動(NSWorkspaceLaunchAsync)が含まれている。ということはこのオプションを付けなければ同期起動(アプリ起動完了まで待つ)ということか。
早速試してみる。
[[NSWorkspace sharedWorkspace]
launchAppWithBundleIdentifier:@"com.apple.dashboardlauncher"
options:0 additionalEventParamDescriptor:nil launchIdentifier:nil]
以前の #launchAppliction: と異なり、アプリケーションを BundleIdentiferで指定する必要がある。Dashboardは "com.apple.dashboardlauncher"だった。
実行結果は意図通り。sleepでタイミングを計らなくともキャプチャが動作するようになった。
- - - -
ただそれでもタイミングによる微妙な問題が残っているため結局 0.5秒のディレイを入れてある。これは Dashboard起動時に Widgetが画面の外から入ってくるようなアニメーションを行なうが、このアニメーション中にキャプチャ処理が開始されるとアニメーション途中のウィンドウがキャプチャ対象になってしまうため。使っている環境によってはこの 0.5秒では足りないかもしれない。うーむ。
2008年8月13日水曜日
アプリケーション起動完了を捉える
Dashboardの起動完了をどう取るか。NSWorkspaceでアプリケーションを起動した場合、NSWorkspaceDidLaunchApplicationNotification という通知を受け取れるようだ。
まず notificationCenter へ通知受取先を登録する。
[[[NSWorkspace sharedWorkspace] notificationCenter]
addObserver:self selector:@selector(didLaunchApplication:)
name:NSWorkspaceDidLaunchApplicationNotification object:nil];
通知を受け取るメソッド。ここでは通知内容をログへ表示する。
- (void)didLaunchApplication:(NSNotification *)notification
{
NSLog(@"%@", notification);
}
試してみる。辞書(Dictionary.app)を起動すると下記の通知が来た。
SimpleCap[9476:10b] NSConcreteNotification 0x1a2ea0 {name = NSWorkspaceDidLaunchApplicationNotification; object = ; userInfo = {
NSApplicationBundleIdentifier = "com.apple.Dictionary";
NSApplicationName = "\U8f9e\U66f8";
NSApplicationPath = "/Applications/Dictionary.app";
NSApplicationProcessIdentifier = 9480;
NSApplicationProcessSerialNumberHigh = 0;
NSApplicationProcessSerialNumberLow = 2650759;
}} よしDashboardは?....通知が来ない。元々常駐しているからか?
さてどうしたものか。
2008年8月12日火曜日
SimpleCap (39) Widgetのキャプチャ
さていよいよ SimpleCap に処理を入れる。手こずると思いきやあっけなくできた。
メニューから "Widget"を選ぶと Dashboardが起動し、ウィンドウのキャプチャが始まる。
できました。
複数の Widgetも問題なし。
コードはほとんど書いていない。通常キャプチャ方式が増えると Handlerプロトコルに準拠したクラスを一つ作らなければならない。今回も WidgetHandler を用意した。ところで機能的にはウィンドウキャプチャと変わらないのでこれを再利用することができる。そこで WindowHandlerクラスを継承させる。
WidgetHandler.h
#import
#import "WindowHandler.h"
@interface WidgetHandler : WindowHandler {
}
@end
これだけ。後はこのクラスを使う前に Dashboardを起動しておいてやるだけ。これだけで Widgetも今までのウィンドウキャプチャと同じ機能が実現できる。おお、オブジェクト指向やってて一番得した気分だ。
実際にはターゲットのウィンドウの種類が若干違うため、下記のコードだけ足してある。
WidgetHandler.m
@implementation WidgetHandler
- (BOOL)isWindow:(CFDictionaryRef)window normalWindow:(BOOL)normal
{
int layer;
CFNumberGetValue(CFDictionaryGetValue(window, kCGWindowLayer),
kCFNumberIntType, &layer);
CFStringRef owner_name;
owner_name = CFDictionaryGetValue(window, kCGWindowOwnerName);
if ([@"Dock" isEqualToString:(NSString*)owner_name] && (layer == 100)) {
return YES;
}
return NO;
}
@end
このメソッドはウィンドウが目的の種類かどうかをチェックするもの。元々はキャプチャ処理クラスのベースクラスである HandlerBaseに実装されていて、通常のウィンドウを対象にしたコードが書かれている。これを WidgetHandlerでオーバーライドして Widgetを対象にするようにした。これで WidgetHanlder の親クラス WindowHandler ををコード修正なしに振る舞いを変えられた。いいなオブジェクト指向は(くどい)。
- - - -
とまあいい事ばかり書いたが、困った点が一つある。Dashboardの起動が完了した状態でないとキャプチャがうまく働かない。当然でキャプチャ処理はその時点で表示されているウィンドウが対象であるため、確実にDashboard起動が完了した後(Widgetが表示された後)にキャプチャを開始させる必要がある。今はインチキして sleep(3)を入れてある。
[NSWorkspace sharedWorkspace] launchApplication:@"/Applications/Dashboard.app"];
[NSThread sleepForTimeInterval:3];
[_capture_controller startCaptureWithHandlerName:@"Widget" withObject:nil];
ちょっとしたプロセス監視が必要になるな。またプロセスが立ち上がったとしても Widgetの表示が完了しているとは限らない。さてどうしたものか。
2008年8月11日月曜日
Widgetの再調査
以前 Widgetについて調査したことがあったが(画面キャプチャその11 - Widgetの取り込み(2) )、WindoListでもう一度見直してみる。
Dashboardを起動すると表の上6つのウィンドウが追加された。
・1番上は左下の Widget追加ボタン(十字のやつ)
・続く4つは Widget。Layer == 100
・最後の1つは画面全体を覆う黒い半透明のウィンドウ。サイズが画面全体(0,0,1440,900)で、Alphaが 0.44(半透明)であることがわかる。
キャプチャするには Dashboardを起動して、layer==100 のウィンドウをターゲットにすれば良さそうだ。
2008年8月10日日曜日
Dashboard を起動する
2008年8月9日土曜日
SimpleCap (38) 範囲選択:Shiftキーによる平行移動
範囲選択で平行移動ができると便利だ。エクセルなどでは Shiftキーを押しながら図形を移動するとX軸、またはY軸方向のどちらかに沿っての平行移動が可能になる。
範囲選択(RubberBand)の移動個所に手を入れてみた。
SelectionHandler.m
NSPoint p_cp = cp;
int constrain_mode = 0;
while ([theEvent type] != NSLeftMouseUp) {
theEvent = [window nextEventMatchingMask:(NSLeftMouseDraggedMask | NSLeftMouseUpMask)];
cp = [view convertPoint:[theEvent locationInWindow] fromView:nil];
BOOL is_shiftkey = ([theEvent modifierFlags] & NSShiftKeyMask) ? YES : NO;
// constrain rule (1)
if (is_shiftkey) {
CGFloat dcpx = fabs(cp.x - p_cp.x);
CGFloat dcpy = fabs(cp.y - p_cp.y);
if (constrain_mode == 0) {
constrain_mode = (dcpx > dcpy) ? 1 : 2;
} else {
if (dcpx == 0 && dcpy > 5.0) {
constrain_mode =2;
} else if (dcpx > 5.0 && dcpy == 0) {
constrain_mode =1;
}
}
switch (constrain_mode) {
case 1: // horizontal moving
_rect.origin.x = cp.x + dx;
break;
case 2: // vertical moving
_rect.origin.y = cp.y + dy;
break;
default:
break;
}
} else {
_rect.origin.x = cp.x + dx;
_rect.origin.y = cp.y + dy;
}
:
:
p_cp = cp;
}
イベントループを形成(while)し、マウスのドラッグに合わせて RubberBandの位置を調整している。Shiftキーが押された場合には constrain_modeを設定し、水平方向または垂直方向のみに移動を強制するようにしている(switch文の個所)。
ただエクセルでもそうだが、例えば垂直方向に移動している途中で、左右に少し大きな移動を行なうと水平方向の平行移動に切り替わる。これは便利なので SimpleCap でもまねする。上記コードで下記の個所がモードきりかえの部分。
if (constrain_mode == 0) {
constrain_mode = (dcpx > dcpy) ? 1 : 2;
} else {
if (dcpx == 0 && dcpy > 5.0) {
constrain_mode =2;
} else if (dcpx > 5.0 && dcpy == 0) {
constrain_mode =1;
}
}最初の ==0 は初期処理。その後の if分が途中でのモード切り替え。例えば、水平方向の移動量が0(dcpx==0)で、垂直方向の移動量が5.0以上(dcpy > 5.0)なら、移動モードを垂直方向に切り替える。 "5.0" は実際に試してみて感覚的に決めた数値。
投稿者
xcatsan
時刻:
5:54
ラベル: RubberBand, SimpleCap
2008年8月8日金曜日
SimpleCap (37) TrackWindow枠線の改良
枠線がちょっとしょぼいので手を入れることにする。
こんな感じ。
拡大。
単に枠を表示するだけではわかりづらいので透明度を変えてゆっくりと点滅させている。
点滅の周期には sin関数を使ってみた。
1周期 6秒で変化する。
ただなんとなくしっくりこない。感覚的には「溜め」が欲しい。
そこでしきい値をもうけて「溜め」を作り出すことにした。イメージはこんな感じ。
コードはこんな感じ。
#define PHASE_SEC 30
int f = counter % PHASE_SEC;
CGFloat alpha = 0.5 - fabs(sinf((float)f/PHASE_SEC*M_PI))/2.0;
if (alpha > 0.2) {
alpha = 0.2;
}
counter は 0.1秒毎にインクリメントされる。alphaを枠線の透明度として使う。
2008年8月7日木曜日
SimpleCap (36) TrackWindow改良
2008年8月6日水曜日
SimpleCap (35) Previewのウィンドウ
Preview.app をキャプチャしていたら右下の拡大縮小アイコンがウィンドウであることが分かった。
右下の部分がそう。
この画像。
Windowsでは一番上のウィンドウを最初に選択状態にする為、Preview.app がアクティブな場合、この画像が最初に選択される。何よりも Track Window では現在の仕様ではウィンドウの切り替えができないので、Preview.appをキャプチャしたい場合困る(まあ実際そんなケースはかなり少ないとは思うけど。。)
Excelでも Track Windowを実行するとワークシートではなくツールバーがヒットしてしまう。
Track Window もターゲットもターゲットのウィンドウをユーザが選べるようにする必要があるな。
2008年8月5日火曜日
2008年8月4日月曜日
SimpleCap (33) アプリケーションキャプチャ(4)
PIDがわたってくれば後は簡単。Windowsキャプチャなどと同じ方法でキャプチャできる。違いは条件にPIDが加わるだけ。
CGImageRef を作成するコードは次のようになる。
ApplicationHandler.m
- (CGImageRef)capture
{
CFDictionaryRef window;
CGRect cgrect = CGRectNull;
CGImageRef cgimage = nil;
int window_id;
CFIndex i;
NSMutableArray *application_windows = [NSMutableArray array];
int pid = [[_application objectForKey:@"pid"] intValue];
int owner_pid;
CFArrayRef window_list = [self getWindowListWindowID:kCGNullWindowID];
for (i=0; i < CFArrayGetCount(window_list); i++) {
window = CFArrayGetValueAtIndex(window_list, i);
CFNumberGetValue(CFDictionaryGetValue(window, kCGWindowOwnerPID),
kCFNumberIntType, &owner_pid);
CFNumberGetValue(CFDictionaryGetValue(window, kCGWindowNumber),
kCGWindowIDCFNumberType, &window_id);
if (pid == owner_pid) {
[application_windows addObject:[NSNumber numberWithInt:window_id]];
}
}
if ([application_windows count]) {
CGWindowID *windowIDs = calloc([application_windows count], sizeof(CGWindowID));
int widx;
for (widx=0; widx < [application_windows count]; widx++) {
windowIDs[widx] = [[application_windows objectAtIndex:widx] intValue];
}
CFArrayRef windowIDsArray = CFArrayCreate(kCFAllocatorDefault, (const void**)windowIDs, widx, NULL);
cgimage = CGWindowListCreateImageFromArray(cgrect, windowIDsArray, kCGWindowImageDefault);
free(windowIDs);
CFRelease(windowIDsArray);
}
CFRelease(window_list);
return cgimage;
}
ターゲットのアプリケーションの PIDと OwnerPIDが一致するウィンドウを一旦 application_windows へ入れておき、最後にこれを CGWindowListCreateImageFromArray へ渡してキャプチャ画像を得る。
実行してみる。Finderをターゲットとしてみよう。

メニューから Application > Finder を選択する。

タイマーが起動し、しばらく待つとキャプチャ画像ができる。いい感じだ。

IntefaceBuilder の接続線もこの通り。

- - - -
元々 Application キャプチャを作ろうと思い立ったのは InterfaceBuilderの接続線を撮りたかったから。ついにこれもキャプチャできるようになりうれしい。
2008年8月3日日曜日
SimpleCap (32) アプリケーションキャプチャ(3)
前回の補足から。"Application"メニューが選択された場合のメッセージフローは次のようになる。
"Application"が選択されると NSMenuの Delegate先である AppController の menuWillOpen: が呼出される。この中で LauncedApplications#updateApplicationMenu: を呼出し、メニューに表示するアプリケーション一覧を更新している。
アプリケーションが選択された場合のフローは次のようになる。
まず AppController#selectApplicationMenu: が呼ばれる。これはアプリケーション一覧を作成する時、NSMenuItemのターゲットに AppController、アクションに selectApplicationMenu: を設定している為。
続いて通常のキャプチャシーケンス(図の (2)(3))が流れる。
ApplicationHandler では後々選択されたアプリケーションの情報を使うので、selectApplicationMenu: の引数にこれを渡してやる。アプリケーション情報は NSWorkspaceで取得した情報とアイコン画像を元に作成したもの。
アプリケーション情報の例:
2008-07-03 23:37:21.691 SimpleCap[3572:10b] {
image = NSImage 0xc8084c0 Size={32, 32} Reps=(
NSIconRefBitmapImageRep 0xc808da0 Size={128, 128} ColorSpace=NSCalibratedRGBColorSpace BPS=8 BPP=32 Pixels=128x128 Alpha=YES Planar=NO Format=0,
NSIconRefBitmapImageRep 0xc808e00 Size={512, 512} ColorSpace=NSCalibratedRGBColorSpace BPS=8 BPP=32 Pixels=512x512 Alpha=YES Planar=NO Format=0,
NSIconRefBitmapImageRep 0xc808e40 Size={32, 32} ColorSpace=NSCalibratedRGBColorSpace BPS=8 BPP=32 Pixels=32x32 Alpha=YES Planar=NO Format=0,
NSIconRefBitmapImageRep 0xc808e80 Size={16, 16} ColorSpace=NSCalibratedRGBColorSpace BPS=8 BPP=32 Pixels=16x16 Alpha=YES Planar=NO Format=0
);
name = "\U30bf\U30fc\U30df\U30ca\U30eb";
pid = 2771;
}- - - -
お膳立てはできた。次は実際のキャプチャ処理に入っていこう。
2008年8月2日土曜日
SimpleCap (31) アプリケーションキャプチャ(2)
メニューで "Application" を選択した時にその時点でのアプリケーション一覧をメニューに表示する。
これを実現するには "Application" が選択された時のイベントを捉える必要がある。これは NSMenuの Delegateを使えば簡単にできる。
IBOutlet NSMenu *_app_menu
として、IntefaceBuilderで "Application"の NSMenuへ接続し、コントローラーの中で NSMenu#setDelegate を呼出す。
[_app_menu setDelegate:self]
すると下記の Delegate 先に下記のメッセージが送信される。
menu:updateItem:atIndex:shouldCancel:
menu:willHighlightItem:
menuDidClose:
menuHasKeyEquivalent:forEvent:target:action:
menuNeedsUpdate:
menuWillOpen:
numberOfItemsInMenu:
今回は #menuWillOpen: を使う。こんな感じ。
- (void)menuWillOpen:(NSMenu *)menu
{
// メニュー更新コード(この時点で起動しているアプリの一覧で NSMenuItem作成)
}










