ページ

2008年2月26日火曜日

NSRect を CoreDataで扱う(その2)

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

前回紹介したサイトに掲載されていたNSKeyedArchiver にカテゴリを追加する方法を試してみる。


まず CoreDataへ行く前に NSKeyedArchiver/NSKeyedUnarchiver を実装した簡単なサンプルで試してみる。

ソースコード:ArchiveSample.zip



"Archive" ボタンを押すと NSRect {{10, 20} , {100, 200}}がデスクトップ上に archiver.sampleというファイル名で保存される。
"UnArchive" ボタンを押すと archiver.sampleを読み込み、NSRectに戻して画面に表示する。

MyController.m

-(id)init
{
if (self = [super init]) {
rect = [[NSValue valueWithRect:NSMakeRect(10, 20, 100, 200)] retain];
rect_string1 = NSStringFromRect([rect rectValue]);
rect_string2 = @"";
}
return self;
}


インスタンス変数 rect がArchiver対象のNSRect。rect_string1, 2 は画面表示用の文字列でラベルから bindingされている(文字列を変更すると画面に反映される)。

- (IBAction)archive:(id)sender
{
BOOL result = [NSKeyedArchiver archiveRootObject:rect toFile:[self filename]];
}


Archiverボタンが押されたら NSKeyedArchiver#archiveRootObject:toFile: で rectを指定し保存を行う。通常では NSKeyedArchiverは構造体をサポートしていないので下記のエラーが出る。

*** -[NSKeyedArchiver encodeValueOfObjCType:at:]: this archiver cannot encode structs



そこで サイトに紹介されていたコードを拝借してカテゴリで Cの構造体をサポートするコードを追加する。

@interface NSKeyedArchiver (MyKeyedArchiver)
- (void)encodeValueOfObjCType:(const char *)valueType at:(const void *)address;
@end

@implementation NSKeyedArchiver (MyKeyedArchiver)
- (void)encodeValueOfObjCType:(const char *)valueType at:(const void *)address
{
NSMutableData *datas = [NSMutableData data];
NSArchiver *arch = [[NSArchiver alloc] initForWritingWithMutableData:datas];
[arch encodeValueOfObjCType:valueType
at:address];
[self encodeObject:[NSData dataWithData:datas]];
[arch release];
}
@end


するとエラーが出なくなる。

UnKeyedArchiverも同様にカテゴリを追加する。

- - - -
さて次回は CoreData で使ってみる。

2008年2月25日月曜日

NSRect を CoreDataで扱う

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

以前、NSRect などが CoreDataで扱えたら便利と書いたが、NSValue というラッパークラスがあることに気がついた。これが使えないだろうか。

NSValue Class Reference

NSValue の生成メソッド:

– initWithBytes:objCType: 
+ valueWithBytes:objCType:
+ value:withObjCType:
+ valueWithNonretainedObject:
+ valueWithPointer:
+ valueWithPoint:
+ valueWithRange:
+ valueWithRect:
+ valueWithSize:


NSRect以外に NSPointや NSRange などよく使う構造体をオブジェクトとして扱うことができるようになる。


CoreDataのエンティティ属性のデータ型を「変換可能」(Transformable)にすれば、NSRectもCoreDataへ格納できるのでは?

参考:Non-Standard Persistent Attributes

さっそくやってみた。



エンティティのコードを生成させて型を NSValue に書き換えた。

@interface SampleEntity :  NSManagedObject 
{
}

@property (retain) NSValue* frame;    // frameにはNSRectが入る

@end



この値を保存するコードを実行してみる。


エラーが出た。。
*** -[NSKeyedArchiver encodeValueOfObjCType:at:]: this archiver cannot encode structs


NSKeyedArchiverでは構造体をエンコードできないとのこと。

Transformable Attributes によれば「変換可能」の場合の NSValueTransformer はデフォルトで NSKeyedUnarchiveFromDataTransformerName タイプが使われる。この場合、NSKeyedArchiverによってエンコードされたデータがファイルへ書き込まれることになる。このエンコード過程でエラーが発生したようだ。


