在基于视图的NSTableView上自定义右键单击突出显示

时间:2012-03-08 14:59:50

标签: objective-c cocoa contextmenu nstableview highlight

我有一个基于视图的NSTableView,带有自定义NSTableCellView和自定义NSTableRowView。我自定义了这两个类,因为我想改变每一行的外观。通过实现[NSTableRowView draw ...]方法,我可以更改背景,选择,分隔符和拖动目标高亮。

我的问题是:如何更改右键单击行并出现菜单时出现的突出显示?

例如,这是常态:

我想将方形高光改为圆形,如下所示:

我想这可以通过调用drawMenuHighlightInRect:或类似的方法在NSTableRowView中完成,但我找不到它。另外,如果我在我的子类中定制了所有绘图方法,并且我不调用超类,那么NSTableRowView类如何才能这样做呢?这是由表本身绘制的吗?

编辑:

经过一些实验后,我发现可以通过将tableview设置为源列表来实现圆形突出显示。尽管如此,我想知道如何在可能的情况下自定义它。

5 个答案:

答案 0 :(得分:9)

我知道我为OP提供任何帮助有点晚了,但希望这可以让其他人节省一点时间。我将NSTableRowView子类化以实现右键单击上下文菜单突出显示(为什么Apple没有公共绘图方法来覆盖它超出我的范围)。这是它的全部荣耀:

<强> BSDSourceListRowView.h

#import <Cocoa/Cocoa.h>

@interface BSDSourceListRowView : NSTableRowView

// This needs to be set when a context menu is shown.
@property (nonatomic, assign, getter = isShowingMenu) BOOL showingMenu;

@end

<强> BSDSourceListRowView.m

#import "BSDSourceListRowView.h"

@implementation BSDSourceListRowView

- (void)drawBackgroundInRect:(NSRect)dirtyRect
{
    [super drawBackgroundInRect:dirtyRect];

    // Context menu highlight:
    if ( self.isShowingMenu ) {
        [self drawContextMenuHighlight];
    }
}

- (void)drawContextMenuHighlight
{
    BOOL selected = self.isSelected;
    CGFloat insetY = ( selected ) ? 2.f : 1.f;
    NSBezierPath *path = [NSBezierPath bezierPathWithRoundedRect:NSInsetRect(self.bounds, 2.f, insetY) xRadius:6.f yRadius:6.f];
    NSColor *fillColor, *strokeColor;

    if ( selected ) {
        fillColor = [NSColor clearColor];
        strokeColor = [NSColor whiteColor];
    } else {
        fillColor = [NSColor colorWithCalibratedRed:95.f/255.f green:159.f/255.f blue:1.f alpha:0.12f];
        strokeColor = [NSColor alternateSelectedControlColor];
    }

    [fillColor setFill];
    [strokeColor setStroke];

    [path setLineWidth:2.f];
    [path fill];
    [path stroke];
}

- (void)drawSelectionInRect:(NSRect)dirtyRect
{
    [super drawSelectionInRect:dirtyRect];
    if ( self.isShowingMenu ) {
        [self drawContextMenuHighlight];
    }
}

- (void)setShowingMenu:(BOOL)showingMenu
{
    if ( showingMenu == _showingMenu )
        return;
    _showingMenu = showingMenu;
    [self setNeedsDisplay:YES];
}

@end

随意使用它,更改任何一个,或做任何你想做的任何事情。玩得开心!


针对Swift 3.x进行了更新:

<强> SourceListRowView.swift

import Cocoa

open class SourceListRowView : NSTableRowView {

    open var isShowingMenu: Bool = false {
        didSet {
            if isShowingMenu != oldValue {
                needsDisplay = true
            }
        }
    }

    override open func drawBackground(in dirtyRect: NSRect) {
        super.drawBackground(in: dirtyRect)
        if isShowingMenu {
            drawContextMenuHighlight()
        }
    }

    override open func drawSelection(in dirtyRect: NSRect) {
        super.drawSelection(in: dirtyRect)
        if isShowingMenu {
            drawContextMenuHighlight()
        }
    }

