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

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 to be the largest that will fit in the derived size of the cell. Here's what I have, which doesn't set the font size:
- (void)drawRect:(NSRect)dirtyRect {
/*
* Observant readers will notice that I update the whole view here. If
* there is a perceived performance problem, then I'll switch to just
* updating the dirty rect.
*/
NSRect boundsRect = self.bounds;
const CGFloat monthHeight = 0.25 * boundsRect.size.height;
NSRect monthRect = NSMakeRect(boundsRect.origin.x,
boundsRect.origin.y + boundsRect.size.height
- monthHeight,
boundsRect.size.width,
monthHeight);
[monthCell drawWithFrame: monthRect inView: self];
NSRect dayRect = NSMakeRect(boundsRect.origin.x,
boundsRect.origin.y,
boundsRect.size.width,
boundsRect.size.height - monthHeight);
[dayCell drawWithFrame: dayRect inView: self];
[[NSColor blackColor] set];
[NSBezierPath strokeRect: boundsRect];
}
So I know that I can ask a string what size it would take for given attributes, and I know that I can ask a control to change its size to fit its content. Neither of those seems applicable: I want the content (in this case, the cell's stringValue) to size to fit the known rect dimensions, with the attributes needed to achieve that being unknown. How can I find the needed size? Assume that I know what font I'll be using (because I do).
update Note: I don't want to truncate the string, I want to grow or shrink it so that the whole thing fits, with the largest text size possible, into the provided rect.

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;
}

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.

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];
}

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.

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;
}
}

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
/******************************************************************************************/

Related

Xcode UI on background thread to render image

