ページ

2008年3月31日月曜日

RoundRect と NSShadow

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

NSShadowの使い方が分かったので RoundRect と組み合わせてみた。

まず表示から。問題なし。


次にファイル書き出し。こちらも問題なし。


ソース:RoundRect-4


と、まあ簡単に実現できたのだけれど、コードはちょっとダサいかもしれない。。

表示の方のコード。

- (void)drawRect:(NSRect)rect {
// Drawing code here.
[NSGraphicsContext saveGraphicsState];
NSShadow* shadow = [[NSShadow alloc] init];
[shadow setShadowOffset:NSMakeSize(10.0, -10.0)];
[shadow setShadowBlurRadius:3.0];
[shadow setShadowColor:[[NSColor blackColor] colorWithAlphaComponent:0.3]];
[shadow set];
[[NSColor whiteColor] set];
[_clip_path fill];
[NSGraphicsContext restoreGraphicsState];

[NSGraphicsContext saveGraphicsState];
[_clip_path setClip];
[self lockFocus];
[_image drawInRect:NSMakeRect(50, 50, 440-50, 320-50)
fromRect:NSZeroRect
operation:NSCompositeSourceOver
fraction:1.0];
[self unlockFocus];
[NSGraphicsContext restoreGraphicsState];
}


以前の RoundRect のコードの前半に影を描画するコードを追加してある。クリッピングで使う RoundRectのパスを使い白色で塗りつぶしている。この行為により影が描かれる。その後、画像で上書きしている。なお数値の50は NSView内に影が表示できるようにサイズ調整している。


次は保存コード。こちらも同じ方法。ポイントだけ抜粋する。
 [rimg lockFocus];
[NSGraphicsContext saveGraphicsState];
NSShadow* shadow = [[NSShadow alloc] init];
[shadow setShadowOffset:NSMakeSize(10.0, -10.0)];
[shadow setShadowBlurRadius:3.0];
[shadow setShadowColor:[[NSColor blackColor] colorWithAlphaComponent:0.3]];
[shadow set];
[[NSColor whiteColor] set];
[round fill];
[NSGraphicsContext restoreGraphicsState];

[round setClip];
[_image compositeToPoint:NSMakePoint(25, 25) operation:NSCompositeSourceOver];
[rimg unlockFocus];


- - - -
画像の四隅を丸くし、影を付けることができた。どちらも専用のクラスが用意されているので簡単だった。cocoa はホント良くできてる。

2008年3月30日日曜日

NSShadow

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

NSShadow を使って影を付けてみる。元ネタは ADCのドキュメントより。
ADC:Adding Shadows to Drawn Paths



ソース:Shadow.zip

MyView.h

- (void)drawRect:(NSRect)rect {
// Drawing code here.
[NSGraphicsContext saveGraphicsState];
NSShadow* shadow = [[NSShadow alloc] init];
[shadow setShadowOffset:NSMakeSize(10.0, -10.0)];
[shadow setShadowBlurRadius:3.0];
[shadow setShadowColor:[[NSColor blackColor] colorWithAlphaComponent:0.3]];
[shadow set];

[_image drawInRect:NSMakeRect(50, 50, 200, 180)
fromRect:NSZeroRect
operation:NSCompositeSourceOver
fraction:1.0];

[NSGraphicsContext restoreGraphicsState];
[shadow release];
}


NSShadowを作り、#set を実行する。その後の描画に対して影が付く。実に簡単。よくできているな。

- - - -
さて次回はRoundRectにこれを組み合わせてみる。

2008年3月29日土曜日

RoundRect その3

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

今回は読み込んだ画像の四隅を丸くして別名で保存してみる。前回のコードを流用する。


"save"ボタンを押すとデスクトップ上に roundrect.png が作成される。こんな感じ。


おお、めちゃくちゃ簡単だ。

保存コード部分だけ抜き出してみた。

MyView.m

-(IBAction)save:(id)sender
{
NSString* path = [NSSearchPathForDirectoriesInDomains(NSDesktopDirectory, NSUserDomainMask, YES) objectAtIndex:0];
NSString* filename = [path stringByAppendingPathComponent:@"roundrect.png"];
NSLog(@"%@", filename);
NSSize size = [_image size];
NSRect rect = NSMakeRect(0, 0, size.width, size.height);

NSImage *rimg = [[NSImage alloc] initWithSize:size];
NSBezierPath* round = [NSBezierPath bezierPathWithRoundedRect:rect
xRadius:15.0 yRadius:15.0];
[rimg lockFocus];
[round setClip];
[_image compositeToPoint:NSZeroPoint operation:NSCompositeSourceOver];
[rimg unlockFocus];

NSData* data = [rimg TIFFRepresentation];
NSBitmapImageRep *bitmap_rep = [NSBitmapImageRep imageRepWithData:data];
data = [bitmap_rep representationUsingType:NSPNGFileType
properties:[NSDictionary dictionary]];

[data writeToFile:filename atomically:YES];

}


保存用の NSImage を作り、RoundRectパスでクリッピングを適用し、元画像で上書きする。これだけ。

ソース:RoundRect-3.zip

- - - -
(調子に乗って)今度は影でも付けてみるか。

2008年3月28日金曜日

RoundRect その2

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

NSBezierPathの RoudRect作成メソッドを使って画像の四隅を丸くしてみる。
まずは画面表示から。これは簡単。



ソース:RoundRect-2.zip

NSBezierPathでクリッピング指定した上で画像を描けば簡単にできる。

MyView.m

- (void)drawRect:(NSRect)rect {
// Drawing code here.
[NSGraphicsContext saveGraphicsState];
[_clip_path setClip];
[self lockFocus];
[_image compositeToPoint:NSZeroPoint operation:NSCompositeSourceOver];
[self unlockFocus];
[NSGraphicsContext restoreGraphicsState];

}

( _clip_pathにはあらかじめ bezierPathWithRoundedRect: で作った RoundRectパスが入っている)

次回は四隅を丸くしたままで保存してみる。

2008年3月27日木曜日

RoundRect その1

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

MacOSX10.5より NSBezierPathに RoudRectbのパス作成メソッドが追加された。
bezierPathWithRoundedRect:xRadius:yRadius:

早速試してみる。



ソース:RoundRect-1.zip

なかなか良い。使い道は多かったのに今まで無かったのが不思議なくらい。昔は自分で円弧と直線を組み合わせて作った覚えがある。


コードはメソッドを呼出してパスを作るだけ。

MyView.m

