custom ratiing help

So, I figured that the best way to make a good looking star rating thing was to just make my own. So I made 5 borderless buttons, made there icon the unclicked star and the alt. icon the clicked star. It looked really nice but the problem is it still doesnt act like a rating thing because right now, if you click the middle star only that one will turn on, and not the other 2 before it. What would be the best solution to having it work like that?

Subclass NSLevelIndicator and NSLevelIndicatorCell and perform custom drawing at certain intervals.

Edit: Not to be so blunt, but that’s pretty much how it’s done. It’s a custom control.

lol, i don’t know enough Obj-C to subclass things like that yet…

I presume you would have to script each button the highlight the others.

click on button three sets the state of the buttons 1,2 & 3 to on
and the state for buttons 4 & 5 to off

click on button four sets the state of buttons 1,2,3 & 4 to on
and the state for button 5 to off

(I think state but at work on a PC).

Regards

Terry

Hendo,

here a simple example for a rating LevelIndicator with some custom drawing (draws red dots, filled/stroked) … hope that helps:

[code]/* CustomLevelIndicator.h */

#import <Cocoa/Cocoa.h>

@interface CustomLevelIndicatorCell : NSLevelIndicatorCell{
}
@end

@interface CustomLevelIndicator : NSControl{
}
@end[/code]

[code]/* CustomLevelIndicator.m */

#import “CustomLevelIndicator.h”

@implementation CustomLevelIndicatorCell

  • (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView{
    unsigned starCount = (int)roundf([self floatValue]);
    NSBezierPath *starPath;
    NSRect starRect = NSMakeRect(0.0,0.0,13.0,13.0);
    if (starCount) { // draw the ‘stars’
    unsigned starIndex;
    [[NSColor redColor] set];
    for (starIndex = 0; starIndex < starCount; starIndex++) {
    starRect.origin.x = 1+starIndex * 13;
    starPath = [NSBezierPath bezierPathWithOvalInRect:NSInsetRect(starRect,1.0,1.0)];
    [starPath fill];
    }
    }
    if ( (starCount < 5) ) { // draw the ‘dots’
    [[NSColor redColor] set];

      unsigned starIndex;
      for (starIndex = starCount; starIndex < 5; starIndex++) {
      	starRect.origin.x = 1+starIndex * 13;
      	starPath = [NSBezierPath bezierPathWithOvalInRect:NSInsetRect(starRect,1.0,1.0)];
      	[starPath stroke];
      }
    

    }
    }
    @end

@implementation CustomLevelIndicator

  • (Class)cellClass{
    return [CustomLevelIndicatorCell class];
    }
  • (id) initWithFrame:(NSRect)frameRect {
    self = [super initWithFrame:frameRect];
    if (self != nil) {
    [self setCell:[[[CustomLevelIndicatorCell alloc] initWithLevelIndicatorStyle:NSRatingLevelIndicatorStyle] autorelease]];
    }
    return self;
    }

@end[/code]
note - please make sure that you set it’s frame to the ‘normal’ NSLevelIndicator size (65 x 13) otherwise this simple example might not work properly …

D.

thats great Dom! How would I modify it to use images as the fill and placeholders instead? Thanks so much :smiley:

assuming your cell class has two images: starImg and dotImg, both sized to (13.0, 13.0), you might draw them like this (two different methods shown in the example …):

[code]- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView{
unsigned starCount = (int)roundf([self floatValue]);
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];
	}
}

}[/code]
perhaps you need an other NSCopositingOperation or different rect sizes, depending on your images … for a description of NSImage’s drawing methods see here:
http://developer.apple.com/documentation/Cocoa/Reference/ApplicationKit/Classes/NSImage_Class/Reference/Reference.html#//apple_ref/occ/instm/NSImage/drawAtPoint:fromRect:operation:fraction:

and here:

http://developer.apple.com/documentation/Cocoa/Conceptual/CocoaDrawingGuide/Images/chapter_7_section_6.html#//apple_ref/doc/uid/TP40003290-CH208-BCIIBDAD

There are some problems with these custom classes you may want to address:

  1. You probably don’t want to call -restoreGraphicsState after performing your drawing, because you never invoke -saveGraphicsState. But from what I can tell, there’s no reason to do anything with NSGraphicsContext anyway, because you aren’t doing anything that requires state reversions in your draw method.

  2. This is an NSControl, so you should either override the designated initializer (-initWithFrame: in this case) or define your own custom designated initializer. (Honestly, I’d go with the former.) In either case, you should also implement -initWithCoder:, because -init and -initWithFrame: are never called when unarchiving an NSControl from a nib.

  3. You should implement +cellClass and return the class of your custom cell:

+ (Class)cellClass { return [CustomLevelIndicatorCell class]; }
4. Remove the initialization in -viewDidMoveToSuperview after you correct the problem in #2. This looks like a workaround for that issue.

this worked fine, except that it has a black background for some reason :confused:

and for some reason it only works if i put it in my windows drawer…if i put it on my actual main window it just reverts back to a the regular crappy star thing. Would this be because it’s a custom window?

Hendo,

I guess it’s the transparent parts showing black? That’s what I mentioned above - you will probably need an other NSCopositingOperation - try NSCompositeSourceOver or NSCompositeSourceATop instead … in case you want to know more - read the docs - see links above …

and Mickey complained about some ugly parts in my example - he is right. You really may remove the unnecessary NSGraphiContext lines I had overseen and use the correct designated initializer (I have corrected the code above …). I don’t see the need to implement/overwrite initWithCoder: …

I have tried the subclass with standard and custom windows and could not see a problem/difference - if you still have problems, feel free to pm me …

D.

-initWithFrame: is not called when unarchiving an NSControl from a nib, like I said. They are serialized to disk when you save the nib file and are read back in and deserialized inside -initWithCoder: when the application loads the nib.

Instantiate an NSLevelIndicator in Interface Builder, set its custom class to a custom NSLevelIndicator subcass, and then place a breakpoint in your subclass’s -initWithFrame: method. The breakpoint will not trigger, but one placed inside of -initWithCoder: will.

Here is an example project that demonstrates this:

http://www.mikey-san.net/CustomLevelIndicator.zip

Thanks for all your help guys, Mikey-San I’ll check that out when I get home, gotta go to a dance and then part;)

:smiley:

it does - instead of setting a custom class for an NSLevelIndicator use a ‘CustomView’ in Interface Builder and your code will report ‘-initWithFrame: was here’ …

But if you use a custom view instead of the superclass (NSLevelIndicator, in this case), you lose all of the functionality IB gives you for manipulating the control. Coder support is REALLY REALLY easy and gives you more flexibility with IB. It’s win/win.

Note: I’m not saying replace -initWithFrame:, but add coder support. The power of Cocoa lies in its flexibility; proper implementation of controls leverages the frameworks successfully.

edited to remove some stupid crap i said

Wise man ;-),

please show me how not to use a CustomView with my subclass example above (ok, we might discuss why I subclassed NSControl instead of choosing NSLevelindicator - I had some reason to do so for my own custom LevelIndicator and this may not meet hendo’s needs, agreed - but when having an NSControl subclass it is impossible to set it as a custom class for an NSLevelIndicator in IB afaik).

I am aware that I am a far from being perfect in writing cocoa code. I just wanted to help … So why don’t you simply post a better example? I am always glad to learn something and everyone reading this thread would take benefit, if my crappy code was replaced by a better one … :wink:
Many thanks for all your corrections and improvements …

D.

Setting the custom class for an NSLevelIndicator is the same as a “custom view”. Add your NSLevelIndicator subclass to the nib, instantiate an NSLevelIndicator, hit command-5 (Custom Class shortcut), then set the class to your custom subclass. That’s it.

No one said to subclass NSControl and use it as an NSLevelIndicator subclass. What was stated was that it’s a control, because NSLevelIndicator is a subclass of NSControl. NSControls don’t get -initWithFrame: called when being unarchived from a nib.

The exampled I posted demonstrates that using “custom class” in IB with an NSLevelIndicator is possible. You simply need -initWithCoder: support, which is trivial to implement.

Because I don’t have one lying around, and I’m under deadline for a piece of software that I must dedicate coding time to. Perhaps when that’s finished (soon), I’ll write a complete rating indicator and post it somewhere. Wouldn’t be a bad idea.

‘No one said’ but I did - if you had read my example thorroughly you had noticed ;-).

not true - when using a NSView subclass with Interface Builder’s ‘CustomView’ -initWithFrame: is called, when using it with it’s super class item ‘-initWithCoder:’.
You may read for example here: http://developer.apple.com/documentation/developertools/conceptual/IBTips/Articles/CustomViews.html
So if you have a direct NSControl subclass and want it to call -initWithCoder:, I guess you would have to palettize it …

Okay, not to sound mean–honestly, I really don’t intend to sound snotty, just inquisitive–but I don’t think you’re following along with the things I’m saying. That, or you’re confused about terminology somehow. You keep introducing some stuff about subclassing NSControl directly and using “custom view”, and I don’t know why it keeps coming back to that, despite everything I’ve said. It’s just muddying the waters. This is really simple:

  1. The star ratings indicator is typically done with NSLevelIndicator.

  2. Subclass NSLevelIndicator and customize it to act like the iTunes ratings indicator.

  3. Implement coder support.

  4. Instantiate an NSLevelIndicator in IB so you get the benefits of IB already having a palette that handles the basics of NSLevelIndicator.

  5. Switch the NSLevelIndicator to your custom subclass.

  6. Done.

Coder support is part of a good control subclass anyway, and it’s trivial to implement. So why are you so against it here? Why give the impression to people that there’s no reason to implement it?

Edit:

“Custom View” isn’t useless, don’t get me wrong. It’s handy to have when you have a subclass of something that doesn’t already exist in your IB palette arsenal, because writing an IB palette is more junk to manage. But as a result of not having a palette, it comes with limitations. When your superclass already exists in IB, it’s generally useful to use it, if possible.

I agree with Mikey-San here I’d rather just directly sublass NSLevelIndicator here. The problem is I have no clue what you are talking about when you say add coder support.