選択枠を表示する【実践的Macintoshプログラミング解説】

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

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

LinkIconホーム

更新日 2009-05-24

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

選択枠を表示する

selectionBorder.png 発振器を選択できる様にしたとして、それをユーザに知らせるには視覚的なフィードバックが必要です。つまり選択されている事が目で見てわかる様にするという事です。

 そのためにOscillatorViewに強調表示の機能を持たせます。選択されたOscillatorViewは外周に太い黒枠を描いて、自らを強調する事にします。

インスタンス変数とアクセサメソッドを追加する

 OscillatorViewに選択されているかどうかを表すフラグと、そのアクセサメソッドを追加します。

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

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


@interface OscillatorView : BezierPathView
{
    BOOL    selected;
}

- (BOOL)selected;
- (void)setSelected:(BOOL)isSelected;

@end

 追加されたフラグを初期化し、アクセサメソッドを追加します。
 初期化は普通イニシャライザで行なうのですが、X軸のOscillatorViewが選択される様に初期化したいので、keyPathメソッドで初期化します。keyPathメソッドはawakeFromNibから呼ばれるので、これで必ず初期化されます。

 選択状態が変わった時は画面の書き換えが必要になりますのでsetNeedsDisplay:メソッドで要変更マークをつけています。

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

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

選択枠を描画する

 描画はスーパークラスのdrawRect:メソッドに任せていたので、OscillatorViewにはdrawRect:メソッドを実装していませんでした。
 そこでdrawRect:メソッドを追加し、フラグに応じて選択枠を描いたり、消したりする処理を追加します。 

 まずスーパークラスのdrawRect:メソッドを呼んで、中身を描いてもらいます。その後、リサイズ中でなければ選択枠を描くか、もしくは消す様にします。リサイズ中に何もしないのは処理を軽くするためです。

 savedFrameに現在のフレームを保存してから、一時的にフレームを3ピクセル拡げます。

 フレームというのはスーパービューから見た位置とサイズの事です。これに対してバウンズは自分の座標系で見た位置とサイズの事です。描画する時は自分の座標系を使います。ちなみに自分の座標系なので原点はいつも(0, 0)です。

 拡げた状態でバウンズを得て、それを1ピクセル縮めます。太さ2の線で選択枠を描くので、1ピクセル縮めておけば外側がぴったりになります。

 selectedがYESなら黒、NOならウィンドウの背景色で選択枠を描きます。背景色で描くという事は、すなわち消すという事です。

 最後にフレームを保存しておいたものに戻して終了です。

//
//  OscillatorView.m
//

#pragma mark -
#pragma mark Event handler

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

領土を主張する

 これだけでうまくいけばよかったのですが、やってみると描いた選択枠がすぐに消されてしまいます。一時的にフレームを拡げても、そこは自分の領土ではないのでクリーンアップされてしまうという事でしょうか?

 そこでvisibleRectというメソッドを使って自分の領土を主張してみたところ、うまくいきました。これでせっかく描いた選択枠を消されなくなります。2という数値は試行錯誤で決めました。

//
//  OscillatorView.m
//

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

ゴミが残る

dirtyWindow.png
 これだけでうまくいけばよかったのですが、残念ながらウィンドウをリサイズすると選択枠を消しきれずにウィンドウにゴミが残ってしまいました。

 先ほどは描いた選択枠が消されてしまい、今度は消されずに残ってしまうわけで、なかなかうまくいかないものです。 

【失敗例】リサイズ中は選択枠を消す

 drawRect:メソッドでリサイズ中は何もしない様にしていましたが、リサイズ中は選択状態に関わらず、選択枠を消す様にしてみました。下記のコードです。

 結果は処理が重くなったせいか、もたつきが発生しました。かつ、まれにゴミが残ってしまいます。

//
//  OscillatorView.m
//

- (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 && ![self inLiveResize])
            [[NSColor blackColor] set];
        else
            [[NSColor windowBackgroundColor] set];
        [borderPath stroke];

        [self setFrame:savedFrame];
}

【対策案1】スーパービューでリサイズスタート時に消す

 消し残しをOscillatorViewで対策するのが難しそうなので、スーパービューに消してもらう事にしました。リサイズスタート時にフラグを立てて、フラグがたっていたらビュー全体を消去してフラグをクリアします。こうしておけば、普段は負荷がかかりません。

 結果はウィンドウにゴミが残らなくなった点は成功です。リサイズ時のもたつきは発生したりしなかったりで再現性がなく、よくわからないのが正直なところです。

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

@end

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

#import "EnclosureView.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
{
    [self frameDidChange:nil];
}

@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)viewWillStartLiveResize
{
    dirty = YES;
}

- (void)drawRect:(NSRect)rect
{
    if(dirty)
    {
        [[NSColor windowBackgroundColor] set];
        [[NSBezierPath bezierPathWithRect:[self bounds]] fill];
        dirty = NO;
    }
}

@end

【対策案2】ダミーのdrawRect:メソッドを設ける

 対策案1は多少不満が残るものの、これ以上の手段を思いつかないのでこれを採用するしかないところです。ところが、ある偶然から別の手段を発見しました。

 説明のためにウィンドウにゴミが残っているスクリーンショットを撮ろうとして、viewWillStartLiveResizeメソッドのdirty = YES;をコメントアウトしてみたのです。これでビュー全体を消去する動作はしなくなるので、ゴミが残る筈です。ところがいくらウィンドウをリサイズしてもゴミが残りません!

 色々といじってみて、結局中身が空のdrawRect:メソッドが存在するだけで対策になる事がわかりました。しかも、これだともたつきが発生しないのです。理由は全くわかりませんし、何だかだまされているようですっきりしませんが、もたつきが発生しないメリットは大きいので、これを採用する事にします。

 でも将来動作が変わってしまうような気がします。念のため環境を書いておくと、Xcode 3.1とMac OS X 10.5.4です。プロセッサはPowerPC G5 デュアル2.3GHzです。

- (void)viewWillStartLiveResize
{
    dirty = YES;
}

- (void)drawRect:(NSRect)rect
{
    if(dirty)
    {
        [[NSColor windowBackgroundColor] set];
        [[NSBezierPath bezierPathWithRect:[self bounds]] fill];
        dirty = NO;
    }
}

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