TextKit 2中绘制复选框及文本缩进与光标控制问题求助
TextKit 2 实现复选框与段落文本共存的最优方案
方案一:NSTextAttachment + 段落样式缩进(推荐)
这种方式利用TextKit原生附件支持,无需自定义NSTextLayoutFragment,能自动适配光标、退格等交互逻辑。
1. 自定义复选框附件
class CheckBoxAttachment: NSTextAttachment { var isChecked: Bool = false override func draw(with bounds: CGRect, characterIndex charIndex: Int, in context: CGContext, layoutManager: NSLayoutManager) { context.saveGState() defer { context.restoreGState() } // 绘制复选框边框 let boxRect = bounds.insetBy(dx: 2, dy: 2) context.setStrokeColor(NSColor.labelColor.cgColor) context.setLineWidth(1.5) context.stroke(boxRect) // 绘制选中状态的对勾 if isChecked { context.setFillColor(NSColor.labelColor.cgColor) let checkPath = NSBezierPath() checkPath.move(to: CGPoint(x: boxRect.minX + 3, y: boxRect.midY)) checkPath.line(to: CGPoint(x: boxRect.midX, y: boxRect.maxY - 3)) checkPath.line(to: CGPoint(x: boxRect.maxX - 3, y: boxRect.minY + 3)) checkPath.fill() } } // 固定附件尺寸,让复选框与文本基线对齐 override var bounds: CGRect { get { let size = CGSize(width: 18, height: 18) return CGRect(origin: CGPoint(x: 0, y: -2), size: size) } set { super.bounds = newValue } } }
2. 配置段落缩进实现文本对齐
为包含复选框的段落设置首行缩进,确保文本在复选框右侧显示:
func createCheckListParagraphStyle() -> NSParagraphStyle { let style = NSMutableParagraphStyle() // 缩进值 = 复选框宽度 + 间距(18+4=22) style.headIndent = 22 style.firstLineHeadIndent = 22 return style.copy() as! NSParagraphStyle }
3. 插入复选框到文本
将附件作为富文本属性插入段落开头:
func insertCheckBox(at range: NSRange, in textView: NSTextView) { let attachment = CheckBoxAttachment() let attrAttachment = NSAttributedString(attachment: attachment) let mutableAttr = NSMutableAttributedString(attributedString: attrAttachment) mutableAttr.append(NSAttributedString(string: " ")) // 添加空格分隔复选框与文本 mutableAttr.addAttribute(.paragraphStyle, value: createCheckListParagraphStyle(), range: NSRange(location: 0, length: mutableAttr.length)) textView.textStorage?.insert(mutableAttr, at: range.location) }
交互逻辑处理
- 光标位置:默认光标会自动定位在附件后的文本区域,符合需求。
- 退格限制:禁止插入点移到复选框左侧,可通过
NSTextViewDelegate拦截:
func textView(_ textView: NSTextView, shouldChangeTextIn range: NSRange, replacementString string: String?) -> Bool { let textStorage = textView.textStorage! if range.location == 0, textStorage.attribute(.attachment, at: 0, effectiveRange: nil) is CheckBoxAttachment { return false } return true }
方案二:NSLayoutManager 代理绘制(适合复选框作为段落样式)
如果不需要复选框随文本复制,可通过布局管理器代理在段落前绘制复选框,同时调整文本排版区域:
1. 标记目标段落
给需要显示复选框的段落添加自定义属性标记:
let CheckListParagraphAttribute = NSAttributedString.Key(rawValue: "CheckListParagraph") // 给指定段落添加标记 textStorage?.addAttribute(CheckListParagraphAttribute, value: true, range: targetParagraphRange)
2. 实现布局管理器代理方法
func layoutManager(_ layoutManager: NSLayoutManager, shouldSetLineFragmentRect lineFragmentRect: UnsafeMutablePointer<CGRect>, lineFragmentUsedRect: UnsafeMutablePointer<CGRect>, baselineOffset: UnsafeMutablePointer<CGFloat>, in textContainer: NSTextContainer, forGlyphRange glyphRange: NSRange) { let charRange = layoutManager.characterRange(forGlyphRange: glyphRange, actualGlyphRange: nil) guard let isCheckList = layoutManager.textStorage?.attribute(CheckListParagraphAttribute, at: charRange.location, effectiveRange: nil) as? Bool, isCheckList else { return } // 调整文本区域,留出复选框空间 lineFragmentRect.pointee.origin.x += 22 lineFragmentRect.pointee.size.width -= 22 lineFragmentUsedRect.pointee.origin.x += 22 lineFragmentUsedRect.pointee.size.width -= 22 } func layoutManager(_ layoutManager: NSLayoutManager, drawGlyphsForGlyphRange glyphsToShow: NSRange, at origin: CGPoint) { // 先绘制默认文本 layoutManager.drawGlyphs(forGlyphRange: glyphsToShow, at: origin) // 绘制复选框 let charRange = layoutManager.characterRange(forGlyphRange: glyphsToShow, actualGlyphRange: nil) guard let isCheckList = layoutManager.textStorage?.attribute(CheckListParagraphAttribute, at: charRange.location, effectiveRange: nil) as? Bool, isCheckList else { return } let firstGlyphIndex = glyphsToShow.location let lineRect = layoutManager.lineFragmentRect(forGlyphAt: firstGlyphIndex, effectiveRange: nil) let checkBoxRect = CGRect(x: origin.x + 2, y: origin.y + lineRect.origin.y + (lineRect.height - 18)/2, width: 18, height: 18) // 绘制复选框边框与对勾(代码同方案一的draw逻辑) let context = NSGraphicsContext.current!.cgContext context.saveGState() context.setStrokeColor(NSColor.labelColor.cgColor) context.setLineWidth(1.5) context.stroke(checkBoxRect.insetBy(dx: 2, dy: 2)) context.restoreGState() }
交互逻辑处理
- 光标位置:文本排版区域已偏移,光标自动停在复选框右侧的文本起始位置。
- 退格行为:复选框不属于文本内容,退格清空文本后,插入点自然停留在段落的文本起始位置,无需额外处理。
内容的提问来源于stack exchange,提问作者FE_Tech
相关产品推荐
相关产品推荐

