基于NSTableView单元格的上下文菜单

9
我想在一个NSTableView上放置一个上下文菜单。这部分已经完成了。我想要做的是根据右键点击的单元格内容显示不同的菜单项,并且不在特定列显示上下文菜单。
具体来说:
第0列和第1列没有上下文菜单。
所有其他单元格应该有以下上下文菜单:
第一项:"删除" + 同行.第1列的值 第二项:"保存" + 同列.表头文本
-编辑-
右侧的示例展示了任何给定单元格的上下文菜单应该是什么样子。

enter image description here

7个回答

39

有一个代理可以做到这一点!- 无需子类化

在IB中,如果您将NSTableView拖放到窗口/视图上,您会注意到表格有一个menu出口。

因此,实现上下文菜单的一种非常简单的方法是将该出口连接到一个存根菜单,并将菜单的代理出口连接到实现NSMenuDelegate协议方法- (void)menuNeedsUpdate:(NSMenu *)menu的对象。

interface builder screen shot

通常情况下,菜单的代理对象是提供表格数据源/代理的同一对象,但也可能是拥有表格的视图控制器。

请查看文档以获取更多信息。

在协议中,你可以做很多聪明的事情,但一个非常简单的实现可能像下面这样:

#pragma mark tableview menu delegates

- (void)menuNeedsUpdate:(NSMenu *)menu
{
NSInteger clickedrow = [mytable clickedRow];
NSInteger clickedcol = [mytable clickedColumn];

if (clickedrow > -1 && clickedcol > -1) {



   //construct a menu based on column and row   
   NSMenu *newmenu = [self constructMenuForRow:clickedrow andColumn:clickedcol];

   //strip all the existing stuff       
   [menu removeAllItems];

   //then repopulate with the menu that you just created        
   NSArray *itemarr = [NSArray arrayWithArray:[newmenu itemArray]];
   for(NSMenuItem *item in itemarr)
   {
      [newmenu removeItem:[item retain]];
      [menu addItem:item];
      [item release];
   }        
}

}

然后是构建菜单的方法。
-(NSMenu *)constructMenuForRow:(int)row andColumn:(int)col
{

    NSMenu *contextMenu = [[[NSMenu alloc] initWithTitle:@"Context"] autorelease];

NSString *title1 = [NSString stringWithFormat:@"Delete %@",[self titleForRow:row]]; 

NSMenuItem *item1 = [[[NSMenuItem alloc] initWithTitle:title1 action:@selector(deleteObject:) keyEquivalent:@""] autorelease];
    [contextMenu addItem:item1];
    //
NSString *title2 = [NSString stringWithFormat:@"Save %@",[self titleForColumn:col]];    

NSMenuItem *item2 = [[[NSMenuItem alloc] initWithTitle:title1 action:@selector(saveObject:) keyEquivalent:@""] autorelease];
    [contextMenu addItem:item2];

return contextMenu;
}

你选择如何实现titleForRow:titleForColumn:取决于你自己。
请注意,NSMenuItem提供了属性representedObject,允许你将任意对象绑定到菜单项,从而向你的方法(例如deleteObject:)发送信息。 编辑 注意 - 在你的NSDocument子类中实现- (void)menuNeedsUpdate:(NSMenu *)menu会阻止出现在标题栏中的自动保存/版本菜单出现在10.8中。
它仍然在10.7中工作,所以想一下。无论如何,菜单代理将需要是你的NSDocument子类之外的东西。

1
这只是让所引用的数据对象构造一个任意菜单并将其传回方法。但在您的情况下,您可能想基于clickedRow]和clickedColumn]构建菜单。我会进行编辑。 - Warren Burton
1
第一个问题是由于空选择器引起的。当您在目标上实现该方法时,该项将变为活动状态。第二个问题-尝试剥离菜单,但不要重新添加那些行/列组合的项目,或者实现委托-(NSInteger)numberOfItemsInMenu:(NSMenu *)menu并在需要时返回0。 - Warren Burton
1
这是NSMenuItem提供的一个类方法,请查看API文档。separatorItem... - Warren Burton
你好,沃伦。我是Sid。上面的解决方案对我非常有效。几个月前我实施了你的这个解决方案。但是,现在我报告了一些问题。 我面临的唯一问题是,这个菜单无法通过Control + Click或CMD + Click弹出。用户必须双击触摸板并同时点击Control或Cmd。为什么会这样? 我有什么遗漏吗? - Sid
抱歉,我不知道为什么你的代码在这个方法中无法工作。我在表格视图和大纲视图中都使用了这种模式,并没有看到这种效果。猜测可能是你有自定义视图/基于视图的单元格吸收了单击事件。 - Warren Burton
显示剩余9条评论

3

编辑:比下面的方法更好的方法是使用代理,如被接受的答案所示。

您可以子类化您的UITableView并实现menuForEvent:方法:

