他のクラスを乱数決定に対応させる【実践的Macintoshプログラミング解説】

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

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

LinkIconホーム

更新日 2009-05-24

原図のパラメータをランダムに決める

他のクラスを乱数決定に対応させる

 ランダマイザができたので、原図のパラメータをランダムに決定する事ができる様になりました。あとはランダマイズアクションを受け取るビューと、ランダマイズメッセージに対応するモデルが必要です。

 またランダマイズアクションを送るボタンをウィンドウ上に配置するのですが、スペースを有効活用するために原図ビューの隣の空きスペースに置く事にします。そうなるとウィンドウがリサイズされた時にボタンをうまく再配置しなくてはいけません。この機能をEnclosureViewに追加します。

MasterMotifCDを対応させる

 まずモデルクラスの対応から説明します。MasterMotifCDクラスの変更点は初期化コードの追加とrandomizeメソッドの追加、そしてアクション名を返すクラスメソッドの追加です。

初期化コードを追加する

 ランダマイザで発振器を特定するためにorderを利用しているので、発振器を作った時にorderを設定するコードを追加します。

  • ミキサーのorderは利用していないので設定する必要はないのですが、ついでなので設定するコードを追加しておきました。
  • パラメータの乱数決定とは無関係ですが、原図の初期状態が斜め45度の線になっているのが気に入らないので、ついでに修正しました。円になる様にphaseLagを設定しています。

//
//  MasterMotifCD.m
//

#pragma mark -
#pragma mark Initializer

- (void)createMixer
{
    int i;
    MixerCD     *mixer;

    for(i=0;i<2;i++)
    {
        mixer = [NSEntityDescription insertNewObjectForEntityForName:@"Mixer"
                                              inManagedObjectContext:MOC];
        [[self mutableSetValueForKey:@"mixers"] addObject:mixer];
        if(i==0)
            [self setX:mixer];
        else
            [self setY:mixer];
    }

    //  ランダマイザの為に発振器のorderをセットする。(ミキサーのorderは使っていないが念のため)
    [[self x] setOrder:[NSNumber numberWithInt:0]];
    [[self y] setOrder:[NSNumber numberWithInt:1]];
    [[[self x] osc1] setOrder:[NSNumber numberWithInt:0]];
    [[[self x] osc2] setOrder:[NSNumber numberWithInt:1]];
    [[[self x] osc3] setOrder:[NSNumber numberWithInt:2]];
    [[[self y] osc1] setOrder:[NSNumber numberWithInt:3]];
    [[[self y] osc2] setOrder:[NSNumber numberWithInt:4]];
    [[[self y] osc3] setOrder:[NSNumber numberWithInt:5]];

    //  初期図形を円にする
    [[[self y] osc1] setPhaseLag:[NSNumber numberWithInt:90]];
    [[[self y] osc2] setPhaseLag:[NSNumber numberWithInt:90]];
    [[[self y] osc3] setPhaseLag:[NSNumber numberWithInt:90]];

    //  addActionNameがセットされる様にクリア
    [UNDO_MANAGER setActionName:@""];
}

【失敗例】アンドゥメニューにAdd Master Motifが現れない!


 上のコードの[UNDO_MANAGER setActionName:@""];は後で追加したものです。最初はありませんでした。これを追加しないと原図を追加した時のアンドゥメニューが"Undo Add Master Motif"ではなく"Undo Change Phase Lag"になるという不具合が発生します。

 データソースを開発している時に、もちろんこのような事が起こらない事は確認していました。その時は初期図形を円にするコードはなかったので、アンドゥマネージャに何もアクション名が設定されておらず、「アクション名が設定されていなければ"Add Master Motif"をセットする」というロジックがうまく働いていたわけです。その事を忘れて初期図形を円にするコードを追加したので、不具合発生となりました。

 そう考えると「アクション名が設定されていなければ"Add Master Motif"をセットする」という方法はあまり良い方法ではありませんね。管理対象オブジェクトがコンテキストに挿入されたとき、それが追加されたのか、ペーストされたのか、ドラッグコピーされたのかを見分ける別の方法があれば良いのですが・・・

randomizeメソッドを追加する

 MasterMotifCDのrandomizeメソッドは、各ミキサーにrandomizeメッセージを送ってfullUpdateを実行すればOKです。

//
//  MasterMotifCD.m
//

#pragma mark -
#pragma mark Randomizing parameters

- (void)randomize
{
    [[self mutableSetValueForKey:@"mixers"] makeObjectsPerformSelector:@selector(randomize)];
    [self fullUpdate];
}

アクション名を返す

 ランダマイズを実行した時にアンドゥメニューに現れるアクション名を返します。

//
//  MasterMotifCD.m
//

#pragma mark -
#pragma mark Action names
    
+ (NSString *)randomizeActionName       {return  @"randomizeMasterMotif";}

MixerCDを対応させる

 MixerCDの対応はrandomizeメッセージを受けて、同じメッセージを各発振器に送る事だけです。

//
//  MixerCD.m
//

#pragma mark -
#pragma mark Randomizing parameters

