BathyScaphe

BathyScapheWiki

ImagePreviewerを作成する

BathyScaphe 1.1 から採用された Previewer プラグインを作成しよう。

目次

用意するもの

チュートリアル(シンプルプレビューアを作ろう)

Complete-t.png完成図

このチュートリアルでは、ごく簡単なプレビューアを作成して、プレビューアの作成方法の基本を覚える手助けをします。
このチュートリアルは Xcode 2.2 がインストールされていること、を前提としています。
本当の基本だけを押さえたチュートリアルであり、コーディングの必要はありますが、それはたったの65行です。

概要

Class-Diagram.pngこのチュートリアルで作成するプレビューアを見てみましょう。
ご覧の通り、一枚のウインドウに一枚の画像を表示するだけのごく簡単なものです。

クラス階層を見てみましょう。
このチュートリアルで作成するプレビューアは NSWidnowController を継承した、SimplePreviewer クラスだけを作成します。
ごくごくシンプルです。

プロジェクトの作成

では早速、Xcodeでプロジェクトを作成しましょう。
「ファイル」メニューから「新規プロジェクト...」を選択してください。
通常のアプリケーションの作成と違いここでは、「Bundle」->「Cocoa Bundle」を選んで「次へ」を押してください。
New_project.png
次に、プロジェクトの名前を決めましょう。
ここでは「SimplePreviewer」としました。「完了」を押してください。
New_project-2.png
これで、プロジェクトが作成されました。

Info.plist の編集

ターゲットのプロパティの編集

Target-Property.pngPreviewer では、Info.plistの修正が必須となっています。
まず、プロジェクトウインドウの「グループとファイル」の「ターゲット」を開きます。
その下層に「SimplePreviewer」ターゲットが自動的に作成されていますので、それをダブルクリックします。
「""ターゲット "SimplePreviewer""の情報」ウインドウが開きます。
このウインドウの「プロパティ」タブを選択してください。
ここで「識別子」(CFBundleIdentifierのことです)を編集します。
通常は自分の所持するドメイン名の順番をひっくり返したものに、プロダクト名を追加したものを使用します。
なければ、適当でよいでしょう。
ここでは「com.masakih.SimplePreviewer」としています。
次に「主要クラス」(NSPrincipalClassのことです)を指定します。
「主要クラス」は「SimplePreviewer」です。
「プロパティ」タブでの作業は以上です。

ターゲットのビルドの編集

Target-Build.png
次に「ビルド」タブを選択してください。
このタブでは、プロダクト名とラーパーの拡張子を編集します。
まず、「構成」を「すべての構成」にします。
これは、デバッグビルド、リリースビルドでもともに有効にするためです。
次に「コレクション」を「パッケージング」にします。
これは、単に見やすくするために表示する項目を減らすためです。
では編集しましょう。
1つ目は「プロダクト名」です。
Previewer プラグインの名前は「ImagePreviewer」でなければならないと決められていますので、「プロダクト名」を「ImagePreviewer」に変更します。大文字小文字に注意してください。
2つ目は、「ラッパーの拡張子」です。
拡張子は「plugin」でなければならないと決められていますので、「ラッパーの拡張子」を「plugin」に変更します。
以上でInfo.plist の編集は終わりです。

インターフェイスの作成

コーディングの前に見た目を作ります。
Interface Builder を起動してください。
「File」メニューから「New...」を選択します。
「Starting Point」というウインドウが開きます。
「Cocoa」の「Window」を選択して「New」をクリックします。
Starting-Point.png

出来上がったnibに部品を追加していきます。
「Window」をダブルクッリクしてウインドウを表示してください。
IB.png

このウインドウに NSImageViewを貼付けます。
Interface-00.png
Interface-01.png

