Resolved Binding sliders to a NSview

Discussion in 'Mac Programming' started by monsieurpaul, Feb 14, 2011.

  1. monsieurpaul, Feb 14, 2011
    Last edited: Feb 28, 2011

    monsieurpaul macrumors regular

    Joined:
    Oct 8, 2009
    #1
    Hello,

    I am trying to understand bindings in cocoa and to that purpose I am creating an app that draws a black rectangle on a blue background using sliders. The interface is as follows :

    [​IMG]

    The interface includes a custom view and 2 sliders: 1 horizontal for the black rectangle width and 1 vertical for the black rectangle height.

    I have a NSView subclass that draw the black rectangle on the blue background. The black rectangle is centered and its size is half the view size. This part works and reacts well to window resizing.

    Code:
    - (void)drawRect:(NSRect)dirtyRect {
    	
    	NSLog(@"drawRect called in : %p", self);
    	
    		// blue background drawing
    	
    	[[NSColor blueColor] set];
    	NSRect fond = [self bounds];
    	NSRectFill(fond);
    		
    		// black rectangle origin and size computation
    	
    	float pixabcisse, pixordonne, pixlargeur, pixhauteur;
    	float fondhauteur = [self bounds].size.height;
    	float fondlargeur = [self bounds].size.width;
    	
    	pixlargeur = (fondlargeur / 100) * blackrectwidth;
    	pixhauteur = (fondhauteur / 100) * blackrectheight;
    	pixabcisse = (fondlargeur - pixlargeur) / 2 ;
    	pixordonne = (fondhauteur - pixhauteur) / 2 ;
    	
    	
    		//black rectangle drawing
    	
    	NSRect pix;
    	[[NSColor blackColor] set];
    	pix = NSMakeRect(pixabcisse,pixordonne, pixlargeur, pixhauteur);
    	NSRectFill(pix);
    }
    
    I have a controller class (NSObject) with 2 float variables (sliderheight and sliderwidth). These variables are bound to their respective sliders using IB. This also works well.

    The problem is that I am not sure how to send the sliders values to my NSView subclass.

    My first attempt was to add the sliderheight and sliderwidth variables to the NSView subclass (and not using a controller class). However, I ended with 2 instances of my NSView subclass, one bound to the custom view and the other bound to the sliders.

    I understand that I can use notifications to pass the sliders value to my NSView class but is there simpler / better way to do that ?

    Thanks,

    Paul
     
  2. kainjow Moderator emeritus

    kainjow

    Joined:
    Jun 15, 2000
    #2
    Simplest would be to setup key-value observing for those slider values and update the view when they change. See addObserver:forKeyPath:eek:ptions:context: and observeValueForKeyPath:eek:fObject:change:context:

    Handling bindings in a custom view is not built-in, but doable. However, IIRC, you can't use IB to setup the bindings, it still has to be done in code by calling bind:toObject:withKeyPath:eek:ptions: so it's not a quick alternative.
     
  3. monsieurpaul thread starter macrumors regular

    Joined:
    Oct 8, 2009
    #3
    Indeed, it works with notifications. I modified the setter method for the float variables in the controller object:

    Code:
    - (void)setSliderheight:(float) x {
    	
    	sliderheight = x; //get the height value from the slider
    	
    		//create 2 NSNumbers to store height and width
    	
    	sliderh = [[NSNumber alloc] initWithFloat:sliderheight];
    	sliderw = [[NSNumber alloc] initWithFloat:sliderwidth];
    	
    		//add the NSNumber to the dictionary (initialized in the init method
    	
    	[dico setObject:sliderh forKey: @"height"];
    	[dico setObject:sliderw forKey: @"width"];
    
    		//Send the notification with the dictionary
    	[nc postNotificationName:@"BB" object:self userInfo :dico];
    	
    		//release the NSNumber
    	[sliderh release];
    	[sliderw release];
    	
    
    }
    
    The setSliderwidth method is very similar.

    I added a "redrawBB" method in my NSView subclass that is called by the notification center:

    Code:
    - (void)redrawBB:(NSNotification *)note {
    		
    		//get height and width from dictionary as NSNumber
    	
    	NSNumber *bbh = [[note userInfo] objectForKey:@"height"];
    	NSNumber *bbw = [[note userInfo] objectForKey:@"width"];
    	
    		//Address value from NSNumber to float variables used in drawrect
    	
    	blackrectheight = [bbh floatValue];
    	blackrectwidth = [bbw floatValue];
    
    		//redraw view;
    	
    	[self setNeedsDisplay:YES];
    	
    		//Release NSnumber bbh et bbw: Not here, I have tried but the app crashed because of that
    	
    		//[bbh release]; 
    		//[bbw release];
    }
    It works as intended. However I find the code not very satisfying and I have 2 NSNumber (bbh et bbw) that I don't know when and how to release.

    EDIT: Thanks Kainjow for your answer, I will look into that.
     
  4. monsieurpaul thread starter macrumors regular

    Joined:
    Oct 8, 2009
    #4
    Now, I am rewriting the app using the "lolview" example (chapter 13) in the book: "Learn Cocoa on the Mac" from Mark and LaMarche.

    • Same interface: 1 custom view and 2 sliders: horizontal and vertical
    • 1 controller class (NSObject) with 2 float variables (sliderh and sliderw) and a IBOutlet (bbview) for the view class. Sliderh and sliderw are bound to their respective slider in IB, bbview is made an outlet of the custom view in IB.
    • 1 view class (NSView) with 2 float variables (bbh and bbw). The class is "attached" to the custom view in IB.

    The bindings between controller and view are written in the controller init method. Below is the controller class:

    Code:
    //BBController.h
    #import <Cocoa/Cocoa.h>
    @class BBView; //BBView is the view class
    
    
    @interface BBController : NSObject {
    	float sliderh;
    	float sliderw;
    	IBOutlet BBView *bbview;
    
    }
    @property (assign) float sliderh;
    @property (assign) float sliderw;
    
    @end
    
    //BBController.m
    
    #import "BBController.h"
    
    
    @implementation BBController
    @synthesize sliderh;
    @synthesize sliderw;
    
    - (id)init {
    	if (![super init]) {
    		return nil;
    	}
    	
    	//binding between BBController and bbview
    
    	[bbview bind:@"bbh" toObject:self withKeyPath:@"sliderh" options:nil];
    	[bbview bind:@"bbw" toObject:self withKeyPath:@"sliderw" options:nil];
    
    	sliderh = 50;
    	sliderw = 50;
    	
    	NSLog(@"BB controller init: %p", self);
    	return self;
    }
    
    - (void)setSliderh:(float) f { //setSliderh overridden just to add a NSLog
    	sliderh = f;
    	NSLog(@"sliderh in controller = %f", sliderh);
    	
    }
    The app compiles with no warnings and the bindings between the sliders and the controller variables works. However, the controller doesn't pass the variables to the view class.

    I haven't modified the view class, except for the variable name.

    In the Apple doc examples (Graphic binding or Joystick view), a lot of code is added in the view classes: the bind:toObject..., observeValueForKeyPath:... are rewritten for example. However, in the Lolview example, no such thing are added in the view class.

    One one hand, I really don't like copy/pasting code that i don't understand. On the other hand, something is missing, but what ?
     
  5. kainjow Moderator emeritus

    kainjow

    Joined:
    Jun 15, 2000
    #5
    Most likely what's happening is at init the nib hasn't loaded yet, so bbview will be nil. Put all that binding stuff in awakeFromNib.
     
  6. monsieurpaul thread starter macrumors regular

    Joined:
    Oct 8, 2009
    #6
    Spot on. Thank you again kainjow.

    In their example, Mark and Lamarche used the App_delegate class as the controller and put the bindings in the applicationDidFinishLaunching method. Xcode should have some kind of timeline feature to help us (or me at least) understand what happens when we click on "run".

    Now, lets save this view as an image...

    ...to be continued.
     
  7. Comrade Yeti macrumors newbie

    Joined:
    Nov 3, 2010
    #7
  8. monsieurpaul thread starter macrumors regular

    Joined:
    Oct 8, 2009
    #8
    Thank you Comrade for the link.
    However I don't know in which order the object in my nib are created. If I change their order in my nib, will it change the creating sequence ?

    What I would like is a timeline based on your application that show when a class or variable is created/changed/released. That could be useful for debugging and memory management.
     
  9. monsieurpaul thread starter macrumors regular

    Joined:
    Oct 8, 2009
    #9
    The save panel was relatively easy:

    Code:
    -(IBAction)saveViewAsFile:(id)sender {
    	
    		//Determining the file suffix
    	NSArray *fileType = [[NSArray alloc] initWithObjects:@"jpg", nil];
    
    		//Defining size of the file and current size of the view
    		NSSize fileSize = NSMakeSize(400,400);
    		NSSize viewSize = NSMakeSize([bbview bounds].size.width, [bbview bounds].size.height);
    	
    		//Init save panel
    	NSSavePanel *bbsavePanel = [NSSavePanel savePanel];
    	[bbsavePanel setAllowedFileTypes:fileType];
    
    		if([bbsavePanel runModal] == NSOKButton){
    			
    			[bbview lockFocus];
    				
    				//Resize the view at the wanted size (400x400)
    				//Not working as intended
    			
    			[bbview setBoundsSize:fileSize];
    			[bbview setFrameSize:fileSize];
    			
    				//Getting URL of file to save from savepanel
    			NSURL *fileUrl = [bbsavePanel URL];
    			
    				//Setting content of view in an NSBitmapImageRep
    			NSBitmapImageRep *bbviewrep = [[NSBitmapImageRep alloc]
    											initWithFocusedViewRect:[bbview bounds]];
    			
    				//Setting content of NSBitmapImageRep in an NSData
    			NSData *data = [[NSData alloc] init];
    			data = [bbviewrep representationUsingType:NSJPEGFileType properties:nil];
    			
    				//Write the file
    			[data writeToURL:fileUrl atomically:YES];
    			
    				//Going back to original size of the view
    				///Not working as intended
    			
    				[bbview setBoundsSize:viewSize];
    				[bbview setFrameSize:viewSize];
    
    			[bbview unlockFocus];
    			
    		} 
    		else {
    			
    			return;
    		}
    }
    However, I am becoming obsessional with this app and I have 2 small problems that I want to address:

    1) I would like a save panel with a dropdown menu to choose the filetype (jpg, png, etc.). Is there an easy way to do that or must I have to create a custom view and add it in the panel ?

    2) The initial custom view is a square. Window resizing is tweaked to keep the custom view proportions using this method:

    Code:
    - (NSSize)windowWillResize:(NSWindow *)sender 
                        toSize:(NSSize)proposedFrameSize 
    { 
    	proposedFrameSize.height = (proposedFrameSize.width * 521)/463; 
    
    //521 and 463 are initial height and width of the window
    
    	return proposedFrameSize; 
    }
    I want that, whatever the view size, the saved file has always the same size (400 x 400 pixels). Using setBoundsSize and setFrameSize in my save method give me the wanted size for the file but the result is a 400 x 400 crop of the view.
     

Share This Page