バインド先を切替える【実践的Macintoshプログラミング解説】

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

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

LinkIconホーム

更新日 2009-05-24

発振器を選択できる様にして、発振器のパラメータを設定するビューを一組に減らす

バインド先を切替える

 発振器のパラメータを表示するビューを一組に減らして、そのバインド先を選択に応じて切替えます。

 旧バージョンではモデルオブジェクトに選択された発振器というプロパティを持たせ、それとビューをバインドしていました。これだとモデルごとに選択された発振器が異なるので、違うモデルを選択すると発振器の選択も切り替わる事になります。ユーザにはわかりにくかったかもしれません。

 今回は発振器の選択状態をモデルオブジェクトに持たせるのではなく、EnclosureViewに持たせる事にします。EnclosureViewは発振器のアウトレットを持っているので適任です。

EnclosureViewに選択状態を持たせる

 EnclosureViewに選択されているOscillatorViewを表すインスタンス変数と、それを切替えるメソッドを追加します。

 dirtyフラグは追加したものの不要になってしまいました。とりあえずコメントアウトしておきます。

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

#import <Cocoa/Cocoa.h>


@class  OscillatorView;

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

    int     whiteSpace,viewMargin;
    //BOOL    dirty;

    OscillatorView  *selectedView;
}

- (void)selectOscillator:(OscillatorView *)sender;

@end

 selectedViewをxViewに初期化します。xViewはアウトレットなので、これが有効になるawakeFromNibで初期化します。

 selectOscillatorメソッドを実装します。このメソッドはOscillatorViewが自らを引数にして呼び出す事を想定しています。
 選択状態が変わらない場合は何もせずにリターンします。
 変わる場合は今まで選択されていたビューを非選択状態にして、引数のビューを選択状態にし、インスタンス変数を更新します。

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

#import "EnclosureView.h"

#import "OscillatorView.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;

        //dirty = NO;
        [nc addObserver:self
               selector:@selector(frameDidChange:)
                   name:NSViewFrameDidChangeNotification
                 object:self];
    }
    return self;
}

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

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

#pragma mark -
#pragma mark Selection

- (void)selectOscillator:(OscillatorView *)sender
{
    if(sender == selectedView)  return;

    [selectedView setSelected:NO];
    [sender setSelected:YES];
    selectedView = sender;
}

@end

@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];
}

- (void)drawRect:(NSRect)rect
{
}

@end

マウスクリックで選択を切替える

 OscillatorViewに追加するのはmouseUp:メソッドです。 OscillatorViewがクリックされてマウスボタンが放されたらこのメソッドが呼ばれるので、EnclosureViewにselectOscillatorメッセージを送ります。引数は自分自身です。

 これでクリックされて選択された発振器のOscillatorViewが強調表示される様になりました。あとは発振器のパラメータを設定するビューのバインド先を切替えれば完成です。

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

#import "OscillatorView.h"

#import "EnclosureView.h"


#define     AMPLITUDE_MAX   100

@implementation OscillatorView

#pragma mark -
#pragma mark Methods to be overrided

- (NSString *)keyPath
{
    NSString    *viewName;

    viewName = [[self toolTip] substringWithRange:NSMakeRange(0,1)];    
    isXView = [viewName isEqualToString:@"x"];
    selected = isXView;

    return  [NSString stringWithFormat:@"selection.%@.bezierPath",viewName];
}

- (float)pathHeight
{
    return  AMPLITUDE_MAX*2.0;
}

#pragma mark -
#pragma mark Accessor methods

- (BOOL)selected    {return selected;}
- (void)setSelected:(BOOL)isSelected
{
    if(selected != isSelected)
    {
        selected = isSelected;
        [self setNeedsDisplay:YES];
    }
}

#pragma mark -
#pragma mark Event handler

- (NSRect)visibleRect
{
    return  NSInsetRect([self bounds],-2,-2);
}

- (void)mouseUp:(NSEvent *)theEvent
{
    if(!selected)
        [(EnclosureView *)[self superview] selectOscillator:self];
}

