Drawing text in a NSButton subclass - positioning - objective-c

I Have a custom button:
My Button http://cld.ly/29ubq
And I need the text to be centered, here's my code:
NSMutableParagraphStyle *style =
[[NSParagraphStyle defaultParagraphStyle] mutableCopy];
[style setLineBreakMode:NSLineBreakByWordWrapping];
[style setAlignment:NSLeftTextAlignment];
att = [[NSDictionary alloc] initWithObjectsAndKeys:
style, NSParagraphStyleAttributeName,
[NSColor blackColor],
NSForegroundColorAttributeName, nil];
[style release];
// other Drawing code here
[[self title] drawInRect:[self bounds] withAttributes:att];
How do I center the text in the center of my button (not center of the bounds)?

You want to get the size of the title first using NSString -sizeWithAttributes:. Then you can use that size to adjust the drawing of title within your bounds.
There is a trick to this, since you will almost always wind up dividing by two at some point in this process. When you do that, you may wind up with a half-pixel result, which Cocoa will happily use. However, it will cause your text to be blurry. You almost always want to call floor() on your coordinates before drawing on them to get yourself back onto integral pixels.
Edit: A basic example of centering (this should compile correctly, I haven't compiled it and my recent work has been over on iPhone which is upside-down):
NSRect rect;
rect.size = [[self title] sizeWithAttributes:att];
rect.origin.x = floor( NSMidX([self bounds]) - rect.size.width / 2 );
rect.origin.y = floor( NSMidY([self bounds]) - rect.size.height / 2 );
[[self title] drawInRect:rect withAttributes:att];
Anyway, something along those lines. You may want to offset a bit because of how your button draws, but this is the basic idea.

You could be interested in Setting a Button’s Image, which also explain how to position the title relative to the button’s image.

Related

NSTextView not drawing text onto NSImage

I'm attempting to draw some text onto an NSImage, but I've run into some issues.
Originally I was just drawing an attributed string onto the NSImage, but if the string was too long it would run off the image and I couldn't find a way to wrap the text to a newline.
To solve this I figured that I could just make an NSTextView, place the text in there, and then draw it onto the NSImage.
Unfortunately, when I attempt to draw the NSTextView to the NSImage, the text does not appear. The NSTextView's background color does show up though.
When I set a breakpoint before I lock focus on the NSImage, and preview the NSTextView, the text view has text. After I draw the text view onto the NSImage, it looks like the NSTextView is drawn, just without the text.
If there is a better way to throw text onto an NSImage that has the ability to have multiple lines, please let me know how.
Here's the code I've written for reference:
NSTextView *textToDraw = [[NSTextView alloc] initWithFrame:NSMakeRect(0, 0, input.size.width - 16, 243)];
textToDraw.backgroundColor = [NSColor blueColor];
[textToDraw setAlignment:NSTextAlignmentCenter];
[textToDraw setEditable:YES];
// textOnImage is a regular NSString
[textToDraw insertText:textOnImage replacementRange:NSMakeRange(0, textOnImage.length)];
[textToDraw setTextColor:[NSColor blackColor]];
[textToDraw setFont:font];
[textToDraw setEditable:NO];
// input is an NSImage
[input lockFocus];
[textToDraw drawRect:NSMakeRect(8, inputImage.size.height - textToDraw.frame.size.height - 8, textToDraw.frame.size.width, textToDraw.frame.size.height)];
[input unlockFocus];
Instead of drawing textview on image you can provide the bounded rect in which text should be drawn. If the string is too long it wouldn't run off the image and will automatically enter in new line.
Here is code where inputImage is the image on which you want to draw text
NSImage *newImage = [[NSImage alloc] initWithSize:inputImage.size];
[newImage lockFocus];
[inputImage drawAtPoint:NSZeroPoint fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1.0];
[textStr drawInRect:boundedRect withAttributes:attrsDictionary];
[newImage unlockFocus];

Drawing text above the divider in an NSSplitView, the top view will occasionally draw over it

Here's a .swf (pardon, the bad website and swf, that was the only way I could capture what was happening)
http://screencast.com/t/rzJ3b5ihSj
What appears to be happening, is that my divider is occasionally drawn first, and then the top NSView in the NSSplitView draws over it. But it seems inconsistent, because sometimes the divider draws on top.
Here is my -drawDividerInRect method, overridden from NSSplitView
-(void) drawDividerInRect:(NSRect)aRect
{
[[NSColor colorWithRed:10.0/255.0 green:10.0/255.0 blue:10.0/255.0 alpha:0.0] set];
NSRectFill(aRect);
id topView = [[self subviews] objectAtIndex:0];
NSRect topViewFrameRect = [topView frame];
NSDictionary *attributes = [NSDictionary dictionaryWithObjectsAndKeys:[NSFont fontWithName:#"Helvetica" size:26], NSFontAttributeName,[NSColor whiteColor], NSForegroundColorAttributeName, nil];
NSAttributedString * currentText=[[NSAttributedString alloc] initWithString:#"Tool Properties" attributes: attributes];
NSSize stringSize = [currentText size];
CGFloat xOffset = ([topView frame].size.width - stringSize.width)/2;
NSRect textRect = NSMakeRect(topViewFrameRect.origin.x+xOffset, topViewFrameRect.size.height+50, stringSize.width, stringSize.height);
[currentText drawInRect:textRect];
}
How can I make it so my divider & text within it draws on top all the time?
So actually the divider, and the text I wanted to draw, were all being done on the NSSplitView itself. The two views on either side of the divider are subviews, and so there's no way to draw in front of them. What I ended up doing was add some padding at the bottom of the top view so that the divider text is always exposed

How to Scroll the text in label automatically

I have an array , data of which 'm displaying on scrollview using UILabel. I can scroll right and left. But I want to add one more feature in that which is automatic scrolling of the label text on scrollview. That means even if I do not scroll I want it to move by itself.
My code for scrolling is as below:
- (void)viewDidLoad
{
[super viewDidLoad];
_scrollView.contentInset=UIEdgeInsetsMake(0, 300, 0, 300);
NSMutableArray *items=[[NSMutableArray alloc] initWithObjects:#"A walk to remember",#"The Last song",#"The Notebook",#"Dear John",#"The Longest Ride",nil];
CGFloat y = 10;
self.label = [[UILabel alloc]initWithFrame:CGRectMake(0,y,800, 25)];
for(int i=0;i<[items count];i++){
y+=20;
NSString* lab=[items objectAtIndex:i];
_label.backgroundColor=[UIColor clearColor];
lab=[items componentsJoinedByString:#" | "];
NSLog(#"label:%#",lab);
[self.label setLineBreakMode:NSLineBreakByWordWrapping];
[self.label setText:lab];
}
NSTimer *timer = [NSTimer timerWithTimeInterval:1
target:self
selector:#selector(timer)
userInfo:nil
repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
//[_scrollView scrollRectToVisible:NSMakeRange(0,0,0.0) animated:YES];
[_scrollView addSubview:_label];
[UIView commitAnimations];
_scrollView.contentSize=CGSizeMake(_scrollView.frame.size.width*[items count], _scrollView.frame.size.height);
}
timer code:
-(void)timer {
[self.label setText:[NSString stringWithFormat:#" %#", self.label.text]];
}
Please help me how could I make the label text move horizontally. Thanks for any help
That's a fairly horrible way to implement scrolling if I may so so! You're scrolling by inserting incremental spaces to the left of the string. There is a much easier way to do this!
Keep your code that adds the UILabels to your scroll view
Get rid of the NSTimer stuff
Add a basic UIView animation to animate the scrollview's content offset.
Something like this:
[UIView animateWithDuration:0.3f delay:0.0f options:UIViewAnimationOptionCurveEaseInOut animations:^{
_scrollview.contentOffset = CGPointMake(-50.0f, 0.0f);
} completion:nil];
This will slide the whole scrollview left by 50 points over 0.3secs. You can play around with the settings.
btw, typically you'd create the UILabels in viewDidLoad (as you have it) but put the animation in viewWillAppear or viewDidAppear
Have had another look at your code. I thought you were adding multiple UILabels to your uiscrollview but actually you're adding just one with a long text string.
You can simplify your code to set up the label quite a bit, then implement the scrolling animation per the below.
NSMutableArray *items=[[NSMutableArray alloc] initWithObjects:#"A walk to remember",#"The Last song",#"The Notebook",#"Dear John",#"The Longest Ride",nil];
CGFloat y = 10;
self.label = [[UILabel alloc]initWithFrame:CGRectMake(0,y,800, 25)];
[self.label setLineBreakMode:NSLineBreakByWordWrapping];
[self.label setBackgroundColor: [UIColor clearColor]];
[self.label setText: [items componentsJoinedByString:#" | "]];
EDIT
To your question about how to show the whole line scrolling and then repeating, there are a couple of steps here.
Make the UILabel wide enough to take all the text on a single line - i.e. increase the width so that it's big enough to fit all the text. You can do this by trial and error or use (CGSize)sizeThatFits:(CGSize)size
to get the size. You call CGSize size = self.label sizeThatFits:CGSizeMake(CGFloatMax, self.label.frame.size.height) to get the size then apply this to the label's frame:
CGRect frame = self.label.frame;
frame.size.width = size.width; //calculated above with sizeThatFits
self.label.frame = frame; //update the frame
Now scroll the scrollview by the full width. So the animateWithDuration code would become:
_scrollview.contentOffset = CGPointMake(-size.width, 0.0f);
The final step is how to make it restart once it's got to the end. Not sure what effect you want here? Should it jump back to the start? (if so set the contentOffset x to zero), or continue scrolling? (if so, use a trick like adding a second UILabel at the end of the first.
BTW, if what you're after is a simple scrolling text like a banner on a webpage, then I suggest you don't bother with the scrollview at all. Instead add the UILabel to self.view and then animate the UILabel's frame origin directly.

Adding an image border

OK, this is what I'm trying to do :
Get an NSImage containing, let's say a photo (1000+ x 1000+ dimensions).
Get another NSImage containing just a tranparent background and a simple black border (500x500).
"Combine" the 2 images, so that the resulting image is the photo with a border.
This is what I've achieved so far :
NSImage* resultImage = [[[self drop] image] copy];
[resultImage lockFocus];
NSRect newRect = NSMakeRect(0, 0, [[[self drop] image] size].width, [[[self drop] image] size].height);
[[[self drop2] image] drawInRect:newRect
fromRect:NSZeroRect
operation:NSCompositeSourceOver
fraction:1.0];
[resultImage unlockFocus];
[[self drop] setImage:resultImage];
Where [self drop] is an ImageWell containing the photo, and [self drop2] an ImageWell containing the border.
The thing is that it IS working. However, the resulting image is - quite obviously - showing a somewhat "stretched" border.
How could I resolve that? Given that the original photo should be of ANY dimensions, how could I make it to use a border (of some fixed dimensions) and still avoid stretching?
How about doing the border directly with CALayer, e.g.:
#import <QuartzCore/QuartzCore.h>
CALayer *layer = imageView.layer;
layer.borderColor = [[NSColor blackColor] CGColor];
layer.borderWidth = 10;
I would do this differently. Just size the image as desired and then add the border. You could do this just by having a simple view with black background, or a suitable image (assuming you want to have customizable image borders, like frames), sized to always keep the resulting border constant. Then you can generate a new image from that view, if you need to.

Changing the fill to a already drawn NSBezierPath

I'd like to change the fill of a button that I've drawn (I subclassed NSButton)
Here's the code I've got already:
- (void)drawRect:(NSRect)dirtyRect {
// Drawing code here.
// Create the Gradient
NSGradient *fillGradient = [[NSGradient alloc] initWithStartingColor:[NSColor lightGrayColor] endingColor:[NSColor darkGrayColor]];
// Create the path
aPath = [NSBezierPath bezierPath];
[aPath moveToPoint:NSMakePoint(10.0, 0.0)];
[aPath lineToPoint:NSMakePoint(85.0, 0.0)];
[aPath lineToPoint:NSMakePoint(85.0, 20.0)];
[aPath lineToPoint:NSMakePoint(10.0, 20.0)];
[aPath lineToPoint:NSMakePoint(0.0, 10.0)];
[aPath lineToPoint:NSMakePoint(10.0, 0.0)];
[fillGradient drawInBezierPath:aPath angle:90.0];
[fillGradient release];
}
- (void)mouseDown:(NSEvent *)theEvent {
NSGradient *fillGradient = [[NSGradient alloc] initWithStartingColor:[NSColor lightGrayColor] endingColor:[NSColor darkGrayColor]];
[fillGradient drawInBezierPath:aPath angle:-90.0];
}
and I get a EXC_BAD_ACCESS signal. How would I do this ?
The reason for the EXC_BAD_ACCESS is that the following line:
aPath = [NSBezierPath bezierPath];
creates an autoreleased bezierPath which will be released at the end of the current iteration of the run loop. To avoid the error, you'd need to change it to:
aPath = [[NSBezierPath bezierPath] retain];
However, you're approaching the problem from the wrong direction. Drawing should only be done in the -drawRect: method (or methods which are only called from -drawRect:). Instead of trying to draw in your mouseDown: method, you should create a new BOOL instance variable for your class (called, for instance, mouseIsDown) and set that in mouseDown:. Then use that boolean to determine how to fill the button:
- (void)drawRect:(NSRect)aRect {
NSGradient *fillGradient = nil;
if (mouseIsDown)
fillGradient = [[NSGradient alloc] initWithStartingColor:[NSColor lightGrayColor] endingColor:[NSColor darkGrayColor]];
else
fillGradient = [[NSGradient alloc] initWithStartingColor:[NSColor lightGrayColor] endingColor:[NSColor darkGrayColor]];
// Do the rest of your drawRect method
}
- (void)mouseDown:(NSEvent *)theEvent {
mouseIsDown = YES;
[self setNeedsDisplay:YES];
}
- (void)mouseUp:(NSEvent *)theEvent {
mouseIsDown = NO;
[self setNeedsDisplay:YES];
}
This isn't directly related to your question, but your question's title belies a false assumption that I want to address.
Changing the fill to a already drawn NSBezierPath
That's not possible.
In Cocoa, “fill” is a verb, not a property of an object like it is in Illustrator or Lineform. You do not set the fill of a path, nor change it later; you fill the path, and what you change by this is pixels in some unseen backing store. The real-world analogy would be setting up splines on a flat pane of glass, then filling the area bounded by the splines with paint and letting it dry. The “fill” is not a property of the splines; it's the act of pouring paint into the shape they define.
As with the analogy, you cannot remove or alter a fill you have previously done—the paint is stuck to the glass; there's no getting it off*. The only way to accomplish that effect is to re-do the fill with some other color. You can move the splines (create a new path) or add splines (add an intersecting subpath to the existing path) before filling, if you want to only change part of the existing drawing.
All of that applies to all drawing operations, including stroking paths and drawing raster images. Text drawing, too, as that's just another case of filling and/or stroking a path.
*OK, you probably could remove real paint from real glass, either chemically or by scraping. No analogy is perfect. ☺
Retain aPath after you create it, since it will have been autoreleased by the time the mouseDown: message is sent. Just make sure to release it in your class's dealloc or wherever else that it would be appropriate.
Edit:
Refer to Matt Ball's answer instead.