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

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

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

LinkIconホーム

更新日 2009-05-24

ビューを原図とミキサーに対応させる

EnclosureViewを拡張する

synthesizeMotif.png EnclosureViewはリサイズされた時にサブビューの大きさと位置を調整するのが役割ですが、第三のプロジェクトと比較してサブビューの数が右の図の様に一気に増えました。

 したがってこれに対応するため、大幅な変更が必要です。内容が難しいわけではありませんが、地味な作業が延々と続きます。

変更内容

viewOutlet.png EnclosureViewはリサイズされた時にサブビューの大きさと位置を変更します。サブビューの変更点を挙げると、以下の様になります。

・発振器の数が二つから六つに増えた
・ミキサーが二つ増えた
・リサージュビューが原図ビューに変わった
・接続線が追加された

 ビューとアウトレットの対応関係は右の図の様になっています。



 またサブビューではありませんがamplitudeのコントロールが増えています。こちらはサイズ調整ではなく、バインディングの切替えに関わる要素です。また周波数だけステッパがないのも統一感に欠けるので、追加しました。

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

#import <Cocoa/Cocoa.h>


@class  OscillatorView, BezierPathView;

@interface EnclosureView : NSView
{
    IBOutlet    OscillatorView      *x1View,*x2View,*x3View;
    IBOutlet    OscillatorView      *y1View,*y2View,*y3View;
    IBOutlet    BezierPathView      *viewX,*viewY,*viewMaster;
    IBOutlet    NSView              *lineX,*lineY;
    IBOutlet    NSView              *lineX1,*lineX2,*lineX3;
    IBOutlet    NSView              *lineY1,*lineY2,*lineY3;

    IBOutlet    NSArrayController   *controller;
    IBOutlet    NSTextField         *amplitudeTextField, *frequencyTextField, *phaseLagTextField;
    IBOutlet    NSSlider            *amplitudeSlider, *frequencySlider, *phaseLagSlider;
    IBOutlet    NSStepper           *amplitudeStepper, *frequencyStepper, *phaseLagStepper;

    int     whiteSpace,viewMargin,lineLength;

    OscillatorView  *selectedView;
}

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

@end

初期化メソッド

 初期化メソッドでは接続線の長さを規定するインスタンス変数lineLengthの設定を追加しています。

 awakeFromNibではlineLengthを使って、短い接続線の長さを設定しています。短い接続線は長さが固定なので、はじめに一回設定するだけで済みます。

//
//  EnclosureView.m
//

#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;
        lineLength = 10;
        [nc addObserver:self
               selector:@selector(frameDidChange:)
                   name:NSViewFrameDidChangeNotification
                 object:self];
    }
    return self;
}

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

- (void)awakeFromNib
{
    NSSize  sizeLineX = NSMakeSize(1,lineLength);
    NSSize  sizeLineY = NSMakeSize(lineLength,1);
    
    selectedView = nil;
    [self selectOscillator:x1View];
    [lineX1 setFrameSize:sizeLineX];
    [lineX3 setFrameSize:sizeLineX];
    [lineY1 setFrameSize:sizeLineY];
    [lineY2 setFrameSize:sizeLineY];

    [self frameDidChange:nil];
    
    [viewMaster setKeyPath:@"selection" isXView:NO];
    [viewX setKeyPath:@"selection.x" isXView:YES];
    [viewY setKeyPath:@"selection.y" isXView:NO];
    [x1View setKeyPath:@"selection.x.osc1" isXView:YES];
    [x2View setKeyPath:@"selection.x.osc2" isXView:YES];
    [x3View setKeyPath:@"selection.x.osc3" isXView:YES];
    [y1View setKeyPath:@"selection.y.osc1" isXView:NO];
    [y2View setKeyPath:@"selection.y.osc2" isXView:NO];
    [y3View setKeyPath:@"selection.y.osc3" isXView:NO];
}

サブビューをリサイズして再配置する

layOutParameter.png 初期化メソッドで通知を受け取る設定を済ませましたので、EnclosureViewがリサイズされるとframeDidChange:メソッドが呼ばれます。ここでサブビューをリサイズして再配置を行ないます。

 各ビューを配置する際のパラメータを右の図のように決めます。whiteSpaceは周囲に対する余白(A)、viewMarginはビュー同士の間隔(B)、lineLengthは接続線の長さ(C)、viewSizeは波形を表示するビューの一辺の長さです。

 まずこのメソッド内で使う変数を宣言します。

//
//  EnclosureView.m
//

@implementation EnclosureView(Private)

#pragma mark -
#pragma mark Event Handler

