custom ratiing help

All view objects, when instantiated in Interface Builder, are “freeze-dried” when saving the nib. That is, they’re written to the nib file on disk in a special way we call “serializing”. All this means is that the complex object graph that constitutes the object is converted to a stream of bytes that can be written to disk and then read back in later by a mechanism that understands the stream.

Since this is a pain in the ass, Apple gave us the NSCoding protocol and the NSCoder class. Since our views have been archived previously during the serialization process, they must be unarchived during nib loading. To do this, we use something called a coder which is capable of decoding the object from its freeze-dried state.

NSCoding and NSCoder are documented here:

http://developer.apple.com/documentation/Cocoa/Reference/Foundation/Protocols/NSCoding_Protocol/Reference/Reference.html
http://developer.apple.com/documentation/Cocoa/Reference/Foundation/Classes/NSCoder_Class/Reference/Reference.html

Look at it this way: you have a button and a bunch of stuff the button knows how to do. You’ve also got all of the state knowledge of the button (frame, bounds, control size, its class, and the other things it knows about itself), and you have connections to and from other objects in the nib. How do you save this data to disk? It’s not magic, it’s just a process of translating it all to a stream of bytes. When you’ve done that, you just need a way to reverse the process. That’s what coders do. Some archive, some unarchive.

When an archived (serialized) view object is read in from a nib, it is given an -initWithCoder: message. Usually what you’ll do is invoke the superclass’s implementation, check it for a valid object on return, and then perform your custom subclass’s custom setup. Then when you’re done, return the object. It’s just like -initWithFrame:, except we’re unarchiving.

Even “Custom View” is sent -initWithCoder:, but your custom class is not sent that message. Why? Well, “Custom View” actually encodes an NSCustomView object into the nib. When the nib is unarchived, the NSCustomView object is initialized with -initWithCoder:, swaps in your custom subclass, and then sends -initWithFrame: to it. You can see this in the call stack of a custom view during -initWithFrame:.

Here is a document that describes the so-called “life cycle” of a nib:

http://developer.apple.com/documentation/Cocoa/Conceptual/LoadingResources/CocoaNibs/chapter_3_section_3.html

Extremely important information for anyone doing custom control work.

ok well this is what i have:

#import "CustomLevelIndicator.h"

@implementation CustomLevelIndicatorCell

- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView{

NSImage *dotImg = [NSImage imageNamed:@"star_off.png"];
NSImage *starImg = [NSImage imageNamed:@"star_on.png"];
	
unsigned starCount = (int)roundf([self floatValue]);
NSGraphicsContext *currentContext = [NSGraphicsContext currentContext];
NSRect starRect = NSMakeRect(0.0,0.0,13.0,13.0);
NSRect imgRect = starRect;
if (starCount) {
unsigned starIndex;
for (starIndex = 0; starIndex < starCount; starIndex++) {
starRect.origin.x = 1+starIndex * 13;
[starImg drawInRect:starRect fromRect:imgRect operation:NSCompositeCopy fraction:1.0];
}
}
if ( (starCount < 5) ) {

unsigned starIndex;
for (starIndex = starCount; starIndex < 5; starIndex++) {
starRect.origin.x = 1+starIndex * 13;
[dotImg drawAtPoint:starRect.origin fromRect:imgRect operation:NSCompositeCopy fraction:1.0];
}
}
[currentContext restoreGraphicsState];
}
@end

@implementation CustomLevelIndicator
- (id) init {
self = [super init];
if (self != nil) {
liCell = [[CustomLevelIndicatorCell alloc] initWithLevelIndicatorStyle:NSRatingLevelIndicatorStyle];
}
[self setCell:liCell];
return self;
}

-(void)viewDidMoveToSuperview{
if (liCell == nil) {
liCell = [[CustomLevelIndicatorCell alloc] initWithLevelIndicatorStyle:NSRatingLevelIndicatorStyle];
}
[self setCell:liCell];

}

- (void) dealloc {
[liCell release];
[super dealloc];
}

@end

I took a look at your file and i just didnt know what to put in and what needed removing from my current code. Im sure it could be tidied up a lot:P

Do I need a mask for the image? Is that why the transparent parts are black? If so how do I implement image masks?

Hendo,
The composite operation NSCompositeCopy simply copies the image when drawing, ignoring transparency. Try using NSCompositeSourceOver instead. (BTW, Dominik said this earlier in this thread).

ohhh…i thought he mean use either or, sorry about that, i should have read it more carefully!

EDIT: So is there anyway I can make this bigger? The standard size is just too small. The stars are so small now that you can barely tell between the clicked and unclicked. :frowning:

well … maybe you didn’t indend to ‘sound snotty’ - but in fact you’re really brilliant in your attempt to let me look like a complete idiot by simply ignoring what I write. thanks… I give up - you have won - at least my time is too precious for me to keep on discussing this here. :wink:

Hendo,

  • I suggest instead of hardwiring the two images in your cell subclass, to overwrite NSLevelIndicatorCells built-in method ‘setImage:’ (which is intended for exactly this task) and add a second method to set the custom dot image.

  • and of course you should follow Mickey’s advise from above …

  • and altering the drawing code so that the stars automatically scale to fit 5 times in the controlView’s frame just needs some simple math …

the subclass might then look like this (please correct me if I am wrong, Mickey):

[code]/* CustomImageLevelIndicator.h */

#import <Cocoa/Cocoa.h>

@interface CustomImageLevelIndicatorCell : NSLevelIndicatorCell{
NSImage *starImg;
NSImage *dotImg;
}

-(void)setDotImage:(NSImage *)dotImage;

@end

@interface CustomImageLevelIndicator : NSLevelIndicator{
}
@end[/code]

[code]#import “CustomImageLevelIndicator.h”

@implementation CustomImageLevelIndicatorCell

  • (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView{

    unsigned starCount = (unsigned)roundf([self floatValue]);

    NSSize starSize = ([[self controlView] frame]).size;
    starSize.width /= 5.0;

    [starImg setSize];
    [dotImg setSize];

    NSRect starRect = NSMakeRect(0.0, 0.0, starSize.width, starSize.height);
    NSRect imgRect = starRect;

    if (starCount) {
    unsigned starIndex;
    for (starIndex = 0; starIndex < starCount; starIndex++) {
    starRect.origin.x = 1.0 + starIndex * starSize.width;
    [starImg drawInRect:starRect fromRect:imgRect operation:NSCompositeSourceOver fraction:1.0];
    }
    }
    if ( (starCount < 5) ) {

      unsigned starIndex;
      for (starIndex = starCount; starIndex < 5; starIndex++) {
      	starRect.origin.x = 1.0 + starIndex * starSize.width;
      	[dotImg drawAtPoint:starRect.origin fromRect:imgRect operation:NSCompositeSourceOver fraction:1.0];
      }
    

    }
    }

-(void)setImage:(NSImage *)image{
if (starImg != image) {
if (starImg != nil) [starImg release];
starImg = [image copy];
[starImg setScalesWhenResized:YES];
}
}

-(void)setDotImage:(NSImage *)dotImage{
if (dotImg != dotImage) {
if (dotImg != nil) [dotImg release];
dotImg = [dotImage copy];
[dotImg setScalesWhenResized:YES];
}
}

  • (void) dealloc {
    if (starImg != nil) [starImg release];
    if (dotImg != nil) [dotImg release];
    [super dealloc];
    }

@end

@implementation CustomImageLevelIndicator

  • (Class)cellClass{
    return [CustomImageLevelIndicatorCell class];
    }
  • (id) initWithCoder:(NSCoder *)aDecoder{
    self = [super initWithCoder];
    if (self != nil) {
    [self setCell:[[[CustomImageLevelIndicatorCell alloc] initWithLevelIndicatorStyle:NSRatingLevelIndicatorStyle] autorelease]];
    [[self cell] setImage:[NSImage imageNamed:@“star_on.png”]];
    [[self cell] setDotImage:[NSImage imageNamed:@“star_off.png”]];
    }
    return self;

}

  • (id) initWithFrame:(NSRect)frameRect {
    self = [super initWithFrame:frameRect];
    if (self != nil) {
    [self setCell:[[[CustomImageLevelIndicatorCell alloc] initWithLevelIndicatorStyle:NSRatingLevelIndicatorStyle] autorelease]];
    [[self cell] setImage:[NSImage imageNamed:@“star_on.png”]];
    [[self cell] setDotImage:[NSImage imageNamed:@“star_off.png”]];
    }
    return self;
    }

@end[/code]

Wow! That looks great Dom! Thanks so much, thats definately cleaner than mine :smiley:

just noticed, that this line:

should be simplified to:

NSSize starSize  = cellFrame.size;

stupid me :wink: … sorry

thanks for the fix :wink:

Just a few quick questions.

  1. I noticed about a pixel or two of the 5th star is not showing up…why?
  2. So no matter how big my star image is it will be resized? Because I want a bigger LevelIndicator…its just too small:P

ok i fixed the part where about a pixel was shaved off. It was simple enough…but I still don’t know how to make this a custom size…its too damn small…

oops - sorry - the one pixel shift was a relict from my first BezierPath drawing example, sorry.

To use a custom size … there are 2 ways:

  • you set the custom size in the initWithCoder: method like this:

... if (self != nil) { NSRect liFrame = [self frame]; liFrame.size.width = 120.0; // new width liFrame.size.height = 34.0; // new height [self setFrame:liFrame]; [self setCell: ...

  • or you just use Interface Builders ‘CustomView’ element and set it’s custom class to our subclass - then you can resize the custom LevelIndicator without limitations …

I’d prefer the second way since then I see the correct dimensions in Interface Builder … :wink:

once again, worked like a charm, It’s now everything I wanted it to be! Thank you so much. I think I learned alot about Obj-C by looking carefully through this and reading your posts :smiley: