searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

[macOS]通过系统剪切板实现拖放功能

2023-12-04 08:54:44
22
0

在MacOS App中拖放行为通过Dragging Session来管理,我们先来看一下Dragging Session如何定义:

Dragging Session

一个完整的Dragging Session是指从源(Source)拖放到目的地(Destination)的处理过程,其中Source需要实现协议NSDraggingSource,Destination需要实现协议NSDraggingDestination协议。在整个拖放过程中,通过NSPasteboard协助实现数据的交换。

Dragging Session的流程如下:

1. 使用鼠标拖动某一项目时自动创建了一个Dragging Session;

2. 将数据信息同步到NSPasteboard;

3. 在Destination松开,目的App或View决定是否接受;

4. 结束Dragging Session;

实现 Dragging Destination

NSView本身默认遵循了NSDraggingDestination协议,因此只需要Override必要的接口即可:

override func awakeFromNib() {
    setup()
}
// 可以接受哪些类型的资源
var acceptableTypes: Set<String> { return [NSURLPboardType] }

func setup() {
    register(forDraggedTypes: Array(acceptableTypes))
}

如上,接口register(forDraggedTypes:)用于注册NSView支持哪些类型的粘贴板项目。

接下来实现是接受还是拒绝来自Dragging Session的项目:

// 假定只接受图片类型
let filteringOptions = [NSPasteboardURLReadingContentsConformToTypesKey:NSImage.imageTypes()]
 
func shouldAllowDrag(_ draggingInfo: NSDraggingInfo) -> Bool {
 
  var canAccept = false
 
  // 访问粘贴板实例 
  let pasteBoard = draggingInfo.draggingPasteboard()
 
  // 判断是否包含想要的内容
  if pasteBoard.canReadObject(forClasses: [NSURL.self], options: filteringOptions) {
    canAccept = true
  }
  return canAccept
 
}

接下来通过接口draggingEntered(:)实现判定逻辑,如果符合要求就显示图片,否则返回一个空的NSDragOperation():

var isReceivingDrag = false {
  didSet {
    needsDisplay = true
  }
}
 
override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
  let allow = shouldAllowDrag(sender)
  isReceivingDrag = allow
  return allow ? .copy : NSDragOperation()
}

同样的,我们还需要处理draggingExited(:)接口:

override func draggingExited(_ sender: NSDraggingInfo?) {
  isReceivingDrag = false
}

为了能够显示拖拽的发生过程,我们根据isReceivingDrag标志位的变化决定是否显示试图边框:

override func draw(_ dirtyRect: NSRect) {
 
  if isReceivingDrag {
    NSColor.selectedControlColor.set()
 
    let path = NSBezierPath(rect:bounds)
    path.lineWidth = Appearance.lineWidth
    path.stroke()
  }
}

最后当用户在视图内部松开鼠标的时候就是Dragging Session结束的时候,也是最后决定是否接受数据的机会:

override func prepareForDragOperation(_ sender: NSDraggingInfo) -> Bool {
    let allow = shouldAllowDrag(sender)
    return allow
}

当接口返回True的时候,系统会调用接口performDragOperation(_:),我们需要在这个接口内实现真正的接收数据逻辑:

override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
    isReceivingDrag = false
    let pasteBoard = sender.draggingPasteboard()
    let point = convert(sender.draggingLocation(), from: nil)
    
    if let urls = pasteBoard.readObjects(forClasses: [NSURL.self], options: filteringOptions) as? [URL], urls.count > 0 {
      // 这里的urls就是图片资源的地址,我们可以通过一个Delegate来实现处理逻辑
      delegate?.processImageURLs(urls, center: point)
      return true
    }
    
    return false
}

Delegate的处理逻辑通过视图的对应Controller实现:

@IBOutlet var topLayer: DestinationView!
@IBOutlet var targetLayer: NSView!

override func viewDidLoad() {
    super.viewDidLoad()
    // 设置视图的Delegate
    topLayer.delegate = self
}

通过Extension实现Delegate的协议接口:

extension StickerBoardViewController: DestinationViewDelegate {
    func processImageURLs(_ urls: [URL], center: NSPoint) {
    for (index, url) in urls.enumerated() {
      if let image = NSImage(contentsOf: url) {
        var newCenter = center
        
        if index > 0 {
          newCenter = center.addRandomNoise(Appearance.randomNoise)
        }
        
        processImage(image, center: newCenter)
      }
    }
  }
  
  func processImage(_ image: NSImage, center: NSPoint) {
    
    // 显示接收到的图片
    let constrainedSize = image.aspectFitSizeForMaxDimension(Appearance.maxStickerDimension)
    let subview = NSImageView(frame: NSRect(x: center.x - constrainedSize.width/2, y: center.y - constrainedSize.height/2, width: constrainedSize.width, height: constrainedSize.height))
    
    subview.image = image
    targetLayer.addSubview(subview)
    let maxrotation = CGFloat(arc4random_uniform(Appearance.maxRotation)) - Appearance.rotationOffset
    subview.frameCenterRotation = maxrotation
  }
}
0条评论
0 / 1000
l****n
14文章数
1粉丝数
l****n
14 文章 | 1 粉丝
原创

