[Swift4/Cocoa] ドラッグでサイズを変更可能なNSViewを作る

2018年7月5日(更新: 2018年7月5日)

特定の領域をマウスでドラッグすることでサイズを変更できる NSView のサンプルです。

NSView をオーバーライドしてカスタムしたクラス ResizableView を作ります。実際の動作は以下の様になります。

ドラッグでリサイズ可能なNSView

ドラッグして左右にバーを伸び縮みさせることができます。

サンプルのソースコード

Cocoa Application のプロジェクトを新規作成し、生成される ViewController.swift を以下のように変更します。

import Cocoa

class ViewController: NSViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let resizableView = ResizableView(frame: NSRect(x: 50, y: 100, width: 100, height: 50))
        self.view.addSubview(resizableView)
    }
}

// ドラッグでリサイズ可能な NSView のクラス
class ResizableView: NSView {
    
    let MAX_WIDTH: CGFloat = 300
    let MIN_WIDTH: CGFloat = 50
    var isResizeMode: Bool = false
    var draggableArea: NSRect!
    var trackingTag: TrackingRectTag?
    
    override init(frame frameRect: NSRect) {
        super.init(frame: frameRect)
        
        self.updateDraggableArea()
    }
    
    required init?(coder decoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func updateDraggableArea() {
        
        // 古い領域を削除
        if let oldTag = self.trackingTag {
           self.removeTrackingRect(oldTag)
        }
        
        // 右端をドラッグ可能な領域に設定
        draggableArea = NSRect(x: self.frame.width - 10, y: 0, width: 10, height: self.frame.height)
        
        // mouseEntered の対象となる領域を設定
        self.trackingTag = self.addTrackingRect(draggableArea, owner: self, userData: nil, assumeInside: false)
    }
    
    override func mouseEntered(with event: NSEvent) {
        // ドラッグ可能領域内ではマウスカーソルを矢印に変更
        self.addCursorRect(draggableArea, cursor: NSCursor.resizeLeftRight)
    }
    
    override func mouseDown(with event: NSEvent) {
        // クリック位置の座標系を NSWindow からこの NSView に変換
        let clickPointInView = self.convert(event.locationInWindow, from: nil)
        print(clickPointInView)
   
        // クリック位置がドラッグ可能領域内だった場合
        if draggableArea.contains(clickPointInView) {
            print("resize mode start")
            isResizeMode = true
        }
    }
    
    override func mouseDragged(with event: NSEvent) {
        // ドラッグでサイズを変更
        if isResizeMode {
            self.frame.size.width = self.getWidth(self.frame.width + event.deltaX)
            self.updateDraggableArea()
        }
    }
    
    override func mouseUp(with event: NSEvent) {
        // リサイズ終了
        print("resize mode end")
        isResizeMode = false
        self.updateDraggableArea()
    }
    
    func getWidth(_ width: CGFloat) -> CGFloat {
        // 大きさの補正
        if width >= MAX_WIDTH {
            return MAX_WIDTH
        }
        
        if width <= MIN_WIDTH {
            return MIN_WIDTH
        }
        
        return width
    }
    
    override func acceptsFirstMouse(for event: NSEvent?) -> Bool {
        // ウィンドウが非アクティブ時にもクリックでマウスイベントを反応させる
        return true
    }
    
    override func draw(_ dirtyRect: NSRect) {
        super.draw(dirtyRect)
        
        // 色付け
        NSColor.red.setFill()
        dirtyRect.fill()
        
        NSColor.orange.setFill()
        draggableArea.fill()
    }
}

ソースコードの詳細

ドラッグ可能領域の設定

今回のサンプルでは NSView の右端の部分をドラッグ可能としています。その情報を格納する変数は draggableArea で、メソッド updateDraggableArea の中で設定しています。

// 右端をドラッグ可能な領域に設定
draggableArea = NSRect(x: self.frame.width - 10, y: 0, width: 10, height: self.frame.height)

この領域(NSRect)にマウスが重なったことを感知するために NSView のメソッド addTrackingRect を使用します。

        
// mouseEntered の対象となる領域を設定
self.trackingTag = self.addTrackingRect(draggableArea, owner: self, userData: nil, assumeInside: false)

これによって、マウスがこの領域に入った際にメソッド mouseEntered が呼び出されるようになります。

また addTrackingRect は、戻り値としてタグ(TrackingRectTag)を出力します。このタグは NSView のサイズが変わり、それに伴いドラッグ可能な領域の位置が変わった際に古いドラッグ領域を削除するために使用します。

// 古い領域を削除
if let oldTag = self.trackingTag {
   self.removeTrackingRect(oldTag)
}

マウスカーソルの画像を変更

マウスがドラッグ可能な領域に入ったことが分かるように、領域内ではマウスカーソルを左右の矢印マークに変更します。これはメソッド addCursorRect で行います。

override func mouseEntered(with event: NSEvent) {
    // ドラッグ可能領域内ではマウスカーソルを矢印に変更
    self.addCursorRect(draggableArea, cursor: NSCursor.resizeLeftRight)
}

ドラッグによるリサイズの処理

まず NSView 上でマウスがクリックされた際に呼び出されるメソッド mouseDown の中で、そのクリック位置がドラッグ可能領域内にあるかどうかを判定します。

mouseDown の引数である event には、プロパティ locationInWindow があり、これはウィンドウ全体のどの位置にマウスのクリックが発生したかの座標情報を持ちます。この座標はウィンドウの左端が原点であるため、これを対象となる NSView の左端が原点となるように convert によって変更します。

let clickPointInView = self.convert(event.locationInWindow, from: nil)

この座標が領域内であった場合に、ドラッグによるリサイズ可能であるというフラグを立てます。

if draggableArea.contains(clickPointInView) {
    isResizeMode = true
}

このフラグが有効である間、ドラッグされた距離によって NSView の幅を変更します。この際、ドラッグ可能領域も同時に更新します。

override func mouseDragged(with event: NSEvent) {
    // ドラッグでサイズを変更
    if isResizeMode {
        self.frame.size.width = self.getWidth(self.frame.width + event.deltaX)
        self.updateDraggableArea()
    }
}

マウスのクリック状態を解除された際に、ドラッグ可能フラグを下ろします。ドラッグ可能領域も更新します。

override func mouseUp(with event: NSEvent) {
    // リサイズ終了
    isResizeMode = false
    self.updateDraggableArea()
}

以上、ドラッグでリサイズ可能な NSView のクラスの実装方法でした。

応用すると NSSlider の様に、長さによって値を設定するUIの様な使い方ができます。

コメントを残す

メールアドレスが公開されることはありません。