使用 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];
}
現在,我們將發現當我們編譯並執行應用程式時,選擇工具將具有更標準的行為。
這裡有一些相當嚴重的效率低下 - 每次選擇框大小更改時,選擇都會從頭開始重新計算。對於少數幾個物件來說這不是問題,但如果我們的繪圖變得複雜,它就可能是問題。一旦我們添加了將響應選擇更改的檢查器,效能可能會進一步下降。我們現在不會擔心這個問題,但也許你可以想出一些簡單的方法來提高效率?