Googleで探してみると同様の問題を取り上げたページがいくつかあった。

Re: coding struct with keyed archivers

Re: NSKeyedArchiver and NSValue

CocoaDev: EncodingNSRectAndNSPointInAKeyedArchiver

Archiver une NSValue avec NSKeyedArchiver
NSKeyedArchiverにカテゴリでCデータをアーカイブ/アンアーカイブするメソッドを実装する例が載っていた。


- - - -
簡単にはできそうもないな。

2008年2月24日日曜日

スクラップブックその12 - 画像の拡大縮小

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

画像を拡大縮小できるようにしてみる。



ソースコード:sp4-12.zip


拡大縮小はよくある四方に□をつけて行うやつではなく、ウィンドウのように右下だけで行うようにする。
<=このアイコンを画像の右下に表示する。この画像は OSについていた resize.gifを拝借している。

リサイズアイコンは画像が選択された時だけ表示して機能するようにする。こんな感じ。




機能実装のポイントは次のとおり。
 (1) リサイズアイコンの表示
 (2) リサイズアイコンドラッグ時の処理(拡大縮小)


(1) リサイズアイコンの表示
以前、導入した ItemIcon クラスを改良して使うことにする。初期化時に画像の4角(左上、右上、左下、右下)のどこへ表示するか指定し、位置決めの為に以前の setOrigin: をやめて、紐づける画像のサイズを渡す setBaseRect: に変更した。
その後、WorkViewに新たに ItemIconのインスタンス変数を追加し、表示処理を加えた。

(2) リサイズアイコンドラッグ時の処理(拡大縮小)
mouseDown: の中でリサイズアイコンのヒットテストを行い、押されているようなら新設のメソッド resizeIcon: を呼ぶ。

WorkView.m

-(void)resizeItem:(Item *)item
{
NSPoint current_point;
NSEvent* theEvent;

while (1) {
theEvent = [[self window] nextEventMatchingMask:(NSLeftMouseDraggedMask | NSLeftMouseUpMask)];
current_point = [self convertPoint:[theEvent locationInWindow]
fromView:nil];

[self autoscroll:theEvent];

[item setSize:NSMakeSize(current_point.x - [item.x floatValue],
current_point.y - [item.y floatValue])];
[_icon setBaseRect:[item rect]];
[_resize_icon setBaseRect:[item rect]];
[self setNeedsDisplay:YES];
if ([theEvent type] == NSLeftMouseUp) {
break;
}
}
}


ここでイベントループを作り、マウスボタンが離されるまで画像サイズの拡大縮小計算を行う。ドラッグの時にやっていた移動量のチェックは今回は行っていない。

画像をビューへ描画するのに以前 compositeToPoint:operation: を使っていたが、これでは拡大縮小表示ができないので別のメソッド NSView#drawInRect:fromRect:operation:fraction: を使う。

fromRect: に NSZeroRectを指定すると画像のサイズが使われ、これが drawInRect: で指定した領域へ写像される(拡大縮小が行われる)。



忘れずに dragImage: に渡す画像作成も compsiteToPoint: から drawInRect: へ切り替えておく。


なお compsiteToPoint: を drawInRect: へ切り替えただけだと画像が上下反転して表示されてしまう。



これは drawInRect: の場合はビューの flip状態を見て描画する為。スクラップブックでは描画先のビューである WorkView の isFlipped で YESを返すようにしている。この為、drawInRect: を使うと図のように上下反転してしまう。そこで画像も反転表示するように setFlipped: を使い flip属性を変えてしまう。最終的な WorkView#drawRect: は次のようになる。