- (void)randomize
{
    [[self mutableSetValueForKey:@"oscillators"] makeObjectsPerformSelector:@selector(randomize)];
}

OscillatorCDを対応させる

 OscillatorCDにもrandomizeメソッドを実装します。自分のorderを引数にしてMasterMotifRandomizerのrandomAmplitudeとrandomFrequencyを呼び出して、得られた値をセットします。randomPhaseLagは引数がありません。

 アクセサメソッドを使ってパラメータをセットすると、波形の更新作業がいちいち発生してしまって無駄なので、setValue:forKey:action:メソッドを直接使ってセットしています。

//
//  OscillatorCD.m
//

#import "MasterMotifRandomizer.h"

#pragma mark -
#pragma mark Randomizing parameters

- (void)randomize
{
    MasterMotifRandomizer   *randomizer = [MasterMotifRandomizer sharedRandomizer];

    [self setValue:[randomizer randomAmplitude:[self order]] forKey:@"amplitude" action:nil];
    [self setValue:[randomizer randomFrequency:[self order]] forKey:@"frequency" action:nil];
    [self setValue:[randomizer randomPhaseLag] forKey:@"phaseLag" action:nil];
}

RMGTableViewを作る

 ランダマイズアクションに応答するビューはテーブルビューが適切です。NVKTableViewは汎用的に使える事を目指しているので、ランダマイズアクションに応答させるのは適切ではありません。そこでNVKTableViewのサブクラスとしてRMGTableViewを作ります。

インターフェイスファイルの実装

 インターフェイスファイルを以下に示します。インスタンス変数もメソッドの定義も何もありません。

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

#import <Cocoa/Cocoa.h>
#import "NVKTableView.h"


@interface RMGTableView : NVKTableView
{
}

@end

メニューの検証

 メニューのアクションがrandomize:であった場合、選択されているオブジェクトがあればYESを、何も選択されていない場合はNOを返す様にします。アクションがrandomize:ではない場合は、スーパークラスに処理を任せます。

ランダマイズアクション

 現在選択されているオブジェクト全てにrandomizeメッセージを送って、アクション名をセットすれば完了です。

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

#import "RMGTableView.h"

#import "MasterMotifCD.h"


@implementation RMGTableView

- (BOOL)validateMenuItem:(NSMenuItem *)menuItem
{    
    if([menuItem action] == @selector(randomize:))
        return  [[arrayController selectedObjects] count] > 0;
    else
        return  [super validateMenuItem:menuItem];
}

#pragma mark -
#pragma mark Action methods

- (IBAction)randomize:(id)sender
{
    [[arrayController selectedObjects] makeObjectsPerformSelector:@selector(randomize)];
    [self setActionName:[[self targetClass] randomizeActionName]];
}

@end

EnclosureViewでランダマイズボタンを配置する

ボタンのアクションを設定する

 ランダマイズボタンをウィンドウ上に追加します。このボタンのrandomize:アクションはファーストレスポンダに接続します。

randomizeActionConnection.png

EnclosureViewにボタンを再配置させる

 ウィンドウがリサイズされた時に、このボタンを再配置する必要がありますので、EnclosureViewにコードを追加します。まずアウトレットを追加します。

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

#import <Cocoa/Cocoa.h>


@class  OscillatorView, BezierPathView, RMGDocument;

@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    NSButton            *randomizeButton;
    IBOutlet    NSTextField         *amplitudeTextField, *frequencyTextField, *phaseLagTextField;
    IBOutlet    NSSlider            *amplitudeSlider, *frequencySlider, *phaseLagSlider;
    IBOutlet    NSStepper           *amplitudeStepper, *frequencyStepper, *phaseLagStepper;
    IBOutlet    RMGDocument         *document;

    int     whiteSpace,viewMargin,lineLength;

    OscillatorView  *selectedView;
}

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

@end

 原図ビューの右側に、原図ビューと同じ大きさでボタンを配置する様にします。これならウィンドウからはみ出す事はありません。

//
//  EnclosureView.m
//

- (void)frameDidChange:(NSNotification *)aNotification
{
    int         width,height,viewSize,marginWidth,marginHeight;
    NSSize      sizeOfView;
    NSSize      sizeLineX,sizeLineY,longSizeLineX,longSizeLineY;
    NSPoint     originLineX,originLineY;
    NSPoint     originViewX,originViewY,originViewMaster,originRandomize;    
    NSPoint     originViewX1,originViewX2,originViewX3;
    NSPoint     originViewY1,originViewY2,originViewY3;
    NSPoint     originLineX1,originLineX2,originLineX3;
    NSPoint     originLineY1,originLineY2,originLineY3;
    
    //  接続線にアンチエイリアスがかかってぼやけない様に、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];
    
    //  Buttonを配置する
    originRandomize.x = originViewMaster.x + viewSize + viewMargin;
    originRandomize.y = originViewMaster.y;
    [randomizeButton setFrameOrigin:originRandomize];
    
    //  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];
    [randomizeButton setFrameSize:sizeOfView];
}