为何NSAttributedString在NSRulerView中随NSTextView滚动,NSRect却不?
问题分析与解决方案
这个问题我之前也碰到过,核心原因是NSRulerView中绘制NSRect时没有结合文本视图的滚动偏移和坐标系转换,而行号文本的绘制逻辑已经适配了滚动,所以才会出现两者行为不一致的情况。
为什么行号文本会跟随滚动,而NSRect不会?
1. 行号文本的绘制逻辑适配了滚动
你在绘制行号时,应该是通过NSTextView的layoutManager获取当前可见区域对应的行,然后根据每行的lineFragmentRect计算在NSRulerView中的位置。这个过程中,你会自动结合文本视图的documentVisibleRect(可见区域)的偏移量,所以每次滚动触发NSRulerView重绘时,行号的位置都会重新计算,自然会跟着滚动走。
2. 手动绘制的NSRect未考虑滚动偏移
如果你的NSRect是基于某个固定的y值绘制,或者没有将文本行的位置转换到NSRulerView的坐标系中,它就会固定在NSRulerView的静态bounds位置里,不会随文本视图的滚动更新位置。
举个反例:如果直接写这样的代码,背景Rect肯定不会滚动:
// 错误示例:固定y值,未考虑滚动 let badBackgroundRect = NSRect(x: 0, y: 50, width: GUTTER_WIDTH, height: 20) badBackgroundRect.fill()
解决方法:动态计算选中行背景Rect的位置
你需要把选中行在文本视图中的位置,转换到NSRulerView的坐标系中,同时结合文本视图的滚动偏移,才能让背景Rect跟随滚动。
修改你的绘制代码,参考以下逻辑:
override func drawHashMarksAndLabels(in rect: NSRect) { super.drawHashMarksAndLabels(in: rect) guard let textView = clientView as? NSTextView else { return } let layoutManager = textView.layoutManager // 1. 获取选中范围对应的行 let selectedRange = textView.selectedRange() let lineRange = layoutManager.lineRange(for: selectedRange) // 2. 获取该行在文本视图中的frame let lineRect = layoutManager.lineFragmentRect(forGlyphAt: lineRange.location, effectiveRange: nil) // 3. 将文本视图中的行frame转换到NSRulerView的坐标系 let convertedRect = textView.convert(lineRect, to: self) // 4. 绘制选中行的背景Rect(宽度用你的GUTTER_WIDTH) let backgroundRect = NSRect( x: 0, y: convertedRect.origin.y, width: GUTTER_WIDTH, height: convertedRect.height ) // 设置背景色并填充 NSColor.systemBlue.withAlphaComponent(0.2).setFill() backgroundRect.fill() // 后续绘制行号文本的逻辑保持不变... }
关键知识点
- macOS的坐标系是左下角为原点,所以转换坐标系时要注意视图层级的位置关系。
convert(_:to:)方法会帮你自动处理不同视图之间的坐标偏移,包括滚动带来的位置变化。- NSRulerView的
drawHashMarksAndLabels(in:)会在文本视图滚动、内容变化时自动触发重绘,所以只要每次重绘时都动态计算位置,就能保证背景Rect和行号同步滚动。
内容的提问来源于stack exchange,提问作者Josef Zoller
相关产品推荐
相关产品推荐

