Draw NSImage for both retina and non-retina?

Discussion in 'Mac Programming' started by ArtOfWarfare, Aug 18, 2012.

  1. ArtOfWarfare macrumors 604

    ArtOfWarfare

    Joined:
    Nov 26, 2007
    #1
    Hi, I'm updating my menubar app Battery Status to include retina graphics. Only two things can be drawn in the menubar (from an App Store acceptable app,): NSStrings for text and NSImages for images.

    As I see it, I have two options:
    1 - Draw doubled versions of all 49 of my icons and include them as resources.
    Pros:
    - Technically Easy
    Cons:
    - Boring
    - Increased bundle size (from 300 kB of resources to 1.5 MB worth of resources... my entire app will go from 900 kB to 2.6 MB, between the new app icon resolutions and the new resources.)
    - Increased amount of resources I have to manage
    - The old icon was 18 pixels wide, so I made 11 different icons for battery levels... the higher resolution icon will be 36 pixels wide, but still only have 11 different icons for battery levels... if I really want to utilize the extra pixels, I aught to actually have more battery levels.

    2 - Have the app draw the icons on the fly.
    Pros:
    - Reduces the bundle size (from 300 kB to a few lines of code... I suspect that code will take up a trivial amount of space.)
    - More flexible with new resolutions.
    - Capable of drawing any battery level... so it can draw a battery level of 57% rather than have to round it to 60%... there was no difference with old screens, but with Retina there is... I don't care how trivial it is.)
    - Reduced resources to manage. When I decide I want to recolor something, I change a number in code, I don't have to change every resource using that color.
    Cons:
    - Technically... beyond what I can do right now?

    So, could I have a little help? Ideally, I'd like some bare bones sample code that shows how to make an NSImage from some drawing commands that looks as good as possible on any (retina or non-retina) screen.

    The points I'm confused at are... where should the context come from that's being drawn into? It seems like making a new context requires dimensions to be passed in... but that makes it so it can't be resolution independent? And then how do I get that drawing to be an NSImage?
     
  2. gnasher729 macrumors P6

    gnasher729

    Joined:
    Nov 25, 2005
    #2
    There's a new method that creates an NSImage by passing in a block of code that is executed to do the drawing. That block will be executed every time the image needs a different resolution (for example, if you have a Retina MBP with an external monitor, and move a window between monitors).
     
  3. xStep macrumors 68000

    Joined:
    Jan 28, 2003
    Location:
    Less lost in L.A.
    #3
    Your battery indicator isn't that complicated. All of it can be drawn in code with some little known tricks. Of course, the issue is learning how to.

    I'm no graphics wizard, but all of the added runtime graphics in my iOS app are created at runtime. The app icon it self is created via a Mac app I wrote specifically for that purpose. I did it this way so I could learn about the graphics frame works. This knowledge came in handy just this week for for an iPad app where we have a bluetooth barcode scanner connected and want to report the battery level. :D

    The obvious starting place is the Apple docs. A Google search of the following words returns some good links; Cocoa Drawing Guide Introduction

    Cocoa Dev Central has a couple of intro articles that may be of interest to you; Introduction to Quartz, Intro to Quartz II.

    The Advanced drawing using AppKit over at Cocoa With Love shows you how to draw something that looks very complicated.

    I don't know about the OS X calls for retina displays, but I would think that when you start your app you could fetch the scaling you need and create an appropriate sized view. From then on the drawRect would always use that size.

    A tip that was given to me about drawing dynamic graphics was to think in layers. Like you may do when using a graphics drawing program. When you do that, you figure out what has to be done in which order. That breaks down the work into logical groups that you can attack individually and before you know it, you have a nice image.

    When I first wrote my graphics for CameraTime, I was thinking in the points system of an older lower resolution iPhone. I later discovered the scale function to get the scaling of the device and adapted my math to that value of either 1 or 2. The graphics now appear sharper on devices newer that an iPhone 3GS. To put that another way, the 3GS is the only low resolution device I currently support.

    Sure, this may be technically beyond what you can do now, but you can easily gain the knowledge in a short time to duplicate or even expand on your icon.
     
  4. ArtOfWarfare thread starter macrumors 604

    ArtOfWarfare

    Joined:
    Nov 26, 2007
    #4
    What method? I just looked through NSImage's class reference and didn't see anything about blocks...
     
  5. gnasher729, Aug 18, 2012
    Last edited: Aug 18, 2012

    gnasher729 macrumors P6

    gnasher729

    Joined:
    Nov 25, 2005
    #5
    I think it's new in Mountain Lion. Specifically for HiDPI support.

    Your drawing commands would be identical for Retina and non retina, but when the NSImage is used on a retina display, the drawing commands will create an image in retina resolution.

    developer.apple.com -> Mac Dev Center -> Featured Content / What's new in MacOS X -> MacOS X v10.8 Mountain Lion -> Block-Based Drawing for Offscreen Images
     
  6. klaxamazoo macrumors 6502

    Joined:
    Sep 8, 2006
    #6
    I've been using PaintCode.
    http://www.paintcodeapp.com/

    It seems to work well and is great for a novice like myself as I can see how different drawing commands are used. And also make a higher quality graphic much quicker than my current level of programming skill would normally let me. It also give immediate feedback as to what an image will look like and how changing the drawing parameters will change my image all without recompiling the program.

    I also like how the drawing canvas can be toggled between normal and retina to show you how your image will look at the two different resolutions. It also can output normal and retina .png's.

    Overall, I've found it to be well worth the money. I bought it through the App Store, but I think there is a demo version available on the website.
     
  7. ArtOfWarfare, Aug 18, 2012
    Last edited: Aug 18, 2012

    ArtOfWarfare thread starter macrumors 604

    ArtOfWarfare

    Joined:
    Nov 26, 2007
    #7
    I watched the video, thought, "Gee, that looks cool... I'll impulse buy if it's $10 and consider if it's $20." At $100 though... I'm already $30K in debt with student loans... I'm not sure if I can justify spending that money to save me a few hours of work... I'll try finding what gnasher mentioned.

    Edit: I found it exactly where gnasher mentioned... the name of the method is imageWithSize:flipped:drawingHandler... but it's really weird because, while Xcode recognized it as being from NSImage.h and offered a template for the method, it has no quick help for it and the docs at OS X 10.8 -> NSImage don't have the method listed... just checked and my docs are all up to date... weird. I'll try out the method and let you know how it goes! - although I still wish I had some sample code to work with so I had some idea of how it works...

    Edit 2X: It works like a charm! Thank you!

    Here's what my code looks like (obviously it's just a proof of concept as it completely ignores the values that are passed in right now.)

    Code:
    - (NSImage *)imageForPercent:(int)percent alternate:(BOOL)alternate charging:(BOOL)charging
    {
        return [NSImage imageWithSize:NSMakeSize(18, 18) flipped:NO drawingHandler:^BOOL(NSRect dstRect) {
            NSBezierPath* outline = [NSBezierPath bezierPathWithRoundedRect:NSMakeRect(1, 5, 14, 8) xRadius:1 yRadius:1];
            [[NSColor colorWithDeviceRed:91.0/255.0 green:91.0/255.0 blue:91.0/255.0 alpha:1.0] setStroke];
            [outline stroke];
            return true;
        }];
    }
    This produces the outline for the icon... unfortunately, I don't own a retina Mac Book Pro to test this on... I'll finish writing the code then send off a test version to my customers that asked for retina support... also, in the interest of supporting OS X versions all the way back to 10.6.6, I'll need to make sure it falls back on the prior methods if it finds itself running on a pre-Mountain Lion OS... the minority of customers who bought the retina Mac Book Pro prior to Mountain Lion's release and don't upgrade will just have to settle with the old graphics... I'm skeptical that they'll exist at all.
     
  8. xStep macrumors 68000

    Joined:
    Jan 28, 2003
    Location:
    Less lost in L.A.
    #8
    WWDC 2012 session 245 might help a bit with that new method. The piece of interest starts at 7:38. It looks interesting. It is also mentioned in session 204.

    Personally, I think you should be looking at using NSView and drawRect:. That may be old school thinking. LOL. I'm thinking you want to draw a different image each time a level changes. Once you've written drawRect code, the action to redraw is as simple as calling setNeedsDisplay on the view. Perhaps you can combine the two ideas.
     
  9. ArtOfWarfare thread starter macrumors 604

    ArtOfWarfare

    Joined:
    Nov 26, 2007
    #9
    How would I go from having an NSView to having an NSImage? Apps that appear in the system menu bar can only display an NSImage and an NSString, methods that modify the NSView are private and so using them would get the app rejected from the app store?
     
  10. chown33 macrumors 604

    Joined:
    Aug 9, 2009
    #10
    Don't use an NSImage. Instead, you'd get the CGContextRef of the NSView, and draw into that.

    Or make an off-screen bit-map and gets its context, draw into that, then get that as an NSImage. Or use an off-screen CGLayer.

    If you haven't read the Quartz 2D Programming Guide, you should. There's a whole section on getting a graphics context to draw into.
     
  11. gnasher729 macrumors P6

    gnasher729

    Joined:
    Nov 25, 2005
    #11
    1. You don't need a retina display to test this. Use Quartz Debug to enable HiDPI modes on your display. Basically that changes for example a 1920 x 1080 monitor to a 960 x 540 Retina display with really huge pixels.

    2. To support earlier OS versions I'd write something like (you'll need to fix it to make it compile)

    Code:
    ^BOOL (NSRect destRect)myDrawingHandler = ^...;
    
    NSImage* image;
    if ([NSImage respondsToSelector:@selector (imageWithSize:flipped:drawingHandler:)]) {
       image = [NSImage imageWithSize:... flipped:... drawingHandler:myDrawingHandler];
    } else {
        image = (old way to create an image);
       ... set up image for drawing
       myDrawingHandler (...);
    }
    So you _either_ get an image that redraws itself as needed, or on an older OS you draw it once. I'd assume that any Mac with retina display supports the new calls.
     
  12. xStep macrumors 68000

    Joined:
    Jan 28, 2003
    Location:
    Less lost in L.A.
    #12
    To get an NSImage from an NS view, it looks like you use dataWithEPSInsideRect: or dataWithPDFInsideRect: and work from that.

    You've now specified something I didn't know; That the menu bar can only display NSImages. Given that tidbit of info, I suspect you don't want to bother with an NSView. Since it wouldn't be displayed, I don't think it would redraw automatically. I think as a first pass to this I'd just create a new NSImage each time I need one. One for the menu bar, and one each for the menu items. You could reuse the menu bar one for the low value on the menu items by drawing into another NSImage for the item.

    I'm starting to come up with more questions. Both technical and implementation wise. Wish I had time to code this up. It would be a fun little learning exercise.
     
  13. JoshDC macrumors regular

    Joined:
    Apr 8, 2009
    #13
    In terms of actually constructing the image, I'd suggest composing from a few separate images instead of trying to do it all in code. I think trying to create complex bezier paths in code will turn out to be more hassle than it's worth.

    For your purpose, I think you need three images: the battery outline, the charge level indicator, and the plugged-in indicator. The graphics context will allow you to add clipped regions and specify composition options as you build up your image to create a number of different effects.

    I've created a project that allows for arbitrary charge levels and sizes using those three image. It uses the block-based NSImage method to:

    1. Draw the level of charge, clipped to the level remaining.
    2. Optionally draw the plugged-in indicator.
    3. Draw the battery outline.

    I hope this points you in the right direction, but if you need additional help I'm happy to post the project.
     

Share This Page