-(NSMenu *)menuForEvent:(NSEvent *)event{
    if (event.type==NSRightMouseDown) {
        if (self.selectedColumn == 0 || self.selectedColumn ==1) {
            return nil;
        }else {
            //create NSMenu programmatically or get a IBOutlet from one created in IB
            NSMenu *menu=[[NSMenu alloc] initWithTitle:@"Custom"];

            //code to set the menu items

            //Instead of the following line get the value from your datasource array/dictionary
            //I used this as I don't know how you have implemented your datasource, but this will also work
            NSString *deleteValue = [[self preparedCellAtColumn:1 row:self.selectedRow] title]; 

            NSString *deleteString = [NSString stringWithFormat:@"Delete %@",deleteValue];
            NSMenuItem *deleteItem = [[NSMenuItem alloc] initWithTitle:deleteString action:@selector(deleteAction:) keyEquivalent:@""];
            [menu addItem:deleteItem];

            //save item
            //similarly 
            [menu addItem:saveItem];

            return menu;
        }
    }
    return nil;
}

应该就这样了。我还没有尝试过这段代码。但这应该会给你一个想法。


不是一个好主意,因为它通常与MVC模式冲突。如果您需要从表视图获取值,则不应该在视图中实现菜单创建,而应在控制器中实现。 - Mike Lischke

2
我也尝试了Warren Burton发布的解决方案,它很好用。 但在我的情况下,我必须将以下内容添加到菜单项中:
[item1 setTarget:self];
[item2 setTarget:self];

如果没有明确设置目标,则上下文菜单将保持禁用状态。

干杯!

Alex

PS:我本来想把这个作为评论发布的,但是我没有足够的声望:(


1

Warren Burton的回答很到位。对于那些在使用Swift的人来说,下面的示例片段可能会为你节省从Objective C翻译的工作。在我的情况下,我是将上下文菜单添加到NSOutlineView中的单元格,而不是NSTableView。在这个例子中,菜单构造器查看项目并根据项目类型和状态提供不同的选项。委托(在IB中设置)是一个管理NSOutlineView的ViewController。

 func menuNeedsUpdate(menu: NSMenu) {
    // get the row/column from the NSTableView (or a subclasse, as here, an NSOutlineView)
    let row = outlineView.clickedRow
    let col = outlineView.clickedColumn
    if row < 0 || col < 0 {
        return
    }
    let newItems = constructMenuForRow(row, andColumn: col)
    menu.removeAllItems()
    for item in newItems {
        menu.addItem(item)
        // target this object for handling the actions
        item.target = self
    }
}

func constructMenuForRow(row: Int, andColumn column: Int) -> [NSMenuItem]
{
    let menuItemSeparator = NSMenuItem.separatorItem()
    let menuItemRefresh = NSMenuItem(title: "Refresh", action: #selector(refresh), keyEquivalent: "")
    let item = outlineView.itemAtRow(row)
    if let block = item as? Block {
        let menuItem1 = NSMenuItem(title: "Delete \(block.name)", action: #selector(deleteBlock), keyEquivalent: "")
        let menuItem2 = NSMenuItem(title: "New List", action: #selector(addList), keyEquivalent: "")
        return [menuItem1, menuItem2, menuItemSeparator, menuItemRefresh]
    }
    if let field = item as? Field {
        let menuItem1 = NSMenuItem(title: "Delete \(field.name)", action: #selector(deleteField), keyEquivalent: "")
        let menuItem2 = NSMenuItem(title: "New Field", action: #selector(addField), keyEquivalent: "")
        return [menuItem1, menuItem2, menuItemSeparator, menuItemRefresh]
    }
    return [NSMenuItem]()
}

0

这里是一个在视图控制器中编程设置NSOutlineView的示例。这就是你需要启动上下文菜单的所有管道。无需子类化。

我之前曾经子类化NSOutlineView来覆盖menu(for event: NSEvent),但在Graham的回答这里Warren的回答的帮助下,我找到了一个更简单的设置。

class OutlineViewController: NSViewController 
{
    // ...
    var outlineView: NSOutlineView!
    var contextMenu: NSMenu! 

    override func viewDidLoad()
    {
        // ...
        outlineView = NSOutlineView()
        contextMenu = NSMenu()
        contextMenu.delegate = self
        outlineView.menu = contextMenu
    }
}

extension OutlineViewController: NSMenuDelegate
{
    func menuNeedsUpdate(_ menu: NSMenu) {

        // clickedRow catches the right-click here 
        print("menuNeedsUpdate called. Clicked Row: \(outlineView.clickedRow)")

        // ... Flesh out the context menu here
    }
}

0
正如TheGoonie所提到的,我也有同样的经验-上下文菜单项仍然被禁用。 然而,导致项目被禁用的原因是“自动启用项目”属性。
将“自动启用项目”属性设置为关闭或以编程方式设置为“否”。
[mTableViewMenu setAutoenablesItems:NO];

0

这是我发现的最简单的自定义/动态 NSMenu 方法,同时保留系统外观(蓝色选择边框)。子类化 NSTableView 并在 menu(for:) 中设置你的菜单。

重要的部分是在表格视图上设置菜单,但从其super调用中返回菜单

override func menu(for event: NSEvent) -> NSMenu? {
    let point = convert(event.locationInWindow, from: nil)
    let clickedRow = self.row(at: point)
    var menuRows = selectedRowIndexes

    // The blue selection box should always reflect the
    // returned row indexes.
    if menuRows.isEmpty || !menuRows.contains(clickedRow) {
        menuRows = [clickedRow]
    }

    // Build your custom menu based on the menuRows indexes
    self.menu = <#myMenu#>

    return super.menu(for: event)
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接