貼付けたNSImageViewをウインドウいっぱいに広げます。
青く表示されるラインはHIGで推奨されるインターフェイスの配置位置や大きさの指標です。
「Tools」メニューから「Show Inspector」を選びインスペクターを表示し、図のようにNSImageView の「Size」の「Autosizing」を設定しておきましょう。
Interface-02.png

ではここで一旦保存しておきましょう。
先ほど作ったXcode のプロジェクトと同じ場所に保存してください。
ここでは必ず名前を「SimplePreviewer.nib」にしてください。
Interface-03.png
Interface Builder は保存したフォルダに Xcode のプロジェクトファイルがあると、そのプロジェクトに保存した nib を含めるかどうかを聞いてきます。
Interface-04.png
もちろん含めてください。この時「SimplePreviewer」ターゲットにチェックを入れておくことを忘れないでください。
まだ、使いますので、nibは閉じないでください。

クラスの作成

まだコーディングはしません!

Class-00.png
クラスの作成はInterface Builder上で行います。
NSWindowControllerクラスのサブクラスSimplePreviewerを作成します。
nibの「Classes」タブを選択してください。
NSWidowControllerクラスを探して、選択します。

Class-01.png
NSWidowControllerクラスを選択した状態で「Enter」キーを押します。
するとNSWindowControllerクラスのサブクラスが作成され、名前の入力待ち状態になります。
もちろん名前は「SimplePreviewer」です。

Class-02.png
では、「SimplePreviewer」の関連ファイルもInterface Builderで作っておきましょう。
「SimplePreviewer」を選択し、「Classes」メニューから「Create Files for SimplePreviewer」を選択します。

Class-03.png
すでに、nibがXcodeのプロジェクトと関連していることをInterface Builderは知っていますので、適切な フォルダが既に選択されています。
また、Xcodeプロジェクトでのターゲットもここで選択可能です。

まだまだ、使いますので、nibは閉じないでください。

クラスとGUI部品を関連付ける (Cocoa Binding)

まだまだコーディングはしません!

「Instances」タブを選択し、「File's Owner」の情報を表示させます。
「Custom Class」で、「File's Owner」のクラスを「SimplePreviewer」にします。
Relation-01.png
「File's Owner」をコントロール+クリックし、コントロールキーを押したまま「Window」にドラッグします。
インスペクターの「Connections」が表示されますので、「window」を選択し「Connect」をクリックします。
Relation-02.png

ついでに、NSImageViewに表示するものも設定しておきましょう。
ウインドウを開いて、NSImageViewを選択します。
インスペクターで「Bindings」を表示してください。
「Value」の「valueURL」を開きます。
ここで、「Bind to」は「File's Owner(SimplePreviewer)」、「Model Key Path」を「imageURL」としてください。
これで、クラスとGUI部品との関連付けは終わりです。
nibを保存して、Interface Builderを閉じてください。
Relation-03.png

コーディング

いよいよコーディングです。コーディングと言っても65行だけですので臆することは全くありません。

まず、プロジェクトにBathyScaphe Plugin Development Kitに含まれているBSImagePreviewerInterface.hを追加します。
Finderからドロップするだけです。
Adopt-00.png
追加するときに「ディスティネーショングループのフォルダに項目をコピーする(必要な場合)」をチェックしておくとプロジェクトのフォルダを移動してもファイルを見失うことがないのでチェックを入れておくことをお薦めします。
Adopt-01.png

やっとコードを書くことが出来ます!
骨組みはInterface Builderが作ってくれていますので、あとは肉付けをするだけです。
まずはヘッダファイルSimplePreviewer.hから。
やるべきことは、

  • BSImagePreviewerInterface.hをインポートすること。(4行目)
  • SimplePreviewerがBSImagePreviewerInterfaceに準拠していることを示すこと。(6行目)
  • プレビューする画像のURLを保存するソケット(インスタンス変数)を用意すること。(8行目)

だけです。
何と、ヘッダはコメント込みのたったの10行。以下に示します。

    1	/* SimplePreviewer */
    2	
    3	#import <Cocoa/Cocoa.h>
    4	#import "BSImagePreviewerInterface.h"
    5	
    6	@interface SimplePreviewer : NSWindowController <BSImagePreviewerProtocol>
    7	{
    8		NSURL *imageURL;
    9	}
   10	@end

簡単ですね。

では実装ファイルにいきましょう。
やるべきことは、

  • NSImageViewにバインドしたimageURLをKVCで表現する。(9~18行目)
  • BSImagePreviewerInterface に準拠する。(21~54行目)

の2点。まずはソースを見てください。コーディングスタイルにクセがありますが、これはCocoMonarからの伝統のようです。

    1	#import "SimplePreviewer.h"
    2	
    3	@implementation SimplePreviewer
    4	- (void) dealloc
    5	{
    6		[imageURL release];
    7		[super dealloc];
    8	}
    9	- (NSURL *) imageURL
   10	{
   11		return imageURL;
   12	}
   13	- (void) setImageURL : (NSURL *) newImageURL
   14	{
   15		id tmp = imageURL;
   16		imageURL = [newImageURL retain];
   17		[tmp release];
   18	}
   19	#pragma mark-
   20	#pragma mark##BSImagePreviewerProtocol##
   21	- (id) initWithPreferences : (AppDefaults *) prefs
   22	{
   23		// クラスと同名のnibファイルを指定。
   24		self = [super initWithWindowNibName : NSStringFromClass([self class])];
   25		if(self) {
   26			// Load nib.
   27			[self window];
   28		}
   29		
   30		return self;
   31	}
   32	- (AppDefaults *) preferences
   33	{
   34		return nil;
   35	}
   36	- (void) setPreferences : (AppDefaults *) aPreferences {}
   37	- (BOOL) showImageWithURL : (NSURL *) inImageURL
   38	{
   39		[self setImageURL : inImageURL];
   40		[self showWindow : self];
   41		return YES;
   42	}
   43	- (BOOL) validateLink : (NSURL *) anURL
   44	{
   45		NSArray *imageExtensions;
   46		NSString *extension;
   47		
   48		extension = [[[anURL path] pathExtension] lowercaseString];
   49		if(!extension) return NO;
   50		
   51		imageExtensions = [NSImage imageFileTypes];
   52		
   53		return [imageExtensions containsObject : extension];
   54	}
   55	@end

以外とあっさりしたものです。

まず、KVCからいってみましょう。
KVCとはKey Value Codingの略で、簡単にいってしまえば、名前で物事のやり取りをしようという考えに基づいたものです。
クラスとGUI部品を関連付ける (Cocoa Binding)の項でNSImageViewに表示する画像のURLがSimplePreviewerのimageURLであると指定しました。
この「imageURL」という名前でURLが取り出せるということです。
また、NSImageViewはimageURLが変更されるのを監視していて、変更が行われると勝手にimageURLを調べてそのURLの先にある画像を表示します。
-[SimplePreviewer setImageURL:]を使ってURLを変更する限り、後の処理はNSImageViewが勝手に行ってくれます。
コーディングの必要はありません。

BSImagePreviewerProtocolに準拠する

次にいよいよ本題であるBSImagePreviewerInterfaceへの準拠です。
実装する必要があるメソッドは次の5つ。

  • -[SimplePreviewer initWithPreferences:]
  • -[SimplePreviewer previewWithLink:]
  • -[SimplePreviewer showImageWithURL:]
  • -[SimplePreviewer preferences]
  • -[SimplePreviewer setPreferences:]

このうち、-[SimplePreviewer preferences]と-[SimplePreviewer setPreferences:]は初期設定のためのメソッドで、ウインドウの位置、大きさを記憶するなどの時に使用します。
ですが、このチュートリアルでは初期設定を使いませんのでこれらは空の状態です。

-[SimplePreviewer initWithPreferences:]は初めてプラグインが使われるときに呼ばれます。引数は初期設定に関するもので、今回は無視しています。
SimplePreviewerクラスはNSWindowControllerのサブクラスですから、NSWidnowControllerの指定イニシャライザである
-[NSWindowController initWithWindowNibName:]を使って初期化します。
すごく変わった初期化の仕方ですが、ウインドウnibファイルとクラス名が同じであればこのようにすることで不要なリテラルを使う必要がありません。
[self window]を呼んでいるのはこの段階でnibファイルを強制的にロードするためです。
このチュートリアルではあまり意味がありませんが、他のGUI部品を含むPreviewerを作る場合は役立つかもしれません。

-[SimplePreviewer previewWithLink:]はBathyScaphe上で何かしらのリンクをクリック、もしくはコントロールクリックしたときに呼ばれます。
渡された NSURL を調べ、もしそのURLを開くことが出来る場合はYESを、開くことが出来ない場合はNOを返します。
今回は NSImage が読み込めるであろう拡張子であるかどうか調べ、読み込めるものであればYESを、そうでなければNOを返しています。

-[SimplePreviewer showImageWithURL:]は実際にユーザーがリンクをプレビューしようとしたときに呼ばれます。
このメソッドで、ダウンロード、表示を行います。
このチュートリアルでは、[self setImageURL:]でimageURLを変更し、[self showWindow:]でウインドウを表示するだけで、その後の処理はNSImageView任せです。
本来は、成功した場合はYESを、失敗した場合はNOを返すことになっています。

使ってみよう

すべての準備は整いました。後はビルドし使うだけです。

Xcodeの「ビルド」ボタンを押します。
ステータスバーに「問題なく完了しました」と出ればOKです。
もし、エラーが出た場合は、もう一度ソースコードを確認してください。
Build-00.png

出来上がった、ImagePreviewer.pluginを$HOME/Libary/Application Support/BathyScaphe/PlugIns/フォルダに入れてBathyScapheを再起動してみましょう。
Build-01.png

Complete-t.png

初期設定を使う。

初期設定を使ってみましょう。
それには

  • -[SimplePreviewer preferences]
  • -[SimplePreviewer setPreferences:]

および

  • -[SimplePreviewer initWithPreferences:]

を使います。

SimplePreviewerにウインドウの位置を保存する機能をつけてみましょう。

ヘッダファイルの変更

ではまず、ヘッダファイル。
SimplePreviewerクラスに、AppDefaultsクラスを持つようにします。

    1	/* SimplePreviewer */
    2	
    3	#import <Cocoa/Cocoa.h>
    4	#import "BSImagePreviewerInterface.h"
    5	
    6	@interface SimplePreviewer : NSWindowController <BSImagePreviewerProtocol>
    7	{
    8		NSURL *imageURL;
    9		AppDefaults *preferences;
   10	}
   11	@end

インプリメントファイルの変更

インプリメントファイルを変更しましょう。
最初にAppDefaultsの保存と取り出しのメソッドを作成します。
これも先述のKVCです。

- (AppDefaults *) preferences
{
	return preferences;
}
- (void) setPreferences : (AppDefaults *) aPreferences
{
	id temp = preferences;
	preferences = [aPreferences retain];
	[temp release];
}

imageURLと同じですね。
保持しているのですから、deallocできちんと解放しておきましょう。

- (void) dealloc
{
	[imageURL release];
	[preferences release];
	[super dealloc];
}

初期設定を保存する前にその方法を見てみましょう。
AppDefaultsは-[AppDefaults imagePreviewerPrefsDict]に答えて、NSMutableDictionaryを返します。
この辞書に書き込んだ設定は、BathyScapheに処理され保存されます。

note
気をつけなくてはいけないのは、この辞書が以前他のプラグインに使用されている可能性があることです。もし一意でないキーを使用していた場合、他のプラグインが設定した値を取り出してしまう可能性があります。それを避けるため、BathyScaphe Plugin Developer Guideでは、CFBundleIdentifierの値を接頭辞に使うことを推奨しています。

これをそのまま使ってもよいのですが、後々機能の拡張をする時のことを考え簡単に設定、取得が出来るメソッドを作成しておきましょう。

- (void) setPreference : (id) pref forKey : (id) key
{
	[[[self preferences] imagePreviewerPrefsDict] setObject : pref forKey : key];
}
- (id) preferenceForKey : (id) key
{
	return [[[self preferences] imagePreviewerPrefsDict] objectForKey : key];
}

こうしておくことで、無駄に長いメッセージ式を書く必要が無くなります。

では、本題のウインドウの位置と大きさの保存と復元を実装しましょう。
これも少し回りくどいですが、保存と復元のメソッドを実装します。

static NSString *SimplePreviewerSavedWindowFrame = @"com.masakih.SimplePreviewerSavedWindowFrame";
- (void) saveWindowFrame
{
	[self setPreference : [[self window] stringWithSavedFrame]
				 forKey : SimplePreviewerSavedWindowFrame];
}
- (void) setWindowFrameFromPreference
{
	id windowFrameString = [self preferenceForKey : SimplePreviewerSavedWindowFrame];
	
	if( windowFrameString ) {
		[[self window] setFrameFromString : windowFrameString];
	}
}

特筆すべき点はありません。
キー「SimplePreviewerSavedWindowFrame」を使いウインドウの位置と大きさを保存、復元をするだけです。
-[SimplePreviewer setWindowFrameFromPreference]内で、キーの内容が空かそうでないかを調べているのに注意してください。
当然ですが、初めて使用されるときにはキーの内容は空です。

つぎに実際に保存と復元をするのですが、
その前に、「いつ保存するのか?」「いつ復元するのか?」を考えましょう。
「いつ保存するのか?」これには3つの答えがあります。
1つはBathyScapheが終了するとき、2つ目はウインドウが閉じられるとき、3つ目はウインドウがリサイズまたは移動されたときです。
今回は、最もとらえやすい2つ目の場合にしましょう。
まずInterface BuilderでSimplePreviewer.nibを開いて、windowのDelegateをFile's Ownerに設定します。
Extra-Section-00.png
これで、Windowの各種NotificationがSimplePleviewerに通知されます。
ここで必要なのは、ウインドウが閉じられそうであるいうNotificationです。

- (void) windowWillClose : (NSNotification *) notification
{
	[self saveWindowFrame];
}

これで、保存されます。単純明快ですね。

次に、「いつ復元するのか?」です。
これは初期化されるときではなくnibが読み込まれたあとです。
初期化時にはバンドルはロードされていますがnibはまだロードされていません。
NSWindowControllerのnibがロードされるのは-[NSWindowController window]もしくは-[NSWindowController showWindow:]が呼ばれた時です。
ただし、それをオーバーライドする必要はなく、nibの読み込み後に、nibに含まれる各インスタンスおよびFile's Ownerに対し -[NSObject awakeFromNib]が呼ばれることになっています。
このメソッドを使いましょう。

- (void) awakeFromNib
{
	[self setWindowFrameFromPreference];
}

これで、ウインドウの位置と大きさを復元できました。

少し発展した情報

ローダブルバンドル(プラグイン)の作成では、それ自身がアプリケーションではないため、デバッグするためにはロードされなくてはなりません。
ここでは、ビルド時に自動で所定の場所にプラグインをインストールする方法と、母艦となるアプリケーションを起動して、デバッグする方法を紹介します。

自動インストール

Xcodeでは、ビルド時に自動的に所定の場所にプロダクトをインストールする方法が提供されています。
「""ターゲット "SimplePreviewer""の情報」ウインドウを開いて「ビルド」タブを選択してください。
Install.png
「コレクション」を「デプロイメント」にします。
まず、「インストール・ビルド・プロダクトの場所」を 「/」にします。
次に、「インストールディレクトリ」を「$HOME/Library/Application Support/BathyScaphe/PlugIns」にします。
さらに、「デプロイメント位置」をチェックします。
これでビルドするたびにプロダクトが指定位置に作成されます。

