EnclosureViewを作る【実践的Macintoshプログラミング解説】

印刷用表示 |テキストサイズ 小 |中 |大 |

CoreData版 Repeating Motif Generator の開発 Repeating Motif Wonderland CoreData 実践的 Macintosh プログラミング解説

LinkIconホーム

更新日 2009-05-24

ウィンドウがリサイズされた時に、OscillatorViewとLissajousViewを連動させてリサイズする

EnclosureViewを作る

enclosureViewDidMade.png ウィンドウがリサイズされた時にビューの位置・サイズをどのように連動させるかについてはautoResizingMaskというものがサポートされています。

 ビューの幅・高さを固定にするのか可変にするのか、上下左右の四辺をスーパービューの各四辺に対して連動させるのかさせないのかを指定するもので、Interface Builderで視覚的に(アニメーション付きで)設定する事ができます。

 単一のビューの動きだけを考えれば良い状況であればこれで十分なのですが、三つのビューを連動させたい時はうまくいかないので、プログラミングが必要になります。

 複数のビューの位置とサイズを調整する役割をどのオブジェクトに持たせるか?旧バージョンではコントローラに持たせていたのですが、ビューに持たせた方がスッキリするような気がするので、今回はカスタムビューを用意する事にします。

ウィンドウを拡げるとおかしな事に…

 ウィンドウのサイズを大きくすると下の図の様になってしまうのですが、無駄な余白が上側と右側にできてしまって何とも間が抜けた状態です。OscillatorViewが一つだけ存在して、かつ形状が正方形でなくてもよいならば、autoresizingMaskをInterface Builderで設定するだけで何とかなるのですが、OscillatorViewが二つ、LissajousViewが一つあって互いの位置関係を保ちながら拡大・縮小したいとなると話はそう単純ではありません。
 OscillatorViewとLissajousViewがそれぞれの位置関係を保ちながらちょうど良い大きさにする機能を持たせたビューEnclosureViewを作って、この問題を解決します。

3rdAppWideWindow.png

新規ファイルでNSViewのサブクラスを作る

 Xcodeのファイルメニューから「新規ファイル…」を実行します。CocoaグループのObjective-C NSView subclassを選択して「次へ」をクリックします。

newNSViewSubclass31.png

 EnclosureViewと入力して完了です。

enclosureView.png

アウトレットとインスタンス変数を追加する

アウトレットを追加する

 OscillatorViewとLissajousViewのサイズを変更しなくてはいけないのですから、それらにアクセスできないといけません。簡単なのはアウトレットを追加する事です。そこでアウトレットを三つ追加します。

 OscillatorViewやLissajousView固有の機能を使うわけではありませんから、NSViewとしてアウトレットを作成しておきます。これなら前方参照やインターフェイスファイルをインポートする必要はありません。

インスタンス変数を追加する

 周囲の余白(whiteSpace)とOscillatorView - LissajousView間の距離(viewMargin)を設定するインスタンス変数を追加します。

//
//  EnclosureView.h
//  RepeatingMotifGenerator
//
//  Copyright NovemberKou 2008. All rights reserved.
//

#import <Cocoa/Cocoa.h>


@interface EnclosureView : NSView
{
    IBOutlet    NSView  *xView, *yView, *lissajousView;

    int     whiteSpace,viewMargin;
}

@end

初期化

インスタンス変数の初期化と通知の設定

 初期化コードで実行する事は、インスタンス変数の初期化と通知を受け取る様に設定する事です。自分がリサイズされた事を知りたいわけですから、NSViewFrameDidChangeNotificationを受ける様にします。objectとしてselfを指定しているので、自分のサイズが変わった通知だけを受け取る事になります。

 通知を受ける登録をしたら、解除の事も考えないといけません。登録解除はdeallocメソッドで行ないます。

起動時の処理

 リサイズされて初めて通知を受け取るわけなので、起動時は通知を受け取れません。したがって起動時は、OscillatorViewとLissajousViewのサイズはInterface Builderで設定されたサイズのままです。Interface Builderできっちり設定しておけばいいのですが、それも面倒なので起動時にもサイズ調整する様にしておきます。
 initメソッドが呼ばれる時点ではアウトレットが接続されている保証はないので、awakeFromNibでサイズ調整します。

