Mac開發基礎06-NSView(二)

Mr.陳發表於2024-08-06

要理解 NSView 更深層的知識,涉及到其渲染機制、事件處理流程、與 CALayer 的關係及效能最佳化等方面。

1. NSView 繪製和渲染機制

NSView 的繪製過程主要依賴於 drawRect:(Objective-C)或 draw(_:)(Swift)方法。這個方法被呼叫是由系統驅動的,通常發生在需要重新繪製檢視的時候,如視窗首次顯示、視窗大小改變或強制重新整理(呼叫 setNeedsDisplay:)。

繪製生命週期

  • - setNeedsDisplay::標記檢視需要重新繪製。
  • - displayIfNeeded:檢查檢視是否需要繪製,如果需要則呼叫 drawRect:.
  • - displayRectIgnoringOpacity:- display:可以手動觸發檢視的繪製。
Objective-C
[view setNeedsDisplay:YES];  // 標記檢視需要重新繪製
[view displayIfNeeded];      // 如果需要的話,立即進行繪製
Swift
view.needsDisplay = true  // 標記檢視需要重新繪製
view.displayIfNeeded()    // 如果需要的話,立即進行繪製

使用 NSGraphicsContextCGContext

drawRect: 方法中,你可以使用 NSGraphicsContext 獲取底層的 CGContext,以便進行更底層的繪圖操作。

Objective-C
- (void)drawRect:(NSRect)dirtyRect {
    [super drawRect:dirtyRect];

    NSGraphicsContext *currentContext = [NSGraphicsContext currentContext];
    CGContextRef context = [currentContext CGContext];
    
    CGContextSetFillColorWithColor(context, [[NSColor redColor] CGColor]);
    CGContextFillRect(context, dirtyRect);
}
Swift
override func draw(_ dirtyRect: NSRect) {
    super.draw(dirtyRect)

    if let context = NSGraphicsContext.current?.cgContext {
        context.setFillColor(NSColor.red.cgColor)
        context.fill(dirtyRect)
    }
}

2. 事件處理流程

響應鏈

macOS 應用中的事件處理是基於響應鏈的。響應鏈起始於視窗,然後傳遞到最適合處理事件的檢視。如果檢視不能處理事件,則繼續傳遞給其超級檢視,直到到達視窗物件。如果視窗物件也無法處理事件,事件將被忽略。

hitTest:

hitTest: 方法用來確定一個點是否在檢視內,它是事件傳遞過程中關鍵的一環。

Objective-C
- (NSView *)hitTest:(NSPoint)aPoint {
    NSView *hitView = [super hitTest:aPoint];
    NSLog(@"Hit view: %@", hitView);
    return hitView;
}
Swift
override func hitTest(_ point: NSPoint) -> NSView? {
    let hitView = super.hitTest(point)
    print("Hit view: \(String(describing: hitView))")
    return hitView
}

使用者事件處理

Objective-C
- (void)mouseDown:(NSEvent *)event {
    NSPoint locationInWindow = [event locationInWindow];
    NSPoint locationInView = [self convertPoint:locationInWindow fromView:nil];
    NSLog(@"Mouse down at %@", NSStringFromPoint(locationInView));
}
Swift
override func mouseDown(with event: NSEvent) {
    let locationInWindow = event.locationInWindow
    let locationInView = self.convert(locationInWindow, from: nil)
    print("Mouse down at \(locationInView)")
}

3. NSView 和 CALayer 的關係

wantsLayer 和 layer-backed 檢視

透過設定 wantsLayer,你可以讓 NSView 支援 Core Animation 並使用 CALayer 來管理其內容。這樣可以利用 Core Animation 的各種特性,比如動畫、變換和更高效的離屏渲染。

Objective-C
[view setWantsLayer:YES];
view.layer.backgroundColor = [[NSColor blueColor] CGColor];
Swift
view.wantsLayer = true
view.layer?.backgroundColor = NSColor.blue.cgColor

4. 效能最佳化

檢視層級

避免過於複雜的檢視層級(檢視層次結構越深,重繪時需要更新的區域也越多)。儘量減少不必要的子檢視。

使用 CALayer

  • 快取:可以使用 CALayercontents 屬性來快取繪製內容,減少重複繪製開銷。
  • 離屏渲染:合理使用 shouldRasterize 來進行離屏渲染,提升複雜檢視的渲染效能。
Objective-C
view.layer.shouldRasterize = YES;
view.layer.rasterizationScale = [NSScreen mainScreen].backingScaleFactor;
Swift
view.layer?.shouldRasterize = true
view.layer?.rasterizationScale = NSScreen.main?.backingScaleFactor ?? 1.0

5. 高階自定義繪圖

使用 NSBezierPath

NSBezierPath 是用於描述向量路徑的類,適合用於自定義複雜的繪圖。

Objective-C
- (void)drawRect:(NSRect)dirtyRect {
    [super drawRect:dirtyRect];

    NSBezierPath *path = [NSBezierPath bezierPathWithRect:dirtyRect];
    [[NSColor redColor] setFill];
    [path fill];
}
Swift
override func draw(_ dirtyRect: NSRect) {
    super.draw(dirtyRect)

    let path = NSBezierPath(rect: dirtyRect)
    NSColor.red.setFill()
    path.fill()
}

6. 自定義 NSView 子類

透過自定義 NSView 子類,可以實現更多高階功能。例如,建立一個互動響應的繪圖檢視:

Objective-C
@interface CustomView : NSView
@end

@implementation CustomView

- (instancetype)initWithFrame:(NSRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        [self setWantsLayer:YES];
        self.layer.backgroundColor = [[NSColor whiteColor] CGColor];
    }
    return self;
}

- (void)drawRect:(NSRect)dirtyRect {
    [super drawRect:dirtyRect];
    
    NSBezierPath *path = [NSBezierPath bezierPathWithOvalInRect:dirtyRect];
    [[NSColor blueColor] setFill];
    [path fill];
}

- (void)mouseDown:(NSEvent *)event {
    NSPoint location = [self convertPoint:event.locationInWindow fromView:nil];
    NSLog(@"Mouse clicked at %@", NSStringFromPoint(location));
}
@end
Swift
class CustomView: NSView {
    
    override init(frame frameRect: NSRect) {
        super.init(frame: frameRect)
        self.wantsLayer = true
        self.layer?.backgroundColor = NSColor.white.cgColor
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func draw(_ dirtyRect: NSRect) {
        super.draw(dirtyRect)
        
        let path = NSBezierPath(ovalIn: dirtyRect)
        NSColor.blue.setFill()
        path.fill()
    }
    
    override func mouseDown(with event: NSEvent) {
        let location = self.convert(event.locationInWindow, from: nil)
        print("Mouse clicked at \(location)")
    }
}

相關文章