如何自定义NSTableView或NSOutlineView的拖放插入标记?
自定义NSOutlineView拖放插入标记的解决方案
好问题!我之前在做NSOutlineView拖放自定义的时候也遇到过一模一样的情况——系统自带的蓝色圆形+条的插入标记,确实没有单独暴露重写绘制逻辑的入口。下面给你两种可行的解决思路,其中最稳妥的是完全手动实现拖放反馈:
一、最可靠的方案:设置draggingDestinationFeedbackStyle = .none,手动实现所有反馈
系统的拖放反馈(包括行上高亮和插入标记)是绑定在一起的,如果你想自定义其中某一部分,最安全的方式是完全接管所有反馈逻辑,具体步骤如下:
关闭系统默认反馈
首先把outlineView的拖放反馈样式设为none,这样系统就不会绘制任何默认的高亮或标记了:outlineView.draggingDestinationFeedbackStyle = .none保留行上放置的自定义高亮
你之前重写NSTableRowView.drawDraggingDestinationFeedback的逻辑依然可以用,但需要手动触发高亮状态的更新。比如给自定义的TableRowView加一个isHighlightedForDrop属性,在拖放过程中主动设置这个属性并触发重绘:class CustomTableRowView: NSTableRowView { var isHighlightedForDrop = false override func draw(_ dirtyRect: NSRect) { super.draw(dirtyRect) if isHighlightedForDrop { // 绘制你自定义的行上高亮效果 NSColor.systemBlue.withAlphaComponent(0.2).setFill() bounds.fill() } } }自定义插入标记视图
创建一个自定义的NSView来绘制你想要的插入标记样式,比如替换默认的蓝色条+圆形:class CustomDropIndicatorView: NSView { override func draw(_ dirtyRect: NSRect) { super.draw(dirtyRect) // 绘制自定义标记:比如橙色的条+方形 let indicatorColor = NSColor.systemOrange indicatorColor.setStroke() // 绘制中间的竖条 let linePath = NSBezierPath() linePath.move(to: NSPoint(x: bounds.midX, y: 0)) linePath.line(to: NSPoint(x: bounds.midX, y: bounds.height)) linePath.lineWidth = 2 linePath.stroke() // 绘制两端的方形 let squareSize: CGFloat = 6 let topSquareRect = NSRect(x: bounds.midX - squareSize/2, y: bounds.height - squareSize, width: squareSize, height: squareSize) let bottomSquareRect = NSRect(x: bounds.midX - squareSize/2, y: 0, width: squareSize, height: squareSize) NSBezierPath(rect: topSquareRect).stroke() NSBezierPath(rect: bottomSquareRect).stroke() } }在拖放代理中管理标记视图和高亮状态
通过NSOutlineView的拖放代理方法,跟踪当前拖放位置,动态添加/移除自定义标记视图,并更新行的高亮状态:var dropIndicatorView: CustomDropIndicatorView? func outlineView(_ outlineView: NSOutlineView, validateDrop info: NSDraggingInfo, proposedItem item: Any?, proposedChildIndex index: Int) -> NSDragOperation { // 先清理之前的反馈 dropIndicatorView?.removeFromSuperview() dropIndicatorView = nil resetAllRowHighlights(outlineView) // 区分行上放置和行之间放置 if index == NSOutlineViewDropOnItemIndex { // 行上放置:高亮对应的行 if let row = outlineView.row(forItem: item), let rowView = outlineView.rowView(atRow: row, makeIfNecessary: false) as? CustomTableRowView { rowView.isHighlightedForDrop = true rowView.setNeedsDisplay() } } else { // 行之间放置:添加自定义标记视图 let indicatorHeight: CGFloat = 16 var indicatorFrame = NSRect(x: 0, y: 0, width: outlineView.bounds.width, height: indicatorHeight) // 根据插入位置计算标记的Y坐标 if let parentItem = item { let parentRow = outlineView.row(forItem: parentItem) if let parentRowView = outlineView.rowView(atRow: parentRow, makeIfNecessary: false) { if index == 0 { // 插入到父项第一个子元素上方 indicatorFrame.origin.y = parentRowView.frame.origin.y - indicatorHeight/2 } else { // 插入到父项第index个元素下方 if let childItem = outlineView.child(index - 1, ofItem: parentItem), let childRow = outlineView.row(forItem: childItem), let childRowView = outlineView.rowView(atRow: childRow, makeIfNecessary: false) { indicatorFrame.origin.y = childRowView.frame.maxY - indicatorHeight/2 } } } } else { // 插入到根节点位置 if index == 0 { indicatorFrame.origin.y = indicatorHeight/2 } else { let lastRow = outlineView.numberOfRows - 1 if let lastRowView = outlineView.rowView(atRow: lastRow, makeIfNecessary: false) { indicatorFrame.origin.y = lastRowView.frame.maxY - indicatorHeight/2 } } } // 添加标记视图到outlineView let indicatorView = CustomDropIndicatorView(frame: indicatorFrame) outlineView.addSubview(indicatorView) dropIndicatorView = indicatorView } return .copy // 根据你的需求返回对应的拖放操作类型 } // 辅助方法:重置所有行的高亮状态 private func resetAllRowHighlights(_ outlineView: NSOutlineView) { for row in 0..<outlineView.numberOfRows { if let rowView = outlineView.rowView(atRow: row, makeIfNecessary: false) as? CustomTableRowView { rowView.isHighlightedForDrop = false rowView.setNeedsDisplay() } } } // 拖放退出时清理反馈 func outlineView(_ outlineView: NSOutlineView, draggingExited info: NSDraggingInfo?) { dropIndicatorView?.removeFromSuperview() dropIndicatorView = nil resetAllRowHighlights(outlineView) } // 拖放完成后清理反馈 func outlineView(_ outlineView: NSOutlineView, performDrop info: NSDraggingInfo, item: Any?, childIndex index: Int) -> Bool { dropIndicatorView?.removeFromSuperview() dropIndicatorView = nil resetAllRowHighlights(outlineView) // 这里处理你的实际拖放逻辑 return true }
二、不推荐的技巧:修改系统私有视图(风险高)
如果你不想完全手动实现,理论上可以通过遍历outlineView的子视图找到系统的插入标记视图(比如私有类NSDropIndicatorView),然后修改它的外观。但这种方法非常不推荐:
- 依赖苹果的私有API,随时可能在系统更新后失效
- 违反App Store审核规则,可能导致应用被拒
所以除非是内部调试应用,否则不建议使用这种方法。
内容的提问来源于stack exchange,提问作者Giles
相关产品推荐
相关产品推荐