I'm rendering an image with text for one of my apps and has a noticeable impact on UI performance (can be as big as ~1 second freeze), so I am doing it on a background thread. Since the image has text, using UILabels and other UIViews makes it easy to lay everything out, and I render the view containing everything to an image.
However, I get a warning from Xcode saying that it's not allowed on the background thread because it uses UIKit. Why am I not allowed to call UIKit on the background thread even though my use case is completely self-contained and isolated from any rendering onscreen?
To help the code below make more sense, it draws an image that is a listing of several items, each of which consists of two small square images and the name of the item all in a row. The list can have several columns. The code has been tweaked slightly (mostly variable names) to avoid showing proprietary code, but does the same job.
My code:
NSArray<MyItem*>* items; // These are the items that I'm drawing. They
// get set before the following code is called.
// Processing code:
const CGFloat TITLE_FONT_SIZE = 50; // font size of the title
const CGFloat ITEM_FONT_SIZE = 25; // font size of the item names
const int OUTER_PADDING = 60; // padding from the edge of the image to the main content
const int ROW_PADDING = 13; // padding between rows
const int COL_PADDING = 100; // padding between columns
const int PADDING = 20; // padding between content items in a row
const int BOX_SIZE = 25; // how high/wide each image is
const int ROW_HEIGHT = BOX_SIZE; // pixel height of a line
const int COL_WIDTH = 500; // pixel width of a column (image1, image2, and name)
// compute the dimensions of the image
UILabel* titleLabel = [[UILabel alloc] init];
titleLabel.font = [UIFont systemFontOfSize:TITLE_FONT_SIZE];
titleLabel.text = #"My image";
[titleLabel sizeToFit];
titleLabel.frame = CGRectMake(OUTER_PADDING, OUTER_PADDING / 2, titleLabel.frame.size.width, titleLabel.frame.size.height);
const int MIN_NUM_COLS = 1 + ((titleLabel.frame.size.width - COL_WIDTH) / (COL_WIDTH + COL_PADDING));
const int NORMAL_NUM_COLS = (int)ceil(sqrt([items count] / (COL_WIDTH / (ROW_HEIGHT))));
const int NUM_COLS = (MIN_NUM_COLS > NORMAL_NUM_COLS ? MIN_NUM_COLS : NORMAL_NUM_COLS);
const int NUM_ROWS = (int)ceil([items count] / (float)NUM_COLS);
const int NUM_OVERFLOW_ROWS = [items count] % NUM_ROWS;
const int titleWidth = titleLabel.frame.size.width;
const int defaultWidth = (NUM_COLS * (COL_WIDTH + COL_PADDING)) - COL_PADDING;
const int pixelWidth = (2 * OUTER_PADDING) + (titleWidth > defaultWidth ? titleWidth : defaultWidth);
const int pixelHeight = (2 * OUTER_PADDING) + (TITLE_FONT_SIZE + PADDING) + (NUM_ROWS * (ROW_HEIGHT + ROW_PADDING)) - ROW_PADDING;
const int nbytes = 4 * pixelHeight * pixelWidth;
byte* data = (byte*)malloc(sizeof(byte) * nbytes);
memset(data, 255, nbytes);
CGContextRef context = CGBitmapContextCreate(data, pixelWidth, pixelHeight, 8, 4 * pixelWidth, CGColorSpaceCreateDeviceRGB(), kCGBitmapByteOrderDefault | kCGImageAlphaNoneSkipLast);
// --------------------------------------------------
// create a view heirarchy and then draw to our context
UIView* mainView = [[UIView alloc] init];
[mainView addSubview:titleLabel];
// setup all the views
int keyIndex = 0;
CGFloat x = OUTER_PADDING;
CGFloat starty = titleLabel.frame.origin.y + titleLabel.frame.size.height + PADDING;
for (int col = 0; col < NUM_COLS; col++)
{
int nrows = (col == NUM_COLS + 1 ? NUM_OVERFLOW_ROWS : NUM_ROWS);
CGFloat y = starty;
for (int row = 0; (row < nrows) && (keyIndex < [items count]); row++)
{
CGFloat tempx = x;
MyItem* item = [items objectAtIndex:keyIndex];
UIImageView* imageview1 = [[UIImageView alloc] initWithImage:item.image1];
imageview1.frame = CGRectMake(tempx, y, BOX_SIZE, BOX_SIZE);
[mainView addSubview:imageview1];
tempx += BOX_SIZE + PADDING;
UIImageView* imageview2 = [[UIImageView alloc] initWithImage:item.imageview2];
imageview2.frame = CGRectMake(tempx, y, BOX_SIZE, BOX_SIZE);
[mainView addSubview:imageview2];
tempx += BOX_SIZE + PADDING;
UILabel* label = [[UILabel alloc] init];
label.font = [UIFont systemFontOfSize:ITEM_FONT_SIZE];
label.text = item.name;
[label sizeToFit];
label.center = CGPointMake(tempx + (label.frame.size.width / 2), imageview2.center.y);
[mainView addSubview:label];
y += ROW_HEIGHT + ROW_PADDING;
keyIndex++;
}
x += COL_WIDTH + COL_PADDING;
}
// --------------------------------------------------
// draw everything to actually generate the image
CGContextConcatCTM(context, CGAffineTransformMake(1, 0, 0, -1, 0, pixelHeight));
[mainView.layer renderInContext:context];
CGImageRef cgimage = CGBitmapContextCreateImage(context);
myCoolImage = [UIImage imageWithCGImage:cgimage];
CGImageRelease(cgimage);
CGContextRelease(context);
free(data);
As we've established in comments, what you're doing is both illegitimate and slow.
Arranging and sizing UILabel and UIImageView objects is slow, and calling
CALayer renderInContext is really slow.
And it isn't how you draw.
Everything you're doing has its analogue in the actual drawing world (Quartz 2D), and if you did it that way, not only would it be legal in the background, it probably wouldn't even need to be in the background because it would be so much faster. So:
Every place you use a UILabel, you can achieve exactly the same effect by using NSAttributedString draw... commands.
Every place you use a UIImageView, you can achieve exactly the same effect by using UIImage draw... commands.
Any of us who does any extensive drawing has learned to create structured layouts of the type you're making by using actual drawing code, and now is your chance to learn to do it too.

How is transparency achieved in cocoa applications

