ページ

ラベル binding の投稿を表示しています。 すべての投稿を表示
ラベル binding の投稿を表示しています。 すべての投稿を表示

2010年5月7日金曜日

Cocoa Bindingsを使ったボタンの Disable

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

アプリが選択されていない時は「ー」ボタンを Disable にしたい。

Outletでつないで制御するのも手だが、Cocoa Bindings を使えばもっと簡単にできる。今回表示データは NSArrayController で管理しているので、ここの値(メソッド)とボタンの Enabled との間でバインドできれば良さそうだ。状況としては、選択されているか、非選択のかの2つの状態が判断できればいい。

NSArrayController のリファレンスを眺めていると、そのものズバリのメソッドはなかったが -[NSArrayController selectionIndexes] が NSIndexSet を返すので、-[NSIndexSet count] をとれば 0(非選択)、0以外(選択)が取れそうだ。これを仕込んでみる。

Key Path に selectionIndexes.count と書けば、selectionIndexes、count と順番にメッセージを投げてくれる。

# もっといい方法があれば教えて欲しい。

- - - -
Cocoa Bindings は便利。


(追加)複数の方の指摘で -[NSObjectController canRemove] があることが判明。

NSObjectController Class Reference - canRemove

こちらの方が適切のようだ。実際期待通りの動作を確認しました。
指摘してくれた方々ありがとうございました。

2009年11月30日月曜日

NSTableView にカスタムビューを表示する (5)カスタムセルへ bindings経由でモデルオブジェクトを渡す

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

(前回)Cocoaの日々: NSTableView にカスタムビューを表示する (4) カスタムセル続き

NSCell は通常一つのプロパティにバインドするようにできている。カスタムセルでは複数のプロパティを扱いたい。Bindings経由で複数プロパティを渡すには、そのプロパティを持つモデルオブジェクトそのものが渡せれば良い。

(例)Book.title ではなく Book 自体を渡す。そうすれば Book.author にもアクセスできる。

今回はこの課題を扱う。


サンプル作成

前回までのコードに手をいれていく。最初に Interface Builder を立ち上げて Bindings 設定を変更する。



カスタムセルを貼付けている NSTableView の NSTableColumn の Bindings パネルを開き、Model Key Pathを変更する。

前回は上のように特定のプロパティ(Homepage.title)を指していたのをやめ下のように空する。

こうするとプロパティではなくモデルクラス Hompage(のインスタンス)自体に Bindings される。※今回試すまでは Model Key Path が空にできるなんてしらなかった。ただ Controller Key と組み合わせてたんにオブジェクトを探す為のパス(文字列)を構成するだけに使われることを考えると動作することはあたりまえか。

こうするとカスタムセルには Homepage インスタンスが渡されるようになる。

続いて Hompage に copyWithZone: を実装する。
Homepage.m

@implementation Homepage

@synthesize title, image;

- (id)copyWithZone:(NSZone *)zone
{
Homepage* homepage = [[[self class] allocWithZone:zone] init];

homepage.image = [image copyWithZone:zone];
homepage.title = [title copyWithZone:zone];

return homepage;
}

@end


セルの内容を表示する時にその元となるデータはコピーされ、それが表示に使われる。仮に copyWithZone: を実装しないと次の例外が起きて落ちてしまう。


CustomCell[5953:10b] *** -[Homepage copyWithZone:]: unrecognized selector sent to instance 0x1377d0
CustomCell[5953:10b] An uncaught exception was raised




最後にセルで Homepage が取得できているか確認するために描画処理内にデバッグコードを入れておく。

CustomCell.m

- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView
{
[[NSColor greenColor] set];
NSRectFill(cellFrame);
[super drawWithFrame:cellFrame inView:controlView];

Homepage* homepage = [self objectValue];
NSLog(@"homepage: title=%@, image=%@", homepage.title, homepage.image);
}



実行

サンプルを実行してみよう。

セルの描画は何もコードを付け足していないので見た目は前回のまま。



デバッグ出力はこう。
CustomCell[5876:10b] homepage: title=SAMPLE-0, image=sample.jpg
CustomCell[5876:10b] setObjectValue: (null)
CustomCell[5876:10b] setObjectValue:
CustomCell[5876:10b] homepage: title=SAMPLE-1, image=sample.jpg
CustomCell[5876:10b] setObjectValue: (null)
CustomCell[5876:10b] setObjectValue:
CustomCell[5876:10b] homepage: title=SAMPLE-2, image=sample.jpg


Homepage のインスタンスがカスタムセルに渡っているのがわかる。意図通りに動いた。


