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

Ale*_*lex 7 cocoa contextmenu objective-c highlight nstableview

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

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

例如,这是常态:

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

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

编辑:

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

Ben*_*ock 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
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

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


针对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()
    }

}
Run Code Online (Sandbox Code Playgroud)

注意:我实际上没有运行它,但我很确定这应该在Swift中完成.

  • @ixany,哦,是的,我忘了提到一个技巧!在 Interface Builder 中,选择您的表格/大纲视图,并将 *Highlight* 设置为 *None*。默认情况下,AppKit 会做一些疯狂的绘图工作,并且在不搞砸一切的情况下很难覆盖。如果这不起作用,我会看看我是否能弄清楚我是如何让我的侧边栏在我当前的项目中正常工作的。我记得它扔了几个曲线球。祝你好运!让我知道上述技巧是否可以解决问题。 (2认同)

Bry*_*yan 5

停止默认绘图

有几个答案描述了如何绘制自定义上下文单击突出显示。然而,AppKit 将继续绘制默认的一个。有一个简单的技巧可以阻止这种情况,我不想让它在评论中丢失:子类NSTableView和覆盖-menuForEvent:

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

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


解决坏苹果工程

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

但不用担心,我们可以为他们完成 Apple 工程部的工作。在我们的自定义NSTableView子类中,我们重写该clickedRow属性:

class MyTableView: NSTableView
{
    private var _clickedRow: Int = -1
    override var clickedRow: Int {
        get { return _clickedRow }
        set { _clickedRow = newValue }
    }
}
Run Code Online (Sandbox Code Playgroud)

现在我们更新-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
}
Run Code Online (Sandbox Code Playgroud)

伟大的。我们解决了这个问题。继续下一件事:


告诉您的 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
    }
Run Code Online (Sandbox Code Playgroud)

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

// NSTableRowView subclass
var isContextualMenuTarget: Bool = false {
    didSet {
        needsDisplay = true
    }
}
Run Code Online (Sandbox Code Playgroud)

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


菜单关闭时清理

您有一个为 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;
            
    }];
}
Run Code Online (Sandbox Code Playgroud)

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

Single-Column:此示例假设单列 tableView 的每一行都有相同的 NSMenu。您可以将相同的技术应用于多列和/或每行不同的 NSMenu。

这就是你如何打败 AppKit,直到它满足你的要求。


wil*_*984 -3

我会看一下NSTableRowView 文档。该类负责在基于视图的NSTableView.