共通機能をスーパークラスに移す【実践的Macintoshプログラミング解説】

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

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

LinkIconホーム

更新日 2009-05-24

ビューを作る

共通機能をスーパークラスに移す

viewClassModel.png リサージュ図形を表示するビューLissajousViewを作成するのですが、ベジエパスを表示するのが主な機能なのでOscillatorViewと機能がほとんど同じになります。そこでOscillatorViewのスーパークラスとしてBezierPathViewを作り、そこに機能を移す事にします。

 このようなことはよくあるので、Xcodeはリファクタリングコマンドを編集メニューに用意しています。これを使って…と思ったのですが、使ってみると意外と手間がかかるので、今回は手動で実施する事にしました。

 まずサブクラスの中身を全てスーパークラスに移してしまいます。そしてサブクラスで変更したい部分を考えて、そこをサブクラスでオーバーライドできる様にメソッドとして独立させます。

 ついでにX軸の波形を横向きに表示する機能も、ここで追加しておく事にしました。

BezierPathViewを作る

 ファイルメニューから「新規ファイル…」を実行します。現れた新規ファイルアシスタントが示すリストからObjective-C NSView subclassを選び、次に進みます。

newNSViewSubClass.png

 ファイル名をBezierPathViewとします。

BezierPathViewAssistant.png

 するとBezierPathView.hとBezierPathView.mができ上がります。とりあえずOscillatorViewの中身を全部移してしまいましょう。するとこうなります。

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

#import <Cocoa/Cocoa.h>


@interface BezierPathView : NSView
{
    IBOutlet    NSArrayController   *controller;

    NSBezierPath    *dataPath, *displayPath;
}

- (NSBezierPath *)dataPath;
- (void)setDataPath:(NSBezierPath *)newPath;

@end

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

#import "BezierPathView.h"


@implementation BezierPathView

#pragma mark -
#pragma mark Initializer and Deallocator

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

    self = [super initWithFrame:frame];
    if(self)
    {
        dataPath = displayPath = nil;
        [nc addObserver:self
               selector:@selector(frameDidChange:)
                   name:NSViewFrameDidChangeNotification
                 object:self];
    }
    return self;
}

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

- (void)awakeFromNib
{
    [self bind:@"dataPath"
      toObject:controller
   withKeyPath:@"selection.waveform"
       options:nil];
}

#pragma mark -
#pragma mark Path

- (void)updateDisplayPath
{
    NSAffineTransform   *at = [NSAffineTransform transform];

    [displayPath release];
    displayPath = [dataPath copy];

    //  変換操作を逆順で登録する
    //      ベジエパスの中心をフレームの中心に移動
    [at translateXBy:[self bounds].size.width/2
                 yBy:[self bounds].size.height/2]; 
    //      サイズをフィットさせる
    [at scaleXBy:([self bounds].size.width-5)/[displayPath bounds].size.width
             yBy:([self bounds].size.height-5)/(AMPLITUDE_MAX*2.0)];
    //      ベジエパスの中心を原点に移動
    [at translateXBy:-NSMidX([displayPath bounds])
                 yBy:-NSMidY([displayPath bounds])];

    [displayPath transformUsingAffineTransform:at];
    [self setNeedsDisplay:YES];
}

#pragma mark -
#pragma mark Event Handler

- (void)drawRect:(NSRect)rect
{
    [[NSBezierPath bezierPathWithRect:[self bounds]] stroke]; 

    if(![self inLiveResize])
        [displayPath stroke];
}

- (void)viewDidEndLiveResize
{
    [self updateDisplayPath];
    [super viewDidEndLiveResize];
}

- (void)frameDidChange:(NSNotification *)aNotification
{
    if(![self inLiveResize]) 
        [self updateDisplayPath];
}

#pragma mark -
#pragma mark Accessor Methods

- (NSBezierPath *)dataPath  {return  dataPath;}
- (void)setDataPath:(NSBezierPath *)newPath
{
    [newPath retain];
    [dataPath release];
    dataPath = newPath;

    [self updateDisplayPath];
}
@end