- (void)drawRect:(NSRect)rect {
// Drawing code here.
[[NSColor whiteColor] set];
NSRectFill(rect);

NSRect rect2 = NSMakeRect(rect.size.width*1.0/4.0/2.0,
rect.size.height*1.0/4.0/2.0,
rect.size.width*3.0/4.0,
rect.size.height*3.0/4.0);

NSBezierPath* path = [NSBezierPath bezierPathWithRoundedRect:rect2
xRadius:15.0
yRadius:15.0];
[[NSColor redColor] set];
[path fill];
}


次回はこれを使って画像の角を丸くしてみよう。

2008年3月26日水曜日

Spaces

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

スクリーンキャプチャのソフトを作っているが画面を覆う1枚のウィンドウを使っている。ところが Spaces.appで画面が移動されるとこのウィンドウが隠れてしまい移動先の画面でキャプチャができない。Spaces.appで画面を移動してもこのウィンドウを常に表示させておきたい。

NSWindow.h を眺めていると次のメソッドを見つけた。


おお使えそうだ。で、使ってみたらうまくいった。常時表示したい NSWindowに対して setCanBeVisibleOnAllSpaces:YES を投げてやると、Spaces.appで画面を移動しても常にそのウィンドウが表示される。

が、deprecated扱いだ。


そもそも ADCのリファレンスにもこのメソッドは載っていないな(ヘッダのコメントも DEPRECATEDと記載がある)。


しかたなく NSWindow.h をまた眺める。するとあった。setCollectionBehavior: 。これは ADCのリファレンスにも載っている。
NSWindow#setCollectionBehavior:


早速試してみる。



ソース:SpacesStudy.zip

チェックが外れた状態がデフォルト。この状態では Spaces.appで画面を切り替えるとウィンドウは隠れてしまう。そこでチェックを入れる。すると NSWindowCollectionBehaviorCanJoinAllSpaces がセットされ、Spaces.appで画面を切り替えてもウィンドウが常に表示されるようになる。ただ、ちと気味が悪い動きだ。。

チェックボックスのアクションコード

- (IBAction)click:(id)sender
{
NSLog(@"%d", [sender intValue]);

if ([sender intValue]) {
[window setCollectionBehavior:NSWindowCollectionBehaviorCanJoinAllSpaces];

} else {
[window setCollectionBehavior:NSWindowCollectionBehaviorDefault];
}
}


- - - -
リファレンスによるともう一つ NSWindowCollectionBehaviorMoveToActiveSpaceというのがある。ウィンドウ(アプリ)をアクティブにした時、表示中のSpaceにウィンドウが表示されるものなのかと思ったが、NSWindowCollectionBehaviorDefault(デフォルト)と変わらないようだ。使い方がまずいのか?

2008年3月25日火曜日

画面キャプチャその18 - CGWindowListCreateImageFromArray( )

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

さらにしつこく複数ウィンドウのキャプチャ。

これまで複数ウィンドウのキャプチャ画像を作るのに個別のウィンドウ画像を地道に合成していた。が、そんな面倒なことをしなくとも WindowIDの配列を渡すだけで1枚の画像にまとめてくれる関数が用意されていたのに気がついた。早速試してみる。



ソース:FullScreenSample-18.zip


コードは自前合成に比べるとずいぶんと簡潔になる。ポイントだけ抜き出してみる。

CGWindowID *windowIDs = calloc([list count], sizeof(CGWindowID));
int i=0;
for (Window* w in list) {
windowIDs[i++] = [w windowID];
}
CFArrayRef windowIDsArray = CFArrayCreate(kCFAllocatorDefault, (const void**)windowIDs, [list count], NULL);
free(windowIDs);

CGImageRef cgimage = CGWindowListCreateImageFromArray(CGRectNull, windowIDsArray, kCGWindowImageDefault);



画像に含めたい WindowIDの配列をCGWindowListCreateImageFromArray( )の第二引数へ CFArrayRef 型で渡してやる。この配列にはCFNumberRef や NSNumberでラッピングせず、 CGWindowID(uint32_tで定義)のまま入れなければならないので注意が必要。上記コードは ADCのサンプルコード(SonOfGrab)より一部拝借した。ついでにコメントも転記しておく。

// CGWindowListCreateImageFromArray expect a CFArray of *CGWindowID*, not CGWindowID wrapped in a CF/NSNumber
// Hence we typecast our array above (to avoid the compiler warning) and use NULL CFArray callbacks
// (because CGWindowID isn't a CF type) to avoid retain/release.



なお引き渡した WindowID順にウィンドウが並べられて画像が作成されるので、この順序を並び替えると画面で見えていたのとは違った任意の順序で合成画像を作ることができる。下記は画面で見えていたのとは逆順にした例。

2008年3月24日月曜日

画面キャプチャその17 - 複数のウィンドウをキャプチャ(7)

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

引き続き、複数ウィンドウのキャプチャ。今回はコード解説。

まずはマスク(clipping path)を作る個所。ウィンドウが選択(mouseDown:)された時に作成する。

マウスカーソル下のウィンドウの当たり判定を行い、選択されたウィンドウを特定する。