[macOS]通过系统剪切板实现拖放功能

2023-12-04 08:54:44
22
0

在MacOS App中拖放行为通过Dragging Session来管理,我们先来看一下Dragging Session如何定义:

Dragging Session

一个完整的Dragging Session是指从源(Source)拖放到目的地(Destination)的处理过程,其中Source需要实现协议NSDraggingSource,Destination需要实现协议NSDraggingDestination协议。在整个拖放过程中,通过NSPasteboard协助实现数据的交换。

Dragging Session的流程如下:

1. 使用鼠标拖动某一项目时自动创建了一个Dragging Session;

2. 将数据信息同步到NSPasteboard;

3. 在Destination松开,目的App或View决定是否接受;

4. 结束Dragging Session;

实现 Dragging Destination

NSView本身默认遵循了NSDraggingDestination协议,因此只需要Override必要的接口即可:

override func awakeFromNib() {
    setup()
}
// 可以接受哪些类型的资源
var acceptableTypes: Set<String> { return [NSURLPboardType] }

func setup() {
    register(forDraggedTypes: Array(acceptableTypes))
}

如上,接口register(forDraggedTypes:)用于注册NSView支持哪些类型的粘贴板项目。

接下来实现是接受还是拒绝来自Dragging Session的项目:

// 假定只接受图片类型
let filteringOptions = [NSPasteboardURLReadingContentsConformToTypesKey:NSImage.imageTypes()]
 
func shouldAllowDrag(_ draggingInfo: NSDraggingInfo) -> Bool {
 
  var canAccept = false
 
  // 访问粘贴板实例 
  let pasteBoard = draggingInfo.draggingPasteboard()
 
  // 判断是否包含想要的内容
  if pasteBoard.canReadObject(forClasses: [NSURL.self], options: filteringOptions) {
    canAccept = true
  }
  return canAccept
 
}

接下来通过接口draggingEntered(:)实现判定逻辑,如果符合要求就显示图片,否则返回一个空的NSDragOperation():

var isReceivingDrag = false {
  didSet {
    needsDisplay = true
  }
}
 
override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
  let allow = shouldAllowDrag(sender)
  isReceivingDrag = allow
  return allow ? .copy : NSDragOperation()
}

同样的,我们还需要处理draggingExited(:)接口:

override func draggingExited(_ sender: NSDraggingInfo?) {
  isReceivingDrag = false
}

为了能够显示拖拽的发生过程,我们根据isReceivingDrag标志位的变化决定是否显示试图边框:

override func draw(_ dirtyRect: NSRect) {
 
  if isReceivingDrag {
    NSColor.selectedControlColor.set()
 
    let path = NSBezierPath(rect:bounds)
    path.lineWidth = Appearance.lineWidth
    path.stroke()
  }
}

最后当用户在视图内部松开鼠标的时候就是Dragging Session结束的时候,也是最后决定是否接受数据的机会:

override func prepareForDragOperation(_ sender: NSDraggingInfo) -> Bool {
    let allow = shouldAllowDrag(sender)
    return allow
}

当接口返回True的时候,系统会调用接口performDragOperation(_:),我们需要在这个接口内实现真正的接收数据逻辑:

override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
    isReceivingDrag = false
    let pasteBoard = sender.draggingPasteboard()
    let point = convert(sender.draggingLocation(), from: nil)
    
    if let urls = pasteBoard.readObjects(forClasses: [NSURL.self], options: filteringOptions) as? [URL], urls.count > 0 {
      // 这里的urls就是图片资源的地址,我们可以通过一个Delegate来实现处理逻辑
      delegate?.processImageURLs(urls, center: point)
      return true
    }
    
    return false
}

Delegate的处理逻辑通过视图的对应Controller实现:

@IBOutlet var topLayer: DestinationView!
@IBOutlet var targetLayer: NSView!

override func viewDidLoad() {
    super.viewDidLoad()
    // 设置视图的Delegate
    topLayer.delegate = self
}

通过Extension实现Delegate的协议接口:

extension StickerBoardViewController: DestinationViewDelegate {
    func processImageURLs(_ urls: [URL], center: NSPoint) {
    for (index, url) in urls.enumerated() {
      if let image = NSImage(contentsOf: url) {
        var newCenter = center
        
        if index > 0 {
          newCenter = center.addRandomNoise(Appearance.randomNoise)
        }
        
        processImage(image, center: newCenter)
      }
    }
  }
  
  func processImage(_ image: NSImage, center: NSPoint) {
    
    // 显示接收到的图片
    let constrainedSize = image.aspectFitSizeForMaxDimension(Appearance.maxStickerDimension)
    let subview = NSImageView(frame: NSRect(x: center.x - constrainedSize.width/2, y: center.y - constrainedSize.height/2, width: constrainedSize.width, height: constrainedSize.height))
    
    subview.image = image
    targetLayer.addSubview(subview)
    let maxrotation = CGFloat(arc4random_uniform(Appearance.maxRotation)) - Appearance.rotationOffset
    subview.frameCenterRotation = maxrotation
  }
}
文章来自个人专栏
文章 | 订阅
0条评论
0 / 1000
请输入你的评论
0
0