マーカ表示機能を追加する【実践的Macintoshプログラミング解説】

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

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

LinkIconホーム

更新日 2009-05-24

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

マーカ表示機能を追加する

markerSample.png
 原図は発振器出力を組み合わせて作られているわけですが、これを視覚的に理解する助けとしてマーカを表示する機能を追加する事にしました。

 各ビューの対応する点に赤い丸印が表示され、スライダーと同期してこの丸印を動かす事ができます。

 縦横のミキサーで表示されるマーカと原図のマーカが同期して動くので、その対応関係が目で見てわかるのではないかと思います。

BezierPathViewにマーカ表示機能を追加する

 発振器、ミキサー、原図の全てにマーカを表示するので、マーカ表示機能を追加するのはBezierPathViewです。まずやる事をリストアップしてみましょう。

  1. マーカを表示するかどうか設定できる様にする
  2. マーカの位置を設定できる様にする
  3. マーカを表示する

 1と2を実現するためにインスタンス変数を追加します。

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

#import <Cocoa/Cocoa.h>


@interface BezierPathView : NSView
{
    IBOutlet    NSArrayController   *controller;
    
    BOOL            isXView, multipleSelection, noSelection, showMarker;
    int             markerPosition;
    NSBezierPath    *dataPath, *displayPath;
}

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

- (void)setKeyPath:(NSString *)keyPath isXView:(BOOL)isX;

@end

初期化メソッド

 初期化メソッドでは追加したインスタンス変数の初期値を設定します。

 awakeFromNibではshowMarkerのバインディングを設定します。ドキュメントごとに設定する必要はなく、アプリケーションで共通の設定として充分でしょう。そこでユーザデフォルトとバインディングする事にしました。

//
//  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;
        showMarker = NO;
        markerPosition = 1;
        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:@"showMarker"
      toObject:[NSUserDefaultsController sharedUserDefaultsController]
   withKeyPath:@"values.showMarker"
       options:nil];
    
    [self bind:@"selectionIndexes"
      toObject:controller
   withKeyPath:@"selectionIndexes"
       options:nil];
}

マーカを表示する

 drawRect:メソッドにshowMarkerフラグに応じてマーカを表示するコードを追加します。メソッド名はdisplayMarkerとしました。

 displayMarkerメソッドでは、まずdisplayPathから現在のマーカ位置に対応する座標を読み出します。その座標を中心として円のパスを作り、それを半透明の赤で塗りつぶしてマーカとします。

//
//  BezierPathView.m
//

- (void)displayMarker
{
    int     markerSize = 6;
    NSPoint p;
    NSRect  markerRect;
    NSBezierPath    *markerPath;
    
    [displayPath elementAtIndex:markerPosition associatedPoints:&p];
    markerRect = NSMakeRect(p.x-markerSize/2.0,p.y-markerSize/2.0,markerSize,markerSize);
    markerPath = [NSBezierPath bezierPathWithOvalInRect:markerRect];
    [[[NSColor redColor] colorWithAlphaComponent:0.5] set];
    [markerPath fill];
}

- (void)drawRect:(NSRect)rect
{
    [[NSBezierPath bezierPathWithRect:[self bounds]] stroke];
    
    if(![self inLiveResize])
        if(multipleSelection || noSelection)
            [self showMessage];
        else
        {
            [displayPath stroke];
            if(showMarker)  [self displayMarker];
        }
}

アクセサメソッドを追加する

 最後に1と2を実現できる様に、アクセサメソッドを追加してBezierPathViewとしては完了です。値をセットした後に再描画しているだけで、特に変わった事をしているわけではありません。

//
//  BezierPathView.m
//

- (BOOL)showMarker      {return showMarker;}
- (void)setShowMarker:(BOOL)isShowMarker
{
    showMarker = isShowMarker;
    [self setNeedsDisplay:YES];
}

- (int)markerPosition   {return markerPosition;}
- (void)setMarkerPosition:(int)newPosition
{
    markerPosition = newPosition;
    [self setNeedsDisplay:YES];
}

