Cocoaはやっぱり!
スクリーンセーバーを作ろう
実はNSView
今回のテーマ

MacOS Xで、ようやくスクリーンセーバーが標準装備されました。そして、スクリーンセーバーモジュールを作るためのフレームワークも用意されています。スクリーンセーバーのフレームワークは、Cocoaのアプリを作るためのFoundationやApplication Kitのフレームワークを使っていますので、Cocoaのアプリを作るのと似た作法で作ることが出来ます。

改版履歴

プロジェクトの作成

プロジェクトを作成する前に、まず、以下のファイルをダウンロードしてください。

13KB

これは、スクリーンセーバー開発用のプロジェクトファイルの雛形です。ダウンロードして解凍すると、「 ScreenSaver.dmg 」というディスクイメージファイルが現れます。これをダブルクリックするとDiskCopyが起動して「ScreenSaver」というドライブがデスクトップに表示されます。「 ScreenSaver 」フォルダーを「 / Developer / ProjectBuilder Extras / Project Templates / Bundle / 」の下に置いてください。

終わりましたら、Project Builderで「 File > New Project... 」メニューを実行します。最初のAssistantの画面では「 Bundle > ScreenSaver 」を選びます。これが先程ダウンロードした雛型です。後は今までどおりに、名前をつけてください。こんな感じで、よくつかうプロジェクトファイル一式を先ほどのフォルダーに置いておくと便利です。

プロジェクトのウィンドウが表示されたら、もうこの段階でビルドすると簡単なスクリーンセーバーが出来あがるようになっています。すぐにでも試したい方は、ビルドして、出来あがった「xxx.saver」というファイルを、自分のホームフォルダーの下の「 Library / Screen Savers / 」に入れてシステム環境設定を起動してください。スクリーンセーバーを開いて、作ったモジュールを選択して、設定ボタンを押すとこんな画面になるはずです。テストボタンを押せば実際に動かして見れます。

ヘッダーの概要

では、ソースを見ていきましょう。ソースは「 MySaver.h 」と「 MySaver.m 」の2本です。大きなものを作らない限りは、この2本で十分でしょう。まずはヘッダーファイルです。必要に応じてここに色々と追加していくことになります。

MySaver.h
#import <AppKit/AppKit.h> #import <ScreenSaver/ScreenSaver.h> @interface MySaver : ScreenSaverView { IBOutlet id configSheet; // 設定用シートウィンドウ } - (IBAction) changeSheet : (id) sender; // 設定が編集されら呼ばれる - (IBAction) closeSheet : (id) sender; // シートが閉じたら呼ばれる @end

このヘッダーを見て分かるように、「 ScreenSaverView 」というクラスがあって、そのサブクラスを作ることがスクリーンセーバーを作ることを意味します。さらに、ScreenSaverViewというのは、実は「 NSView 」のサブクラスになっていまして、つまりは今までNSViewについて学んだ知識(描画など)はほとんどそのまま通用します。Outletが1つと、Actionが2つ定義されていますが、概要はコメントで書いてあるとおりです。詳細は後で説明します。

ソースの概要

次はソースファイル。

MySaver.m
#import "MySaver.h" @implementation MySaver // スクリーンセーバー本体 @end

雛形にはスクリーンセーバー本体のコードがすでに書きこまれていますが、実はなんにも書かなくても動作します。といっても真っ暗になるだけなんですけどね。このように、ほとんど何も書かなくても一応動作するものがScreenSaverViewとして提供されているわけです。今回用意したプロジェクトの雛形は、必ず書くであろうメソッドについては実装してあります。

動画を描く

描画を行うメソッドは「animateOneFrame」です。animateOneFrameはとにかくディレイ無しで繰り返し呼ばれます。

