Search code examples
objective-cxcodecocoanstableview

How to change NSTableView header background color in MAC OS X app?


I've tried all found suggested solutions but ended up with this as the closest:

enter image description here

The target is to have custom color for:

  1. complete header background (e.g. green)
  2. text (e.g. white)
  3. sort control color (e.g. white)

Currently I can only set the interior bg and text color properly while leaving the header borders and sort controls in default white color.

I use the approach of custom NCTableHeaderCell.

// <C> changing the bgColor doesn't work this way
[self.tv.headerView setWantsLayer:YES];
self.tv.headerView.layer.backgroundColor = NSColor.redColor.CGColor;

for (NSTableColumn *tc in self.tv.tableColumns) {

    // <C> this only helps to change the header text color
    tc.headerCell = [[NCTableHeaderCell_customizable alloc]initTextCell:@"Hdr"];

    // <C> this changes the bgColor of the area of the headerCell label text (the interior) but it leaves border and sort controls in white color;
    tc.headerCell.drawsBackground = YES;
    tc.headerCell.backgroundColor = NSColor.greenColor;

    // <C> changing the textColor doesn't work this way
    // <SOLUTION> use NCTableHeaderCell_customizable as done above;
    tc.headerCell.textColor = NSColor.redColor;
}

My custom class look like this:

@implementation NCTableHeaderCell_customizable

// <C> this works as expected
- (NSColor *) textColor
{
    return NSColor.whiteColor;
}

// <C> this only sets the interior bgColor leaving the borders in standard color
//
//- (NSColor *) backgroundColor
//{
//    return NSColor.redColor;
//}

- (void) drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView;
{
    // <C> this only sets the interior bgColor leaving the borders in standard color
    //
    //self.backgroundColor = NSColor.orangeColor;

    [super drawWithFrame:cellFrame inView:controlView];

    // <C> this draws the red bg as expected but doesn't show the interior;
    //
    //    [NSColor.redColor set];
    //    NSRectFillUsingOperation(cellFrame, NSCompositingOperationSourceOver);

    // <C> this draws the red bg as expected but
    //     1) doesn't layout the interior well (I could fix this);
    //     2) doesn't show the sort controls (it's over-drawn with the code bellow);
    //
    //    [NSColor.redColor setFill];
    //    NSRectFill(cellFrame);
    //    CGRect titleRect = [self titleRectForBounds:cellFrame];
    //    [self.attributedStringValue drawInRect:titleRect];
}

- (void) drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView;
{
    [super drawInteriorWithFrame:cellFrame inView:controlView];
}

- (void) drawFocusRingMaskWithFrame:(NSRect)cellFrame inView:(NSView *)controlView;
{
    [super drawFocusRingMaskWithFrame:cellFrame inView:controlView];
}

- (void) drawSortIndicatorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView ascending:(BOOL)ascending priority:(NSInteger)priority;
{
    [super drawSortIndicatorWithFrame:cellFrame inView:controlView ascending:ascending priority:priority];
    //NSTableHeaderView *v = (NSTableHeaderView *)controlView;
}

I'm quite close to the solution but I don't know how to draw correctly the custom header cell to archive the goal.


Solution

  • Thanks guys for your help. I've ended up with this simple solution which also solves the sort indicators. I've gave up on custom NSHeaderCell and resolved everything in single custom NSTableHeaderRowView method:

    - (void)drawRect:(NSRect)dirtyRect
    {
        [NSGraphicsContext saveGraphicsState];
    
        // Row: Fill the background
        //
        [NSColor.whiteColor setFill];
        const CGRect headerRect = CGRectMake(0.0, 1.0, self.bounds.size.width, self.bounds.size.height-1.0);
        [[NSBezierPath bezierPathWithRect:headerRect] fill];
    
        // Columns
        //
        for (NSUInteger i = 0; i < self.tableView.numberOfColumns; ++i) {
            NSRect rect = [self headerRectOfColumn:i];
    
            // separator on left
            //
            if (i != 0) {
                [[NSColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:0.35] setFill];
                [[NSBezierPath bezierPathWithRect:NSMakeRect(rect.origin.x, rect.origin.y+4.0, 1.0, rect.size.height-8.0)] fill];
            }
    
            NSTableColumn *tableColumn = self.tableView.tableColumns[i];
            NSSortDescriptor *col_sd = tableColumn.sortDescriptorPrototype;
            NSTableHeaderCell *tableHeaderCell = tableColumn.headerCell;
    
            // text
            //
            NSString *columnText = tableHeaderCell.stringValue;
            [columnText drawInRect:NSInsetRect(rect, 5.0, 4.0) withAttributes:nil];
    
            // sort indicator
            //
            for (NSInteger priority = 0; priority < self.tableView.sortDescriptors.count; ++priority) {
                NSSortDescriptor *sortDesciptor = self.tableView.sortDescriptors[priority];
                // <C> there is no way to get column from sortDesciptor so I use this trick comparing sortDesciptor with current column.sortDescriptorPrototype;
                // <C> not sure why sel_isEqual() dosn't work so I compare its string representation;
                if ([NSStringFromSelector(sortDesciptor.selector) isEqualToString:NSStringFromSelector(col_sd.selector)] == YES && [sortDesciptor.key isEqualToString:col_sd.key] == YES) {
                    SDEBUG(LEVEL_DEBUG, @"sort-hdr", @"MATCH: sel=%@", NSStringFromSelector(sortDesciptor.selector));
                    // <C> default implementation draws indicator ONLY for priority 0; Otherwise the indicator would have to graphically show the prio;
                    // <C> it is a support for multi-column sorting;
                    // <REF> shall you need it: https://stackoverflow.com/questions/46611972/sorting-a-viewbased-tableview-with-multiple-sort-descriptors-doesnt-work
                    [tableHeaderCell drawSortIndicatorWithFrame:rect inView:self ascending:sortDesciptor.ascending priority:priority];
                }
            }
        }
    
        [NSGraphicsContext restoreGraphicsState];
    }