    private func drawContextMenuHighlight() {

        let insetY: CGFloat = isSelected ? 2 : 1
        let path = NSBezierPath(roundedRect: bounds.insetBy(dx: 2, dy: insetY), xRadius: 6, yRadius: 6)
        let fillColor, strokeColor: NSColor

        if isSelected {
            fillColor = .clear
            strokeColor = .white
        } else {
            fillColor = NSColor(calibratedRed: 95/255, green: 159/255, blue: 1, alpha: 0.12)
            strokeColor = .alternateSelectedControlColor
        }

        fillColor.setFill()
        strokeColor.setStroke()

        path.lineWidth = 2
        path.fill()
        path.stroke()
    }

}

注意:我实际上并没有运行它,但我很确定这应该可以在Swift中实现。

答案 1 :(得分:1)

这已经有点老了,但是我已经浪费了很多时间,因此发布我的解决方案以防它对任何人有帮助:

  1. 就我而言,我想完全删除行
  2. 行不是“焦点”环,它们是Apple在非文档API中正在做的事情
  3. 我发现删除它们的唯一方法(不使用未公开的API)是通过以编程方式打开NSMenu,而无需使用Interface Builder。
  4. 为此,我不得不在TableViewRow上缓存“右键单击”事件,因为它并不总是被调用,所以存在一些问题,因此我也处理了该问题。

A。子类NSTableView: 覆盖右键单击事件,计算单击位置以获得正确的行,并将其传输到我的自定义NSTableRowView!

class TableView: NSTableView {
    override func rightMouseDown(with event: NSEvent) {
        let location = event.locationInWindow
        let toMyOrigin = self.superview?.convert(location, from: nil)
        let rowIndex = self.row(at: toMyOrigin!)
        if (rowIndex < 0 || self.numberOfRows < rowIndex) {
            return
        }
        if let isRowExists = self.rowView(atRow: rowIndex, makeIfNecessary: false) {
            if let isMyTypeRow = isRowExists as? MyNSTableRowView {
                isMyTypeRow.costumRightMouseDown(with: event)
            }
        }
    }

}

B。子类MyNSTableRowView 以编程方式呈现NSMenu

class MyNSTableRowView: NSTableRowView {
    //My custom selection colors, don't have to implement this if you are ok with the default system highlighted background color
    override func drawSelection(in dirtyRect: NSRect) {
        if self.selectionHighlightStyle != .none {
            let selectionRect = NSInsetRect(self.bounds, 0, 0)
            Colors.tabSelectedBackground.setStroke()
            Colors.tabSelectedBackground.setFill()
            let selectionPath = NSBezierPath.init(roundedRect: selectionRect, xRadius: 0, yRadius: 0)
            selectionPath.fill()
            selectionPath.stroke()
        }
    }