- (void)mouseDown:(NSEvent *)theEvent
{
CGWindowID window_id = (CGWindowID)[[self window] windowNumber];
NSPoint cp = [self convertPoint:[theEvent locationInWindow] fromView:nil];
CFArrayRef window_list =CGWindowListCopyWindowInfo((kCGWindowListOptionOnScreenOnly|kCGWindowListOptionOnScreenBelowWindow|kCGWindowListExcludeDesktopElements), window_id);
CFDictionaryRef window;
CFIndex i, j;
CGRect rect = CGRectZero;
CGWindowID hit_window_id;

BOOL hit_flag = NO;
for (i=0; i < CFArrayGetCount(window_list); i++) {
window = CFArrayGetValueAtIndex(window_list, i);
CGRectMakeWithDictionaryRepresentation(CFDictionaryGetValue(window, kCGWindowBounds), &rect);
if (CGRectContainsPoint(rect, CGPointMake(cp.x, cp.y))) {
if ([self isDockWindow:window]) {
hit_flag = YES;
CFNumberGetValue(CFDictionaryGetValue(window, kCGWindowNumber),
kCGWindowIDCFNumberType, &hit_window_id);
break;
}
}
}



続いてマスク作り。
 NSRect rect2 = NSMakeRect(rect.origin.x, rect.origin.y, rect.size.width, rect.size.height);
if (hit_flag) {
Window *w = [self windowInList:hit_window_id];
if (w) {
[_list removeObject:w];
} else {
w = [[[Window alloc] initWithID:hit_window_id Rect:rect2 Order:i*10+1] autorelease];
[_list addObject:w];

NSBezierPath* path = [NSBezierPath bezierPath];
for (j=0; j < i; j++) {
window = CFArrayGetValueAtIndex(window_list, j);
CGRectMakeWithDictionaryRepresentation(CFDictionaryGetValue(window, kCGWindowBounds), &rect);
NSRect rect3 = NSMakeRect(rect.origin.x, rect.origin.y, rect.size.width, rect.size.height);
if (NSIntersectsRect(rect2, rect3)) {
[path appendBezierPathWithRect:rect3];
}
}
[w setClippingPath:path];
}
[self setNeedsDisplayInRect:rect2];
[self setNeedsDisplay:YES];
}
CFRelease(window_list);


前回説明したアルゴリズムの中のマスク作成の実装がこれ。選択されたウィンドウの上に重なっているウィンドウを見つけ、そのRectを NSBezierPathのパスとして合成していく。作成したパスは描画時に使うので Windowクラスのメンバ変数 _cliiping_pathに取っておく。


次は描画コード。
- (void)drawRect:(NSRect)rect {
// Drawing code here.
[[NSColor colorWithDeviceRed:0.0 green:0.0 blue:0.0 alpha:0.75] set];
NSRectFill(rect);

NSMutableArray* array = [NSMutableArray arrayWithArray:_list];
if (_spot_window) {
[array addObject:_spot_window];
}
NSSortDescriptor* sort = [[[NSSortDescriptor alloc] initWithKey:@"order" ascending:NO] autorelease];
NSArray *list = [array sortedArrayUsingDescriptors:[NSArray arrayWithObject:sort]];
for (Window* w in list) {
if (w == _spot_window) {
[[NSColor colorWithDeviceRed:0.0 green:0.0 blue:0.0 alpha:0.25] set];
NSRectFill([w rect]);
[[NSColor colorWithDeviceRed:1.0 green:1.0 blue:1.0 alpha:0.75] set];
// NSFrameRectWithWidth([w rect], 5.0);
} else {
[[NSColor colorWithDeviceRed:0.0 green:0.0 blue:0.0 alpha:0.0] set];
NSRectFill([w rect]);
}
if (![[w clippingPath] isEmpty]) {
[[NSColor colorWithDeviceRed:0.0 green:0.0 blue:0.0 alpha:0.75] set];
[NSGraphicsContext saveGraphicsState];
[[w clippingPath] setClip];
NSRectFill([w rect]);
[NSGraphicsContext restoreGraphicsState];
}
}
}

これも前回のアルゴリズムに沿ってコーディングしただけ。 _spot_windowが入り込んで若干わかりずらくなっているが、これは mouseMove: 時にマウスカーソル下のウィンドウを指している。このウィンドウは選択はされていないが、マウスカーソルが載っている時にハイライトさせる必要があるので、今回のクリッピングと同じ仕組みで処理させている(最初に配列に足している)。

- - - -
mouseDon: と mouseMove: で似たコードが存在するようになってしまった。うまくまとめたかったが、複雑になりそうで結局コピーとなった。うまい方法があるといいのだが。

2008年3月23日日曜日

画面キャプチャその16 - 複数のウィンドウをキャプチャ(6)

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

選択されたウィンドウをハイライトする方法を解説する。

ここでは(2)と(5)のウィンドウが選択されているとする。表示は下のようにしたい。


選択されたウィンドウ毎にマスク処理を施していく。順番は表示順序で一番下から。今回のケースでは(5)から始める。


[1] まずウィンドウの領域をすべてハイライトする。この時点ではハイライトしたくない所も含まれる。


[2] 次にマスクを作成する為に(5)のウィンドウの上に重なっているウィンドウを探す。すると(3)と(4)が見つかる。※(2)も該当するが後でハイライト表示するので今回は無視している。


[3] (3)と(4)のウィンドウ領域を合成した領域を作る。実装コードでは NSBezierPathを使う。


[4] この領域をクリッピングエリアとして指定し、その上で(5)の領域を灰色で塗りつぶす。すると左上のマスクしたい部分だけが塗りつぶされる。これで(5)の見えている部分だけがハイライト表示となった。


[5] 同様にして他の選択されたウィンドウを処理する。(2)の場合は、上に(1)のウィンドウが重なっているので、これを元にしてマスク領域を作り塗りつぶす。


次回は実装コードの解説を行う。

2008年3月22日土曜日

画面キャプチャその15 - 複数のウィンドウをキャプチャ(5)

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

引き続き複数ウィンドウのキャプチャ。クリック毎に選択されたウィンドウがハイライトになるようにする。もう一度選択すると解除される。

ちょっと手間取ったができた。


任意のキーを押すと選択ウィンドウのキャプチャ画像ファイルが生成される。



ソース: FullScreenSample-15.zip


今回も独特な?マスク方法を考えて実装した。解説は明日(続く)。

2008年3月21日金曜日

Clipping

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

画面キャプチャで複数ウィンドウを扱う時、選んだウィンドウをすべてハイライト表示にさせたい。その為にはマスク処理を見直す必要がある。そこでいろいろと試している。

今回は NSBezierPath のクリッピングを試すサンプルを作ってみた。

ClipSample.zip

実行すると壁紙のJPEG画像が表示される。


ここでボタンを押すと、円形にくりぬかれる。



NSViewの描画コードは次のとおり。
MyView.h

- (void)drawRect:(NSRect)rect {
if (_is_clip) {
[NSGraphicsContext saveGraphicsState];
NSBezierPath* path = [NSBezierPath bezierPath];
[path appendBezierPathWithOvalInRect:NSMakeRect(0, 0, 480, 360)];
[path addClip];
}

NSImage* image = [[NSImage alloc] initWithContentsOfFile:@"/Library/Desktop Pictures/Nature/Rocks.jpg"];
[self lockFocus];
[image compositeToPoint:NSZeroPoint operation:NSCompositeSourceOver];
[self unlockFocus];
[image release];

if (_is_clip) {
[NSGraphicsContext restoreGraphicsState];
}

}


ボタンを押すと _is_clipping が YES/NOと入れ替わる。YESの場合は NSBezierPath のクリッピングを実行している。今回は NSBezierPath#appendBezierPathWithOvalInRect: を使い円形のパスを作成した後、 #addClip で描画領域を限定している。この後の NSImageの描画は、このクリップ領域が有効に働いて、結果円形でくり抜いたような表示となる。

NSGraphicsContext#storeGraphicsState と #restoreGraphicsState はコンテキストの保存と復帰で使っている。これを行わないとウィンドウ内の描画がおかしくなる(実際 restoreGraphicsStateをしないと、ボタンの表示位置がおかしな場所になったりする)。


参照情報
ADC "Cocoa Drawing Guide: Setting the Clipping Region"

- - - -
クリップ領域はてっきり四角(Rect)しかできないと思っていたが、NSBezierPathを使うことでかなり複雑なことができそうだ。これを使って複数ウィンドウ選択のハイライトに挑戦してみよう。

2008年3月20日木曜日

画面キャプチャその14 - 複数のウィンドウをキャプチャ(3)

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

キャプチャウィンドウに影を付けてみた。



ソース:FullScreenSample-14.zip


ウィンドウが一つの場合は、CGWindowListCreateImage() でkCGWindowImageDefault オプションを使えば標準で影が付いてくるので簡単にできる。


この場合、第一引数のCGRectには、CGRectNullを渡してやる。


一方、複数ウィンドウの場合は最後に画像を合成する必要がある為、影で増えた分のサイズを計算する必要がある。前回まではこれを考慮していなかったので影が付いていなかった。



これは第一引数にウィンドウの実際の大きさを指定していた為、周りの影の部分が無視されていた。第一引数にウィンドウの実サイズではなく、CGRectNullを渡してやると次のようになる。



影が入るようになったが右上が欠けてしまっている。これは最後に合成する画像のサイズがウィンドウの実サイズを元に計算されている為。そこで影で増えた分(40ピクセル)を補正してやる。



うまくいった。

- - - - -
「影で増えた分(40ピクセル)」はマジックナンバーで、定数や関数から取った値ではない。調べてみたが分からなかったので実測で出した(もし誰か情報を持っていたら是非教えて下さい...)。そういった定数や関数が無ければ、今回のようなマジックナンバーを埋め込むか、もしくは実行時に影付きの画像を作り、ウィンドウの実サイズと比較するしかない。

(追記)いろいろ書いたが、一旦すべてのウィンドウの画像(CGImage)が揃えられれば最終サイズは容易に出せるので実はそんなに問題ではない。

2008年3月19日水曜日

CGWindowListCreateImage のオプション

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

CGWindowListCreateImage() の最後のオプションは、キャプチャ画像の種類を指定できる。それぞれの種類毎にどんな画像になるのかを確認してみた。それぞれの変数(enum)は CGWindow.h で定義されている。

(1) kCGWindowImageDefault

影あり、背景あり(透明)。

上記画像では分かりづらいが背景は透明になっている。


(2) kCGWindowImageBoundsIgnoreFraming

影なし、背景なし。



(3) kCGWindowImageShouldBeOpaque

影あり、背景あり(不透明)。

上記画像では分かりづらいが背景は白色で塗りつぶされている(不透明)。

(4) kCGWindowImageOnlyShadows

影だけ。

2008年3月18日火曜日

NSBGOnly と NSUIElement と LSUIElement

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

ステータスバーで動作させるユーティリティアプリを開発している。



このアプリはメニューやDockを表示させたくないので info.plist で NSBGOnly を設定していた。

 <key>NSUIElement</key>
<string>1</string>


ところが、このアプリからウィンドウを表示させた時に、ウィンドウにフォーカスがあたらないことがわかった。
(例)
(1) ステータスバーのメニューで項目を選択
(2) ウィンドウを表示(フォーカスがあたっていない)
(3) ウィンドウを選択(ようやくフォーカスがあたる)

これだと困る。ウィンドウを開いたのにいちいち選択してやらなければフォーカスがあたらないなんて不便だ。

ネットを調べていると NSUIElement が見つかった。
http://cathand.org/diary2002.html
http://www.padmacolors.org/archives/2004/02/04_080905.php
http://forums.macosxhints.com/archive/index.php/t-18403.html

ADCで検索してみると NSUIElement では見つからなくて LSUIElement となっていた。
LSUIElement

これらによると LSUIElement を指定したアプリケーションは下記の動作モードを持つようになる。

・Dockに現れない
・強制終了ウィンドウに現れない


早速試してみる。
 <key>LSUIElement</key>
<string>1</string></pre>


うまくいった。
(例)
(1) ステータスバーのメニューで項目を選択
(2) ウィンドウを表示(フォーカスがあたっている)


ちなみに NSUIElement でも同様に動作した。動作上の違いは今回だけではわからなかったが CocoaBuilderの記事によると NSUIElementは deprecatedの様子。
http://www.cocoabuilder.com/archive/message/cocoa/2005/7/13/141793

2008年3月17日月曜日

Scripting Bridge 情報

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

引き続き情報を紹介する。Scripting Bridge は MacOSX10.5から導入されたもので Objective-Cの文法を使いアプリケーションを操作できるというもの。Objective-Cに留まらず、Rubyや PythonからもCocoaアプリケーションへアクセスすることができる。

Scripting Bridgeのリリースノート。概要を知るには良い。
Scripting Bridge Release Notes for Mac OS X v10.5

ここに前回の sdef コマンドの紹介もある。その他、Objective-Cによる具体的な利用法の解説もある。


ADCのリファレンス
Scripting Bridge Programming Guide for Cocoa

Cocoaからの利用方法について詳しい説明がある。パフォーマンスを考慮した利用テクニックなども紹介されている。

- - - -
これまでは他のアプリと連携するには AppleScriptを使っていたが今後は Scripting Bridgeが利用できる。AppleScript はあまり詳しくないし今後もあまり使う機会が無いと思うので、Cocoa-Nativeで実装できるのはうれしい。

2008年3月16日日曜日

Mail.h の謎(sdef と sdp)

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

前回 Mail.app でメールを送る ADCのサンプルを紹介した。その中で Mail.h が生成されているのが分かった。これは OSに付属するわけでもなく、ビルド時に動的に生成されいるようだ。



サンプルの ReadMe.txtを読んでみるとビルド時に sdef と sdp コマンドを使ってヘッダファイルを生成していることが分かった。ターゲットのルールは次のようになっている。




sdef コマンドは指定したアプリケーションからスクリプト定義(scripting definitions) をXMLで取得する。sdp はそれを Objective-Cのヘッダファイルに加工する。それぞれの man を抜粋。

sdef

NAME
sdef -- scripting definition file

DESCRIPTION
Scripting definition files (sdefs) are XML files that describe everything
about an application scripting interface: terminology, implementation
information, and complete documentation. Applications may incorporate
them to define their own scriptability, and scripting clients such as
AppleScript and Scripting Bridge read them to determine what operations
an application supports.
  :


sdp
NAME
sdp -- scripting definition (sdef) processor

SYNOPSIS
sdp -f {ahst} [-o directory | file | -] [options...] [file]

DESCRIPTION
sdp transforms a scripting definition (``sdef'') file, or standard input
if none is specified, into a variety of other formats for use with a
scriptable application. The options are as follows:

-f format
Specify the output format. The format may be one or more of the
following. Use these when you want to create a scriptable applica-
tion:

a Rez(1) input describing an `aete' resource.
s Cocoa Scripting ``.scriptSuite'' file.
t Cocoa Scripting ``.scriptTerminology'' file.

These formats are only necessary when creating a scriptable appli-
cation that will run on Mac OS X 10.4 (Tiger) or earlier; as of
10.5 (Leopard), an application may use only an sdef.

Use these when you want to control a scriptable application:

h Scripting Bridge Objective-C header.

 :



試しに iTunes でこれらのコマンドを使ってみた。ターミナルから下記を実行する。

$ sdef /Applications/iTunes.app |sdp -fh --basename iTunes


すると iTunes.h というファイルが作成される。中身はこんな感じ。

/*
* iTunes.h
*/

#import
#import


@class iTunesPrintSettings, iTunesApplication, iTunesItem, iTunesArtwork, iTunesEncoder, iTunesEQPreset, iTunesPlaylist, iTunesAudioCDPlaylist, iTunesDevicePlaylist, iTunesLibraryPlaylist, iTunesRadioTunerPlaylist, iTunesSource, iTunesTrack, iTunesAudioCDTrack, iTunesDeviceTrack, iTunesFileTrack, iTunesSharedTrack, iTunesURLTrack, iTunesUserPlaylist, iTunesFolderPlaylist, iTunesVisual, iTunesWindow, iTunesBrowserWindow, iTunesEQWindow, iTunesPlaylistWindow;

typedef enum {
iTunesEKndTrackListing = 'kTrk' /* a basic listing of tracks within a playlist */,
iTunesEKndAlbumListing = 'kAlb' /* a listing of a playlist grouped by album */,
iTunesEKndCdInsert = 'kCDi' /* a printout of the playlist for jewel case inserts */
} iTunesEKnd;

typedef enum {
iTunesEnumStandard = 'lwst' /* Standard PostScript error handling */,
iTunesEnumDetailed = 'lwdt' /* print a detailed report of PostScript errors */
} iTunesEnum;
 :


ああなるほど。これは便利だ。

2008年3月15日土曜日

ADCサンプルより - SBSendEmail

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

ADCサンプルより。

SBSendEmail
(2008-02-28)

Descriptionより引用
This sample illustrates how to use the Scripting Bridge Framework to tell the Mail application to send an email message with optional attachments. The included readme file provides detailed discussion of the steps involved in creating this sample.

Scripting Bridge Frameworkを使い Mail.appからメールを送るサンプル。
実行するとメール送信に必要な情報を入力するウィンドウが表示される。



"send"ボタンを押すと Mail.app が起動され入力した内容でメールが送られる。

ソースコードは Controller.m だけで構成されていてシンプル。
ポイントは SBApplication を使った Mailアプリケーションの操作。

MailApplication や MailOutgoingMessage などのオブジェクトを取得して、これに対して操作を行う。
Controller.m(抜粋)

- (IBAction)sendEmailMessage:(id)sender {

/* create a Scripting Bridge object for talking to the Mail application */
MailApplication *mail = [SBApplication
applicationWithBundleIdentifier:@"com.apple.Mail"];

/* create a new outgoing message object */
MailOutgoingMessage *emailMessage =
[[[mail classForScriptingClass:@"outgoing message"] alloc]
initWithProperties:
[NSDictionary dictionaryWithObjectsAndKeys:
[self.subjectField stringValue], @"subject",
[[self.messageContent textStorage] string], @"content",
nil]];

/* add the object to the mail app */
[[mail outgoingMessages] addObject: emailMessage];

/* set the sender, show the message */
emailMessage.sender = [self.fromField stringValue];
emailMessage.visible = YES;
   :
   :



MailApplicationは Mail.h で定義されていて SBApplication のサブクラスとなっている。
Mail.h
@interface MailApplication : SBApplication


他にもスクリプティング向けにたくさんのクラス定義が提供されているのがわかる。

@class MailItem, MailApplication, MailColor, MailDocument, MailWindow, MailText, MailAttachment, MailParagraph, MailWord, MailCharacter, MailAttributeRun, MailOutgoingMessage, MailLdapServer, MailApplication, MailMessageViewer, MailSignature, MailMessage, MailAccount, MailImapAccount, MailMacAccount, MailPopAccount, MailSmtpServer, MailMailbox, MailRule, MailRuleCondition, MailRecipient, MailBccRecipient, MailCcRecipient, MailToRecipient, MailContainer, MailHeader, MailMailAttachment;


- - - -
Scripting Bridge を使うと簡単に外部アプリを操作できることがわかった。ADCでは他にファインダの操作などのサンプルも紹介されている。ところでヘッダファイル Mail.h はビルドディレクトリ内に存在するがこれはいったいどこからやってきたのか?

2008年3月14日金曜日

画面キャプチャその13 - 複数のウィンドウをキャプチャ(2)

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

前回の続き。


選択したウィンドウを覚えておくために Winodwクラスを新規に作成する。

@interface Window : NSObject {

CGWindowID _window_id;
NSRect _rect;
CFIndex _order;
}


ウィンドウの選択毎に Windowのインスタンスを作成して CGWindowIDや矩形領域、表示順序をとっておく。

キー入力があったらとっておいた Windowの情報を元に最終画像を合成する。まず全てのウィンドウを含む矩形領域のサイズを算出する。これは NSUnionRect()関数を使うと簡単にできる。
 for (Window* w in list) {
wrect = [w rect];
irect = NSUnionRect(irect, wrect);
}
NSImage* bigimg = [[NSImage alloc] initWithSize:irect.size];



最終画像をおさめる NSImageが用意できたらキャプチャ画像を1つ1つ取得し compositeToPoint: で合成していく。
 for (Window* w in list) {
wrect = [w rect];
rect = CGRectMake(wrect.origin.x, wrect.origin.y, wrect.size.width, wrect.size.height);
cgimage = CGWindowListCreateImage(rect, kCGWindowListOptionIncludingWindow, [w windowID], kCGWindowImageDefault);
bitmap_rep = [[[NSBitmapImageRep alloc] initWithCGImage:cgimage] autorelease];
image = [[[NSImage alloc] init] autorelease];
[image addRepresentation:bitmap_rep];
p.x = wrect.origin.x - irect.origin.x;
p.y = (irect.size.height - wrect.size.height) - (wrect.origin.y - irect.origin.y);
[bigimg lockFocus];
[image compositeToPoint:p operation:NSCompositeSourceOver];
[bigimg unlockFocus];
}


この時 NSImageの座標系は左下が原点、CGWindowListCreateImage()が左上が原点、とことなるので補正しておく。

合成が終わったら後はファイルへ書き出してやる。
 NSData* data = [bigimg TIFFRepresentation];
NSBitmapImageRep *pngrep = [NSBitmapImageRep imageRepWithData:data];
data = [pngrep representationUsingType:NSPNGFileType
properties:[NSDictionary dictionary]];
[data writeToFile:filename atomically:YES];


- - - -
今のままだとウィンドウの陰が含まれていないので、のっぺりした感じになってしまうな。

2008年3月13日木曜日

画面キャプチャその12 - 複数のウィンドウをキャプチャ(1)

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

説明用のドキュメントを書いていると複数のウィンドウをキャプチャしたい時がある。キャプチャのソフトでは見たことがないが便利そうなので作ってみる。

ソース:FullScreenSample-12.zip

実行すると画面が暗くなり、ウィンドウが選択できるようになる。


キャプチャに含めたいウィンドウを選んでいき最後に何でも良いのでキーを押す。すると選択したウィンドウのキャプチャができる。こんな感じ。


キャプチャした画像を1枚の NSImageに合成して最終画像を作っている。

詳しい解説はまた明日。

2008年3月12日水曜日

NSBorderlessWindowMask ウィンドウでのキーイベントの受取

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

キャプチャのサンプルを作る過程でキー入力のハンドリングが必要になった。今までのサンプルではウィンドウをNIB経由ではなく実行時に動的に作っていた。このウィンドウは NSBorderlessWindowMask スタイルで作ってあるためにそのままではキーイベントが拾えない。そこでいくつかの必要な実装を加える。

まず NSWindowのサブクラスを作り canBecomeKeyWindowをオーバーライドする。

@interface MyWindow : NSWindow { }
@end
@implementation MyWindow
- (BOOL)canBecomeKeyWindow { return YES; }

@end



その上でこのサブクラスを使うようにする。
(旧)_fullscreen_window = [[NSWindow alloc] initWithContentRect:
(新)_fullscreen_window = [[MyWindow alloc] initWithContentRect: ...


最後に keyDown: を実装する。

ビューの方でハンドリングしたい場合は acceptsFirstResponder をオーバーライドし、keyDown: を実装する。
- (BOOL)acceptsFirstResponder
{
NSLog(@"acceptFirstResponder");


- (void)keyDown:(NSEvent *)theEvent
{
NSLog(@"keyDown:");
}



またウィンドウのレスポンダチェインへ追加する。
[_fullscreen_window setNextResponder:_fullscreen_view];

2008年3月11日火曜日

画面キャプチャその11 - Widgetの取り込み(2)

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

CGWindowListCopyWindowInfo を使って Widgetのウィンドウ情報も取れることが分かった。そこで以前作ったサンプル(FullScreenSample-4.zip)を改良して Widgetのキャプチャも取れるようにする。

以前のサンプルでは Dockという名のプロセスが持つ画面全体をほぼ覆うウィンドウを避ける為に kCGWindowName != nil のウィンドウを対象にしていた。

Widgetなしの場合のウィンドウリスト
2008-03-03 09:09:46.733 ScreenList[13010:10b] 00000019: SystemUIServer, , (1111,0,283,22)
2008-03-03 09:09:46.733 ScreenList[13010:10b] 00000053: Spotlight, , (1394,0,46,22)
2008-03-03 09:09:46.734 ScreenList[13010:10b] 00000020: SystemUIServer, (null), (0,0,1440,22)
2008-03-03 09:09:46.735 ScreenList[13010:10b] 00000006: Window Server, Shared Menubar, (0,0,1440,22)
2008-03-03 09:09:46.735 ScreenList[13010:10b] 00000014: Dock, (null), (0,22,1440,878)
2008-03-03 09:09:46.736 ScreenList[13010:10b] 00030269: Dock, Magic Mirror, (240,851,961,49)
 :


上記の Dock,(null),(0,22,1440,878) というウィンドウがこれにあたる。これは恐らく Dockのアニメーションなどスクリーン上での描画を行う為に存在しているウィンドウだと思われる。

ウィンドウの構成イメージはこんな感じか?


無条件にマウスのヒットテストを行うとこのウィンドウがヒットしてしまう為、弾く処理(kCGWindowName==nil)を行っていた。


ところが Widget も同様に kCGWindowOwnerName が Dock で kCGWindowName が nil となっている。

Widget有りの場合のウィンドウリスト
2008-03-03 09:13:00.245 ScreenList[13012:10b] 00014970: Dock, (null), (10,855,36,36)
2008-03-03 09:13:00.246 ScreenList[13012:10b] 00014973: Dock, (null), (834,53,437,375)
2008-03-03 09:13:00.247 ScreenList[13012:10b] 00014972: Dock, (null), (776,487,172,248)
2008-03-03 09:13:00.247 ScreenList[13012:10b] 00014971: Dock, (null), (177,241,510,473)
2008-03-03 09:13:00.247 ScreenList[13012:10b] 00059707: Dock, (null), (0,0,1440,900)
2008-03-03 09:13:00.248 ScreenList[13012:10b] 00000019: SystemUIServer, , (1111,0,283,22)
2008-03-03 09:13:00.250 ScreenList[13012:10b] 00000053: Spotlight, , (1394,0,46,22)
2008-03-03 09:13:00.257 ScreenList[13012:10b] 00000020: SystemUIServer, (null), (0,0,1440,22)
2008-03-03 09:13:00.260 ScreenList[13012:10b] 00000006: Window Server, Shared Menubar, (0,0,1440,22)
 :


その為、kCGWindowName==nil の処理だけでは Widgetも弾かれてしまう。 画面全体を覆う Dockのウィンドウだけを特定して弾く処理が必要である。CGWindowListCopyWindowInfoで得られる情報では kCGWindowOwnerName と kCGWindowBounds がこの判断に使えそうである(逆に言えば他は使えそうもない)。

CGWindowListCopyWindowInfoの情報
 kCGWindowAlpha = 1;
kCGWindowBounds = {
Height = 22;
Width = 322;
X = 1072;
Y = 0;
};
kCGWindowIsOnscreen = 1;
kCGWindowLayer = 25;
kCGWindowMemoryUsage = 41820;
kCGWindowName = "";
kCGWindowNumber = 19;
kCGWindowOwnerName = SystemUIServer;
kCGWindowOwnerPID = 119;
kCGWindowSharingState = 1;
kCGWindowStoreType = 2;



そこで (1) kCGWindowOwnerNameが "Dock"でかつ (2)サイズがほぼ画面と同じ、という条件で弾くことにする。判断用のメソッドを用意しこれを使う。

MyView.m
- (BOOL)isDockWindow:(CFDictionaryRef)window
{
CGRect rect;
CGRectMakeWithDictionaryRepresentation(CFDictionaryGetValue(window, kCGWindowBounds), &rect);
CFStringRef owner_name;
CFStringRef window_name;
window_name = CFDictionaryGetValue(window, kCGWindowName);
owner_name = CFDictionaryGetValue(window, kCGWindowOwnerName);

if ([@"Dock" isEqualToString:(NSString*)owner_name]) {
NSSize screen_size = [[NSScreen mainScreen] frame].size;
if (screen_size.width <= (rect.origin.x + rect.size.width) &&
screen_size.height <= (rect.origin.y + rect.size.height)) {
return NO;
}
}
return YES;
}


簡易だが実用上は多分これで十分だろう。

実行して Widgetを呼出してみる。


選択できて問題なくキャプチャ画像を取ることができた。



ソース:FullScreenSample-11.zip

2008年3月10日月曜日

画面キャプチャその10 - Widgetの取り込み(1)

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

次は Widget のキャプチャに挑戦してみる。が、その前に CGWindowListCopyWindowInfo() で取得できる情報を一度整理してみる。

以前の検証ではウィンドウ毎に次の種類の情報が取得できることが分かっている。

 kCGWindowAlpha = 1;
kCGWindowBounds = {
Height = 22;
Width = 322;
X = 1072;
Y = 0;
};
kCGWindowIsOnscreen = 1;
kCGWindowLayer = 25;
kCGWindowMemoryUsage = 41820;
kCGWindowName = "";
kCGWindowNumber = 19;
kCGWindowOwnerName = SystemUIServer;
kCGWindowOwnerPID = 119;
kCGWindowSharingState = 1;
kCGWindowStoreType = 2;


それぞれの意味は CGWindow.h に説明がある。


さて、まずは Widgetのウィンドウがこの関数で取得できるのかを検証してみる。検証用のコードを書いてみた。

ソース:ScreenList.zip

実行すると5秒間隔でデバッガコンソールへ CGWindowListCopyWindowInfo の結果一覧を表示する。



では実行してみる。まずは Widgetなしの状態。

2008-03-02 09:34:14.311 ScreenList[12260:10b] 00000119: SystemUIServer, , (1111,0,283,22)
2008-03-02 09:34:14.312 ScreenList[12260:10b] 00000110: Spotlight, , (1394,0,46,22)
2008-03-02 09:34:14.314 ScreenList[12260:10b] 00000119: SystemUIServer, (null), (0,0,1440,22)
2008-03-02 09:34:14.315 ScreenList[12260:10b] 00000061: Window Server, Shared Menubar, (0,0,1440,22)
2008-03-02 09:34:14.315 ScreenList[12260:10b] 00000115: Dock, (null), (0,22,1440,878)
2008-03-02 09:34:14.316 ScreenList[12260:10b] 00000115: Dock, Magic Mirror, (209,851,1023,49)
2008-03-02 09:34:14.316 ScreenList[12260:10b] 00002477: EasyCalendar, Window, (-4,814,213,150)
2008-03-02 09:34:14.317 ScreenList[12260:10b] 00012260: ScreenList, Window, (335,128,252,166)
2008-03-02 09:34:14.317 ScreenList[12260:10b] 00012127: Xcode, ScreenList — デバッガコンソール, (489,47,794,792)
2008-03-02 09:34:14.318 ScreenList[12260:10b] 00012127: Xcode, MyController.m, (771,215,675,587)
 :
 :


表示情報は左から kCGWindowOwnerPID、kCGWindowOwnerName、kCGWindowName、kCGWindowBounds(x,y,w,h)。
画面の重なり順に表示されるので、リストの一番上が画面でも一番上のウィンドウとなる。

この状態で Widgetを表示させると...

2008-03-02 09:34:19.312 ScreenList[12260:10b] 00000115: Dock, (null), (10,855,36,36)
2008-03-02 09:34:19.312 ScreenList[12260:10b] 00000115: Dock, (null), (1258,648,172,248)
2008-03-02 09:34:19.317 ScreenList[12260:10b] 00000115: Dock, (null), (177,241,510,473)
2008-03-02 09:34:19.317 ScreenList[12260:10b] 00000115: Dock, (null), (727,401,381,49)
2008-03-02 09:34:19.318 ScreenList[12260:10b] 00000115: Dock, (null), (0,0,1440,900)
2008-03-02 09:34:19.318 ScreenList[12260:10b] 00000119: SystemUIServer, , (1111,0,283,22)
2008-03-02 09:34:19.319 ScreenList[12260:10b] 00000110: Spotlight, , (1394,0,46,22)
2008-03-02 09:34:19.319 ScreenList[12260:10b] 00000119: SystemUIServer, (null), (0,0,1440,22)
2008-03-02 09:34:19.319 ScreenList[12260:10b] 00000061: Window Server, Shared Menubar, (0,0,1440,22)
2008-03-02 09:34:19.320 ScreenList[12260:10b] 00000115: Dock, (null), (0,22,1440,878)
2008-03-02 09:34:19.320 ScreenList[12260:10b] 00000115: Dock, Magic Mirror, (209,851,1023,49)
2008-03-02 09:34:19.321 ScreenList[12260:10b] 00002477: EasyCalendar, Window, (-4,814,213,150)
2008-03-02 09:34:19.321 ScreenList[12260:10b] 00012260: ScreenList, Window, (335,128,252,166)
2008-03-02 09:34:19.322 ScreenList[12260:10b] 00012127: Xcode, ScreenList — デバッガコンソール, (489,47,794,792)
2008-03-02 09:34:19.322 ScreenList[12260:10b] 00012127: Xcode, MyController.m, (771,215,675,587)
 :
 :


"Dock"という名のウィンドウが5つほど増えている。この時の Widget表示は次のようになっていた。



最初の 36x36は左下の十字アイコンだと思われる。
2008-03-02 09:34:19.312 ScreenList[12260:10b] 00000115: Dock, (null), (10,855,36,36)


続く3つのウィンドウはそれぞれ画面に表示されている Widget。
2008-03-02 09:34:19.312 ScreenList[12260:10b] 00000115: Dock, (null), (1258,648,172,248)
2008-03-02 09:34:19.317 ScreenList[12260:10b] 00000115: Dock, (null), (177,241,510,473)
2008-03-02 09:34:19.317 ScreenList[12260:10b] 00000115: Dock, (null), (727,401,381,49)


最後の 1400x900 は画面を覆っている黒い半透明のウィンドウだろう。
2008-03-02 09:34:19.318 ScreenList[12260:10b] 00000115: Dock, (null), (0,0,1440,900)


つまり Widgetのウィンドウも問題なく取得できるということだ。加えてWidgetが特別な仕組みを使っている訳ではないことも分かった。イメージはこんな感じ?


- - - -
Widgetのウィンドウも問題なく取得できることが確認できたので次回は以前のサンプルを改造して Widgetのキャプチャを行ってみる。

2008年3月9日日曜日

画面キャプチャその9 - タイマー取り込み(3)

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

前回、メニュー選択中は NSTimerが発火しないことが分かった。NSTimerのリファレンスを読んでみる。

NSTimer Class Reference

Overview によると NSTimerは3つの生成方法があるらしい。

(1) current NSRunLoop + NSDefaultRunLoopMode の組み合わせ
(2) Run Loop と Mode ができる
(3) 初回コールバックの日時を NSDateで指定できる


前回まで使っていたのは (1)の方法。scheduleから始まるクラスメソッドを使う場合がこれに相当する。

[NSTimer scheduledTimerWithTimeInterval:1.0
target:_fullscreen_view
selector:@selector(fire:)
userInfo:nil
repeats:YES];


これだとメニュー操作中は NSTimerのコールバックが機能しない。

そこで (2)の方法を取ってみた。timerから始まるクラスメソッドがこれにあたる。
NSTimer* timer = [NSTimer timerWithTimeInterval:1.0
target:_fullscreen_view
selector:@selector(fire:)
userInfo:nil
repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];


timerWithTimeInterval:.. で NSTimerを作っただけではコールバックが起動しないので、これを NSRunLoop へ追加してやる。この時、NSRunLoopCommonModes を指定するとうまくいった。NSRunLoopCommonModesは MacOSX10.5から追加されたモード。モードの種類は NSRunLoop のリファレンスに書いてある。

NSRunLoop Class Reference - Run Loop Modes


これで無事メニュー操作中のスクリーンショットも取れるようになった。



ソース:FullScreenSample-9.zip

2008年3月8日土曜日

画面キャプチャその8 - タイマー取り込み(2)

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

タイマー取り込みの続き。カウントダウン中に操作ができるようにする。画面全体を暗くするのをやめて、カウントダウンの数字以外は透明にするようにした。



左上の白い数字がカウントダウン。

ソース:FullScreenSample-8.zip


画面全体を黒くするコードをコメントアウトする。

MyView.m

- (void)drawRect:(NSRect)rect {
// [[NSColor colorWithDeviceRed:0.0 green:0.0 blue:0.0 alpha:0.75] set];
// NSRectFill(rect);
  :


するとカウントダウンの数字以外は透明、すなわち普通にデスクトップが見えるようになる。これでカウントダウン中も通常通り他のアプリケーションの操作が可能になり、カウント=0でその時点でのスクリーンショットが保存される。

以前 NSWindow#setIgnoresMouseEvents:NO を使っていたがこれはコメントアウトしておく。そうしないとビューとウィンドウが透明であっても、それより下のウィンドウが操作できない。


- - - -
さて今回も簡単に行ったようだ。

と思った矢先、カウントダウン中にメニューを操作するとカウントダウンが止まってしまう。メニュー操作中はどうも NSTimerが発火しないようだ。

2008年3月7日金曜日

画面キャプチャその7 - タイマー取り込み

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

さてウィンドウ指定、範囲指定と作ったので次はタイマー取り込みを実装する。

今までに比べればこれは簡単。すぐにできた。



ソース:FullScreenSample-7.zip


実行すると画面が暗くなり、左上にカウントダウンの数字が表示いされる。"5 4 3 ..."と数字が減っていき 1になったところで画面全体がキャプチャされる。



PNGファイルが作成される。



カウントダウンには NSTimerを使う。コントローラで NSTimerを仕掛け、ビューのメソッド(fire:)を呼ぶように設定する。

MyController.m

[NSTimer scheduledTimerWithTimeInterval:1.0
target:_fullscreen_view
selector:@selector(fire:)
userInfo:nil
repeats:YES];



ビューでは fire: がよばれたらメンバ変数 _count を1減らし、画面に数字を描画する。

MyView.m
- (void)fire:(NSTimer*)theTimer
{
[self setNeedsDisplay:YES];
_count--;
   :


- (void)drawRect:(NSRect)rect {
   :
NSMutableAttributedString* str = [[NSMutableAttributedString alloc]
initWithString:[NSString stringWithFormat:@"%d", _count]];
[str addAttribute:NSFontAttributeName value:[NSFont fontWithName:@"Arial" size:72]
range:NSMakeRange(0, 1)];
[str addAttribute:NSForegroundColorAttributeName value:[NSColor whiteColor]
range:NSMakeRange(0, 1)];
[[NSColor whiteColor] set];
[str drawAtPoint:NSMakePoint(50, 50)];
}



_count == 0になったら画面全体のキャプチャを撮る。

CGWindowID window_id = (CGWindowID)[[self window] windowNumber];
CGImageRef cgimage = CGWindowListCreateImage(CGRectInfinite, kCGWindowListOptionOnScreenBelowWindow, window_id, kCGWindowImageDefault);
NSBitmapImageRep *bitmap_rep = [[NSBitmapImageRep alloc] initWithCGImage:cgimage];
NSData* data = [bitmap_rep representationUsingType:NSPNGFileType
properties:[NSDictionary dictionary]];


CGWindowListCreateImage に CGRectInfinite を渡すと画面全体が対象となる。

- - - -
今回の方法だとカウントダウン中は画面の操作が何もできない。このままではタイマーの意味が無いので次回は、カウントダウン中に操作ができるように改良する。