如何在NSView中无交互渲染VZVirtualMachineView生成预览?
实现无交互的VZVirtualMachineView网格预览方案
问题背景
需要基于VZVirtualMachineView制作网格形式的虚拟机预览,但直接使用该视图会出现以下问题:
- 捕获鼠标光标并隐藏
- 响应点击、鼠标移动等交互操作
尝试通过vmView.layer?.render(in: context.cgContext)在其他NSView中渲染,但现有代码效果不佳,需要解决:
- 如何正确实现无交互的预览效果
- 采用何种高效的画面刷新机制
- 是否需要将VZVirtualMachineView添加到视图层级
- 能否避免生成图片的方式实现(除非该方式高效)
现有尝试代码
class VMPreview: NSView { let vmView: VZVirtualMachineView var timer: Timer? init(virtualMachine: VZVirtualMachine) { self.vmView = .init() self.vmView.virtualMachine = virtualMachine super.init(frame: .zero) addSubview(self.vmView) dr() } func dr() { self.setNeedsDisplay(bounds) DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { self.dr() } } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override var frame: NSRect { didSet { self.vmView.frame = bounds.offsetBy(dx: 0, dy: bounds.height) } } override func draw(_ dirtyRect: NSRect) { guard let context = NSGraphicsContext.current else { return } vmView.layer?.render(in: context.cgContext) self.setNeedsDisplay(bounds) } }
解决方案
1. 视图层级与交互屏蔽
VZVirtualMachineView必须保留在视图层级中才能正常渲染虚拟机画面(Virtualization框架依赖视图生命周期更新画面),但需要完全屏蔽其交互:
- 自定义子类覆盖
hitTest,让视图不响应任何点击 - 禁用用户交互,拦截光标修改操作
- 将视图移到可视区域外(而非设置
hidden=true,避免系统暂停渲染)
2. 高效刷新机制:CADisplayLink替代Timer
用CADisplayLink替代定时任务,它能和屏幕刷新率同步,既保证画面流畅,又避免过度刷新消耗资源。
3. 优化渲染方式:缓存视图画面
使用bitmapImageRepForCachingDisplay缓存VZVirtualMachineView的画面,比直接渲染图层更高效,利用系统缓存机制减少重复绘制。
完整实现代码
import Cocoa import Virtualization // 自定义无交互的VZVirtualMachineView子类 class NonInteractiveVZVirtualMachineView: VZVirtualMachineView { override func hitTest(_ point: NSPoint) -> NSView? { // 完全屏蔽点击响应 return nil } override func cursorUpdate(with event: NSEvent) { // 强制恢复默认光标,阻止视图修改光标 NSCursor.arrow.set() } } class VMPreview: NSView { private let vmView: NonInteractiveVZVirtualMachineView private var displayLink: CADisplayLink? init(virtualMachine: VZVirtualMachine) { self.vmView = NonInteractiveVZVirtualMachineView() self.vmView.virtualMachine = virtualMachine self.vmView.isUserInteractionEnabled = false self.vmView.wantsLayer = true super.init(frame: .zero) self.wantsLayer = true // 添加到当前视图层级,移到可视区域外避免显示 addSubview(vmView) // 初始化画面刷新机制 setupDisplayLink() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func setupDisplayLink() { displayLink = CADisplayLink(target: self, selector: #selector(updatePreview)) displayLink?.add(to: .main, forMode: .common) } @objc private func updatePreview() { // 标记需要重绘 setNeedsDisplay(bounds) } override var frame: NSRect { didSet { // 保持vmView与预览视图同尺寸,移到可视区域外 vmView.frame = bounds.offsetBy(dx: 0, dy: bounds.height * 2) } } override func draw(_ dirtyRect: NSRect) { super.draw(dirtyRect) guard let context = NSGraphicsContext.current?.cgContext else { return } // 缓存vmView画面并绘制到当前视图 if let rep = vmView.bitmapImageRepForCachingDisplay(in: vmView.bounds) { vmView.cacheDisplay(in: vmView.bounds, to: rep) // 只绘制需要更新的区域,优化性能 rep.draw(in: dirtyRect, from: vmView.bounds.intersection(dirtyRect.offsetBy(dx: 0, dy: -bounds.height * 2)), operation: .copy, fraction: 1.0) } } deinit { // 销毁刷新机制,清理视图 displayLink?.invalidate() vmView.removeFromSuperview() } }
关键注意事项
- 必须保留VZVirtualMachineView在视图层级中,否则虚拟机画面会停止更新
- 不要设置
vmView.hidden = true,部分场景下隐藏视图会被系统暂停渲染 - 网格预览场景下,可以根据窗口可见性暂停/恢复
CADisplayLink,减少资源占用
内容的提问来源于stack exchange,提问作者Damian Dudycz
相关产品推荐
相关产品推荐