    func costumRightMouseDown(with event: NSEvent) {
        let menu = NSMenu.init(title: "Actions:")
        menu.addItem(NSMenuItem.init(title: "Some", action: #selector(foo), keyEquivalent: "a"))
        NSMenu.popUpContextMenu(menu, with: event, for: self)
    }

    @objc func foo() {

    }
}

答案 2 :(得分:0)

我同意MCMatan的观点,您不能通过更改任何抽签来对其进行调整。该框将保留。

我采用了绕过默认菜单启动的方法,但是将上下文菜单设置保留为NSTableView中的默认菜单。我认为这是一种更简单的方法。

我从NSTableView派生并添加以下内容:

public private(set) var rightClickedRow: Int = -1

override func rightMouseDown(with event: NSEvent)
{
    guard let menu = self.menu else { return }

    let windowClickLocation = event.locationInWindow
    let outlineClickLocation = convert(windowClickLocation, from: nil)
    rightClickedRow = row(at: outlineClickLocation)

    menu.popUp(positioning: nil, at: outlineClickLocation, in: self)
}

override func rightMouseUp(with event: NSEvent) {
    rightClickedRow = -1
}

我的rightClickedRow与表视图的clickedRow类似。我有一个NSViewController来照顾我的表,并将其设置为表的菜单委托。我可以在委托调用中使用rightClickedRow,例如menuNeedsUpdate()

答案 3 :(得分:0)

停止默认图形

几个答案描述了如何绘制自定义上下文单击突出显示。但是,AppKit将继续绘制默认值。有一个简单的技巧可以阻止这种情况,我不希望它在注释中迷路:子类NSTableView并覆盖-menuForEvent:

// NSTableView subclass
override func menu(for event: NSEvent) -> NSMenu?
{
    // DO NOT call super's implementation.
    return self.menu
}

在这里,我假设您已经在IB中为tableView分配了一个菜单,或者已通过编程方式设置了tableView的menu属性。 NSTableView的默认实现-menuForEvent:是上下文菜单突出显示的地方。


解决不良的Apple工程

现在,我们不调用super的menuForEvent:实现,右键单击时,tableView的clickedRow属性将始终为-1,这意味着我们的menuItems不会定位到tableView的正确行。

但是不用担心,我们可以为他们完成Apple Engineering的工作。在我们的自定义NSTableView子类上,我们覆盖clickedRow属性:

class MyTableView: NSTableView
{
    private var _clickedRow: Int = -1
    override var clickedRow: Int {
        get { return _clickedRow }
        set { _clickedRow = newValue }
    }
}

现在,我们更新-menuForEvent:方法:

// NSTableView subclass
override func menu(for event: NSEvent) -> NSMenu?
{
    let location: CGPoint = convert(event.locationInWindow, from: nil)
    clickedRow = row(at: location)

    return self.menu
}

太好了。我们解决了这个问题。转到下一件事情:


告诉您的RowView进行自定义绘图

正如其他人所建议的,将自定义Bool属性添加到您的NSTableRowView子类中。然后,在您的绘图代码中,检查该值,以决定是否绘制自定义上下文高光。但是,使用相同的NSTableView方法设置该值的正确位置:

// NSTableView subclass
override func menu(for event: NSEvent) -> NSMenu?
    {
        let location: CGPoint = convert(event.locationInWindow, from: nil)
        clickedRow = row(at: location)
        
        if clickedRow > 0,
           let rowView: MyCustomRowView = rowView(atRow: tableRow, makeIfNecessary: false) as? MyCustomRowView
        {
            rowView.isContextualMenuTarget = true
        }
        
        return self.menu
    }

上面,我创建了MyCustomRowViewNSTableRowView的子类)并添加了一个自定义属性:isContextualMenuTarget。该自定义属性如下所示:

// NSTableRowView subclass
var isContextualMenuTarget: Bool = false {
    didSet {
        needsDisplay = true
    }
}

在绘制方法中,我检查该属性的值,如果为true,则绘制我的自定义突出显示。


菜单关闭时清理

您有一个控制器,用于为tableView实现数据源和委托方法。该控制器也可能是tableView菜单的委托。 (您可以在IB中或以编程方式进行分配。)

无论菜单对象是什么对象,都应实现menuDidClose:方法。在这里,我在Objective-C中工作,因为我的控制器仍然是ObjC:

// NSMenuDelegate object
- (void) menuDidClose:(NSMenu *)menu
{
    // We use a custom flag on our rowViews to draw our own contextual menu highlight, so we need to reset that.
    [_outlineView enumerateAvailableRowViewsUsingBlock:^(__kindof MyCustomRowView * _Nonnull rowView, NSInteger row) {
        
        rowView.isContextualMenuTarget = NO;
            
    }];
}

性能说明:我的tableView的条目永远不会超过50个。如果您的表中有数千行可见行,则最好保存设置为isContextualMenuTarget=true的rowView,然后直接在-menuDidClose:中访问该rowView,这样就不必枚举所有rowViews。

单列:此示例假定单列tableView的每一行都具有相同的NSMenu。您可以针对多行和/或每行不同的NSMenu调整相同的技术。

这就是您如何击败AppKit直到达到您想要的效果。

答案 4 :(得分:-1)

我会看一下NSTableRowView documentation。它是负责在基于视图的NSTableView中绘制选择和拖动反馈的类。