解説

サンプルコードの構成はこんな感じになっている。


NSArrayController を通じて Homepage インスタンスが NSTableColumn へ渡り、最終的に CustomCell へコピーが渡される。CutomCell から見ると何もすることなく自動的に表示対象のデータ objectValue (すなわち Homepageインスタンス)が渡されることになる。このおかげで CustomCell は表示だけに専念できる。




ソースコード

github からどうぞ
CustomCell at 20091130 from xcatsan's SampleCode - GitHub


参考

複製を作るためにNSCopyingプロトコルに準拠する

copyWithZone: の件で参考になった。
最初 copyWithZone: の戻りを次のように autorelease していたらプログラムが吹っ飛んでいた。

Homepage* homepage = [[[[self class] allocWithZone:zone] init] autorelease];

autoreleae してはいけない。

Homepage* homepage = [[[self class] allocWithZone:zone] init];


Mac Dev Center: NSCopying Protocol Reference


- - - - -
Bindings が使えると実装がぐっと楽になる。次回は取得した複数のプロパティをカスタムセルで表示(描画)する方法を考えてみよう。

2009年11月25日水曜日

NSTableView にカスタムビューを表示する(2) ひな形作成〜Cocoa Bindings

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

(前回)Cocoaの日々: NSTableView にカスタムビューを表示する(調査中)

調査用にサンプルプログラムを作る。今回はテーブルビューにテストデータを表示するだけのプロジェクトを作成してみた。

クラス作成

Xcodeを立ち上げテンプレートから Cocoa Application を選んでプロジェクトを新規に作成する(Xcode3.1 / MacOSX10.5 で作成)。

まずコントローラーを作る。このクラスは表示用のデータ(配列)を保持している。初期化時にテストデータを生成して配列へ詰めておく。

TableViewController.h

@interface TableViewController : NSObject {

NSMutableArray* list;
}
@property (assign) NSMutableArray* list;

@end


TableViewController.m

- (id)init
{
self = [super init];
if (self) {
list = [[NSMutableArray alloc] init];

Homepage* homepage;

for (int i=0; i < 10; i++) {
homepage = [[[Homepage alloc] init] autorelease];
homepage.title = [NSString stringWithFormat:@"SAMPLE-%d", i];
homepage.image = @"sample.jpg";
[list addObject:homepage];
}
}
return self;
}



UI作成

次に InterfaceBuilder を起動しウィンドウへ NSTableView を貼付ける。

TableViewController と NSArrayController をインスタンス化しておく。

NSArrayController は NSTableView(のカラム)と TableViewController の間でのデータの橋渡しの役割を果たす。

カラムのバインディング設定で NSArrayController へバインドする。

一方、NSArrayController は TableViewController の list(NSMutableArray)へバインドする。

これでMVCが構成される。
 TableViewController.list <==> NSArrayController <==> NSTableViewColumn

 (Model)          (Controller)    (View)

(参考)Cocoaの日々 - 2005年8月


最終的な NSArrayController のバインド状態は下図のようになる。


実行

実行してみよう。

出た。


ソースコード

GitHub からどうぞ。
CustomCell at 20091125 from xcatsan's SampleCode - GitHub


- - - -
これをベースにしてカスタムビューを表示する検証を進める。

2009年11月21日土曜日

Cocoa Bindings のデバッグTips - ” is not key value coding-compliant for the key "

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

Cocoa bindings を使っているたまに遭遇するエラーとして、指定したキーパスが存在しないケースがある。下記は Homepage エンティティに image というキーが無いと指摘するエラー。

2009-11-21 12:25:07.548 BlogAssistant[7361:10b] [ valueForUndefinedKey:]: the entity Homepage is not key value coding-compliant for the key image.

もう少し詳しい情報を出力するオプションがあるらしい。

Mac Dev Center: Cocoa Bindings Programming Topics: Troubleshooting Cocoa Bindings

上記内の Binding to the incorrect key path によると詳細情報を出力するオプションがあるとのこと。起動時オプションに下記を加える。
-NSBindingDebugLogLevel 1






エラーメッセージはこんな感じ。
2009-11-21 12:38:11.667 BlogAssistant[7424:10b] Cocoa Bindings: Error accessing value for key path image of object  (entity: Homepage; id: 0x161200  ; data: {
    createdDate = 2009-11-17 06:08:46 +0900;
    imageName = "sample2.png";
    memo = MEMO;
    modifiedDate = 2009-11-17 06:08:46 +0900;
    title = "TEST TITLE:2009-11-17 06:08:46 +0900";
    url = "http://xcatsan.com/";
}) (from bound object (null)): [ valueForUndefinedKey:]: the entity Homepage is not key value coding-compliant for the key image.
対象となるモデルの情報が表示される(だけ)のようだ。