- (void)drawRect:(NSRect)rect
{
    NSRect          bounds,frame,savedFrame;
    NSBezierPath    *borderPath;

    [super drawRect:rect];

    if(![self inLiveResize])
    {
        savedFrame = [self frame];
        frame = NSInsetRect(savedFrame,-3,-3);
        [self setFrame:frame];

        bounds = [self bounds];
        bounds = NSInsetRect(bounds,1,1);
        borderPath = [NSBezierPath bezierPathWithRect:bounds];
        [borderPath setLineWidth:2];
        if(selected)
            [[NSColor blackColor] set];
        else
            [[NSColor windowBackgroundColor] set];
        [borderPath stroke];

        [self setFrame:savedFrame];
    }
}

@end

バインド先を切替える

3rdWindowDesign2.png

 最後に発振器のパラメータを入力するビューのバインド先を切替えます。ビューはこれを機会にレイアウトを見直し、スライダーを復活させました。またステッパを追加しました。

 レイアウトを右図に示します。

 バインド先を切替えるためにEnclosureViewにNSArrayControllerのアウトレットを追加します。そしてバインド先を切り替えるビューのアウトレットも追加します。

 ついでなのでxViewとyViewアウトレットのクラス指定をNSViewからOscillatorViewに変更しました。これはコンパイル時の警告を消すためです。

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

#import <Cocoa/Cocoa.h>


@class  OscillatorView;

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

    IBOutlet    NSArrayController   *controller;
    IBOutlet    NSTextField         *frequencyTextField, *phaseLagTextField;
    IBOutlet    NSSlider            *frequencySlider, *phaseLagSlider;
    IBOutlet    NSStepper           *phaseLagStepper;

    int     whiteSpace,viewMargin;
    //BOOL    dirty;

    OscillatorView  *selectedView;
}

- (void)selectOscillator:(OscillatorView *)sender;

@end

バインディングの解除と再設定

 selectOscillator:メソッドにバインド先を切替える処理を追加しました。各ビューに似たような処理をするのでサブルーチン化しています。

 ビューにunbind:メッセージを送って現在のバインディングを解除してから新たなバインディングを設定しています。unbind:メッセージを送らなくても動作はするのですが、メモリリークが心配なのでunbindしてみました。

 unbindしないとメモリリークするのかどうかを確かめようとしてXocdeの実行メニューからパフォーマンスツールを使って開始 - Leaksを実行してみましたがInstrumentsがフリーズしてしまいます。Instrumentsを単体で起動するとフリーズしないのですが…。

 Instrumentsを単体起動して、unbind有と無でそれぞれLeaksを確認してみましたが、どちらもメモリリークは検出されませんでした。何度バインドし直しても大丈夫だし、unbindする必要もなさそうです。

//
//  EnclosureView.m
//

#pragma mark -
#pragma mark Selection

- (NSString *)frequencyKeyPath
{
    if(selectedView == xView)
        return  @"selection.x.frequency";
    else
        return  @"selection.y.frequency";
}

- (NSString *)phaseLagKeyPath
{
    if(selectedView == xView)
        return  @"selection.x.phaseLag";
    else
        return  @"selection.y.phaseLag";
}

- (void)bind:(id)object withKeyPath:(NSString *)keyPath
{
    [object unbind:@"value"];
    [object bind:@"value" toObject:controller withKeyPath:keyPath options:nil];
}

- (void)selectOscillator:(OscillatorView *)sender
{
    if(sender == selectedView)  return;

    [selectedView setSelected:NO];
    [sender setSelected:YES];
    selectedView = sender;

    [self bind:frequencyTextField   withKeyPath:[self frequencyKeyPath]];
    [self bind:frequencySlider      withKeyPath:[self frequencyKeyPath]];
    [self bind:phaseLagTextField    withKeyPath:[self phaseLagKeyPath]];
    [self bind:phaseLagSlider       withKeyPath:[self phaseLagKeyPath]];
    [self bind:phaseLagStepper      withKeyPath:[self phaseLagKeyPath]];
}