I'm trying to figure out how to custom draw Buttons in Cocoa/OSX. Since my view is custom drawn I will not use IB and want to do it all in code. I created a subclass of NSButtonCell and a subclass of NSButton. In the Subclass of NSButtonCell I override the Method drawBezelWithFrame:inView: and in the initWithFrame Method of my subclassed NSButton I use setCell to set my CustomCell in the Button. However, drawBezelWithFrame gets not called and I don't understand why. Can someone point out what I've done wrong or what I miss here?
Subclass of NSButtonCell:
#import "TWIButtonCell.h"
#implementation TWIButtonCell
-(void)drawBezelWithFrame:(NSRect)frame inView:(NSView *)controlView
{
//// General Declarations
[[NSGraphicsContext currentContext] saveGraphicsState];
//// Color Declarations
NSColor* fillColor = [NSColor colorWithCalibratedRed: 0 green: 0.59 blue: 0.886 alpha: 1];
//// Rectangle Drawing
NSBezierPath* rectanglePath = [NSBezierPath bezierPathWithRect: NSMakeRect(8.5, 7.5, 85, 25)];
[fillColor setFill];
[rectanglePath fill];
[NSGraphicsContext restoreGraphicsState];
}
#end
Subclass of NSButton:
#import "TWIButton.h"
#import "TWIButtonCell.h"
#implementation TWIButton
- (id)initWithFrame:(NSRect)frame
{
self = [super initWithFrame:frame];
if (self)
{
TWIButtonCell *cell = [[TWIButtonCell alloc]init];
[self setCell:cell];
}
return self;
}
- (void)drawRect:(NSRect)dirtyRect
{
// Drawing code here.
}
#end
Usage:
- (void)addSendButton:(NSRect)btnSendRectRect
{
TWIButton *sendButton = [[TWIButton alloc] initWithFrame:btnSendRectRect];
[self addSubview:sendButton];
[sendButton setTitle:#"Send"];
[sendButton setTarget:self];
[sendButton setAction:#selector(send:)];
}
Following are the things seems to be missing out from your code.
You are not calling the [super drawRect:dirtyRect]
You are not overriding + (Class)cellClass in the Class(TWIButton) which is derived from NSButton.
Below is the code after changes:
#implementation TWIButton
- (id)initWithFrame:(NSRect)frame
{
self = [super initWithFrame:frame];
if (self)
{
TWIButtonCell *cell = [[TWIButtonCell alloc]init];
[self setCell:cell];
}
return self;
}
- (void)drawRect:(NSRect)dirtyRect
{
// Drawing code here.
//Changes Added!!!
[super drawRect:dirtyRect];
}
//Changes Added!!!!
+ (Class)cellClass
{
return [TWIButtonCell class];
}
#end
Now keep the break point at drawBezelWithFrame and check it will get called.
One might forgo the NSButton subclass, since it looks like you are ONLY using it to instantiate the Cell type within the initializer.
Simply
NSButton *button ...
[button setCell: [[TWIButtonCell alloc] init] autorelease]];
btw. you will probably have a leak in the previous examples since you init and then call setCell that likely has its own retain.
Related
I want to show images in a grid on iPhone using a UICollectionView, showing 3 images a row. For a "dead simple test" (as I thought), I've added 15 JPG images to my project, so that they'll be in my bundle and I can load them simply via [UIImage imageNamed:...].
I think I've done everything correct (setting up & registering UICollectionViewCell subclass, use of UICollectionViewDataSource Protocol methods), however, the UICollectionView behaves very weird:
It shows only a few of the images in the following pattern:
First line shows image 1 & 3, second line is blank, next line like the first again (image 1 & 3 showing properly), fourth line blank, and so on...
If I push a button in my NavBar that triggers [self.collectionView reloadData], random cells appear or disappear. What drives me nuts is that it's not only an issue of images appear or not. Sometime, images also swap between the cells, i.e. they appear for a indexPath they are definitely not wired up!
Here is my code for the cell:
#interface AlbumCoverCell : UICollectionViewCell
#property (nonatomic, retain) IBOutlet UIImageView *imageView;
#end
#implementation AlbumCoverCell
#synthesize imageView = _imageView;
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
_imageView = [[UIImageView alloc] initWithFrame:frame];
[self.contentView addSubview:_imageView];
}
return self;
}
- (void)dealloc
{
[_imageView release];
[super dealloc];
}
- (void)prepareForReuse
{
[super prepareForReuse];
self.imageView.image = nil;
}
#end
Part of the code for my UICollectionViewController subclass, where 'imageNames' is an NSArray holding all jpg filenames:
- (void)viewDidLoad
{
[super viewDidLoad];
[self.collectionView registerClass:[AlbumCoverCell class] forCellWithReuseIdentifier:kAlbumCellID];
}
#pragma mark - UICollectionViewDataSource Protocol methods
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return [self.imageNames count];
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
AlbumCoverCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kAlbumCellID forIndexPath:indexPath];
NSString *imageName = [self.imageNames objectAtIndex:indexPath.row];
NSLog(#"CV setting image for row %d from file in bundle with name '%#'", indexPath.row, imageName);
cell.imageView.image = [UIImage imageNamed:imageName];
return cell;
}
#pragma mark - UICollectionViewDelegateFlowLayout Protocol methods
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath;
{
return CGSizeMake(100, 100);
}
- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section;
{
return UIEdgeInsetsMake(0, 0, 0, 0);
}
From the NSLog statement in cellForItemAtIndexPath: I can see that the method is called for all of the cells (not only the one's displayed) and that the mapping between indexPath.row and filename is correct.
Has anybody an idea what could cause this weird behavior?
In the meantime, I've found the solution. It was actually a very subtle error in the implementation of my UICollectionViewCell subclass AlbumCoverCell.
The problem is that I've set the frame of the cell instance as the frame of the UIImageView subview instead of passing the bounds property of the cell's contentView!
Here is the fix:
#implementation AlbumCoverCell
#synthesize imageView = _imageView;
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// WRONG:
// _imageView = [[UIImageView alloc] initWithFrame:frame];
// RIGHT:
_imageView = [[UIImageView alloc] initWithFrame:self.contentView.bounds];
[self.contentView addSubview:_imageView];
}
return self;
}
- (void)prepareForReuse
{
[super prepareForReuse];
// reset image property of imageView for reuse
self.imageView.image = nil;
// update frame position of subviews
self.imageView.frame = self.contentView.bounds;
}
...
#end
I am trying to create a Cocoa UI that consists of two sets of squares (chess-like grids) that will assume different colours while an underlying algorithm is running. When the execution of the algorithm comes to an end, the UI should be able to handle clicks, panning and other gestures.
The hierarchy I have so far is the following (please check the attached code for specifics):
1) the main window that is the window of a window controller
2) a split view with two custom views, mainView and sideView (each one would hold a set of squares)
3) two view controllers (mainViewController and sideViewController)
I would like to be able to load the squares as subviews of mainView and sideView.
I thought of having another custom view, say SquareView with another nib file. My questions would be:
a) how do I create this SquareView so that it can be used to create the squares that will be added to mainView and sideView as subviews to form chess-like grids?
b) how do I add subviews to mainView and sideView to built the two grids? For the sake of simplicity, let's assume there would be four non-overlapping squares for each of the previously mentioned views.
Thank you!
MainView.m
#import "MainView.h"
#implementation MainView
- (void)drawRect:(NSRect)TheRect
{
[[NSColor grayColor] set];
[NSBezierPath fillRect:[self bounds]];
}
SideView.m
#import "SideView.h"
#implementation MainView
- (void)drawRect:(NSRect)TheRect
{
[[NSColor whiteColor] set];
[NSBezierPath fillRect:[self bounds]];
}
MainWindowController.h
#import <Cocoa/Cocoa.h>
#class SideViewController;
#class MainViewController;
#interface MainWindowController : NSWindowController
{
IBOutlet NSSplitView* oMainSplitView;
SideViewController* sideViewController;
MainViewController* mainViewController;
}
#end
MainWindowController.m
#import "MainWindowController.h"
#import "SideViewController.h"
#import "MainViewController.h"
#implementation MainWindowController
- (void)windowDidLoad
{
sideViewController = [[SideViewController alloc] initWithNibName:#"SideView" bundle:nil];
NSView* splitViewLeftView = [[oMainSplitView subviews] objectAtIndex:0];
NSView* sideView = [sideViewController view];
[sideView setFrame:[splitViewLeftView bounds]];
[sideView setAutoresizingMask:(NSViewWidthSizable | NSViewHeightSizable)];
[splitViewLeftView addSubview:sideView];
mainViewController = [[MainViewController alloc] initWithNibName:#"MainView" bundle:nil];
NSView* splitViewRightView = [[oMainSplitView subviews] objectAtIndex:1];
NSView* mainView = [mainViewController view];
[mainView setFrame:[splitViewRightView bounds]];
[mainView setAutoresizingMask:(NSViewWidthSizable | NSViewHeightSizable)];
[splitViewRightView addSubview:mainView];
}
You can make this as simple or as complicated as you desire: simple? do everything you want in MainView's drawRect method; complex: nest NSViews (or NSCell's, or NSBox's, etc.) and have each one draw itself.
Personally, I'd vote to keep it simpleā¦
a) I think the easiest way would be to create a matrix of NSBoxes, which you could do in code or in IB. Having the squares in a matrix would make it easy to loop through them or access specific ones.
b) I'm not sure what your question is here -- you would do it just as you did in your posted code, using [mainView addSubview:squareMatrix];
After Edit: Actually, it looks like IB won't let you embed NSBoxes in a matrix. In the past, I've made a matrix of subclassed NSButtonCells (to allow background color with no border) that had a grid of 64x64 cells that were clickable and would change color with those clicks. I don't know if you want a fixed number of cells in your views, or do you need to dynamically change the number? Something like this could work for you I think -- I actually created this in code, because IB was really slow in updating with that many cells.
Here is what I did. In my case, I needed cells with no border but with background color, so I had to subclass NSButtonCell, like this:
-(id)initWithRGBAlpha:(NSArray *)rgbAlpha {
if (self == [super init]) {
NSColor *color = [NSColor colorWithCalibratedRed:[[rgbAlpha objectAtIndex:0]doubleValue]
green:[[rgbAlpha objectAtIndex:1]doubleValue]
blue:[[rgbAlpha objectAtIndex:2]doubleValue]
alpha:[[rgbAlpha objectAtIndex:3]doubleValue]];
[self setBackgroundColor:color];
[self setTitle:#""];
[self setBordered:NO];
[self setTag:0];
[self setImageScaling:3];
return self;
}else{
return nil;
}
}
-(void) setState:(NSInteger)value {
if (value == 1) {
self.backgroundColor = self.selectedColor;
[super setState:value];
}else {
self.backgroundColor = self.backgroundColor;
[super setState:value];
}
}
-(void) setBackgroundColor:(NSColor *)color {
backgroundColor = color;
selectedColor = [color colorWithAlphaComponent:.75];
}
- (void)encodeWithCoder:(NSCoder *)encoder {
[super encodeWithCoder:encoder];
[encoder encodeObject:self.backgroundColor forKey:#"bColor"];
}
- (id)initWithCoder:(NSCoder *)decoder {
[super initWithCoder:decoder];
self.backgroundColor = [decoder decodeObjectForKey:#"bColor"];
return self;
}
I created the matrix in code, like so:
#implementation RDMatrix
-(void) initWithParentView:(NSView *) cv {
NSNumber *one = [NSNumber numberWithInt:1];
NSArray *colors = [NSArray arrayWithObjects:one,one,one,one,nil];
RDButtonCell *theCell = [[RDButtonCell alloc ]initWithRGBAlpha:colors];
[self initWithFrame:NSMakeRect(200,100,1,1) mode:2 prototype:theCell numberOfRows:64 numberOfColumns:64];
[self setSelectionByRect:TRUE];
[self setCellSize:NSMakeSize(8,8)];
[self sizeToCells];
self.target = self;
self.action = #selector(matrixClick:);
self.backgroundColor = [NSColor lightGrayColor];
self.drawsBackground = TRUE;
self.autoresizingMask = 8;
self.allowsEmptySelection = TRUE;
[cv addSubview:self];
}
-(void) matrixClick: (id) sender {
for (RDButtonCell *aCell in self.selectedCells){
if ([self.selectedCells count] < 64) {
aCell.backgroundColor = [NSColor colorWithCalibratedRed:1 green:0 blue:0 alpha:1];
}else{
aCell.backgroundColor = [NSColor colorWithCalibratedRed:0 green:.5 blue:1 alpha:1];
}
}
[self deselectAllCells];
}
#end
I have this in a custom class on my NSView and when I press a button using my drawMyRec IBAction I want to change the color of my NSRect however this is not working, can anyone help?
#import "myView.h"
#implementation myView
- (id)initWithFrame:(NSRect)frame
{
self = [super initWithFrame:frame];
if (self) {
RectColor = [[NSColor blackColor] init];
}
return self;
}
- (IBAction)drawMyRec:(id)sender;
{
NSLog(#"pressed");
RectColor = [[NSColor blueColor] init];
[self setNeedsDisplay:YES];
}
- (void)drawRect:(NSRect)dirtyRect
{
NSRectFill(dirtyRect);
[RectColor set];
}
#end
Your drawRect function is incorrect. change to this:
- (void)drawRect:(NSRect)dirtyRect
{
[RectColor set];
NSRectFill(dirtyRect);
}
First, why are you calling -init on an NSColor? Second, you're setting the color after you've drawn the rect, so it won't take affect until the next redraw. 3rd, what's dirtyRect when -drawRect: is called? Why not just fill the entire rect regardless of what's dirty?
I've got a custom NSView subclass which I want to have rounded corners. I use the following code in the .m file:
#import "ItemImageSelectionView.h"
#implementation ItemImageSelectionView
- (id)initWithFrame:(NSRect)frame
{
self = [super initWithFrame:frame];
if (self) {
self.wantsLayer = YES;
self.layer.frame = self.frame;
}
return self;
}
- (void)drawRect:(NSRect)dirtyRect
{
[[NSColor colorWithCalibratedRed:0.0 green:0.5 blue:1 alpha:1] set];
NSRectFill(dirtyRect);
[self.layer setCornerRadius:5.0];
}
#end
I use this code to initialize the view, pretty default:
NSView *imageSelectionView = [[ItemImageSelectionView alloc] initWithFrame:CGRectMake(imageView.frame.origin.x - 2, imageView.frame.origin.y - 2, imageView.frame.size.width + 4, imageView.frame.size.height + 4)];
[self addSubview:imageSelectionView positioned:NSWindowBelow relativeTo:imageView];
But it doesn't set any rounded corners! What am I doing wrong?
You don't need to set rounded corners in the drawrect method; set that in the init. Also, you should probably call the super method, [super drawRect:dirtyRect] in your overrided method. I'm not sure how your fill custom will interact with the layers, though. You may need to set [self.layer setMasksToBounds:YES];
I am confused about why this code does not display any image:
In the app delegate:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
NSRect rect = window.frame;
rect.origin.x = 0;
rect.origin.y = 0;
BlueImageView *blueImageView = [[BlueImageView alloc]initWithFrame:rect];
window.contentView = blueImageView; // also tried [window.contentView addSubview: blueImageView];
}
BlueImageView.h:
#interface BlueImageView : NSImageView {
}
#end
BlueImageView.m:
#implementation BlueImageView
- (id)initWithFrame:(NSRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self setImage: [NSImage imageNamed:#"imagefile.png"]];
NSAssert(self.image, #"");
NSLog (#"Initialized");
}
return self;
}
- (void)drawRect:(NSRect)dirtyRect {
}
#end
The file imagefile.png exists. The NSAssert is not causing an exception. The NSLog is firing. But no image shows up in the window.
The drawRect: method is called to draw the view, and your implementation immediately returns. To get NSImageView to draw the image for you, call [super drawRect:dirtyRect]; in your implementation of drawRect:. If you aren't going to do any other drawing in drawRect:, just remove the method to speed up drawing.