要理解 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() // 如果需要的話,立即進行繪製
使用 NSGraphicsContext
和 CGContext
在 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
- 快取:可以使用
CALayer
的contents
屬性來快取繪製內容,減少重複繪製開銷。 - 離屏渲染:合理使用
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)")
}
}