Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

*Accurately* calculating text height in Cocoa (for Mac, not iOS)

How can I calculate the height of a string within a particular label (NSTextField), for some given fixed width?

I Googled up various methods and tried this method from Apple. It works, except that the height becomes one line too short for longer strings. Thus, there appear to be some inaccuracies in the present methods.

I also had the same issue with some other code I Googled up.

Does someone here have any accurate code for finding text height (given a particular width)?

like image 981
Enchilada Avatar asked Apr 01 '11 19:04

Enchilada


4 Answers

(a solution in Swift is at the end of the post)

I tried all of these methods and none worked for me. The 'official' way (as mentioned by OP) was off most of the time for my NSTextField. Switching to NSTextView was impossible because of XCode/IB always crashing after adding a "NSTextView in an NSScrollView".

Long story short, I found this easy way to determine the height of my NSTextField but this can be very easily adapted to width.

CGFloat minHeight = [((NSTextFieldCell *)[yourTextField cell]) cellSizeForBounds:NSMakeRect(0, 0, YOUR_MAX_WIDTH, FLT_MAX)].height;

This code has worked for every single test-case that failed with the other methods. I hope this helps someone. It always makes me feel so darn stupid when I take 2 hours to find the solution to a seemingly easy problem.

SLIGHT EDIT: Upon closer inspection this only works if you read the stringValue of the NSTextField before I ask the cell to calculate its height. So I added the following nonsensical line before the actual calculation:

[yourTextField setStringValue:yourTextField.stringValue];

I know this is really really bad but at this point I don't care anymore. I just want a reliable solution. Feel free to downvote me or better yet: suggest a better solution.


Solution in Swift (thank you iphaaw for bringing this to my attention!)

myTextField.cell!.cellSizeForBounds(NSMakeRect(CGFloat(0.0), CGFloat(0.0), width, CGFloat(FLT_MAX))).height
like image 155
vigorouscoding Avatar answered Nov 18 '22 17:11

vigorouscoding


I solved the problem using the NS(Attributed)String+Geometrics category provided at http://www.sheepsystems.com/sourceCode/sourceStringGeometrics.html Within the header file of that category is a good explanation of how these things work. Specifically, they mention that one should use NSTextView instead of NSTextField because it's virtually impossible to get an accurate height for the latter.

Thus, I replaced my NSTextField with NSTextView. First, I made my NSTextView look like my NSTextField with this code:

[textView setEditable:YES];
[textView insertText:someString];
[textView setFont:[NSFont fontWithName:@"Lucida Grande"
   size:11] range:NSMakeRange(0,[someString length])];
[textView setEditable:NO];
[textView setSelectable:NO];

Then I got the height with this code:

NSDictionary *attributes
  = [NSDictionary dictionaryWithObjectsAndKeys:
     [textView font],NSFontAttributeName,nil];
NSAttributedString *attributedString
  = [[NSAttributedString alloc] initWithString:
     [textView string] attributes:attributes];
CGFloat height = [attributedString heightForWidth:
                 [textView frame].size.width];

The height does indeed turn out to be accurate for NSTextView.

I may have omitted some minor details, but you get the picture. I hope this helps someone out there.

like image 19
Enchilada Avatar answered Nov 18 '22 16:11

Enchilada


Thanks indeed to iphaaw and Enchilada for this really cool little solution. I found it worked so well at setting row heights etc that I converted iphaaw's Swift into a couple of NSTextField extensions. Enjoy

extension NSTextField {
func bestheight(text: String, width: CGFloat) -> CGFloat {

    self.stringValue = text
    let getnumber = self.cell!.cellSizeForBounds(NSMakeRect(CGFloat(0.0), CGFloat(0.0), width, CGFloat(FLT_MAX))).height

    return getnumber
   }
}

extension NSTextField {
func bestwidth(text: String, height: CGFloat) -> CGFloat {

    self.stringValue = text
    let getnumber = self.cell!.cellSizeForBounds(NSMakeRect(CGFloat(0.0), CGFloat(0.0), CGFloat(FLT_MAX), height)).width

    return getnumber
   }
}

I hope someone finds these helpful. regards KGH

like image 5
ArtbyKGH Avatar answered Nov 18 '22 16:11

ArtbyKGH


This is ArtbyKGH's answer for Swift 4+:

extension NSTextField {
    func bestHeight(for text: String, width: CGFloat) -> CGFloat {
        stringValue = text
        let height = cell!.cellSize(forBounds: NSRect(x: 0, y: 0, width: width, height: .greatestFiniteMagnitude)).height

        return height
    }

    func bestWidth(for text: String, height: CGFloat) -> CGFloat {
        stringValue = text
        let width = cell!.cellSize(forBounds: NSRect(x: 0, y: 0, width: .greatestFiniteMagnitude, height: height)).width

        return width
    }
}
like image 4
Tamás Sengel Avatar answered Nov 18 '22 15:11

Tamás Sengel