Change NSTableView alternate row colors
Asked Answered
M

9

18

I'm using the "Alternating Rows" option in Interface Builder to get alternating row colors on an NSTableView. Is there any way to change the colors of the alternating rows?

Monday answered 20/10, 2010 at 0:9 Comment(0)
M
7

Found a better way to do it here. That method overrides the highlightSelectionInClipRect: method in an NSTableView subclass so you can use any color you want for the alternating rows. It's not as hackish as using an NSColor category, and it only affects table views you choose.

Monday answered 26/10, 2010 at 2:46 Comment(2)
+100 I came across this question a few days ago hoping for a good answer, but didn't find a satisfactory one. Thankfully, I happened to see it come up again! Thanks!Intelligent
This didn't quite work for me. It looks fine unless you start scrolling up and down with the scrollbar. Then the re-drawing gets messed up and you end up with lots of horizontal white lines in the colored rows. I put my implementation below, but it's so simple that I wonder if I'm missing something.Zoomorphism
A
7

If you want to use an undocumented way, make a NSColor category and override _blueAlternatingRowColor like this:

@implementation NSColor (ColorChangingFun)

+(NSColor*)_blueAlternatingRowColor
{
    return [NSColor redColor];
}

@end

or to change both colors, override controlAlternatingRowBackgroundColors to return an array of colors you want alternated.

@implementation NSColor (ColorChangingFun)

+(NSArray*)controlAlternatingRowBackgroundColors
{
    return [NSArray arrayWithObjects:[NSColor redColor], [NSColor greenColor], nil];
}

@end
Abhorrent answered 20/10, 2010 at 0:59 Comment(6)
Why hack when the API gives you a way?Gamma
You want to change the white color too? What are you doing this for exactly?Abhorrent
@Ken I think its a fairly common practice to customize the table view cell colors, surprised that NSTableView doesn't support this by default. The controlAlternatingRowBackgroundColors method worked great, I think its the only way to do this.Monday
The only down-side is that it will affect all such tableviews in the app.Abhorrent
@KenAspeslagh am getting the coloured rows in other tableviews also. For example: in my tableview i want to add some songs using NSOpenPanel, when the panel appears, select the list view option, it will automatically colour that tables also.Beiderbecke
Overriding a method/property in a category is undefined behaviour. Compare "Avoid Category Method Name Clashes" in developer.apple.com/library/content/documentation/Cocoa/…: "... the behavior is undefined as to which method implementation is used at runtime."Francyne
M
7

Found a better way to do it here. That method overrides the highlightSelectionInClipRect: method in an NSTableView subclass so you can use any color you want for the alternating rows. It's not as hackish as using an NSColor category, and it only affects table views you choose.

Monday answered 26/10, 2010 at 2:46 Comment(2)
+100 I came across this question a few days ago hoping for a good answer, but didn't find a satisfactory one. Thankfully, I happened to see it come up again! Thanks!Intelligent
This didn't quite work for me. It looks fine unless you start scrolling up and down with the scrollbar. Then the re-drawing gets messed up and you end up with lots of horizontal white lines in the colored rows. I put my implementation below, but it's so simple that I wonder if I'm missing something.Zoomorphism
Z
4

I subclassed NSTableView and implemented drawRow:clipRect: like this...

- (void)drawRow:(NSInteger)row clipRect:(NSRect)clipRect
{
    NSColor *color = (row % 2) ? [NSColor redColor] : [NSColor whiteColor];
    [color setFill];
    NSRectFill([self rectOfRow:row]);
    [super drawRow:row clipRect:clipRect];
}

It seems to work, but it's so simple that I'm wondering if I'm missing something.

Zoomorphism answered 24/2, 2011 at 8:8 Comment(4)
Unfortunately it doesn't provide alternating colors for empty rows, only rows which contain data.Raab
This worked well for me. You may want to wrap the first three lines in an if (row != [self selectedRow]) to prevent issues with the highlighted background of selected rows.Disrespect
@Raab Check this question for a solution to also custom draw the table view background w/o rowsIntermediate
Note that this method should only be used for cell-based table views. According to the docs: "This method should not be subclassed or overridden for a "View Based TableView". Instead, row drawing customization can be done by subclassing NSTableRowView."Brachycephalic
T
3

I wanted a solution that worked just like the regular NSTableView, including support for elastic scrolling and such, so I created an NSTableView subclass that has an NSColor* property called alternateBackgroundColor, and then overrode the -drawBackgroundColorInClipRect: method like so:

- (void) drawBackgroundInClipRect:(NSRect)clipRect {
    if([self alternateBackgroundColor] == nil) {
        // If we didn't set the alternate colour, fall back to the default behaviour
        [super drawBackgroundInClipRect:clipRect];
    } else {
        // Fill in the background colour
        [[self backgroundColor] set];
        NSRectFill(clipRect);

        // Check if we should be drawing alternating coloured rows
        if([self alternateBackgroundColor] && [self usesAlternatingRowBackgroundColors]) {
            // Set the alternating background colour
            [[self alternateBackgroundColor] set];

            // Go through all of the intersected rows and draw their rects
            NSRect checkRect = [self bounds];
            checkRect.origin.y = clipRect.origin.y;
            checkRect.size.height = clipRect.size.height;
            NSRange rowsToDraw = [self rowsInRect:checkRect];
            NSUInteger curRow = rowsToDraw.location;
            while(curRow < rowsToDraw.location + rowsToDraw.length) {
                if(curRow % 2 != 0) {
                    // This is an alternate row
                    NSRect rowRect = [self rectOfRow:curRow];
                    rowRect.origin.x = clipRect.origin.x;
                    rowRect.size.width = clipRect.size.width;
                    NSRectFill(rowRect);
                }

                curRow++;
            }

            // Figure out the height of "off the table" rows
            CGFloat rowHeight = [self rowHeight];
            if( ([self gridStyleMask] & NSTableViewSolidHorizontalGridLineMask) == NSTableViewSolidHorizontalGridLineMask
               || ([self gridStyleMask] & NSTableViewDashedHorizontalGridLineMask) == NSTableViewDashedHorizontalGridLineMask) {
                rowHeight += 2.0f; // Compensate for a grid
            }

            // Draw fake rows below the table's last row
            CGFloat virtualRowOrigin = 0.0f;
            NSInteger virtualRowNumber = [self numberOfRows];
            if([self numberOfRows] > 0) {
                NSRect finalRect = [self rectOfRow:[self numberOfRows]-1];
                virtualRowOrigin = finalRect.origin.y + finalRect.size.height;
            }
            while(virtualRowOrigin < clipRect.origin.y + clipRect.size.height) {
                if(virtualRowNumber % 2 != 0) {
                    // This is an alternate row
                    NSRect virtualRowRect = NSMakeRect(clipRect.origin.x,virtualRowOrigin,clipRect.size.width,rowHeight);
                    NSRectFill(virtualRowRect);
                }

                virtualRowNumber++;
                virtualRowOrigin += rowHeight;
            }

            // Draw fake rows above the table's first row
            virtualRowOrigin = -1 * rowHeight;
            virtualRowNumber = -1;
            while(virtualRowOrigin + rowHeight > clipRect.origin.y) {
                if(abs(virtualRowNumber) % 2 != 0) {
                    // This is an alternate row
                    NSRect virtualRowRect = NSMakeRect(clipRect.origin.x,virtualRowOrigin,clipRect.size.width,rowHeight);
                    NSRectFill(virtualRowRect);
                }

                virtualRowNumber--;
                virtualRowOrigin -= rowHeight;
            }
        }
    }
}
Tunnage answered 8/8, 2012 at 19:8 Comment(1)
To make this work I had to disable usesAlternatingRowBackgroundColors and take the conditional check out. Otherwise, the table view was drawing over the top of this and I only saw the drawn virtual rows beyond the table bounds.Dekeles
G
3

I'm not sure how recently this was added, or if it is as flexible as you need it to be, but I noticed that you can specify "Alternating" rows in Interface Builder in Xcode 4.6 (and possibly earlier).

  1. Open your nib in Xcode and select your NSTableView or NSOutlineView
  2. Show the Attributes Inspector in the Utilities Pane (⎇⌘4)
  3. Notice the Highlight Alternating Rows checkbox.

enter image description here

Gristly answered 21/6, 2013 at 23:46 Comment(1)
Yeah, this is the option I was using (it's been around for a while). The problem was being able to customize the alternating colours since ticking that box just gives you the default light blue/white system colours.Monday
G
1

There is no settable property for this, however you can respond to the delegate method -tableView:willDisplayCell:forTableColumn:row: and set the cell's background color based on the evenness of the row number.

Gamma answered 20/10, 2010 at 0:19 Comment(1)
This doesn't seem to work when there are no cells in the table view (doesn't change the background of the table view itself)Monday
P
1

Nate Thorn's answer worked perfectly for me.

Here it is, refactored for Swift:

import Foundation
import Cocoa
import AppKit

public class SubclassedTableView : NSTableView {

    private func
    alternateBackgroundColor() -> NSColor? {
        return NSColor.redColor() // Return any color you like
    }

    public override func
    drawBackgroundInClipRect(clipRect: NSRect) {

        if alternateBackgroundColor() == nil {
            // If we didn't set the alternate colour, fall back to the default behaviour
            super.drawBackgroundInClipRect(clipRect)
        } else {
            // Fill in the background colour
            self.backgroundColor.set()
            NSRectFill(clipRect)

            // Check if we should be drawing alternating coloured rows
            if usesAlternatingRowBackgroundColors {
                // Set the alternating background colour
                alternateBackgroundColor()!.set()

                // Go through all of the intersected rows and draw their rects
                var checkRect = bounds
                checkRect.origin.y = clipRect.origin.y
                checkRect.size.height = clipRect.size.height
                let rowsToDraw = rowsInRect(checkRect)
                var curRow = rowsToDraw.location
                repeat {
                    if curRow % 2 != 0 {
                        // This is an alternate row
                        var rowRect = rectOfRow(curRow)
                        rowRect.origin.x = clipRect.origin.x
                        rowRect.size.width = clipRect.size.width
                        NSRectFill(rowRect)
                    }

                    curRow++
                } while curRow < rowsToDraw.location + rowsToDraw.length

                // Figure out the height of "off the table" rows
                var thisRowHeight = rowHeight
                if gridStyleMask.contains(NSTableViewGridLineStyle.SolidHorizontalGridLineMask)
                   || gridStyleMask.contains(NSTableViewGridLineStyle.DashedHorizontalGridLineMask) {
                    thisRowHeight += 2.0 // Compensate for a grid
                }

                // Draw fake rows below the table's last row
                var virtualRowOrigin = 0.0 as CGFloat
                var virtualRowNumber = numberOfRows
                if numberOfRows > 0 {
                    let finalRect = rectOfRow(numberOfRows-1)
                    virtualRowOrigin = finalRect.origin.y + finalRect.size.height
                }
                repeat {
                    if virtualRowNumber % 2 != 0 {
                        // This is an alternate row
                        let virtualRowRect = NSRect(x: clipRect.origin.x, y: virtualRowOrigin, width: clipRect.size.width, height: thisRowHeight)
                        NSRectFill(virtualRowRect)
                    }

                    virtualRowNumber++
                    virtualRowOrigin += thisRowHeight
                } while virtualRowOrigin < clipRect.origin.y + clipRect.size.height

                // Draw fake rows above the table's first row
                virtualRowOrigin = -1 * thisRowHeight
                virtualRowNumber = -1
                repeat {
                    if abs(virtualRowNumber) % 2 != 0 {
                        // This is an alternate row
                        let virtualRowRect = NSRect(x: clipRect.origin.x, y: virtualRowOrigin, width: clipRect.size.width, height: thisRowHeight)
                        NSRectFill(virtualRowRect)
                    }

                    virtualRowNumber--
                    virtualRowOrigin -= thisRowHeight
                } while virtualRowOrigin + thisRowHeight > clipRect.origin.y
            }
        }
    }
}
Perspective answered 6/2, 2016 at 6:52 Comment(0)
M
1

Swift 4+ version of AlternateRealist's answer:

public class AlternateBgColorTableView: NSTableView {
    var alternateBackgroundColor: NSColor? = .red