- (void)frameDidChange:(NSNotification *)aNotification
{
    int         width,height,viewSize,marginWidth,marginHeight;
    NSSize      sizeOfView;
    NSSize      sizeLineX,sizeLineY,longSizeLineX,longSizeLineY;
    NSPoint     originLineX,originLineY;
    NSPoint     originViewX,originViewY,originViewMaster;    
    NSPoint     originViewX1,originViewX2,originViewX3;
    NSPoint     originViewY1,originViewY2,originViewY3;
    NSPoint     originLineX1,originLineX2,originLineX3;
    NSPoint     originLineY1,originLineY2,originLineY3;

ビューのサイズを計算する

layOutParameter.png 右の説明図の(D).viewSizeを計算で求めます。widthとheightはそれぞれ許される幅と高さで、どちらか小さい方がviewSizeとなります。

 widthとheightの計算方法は右の説明図を見ればわかると思います。幅方向には(A)が二個、(B)が三個、(C)が二個、(D)が四個入っているので、EnclosureViewの横幅からA×2, B×3, C×2をそれぞれ引いて4で割ればDが求まります。ソースコードでは8で割って二倍していますが、これはwidthを偶数にするための工夫です。こうしないと接続線が表示される時にアンチエイリアスがかかってぼやけた表示になってしまいます。

 viewSizeが決まったら全体がセンタリングされる様に実際の余白を決定します。marginWidthとmarginHeightがy3Viewの原点座標となります。


    //  接続線にアンチエイリアスがかかってぼやけない様に、widthとheightを偶数にする
    width = ([self bounds].size.width - whiteSpace*2 - viewMargin*3 - lineLength*2)/8;
    width *= 2;
    height = ([self bounds].size.height - whiteSpace*2 - viewMargin*2 - lineLength*2)/6;
    height *= 2;
    viewSize = width < height ? width : height;
    marginWidth = ([self bounds].size.width - viewSize*4 - viewMargin*3 - lineLength*2)/2;
    marginHeight = ([self bounds].size.height - viewSize*3 - viewMargin*2 - lineLength*2)/2;

各ビューを配置する

 あとは計算で求めた座標に各ビューを配置していくだけです。


    //  Y軸のOscillatorViewを配置する
    originViewY1.x = originViewY2.x = originViewY3.x = marginWidth;
    originViewY3.y = marginHeight;
    originViewY2.y = originViewY3.y + viewSize + viewMargin;
    originViewY1.y = originViewY2.y + viewSize + viewMargin;
    [y1View setFrameOrigin:originViewY1];
    [y2View setFrameOrigin:originViewY2];
    [y3View setFrameOrigin:originViewY3];

    //  Y軸のOscillatorViewにつながる接続線を配置する
    originLineY1.x = originLineY2.x = originLineY3.x = originViewY1.x + viewSize + viewMargin;
    originLineY1.y = originViewY1.y + viewSize*0.5;
    originLineY2.y = originViewY2.y + viewSize*0.5;
    originLineY3.y = originViewY3.y + viewSize*0.5;    
    [lineY1 setFrameOrigin:originLineY1];
    [lineY2 setFrameOrigin:originLineY2];
    [lineY3 setFrameOrigin:originLineY3];

    //  一番下の接続線を長くする
    longSizeLineY = NSMakeSize(lineLength*2,1);
    [lineY3 setFrameSize:longSizeLineY];

    //  Y軸の縦の接続線を配置する
    originLineY.x = originLineY3.x + lineLength;
    originLineY.y = originLineY3.y;
    sizeLineY = NSMakeSize(1,viewMargin*2+viewSize*2);
    [lineY setFrameOrigin:originLineY];
    [lineY setFrameSize:sizeLineY];

    //  Y軸のMixerViewを配置する
    originViewY.x = originLineY3.x + longSizeLineY.width;
    originViewY.y = originViewY3.y;
    [viewY setFrameOrigin:originViewY];

    //  MasterMotifViewを配置する
    originViewMaster.x = originViewY.x + viewSize + viewMargin;
    originViewMaster.y = originViewY.y;
    [viewMaster setFrameOrigin:originViewMaster];

    //  X軸のMixerViewを配置する
    originViewX.x = originViewMaster.x;
    originViewX.y = originViewMaster.y + viewSize + viewMargin;
    [viewX setFrameOrigin:originViewX];

    //  X軸の横の接続線を配置する
    originLineX.x = originViewX.x - viewSize*0.5 - viewMargin;
    originLineX.y = originViewX.y + viewSize + lineLength;
    [lineX setFrameOrigin:originLineX];
    sizeLineX = NSMakeSize(viewMargin*2+viewSize*2,1);
    [lineX setFrameSize:sizeLineX];

    //  X軸の縦の接続線を配置する
    originLineX1.x = originLineX.x;
    originLineX2.x = originLineX1.x + viewSize + viewMargin;
    originLineX3.x = originLineX2.x + viewSize + viewMargin;
    originLineX1.y = originLineX3.y = originLineX.y;    
    originLineX2.y = originLineX.y - lineLength;
    [lineX1 setFrameOrigin:originLineX1];
    [lineX2 setFrameOrigin:originLineX2];
    [lineX3 setFrameOrigin:originLineX3];

    //  真ん中の接続線を長くする
    longSizeLineX = NSMakeSize(1,lineLength*2);
    [lineX2 setFrameSize:longSizeLineX];

    //  X軸のOscillatorViewを配置する
    originViewX1.x = originViewX.x - viewSize - viewMargin;
    originViewX2.x = originViewX.x;
    originViewX3.x = originViewX.x + viewSize + viewMargin;
    originViewX1.y = originViewX2.y = originViewX3.y = originLineX.y + lineLength + viewMargin;
    [x1View setFrameOrigin:originViewX1];
    [x2View setFrameOrigin:originViewX2];
    [x3View setFrameOrigin:originViewX3];

    //  サイズをセットする
    sizeOfView = NSMakeSize(viewSize,viewSize);
    [viewMaster setFrameSize:sizeOfView];
    [viewX setFrameSize:sizeOfView];
    [viewY setFrameSize:sizeOfView];
    [x1View setFrameSize:sizeOfView];
    [x2View setFrameSize:sizeOfView];
    [x3View setFrameSize:sizeOfView];
    [y1View setFrameSize:sizeOfView];
    [y2View setFrameSize:sizeOfView];
    [y3View setFrameSize:sizeOfView];
}