2009年11月18日水曜日

BlogAssistant(2) - NSTableViewへ画像を表示する

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

(前回)Cocoaの日々: BlogAssistant(1) - CoreData を NSTableView へ表示させる(NSArrayController経由でバインド)


前回は CoreData をテーブルビューへ表示する簡単なプログラムを作った。今回はテーブルビューに画像を表示させてみよう。画像データはファイルとして保存し、CoreData ではファイル名だけを管理するようにする。


サンプル画像の準備

最初はサンプル画像を用意してそれを表示するだけにする。後々は Webページのサムネイルが表示される。
今回は  sample1.png と sample2.png と2つ用意してみた。


NSImageCell 追加

まず InterfaceBuilder を開き、テーブルビューのカラムに NSImageCell を設定する。カラムの高さは NSTableView の属性(size)で調整した。

画像の表示方法

前回同様できるだけコードを書かずにすませたい。画像の表示も Bindings を使う。ただ CoreData にはファイル名しか格納されていないのでそのままでは表示できない。なんらかの方法で NSImage に変換させる必要がある。今回は Bindings の Value Transformer という機構を使い、ファイル名 => NSImage 変換を行わせよう。


バインディング

前回の設定に Value Transformer へ ImageTransformer を追加する。これはクラス名を表していてこの後定義する。

NSValueTransformer

NSValueTransformer は bindings を使った MVCアーキテクチャの中で、モデルとビュー間のデータ形式を変換するのに使う。使い方は NSValueTransformer のサブクラスを用意し、これをビュー側の bindings設定へ追加するだけ。こうするとモデルから渡されたデータは NSValueTransformer で変換された後、ビューで表示に使われる。逆にビューから入力されたデータを NSValueTransformer で変換してモデルへ格納することもできる。


(参考)

Mac Dev Center: NSValueTransformer Class Reference
Mac Dev Center: Value Transformer Programming Guide: Introduction to Value Transformers

Cocoaの日々: アプリケーションを開くを改造する(2)
Cocoaの日々: NSValueTransformer

今回は CoreData で管理しているファイル名を元に表示用の NSImage へ変換するクラスを作ってみた。モデル=>ビュー方向の変換のみ用意してある。

ImageTransformer.h
@interface ImageTransformer : NSValueTransformer {

}

@end

ImageTransformer.m

@implementation ImageTransformer

+ (Class)transformedValueClass
{
    return [NSImage class];
}

- (id)transformedValue:(id)value
{
if (!value) {
return  nil;
}
NSImage* image = nil;
NSString* path = [[NSBundle mainBundle] pathForImageResource:value];
if (path) {
image = [[NSImage alloc] initWithContentsOfFile:path];
}
return image;
}
@end


今回はサンプル画像がリソースフォルダ内にあるので、CoreDataから取り出したファイル名(value)を元に NSImage インスタンスを生成して返す。


動作確認

さて動かしてみよう。


おー出た。

こうも意図通りに動くと楽しくてしょうがない。
徐々にバインディングとCoreDataのコツが掴めてきた気がする。
Cocoa プログラミングは面白い。


ソースコード

BlogAssistant at master from xcatsan's SampleCode - GitHub

※git に慣れてなくて機能のコードを上書きしてしまった。。タグ使い方を勉強をせねば。

2009年11月17日火曜日

BlogAssistant(1) - CoreData を NSTableView へ表示させる(NSArrayController経由でバインド)

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

以前、書いたようにブログ書き支援用の小アプリを作る。
Cocoaの日々: SimpleCapへ Webページのスクリーンショット機能を追加する(予定)

HTMLのAタグとサムネイル画像を作るようなアプリを考えている。名前は BlogAssistant にしよう。このデータを CoreData で管理させようと思っていろいろ試行錯誤している。今回は簡単なモデルを作成し、それを NSTableView へ表示させてみよう。

Xcodeで Core Data Application を作成する

(以下、MacOSX10.5、XCode 3.1.1で操作)

新規プロジェクトを作る。後々 Safariのプラグインに変える予定だが CoreDataの挙動を確認しながら作りたいのでまずは単体のアプリケーションとして作る。Xcodeで新規プロジェクトを作成しテンプレートの中から Core Data Application を選択する。



自動的に *_AppDelegate クラスが生成される。


