52

I have an NSTableView with several text columns. By default, the dataCell for these columns is an instance of Apple's NSTextFieldCell class, which does all kinds of wonderful things, but it draws text aligned with the top of the cell, and I want the text to be vertically centered in the cell.

There is an internal flag in NSTextFieldCell that can be used to vertically center the text, and it works beautifully. However, since it is an internal flag, its use is not sanctioned by Apple and it could simply disappear without warning in a future release. I am currently using this internal flag because it is simple and effective. Apple has obviously spent some time implementing the feature, so I dislike the idea of re-implementing it.

So; my question is this: What is the right way to implement something that behaves exactly like Apple's NStextFieldCell, but draws vertically centered text instead of top-aligned?

For the record, here is my current "solution":

@interface NSTextFieldCell (MyCategories)
- (void)setVerticalCentering:(BOOL)centerVertical;
@end

@implementation NSTextFieldCell (MyCategories)
- (void)setVerticalCentering:(BOOL)centerVertical
{
    @try { _cFlags.vCentered = centerVertical ? 1 : 0; }
    @catch(...) { NSLog(@"*** unable to set vertical centering"); }
}
@end

Used as follows:

[[myTableColumn dataCell] setVerticalCentering:YES];
e.James
  • 109,080
  • 38
  • 170
  • 208
  • I don't think the try/catch block makes any sense in this case, because _cflags is a C structure, not an Objective C object. If this struct is changed in a future version of Mac OS X, all sorts of weird things might happen, but no exception will be thrown. – Jakob Egger Oct 24 '11 at 21:52
  • @Jakob Egger: You are probably right. I found that solution elsewhere on the internet, and copied it in as-is. – e.James Oct 24 '11 at 22:45
  • You should accept Jakob Egger's answer. When the code from the accepted answer is used, it causes a weird glitch when the `NSTextFieldCell` is edited. Jakob's answer resolves the issue. – Jack Humphries Jan 06 '13 at 06:27
  • I had an app rejected from the MAS for use of `_cFlags.vCentered`. You've been warned. – Keith Smiley Mar 09 '13 at 18:35
  • @KeithSmiley: thanks for the heads-up! – e.James Mar 10 '13 at 17:57
  • Possible duplicate of [NSTextField Vertical alignment](https://stackoverflow.com/questions/10205088/nstextfield-vertical-alignment) – Mattie Jun 27 '19 at 19:49

7 Answers7

37

The other answers didn't work for multiple lines. Therefore I initially continued using the undocumented cFlags.vCentered property, but that caused my app to be rejected from the app store. I ended up using a modified version of Matt Bell's solution that works for multiple lines, word wrapping, and a truncated last line:

-(void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView {
    NSAttributedString *attrString = self.attributedStringValue;

    /* if your values can be attributed strings, make them white when selected */
    if (self.isHighlighted && self.backgroundStyle==NSBackgroundStyleDark) {
        NSMutableAttributedString *whiteString = attrString.mutableCopy;
        [whiteString addAttribute: NSForegroundColorAttributeName
                            value: [NSColor whiteColor]
                            range: NSMakeRange(0, whiteString.length) ];
        attrString = whiteString;
    }

    [attrString drawWithRect: [self titleRectForBounds:cellFrame] 
                     options: NSStringDrawingTruncatesLastVisibleLine | NSStringDrawingUsesLineFragmentOrigin];
}

- (NSRect)titleRectForBounds:(NSRect)theRect {
    /* get the standard text content rectangle */
    NSRect titleFrame = [super titleRectForBounds:theRect];

    /* find out how big the rendered text will be */
    NSAttributedString *attrString = self.attributedStringValue;
    NSRect textRect = [attrString boundingRectWithSize: titleFrame.size
                                               options: NSStringDrawingTruncatesLastVisibleLine | NSStringDrawingUsesLineFragmentOrigin ];

    /* If the height of the rendered text is less then the available height,
     * we modify the titleRect to center the text vertically */
    if (textRect.size.height < titleFrame.size.height) {
        titleFrame.origin.y = theRect.origin.y + (theRect.size.height - textRect.size.height) / 2.0;
        titleFrame.size.height = textRect.size.height;
    }
    return titleFrame;
}

(This code assumes ARC; add an autorelease after attrString.mutableCopy if you use manual memory management)

Jakob Egger
  • 11,393
  • 4
  • 35
  • 47
  • This worked for me and it is a very nice implementation - thanks. – ioquatix May 22 '12 at 04:24
  • This works very well. The accepted answer caused a weird glitch when editing the cell. – Jack Humphries Jan 06 '13 at 06:25
  • 3
    By the way, is it possible to center the text when it is being edited? My `NSTextFieldCell` is multiple lines long, and, when the user double clicks the text to edit it, it goes back to the top until editing is complete. – Jack Humphries Jan 06 '13 at 19:23
  • My app can only view data, so this issue hasn't come up for me. However, I believe you will have to provide your own field editor, and do some tricky things to realign the field editor as the user types. See [Working With the Field Editor](https://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/TextEditing/Tasks/FieldEditor.html) in Apple's docs. – Jakob Egger Jan 08 '13 at 08:58
  • 2
    Is there a way to recenter/redraw the text during writing in the field? – Nikolay Tsenkov May 20 '14 at 06:59
  • This is a perfect solution over RSVerticallyCenteredTextFieldCell. Thank you developer. Cheers.... – Sid Jun 04 '14 at 04:27
  • nice, but not perfect. Still doesn't handle the field editor, so not a complete solution for editable fields. – uchuugaka Mar 05 '15 at 07:13
34

Overriding NSCell's -titleRectForBounds: should do it -- that's the method responsible for telling the cell where to draw its text:

- (NSRect)titleRectForBounds:(NSRect)theRect {
    NSRect titleFrame = [super titleRectForBounds:theRect];
    NSSize titleSize = [[self attributedStringValue] size];
    titleFrame.origin.y = theRect.origin.y + (theRect.size.height - titleSize.height) / 2.0;
    return titleFrame;
}

- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView {
    NSRect titleRect = [self titleRectForBounds:cellFrame];
    [[self attributedStringValue] drawInRect:titleRect];
}
Matt Ball
  • 6,546
  • 1
  • 30
  • 31
  • 1
    This seems like a promising idea, but it doesn't work for me. I created a subclass of `NSTextFieldCell`, copied your code in, and used the custom class for the cells in my `NSTableView`, but the `titleRectForBounds` method is never called. – e.James Aug 06 '09 at 06:47
  • 3
    Ah, I forgot that NSTextFieldCell basically lies in its documentation -- you're right, NSTextFieldCell by default doesn't use -titleRectForBounds:. I've updated my answer to override -drawInteriorWithFrame:inView: in order to draw the text at the right spot. – Matt Ball Aug 06 '09 at 10:13
  • 2
    Is this valid also with the NSTextFieldCell inside an NSTextField? It doesn't seem to work. *edit* 2009? Maybe it's a bit old. :) – Donovan Aug 25 '11 at 13:41
  • 5
    @MattBall If the text in NSTextField is selected, it back to top-aligned again. – Mil0R3 Aug 03 '12 at 03:08
  • @MattBall i'm getting a weird double text shadow after implementing your solution. Also the placeholder text isn't centered vertically? Any Thoughts? http://imgur.com/HaEOl – kwbock Nov 05 '12 at 20:01
  • Works great for me. Ported to Swift 3 [here](https://gist.github.com/zpasternack/b8ae67901206bc6f80c9b0c65df8a8a8). – zpasternack Mar 08 '17 at 04:19
5

For anyone attempting this using Matt Ball's drawInteriorWithFrame:inView: method, this will no longer draw a background if you have set your cell to draw one. To solve this add something along the lines of

[[NSColor lightGrayColor] set];
NSRectFill(cellFrame);

to the beginning of your drawInteriorWithFrame:inView: method.

Greg Sexton
  • 8,879
  • 6
  • 29
  • 34
4

FYI, this works well, although I haven't managed to get it to stay centered when you edit the cell... I sometimes have cells with large amounts of text and this code can result in them being misaligned if the text height is greater then the cell it's trying to vertically center it in. Here's my modified method:

- (NSRect)titleRectForBounds:(NSRect)theRect 
 {
    NSRect titleFrame = [super titleRectForBounds:theRect];
    NSSize titleSize = [[self attributedStringValue] size];
     // test to see if the text height is bigger then the cell, if it is,
     // don't try to center it or it will be pushed up out of the cell!
     if ( titleSize.height < theRect.size.height ) {
         titleFrame.origin.y = theRect.origin.y + (theRect.size.height - titleSize.height) / 2.0;
     }
    return titleFrame;
}
e.James
  • 109,080
  • 38
  • 170
  • 208
2

Though this is pretty old question...

I believe default style of NSTableView implementation is intended strictly for single line text display with all same size & font.

In that case, I recommend,

  1. Set font.
  2. Adjust rowHeight.

Maybe you will get quietly dense rows. And then, give them padding by setting intercellSpacing.

For example,

    core_table_view.rowHeight               =   [NSFont systemFontSizeForControlSize:(NSSmallControlSize)] + 4;
    core_table_view.intercellSpacing        =   CGSizeMake(10, 80);

Here what you'll get with two property adjustment.

enter image description here

This won't work for multi-line text, but very good enough for quick vertical center if you don't need multi-line support.

eonil
  • 75,400
  • 74
  • 294
  • 482
  • That is a reasonable solution for one-line cells, which probably covers most use cases. Thanks! – e.James Dec 01 '14 at 19:35
2

No. The right way is to put the Field in another view and use auto layout or that parent view's layout to position it.

Frank Krueger
  • 64,851
  • 44
  • 155
  • 203
  • 1
    OK - but then how would I make the Field resize itself (change its height) to match the text? Without that, I will have a nicely centered text Field, but the text inside the field will still be top-aligned, and so the text will appear above center in the parent view. – e.James Dec 01 '14 at 19:34
  • This is absolutely the proper answer. Allow NSTextField to rely on its intrinsic size (adjust the font size as you prefer, which will correspond to a change in intrinsic size), then add it as a subview to a container view that will frame it in relation to whatever holds the text field and that will frame the text field's intrinsic size in relation to the overall space. – Asher Nov 05 '15 at 07:45
  • @Asher, actually no, changing the font size does not cause the field to resize: https://www.dropbox.com/s/9b4vgcp7kuujll2/Screenshot%202016-03-01%2012.02.26.png?dl=0 – Andy Hin Mar 01 '16 at 17:02
  • That sounds like autolayout is not configured properly. – Asher Sep 27 '16 at 19:57
1

I had the same problem and here is the solution I did :

1) In Interface Builder, select your NSTableCellView. Make sure it as big as the row height in the Size Inspector. For example, if your row height is 32, make your Cell height 32

2) Make sure your cell is well placed in your row (I mean visible)

3) Select your TextField inside your Cell and go to your size inspector

4) You should see "Arrange" item and select "Center Vertically in Container"

--> The TextField will center itself in the cell