@end

OscillatorViewの選択に伴うバインディングの切替え

 selectOscillatorメソッドで発振器のパラメータを設定するビューのバインディングを切替えていますが、振幅を設定するビューが増えたので対応します。

 まずamplitudeKeyPathメソッドを追加します。第三のプロジェクトでは選択されているビューを見てキーパスを決めていましたが、数が二個から六個に増えたのでそれではコードが長くなりすぎます。そのためにOscillatorViewにoscillatorKeyPathメソッドを用意したので、これを使ってキーパスを生成する様にします。これはfrequencyKeyPathとphaseLagKeyPathでも同じですので、同じ変更を加えます。

 細かい話ですが、原図が何も選択されていないとき、あるいは複数選択されている時にテキストフィールドにデフォルトの長いプレイスホルダー文字列が表示されてはみ出しているのが気になります。そこで、はみ出さないような短い文字列をプレイスホルダーとして設定する事にします。これは辞書に指定されたキーで値を書き込み、bind:toObject:withKeyPath:options:メソッドのoptions引数としてその辞書を渡す事で実現できます。
 無選択時と複数選択時のプレイスホルダー文字列を設定していますので、ソースコードを参照して下さい。

 selectOscillatorメソッドでは振幅を設定するビューと周波数のステッパの設定を追加しています。

//
//  EnclosureView.m
//

#pragma mark -
#pragma mark Selection

- (NSString *)amplitudeKeyPath
{
    return  [[selectedView oscillatorKeyPath] stringByAppendingString:@".amplitude"];
}

- (NSString *)frequencyKeyPath
{
    return  [[selectedView oscillatorKeyPath] stringByAppendingString:@".frequency"];
}

- (NSString *)phaseLagKeyPath
{
    return  [[selectedView oscillatorKeyPath] stringByAppendingString:@".phaseLag"];
}

- (void)bind:(id)object withKeyPath:(NSString *)keyPath
{
    NSDictionary    *options = [NSDictionary dictionaryWithObjectsAndKeys:
                                @"-",NSMultipleValuesPlaceholderBindingOption,
                                @"-",NSNoSelectionPlaceholderBindingOption,
                                nil];
    
    [object bind:@"value" toObject:controller withKeyPath:keyPath options:options];
}

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

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

    [self bind:amplitudeTextField   withKeyPath:[self amplitudeKeyPath]];
    [self bind:amplitudeSlider      withKeyPath:[self amplitudeKeyPath]];
    [self bind:amplitudeStepper     withKeyPath:[self amplitudeKeyPath]];
    [self bind:frequencyTextField   withKeyPath:[self frequencyKeyPath]];
    [self bind:frequencySlider      withKeyPath:[self frequencyKeyPath]];
    [self bind:frequencyStepper     withKeyPath:[self frequencyKeyPath]];
    [self bind:phaseLagTextField    withKeyPath:[self phaseLagKeyPath]];
    [self bind:phaseLagSlider       withKeyPath:[self phaseLagKeyPath]];
    [self bind:phaseLagStepper      withKeyPath:[self phaseLagKeyPath]];
}