PawelPawel
PawelPawel

Reputation: 77

Calculate height of the text container

In order to display views in NSTableView I have to calculate of text in NSTextField. I'm using this method:

float heightForStringDrawing(NSString *myString, NSFont *myFont,float myWidth) {
    NSTextStorage *textStorage = [[NSTextStorage alloc] initWithString:myString];
    NSTextContainer *textContainer = [[NSTextContainer alloc] initWithContainerSize:NSMakeSize(myWidth, FLT_MAX)];
    NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
    [layoutManager addTextContainer:textContainer];
    [textStorage addLayoutManager:layoutManager];
    [textStorage addAttribute:NSFontAttributeName value:myFont
                        range:NSMakeRange(0, [textStorage length])];


    (void) [layoutManager glyphRangeForTextContainer:textContainer];
    return [layoutManager usedRectForTextContainer:textContainer].size.height;
}

In case of line liners it works good, but when I have more then one line for the exact width of my view height is too small. I've been looking for answer for hours but I still don't have anything. Here's the screen shot:

enter image description here

I'm adding ten to the result of the function because of the padding. As you can see for short message, it works good, for longer it's cropped. Thank you for your help.

Upvotes: 0

Views: 816

Answers (2)

Ken Thomases
Ken Thomases

Reputation: 90521

You didn't show the code which calls your heightForStringDrawing() function. What is the width that you're providing? If it's the width of the text field, then that's wrong, because the text within the text field isn't allowed to go all the way to the edges of the text field. There's horizontal padding, too.

Anyway, there's a more straightforward approach. Create a text field matching the properties of the one you're trying to measure. Set its stringValue and font. Set its preferredMaxLayoutWidth to the available width. (Note this is in terms of the alignment rect, not the frame rect. Use -alignmentRectForFrame: if you need to convert.) Now, ask it for its intrinsicSize. You then have to convert that from alignment rect to frame rect using -frameForAlignmentRect:.

Depending on your situation, you can duplicate an existing text field by archiving and unarchiving it, or you can load the NIB which contains it to obtain it.


One way to obtain a text field with the necessary characteristics is to clone an existing one:

NSData* data = [NSKeyedArchiver archivedDataWithRootObject:_label];
NSTextField* clone = [NSKeyedUnarchiver unarchiveObjectWithData:data];

To calculate the needed frame height to fit in a given width:

NSRect rect = { NSZeroPoint, NSMakeSize(desiredWidth, 100 /* arbitrary */) };
rect = [clone alignmentRectForFrame:rect];
clone.preferredMaxLayoutWidth = NSWidth(rect);
rect.size = clone.intrinsicContentSize;
rect = [clone frameForAlignmentRect:rect];
CGFloat resultingHeight = NSHeight(rect);

Now, in you screenshot, the row height has to be taller than the text field height because there's a margin there. How much taller depends on your layout. Since you mention constraints in your comment, you're presumably using auto layout. In that case, the margins (constraint distances) would actually be applied to the alignment rect of the text field. So, you would skip the second to last line of code. That is, the row height would be the text field's intrinsic height (not adjusted) plus the margins imposed by your constraints.

To avoid a lot of this stuff with fiddling with margins, you should perhaps work on a complete view hierarchy matching your table cell view. So, you'd load your table cell view NIB directly. You'd set the text field's content as appropriate (and whatever other content your cell view's subviews require). You'd apply a width constraint to the cell view itself to simulate the column width. Then, you'd ask the cell view for its fittingSize. Let the auto layout system compute the overall height, including margins and everything implied by your constraints, from the content.

Of course, this only works if the text field is using preferredMaxLayoutWidth. Otherwise, it won't increase its intrinsic height in response to wrapping. Since you don't want to hard-code the preferred width or margin sizes, you can do two passes. After setting the content for the controls within the cell view and adding the width constraint, call -layoutSubtreeIfNeeded. Then, query the text field's frame, convert it to an alignment rect, set the preferredMaxLayoutWidth to the alignment width, and only then query the cell view's fittingSize.

Upvotes: 1

PawelPawel
PawelPawel

Reputation: 77

Perfectly working solution for me:

// creating temporary cell just to take height from it later
NSTextFieldCell *tmp = [NSTextFieldCell new];
// settings proper values from out NSTextField which we want to check height for
[tmp setStringValue:s];
[tmp setFont:font];
NSRect rect;
rect.size.height=FLT_MAX;
// width which we want to check height for
// in my case 100 (width of nstextfield), -4 (minus left and right margin)
rect.size.width=100-4; 
// getting needed height
h = [tmp cellSizeForBounds:rect].height;

Upvotes: 0

Related Questions