このクラスにはあらかじめ Core Data 永続化スタックを使うのに必要なコードが自動的に用意されている。ヘッダファイルはこんな感じ。
BlogAssistant_AppDelegate.h

#import

@interface BlogAssistant_AppDelegate : NSObject
{
    IBOutlet NSWindow *window;
  
    NSPersistentStoreCoordinator *persistentStoreCoordinator;
    NSManagedObjectModel *managedObjectModel;
    NSManagedObjectContext *managedObjectContext;
}

- (NSPersistentStoreCoordinator *)persistentStoreCoordinator;
- (NSManagedObjectModel *)managedObjectModel;
- (NSManagedObjectContext *)managedObjectContext;

- (IBAction)saveAction:sender;

@end

これをベースに作って行こう。


モデルの定義

*.xcdatamodel を開いてデータモデルを定義する。エンティティ Homepage をこんな感じで作ってみた。

併せてモデルクラスを用意する。

新規ファイルを作成し、属性をプロパティ定義していく。このあたりXCodeが自動生成してくれると良いのだが(何か方法があるのだろうか?)。
Homepage.h

#import

@interface Homepage : NSManagedObject {

}
@property (retain, nonatomic) NSDate * createdDate;
@property (retain, nonatomic) NSString * imageName;
@property (retain, nonatomic) NSString * memo;
@property (retain, nonatomic) NSDate * modifiedDate;
@property (retain, nonatomic) NSString * title;
@property (retain, nonatomic) NSString * url;

@end



Homepage.m

#import "Homepage.h"

@implementation Homepage

@dynamic createdDate;
@dynamic imageName;
@dynamic memo;
@dynamic modifiedDate;
@dynamic title;
@dynamic url;

@end



@dynamic 指定している件については以前のブログを参照されたし。
Cocoaの日々: CoreData - NSManagedObject のプロパティ


テーブルビューの用意とバインディング

InterfaceBuilder を立ち上げ、ウィンドウへテーブルビューを配置する。


続いてモデルとビューの橋渡しを行う NSArrayController を MainMenu.xib へ追加する。


これを MainMenu.xib へ追加。

インスペクタを開き、Attributes を編集する。

変更点は下記の通り:
  • Mode を Entity へ変更する
  • Entity Name へ先ほど用意したモデルでのエンティティ名 Homepage を指定する
  • Prepares Content にチェックを入れる(これで NSArrayController が自動的に NSManagedObjectContext 経由で保存データを読み込んでくれる)。 

次にテーブルビューのカラムを選択した後、NSArrayController へバインドする。

Controller Key へ arrangedObjects を、Model Key Path に title(カラムに表示するモデルの属性)を指定する。

他のカラムも同様にバインドしておく。

ここまででコントローラー(NSArrayController)とビュー(NSTableColumn)とのバインドができた。後はコントローラとモデルの紐付けが必要。NSArrayController は(親クラスの NSControllerは)CoreDataをサポートしていて -[setManagedObjectContext:] がある。これを使ってコントローラとモデルを結びつける。
参照:Mac Dev Center: NSObjectController Class Reference

BlogAssistant_AppDelegate.h

@interface BlogAssistant_AppDelegate : NSObject
{
      :
    IBOutlet NSArrayController *arrayController;
}



ヘッダへアウトレットを一つ追加し、InterfaceBuilderでNSArrayController へ接続する。

最後に NSArrayController へ NSManagedObjectContext を設定する。これは今回コードで書く。

BlogAssistant_AppDelegate.m

- (void)awakeFromNib
{
[arrayController setManagedObjectContext:[self managedObjectContext]];

}



これで Model-View-Controller が全てつながった。データの管理は NSManagedObjectContext が良きに?取りはからってくれる。


テスト用ボタン追加

表示を確認するにはテストデータが必要。テストデータを追加するボタンを付ける。

ボタンのターゲットは BlogAssistant_AppDelegate とし、アクションは addTestRecord: につなぐ。実装はこんな感じ。

BlogAssistant_AppDelegate.m

- (IBAction)addTestRecord:(id)sender
{
Homepage* homepage = [NSEntityDescription insertNewObjectForEntityForName:@"Homepage"
inManagedObjectContext:[self managedObjectContext]];
homepage.title = [NSString stringWithFormat:@"TEST TITLE:%@", [[NSDate date] description]];
homepage.imageName = @"TEST IMAGE";
homepage.url = @"http://xcatsan.com/";
homepage.createdDate = [NSDate date];
homepage.modifiedDate = [NSDate date];
homepage.memo = @"MEMO";

NSError* error = nil;
[[self managedObjectContext] save:&error];

if (error) {
NSLog(@"INSERT ERROR: %@", error);
} else {
NSLog(@"INSERTED");
}

}