    override public func drawBackground(inClipRect clipRect: NSRect) {
        // If we didn't set the alternate color, fall back to the default behavior
        guard let alternateBackgroundColor = alternateBackgroundColor else {
            super.drawBackground(inClipRect: clipRect)

            return
        }

        // Fill in the background color
        backgroundColor.set()
        clipRect.fill()

        // Check if we should be drawing alternating colored rows
        if usesAlternatingRowBackgroundColors {
            // Set the alternating background color
            alternateBackgroundColor.set()

            // Go through all of the intersected rows and draw their rects
            var checkRect = bounds
            checkRect.origin.y = clipRect.origin.y
            checkRect.size.height = clipRect.height
            let rowsToDraw = rows(in: checkRect)
            var currentRow = rowsToDraw.location

            repeat {
                if currentRow % 2 != 0 {
                    // This is an alternate row
                    var rowRect = rect(ofRow: currentRow)
                    rowRect.origin.x = clipRect.origin.x
                    rowRect.size.width = clipRect.width
                    rowRect.fill()
                }

                currentRow += 1
            } while currentRow < rowsToDraw.location + rowsToDraw.length

            // Figure out the height of "off the table" rows
            var thisRowHeight = rowHeight

            if gridStyleMask.contains(.solidHorizontalGridLineMask) || gridStyleMask.contains(.dashedHorizontalGridLineMask) {
                thisRowHeight += 2 // Compensate for a grid
            }

            // Draw fake rows below the table's last row
            var virtualRowOrigin: CGFloat = 0
            var virtualRowNumber = numberOfRows

            if numberOfRows > 0 {
                let finalRect = rect(ofRow: numberOfRows - 1)
                virtualRowOrigin = finalRect.origin.y + finalRect.height
            }

            repeat {
                if virtualRowNumber % 2 != 0 {
                    // This is an alternate row
                    let virtualRowRect = NSRect(x: clipRect.origin.x, y: virtualRowOrigin, width: clipRect.width, height: thisRowHeight)
                    virtualRowRect.fill()
                }

                virtualRowNumber += 1
                virtualRowOrigin += thisRowHeight
            } while virtualRowOrigin < clipRect.origin.y + clipRect.size.height

            // Draw fake rows above the table's first row
            virtualRowOrigin = -1 * thisRowHeight
            virtualRowNumber = -1

            repeat {
                if abs(virtualRowNumber) % 2 != 0 {
                    // This is an alternate row
                    let virtualRowRect = NSRect(x: clipRect.origin.x, y: virtualRowOrigin, width: clipRect.width, height: thisRowHeight)
                    virtualRowRect.fill()
                }

                virtualRowNumber -= 1
                virtualRowOrigin -= thisRowHeight
            } while virtualRowOrigin + thisRowHeight > clipRect.origin.y
        }
    }
}
Medick answered 4/4, 2019 at 19:16 Comment(0)
C
0

If you wish to set a custom color paired with the data in each row, I found this way of doing so, thanks to the posts here above, and this solution is also working to implement a custom alternate row color ( except for the empty rows ) :

create a subclass of NSTableView :

ColoredRowTableView.h

   @protocol ColoredRowTableViewDelegate <NSTableViewDelegate>
   -(NSColor *)colorForRow:(NSInteger)row;
   @end
   @interface ColoredRowTableView : NSTableView
   @end

in ColoredRowTableView.m

- (void)drawRow:(NSInteger)row clipRect:(NSRect)clipRect
{
    if (row == [self selectedRow])
    {
        [super drawRow:row clipRect:clipRect];
         return;
    }
    NSColor *color = [(id<ColoredRowTableViewDelegate>)[self delegate] colorForRow:row];
    [color setFill];
    NSRectFill([self rectOfRow:row]);
    [super drawRow:row clipRect:clipRect];
}

then in your table view delegate :

#import "ColoredRowTableView.h"
@interface MyTableViewDelegate : NSViewController <ColoredRowTableViewDelegate>
// instead of <NSTableViewDelegate>

implement the colorForRow: method in your delegate. If the goal is only to provide alternate row it can return a color based on the row number, if it is even or odd. Also don't forget to change the class of your table view to ColoredRowTableView in interface builder

Cuttler answered 16/11, 2019 at 23:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.