macOS Cocoa开发:NSWindow调整大小时如何等比例缩放全部内容
解决方案
核心思路是放弃用约束/Autoresizing动态调整各元素尺寸位置,直接对整个内容层级施加仿射缩放变换,所有元素会自动保持相对比例和位置,仅整体缩放,无需单独适配子视图、绘制内容和CAEmitterLayer。
具体实现步骤
- 新增一个固定尺寸的内容容器视图,所有需要缩放的内容(包括你的子视图、CAEmitterLayer)都放到该容器内,容器初始尺寸与你原始基准尺寸一致(600x500)
- 重写父视图的
layout()方法,每次窗口尺寸变化时计算当前缩放比例,对容器施加缩放变换,并将容器居中显示 - 子视图内的所有硬编码边距、尺寸、CAEmitterLayer位置都使用原始基准尺寸下的固定值/相对比例即可,无需动态修改
修改后的完整代码
import Cocoa // 全局基准尺寸常量,所有原始布局都基于该尺寸计算 let kBaseContentSize = CGSize(width: 600, height: 500) let kSideMargin: CGFloat = 20 class MyViewController: NSViewController { public init () { super.init(nibName: nil, bundle: nil) self.view = MyView(frame: NSRect(origin: .zero, size: kBaseContentSize)) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } class MyView: NSView { // 内容容器,所有需要缩放的内容都放在这里 private let contentContainer = NSView(frame: NSRect(origin: .zero, size: kBaseContentSize)) override init(frame frameRect: NSRect) { super.init(frame: frameRect) addSubview(contentContainer) // 容器不要加任何自适应规则,尺寸永远固定为基准尺寸,仅通过transform缩放 contentContainer.translatesAutoresizingMaskIntoConstraints = true contentContainer.autoresizingMask = [] // 添加子视图,所有布局都用基准尺寸下的固定值 let subView = MySubView(frame: NSRect( x: 0, y: kBaseContentSize.height / 3, width: kBaseContentSize.width, height: kBaseContentSize.height / 10 )) contentContainer.addSubview(subView) // 子视图也不需要自适应,尺寸固定 subView.autoresizingMask = [] } override func layout() { super.layout() // 计算等比例缩放系数,保持原始宽高比 let scale = min(bounds.width / kBaseContentSize.width, bounds.height / kBaseContentSize.height) // 施加缩放变换 contentContainer.transform = .init(scaleX: scale, y: scale) // 容器居中显示 contentContainer.frame.origin = CGPoint( x: (bounds.width - kBaseContentSize.width * scale) / 2, y: (bounds.height - kBaseContentSize.height * scale) / 2 ) } required init?(coder decoder: NSCoder) { fatalError("init(coder:) has not been implemented") } } class MySubView: NSView { // 如果有用到CAEmitterLayer,直接存在相对比例即可,不需要动态修改位置 var emitterPositionRatio: CGPoint = .zero { didSet { // 仅在比例变化时更新位置,用基准尺寸计算,不需要每次缩放都调用 emitterLayer.position = CGPoint( x: kBaseContentSize.width * emitterPositionRatio.x, y: bounds.height * emitterPositionRatio.y ) } } private let emitterLayer = CAEmitterLayer() override init(frame frameRect: NSRect) { super.init(frame: frameRect) // 挂载CAEmitterLayer示例 wantsLayer = true emitterLayer.frame = bounds layer?.addSublayer(emitterLayer) } override func draw(_ dirtyRect: NSRect) { let context = NSGraphicsContext.current!.cgContext context.setStrokeColor(.black) context.setFillColor(CGColor(red: 0.7, green: 0.7, blue: 0.7, alpha: 1)) context.setLineWidth(2) // 所有边距、尺寸都用基准尺寸下的固定值,缩放时自动变换 let borderRect = CGRect( x: kSideMargin, y: 5, width: bounds.width - 2 * kSideMargin, height: bounds.height - 10 ) let roundedRect = CGPath(roundedRect: borderRect, cornerWidth: 5, cornerHeight: 5, transform: nil) context.addPath(roundedRect) context.fillPath() context.strokePath() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } }
关键说明
- 缩放变换会作用于整个视图层级的所有元素,包括子视图、自定义绘制内容、子Layer,所有相对比例和位置完全保持原始状态,不需要单独适配
- CAEmitterLayer的位置只需存储相对基准尺寸的比例,仅在用户调整时更新一次即可,窗口缩放时会自动跟随变换,不需要在
draw()中重复计算 - 如果不需要保持原始宽高比,允许窗口拉伸时内容不等比例缩放,直接把缩放系数拆分为
scaleX和scaleY分别传入变换即可
内容的提问来源于stack exchange,提问作者MassMover
相关产品推荐
相关产品推荐