保存形式を SQLite へ変更する

自動生成されたコードでは保存形式が XMLになっている。これを SQLへ変更する。


BlogAssistant_AppDelegate.m

- (NSPersistentStoreCoordinator *) persistentStoreCoordinator {

    url = [NSURL fileURLWithPath: [applicationSupportFolder stringByAppendingPathComponent: @"BlogAssistant.db"]];
    persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: [self managedObjectModel]];
    if (![persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:url options:nil error:&error]){
        [[NSApplication sharedApplication] presentError:error];
    }  


}



動作確認

動かしてみよう。起動して何度かボタンを押してテストデータを生成する。


NSManagedObjectContext へ対するテストデータ追加がテーブルビューへ自動的に反映された。ダブルクリックすると変更もできる。

アプリを止めて再起動するとデータが復元される。

ここまでで MVCの連携コードを書いて InterfaceBuilder を設定しただけ。CoreDataとBindingsを使うと、ローカルDB付きのテーブルビューがこんなに簡単にできてしまう。


保存データ

さてデータはいったいどこに保存されているのか。自動生成されたコードを見るとライブラリ配下の Application Support にあるようだ。

    url = [NSURL fileURLWithPath: [applicationSupportFolder stringByAppendingPathComponent: @"BlogAssistant.db"]];



ターミナルを開き sqlite3 コマンドで中身が確認できる。




ソースコード

github へ上げました。
BlogAssistant at master from xcatsan's SampleCode - GitHub



おまけ:SQLite のログ

実行時オプションにを -com.apple.CoreData.SQLDebug 1 付けるとデバッガコンソールへ SQLite のログを出力させることができる。

(参考)Cocoa Touch の日々: CoreData で発行されている SQL をデバッグ出力する

こんな感じ。


- - - -
BlogAssistant開発:続く..

2009年2月11日水曜日

Cocoa Bindingsを使ったメニューの排他選択

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

前回 SimpleViewerにコンテキストメニューを追加したが、この中で背景は Black, Checkboard, White の3つから一つ選択するようになっている。


例えば Blackにチェックがついている状態でCheckboardを選ぶと、Balckのチェックが外れ Checkboardにチェックが付く。これを実装するには少し工夫が必要で以前はTarget/Actionを使って力技で対処した。

Cocoaの日々: simpleCapへコンテキストメニューを追加 (2)


今回はTarget/Actionではなく Cocoa Bindings で対処してみた。

NSMenuItemの Value へモデルを紐付け(バインディイング)しておくと、その値に応じてメニューにチェックが付いたり、外れたりする。例えば下の図では NSMenuItem "Balck" に backgroundBlack というモデルを紐づけてある。モデルのオーナーは "Simple Viewer Controller" にした。これは InterfaceBuilder上でインスタンス化してあって扱いやすいため。同様に "Checkboard"には backgroundCheckboard、"White"には backgroundWhite という名前を紐づけておく。


次はモデルの実装を行う。Cocoa Bindings では Key-Value-Codingの仕組みを採用しているので実体はなんであれ setter/getter を用意してやるとそれが使われる。先ほどの backgroundBlack というモデルの場合、下記のようなインターフェイスを用意すればいい。

- (BOOL)backgroundBlack;
- (void)setBackgroundBlack:(BOOL)flag;

同様に他のモデルも用意してやる。
- (BOOL)backgroundWhite;
- (void)setBackgroundWhite:(BOOL)flag;
- (BOOL)backgroundCheckboard;
- (void)setBackgroundCheckboard:(BOOL)flag;

これでメニューが表示される時、もしくは選択された場合は自動的にこれらのメソッドが呼出される。

さて実装だが、背景の値はUserDefaultsで管理している。この実体は整数値で 0=Black, 1=Checkboard, 2=Whiteと割当てある。
それを踏まえた上で backgroundBlackの実装を見てみよう。まずメニューを表示する時には現在の値をチェックする為に getter である #backgroundBlack が呼出される。チェックを付けるか否かは背景値が 0(=Black)かどうかで判断できる。こんな感じ。
- (BOOL)backgroundBlack{
return ([[UserDefaults valueForKey:UDKEY_VIEWER_BACKGROUND] intValue] == 0);
}


次はメニューで選択された時。この場合は setterにあたる #setBackgroundBlack: が呼ばれる。
- (void)setBackgroundBlack:(BOOL)flag
{
if (flag) {
[UserDefaults setValue:[NSNumber numberWithInt:0] forKey:UDKEY_VIEWER_BACKGROUND];
[self setBackgroundCheckboard:NO];
[self setBackgroundWhite:NO];
[UserDefaults save];
} else if ([[UserDefaults valueForKey:UDKEY_VIEWER_BACKGROUND] intValue] == 0) {
[self setBackgroundCheckboard:YES];
}
}

もし flag==YES ならチェックされたことになるので、他の checkboard, white の値を NOにする。これが排他処理のポイント。そして後半の else if は、元々チェックが付いている状態で選択がされた時の処理を行っている。何もしないと見た目はチェックが外れてしまうので、今回は次の checkboardにチェックが付く様にしておいた。blackからチェックを外さないというインターフェイスもあるが、今回のBindingsだけを使う方法では難しい。特に害は無いと思うので今回はこれで行こう。

他の2つの設定についても同じ処理を書けばできあがり。

- - - -
もっとスマートな方法があると良いのだが
今はこれが精一杯か。
(良い方法があれば是非教えて欲しい)

2009年1月29日木曜日

キー監視(で失敗)

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

プリファレンス設定を変更した時に SimpleViewerの表示へ即時反映したい。


NSUserDefaultsに対してキー監視を登録してやれば良い。

[[NSUserDefaults standardUserDefaults]
addObserver:self
forKeyPath:@"ViewerOptions.ImageBounds"
options:NSKeyValueObservingOptionNew
context:nil];


とすると、NSUserDefaultsの ViewerOptions_ImageBounds 値が変更されると下記がコールバックされる。
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context


だがアプリ起動時に下記エラーを出して強制終了してしまった。
[ addObserver: forKeyPath:@"ViewerOptions.ImageBounds" options:0x1 context:0x0] was sent to an object that is not KVC-compliant for the "ViewerOptions" property.


原因は Keypath文字列に . (ドット)が含まれていたこと。ドットは Keypathの区切りとして使われる特別な文字な為、別の解釈をされてしまった。 . をやめれば問題ない。

[[NSUserDefaults standardUserDefaults]
addObserver:self
forKeyPath:@"ViewerOptions_ImageBounds"  アンダースコア _ にしてみた。
options:NSKeyValueObservingOptionNew
context:nil];


- - - -
不注意にも NSUserDefaultsのキーのほとんどで . を使っていたので結局全部 _ に書き換えた。やれやれ。。

2008年12月1日月曜日

バインディングメモ(EnabledとHidden)

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

Cocoa Bindings には値のバインディングの他にコントロールの状態(Availability)を制御できるものが用意されている。それが EnabledとHidden。下図は InterfaceBuilderで NSMenuIitemの Hidden項目を見たところ。



ここで変数やメソッドなどにバインディングすると、その内容によってそのNSMenuItemを隠したり、ディゼーブルにすることができる。

これを使っていて気がついたことがある。
"Enabled" のバインディングはメニューを表示するたびに値が評価されるのだが、Hiddenは初回のみので表示毎には評価してくれない。つまりある条件によって、そのNSMenuItemを表示したり、隠したりすることは初回表示時のみ可能で、それ以降の表示では制御できない。

それだけ。

2008年4月14日月曜日

WindowList (その4) - 選択したウィンドウを反転させる

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

リスト上で選択したウィンドウをハイライトするようにしてみた。



ソース:WindowList-4.zip

アルゴリズムは以前紹介したものをそのまま使っている。

2008年4月13日日曜日

WindowList (その3) - バインディング値の表示形式

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

さて NSValueTransformer、NSFormatter と検証して、その結果、バインディング値を任意の形式で表示させるには NSFormatterが良いことが分かったので、これを Windowリストへ組み込んでみる。

ソース:WindowList-3.zip

NSFormatterを組み込んだ結果、Boundsの表示が下の様だったのが


こういう風になった。


NSFormatter導入前は、NSCFDictionary の文字列表記(#description)の一行目が表示されていた。

{     <--ここだけが表示されていた
Height = 383;
Width = 740;
X = 625;
Y = 377;
}


今回 RectFormatterを導入して NSCFDictionaryに格納されている上記値をカンマ区切りで並べた文字列に変換するようにした。

RectFormatter.h
@interface RectFormatter : NSFormatter {
}
@end


RectFormatter.m
@implementation RectFormatter
- (NSString *)stringForObjectValue:(id)anObject
{
if (![anObject isKindOfClass:[NSDictionary class]]) {
return nil;
}
return [NSString stringWithFormat:@"%4d,%4d,%4d,%4d",
[[anObject objectForKey:@"X"] intValue],
[[anObject objectForKey:@"Y"] intValue],
[[anObject objectForKey:@"Width"] intValue],
[[anObject objectForKey:@"Height"] intValue]];
}
 :


メソッド内のクラスチェックは重要で、最初これをやらなかったのでクラッシュした。理由はカラム値の初期値に InterfaceBuilderで設定された "Text Cell"が入っており、これが一番最初にこのメソッドに渡されるため。その結果 NSStringのインスタンスに対して #objectForKey: を投げることになったのでプログラムがクラッシュしていた。


クラス定義後に Interface Builder でインスタンス化してやり、目的の列(NSTextFieldCell)の fomatterアウトレットに接続する。


さらに今回は桁の表示位置を合わせたかったので等幅フォントを選び、サイズも 11ptと小さくした。これを行なう為に #attributedStringForObjectValue:withDefaultAttributes: を実装する。

- (NSAttributedString *)attributedStringForObjectValue:(id)anObject withDefaultAttributes:(NSDictionary *)attributes
{
NSString* str = [self stringForObjectValue:anObject];
NSFont *font = [NSFont userFixedPitchFontOfSize:11.0];
[attributes setValue:font forKey:NSFontAttributeName];

return [[[NSAttributedString alloc] initWithString:str
attributes:attributes] autorelease];
}



NSFormatterは初めて作ったが便利だ。アウトレットの接続だけで表示形式が追加できるとは Cocoaはよく考えられてるな。

2008年4月9日水曜日

WindowList(その2) - 全情報表示

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

前回スクリーン上にあるウィンドウのリストを表示するプログラムを作ったが、今回全ての情報が表示できるように手を加えた。

こんな感じ。ずいぶんと横幅を取るようになったが全ての情報が一覧できるようになった。ついでに CGWindowListOptionも制御できるようにした。


ソース:WindowList-2.zip

前回は WindowItemなるクラスを用意して、わざわざCGWindowListCopyWindowInfo() から取得した情報をつめ直していた。しかしよくよく考えると、CGWindowListCopyWindowInfo()で得られる結果は CFDictionaryRef であり、CGWindow.h で定義される下記のようなキーを持っている。

/* sample
kCGWindowAlpha = 1;
kCGWindowBounds = {
Height = 439;
Width = 262;
X = 15;
Y = 22;
};
kCGWindowIsOnscreen = 1;
kCGWindowLayer = 0;
kCGWindowMemoryUsage = 510808;
kCGWindowName = "Account Information";
kCGWindowNumber = 21155;
kCGWindowOwnerName = EasyToDo;
kCGWindowOwnerPID = 5452;
kCGWindowSharingState = 1;
kCGWindowStoreType = 2;
kCGWindowWorkspace = 1;
*/


CFDictionaryRef は NSDicrionaryと互換があるし、キーバリュー値コーディングに準拠している。
ということはバインディングが使えるのでは?

そこでやってみた。まずビュー(NSTableViewのカラム)とモデル(CFDictionaryRefの配列)を仲介する NSObjectController を用意し、MyControllerのインスタンス変数 windowList へバインドする。後はすべてのカラムの Key Path にキー名を指定する(下記例では "kCGWindowOwnerName")。


するとあっさりと、できた。

個々のカラムの表示設定はGUI側の設定だけで、コーディングはいっさい行っていない。すごいな。バインディングは。


全体の関係はこんな感じ。


コードも非常に簡単になった。

-(void)reloadList
{
[windowList removeAllObjects];

UInt32 option = ...(略);

CFArrayRef list =CGWindowListCopyWindowInfo(option, [window windowNumber]);

CFDictionaryRef w;
CFIndex i;

for (i=0; i < CFArrayGetCount(list); i++) {
w = CFArrayGetValueAtIndex(list, i);
[windowList addObject:(NSDictionary*)w];
}

[arrayController rearrangeObjects];
}


カラムに関するコードがまったく無い。

- - - -
リストを眺めているといろいろと面白いことがわかる。この辺りはまた別に紹介しよう。
ただ問題があって見ての通り "Bounds" がうまく表示できていない。バインディング値を表示用に変換する仕組みが必要だな。

2008年4月8日火曜日

WindowList(その1) - しつこく CGWindowListCopyWindowInfo

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

しつこく CGWindowListCopyWindowInfoの話題を。現在表示中のウィンドウリストをNSTableViewで表示させる。
ADCのサンプルがまさにそういうアプリなのだが、作成中のアプリで必要なので自分で組み立ててみる。

こんな感じ。


ソース:WindowList.zip

バインディングを使うとあっと言う間?にできる。組み立ては下のようになる。


注意点としては NSArrayController を介している為、直接 windowList(NSMutableArray)を変更しても NSTableViewに反映されないこと。NSArrayControllerのメソッド(addObject:など)を使うか、変更後に rearrangeObjectsを呼出す必要がある。NSTableViewの reloadData や setNeedDisplay: を使ってもダメ。

メイン部分のコードを掲載しておく。

MyController.m

-(void)reloadList
{
[windowList removeAllObjects];

CFArrayRef list =CGWindowListCopyWindowInfo(
(kCGWindowListOptionAll|kCGWindowListOptionOnScreenOnly|kCGWindowListExcludeDesktopElements), kCGNullWindowID);
CFDictionaryRef w;
CFIndex i;

CGWindowID windowID;
CFStringRef name;
CFStringRef owner;
CGRect rect;

for (i=0; i < CFArrayGetCount(list); i++) {
w = CFArrayGetValueAtIndex(list, i);

CFNumberGetValue(CFDictionaryGetValue(w, kCGWindowNumber),
kCGWindowIDCFNumberType, &windowID);

name = CFDictionaryGetValue(w, kCGWindowName);
owner = CFDictionaryGetValue(w, kCGWindowOwnerName);

CGRectMakeWithDictionaryRepresentation(CFDictionaryGetValue(w, kCGWindowBounds), &rect);

WindowItem* item = [[[WindowItem alloc] init] autorelease];
[item setValue:[NSNumber numberWithInt:windowID] forKey:@"windowID"];
[item setValue:(NSString*)name forKey:@"name"];
[item setValue:(NSString*)owner forKey:@"owner"];
NSRect rect2 = NSRectFromCGRect(rect);
[item setValue:NSStringFromRect(rect2) forKey:@"frame"];
[windowList addObject:item];
}

[arrayController rearrangeObjects];
}



- - - -
作ってはみたものの、表示情報が足りないな。
全情報を表示するよう手を加えよう。

2008年4月4日金曜日

PreferencesをBindingで使う(その2)

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

Preferences情報を NSUserDefaultsController で管理する場合、自前のコントローラコードでこれを参照するにはどうするか。単純にアウトレットでつなぎ NSUserDefaultsを取得してみた。

ボタンを押すと MyControllerクラスがPrefereces情報から FileNameを取得して表示する。


ソース: BindingPreferences2.zip



MyController.h

@interface MyController : NSObject {

IBOutlet NSTextField* _label;
IBOutlet NSUserDefaultsController* _user_defaults_controller;
}
-(IBAction)getName:(id)sender;


InterfaceBuilderで上記アウトレットをそれぞれ繋いでおく。


MyController.m
-(IBAction)getName:(id)sender
{
NSUserDefaults* defaults = [_user_defaults_controller defaults];
NSString* str = [defaults stringForKey:@"FileName"];
[_label setStringValue:str];
}


ボタンが押されたらアウトレットの NSUserDefaultsControllerを経由して NSUserDefaultsを取得する。


ベタだが、裏方はこんなところか。

2008年4月3日木曜日

PreferencesをBindingで使う

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

PreferecesをBindingを利用して使ってみた。

参考情報:ADC: Binding your Preferences in Cocoa

下のようなウィンドウを作り、各コントロールを NSUserDefaultsController とバインドする。


下は "Shadow"チェックボックスのバインディング。Valueの Model Key Path に適当な名前を入れておく。ここでは項目名と同じ "Shadow"を入れているが何でも良い。


すると自動的に MainMenu.nib に Shared User Defaults Controller が追加される。



これだけでおしまい。

アプリを実行してチェックボックスやテキストボックスを編集するとそれが自動的に保存される。次回起動時は保存した値がやはり自動的に反映される。

保存情報はユーザディレクトリに作成される。
ディレクトリ:~/Library/Preferences/
ファイル名:<アプリ名>.plist

下記はサンプルアプリで作成されたファイル。


plist形式で中身はこんな感じ。バインド時に指定した Key Path が項目名として使われている。



なお Pop Up Button は Selected Value にバインドさせた。
また Textfield は Textfield Cellにバインドさせること。

ソース: BindingPreferences.zip

- - - -
参考情報のように Interface Builder上で扱えるコントロールのバインドは簡単にできる。でもコントローラのコード内で値を参照したい場合はどうするのか?