使用 Cocoa 为初学者编程 Mac OS X/更多形状
在我们可以对不同的工具进行操作之前,我们需要实现不同形状的类。目前我们有四个工具按钮,所以最好有一个是选择工具,三个是形状绘制工具。我们已经有了矩形形状,所以我们还需要另外两个。我们将创建一个椭圆形和一个稍微复杂的多边形形状。如果您愿意,可以继续创建任意数量的其他形状,并扩展工具面板以访问它们。
选择 WKDShape.m 并选择文件 -> 新建文件... 选择 Objective-C 类,并将其命名为“WKDOvalShape.m”。顺便说一下,在执行此操作之前选择 WKDShape 文件的唯一原因是为了确保新文件位于同一组中 - 不幸的是,Xcode 不会自动对现有类进行子类化,因此我们必须手动执行此操作。
打开 WKDOvalShape.h 文件并将 #import "WKDShape.h" 添加到文件顶部。然后将 NSObject 更改为 WKDShape。您的类定义如下
#import <Cocoa/Cocoa.h> #import "WKDShape.h" @interface WKDOvalShape : WKDShape { } @end
唯一要实现的方法是 'path',我们不需要声明它。只需将其添加到实现中,如下所示
@implementation WKDOvalShape - (NSBezierPath*) path { return [NSBezierPath bezierPathWithOvalInRect:[self bounds]]; } @end
现在重复整个练习,这次为名为 'WKDPolyShape' 的类创建文件。以下是我们需要的实现
@implementation WKDPolyShape - (NSBezierPath*) path { NSBezierPath* path = [NSBezierPath bezierPath]; NSRect br = [self bounds]; NSPoint p; [path moveToPoint:br.origin]; p.x = NSMidX( br ); p.y = NSMidY( br ); [path lineToPoint:p]; p.x = NSMaxX( br ); p.y = NSMinY( br ); [path lineToPoint:p]; p.x = NSMaxX( br ); p.y = NSMaxY( br ); [path lineToPoint:p]; p.x = NSMidX( br ); p.y = NSMidY( br ); [path lineToPoint:p]; p.x = NSMinX( br ); p.y = NSMaxY( br ); [path lineToPoint:p]; [path closePath]; return path; } @end
现在返回到 WKDDrawView.m。将这两个新文件的导入添加到文件顶部。将以下内容添加到方法 'shapeForCurrentTool' 中。
- (WKDShape*) shapeForCurrentTool { switch([self currentTool]) { default: return nil; case 1: return [[[WKDShape alloc] init] autorelease]; case 2: return [[[WKDOvalShape alloc] init] autorelease]; case 3: return [[[WKDPolyShape alloc] init] autorelease]; } } - (int) currentTool { return sCurrentTool; }
这很简单 - 根据当前工具,我们创建三种不同的形状之一。我们将 'nil' 作为默认情况返回,我们将使用它来表示选择工具。构建并运行,并检查是否可以绘制三种形状。工具 ID 为 0 的工具目前将记录错误,因为我们还没有调整我们的 mouseDown: 方法来处理选择工具的情况。我们接下来将解决这个问题。
选择框的概念非常简单 - 用户使用选择工具在视图中点击,并拖动一个矩形框覆盖一组对象。框接触的任何对象都会被选中,而框外的其他对象都会被取消选中。
为了使这能够工作,我们需要一种方法来判断哪些对象被选择框接触,哪些没有。这很简单,因为我们已经见过的实用函数 NSIntersectsRect 将会解决这个问题。我们只需要遍历我们的对象,检索它们的边界(或更准确地说,绘制边界)并测试它们是否与选择框相交。如果相交,则选中,否则未选中。
为了绘制选择框本身,我们需要跟踪它的原始锚点,并使用它来形成一个矩形。所有必需的代码都位于视图类中。我们还添加了一对 NSPoint 数据成员,_anchor 和 _marquee,用于跟踪选择框的当前大小。
- (void) updateMarquee:(NSPoint) newPoint { _marquee = newPoint; NSRect mr = NSRectFromTwoPoints( _anchor, _marquee ); NSEnumerator* iter = [[[self delegate] objects] objectEnumerator]; WKDShape* obj; NSRect br; [[self delegate] deselectAll]; while ( obj = [iter nextObject]) { br = [obj drawBounds]; if ( NSIntersectsRect( mr, br )) [[self delegate] selectObject:obj]; } [self setNeedsDisplayInRect:mr]; } - (void) drawMarquee { NSRect mr = NSInsetRect( NSRectFromTwoPoints( _anchor, _marquee ), 1, 1 ); [[NSColor grayColor] set]; NSFrameRect( mr ); }
updateMarquee: 完成了大部分工作。它设置 _marquee 数据成员,然后使用它和 _anchor 来计算选择框的矩形。然后它取消选择所有内容,并遍历对象,测试每个对象的绘制边界是否与选择框矩形相交。如果相交,则选中该对象。
drawMarquee 也计算选择框矩形,并在灰色中将其框起来。这两个方法都调用实用程序 NSRectFromTwoPoints。实际上,Cocoa 没有提供此函数,因此我们自己定义它。请注意,这是一个普通 C 函数,位于类实现之外。它应该放在 WKDDrawView.m 中的 @implementation 声明上方。
NSRect NSRectFromTwoPoints( NSPoint a, NSPoint b ) { NSRect r; r.origin.x = MIN( a.x, b.x ); r.origin.y = MIN( a.y, b.y ); r.size.width = ABS( a.x - b.x ); r.size.height = ABS( a.y - b.y ); return r; }
现在我们需要修改我们的鼠标方法以处理选择框。所有三个方法都受到影响,但 mouseDown: 最复杂。方法的逻辑需要更改,以便实现更一致的行为,现在我们有了可用的工具面板。与其立即查找鼠标下的对象,我们需要先确定当前工具 - 我们只需要在选择工具处于活动状态时查找鼠标下的对象。否则,我们将创建一个新对象。因此,方法顶部的逻辑顺序需要更改。以下是修改后的方法
- (void) mouseDown:(NSEvent*) evt { NSPoint pt = [self convertPoint:[evt locationInWindow] fromView:nil]; _dragShape = [self shapeForCurrentTool]; if (_dragShape == nil) { _dragShape = [[self delegate] objectUnderMousePoint:pt]; if ( _dragShape == nil ) { // marquee drag _anchor = pt; [self updateMarquee:pt]; return; } } else { [[self delegate] addObject:_dragShape]; [_dragShape setLocation:pt]; } if (([evt modifierFlags] & NSShiftKeyMask) == 0) { [[self delegate] deselectAll]; [[self delegate] selectObject:_dragShape]; } else { if ([[[self delegate] selection] containsObject:_dragShape]) { [[self delegate] deselectObject:_dragShape]; _dragShape = nil; } else [[self delegate] selectObject:_dragShape]; } [self setNeedsDisplay:YES]; [_dragShape mouseDown:pt]; } - (void) mouseDragged:(NSEvent*) evt { NSPoint pt = [self convertPoint:[evt locationInWindow] fromView:nil]; NSRect update = [_dragShape drawBounds]; if ( _dragShape ) [_dragShape mouseDragged:pt]; else [self updateMarquee:pt]; [self setNeedsDisplayInRect:NSUnionRect([_dragShape drawBounds], update)]; } - (void) mouseUp:(NSEvent*) evt { NSPoint pt = [self convertPoint:[evt locationInWindow] fromView:nil]; if ( _dragShape ) [_dragShape mouseUp:pt]; else { [self setNeedsDisplayInRect:NSRectFromTwoPoints( _anchor, _marquee )]; _anchor = _marquee = NSZeroPoint; } [self setNeedsDisplayInRect:[_dragShape drawBounds]]; _dragShape = nil; }
如果选择工具当前处于活动状态,我们将有一个为 nil 的 dragShape,因此我们使用它来随后通过测试鼠标是否点击了空空间或现有对象来检测选择框情况。对于 mouseDown:,我们设置 _anchor 的值,然后调用更新选择框。在 mouseDragged: 上,我们检测选择框情况并再次更新它,根据需要更改选择。在 mouseUp: 上,我们刷新选择框区域并将选择框点设置为零。这将擦除选择框,使其所选的任何内容都仍然被选中。唯一剩下的任务是使选择框可见。为此,我们只需将对 drawMarquee 的调用添加到我们的 drawRect: 方法中即可。
- (void) drawRect:(NSRect) rect { [[NSColor whiteColor] set]; NSRectFill( rect ); NSArray* drawList = [[self delegate] objects]; NSArray* selection = [[self delegate] selection]; NSEnumerator* iter = [drawList objectEnumerator]; WKDShape* shape; while( shape = [iter nextObject]) { if ( NSIntersectsRect( rect, [shape drawBounds])) [shape drawWithSelection:[selection containsObject:shape]]; } [self drawMarquee]; }
现在,我们将发现当我们编译并运行应用程序时,选择工具将具有更标准的行为。
这里有一些相当严重的效率低下 - 每次选择框大小更改时,选择都会从头开始重新计算。对于少数几个对象来说这不是问题,但如果我们的绘图变得复杂,它就可能是问题。一旦我们添加了将响应选择更改的检查器,性能可能会进一步下降。我们现在不会担心这个问题,但也许你可以想出一些简单的方法来提高效率?