サブクラスで変更したい部分はどこか

 このコードの中でOscillatorViewとLissajousViewで変えたい所はどこなのかを考えてみます。

 まずバインディングを設定する際のキーパスが異なる筈です。LissajousViewのキーパスが"selection.bezierPath"であるのに対してOscillatorViewのキーパスは"selection.x.bezierPath"あるいは"selection.y.bezierPath"であるはずです。

 次に異なる点はパスの表示方法です。OscillatorViewの場合、最大振幅が100であるとして大きさを調整しています。LissajousViewはパスを自分の大きさにフィットさせて表示します。

 その点を考慮すると以下の様に変更するのがよさそうです。

// 
//  BezierPathView.m
//

#pragma mark -
#pragma mark Methods to be overrided

- (NSString *)keyPath
{
    return  nil;
}

- (float)pathHeight
{
    return  [displayPath bounds].size.height;
}

#pragma mark -
#pragma mark Initializer and Deallocator

- (void)awakeFromNib
{
    [self bind:@"dataPath"
      toObject:controller
   withKeyPath:[self keyPath]
       options:nil];
}

#pragma mark -
#pragma mark Path

- (void)updateDisplayPath
{
    NSAffineTransform   *at = [NSAffineTransform transform];

    [displayPath release];
    displayPath = [dataPath copy];

    //  変換操作を逆順で登録する
    //      ベジエパスの中心をフレームの中心に移動
    [at translateXBy:[self bounds].size.width/2
                 yBy:[self bounds].size.height/2]; 
    //      サイズをフィットさせる
    [at scaleXBy:([self bounds].size.width-5)/[displayPath bounds].size.width
             yBy:([self bounds].size.height-5)/[self pathHeight]];
    //      ベジエパスの中心を原点に移動
    [at translateXBy:-NSMidX([displayPath bounds])
                 yBy:-NSMidY([displayPath bounds])];

    [displayPath transformUsingAffineTransform:at];
    [self setNeedsDisplay:YES];
}

X軸のOscillatorViewで波形を横向きに表示させる

 X軸の発振器出力は波形を横向きに変化させるのに使われるので、横向きに表示された方が自然です。そこでX軸のビューであるかどうかを表すフラグを追加します。

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

#import <Cocoa/Cocoa.h>


@interface BezierPathView : NSView
{
    IBOutlet    NSArrayController   *controller;

    BOOL            isXView;
    NSBezierPath    *dataPath, *displayPath;
}

- (NSBezierPath *)dataPath;
- (void)setDataPath:(NSBezierPath *)newPath;

@end

 イニシャライザでフラグをNOに初期化します。サブクラスは必要であればこのフラグをYESに初期化します。

 updateDisplayPathメソッドで、フラグに応じてパスを回転させます。

// 
//  BezierPathView.m
//

#pragma mark -
#pragma mark Initializer and Deallocator

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

    self = [super initWithFrame:frame];
    if(self)
    {
        isXView = NO;
        dataPath = displayPath = nil;
        [nc addObserver:self
               selector:@selector(frameDidChange:)
                   name:NSViewFrameDidChangeNotification
                 object:self];
    }
    return self;
}

#pragma mark -
#pragma mark Path

- (void)updateDisplayPath
{
    NSAffineTransform   *at = [NSAffineTransform transform];

    [displayPath release];
    displayPath = [dataPath copy];

    //  変換操作を逆順で登録する
    //      ベジエパスの中心をフレームの中心に移動
    [at translateXBy:[self bounds].size.width/2
                 yBy:[self bounds].size.height/2]; 
    //      必要なら90度回転させる
    if(isXView)
        [at rotateByDegrees:-90];
    //      サイズをフィットさせる
    [at scaleXBy:([self bounds].size.width-5)/[displayPath bounds].size.width
             yBy:([self bounds].size.height-5)/[self pathHeight]];
    //      ベジエパスの中心を原点に移動
    [at translateXBy:-NSMidX([displayPath bounds])
                 yBy:-NSMidY([displayPath bounds])];

    [displayPath transformUsingAffineTransform:at];
    [self setNeedsDisplay:YES];
}