- (void)drawRect:(NSRect)rect {
[[NSColor whiteColor] set];
NSRectFill(rect);

for (Item* item in [_controller items]) {
[item.image setFlipped:YES];
[item.image drawInRect:[item rect]
fromRect:NSZeroRect operation:NSCompositeSourceOver
fraction:1.0f];
[item.image setFlipped:NO];

if (_selected_item == item) {
// draw selection
[[[NSColor grayColor] colorWithAlphaComponent:0.3f] set];
[NSBezierPath fillRect:[item rect]];
[_icon draw];
[_resize_icon draw];
}
}
}


これで画像が正しく表示されるようになった。

- - - -
選択すると灰色になるがちょっと見づらい。枠に戻すか、別の表現の方がよさそう。

2008年2月23日土曜日

スクラップブックその11 - アイコン表示の変更ほか

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

マウスオーバーでアイコンの表示・非表示を制御するとちらちらして少々うっとおしい。そこで選択された時のみアイコンを表示するようにする。

mouseMoved: の中でマウス下の画像をチェックしていたがそれが不要になった。画像が選択されている (_selected_item != nil)の時だけマウスの位置をチェックするようにする。

WorkView.m

- (void)mouseMoved:(NSEvent *)theEvent
{
if (_selected_item == nil) {
return;
}

NSPoint cp = [self convertPoint:[theEvent locationInWindow] fromView:nil];

if ([_icon inRectAtPoint:cp]) {
[_icon setState:2];
} else {
[_icon setState:1];
}

[self setNeedsDisplayInRect:[_icon frame]];
[self setNeedsDisplay:YES];
}


アイコンの上にマウスがあるかを ItemIcon#inRectAtPoint で判断するだけの処理となる。すっきりした。

これに合わせて mouseDown: の処理も少し書き換える。具体的には、(1)画像選択時にアイコンを左上に表示する (2)画像選択時にアイコンを消す (3)画像をドラッグ中にアイコンの位置も合わせて移動させる。


ソースコード:sp4-11.zip



- - - -

WorkView.m の initWithFrame: 内にあった ItemIconの作成を今回 awakeFromNibへ移した。XCode上の実行では問題なかったが、アプリケーションとしてビルドし、別のマシンで実行したところアイコン表示が出ていないことに気がついたので。

2008年2月22日金曜日

スクラップブックその10 - 画像をファインダへドラッグ&ドロップする

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

スクラップブック上の画像をファインダへドラッグ&ドロップできるようにする。こんな感じ↓



をつかみファインダ上でドロップするとその画像のファイルがコピーされる。

ソースコード:sp4-10.zip


他アプリへのドラッグ&ドロップは NSView#dragImage:at:offset:event:pasteboard:source:slideBack: を使えば容易に実現できる。方法は以前の投稿で紹介した(「Finderへドロップ」)。

実装はまず最初にアイコンが押された(ドラッグに入った)かどうかの判定から入る。

WorkView.m

- (void)mouseDown:(NSEvent *)theEvent
{
   :
   :
if ([_icon inRectAtPoint:start_point]) {
   // 他アプリへのドラッグ&ドロップ処理
  }
   :
  // 通常の画像のドラッグ処理
}


アイコンが押されたらペースとボードとドラッグ用の画像を用意して dragImage:... を呼出す。
まず新規に NSImage を作成し、これにコピー元となる画像を半透明で描画する。

  NSImage *dragged_image = [[[NSImage alloc] initWithSize:[item image].size] autorelease];
[dragged_image lockFocus];
[[item image] compositeToPoint:NSZeroPoint
operation:NSCompositeSourceOver
fraction:0.8f];
[dragged_image unlockFocus];


続いて NSPasteboardの準備。NSFilesPromisePboardType を登録しておく。登録しておくとドロップ時に namesOfPromisedFilesDroppedAtDestination: がコールバックされる。

  NSPasteboard *pboard = [NSPasteboard pasteboardWithName:NSDragPboard];
[pboard declareTypes:[NSArray arrayWithObject:NSFilesPromisePboardType] owner:self];
[pboard setPropertyList:[NSArray arrayWithObject:[item.filename pathExtension]]
forType:NSFilesPromisePboardType];