MySaver.m > animateOneFrame
- (void) animateOneFrame { // 描画 }

呼ばれる間隔を変更したい場合は、「animationTimeInterval」を実装して、秒数を返すようにします。そうすると、この秒数の間隔で呼ばれます。NSTimeIntervalというのは実数なので、0.1を返せば0.1秒間隔で呼ばれるようになります。

MySaver.m > animationTimeInterval
- (NSTimeInterval) animationTimeInterval { return( 秒数 ); }

グローバル変数の値は保持されますので、アニメーションの前回の状態を覚えておくのに特別な方法は必要ありません。単純にグローバル変数を使えばOK。

静止画を描く

もし静止画を描くだけのスクリーンセーバーでよければ、animateOneFrameを実装せず「 drawRect : 」を実装すればOKです。つまり、NSViewと同じってことですね。

MySaver.m > animationTimeInterval
- (void) drawRect : (NSRect) rect { // 描画 }

ただ、drawRectを実装すると、スクリーンセーバーが起動する前に画面全体を白の縞模様で塗りつぶしてくれますので、その上に描画を行うことになります。ちなみに、drawRectとanimateOnFrameの両方を実装するとどうなるかですが、最初にdrawRectが呼ばれて、その後animateOneFrameが繰り返し呼ばれます。あまりこういう使い方はしないかもしれませんけど..。

初期化と後処理

描画前に初期化が必要になることもありますが、これもNSViewと同じく「 initWithFrame : 」を実装すれば、これを呼んでくれます。

MySaver.m > initWithFrame :
- (id) initWithFrame : (NSRect) frame { [ super initWithFrame : frame ]; // スーパークラスを初期化 // 初期化 ( 初期設定を読みこんだりする ) return( self ); }

初期化メソッドにはもう1つ「 initWithFrame : isPreview : 」というのがあります。システム環境設定の中で小さい画面でプレビューを行うことが出来ますが、こちらのメソッドを使うと、スクリーンセーバーがプレビューとして起動されたのか、本番として起動されたのかを見分けることができるわけです。引き数の「 isPreview 」がYESならプレビューということです。

MySaver.m > initWithFrame : isPreview :
- (id) initWithFrame : (NSRect) frame isPreview : (BOOL) isPreview { [ super initWithFrame : frame isPreview : isPreview ]; // 初期化 ( 初期設定を読みこんだりする ) return( self ); }

特に理由が無ければ、こちらで作っておく方がいいでしょう。プレビューかを見分ける必要が生じてからソースを書き換えるのもちょっと無駄な作業になりますから。初期化メソッドの中でメモリを確保したりした場合には、終了処理が必要になりますが、その場合も今までどおり「release」や「dealloc」を実装します。描画のときに現在がプレビューかどうかを見たいことがあると思いますが、isPreviewを呼ぶだけで分かります。

ScreenSaverView : 今プレビューかどうか
書式 : - (BOOL) isPreview 出力 : プレビューならYES、本番ならNO

アニメーションに対しての初期化と終了の処理が必要な場合は、「 startAnimation 」と「 stopAnimation 」を実装してください。startAnimationは最初のanimateOnFrameの前に呼ばれ、ユーザーがマウスを動かしたりするとstopAnimationが呼ばれます。

MySaver.m
- (void) startAnimation { // アニメーション用の初期化 } - (void) stopAnimation { // アニメーション用の後処理 }

流れをまとめますと、こうなります。

initWithFrame: isPreview: drawRect: startAnimation animateOnFrame : 繰り返し : stopAnimation release dealloc

ここまで分かれば、とりあえずanimateOneFrameに色々書いてスクリーンセーバーが作れると思います。

スクリーンセーバーの設定

スクリーンセーバーはシステム環境設定でモジュール単位の設定を行うことが出来ます。今回は、その設定画面を作りましょう。その設定画面を持ちたい場合は、まず設定画面を持っていることを意思表示します。そのためには「 hasConfigureSheet 」というメソッドを実装してYESを返すようにすればOK。実装しないのと実装してNOを返すのは同じ意味になります。ここでYESを返すとシステム環境設定で設定ボタンが使えるようになります。

MySaver.m > hasConfigureSheet
- (BOOL) hasConfigureSheet { return YES; }

雛形ではすでにYESを返すようにしています。設定が無くてもアバウト画面くらいは付けたいので、これでいいでしょう。で、その設定ボタンが押されると「 configureSheet 」というメソッドが呼ばれます。このメソッドは設定用に使用するシートのウィンドウを返り値とするメソッドです。このメソッドの中でシートのオブジェクトを作成して初期化して返します。そうすると、このメソッドの呼出元になるシステム環境設定がシートを表示してくれます。

MySaver.m > configureSheet
- (NSWindow*) configureSheet { [ NSBundle loadNibNamed : @"MySaver" owner : self ]; // シートウィンドウを初期化 return( configSheet ); }

NSBundle loadNibNamed : owner : 」は、Interface Builderで作ったnibファイル ( NeXT Interface Builder ) を読み込むものです。雛形には「 MySaver.nib 」がありますが、これを読みこんでいます。中身はInterface Builderで確認すると、こんな感じです。

「 loadNibName : owner : 」の2番目の引き数のownerですが、これは、実はInterface Builder上のMySaver.nibのウィンドウにある「Files's Owner」と結びつくものです。File's Ownerとコネクションを張ることは、「 loadNibName : owner : 」の引き数のownerとコネクションを張ることなのです。selfを引き数として指定しているので、MySaverクラスとウィンドウ上のViewとやり取りができるようになるわけですね。

MySaverのOutletであるconfigSheetは、この仕組みを使ってnibのWindowと繋がっています。nibを読みこむとOutletであるconfigSheetに実際にWindowが繋がりますので、これを返り値として返すわけです。続いて、シートを閉じる方です。MySaver.nibのWindowのOKボタンは、File's OwnerのActionの「 closeSheet 」に結びついています。「 NSApp endSheet : configSheet 」でシートを実際に閉じます。

MySaver.m > closeSheet
- (void) closeSheet : (id) sender { // 設定を保存 [ NSApp endSheet : configSheet ]; }

シートを開くのと閉じるのはこれでできるようになりましたので、アバウト画面は作れるようになりました。今度は、設定値を1つ設けて読み書きしてみましょう。

上のようにスライダーを1つ付けます。これで、animationIntervalメソッドで返す値を変えてアニメーションのスピードを変えられるようにします。このスライダーのActionとしてFiles's OwnerのchangeSheetを呼びます。また、スライダーをFile's Ownerから参照するために、MySaverにはintervalというOutletを追加して、スライダーを繋ぎます。これでスライダーの値を参照したり出来ます。ちなみに、スライダーの持てる値はInerface Builder上で0〜5にしました。

では、ソースを書き換えていきます。まず、gTiというグローバル変数を作って、そこに設定値を覚えることにします。初期設定値を読み込みは、初期化メソッドで行います。

MySaver.m > initWithFrame: isPreview:
NSTimeInterval gTi = 1; - (id) initWithFrame : (NSRect) frame isPreview : (BOOL) isPreview { [ super initWithFrame : frame isPreview : isPreview ]; { ///// 初期設定を読みこんだりする ///// ScreenSaverDefaults *ssd = [ ScreenSaverDefaults defaultsForModuleWithName : @"MySaver" ]; if ( ssd ) gTi = [ ssd floatForKey : @"interval" ]; } return( self ); }

スクリーンセーバーモジュールの初期設定値を扱う「 ScreenSaverDefaults 」というクラスが用意されていますのでこれを使います。ScreenSaverDefaultsというのは、「 NSUserDefaults 」のサブクラスになっています。NSUserDefaultsを直接使ってしまうと、システム環境設定の設定値を変えてしまうことになりますので使えないわけですね。

初期設定をファイルから読み込むのが「 defaultsForModuleWithName 」です。引き数には、このスクリーンセーバーモジュールの名前を渡します。この文字列が他のスクリーンセーバーとぶつかると設定値の取り合いになりますので、長めのものがいいでしょう。

この初期設定のクラスは辞書になっていますので、以前やったようにキーを指定して対応する値を取り出します。「 floatForKey 」で実数値を取り出してます。保存は、シートが閉じられる時に行います。読み出してきて、setFloat : で値をセットして、synchronizeでファイルへ書き込みます。

MySaver.m > closeSheet
- (void) closeSheet : (id) sender { { ///// 設定を保存 ///// ScreenSaverDefaults *ssd = [ ScreenSaverDefaults defaultsForModuleWithName : @"MySaver" ]; [ ssd setFloat : gTi forKey : @"interval" ]; [ ssd synchronize ]; } [ NSApp endSheet : configSheet ]; }

それから、シートを表示する場合は、スライダーを設定値まで動かしておく必要があります。

MySaver.m > configureSheet
- (NSWindow*) configureSheet { [ NSBundle loadNibNamed : @"MySaver" owner : self ]; gTi = [ interval floatValue ]; // シートを初期化 return( configSheet ); }

最後に、シート上からスライダーを動かされた時に、スライダーの値をグローバル変数に記憶させます。

.m
- (IBAction) changeSheet : (id) sender { gTi = [ interval floatValue ]; }

これで完成です。後、いくつか細かい情報を。

MySaver.m > performGammaFade
+ (BOOL) performGammaFade { return NO; }

ScreenSaverViewにperformGammaFadeというメソッドを実装してNOを返すようにすると、スクリーンセーバー起動時に一旦真っ黒にフェードアウトするのを止めることが出来ます。

ScreenSaver.hにはちょっとした便利な関数が用意されています。「/ System / Library / Frameworks / ScreenSaver.framework / 」にフレームワークがありますので見てみましょう。

aとbの間の整数の乱数を生成 int SSRandomIntBetween( int a, int b ) aとbの間の実数の乱数を生成 float SSRandomFloatBetween( float a, float b ) outerRectの中央に来るようにinnerRectを配置したときの矩形を取得 NSRect SSCenteredRectInRect( NSRect innerRect, NSRect outerRect )