RMGDocumentをコントローラとする

 マーカ表示機能を実現するビューができたので、あとはモデルとコントローラを作ってバインディングを設定すればOKです。showMarkerはアプリケーション全体で共通の設定としたので、デフォルトデータベースをモデル、ユーザデフォルトコントローラをコントローラとする事ができます。しかしmarkerPositionをアプリケーション全体で共通とするのは少し違和感があります。こちらはドキュメント固有の値としたいところです。

 markerPositionをファイルに保存したいのならCoreDataを使うべきですが、保存する必要は特にありません。そこでRMGDocumentをコントローラとして使う事にします。RMGDocumentならInterface BuilderでFile's Ownerとしてバインディングを設定できます。

 またマーカの最大値は原図のresolutionに依存しますので、スライダーの最大値をresolutionが変わった時に適切に設定する必要があります。RMGDocumentがpositionMaxという値を提供する事で、これを実現します。

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

 前述した機能を実現するために、markerPositionとpositionMaxというインスタンス変数を追加します。また原図のresolutionを監視するためにバインディングを設定するので、配列コントローラのアウトレットを追加します。

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

#import <Cocoa/Cocoa.h>


@class  BezierPathView;

@interface RMGDocument : NSPersistentDocument
{
    IBOutlet    NSArrayController   *arrayController;

    int     markerPosition, positionMax;
}

@end

バインディングを設定し、アクセサメソッドを追加する

 原図のresolutionを監視するために、windowControllerDidLoadNibメソッドでバインディングを設定します。resolutionのSetterではpositionMaxをresolution-1に設定します。例えばresolutionが200の場合はmarkerを0から199に設定できる様にするわけです。

 resolutionを下げた場合、markerPositionがpositionMaxを超える場合があり得ます。その場合はmarkerPositionをpositionMaxに設定します。

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

#import "RMGDocument.h"


@implementation RMGDocument

- (id)init 
{
    self = [super init];
    if(self != nil)
    {
    }
    return self;
}

- (NSString *)windowNibName 
{
    return @"RMGDocument";
}

- (void)windowControllerDidLoadNib:(NSWindowController *)windowController 
{
    [super windowControllerDidLoadNib:windowController];

    [self bind:@"resolution" toObject:arrayController withKeyPath:@"selection.resolution" options:nil];
}

#pragma mark -
#pragma mark Accessor methods

- (int)markerPosition                       {return markerPosition;}
- (void)setMarkerPosition:(int)newPosition  {markerPosition = newPosition;}

- (int)positionMax                          {return positionMax;}
- (void)setPositionMax:(int)newMax          {positionMax = newMax;}

- (int)resolution    {return nil;}
- (void)setResolution:(int)newValue
{
    if(newValue == 0)   return;     //  起動時ゼロが渡される対策
    [self setPositionMax:newValue-1];
    if(markerPosition > positionMax)
        [self setMarkerPosition:positionMax];
}

@end

BezierPathViewをRMGDocumentにバインディングする

 BezierPathViewのバインディングを設定します。設定するのは各BezierPathViewのアウトレットを持つEnclosureViewが適任です。まずRMGDocumentのアウトレットを追加します。

//
//  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    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

 awakeFromNibでバインディングを設定します。

//
//  EnclosureView.m
//

- (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];

    NSArray *views = [NSArray arrayWithObjects:
                      viewMaster, viewX, viewY, x1View, x2View, x3View, y1View, y2View, y3View, nil];
    NSEnumerator    *enumerator = [views objectEnumerator];
    NSView          *view;
    
    while(view = [enumerator nextObject])
        [view bind:@"markerPosition" toObject:document withKeyPath:@"markerPosition" options:nil];
}

Interface Builder上の設定

showMarkerBinding.png 以上の様に色々と用意したので、Interface Builderで設定ができる様になります。

 まずshow markerチェックボックスのバインディング設定ですが、これはユーザデフォルトコントローラとバインディングする様に設定します。



positionMaxBinding.png 次はマーカ位置を設定するスライダーの設定です。最大値をFile's Owner (=RMGDocument)のpositionMaxとバインディングします。



markerPositionBinding.png スライダーの値はFile's Owner のmarkerPositionとバインディングします。



markerSliderEnabledBinding.png スライダーのEnabledをshowMarkerとバインディングして、showMarkerがチェックされていない時はスライダーを無効にしておいた方が親切でしょう。