最後に drawImage:... を呼出す。これでドラッグ操作が開始される。

  [self dragImage:dragged_image
at:NSMakePoint([item.x floatValue], [item.y floatValue]+[item.height floatValue])
offset:NSZeroSize
event:theEvent
pasteboard:pboard
source:self
slideBack:YES];



ペーストボードには NSFilesPromisePboardType が登録されているので、ファインダへドロップした時に namesOfPromisedFilesDroppedAtDestination: が呼出される。ここで画像のコピー処理を書いてやれば良い。

- (NSArray *)namesOfPromisedFilesDroppedAtDestination:(NSURL *)dropDestination
{
id dst_path = [[dropDestination relativePath] stringByAppendingPathComponent:_dragged_filename];
id src_path = [[_controller pathToSave] stringByAppendingPathComponent:_dragged_filename];

[[NSFileManager defaultManager] copyPath:src_path
toPath:dst_path
handler:nil];
return [NSArray arrayWithObject:_dragged_filename];
}


ドラッグしている画像のファイル名はこのメソッドだけでは得られないので、ドラック開始前にあらかじめメンバ変数 _dragged_filename へ取っておく。



なおこのままだと自分自身へもドロップができてしまう。この場合、実体の画像ファイルが1つなのに画面上は2つの同じ画像が表示される状態になる。これはこれで使い道もあるのだが、現状では削除した場合に矛盾が生じてしまうので禁止することにする。draggingEntered: と performDragOperation: に自分自身へのドロップかどうかのチェックを入れておく。

- (NSDragOperation)draggingEntered:(id )sender
{
if (self == [sender draggingSource]) {
return NSDragOperationMove;
}
return NSDragOperationCopy;
}