//
//  EnclosureView.m
//  RepeatingMotifGenerator
//
//  Copyright NovemberKou 2008. All rights reserved.
//

#import "EnclosureView.h"


@interface EnclosureView(Private)
- (void)frameDidChange:(NSNotification *)aNotification;
@end

@implementation EnclosureView

#pragma mark -
#pragma mark Initializer and Deallocator

- (id)initWithFrame:(NSRect)frame
{
    NSNotificationCenter    *nc = [NSNotificationCenter defaultCenter];

    self = [super initWithFrame:frame];
    if(self)
    {
        whiteSpace = 4;
        viewMargin = 8;
        [nc addObserver:self
               selector:@selector(frameDidChange:)
                   name:NSViewFrameDidChangeNotification
                 object:self];
    }
    return self;
}

- (void)dealloc
{
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    [super dealloc];
}

- (void)awakeFromNib
{
    [self frameDidChange:nil];
}

@end

自分がリサイズされた時の処理

 自分がリサイズされたらframeDidChange:が呼ばれます。ここではサイズの計算をして三つのサブビューを再配置しています。

 まずサブビューのサイズを計算します。widthはサブビューに許される最大幅です。これはEnclosureViewの幅から両脇の余白(whiteSpace*2)を引き、サブビュー間の距離(viewMargin)を引いて2で割ればでます。2で割っているのは横方向にサブビューが2個(Y軸のOscillatorViewとLissajousView)あるからです。

 同じ様にサブビューに許される最大高さheightを計算します。widthとheightのどちらか小さい方がサブビューのサイズになります。

 サブビューのサイズがでたら、サブビューがセンタリングされる様に左側と下側のマージンを計算します。

 計算結果に基づいて各サブビューの原点とサイズを決め、配置して終了です。

//
//  EnclosureView.m
//

@implementation EnclosureView(Private)

#pragma mark -
#pragma mark Event Handler

- (void)frameDidChange:(NSNotification *)aNotification
{
    int         width,height,viewSize,marginWidth,marginHeight;
    NSSize      sizeOfView;
    NSPoint     originViewX,originViewY,originViewLissajous;    

    width = ([self bounds].size.width - whiteSpace*2 - viewMargin)/2;
    height = ([self bounds].size.height - whiteSpace*2 - viewMargin)/2;
    viewSize = width < height ? width : height;
    marginWidth = ([self bounds].size.width - viewSize*2 - viewMargin)/2;
    marginHeight = ([self bounds].size.height - viewSize*2 - viewMargin)/2;

    originViewY.x           = marginWidth;
    originViewY.y           = marginHeight;
    originViewX.x           = originViewY.x + viewSize + viewMargin;
    originViewX.y           = originViewY.y + viewSize + viewMargin;
    originViewLissajous.x   = originViewX.x;
    originViewLissajous.y   = originViewY.y;
    [xView setFrameOrigin:originViewX];
    [yView setFrameOrigin:originViewY];
    [lissajousView setFrameOrigin:originViewLissajous];

    sizeOfView = NSMakeSize(viewSize,viewSize);
    [xView setFrameSize:sizeOfView];
    [yView setFrameSize:sizeOfView];
    [lissajousView setFrameSize:sizeOfView];
}

@end

【コラム】カテゴリについて


 ソースコードを注意深く見ていた方は気づいたと思いますが、frameDidChange:メソッドはPrivateという名前のカテゴリの中に入っています。これはインターフェイス部に宣言されていない局所的なメソッドが、その定義より前のコードから使えない(警告が出る)問題を解決するための手段としてカテゴリを使っている例です。

 Cのプロトタイプ宣言と同じ効果があると言った方がわかりやすいかもしれません。

 awakeFromNibより前にframeDidChange:を移動させれば済む話なのですが、初期化関連のメソッドよりも前にこのようなごちゃごちゃしたメソッドが来るのはどうにも気持ちが悪いので、カテゴリを使いました。