I am trying to understand how is transparency actually implemented in cocoa applications. I was expecting the standard blending equation to be used i.e.
BlendedColour = alpha * layerColour + (1-alpha)*backgroundColour
However, I noticed that there is the slight difference in the blended colour expected if the above equation is used. To verify it, I did a small experiment as follows:
1.) Created a window, added a transparency of 0.8 to the window and grabbed a screenshot.
2.) I took a screenshot of the part of the screen where I am overlaying the window in step one without the window and overlayed the same image as in step 1, using the equation mentioned above. (I used openCV for that).
There is a slight difference in the colours for the two images, if you look closely. I wanted to understand what is causing the difference.
Resources:
1.) Images from Step 1 and Step2 respectively
2.) Code used in step 1
NSRect windowRect = {0,0,200,200};
m_NSWindow = [[NSWindow alloc] initWithContentRect:windowRect styleMask:NSBorderlessWindowMask backing:NSBackingStoreBuffered defer:NO];
[m_NSWindow setTitle:#"overlayWindow"];
[m_NSWindow makeKeyAndOrderFront:nil];
g_imageView = [[NSImageView alloc] initWithFrame:NSMakeRect(0,0,200,200)];
[m_NSWindow.contentView addSubview:g_imageView];
[m_NSWindow setOpaque:NO];
[m_NSWindow setAlphaValue:0.8];
NSBitmapImageRep* imageRep = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:nil
pixelsWide:200
pixelsHigh:200
bitsPerSample:8
samplesPerPixel:4
hasAlpha:YES
isPlanar:NO
colorSpaceName:NSDeviceRGBColorSpace
bitmapFormat:NSAlphaNonpremultipliedBitmapFormat
bytesPerRow:(200*4)
bitsPerPixel:32];
memcpy(imageRep.bitmapData,m_paintBuffer.data,160000);
NSSize imageSize = NSMakeSize(200,200);
NSImage* myImage = [[NSImage alloc] initWithSize: imageSize];
[myImage addRepresentation:imageRep];
[g_imageView setImage:myImage];
4.) Code for step 2
void overlayImage(const cv::Mat &background, const cv::Mat &foreground,
cv::Mat &output, cv::Point2i location)
{
background.copyTo(output);
// start at the row indicated by location, or at row 0 if location.y is negative.
for (int y = max(location.y, 0); y < background.rows; ++y)
{
int fY = y - location.y; // because of the translation
// we are done of we have processed all rows of the foreground image.
if (fY >= foreground.rows)
break;
// start at the column indicated by location,
// or at column 0 if location.x is negative.
for (int x = max(location.x, 0); x < background.cols; ++x)
{
int fX = x - location.x; // because of the translation.
// we are done with this row if the column is outside of the foreground image.
if (fX >= foreground.cols)
break;
// determine the opacity of the foregrond pixel, using its fourth (alpha) channel.
double opacity =
((double)foreground.data[fY * foreground.step + fX * foreground.channels() + 3])
/ 255.;
// and now combine the background and foreground pixel, using the opacity,
// but only if opacity > 0.
for (int c = 0; opacity > 0 && c < output.channels(); ++c)
{
unsigned char foregroundPx =
foreground.data[fY * foreground.step + fX * foreground.channels() + c];
unsigned char backgroundPx =
background.data[y * background.step + x * background.channels() + c];
output.data[y*output.step + output.channels()*x + c] =
backgroundPx * (1. - opacity) + foregroundPx * opacity;
}
}
}
}

calculate height and width based on text , font , text color in xcode

what is the best way to calculate (formated text including text size , text color , text font) proper height and width of text field in a fixed UIView , including word wrap etc
in the following image text is not right align Check the Image
in the following image text is not properly showing check the image
i am calculating image height and width with the following code
NSString* text=[textField stringValue];
NSDictionary *attributes;
NSTextView* textView =[[NSTextView alloc] init];
[textView setString:text];
attributes = #{NSFontAttributeName : [NSFont fontWithName:fontName size:fontValue], NSForegroundColorAttributeName : [NSColor colorWithCalibratedRed:redValueTextColor green:GreenValueTextColor blue:blueValueTextColor alpha:1], NSBackgroundColorAttributeName : [NSColor colorWithCalibratedRed:redValueTextBackgroundColor green:GreenValueTextBackgroundColor blue:blueValueTextBackgroundColor alpha:1]};
textView.backgroundColor=[NSColor colorWithCalibratedRed:redValueTextBackgroundColor green:GreenValueTextBackgroundColor blue:blueValueTextBackgroundColor alpha:1];
NSInteger maxWidth = 600;
NSInteger maxHeight = 20000;
CGSize constraint = CGSizeMake(maxWidth, maxHeight);
NSRect newBounds = [text boundingRectWithSize:constraint options:NSLineBreakByCharWrapping|NSStringDrawingUsesFontLeading attributes:attributes];
textView.frame = NSMakeRect(textView.frame.origin.x, textView.frame.origin.y, newBounds.size.width, newBounds.size.height);
textView =[NSColor colorWithCalibratedRed:redValueTextColor green:GreenValueTextColor blue:blueValueTextColor alpha:1];
[textView setFont:[NSFont fontWithName:fontName size:fontValue]];
Core Text solution (note, maxWidth allows wrapping if you want it):
(CGSize)sizeForText:(NSAttributedString *)string maxWidth:(CGFloat)width
{
CTTypesetterRef typesetter = CTTypesetterCreateWithAttributedString((__bridge CFAttributedStringRef)string);
CFIndex offset = 0, length;
CGFloat y = 0, lineHeight;
do {
length = CTTypesetterSuggestLineBreak(typesetter, offset, width);
CTLineRef line = CTTypesetterCreateLine(typesetter, CFRangeMake(offset, length));
CGFloat ascent, descent, leading;
CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
CFRelease(line);
offset += length;
ascent = roundf(ascent);
descent = roundf(descent);
leading = roundf(leading);
lineHeight = ascent + descent + leading;
lineHeight = lineHeight + ((leading > 0) ? 0 : roundf(0.2*lineHeight)); //add 20% space
y += lineHeight;
} while (offset < [string length]);
CFRelease(typesetter);
return CGSizeMake(width, y);
}

Need sample code to swing needle in Cocoa/Quartz 2d Speedometer for Mac App

I'm building this to run on the Mac, not iOS - which is quit different. I'm almost there with the speedo, but the math of making the needle move up and down the scale as data is input eludes me.
I'm measuring wind speed live, and want to display it as a gauge - speedometer, with the needle moving as the windspeed changes. I have the fundamentals ok. I can also - and will - load the images into holders, but later. For now I want to get it working ...
- (void)drawRect:(NSRect)rect
{
NSRect myRect = NSMakeRect ( 21, 21, 323, 325 ); // set the Graphics class square size to match the guage image
[[NSColor blueColor] set]; // colour it in in blue - just because you can...
NSRectFill ( myRect );
[[NSGraphicsContext currentContext] // set up the graphics context
setImageInterpolation: NSImageInterpolationHigh]; // highres image
//-------------------------------------------
NSSize viewSize = [self bounds].size;
NSSize imageSize = { 320, 322 }; // the actual image rectangle size. You can scale the image here if you like. x and y remember
NSPoint viewCenter;
viewCenter.x = viewSize.width * 0.50; // set the view center, both x & y
viewCenter.y = viewSize.height * 0.50;
NSPoint imageOrigin = viewCenter;
imageOrigin.x -= imageSize.width * 0.50; // set the origin of the first point
imageOrigin.y -= imageSize.height * 0.50;
NSRect destRect;
destRect.origin = imageOrigin; // set the image origin
destRect.size = imageSize; // and size
NSString * file = #"/Users/robert/Documents/XCode Projects/xWeather Graphics/Gauge_mph_320x322.png"; // stuff in the image
NSImage * image = [[NSImage alloc] initWithContentsOfFile:file];
//-------------------------------------------
NSSize view2Size = [self bounds].size;
NSSize image2Size = { 149, 17 }; // the orange needle
NSPoint view2Center;
view2Center.x = view2Size.width * 0.50; // set the view center, both x & y
view2Center.y = view2Size.height * 0.50;
NSPoint image2Origin = view2Center;
//image2Origin.x -= image2Size.width * 0.50; // set the origin of the first point
image2Origin.x = 47;
image2Origin.y -= image2Size.height * 0.50;
NSRect dest2Rect;
dest2Rect.origin = image2Origin; // set the image origin
dest2Rect.size = image2Size; // and size now is needle size
NSString * file2 = #"/Users/robert/Documents/XCode Projects/xWeather Graphics/orange-needle01.png";
NSImage * image2 = [[NSImage alloc] initWithContentsOfFile:file2];
// do image 1
[image setFlipped:YES]; // flip it because everything else is in this exerecise
// do image 2
[image2 setFlipped:YES]; // flip it because everything else is in this exerecise
[image drawInRect: destRect
fromRect: NSZeroRect
operation: NSCompositeSourceOver
fraction: 1.0];
[image2 drawInRect: dest2Rect
fromRect: NSZeroRect
operation: NSCompositeSourceOver
fraction: 1.0];
NSBezierPath * path = [NSBezierPath bezierPathWithRect:destRect]; // draw a red border around the whole thing
[path setLineWidth:3];
[[NSColor redColor] set];
[path stroke];
}
// flip the ocords
- (BOOL) isFlipped { return YES; }
#end
The result is here. The gauge part that is. Now all I have to do is make the needle move in response to input.
Apple has some sample code, called SpeedometerView, which does exactly what you're asking. It'll surely take some doing to adapt it for your use, but it's probably a decent starting point.

How to get frame for NSStatusItem

Is it possible to get the frame of a NSStatusItem after I've added it to the status bar in Cocoa? When my app is launched, I am adding an item to the system status bar, and would like to know where it was positioned, is possible.
The following seems to work - I have seen similar solutions for iOS applications and supposedly they permit submission to the app store because you are still using standard SDK methods.
NSRect frame = [[statusBarItem valueForKey:#"window"] frame];
With 10.10, NSStatusItem has a button property that be used to get the status item position without setting a custom view.
NSStatusBarButton *statusBarButton = [myStatusItem button];
NSRect rectInWindow = [statusBarButton convertRect:[statusBarButton bounds] toView:nil];
NSRect screenRect = [[statusBarButton window] convertRectToScreen:rectInWindow];
NSLog(#"%#", NSStringFromRect(screenRect));
You can use statusItem.button.superview?.window?.frame in swift
If you have set a custom view on the status item:
NSRect statusRect = [[statusItem view] frame];
NSLog(#"%#", [NSString stringWithFormat:#"%.1fx%.1f",statusRect.size.width, statusRect.size.height]);
Otherwise I don't think it's possible using the available and documented APIs.
Edit: Incorporated comments.
It's possible to do this without any private API. Here's a category for NSScreen. This uses image analysis to locate the status item's image on the menu bar. Fortunately, computers are really fast. :)
As long as you know what the status item's image looks like, and can pass it in as an NSImage, this method should find it.
Works for dark mode as well as regular mode. Note that the image you pass in must be black. Colored images will probably not work so well.
#implementation NSScreen (LTStatusItemLocator)
// Find the location of IMG on the screen's status bar.
// If the image is not found, returns NSZeroPoint
- (NSPoint)originOfStatusItemWithImage:(NSImage *)IMG
{
CGColorSpaceRef csK = CGColorSpaceCreateDeviceGray();
NSPoint ret = NSZeroPoint;
CGDirectDisplayID screenID = 0;
CGImageRef displayImg = NULL;
CGImageRef compareImg = NULL;
CGRect screenRect = CGRectZero;
CGRect barRect = CGRectZero;
uint8_t *bm_bar = NULL;
uint8_t *bm_bar_ptr;
uint8_t *bm_compare = NULL;
uint8_t *bm_compare_ptr;
size_t bm_compare_w, bm_compare_h;
BOOL inverted = NO;
int numberOfScanLines = 0;
CGFloat *meanValues = NULL;
int presumptiveMatchIdx = -1;
CGFloat presumptiveMatchMeanVal = 999;
// If the computer is set to Dark Mode, set the "inverted" flag
NSDictionary *globalPrefs = [[NSUserDefaults standardUserDefaults] persistentDomainForName:NSGlobalDomain];
id style = globalPrefs[#"AppleInterfaceStyle"];
if ([style isKindOfClass:[NSString class]]) {
inverted = (NSOrderedSame == [style caseInsensitiveCompare:#"dark"]);
}
screenID = (CGDirectDisplayID)[self.deviceDescription[#"NSScreenNumber"] integerValue];
screenRect = CGDisplayBounds(screenID);
// Get the menubar rect
barRect = CGRectMake(0, 0, screenRect.size.width, 22);
displayImg = CGDisplayCreateImageForRect(screenID, barRect);
if (!displayImg) {
NSLog(#"Unable to create image from display");
CGColorSpaceRelease(csK);
return ret; // I would normally use goto(bail) here, but this is public code so let's not ruffle any feathers
}
size_t bar_w = CGImageGetWidth(displayImg);
size_t bar_h = CGImageGetHeight(displayImg);
// Determine scale factor based on the CGImageRef we got back from the display
CGFloat scaleFactor = (CGFloat)bar_h / (CGFloat)22;
// Greyscale bitmap for menu bar
bm_bar = malloc(1 * bar_w * bar_h);
{
CGContextRef bmCxt = NULL;
bmCxt = CGBitmapContextCreate(bm_bar, bar_w, bar_h, 8, 1 * bar_w, csK, kCGBitmapAlphaInfoMask&kCGImageAlphaNone);
// Draw the menu bar in grey
CGContextDrawImage(bmCxt, CGRectMake(0, 0, bar_w, bar_h), displayImg);
uint8_t minVal = 0xff;
uint8_t maxVal = 0x00;
// Walk the bitmap
uint64_t running = 0;
for (int yi = bar_h / 2; yi == bar_h / 2; yi++)
{
bm_bar_ptr = bm_bar + (bar_w * yi);
for (int xi = 0; xi < bar_w; xi++)
{
uint8_t v = *bm_bar_ptr++;
if (v < minVal) minVal = v;
if (v > maxVal) maxVal = v;
running += v;
}
}
running /= bar_w;
uint8_t threshold = minVal + ((maxVal - minVal) / 2);
//threshold = running;
// Walk the bitmap
bm_bar_ptr = bm_bar;
for (int yi = 0; yi < bar_h; yi++)
{
for (int xi = 0; xi < bar_w; xi++)
{
// Threshold all the pixels. Values > 50% go white, values <= 50% go black
// (opposite if Dark Mode)
// Could unroll this loop as an optimization, but probably not worthwhile
*bm_bar_ptr = (*bm_bar_ptr > threshold) ? (inverted?0x00:0xff) : (inverted?0xff:0x00);
bm_bar_ptr++;
}
}
CGImageRelease(displayImg);
displayImg = CGBitmapContextCreateImage(bmCxt);
CGContextRelease(bmCxt);
}
{
CGContextRef bmCxt = NULL;
CGImageRef img_cg = NULL;
bm_compare_w = scaleFactor * IMG.size.width;
bm_compare_h = scaleFactor * 22;
// Create out comparison bitmap - the image that was passed in
bmCxt = CGBitmapContextCreate(NULL, bm_compare_w, bm_compare_h, 8, 1 * bm_compare_w, csK, kCGBitmapAlphaInfoMask&kCGImageAlphaNone);
CGContextSetBlendMode(bmCxt, kCGBlendModeNormal);
NSRect imgRect_og = NSMakeRect(0,0,IMG.size.width,IMG.size.height);
NSRect imgRect = imgRect_og;
img_cg = [IMG CGImageForProposedRect:&imgRect context:nil hints:nil];
CGContextClearRect(bmCxt, imgRect);
CGContextSetFillColorWithColor(bmCxt, [NSColor whiteColor].CGColor);
CGContextFillRect(bmCxt, CGRectMake(0,0,9999,9999));
CGContextScaleCTM(bmCxt, scaleFactor, scaleFactor);
CGContextTranslateCTM(bmCxt, 0, (22. - IMG.size.height) / 2.);
// Draw the image in grey
CGContextSetFillColorWithColor(bmCxt, [NSColor blackColor].CGColor);
CGContextDrawImage(bmCxt, imgRect, img_cg);
compareImg = CGBitmapContextCreateImage(bmCxt);
CGContextRelease(bmCxt);
}
{
// We start at the right of the menu bar, and scan left until we find a good match
int numberOfScanLines = barRect.size.width - IMG.size.width;
bm_compare = malloc(1 * bm_compare_w * bm_compare_h);
// We use the meanValues buffer to keep track of how well the image matched for each point in the scan
meanValues = calloc(sizeof(CGFloat), numberOfScanLines);
// Walk the menubar image from right to left, pixel by pixel
for (int scanx = 0; scanx < numberOfScanLines; scanx++)
{
// Optimization, if we recently found a really good match, bail on the loop and return it
if ((presumptiveMatchIdx >= 0) && (scanx > (presumptiveMatchIdx + 5))) {
break;
}
CGFloat xOffset = numberOfScanLines - scanx;
CGRect displayRect = CGRectMake(xOffset * scaleFactor, 0, IMG.size.width * scaleFactor, 22. * scaleFactor);
CGImageRef displayCrop = CGImageCreateWithImageInRect(displayImg, displayRect);
CGContextRef compareCxt = CGBitmapContextCreate(bm_compare, bm_compare_w, bm_compare_h, 8, 1 * bm_compare_w, csK, kCGBitmapAlphaInfoMask&kCGImageAlphaNone);
CGContextSetBlendMode(compareCxt, kCGBlendModeCopy);
// Draw the image from our menubar
CGContextDrawImage(compareCxt, CGRectMake(0,0,IMG.size.width * scaleFactor, 22. * scaleFactor), displayCrop);
// Blend mode difference is like an XOR
CGContextSetBlendMode(compareCxt, kCGBlendModeDifference);
// Draw the test image. Because of blend mode, if we end up with a black image we matched perfectly
CGContextDrawImage(compareCxt, CGRectMake(0,0,IMG.size.width * scaleFactor, 22. * scaleFactor), compareImg);
CGContextFlush(compareCxt);
// Walk through the result image, to determine overall blackness
bm_compare_ptr = bm_compare;
for (int i = 0; i < bm_compare_w * bm_compare_h; i++)
{
meanValues[scanx] += (CGFloat)(*bm_compare_ptr);
bm_compare_ptr++;
}
meanValues[scanx] /= (255. * (CGFloat)(bm_compare_w * bm_compare_h));
// If the image is very dark, it matched well. If the average pixel value is < 0.07, we consider this
// a presumptive match. Mark it as such, but continue looking to see if there's an even better match.
if (meanValues[scanx] < 0.07) {
if (meanValues[scanx] < presumptiveMatchMeanVal) {
presumptiveMatchMeanVal = meanValues[scanx];
presumptiveMatchIdx = scanx;
}
}
CGImageRelease(displayCrop);
CGContextRelease(compareCxt);
}
}
// After we're done scanning the whole menubar (or we bailed because we found a good match),
// return the origin point.
// If we didn't match well enough, return NSZeroPoint
if (presumptiveMatchIdx >= 0) {
ret = CGPointMake(CGRectGetMaxX(self.frame), CGRectGetMaxY(self.frame));
ret.x -= (IMG.size.width + presumptiveMatchIdx);
ret.y -= 22;
}
CGImageRelease(displayImg);
CGImageRelease(compareImg);
CGColorSpaceRelease(csK);
if (bm_bar) free(bm_bar);
if (bm_compare) free(bm_compare);
if (meanValues) free(meanValues);
return ret;
}
#end
you can hack the window ivar like this :
#interface NSStatusItem (Hack)
- (NSRect)hackFrame;
#end
#implementation NSStatusItem (Hack)
- (NSRect)hackFrame
{
int objSize = class_getInstanceSize( [NSObject class] ) ;
id * _ffWindow = (void *)self + objSize + sizeof(NSStatusBar*) + sizeof(CGFloat) ;
NSWindow * window = *_ffWindow ;
return [window frame] ;
}
#end
This is useful for status items without a custom view.
Tested on Lion

Resources