- (BOOL)performDragOperation:(id )sender {

if (self == [sender draggingSource]) {
return NO;
}
 :


NSDraggingInfo#draggingSource にはドラッグ元のオブジェクト(id)が入る(ドラッグ元が他のアプリケーションの場合は nilとなっていた)。



ここまでで簡単なスクラップブックの機能が実装できた。振り返って見ると改めて cocoaの強力さを感じた。ところどころ良くできているのに感心し、それとともに流れが分かってくると結構楽しかった。
なお、検証目的でとにかく早く作っていったので、メモリ確保の後始末(release)や描画の効率化、エラー処理の実装などは手を抜いてきた。きちんと作るとこの辺りで結局コード量は膨らむと思う。


- - - -
スクラップブックはこの後も検証目的ですこしずつ改良を加えていくつもりだが、そろそろちゃんとしたアプリの制作にとりかかろうと思う。最終目的はスクラップブックに近いものだがその前に小さなツールをいくつか作ってみたい。今後はそのあたりの開発過程もこのブログで紹介していきたいと思う。

2008年2月21日木曜日

スクラップブックその9 - 画像上にアイコンを表示する

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

マウスカーソルが画像の上に来たら、左上にアイコンを表示するようにする。



ソースコード:sp4-9.zip

このアイコンは他のアプリケーション(ファインダなど)へドラッグ&ドロップして画像をコピーする時につかむ位置を表している。ドラッグ処理は次回へ取っておいて、今回は表示する処理を追加する。


アイコンを管理する為に新しいクラス ItemIcon を追加した。

ItemIcon.h

@interface ItemIcon : NSObject {

NSImage *_image;
int _state;
NSRect _frame;
}
- (id)initWithImage:(NSImage*)image;
- (void)setOrigin:(NSPoint)point;
- (BOOL)inRectAtPoint:(NSPoint)point;
- (void)setState:(int)state; // 0=off/1=on/2=pushed
- (void)draw;
- (NSRect)frame;


ItemIconはアイコン画像と表示位置を保持する。これらの情報を使いマウスポインタがアイコン上にあるかどうかのチェックや、アイコン画像の描画処理を行う。3種類の状態(0=無効、1=画像上にマウスあり、2=アイコン上にマウスあり)を持ち、マウスの位置によって表示の有無や、透明度を変えている。


WorkView はこのインスタンスを変数として持ち、マウスポインタの位置などによって状態を制御する。

WorkView
- (void)mouseMoved:(NSEvent *)theEvent
{
   :
   :
[_icon setOrigin:[_mouseon_item rect].origin];
if ([_icon inRectAtPoint:cp]) {
[_icon setState:2];    // 2: アイコンの上にマウスがある
} else if (_mouseon_item) {
[_icon setState:1];    // 1: 画像の上にマウスがある(しかしアイコンの上には無い)
} else {
[_icon setState:0];    // 0: 画像の上にマウスが無い
}

[self setNeedsDisplayInRect:[_mouseon_item rect]];
[self setNeedsDisplay:YES];
}


マウス移動のイベントハンドラ(mouseMoved:)内でマウスポインタをチェックし、状態を決めている。そして drawRect: 内で描画する。これは ItemIcon#draw を呼び出すだけで良い。


アイコン画像はバンドル内に配置し、アプリ起動時に MyController がこれを読み込み NSImageクラスのインスタンスとして作成する。

MyController.m
- (void)awakeFromNib
{
   :
NSMutableDictionary* dict = [NSMutableDictionary dictionary];
NSArray* paths = [[NSBundle mainBundle] pathsForResourcesOfType:@"png"
inDirectory:nil];
for (NSString* file in paths) {
NSImage *image = [[[NSImage alloc] initWithContentsOfFile:file] autorelease];
[dict setObject:image forKey:[[file lastPathComponent] stringByDeletingPathExtension]];
}
_icons = [dict retain];
}


NSDictionary へ名前付きで入れておき、WorkViewが ItemIconを作成する時に取り出して使う。

- - - -

次回はこのアイコンをドラッグして別アプリ(ファインダなど)へコピーする処理を追加する。

2008年2月20日水曜日

スクラップブックその8 - マウスカーソル下の画像を反転させる

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

マウストラッキングの仕組みが分かったのでスクラップブックで使ってみる。まずはマウスカーソルが画像の上に乗ったら色を変える処理を加えてみよう。こんな感じ。



画像の上にマウスカーソルが乗ると赤くなる。

サンプル:sp4-8.zip


マウストラッキングには NSTrackingArea を使う。

WorkView.m

  _tracking_area = [[NSTrackingArea alloc]
initWithRect:[self bounds]
options:(NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved |NSTrackingActiveInKeyWindow | NSTrackingInVisibleRect| NSTrackingEnabledDuringMouseDrag)
owner:self
userInfo:nil];
[self addTrackingArea:_tracking_area];


NSScrollViewを使う場合は、トラッキングオプションに NSTrackingInVisibleRect を加えてやるとスクロールバーがトラッキング領域から外れるので便利。

これで mouseMoved: メッセージが送られてくるようになるので、マウスカーソル下に画像があるかチェックする。
- (void)mouseMoved:(NSEvent *)theEvent
{
NSPoint cp = [self convertPoint:[theEvent locationInWindow] fromView:nil];
_mouseon_item = nil;
for (Item* item_test in [[_controller items] reverseObjectEnumerator]) {
if (NSPointInRect(cp, [item_test rect])) { // ヒットテスト
_mouseon_item = item_test;
break;
}
}
if (_mouseon_item) {
[self setNeedsDisplayInRect:[_mouseon_item rect]];
}
[self setNeedsDisplay:YES];
}


画像があったら _mouseon_item へ入れて再描画する。

- (void)drawRect:(NSRect)rect {
 :
if (_mouseon_item == item) {
[[[NSColor redColor] colorWithAlphaComponent:0.3f] set];
[NSBezierPath fillRect:[item rect]];
}


これでマウスカーソル下の画像が赤くなる。

画像の数が多くなると上記のヒットテストは無駄が多い。mouseMoved:が呼ばれる頻度を考えるともっと負荷をかけない方法が必要。

- - - -

仕組みができたので次はこれを利用して画像にアイコンを貼付ける。