How do I set the font size in a text cell so that the string fills the cell's rect?

后端 未结 6 1253
刺人心
刺人心 2020-12-30 15:13

I have a view that contains two NSTextFieldCells. The size at which these cells are drawn is derived from the size of the view, and I want the text in each cell

相关标签:
6条回答
  • 2020-12-30 15:52

    Sorry: it's been five years. Text width might no longer be the paramount concern in your waking life. However, I have the answer; maybe others will benefit.

    The crucial key to accurate text-width-sizing (and this works also for text-height) is to realize that the width of rendered text does of course vary - but linearly! - with the setting of the font-size attribute. There is no need to binary search, or to pick through and test all the possible font-size attribute values, when one has a linear function; one only needs to be sure of two points on the graph.

    To prepare, don't draw the string, but calculate the width of your rendered string at, for example, text size 20 and at text size 40. This gives you two data points on the linear function "rendered string width as a function of text-size attribute". Then, extrapolate to fit the string into whatever rendered width you currently need.

    I have found this method to uniformly yield good and fast results. With variations in font, of course, sometimes you might obtain characters that hang two or three pixels over the edge of the bounding box - but this is an artefact of font design. The well-designed fonts will work well, and even with crazy fonts, one usually has to only provide a few pixels' border of leeway.

    Here are the routines I used when I had this problem last month. Feel free to use this code.

    /******************************************************************************************/
    
    //
    //  text.m
    //
    
    /******************************************************************************************/
    
    @interface drawtext : NSObject {
    
      // name of the font to be used
      NSString *fontname;
    
      // instantiations of that font, at size 20 and at size 40, and at the currently-best size
      NSFont   *font20, *font40, *font;
    
      // first sizing function: rendered string height as a function of the font-size attribute
      CGFloat mh, bh; 
    
      // second sizing function: rendered string width as a function of the font-size attribute
      CGFloat mw, bw; 
    
    }
    
    @end
    
    /******************************************************************************************/
    
    @implementation drawtext
    
    /******************************************************************************************/
    
    // CLASS METHODS
    
    /******************************************************************************************/
    
    // The caller specifies the text string (all capitals! no descenders!) to be drawn, the
    // name of the font to use, the box in which to draw the text, and a border if desired.
    //
    // The routine returns the fontsize to be used, and the origin to be used for the
    // "drawAtPoint" message. This will result in the largest rendition of the text string
    // which meets the constraints.
    
    + (void) sizeText: (NSString *) captext   // the string of text to evaluate for font size
        usingFontName: (NSString *) fontname  // the string name of the font to be employed
                inBox: (NSRect)     box       // the containing box on the screen
           withBorder: (NSSize)     border    // the # of pixels to leave blank as X & Y borders
        usingFontSize: (CGFloat *)  fontsize  // (returned) what font-size to use
             atOrigin: (NSPoint *)  origin    // (returned) where to execute the drawAtPoint
    {
      // let's start by redefining the containing box to presume the borders
    
      NSRect newBox;
      newBox.origin.x    = box.origin.x + border.width;
      newBox.origin.y    = box.origin.y + border.height;
      newBox.size.width  = box.size.width - 2.0 * border.width;
      newBox.size.height = box.size.height - 2.0 * border.height;
    
      // find out dimensions at font size = 20, then at font size = 40, to use for extrapolation
    
      NSSize s20, s40;
    
      NSFont *f20 = [NSFont fontWithName:fontname size:20];
      NSMutableAttributedString *mtext20 = [[NSMutableAttributedString alloc] initWithString:captext];
      [mtext20 addAttribute:NSFontAttributeName value:f20 range:NSMakeRange(0,[mtext20 length])];
      s20.width  = mtext20.size.width;
      s20.height = f20.capHeight;
    
      NSFont *f40 = [NSFont fontWithName:fontname size:40];
      NSMutableAttributedString *mtext40 = [[NSMutableAttributedString alloc] initWithString:captext];
      [mtext40 addAttribute:NSFontAttributeName value:f40 range:NSMakeRange(0,[mtext40 length])];
      s40.width  = mtext40.size.width;
      s40.height = f40.capHeight;
    
      // hsize is "font size to cause height of rendered string to match box height"
      // wsize is "font size to cause width of rendered string to match box width"
    
      CGFloat x1, x2, y1, y2, m, b, hsize, wsize;
    
      // cap height as function of text size, in y = mx + b format
    
      x1 = 20;
      y1 = s20.height;
      x2 = 40;
      y2 = s40.height;
      m  = ( y2 - y1 ) / ( x2 - x1 );
      b  = y1 - ( m * x1 );
      hsize = ( newBox.size.height - b ) / m;
    
      // string len as function of text size, y = mx + b format
    
      x1 = 20;
      y1 = s20.width;
      x2 = 40;
      y2 = s40.width;
      m  = ( y2 - y1 ) / ( x2 - x1 );
      b  = y1 - ( m * x1 );
      wsize = ( newBox.size.width - b ) / m;
    
      // choose the lesser of the two extrapolated font-sizes to fit the string into the box,
      // and at the same time, find the origin point at which to render the string
      //
      // if ( hsize < wsize ) { // there will be east-west spaces
      // else { // there will be north-south spaces
    
      *fontsize = fmin( hsize, wsize );
    
      NSSize  textSize;
    
      NSMutableAttributedString *mtext = [[NSMutableAttributedString alloc] initWithString:captext];
      NSFont *f = [NSFont fontWithName:fontname size:*fontsize];
      [mtext addAttribute:NSFontAttributeName value:f range:NSMakeRange(0,[mtext length])];
      textSize.width  = mtext.size.width;
      textSize.height = f.capHeight;
    
      // don't forget "descender", as this is an all-caps string (strings with descenders are
      // left as an extra credit exercise for the reader :)
      origin->y = newBox.origin.y + f.descender + ( ( newBox.size.height / 2.0 ) - ( textSize.height / 2.0 ) );
      origin->x = ( newBox.origin.x + ( newBox.size.width / 2.0 ) ) - ( textSize.width / 2.0 );
    }
    
    /******************************************************************************************/
    
    // Like the previous routine, except the font size is specified by the caller (this is
    // employed in the case it is desired that various text strings, in different containing
    // boxes, are to be drawn in the same font size).
    
    + (void) placeText: (NSString *) captext   // the string of text to evaluate for positioning
         usingFontName: (NSString *) fontname  // the string name of the font to be employed
                 inBox: (NSRect)     box       // the containing box on the screen
            withBorder: (NSSize)     border    // the # of pixels to leave blank as X & Y borders
         usingFontSize: (CGFloat)    fontsize  // (passed) what font-size to use
              atOrigin: (NSPoint *)  origin    // (returned) where to execute the drawAtPoint
    {
      NSRect newBox;
    
      newBox.origin.x    = box.origin.x + border.width;
      newBox.origin.y    = box.origin.y + border.height;
      newBox.size.width  = box.size.width - 2.0 * border.width;
      newBox.size.height = box.size.height - 2.0 * border.height;
    
      NSSize  textSize;
    
      NSMutableAttributedString *mtext = [[NSMutableAttributedString alloc] initWithString:captext];
      NSFont *f = [NSFont fontWithName:fontname size:fontsize];
      [mtext addAttribute:NSFontAttributeName value:f range:NSMakeRange(0,[mtext length])];
      textSize.width  = mtext.size.width;
      textSize.height = f.capHeight;
    
      // don't forget "descender", as this is an all-caps string
      origin->y = newBox.origin.y + f.descender + ( ( newBox.size.height / 2.0 ) - ( textSize.height / 2.0 ) );
      origin->x = ( newBox.origin.x + ( newBox.size.width / 2.0 ) ) - ( textSize.width / 2.0 );
    }
    
    /******************************************************************************************/
    
    // This routine actually draws the text (the previous routines only determine how it
    // should be drawn).
    //
    // The second routine can be used to draw a string with attributes such as color (i.e., 
    // attributes which don't affect the size of the rendered string).
    
    + (void) drawText: (NSString *)  captext   // the string of text to be drawn
        usingFontName: (NSString *)  fontname  // the string name of the font to be employed
          andFontSize: (CGFloat)     fontsize  // what font-size to use
             atOrigin: (NSPoint)     origin    // where to execute the drawAtPoint
    {
      NSMutableAttributedString *mtext = [[NSMutableAttributedString alloc] initWithString:captext];
      NSFont *f = [NSFont fontWithName:fontname size:fontsize];
      [mtext addAttribute:NSFontAttributeName value:f range:NSMakeRange(0,[mtext length])];
      [mtext drawAtPoint:origin];
    }
    
    + (void) drawMText: (NSMutableAttributedString *) captext // the string of Mtext to be drawn
        usingFontName:  (NSString *)  fontname  // the string name of the font to be employed
          andFontSize:  (CGFloat)     fontsize  // what font-size to use
             atOrigin:  (NSPoint)     origin    // where to execute the drawAtPoint
    {
      NSFont *f = [NSFont fontWithName:fontname size:fontsize];
      [captext addAttribute:NSFontAttributeName value:f range:NSMakeRange(0,[captext length])];
      [captext drawAtPoint:origin];
    }
    
    /******************************************************************************************/
    
    // INSTANCE METHODS
    
    /******************************************************************************************/
    
    // When you instantiate the object, you set the font; from this, you can elucidate the 
    // first of the two sizing functions: rendered string height as a function of the 
    // font-size attribute. The function is stored in the instance variables of the object,
    // in the variables { mh, bh }, to be used with the classic "y(x) = mx + b" format, where:
    //
    //     y is rendered string height
    //     m is mh
    //     x is font size attribute
    //     b is bh
    
    - (id) initUsingFontName: (NSString *) fname   // string name of font to be employed
    {
      if ( !self ) self = [super init];
    
      fontname = [[NSString alloc] initWithString:fname];
    
      font20 = [NSFont fontWithName:fontname size:20];
      font40 = [NSFont fontWithName:fontname size:40];
    
      // "cap height as function of text size", in y = mx + b format (mh is m, bh is b)
    
      CGFloat x1, x2, y1, y2;
    
      x1 = 20;
      y1 = font20.capHeight;
      x2 = 40;
      y2 = font40.capHeight;
    
      mh = ( y2 - y1 ) / ( x2 - x1 );
      bh = y1 - ( mh * x1 );
    
      return self;
    }
    
    /******************************************************************************************/
    
    // After initializing the object, you size a text string; this stores a second sizing
    // function: rendered string width as a function of the font-size attribute, in { mw, bw }.
    
    - (void) sizeString: (NSString *) captext   // one string of text to evaluate for font size
    {
      CGFloat x1, x2, y1, y2;
    
      NSMutableAttributedString *mtext = 
        [[NSMutableAttributedString alloc] initWithString:captext];
    
      [mtext addAttribute:NSFontAttributeName 
                    value:font20 
                    range:NSMakeRange(0,[mtext length])];
    
      x1 = 20;
      y1 = mtext.size.width;
    
      [mtext addAttribute:NSFontAttributeName 
                    value:font40 
                    range:NSMakeRange(0,[mtext length])];
    
      x2 = 40;
      y2 = mtext.size.width;
    
      // "string width as function of text size", in y = mx + b format (mw is m, bw is b)
    
      mw = ( y2 - y1 ) / ( x2 - x1 );
      bw = y1 - ( mw * x1 );
    }
    
    /******************************************************************************************/
    
    // Then to draw the text string in a box, you use this routine, which will draw it at the 
    // largest size possible given all the constraints, including the provided box and border.
    //
    // A similar routine is provided following this one, to draw a mutable string which may
    // contain attributes, such as color, which do not affect the size of the rendered string.
    
    - (void) drawString: (NSString *) captext   // string of text to be drawn
                  inBox: (NSRect)     box       // containing box on the screen
             withBorder: (NSSize)     border    // # of pixels to leave blank as X & Y borders
    {
      NSRect newBox;
    
      newBox.origin.x    = box.origin.x + border.width;
      newBox.origin.y    = box.origin.y + border.height;
      newBox.size.width  = box.size.width - 2.0 * border.width;
      newBox.size.height = box.size.height - 2.0 * border.height;
    
      // solve linear sizing functions for text size, and choose the smaller text size
      //
      // if ( hsize < wsize ) there will be east-west spaces
      // if ( wsize < hsize ) there will be north-south spaces
    
      CGFloat hsize, wsize, fontsize;
    
      hsize = ( newBox.size.height - bh ) / mh;
      wsize = ( newBox.size.width  - bw ) / mw;
    
      fontsize = fmin( hsize, wsize );
    
      font = [NSFont fontWithName:fontname size:fontsize];
    
      NSMutableAttributedString *mtext = 
        [[NSMutableAttributedString alloc] initWithString:captext];
    
      [mtext addAttribute:NSFontAttributeName value:font range:NSMakeRange(0,[mtext length])];
    
      // find the origin-point at which to render the given string,
      // so that the text is centered in the box
    
      NSSize textSize;
    
      textSize.width  = mtext.size.width;
      textSize.height = font.capHeight;
    
      NSPoint origin;
    
      origin.y = newBox.origin.y + font.descender + 
                   ( ( newBox.size.height / 2.0 ) - ( textSize.height / 2.0 ) );
      origin.x = ( newBox.origin.x + ( newBox.size.width / 2.0 ) ) - ( textSize.width / 2.0 );
    
      [mtext drawAtPoint:origin];
    }
    
    /******************************************************************************************/
    
    // To draw a mutable text string in a box (a string containing attributes e.g. color, which
    // do not affect the sizing of the rendered string), use this routine.
    
    - (void) drawMString: (NSMutableAttributedString *) captext // the M-string to be drawn
                   inBox: (NSRect)     box       // containing box on the screen
              withBorder: (NSSize)     border    // # of pixels to leave blank as X & Y borders
    {
      NSRect newBox;
    
      newBox.origin.x    = box.origin.x + border.width;
      newBox.origin.y    = box.origin.y + border.height;
      newBox.size.width  = box.size.width - 2.0 * border.width;
      newBox.size.height = box.size.height - 2.0 * border.height;
    
      // solve linear sizing functions for text size, and choose the smaller text size
      //
      // if ( hsize < wsize ) there will be east-west spaces
      // if ( wsize < hsize ) there will be north-south spaces
    
      CGFloat hsize, wsize, fontsize;
    
      hsize = ( newBox.size.height - bh ) / mh;
      wsize = ( newBox.size.width  - bw ) / mw;
    
      fontsize = fmin( hsize, wsize );
    
      font = [NSFont fontWithName:fontname size:fontsize];
    
      [captext addAttribute:NSFontAttributeName 
                      value:font 
                      range:NSMakeRange(0,[captext length])];
    
      // find the origin-point at which to render the given string,
      // so that the text is centered in the box
    
      NSSize textSize;
    
      textSize.width  = captext.size.width;
      textSize.height = font.capHeight;
    
      NSPoint origin;
    
      origin.y = newBox.origin.y + font.descender + 
                   ( ( newBox.size.height / 2.0 ) - ( textSize.height / 2.0 ) );
      origin.x = ( newBox.origin.x + ( newBox.size.width / 2.0 ) ) - ( textSize.width / 2.0 );
    
      [captext drawAtPoint:origin];
    }
    
    /******************************************************************************************/
    
    @end
    
    /******************************************************************************************/
    
    0 讨论(0)
  • 2020-12-30 15:55

    It was recommended out of band that I try a binary search for an appropriate size. This is a very limited example of that:

    - (NSFont *)labelFontForText: (NSString *)text inRect: (NSRect)rect {
        CGFloat prevSize = 0.0, guessSize = 16.0, tempSize;
        NSFont *guessFont = nil;
        while (fabs(guessSize - prevSize) > 0.125) {
            guessFont = [NSFont labelFontOfSize: guessSize];
            NSSize textSize = [text sizeWithAttributes: 
                                [NSDictionary dictionaryWithObject: guessFont
                                                            forKey: NSFontAttributeName]];
            if (textSize.width > rect.size.width || 
                textSize.height > rect.size.height) {
                tempSize = guessSize - (guessSize - prevSize) / 2.0;
            }
            else {
                tempSize = guessSize + (guessSize - prevSize) / 2.0;
            }
            prevSize = guessSize;
            guessSize = tempSize;
        }
        return [[guessFont retain] autorelease];
    }
    

    The limitations (you'd better not need a 32pt or larger font, or anything that ain't Lucida Grande) are not important for my need, but certainly would put some people off using this method. I'll leave the question open, and accept a more robust approach.

    0 讨论(0)
  • 2020-12-30 15:56

    Two ideas. One I've tried, the other might work:

    1) Do like in this question: How to truncate an NSString based on the graphical width? i.e. just try out different sizes until it doesn't fit anymore

    2) Create the cell, give it the maximum rect and set it to fit its text into the cell, then ask it for its ideal size (there's a method on there that does that) then resize the cells again. At last if I understood your problem correctly.

    0 讨论(0)
  • 2020-12-30 16:05

    Here is a method that does not do guess and check. Depending on the font, a little padding may be necessary to prevent overflow (sizeWithAttributes doesn't scale perfectly). Boom!

    -(float)scaleToAspectFit:(CGSize)source into:(CGSize)into padding:(float)padding
    {
        return MIN((into.width-padding) / source.width, (into.height-padding) / source.height);
    }
    
    -(NSFont*)fontSizedForAreaSize:(NSSize)size withString:(NSString*)string usingFont:(NSFont*)font;
    {
        NSFont* sampleFont = [NSFont fontWithDescriptor:font.fontDescriptor size:12.];//use standard size to prevent error accrual
        CGSize sampleSize = [string sizeWithAttributes:[NSDictionary dictionaryWithObjectsAndKeys:sampleFont, NSFontAttributeName, nil]];
        float scale = [self scaleToAspectFit:sampleSize into:size padding:10];
        return [NSFont fontWithDescriptor:font.fontDescriptor size:scale * sampleFont.pointSize];
    }
    
    0 讨论(0)
  • 2020-12-30 16:05

    In my case I use the following:

    - (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView
    {
    
        //Create attributes
        NSColor *text_color = nil;
        NSFont *font = [self font];
        NSString *fontName = [font fontName];
        double fontSize = [font pointSize];
    
        NSInteger text_size = (int) fontSize;
    
        if([self isHighlighted])
            text_color = [NSColor colorWithCalibratedRed:1 green:1 blue:1 alpha:1];
        else
            text_color = [NSColor colorWithCalibratedRed:0 green:0 blue:0 alpha:1];
    
    
        NSDictionary *attributes = [NSDictionary dictionaryWithObjectsAndKeys:
                                    [NSFont fontWithName:fontName size:fontSize], NSFontAttributeName,
                                    text_color, NSForegroundColorAttributeName,
                                    nil];
    
    
        NSAttributedString * currentText=[[NSAttributedString alloc] initWithString:[self title] attributes: attributes];
    
        NSSize attrSize = [currentText size];
    
        while (attrSize.width > cellFrame.size.width && --text_size > 0) {
    
    
            attributes = [NSDictionary dictionaryWithObjectsAndKeys:
                          [NSFont fontWithName:fontName size:text_size], NSFontAttributeName,
                          text_color, NSForegroundColorAttributeName,
                          nil];
    
            currentText=[[NSAttributedString alloc] initWithString:[self title] attributes: attributes];
    
            attrSize = [currentText size];
    
        }
    
        switch ([self alignment]) {
            default:
            case NSLeftTextAlignment:
                [currentText drawAtPoint:NSMakePoint( cellFrame.origin.x,
                                                     cellFrame.origin.y + (cellFrame.size.height/2) - (attrSize.height/2))];
                break;
    
            case NSRightTextAlignment:
                [currentText drawAtPoint:NSMakePoint( cellFrame.origin.x + (cellFrame.size.width) - (attrSize.width),
                                                     cellFrame.origin.y + (cellFrame.size.height/2) - (attrSize.height/2))];
                break;
    
            case NSCenterTextAlignment:
                [currentText drawAtPoint:NSMakePoint( cellFrame.origin.x + (cellFrame.size.width /2) - (attrSize.width/2),
                                                     cellFrame.origin.y + (cellFrame.size.height/2) - (attrSize.height/2))];
                break;
    
    
        }
    
    
    
    
    }
    
    0 讨论(0)
  • 2020-12-30 16:10

    I use some similar code but it handles different fonts, sizes up to 10,000 and takes into account the available height as well as width of the area the text is being displayed in.

    #define kMaxFontSize    10000
    
    - (CGFloat)fontSizeForAreaSize:(NSSize)areaSize withString:(NSString *)stringToSize usingFont:(NSString *)fontName;
    {
        NSFont * displayFont = nil;
        NSSize stringSize = NSZeroSize;
        NSMutableDictionary * fontAttributes = [[NSMutableDictionary alloc] init];
    
        if (areaSize.width == 0.0 || areaSize.height == 0.0) {
            return 0.0;
        }
    
        NSUInteger fontLoop = 0;
        for (fontLoop = 1; fontLoop <= kMaxFontSize; fontLoop++) {
            displayFont = [[NSFontManager sharedFontManager] convertWeight:YES ofFont:[NSFont fontWithName:fontName size:fontLoop]];
            [fontAttributes setObject:displayFont forKey:NSFontAttributeName];
            stringSize = [stringToSize sizeWithAttributes:fontAttributes];
    
            if (stringSize.width > areaSize.width)
                break;
            if (stringSize.height > areaSize.height)
                break;
        }
    
        [fontAttributes release], fontAttributes = nil;
    
        return (CGFloat)fontLoop - 1.0;
    }
    
    0 讨论(0)
提交回复
热议问题