ローダブルバンドル(プラグイン)のデバッグ

ローダブルバンドル(プラグイン)のデバッグをXcode上で行うためには、「実行可能ファイル」を作成する必要があります。
「プロジェクト」メニューから「新規カスタム実行可能ファイル...」を選択します。
Executable.png
「実行可能ファイル名」に名前を(ファイル名とありますが別にどんな名前でも可能です。)。
「実行可能ファイルのパス」でアプリケーションを選択します。
これで、母艦であるアプリケーションを使ってのデバッグが出来ます。
もちろんXcodeで設定したブレイクポイント等も有効です。

注意すべき点は、デバッグ前に母艦であるアプリケーションが終了していることを確認することです。
Xcodeでの(GDBを使った)デバッグでは、対象アプリケーションを新たなプロセスとして起動しますので、母艦であるアプリケーションが既に起動していてももう一つ別のプロセスが起動されてしまいます。
同じアプリケーションが2つ起動すると予期せぬエラーが発生する場合がありますので注意してください。

Attach(接続)
Xcode 2.2 で、GDBの機能である、起動中のアプリケーションをデバッグ対象にする「接続」機能が追加されました。しかし、プラグインは一度読み込まれてしまうと、それが変更されても再読み込みはされません。プラグインのデバッグでは使わないようにしましょう。

よくある質問と答え

Q, BathyScaphe Plugin Developer Guideにはdeallocは呼ばれないと書かれていますが?

A, はい、呼ばれません。チュートリアルで書かれているものは単なる「おまじない」だと思ってください。

Q, では本当にどうしても終了時にやらなければならない処理がある場合はどうすればよいですか?

A, BathyScaphe Plugin Developer Guideに書かれている通り、NSApplicationWillTerminateNotificationをとらえて処理してください。

Q, masakihの公開しているプラグインではAppDefaultsをretainしてません。大丈夫なんですか?

A, ただの手抜きですが、現時点では大丈夫です。
現時点ではそれは不変なオブジェクトなのです。

Q, -[NSBundle pathForResource:ofType:] がうまく動きません。

A, +[NSBundle mainBundle] でバンドルを取得してませんか?
+[NSBundle mainBundle]は文字通りメインのバンドル、つまりBathyScapheのバンドルを返します。
プラグインのバンドルを取得するためには、+[NSBundle bundleForClass:] を用いる必要があります。
チュートリアルの場合ですと

[NSBundle bundleForClass : [SimplePreviewer class]]

とすればプラグインのバンドルが取得できます。

Q, NSLocalizedString がちゃんと動きません。

A, -[NSBundle pathForResource:ofType:] がうまく動きません。の亜種ですね。
NSLocalizedString はマクロで定義されていて、その中で+[NSBundle mainBundle]を呼んでいます。
代わりに NSLocalizedStringFromTableInBundle を使います。
例えばこうです。

NSLocalizedStringFromTableInBundle( @"Hide TabPreviewer",@"Localizable",[NSBundle bundleForClass:[self class]], @"Hide TabPreviewer" )~

また、このようなマクロを定義しておくのも良いでしょう。

#define SPLocalizedString( str, comment ) 
NSLocalizedStringFromTableInBundle( (str), @"Localizable", [NSBundle bundleForClass:[SimplePreviewer class]], (comment) )

こうしておけば、genstringsコマンドも使用可能です。(実際は1行に書くか、改行前にバックスラッシュを挿入してください。)

Q, 私が聞きたい質問がここにありません。どうすればよいですか?

A, ここにQだけ書いておけば誰かが答えてくれるかもしれません。

カテゴリ: Development

Last Modified: