I have a scroll view that shows a number of custom views.
In that custom view I use my own class so I can use the drawRect method to draw a line at the bottom of the view.
#interface MyListItemView : NSView
Here is my implementation of drawRect
#implementation MyListItemView
-(void)drawRect:(NSRect)rect
{
[NSGraphicsContext saveGraphicsState];
NSBezierPath * path;
path = [NSBezierPath bezierPath];
[path moveToPoint: rect.origin];
[path lineToPoint: NSMakePoint(rect.size.width, 0)];
[path setLineWidth:1];
[[NSColor grayColor] set];
[path stroke];
[NSGraphicsContext restoreGraphicsState];
}
#end
This draws the line as expected, but when I scroll the container I get artifacts appearing, like shown on the right hand side of the image below:
I tried saving and restoring the graphics context, but this made no difference and it doesn't look like that code is required. What am I missing?
Inspiration hit me while on a walk.
I'm drawing a line on the very edge of my view. It looks like some of that is getting drawn just outside the view (possibly due to anti aliasing) and isn't getting cleaned up properly.
All I needed to do was move my line up by half a pixel, changing my line code to this:
[path moveToPoint: NSMakePoint(0, 0.5)];
[path lineToPoint: NSMakePoint(rect.size.width, 0.5)];
Related
This is causing me some pain...
I wish to use layer-hosting views in my app and I'm having this weird problem.
Here is a simple example. Simply implemented by creating a new project in Xcode and entering the following in the AddDelegate: (after adding QuartzCore to the project):
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
NSView *thisView = [[NSView alloc] initWithFrame:CGRectInset([self.window.contentView bounds], 50, 50)];
[thisView setLayer:[CALayer layer]];
[thisView setWantsLayer:YES];
thisView.layer.delegate = self;
thisView.layer.backgroundColor = CGColorCreateGenericRGB(1,1,0,1);
thisView.layer.anchorPoint = NSMakePoint(0.5, 0.5);
[self.window.contentView addSubview:thisView];
//Create custom content
[thisView.layer display];
}
I also implement the following CALayer Delegate method:
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx {
[[NSColor blueColor] setFill];
NSBezierPath *theBez = [NSBezierPath bezierPathWithOvalInRect:layer.bounds];
[theBez fill];
}
If I run this code, I can see the subview being added to the windows contentView (big yellow rectangle), and I'm supposing it is a layer-hosting view... and I can see the oval being drawn in blue, but it is underneath the yellow rectangle, and it's origin is at (0,0) in the main Window... it is like it is not actually being drawn inside the yellow layer.
I'm guessing either my view is not really layer-hosting, or that the context being passed to the layer is wrong... but why would it be underneath?
I must be doing something wrong...
To continue with the weirdness, if I add a CABasicAnimation to the layer, like so:
CABasicAnimation *myAnimation = [CABasicAnimation animation];
myAnimation = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
myAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
myAnimation.fromValue = [NSNumber numberWithFloat:0.0];
myAnimation.toValue = [NSNumber numberWithFloat:((360*M_PI)/180)];
myAnimation.duration = 1.0;
myAnimation.repeatCount = HUGE_VALF;
[thisView.layer addAnimation:myAnimation forKey:#"testAnimation"];
thisView.layer.anchorPoint = NSMakePoint(0.5, 0.5);
The yellow background gets animated, rotating about its center, but the blue ellipse gets drawn correctly inside the layer's frame (but also outside, at the origin of the Window, so it is there twice) but does not animate. I would expect the ellipse to rotate with the rest of the layer of course.
I have made this project available here for those willing to give a hand.
Renaud
Got it. I was confused by the fact that the context being called in this situation is a CGContextRef, not an NSGraphicsContext!
I seem to be able to get the result I need by setting the NSGraphicsContext from the CGContextRef:
NSGraphicsContext *gc = [NSGraphicsContext graphicsContextWithGraphicsPort:ctx flipped:NO];
[NSGraphicsContext saveGraphicsState];
[NSGraphicsContext setCurrentContext:gc];
//Insert drawing code here
[NSGraphicsContext restoreGraphicsState];
I have a NSMatrix with a couple of NSButtons in it which have no Text but are only images. One of the images is downloaded from the internet and I would like to have it rounded corners in my OS X application.
I found one answer which nearly is what I was looking for: How to draw a rounded NSImage but sadly it acts crazy when I use it:
// In my NSButtonCell subclass
- (void)drawImage:(NSImage*)image withFrame:(NSRect)imageFrame inView:(NSView*)controlView
{
// [super drawImage:image withFrame:imageFrame inView:controlView];
[NSGraphicsContext saveGraphicsState];
NSBezierPath *path = [NSBezierPath bezierPathWithRoundedRect:imageFrame xRadius:5 yRadius:5];
[path addClip];
[image drawInRect:imageFrame fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1.0];
[NSGraphicsContext restoreGraphicsState];
}
The problem is that if a image is partly transparent (PNG) then it is completly destroyed and I only see a couple of white pixels on a black background.
And if the image is not transparent then it gets the rounded corners but is rotated 180° and I don't know why.
Any suggestions?
You need to make sure you set the image's size correctly before drawing it, and you should use the NSImage method drawInRect:fromRect:operation:fraction:respectFlipped:hints: to ensure the image is drawn the correct way up:
- (void)drawImage:(NSImage*)image withFrame:(NSRect)imageFrame inView:(NSView*)controlView
{
// [super drawImage:image withFrame:imageFrame inView:controlView];
[NSGraphicsContext saveGraphicsState];
NSBezierPath *path = [NSBezierPath bezierPathWithRoundedRect:imageFrame xRadius:5 yRadius:5];
[path addClip];
//set the size
[image setSize:imageFrame.size];
//draw the image
[image drawInRect:imageFrame fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1.0 respectFlipped:YES hints:nil];
[NSGraphicsContext restoreGraphicsState];
}
The image should draw correctly if you do this, even if it's a translucent PNG image.
I have asked (and answered) a very similar question before. That question was able to be solved because I knew the dirtyRect, and thus, knew where I should draw the image. Now, I am seeing the same behavior with an subclassed NSButtonCell:
In my subclass, I am simply overriding the drawImage:withFrame:inView method to add a shadow.
- (void)drawImage:(NSImage *)image withFrame:(NSRect)frame inView:(NSView *)controlView {
NSGraphicsContext *ctx = [NSGraphicsContext currentContext];
[ctx saveGraphicsState];
// Rounded Rect
NSBezierPath *path = [NSBezierPath bezierPathWithRoundedRect:frame cornerRadius:kDefaultCornerRadius];
NSBezierPath *shadowPath = [NSBezierPath bezierPathWithRoundedRect:NSInsetRect(frame, 0.5, 0.5) cornerRadius:kDefaultCornerRadius]; // prevents the baground showing through the image corner
// Shadow
NSColor *shadowColor = [UAColor colorWithCalibratedWhite:0 alpha:0.8];
[NSShadow setShadowWithColor:shadowColor
blurRadius:3.0
offset:NSMakeSize(0, -1)];
[[NSColor colorWithCalibratedWhite:0.635 alpha:1.0] set];
[shadowPath fill];
[NSShadow clearShadow];
// Background Gradient in case image is nil
NSGradient *gradient = [[NSGradient alloc] initWithStartingColor:[UAColor grayColor] endingColor:[UAColor lightGrayColor]];
[gradient drawInBezierPath:shadowPath angle:90.0];
[gradient release];
// Image
if (image) {
[path setClip];
[image drawInRect:frame fromRect:CGRectZero operation:NSCompositeSourceAtop fraction:1.0];
}
[ctx restoreGraphicsState];
}
It seems all pretty standard, but the image is still drawing outside of the containing scrollview bounds. How can I avoid this?
You shouldn't call -setClip on an NSBezierPath unless you really mean it. Generally you want ‑addClip, which adds your clipping path to the set of current clipping regions.
NSScrollView works by using its own clipping path and you're blowing that path away before drawing your image.
This tripped me up for a long time too.
Im working on a drag n' drop view and found some handlers for drag and drop actions on the web. I want to make it so it turns blue when the user drags a file over the drag and drop area and gray again when they exit the drag and drop area. The issues is its not updating when you drag your mouse over it or exit it. Heres some of the code:
- (void)drawRect:(NSRect)rect
{
NSRect bounds = [self bounds];
[[NSColor grayColor] set];
[NSBezierPath fillRect:bounds];
}
- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender {
NSRect bounds = [self bounds];
[[NSColor blueColor] set];
[NSBezierPath fillRect:bounds];
return NSDragOperationCopy;
}
- (void)draggingExited:(id <NSDraggingInfo>)sender {
NSRect bounds = [self bounds];
[[NSColor grayColor] set];
[NSBezierPath fillRect:bounds];
}
Thanks for any help.
Are you calling [yourView: setNeedsDisplay] anywhere?
This is how you let the drawing framework know it needs to message your UIView subclass with drawRect:, so you should do it whenever things have changed. In your case, this probably means when the mouse enters or exits the drop area.
Drawing only works when a context (like a canvas for painting) is set up for you to draw into. When the framework calls -drawRect: it has set up a drawing context for you, so drawing commands like -[NSColor set] and -[NSBezierPath fillRect:] work as you expect.
Outside of -drawRect: there is usually no drawing context set up. Using drawing commands outside of -drawRect: is like waving a paintbrush in the air; there's no canvas, so no painting happens.
In 99.99% of cases, all view drawing should be kept within -drawRect: because NSView does a lot of work that you don't want to do to get the drawing context set up correctly and efficiently.
So, how do you change your view's drawing within your -draggingEntered: and -draggingExited: methods? By side effects.
You're doing the same thing in all three cases: 1) Setting a color and 2) Drawing a rectangle. The only difference is the color changes in each method. So, why not control which color you use in -drawRect: with an ivar, like so:
- (void)draggingEntered:(id <NSDraggingInfo>)sender {
drawBlueColorIvar = YES;
// ...
}
Then in -drawRect: you do this:
- (void)drawRect:(NSRect)rect {
NSColor *color = drawBlueColorIvar ? [NSColor blueColor] : [NSColor grayColor];
[color set];
[NSBezierPath fillRect:rect];
}
(Notice I didn't use [self bounds]. It is more efficient to just draw into the "dirty" rect, when possible.)
Finally, you need some way to tell the framework that your view needs to redraw when drawBlueColorIvar changes. The framework won't draw anything unless it's told it needs to. As Chris Cooper said, you do this with [self setNeedsDisplay:YES]. This should go after any place you change drawBlueColorIvar.
I'm having some very basic problems changing line color/width when drawing different lines (intended for a chart) with NSBezierPath. The following code should make it clear what I'm trying to do:
#import "DrawTest.h"
#implementation DrawTest
- (id)initWithFrame:(NSRect)frameRect
{
NSLog(#"in 'initWithFrame'...");
if ((self = [super initWithFrame:frameRect]) != nil)
{
[self drawBaseline];
[self display]; // ...NO!
[self drawPlotline];
}
return self;
}
- (void)drawBaseline
{
NSRect windowRect;
int width, height;
windowRect = [self bounds];
width = round(windowRect.size.width);
height = round(windowRect.size.height);
theLineWidth=1.0;
[[NSColor grayColor] set];
// allocate an instance of 'NSBezierPath'...
path = [[NSBezierPath alloc]init];
// draw a HORIZONTAL line...
[path moveToPoint:NSMakePoint(0,height/2)];
[path lineToPoint:NSMakePoint(width,height/2)];
}
- (void)drawPlotline
{
theLineWidth=10.0;
[[NSColor redColor] set];
// draw a VERTICAL line...
[path moveToPoint:NSMakePoint(100,125)]; // x,y
[path lineToPoint:NSMakePoint(100,500)];
}
- (void)drawRect:(NSRect)rect
{
NSLog(#"in 'drawRect'...");
// draw the path line(s)
// [[NSColor redColor] set];
[path setLineWidth:theLineWidth];
[path stroke];
}
- (void)dealloc
{
[path release];
[super dealloc];
}
#end
The problem obviously lies in the fact that 'drawRect' only gets called ONCE (after both methods have run), with the result that all lines appear in the last color and line width set. I've tried calling [self display] etc. in the hope of forcing 'drawRect' to redraw the NSView contents between the two method calls, but to no avail.
Can anyone suggest a basic strategy to achieve what I'm trying to do here, please? Any help would be much appreciated.
Quick answer is: move [self drawBaseline] and [self drawPlotline] inside drawRect.
Also, you need to call [path stroke] once per color, before changing the color. So, the pseudo-code is something like
-(void)drawRect:(NSRect)rect
{
NSBezierPath*path1=...
construct the path for color red...
[[NSColor redColor] set];
[path1 stroke];
NSBezierPath*path2=...
construct the path for color blue...
[[NSColor blueColor] set];
[path2 stroke];
}
Remember:
[path moveToPoint:] etc. does not draw the path. It just constructs the path inside the object. Once it's constructed, you stroke the path using [path stroke].
Also, the color is not the property of the path constructed inside the NSBezierPath instance. So, [color set] is not recorded inside your NSBezierPath instance! It's the property of the graphic context. Conceptually, 1. you construct the path. 2. you set the color to the context. 3. you stroke the path.
In Cocoa, it's not that you tell the system when to draw, when to refresh, etc. Cocoa tells you when to draw, by calling your drawRect:. The graphic context on which you draw is not available outside of drawRect:, unless you create off-screen context yourself. So, don't bother drawing beforehand. Just draw everything inside drawRect .