实现思路:

标识点显示

标识点类型:

enum DotType {
    case corner  // 四个角的点
    case top     // 上边中心对应的点
    case bottom  // 下边中心对应的点
    case right   // 右边中心对应的点
    case left    // 左边中心对应的点
    case none    // 无类型标识点,当没有标识点被选到时的默认值
}

实现思路:

class ClipView: NSView {
    
    var image: NSImage?
    var drawingRect: NSRect?
    var showDots = false  // 是否显示标识点
    var paths: [(NSBezierPath, DotType)] = [] // 记录标识点的path和对应的类型

    override func draw(_ dirtyRect: NSRect) {
        
        super.draw(dirtyRect)

        // Drawing code here.
        guard let image = self.image else {
            return
        }
        var rect = NSIntersectionRect(self.drawingRect!, self.bounds)
        rect = NSIntegralRect(rect)
        image.draw(in: rect, from: rect, operation: .sourceOver, fraction: 1.0)
				
				// 根据当前drawingRect绘制标识点
        if self.showDots {
            self.drawDots(rect: rect)
        }

    }
    
    private func drawDots(rect: NSRect) {
        let radius: CGFloat = 1.5 // 标识点半径
        let dots = self.getDotsCoord(with: rect, radius: radius)  // 获取八个点的坐标
        self.paths = []
        for (dot, type) in dots {
            let path = NSBezierPath(ovalIn: NSRect(origin: dot, size: CGSize(width: radius*2, height: radius*2)))
            self.paths.append((path, type))
            path.lineWidth = 3
            NSColor.white.set()
            path.stroke()
        }
    }
    
    func getDotsCoord(with rect:NSRect, radius: CGFloat) -> [(NSPoint, DotType)] {
        let width = rect.width
        let height = rect.height
				// 为了让标识点的中心落在正确的位置,根据半径提前偏移rect
				// 计算出来的位置是标识点圆圈对应矩阵的左下角(起始坐标)
        let origin = rect.offsetBy(dx: -radius, dy: -radius).origin 
        let dots: [(NSPoint, DotType)] = [
            (origin, .corner),
            (origin.offsetBy(dx: width/2, dy: 0), .bottom),
            (origin.offsetBy(dx: 0, dy: height/2), .left),
            (origin.offsetBy(dx: width, dy: 0), .corner),
            (origin.offsetBy(dx: 0, dy: height), .corner),
            (origin.offsetBy(dx: width, dy: height/2), .right),
            (origin.offsetBy(dx: width/2, dy: height), .top),
            (origin.offsetBy(dx: width, dy: height), .corner)
        ].map({
            (point, type) in
						// 取整
            return (NSPoint(x: Int(point.x), y: Int(point.y)), type)
        })
        return dots
    }   
    
}

区域调整

在Controller中记录当前选择的标识点,并在mouseDown中添加对adjust or drag的判断

class ClipWindowController: NSWindowController {
    
    
    var clipView: ClipView?
		var screenImage: NSImage?
    
    var lastRect: NSRect?  
    var highlightRect: NSRect?  
		var startPoint: NSPoint?     
    var lastPoint: NSPoint?    

		var selectDotType: DotType = .none  // 当前选择标识点的类型,默认为none,可以用于判断是否有选择标识点
    var selectDot: NSPoint = .zero      // 当前选择的标识点,默认为zero
    
		// other functions ....

		override func mouseDown(with event: NSEvent) {
        let location = event.locationInWindow
        switch ClipManager.shared.status {
        case .ready:
            // ...
        case .select:
            guard let rect = self.highlightRect,
                  let view = self.clipView,
                  rect.insetBy(dx: -5, dy: -5).contains(location) // 适当放宽点击判定区域
            else { return }
            for (path, type) in view.paths {
								 // 适当放宽点击判定区域
                if path.bounds.insetBy(dx: -5, dy: -5).contains(location) {
                    self.selectDotType = type
                    self.selectDot = path.bounds.center()
                    break
                }
            }
            if self.selectDotType != .none {
                ClipManager.shared.status = .adjust
            } else {
                ClipManager.shared.status = .drag
            }
            self.lastPoint = location
        default:
            return
        }
    }

		override func mouseUp(with event: NSEvent) {
        switch ClipManager.shared.status {
        case .start:
            // ...
        case .drag, .adjust:
            guard let rect = self.highlightRect else { return }
            self.startPoint = rect.origin
            self.selectDotType = .none
            self.lastRect = rect
            ClipManager.shared.status = .select
        default:
            return
        }
    }

		override func mouseDragged(with event: NSEvent) {
        let location = event.locationInWindow
        switch ClipManager.shared.status {
        case .start:
            // ....
        case .adjust:
            guard let lastRect = self.lastRect,
                  self.selectDotType != .none
            else { break }
            let dx = location.x - self.selectDot.x
            let dy = location.y - self.selectDot.y
            var rect: NSRect = .zero
            switch self.selectDotType {
            case .corner:
                let symPoint = lastRect.symmetricalPoint(point: self.selectDot)
                rect = RectUtil.getRect(aPoint: symPoint, bPoint: location)
            case .top:
                rect = RectUtil.getRect(
                    aPoint: lastRect.origin,
                    bPoint: self.selectDot.offsetBy(dx: lastRect.width/2, dy: dy)
                )
            case .bottom:
                rect = RectUtil.getRect(
                    aPoint: lastRect.origin.offsetBy(dx: 0, dy: lastRect.height),
                    bPoint: self.selectDot.offsetBy(dx: lastRect.width/2, dy: dy)
                )
            case .left:
                rect = RectUtil.getRect(
                    aPoint: lastRect.origin.offsetBy(dx: dx, dy: 0),
                    bPoint: self.selectDot.offsetBy(dx: lastRect.width, dy: lastRect.height/2)
                )
            case .right:
                rect = RectUtil.getRect(
                    aPoint: lastRect.origin,
                    bPoint: self.selectDot.offsetBy(dx: dx, dy: lastRect.height/2)
                )
            
            default:
                break
            }
            self.highlightRect = rect
            self.lastPoint = location
            self.startPoint = rect.origin
            self.highlight()
        case .drag:
            // ...
        default:
            break
        }
    }
}

其中symmetricalPoint是获取rect中某个点关于中心对称的点:

extension NSRect {
    func center() -> NSPoint{
        let origin = self.origin
        return NSPoint(x: origin.x+self.width/2, y: origin.y+self.height/2)
        
    }
    
    func symmetricalPoint(point: NSPoint) -> NSPoint {
        let center = self.center()
				// 转换为整数,放置抖动
        return NSPoint(x: Int(2*center.x-point.x), y: Int(2*center.y-point.y))
    }
}

单屏幕Pin实现 ➡️