Cocoa:给 NSTableView 加上右键菜单

2017-06-05 刘枭

可以说几乎每个应用都需要用到 TableView ,iOS 上如此 macOS 也不例外。因为桌面应用支持鼠标操作,所以当需要操作某一行的内容时,最常见的就是右键弹出菜单,选择需要的操作。开发一个 macOS 应用,这是最基本也是一定会遇到的需求。下图是 Things 3 任务列表的右键菜单。

但是在我的印象中,加个右键菜单并不容易,这个印象来自 12 年在公司做  doit.im  的 Mac 客户端,当时也是要做 Things 这样的操作菜单。那时候还是 Cocoa 开发萌新,Cocoa 的资料又是出了名的少,各种 Google 和 Stackoverflow,找到了合适的方案来做,一直到最近我在开发一个书签管理应用,我都还是沿用之前的办法,不是很难,但是有点绕。今天下午偶然看了一下 NSMenuDelegate 的 API,说出无数声卧槽后想出了最合适和最简单的方法来加这个菜单。分享给大家面的走弯路,或者也有可能就我走了这个弯路。

先回忆一下旧办法

旧方法核心是继承 NSTableView ,因为 NSView 有方法 - (nullable NSMenu *)menuForEvent:(NSEvent *)event; NSTableViewNSView 的子类,所以我们继承后可以 override 这个方法。里面通过 delegate 方法来问 TableView 的 delegate(一般是 ViewController)要一个 Menu 回来,直接看我 Pinbox 的代码吧,比较古老还是 OC 写的。

- (NSMenu *)menuForEvent:(NSEvent*)event
{
    NSPoint location = [self convertPoint:[event locationInWindow] fromView:nil];
    NSInteger row = [self rowAtPoint:location];

    if (row < 0 || ([event type] != NSRightMouseDown)) {
        return [super menuForEvent:event];
    }

    NSIndexSet *rowIndex = [NSIndexSet indexSetWithIndex:row];
    [self selectRowIndexes:rowIndex byExtendingSelection:NO];

    if ([self.delegate respondsToSelector:@selector(tableView:menuForRows:)]) {
        return [self.delegate performSelector:@selector(tableView:menuForRows:)
                                   withObject:self
                                   withObject:rowIndex];
    }

    return [super menuForEvent:event];
}

每一个 Cell 的 Menu 很可能是不一样的,比如书签加星操作,有时是 Star 有时是 Unstar,所以这里 Menu Item 是动态的。代码里通过点击事件的位置知道了点击在哪一行上,然后通过代码选中这一行,最后通过代理方法传过去选中的 index,View Controller 拿到 index 后结合 cell 对应的 Object 来返回对应的 Menu。

更合理的方式

就这么过了几年,我写过的全部应用都是这么实现的,直到今天下午。先看两个 API,事情就很直观了。

// NSResponder
open var menu: NSMenu?
..
// NSMenuDelegate
optional public func menuNeedsUpdate(_ menu: NSMenu)

NSTableView  的继承关系是 NSTableView - NSControl - NSView - NSResponder,所以我们可以直接给 tableView 设置一个 menu 就好了。如果要解决上面说的动态 item 问题,只需要实现 menu 的 delegate,在 menu need update 的时候去更新。怎么知道这时候右键点击的是哪一行呢?直接通过 TableView 的 clickedRow  属性获取,这里容易混淆,TableView 还有一个 selectedRow 注意右键的时候并没有 Select  ,UI 上的反馈也是不一样的。这里我也搞混过,代码很简单,就不写 Demo 了。

class ViewController: NSViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        let menu = NSMenu()
            menu.delegate = self
            tableView.menu = menu
    }
}

extension ViewController: NSMenuDelegate {
    func menuNeedsUpdate(_ menu: NSMenu) {
        menu.removeAllItems()
    // 在这里动态添加 menu item
        menu.addItem(NSMenuItem(title: "Delete", action: #selector(handleDeleteClickedRow), keyEquivalent: ""))
    }
}

写这篇文章不仅是个记录,其实是想说固有的认识不一定是正确或者最好的,技术之路需要不断探索和实践。走点弯路也没什么不好,走对后能笑的更开心。

广告

我建了一个付费的小密圈,主要由我分享自己在自由职业、独立开发、远程工作的经验,iOS/macOS 应用产品、技术、运营心得,所见所闻所读的想法。感兴趣的朋友可以关注,圈子链接: http://t.xiaomiquan.com/AuFuBAy

也可以购买我的应用支持我 : ) 访问 seedlab.io 


用户评论
开源开发学习小组列表