PDA

View Full Version : CGRect drawRect:(CGRect)rect - Using "rect"?




Buschmaster
Apr 15, 2008, 02:04 PM
My implementation may be incorrect but I'm confused as to how to actually use the rect being pushed into CGRect.

I have this code:
-(void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGPoint upperLeft = CGPointMake(0.0,0.0);
CGRect teamFrame = rect;
if(CGRectContainsPoint(teamFrame,upperLeft)) {
CGContextSetRGBFillColor(context, 1.0,0.0,0.0,1.0);
} else {
CGContextSetRGBFillColor(context, 0.0,0.0,1.0,1.0);
}
CGContextFillRect(context, teamFrame);
}

-(void)setUpTeams {
CGRect teamOneFrame = CGRectMake(0,0,160,480);
[self drawRect:teamOneFrame];
CGRect teamTwoFrame = CGRectMake(160,0,160,480);
[self drawRect:teamTwoFrame];
}


I am calling setUpTeams, and what I want it to do is take the first frame at the origin and make it 160 wide and 480 tall (or half of an iPhone screen).

Then it should draw it and the only reason I do the check to see if the rect is starting at the origin is because then I can have the screen split into two different parts.

However, this just seems to draw one big rectangle. Why am I not able to use "rect"?



HiRez
Apr 15, 2008, 04:03 PM
I'm not really familiar with Core Graphics, but maybe the rectangle and point aren't using the same coordinate space? Maybe you need to call CGContextConvertPointToDeviceSpace() and/or CGContextConvertRectToDeviceSpace() on your objects before testing?

Spike099
Apr 15, 2008, 05:37 PM
The first flaw in this design I notice is that you are sending the drawRect:rect method custom rectangles. drawRect is used to refresh the current view.

So, for example, when [self setNeedsDisplay:YES] is used, the drawRect: method is eventually called with the area of the entire view as it's rect. This may be why you are only seeing one rectangle been displayed.

Second flaw, you are using aspects of Core Graphics that aren't quite needed.

I would think you would want something more along the lines of this.

-(void)drawRect:(CGRect)rect {
// First, create a new rect with the upper half of the view
CGRect upperRect = CGRectMake(rect.origin.x, rect.origin.y, rect.size.width, rect.size.height / 2);
// Very similar to creating the upperRect, however for the lower portion of the view
CGRect lowerRect = CGRectMake(rect.origin.x, rect.origin.y + (rect.size.width / 2), rect.size.width, rect.size.height / 2);

[[UIColor redColor] set]; // red team color
UIRectFill(upperRect); // this will fill the upper rect all red,
// if you'd like, try these lines to get a nice effect
// [[UIColor whiteColor] set];
// UIRectFrame(upperRect); // This will frame the rect with a one point line I believe
[[UIColor blueColor] set]; // blue team color
UIRectFill(lowerRect); // and of course, fill our lower rect with blue
}

So, this eliminates your setUpTeams method and slightly simplifies things. Let me know if you'd like me to expand on any of this code. I haven't tested this btw.

EDIT: I just tested the code and it does draw the top half of the view red and the lower blue. I do believe this is what you were trying to accomplish. Plus, I threw in some more comments for you.

kaius
Apr 24, 2008, 11:56 AM
(void)drawRect:(CGRect)rect {
draw oscilloscope tracing goes here
{

I am trying to draw an oscilloscope type tracing, which is drawn slowly and over time from data in an array.

If I place my code in the above method, how can I ask it to draw slowly (using a timer for instance, where I draw a point every 1/20th of a second)? It seems to only want to run ONCE. That is, with no timer, I can draw all my tracing, no problems, and with the timer, only the first point is drawn...

I would appreciate your help.

Catfish_Man
Apr 24, 2008, 12:27 PM
(void)drawRect:(CGRect)rect {
draw oscilloscope tracing goes here
{

I am trying to draw an oscilloscope type tracing, which is drawn slowly and over time from data in an array.

If I place my code in the above method, how can I ask it to draw slowly (using a timer for instance, where I draw a point every 1/20th of a second)? It seems to only want to run ONCE. That is, with no timer, I can draw all my tracing, no problems, and with the timer, only the first point is drawn...

I would appreciate your help.


You're fairly fundamentally misunderstanding how drawRect works. It's not something that *you call* to draw things. It's something that *cocoa* calls when it wants to know what the contents of a certain area are. You will need to do something like call setNeedsDisplayInRect ever 1/20th of a second on a timer.

kaius
Apr 24, 2008, 01:15 PM
Catfish:

I totally I agree I don't understand this, although I did realize it was cocoa calling the draw method. I am very new to both the iPhone, objective C, and graphics. I am obviously not an engineer (I'm an MD for whatever that is worth these days)!

Here is what I have:
1) a method called drawWaveform which draws an osciloscope tracing from an array using CGContextAddLineToPoint(ctx, xCoord, yCoord);
2) a timer (HBTimer) that goes of every 1/20th of a second
3) - (void)onHeartbeat: (NSTimer *) HBtimer
{}

How do I connect these three with your answer so that in my loop for drawing each point inside drawWaveform I can add a 1/20thsecond delay?

Thx for your patience.

lucasgladding
Apr 24, 2008, 08:56 PM
You would probably be well served reading the Cocoa Drawing Guide.

If you add an instance variable to the view class to track the progress, you can increment that value and call setNeedsDisplay: each time the timer fires. The instance variable can then be used from within the drawRect: method.

Catfish:

I totally I agree I don't understand this, although I did realize it was cocoa calling the draw method. I am very new to both the iPhone, objective C, and graphics. I am obviously not an engineer (I'm an MD for whatever that is worth these days)!

Here is what I have:
1) a method called drawWaveform which draws an osciloscope tracing from an array using CGContextAddLineToPoint(ctx, xCoord, yCoord);
2) a timer (HBTimer) that goes of every 1/20th of a second
3) - (void)onHeartbeat: (NSTimer *) HBtimer
{}

How do I connect these three with your answer so that in my loop for drawing each point inside drawWaveform I can add a 1/20thsecond delay?

Thx for your patience.

kaius
Apr 25, 2008, 01:48 PM
Ok, I went reading, did some homework, and I am almost able to draw an oscilloscope like tracing - almost. The following code gives me two problems:

1) I am unable to "connect the dots" - that is, I draw the dots in an animated fashion, can't seem to connect them. I know my CGContextMoveToPoint has issues, but can't figure out why, and

2) when at the end I do my CGContextClearRect, it does not really work properly and my oscilloscope tracing starts again on the left, the dot's I have previously written show up...

Any ideas from you code warriors that know cocoa inside out?

#define SPO2_BUFFER 40
#define SPO2_WAVE_START_COORD_Y 290
#define SPO2_WAVE_START_COORD_X 10
#define CURVE_FATNESS 1

int gxCoord = SPO2_WAVE_START_COORD_X;
int gyCoord= SPO2_WAVE_START_COORD_Y;
CGContextRef gContext;
int gCounter = 0;
int gHighestArrayValue;

int gMonitorData[SPO2_BUFFER]=
{
10, 10, 10, 10, 11, 13, 16, 21, 30, 42,
54, 66, 78, 88, 92, 94, 95, 94, 93, 88,
82, 75, 66, 67, 72, 72, 67, 60, 55, 48,
41, 35, 29, 23, 18, 14, 12, 10, 10, 10
};

- (void)drawRect:(CGRect)rect
{
if (gCounter < 1)
return;

CGContextRef gContext = UIGraphicsGetCurrentContext();
CGContextBeginPath(gContext);
CGContextSetRGBStrokeColor(gContext,0.51,0.52, 0.9, 2.0);
CGContextSetLineWidth(gContext,2.0);

if(gxCoord <= rect.size.width - SPO2_WAVE_START_COORD_X) //stop drawing at end of screen
{
CGContextMoveToPoint(gContext, gxCoord, gyCoord +1);// find last place
CGContextAddLineToPoint(gContext, gxCoord , gyCoord );
CGContextStrokePath(gContext);
if (gyCoord == SPO2_WAVE_START_COORD_Y - gHighestArrayValue )
[self PlayHeartSound];
}
else
{
gxCoord = SPO2_WAVE_START_COORD_X;
CGContextClearRect(gContext, rect);
}
}

- (void)HeartbeatTimer{
NSTimer *HBtimer;
HBtimer = [NSTimer scheduledTimerWithTimeInterval:1.0/20.0
target:self
selector:@selector(onHeartbeat:)
userInfo:nil
repeats:YES];
}

- (void)onHeartbeat: (NSTimer *) HBtimer
{

// find highest value in current array
gHighestArrayValue = gMonitorData[0];
for (int i=0; i<SPO2_BUFFER;i++)
if (gMonitorData[i] > gHighestArrayValue)
gHighestArrayValue = gMonitorData[i];

gCounter = gCounter + 1;
if(gCounter <= SPO2_BUFFER-1)
{
gyCoord = -gMonitorData[gCounter]; // invert so as to draw up...
gyCoord = gyCoord + SPO2_WAVE_START_COORD_Y; // start curve almost half way down
gxCoord = gxCoord + CURVE_FATNESS; // number of x moves per each y move
[self setNeedsDisplay];
}
else
{
gCounter = 0;
}
}

kaius
Apr 26, 2008, 09:53 AM
This code generates and oscilloscope, but it still FLICKERS. I suspect it is the MOVETOPOINT that needs to be called all the time. Am I right in my assumption? Can anyone suggest fixes?

int gxCoord = SPO2_WAVE_START_COORD_X;
int gyCoord= SPO2_WAVE_START_COORD_Y;
int gxCoordOld = SPO2_WAVE_START_COORD_X;
int gyCoordOld= SPO2_WAVE_START_COORD_Y;
int gCounter = 0;
float gHighestArrayValue;

float gMonitorData[SPO2_BUFFER]=
{
10.0, 10.0, 10.0, 10.0, 11.0, 13.0, 16.0, 21.0, 30.0, 42.0,
54.0, 66.0, 78.0, 88.0, 92.0, 94.0, 95.0, 94.0, 93.0, 88.0,
82.0, 75.0, 66.0, 67.0, 72.0, 72.0, 67.0, 60.0, 55.0, 48.0,
41.0, 35.0, 29.0, 23.0, 18.0, 14.0, 12.0, 10.0, 10.0, 10.0
};


- (id)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame])
{
[self HeartbeatTimer]; //initiate timer - maybe not the best place, but it works...
}

return self;
}

- (void)drawRect:(CGRect)rect
{
if (gCounter < 1)
return;

// if (gDemoModeSwitchStatus == 0)
// return;

if(gxCoord == rect.size.width - SPO2_WAVE_START_COORD_X) // do this here - if I don't clear before I start, nothing happens
CGContextClearRect(gContext, rect);

if (gContext == NULL) // do this so as to not got through this code all the time - I thought this could be an issue
{
gContext = UIGraphicsGetCurrentContext();
CGContextBeginPath(gContext);
CGContextSetLineWidth(gContext,1.0);
}

if(gxCoord <= rect.size.width - SPO2_WAVE_START_COORD_X) //stop drawing at end of screen
{
CGContextSetRGBStrokeColor(gContext,0.51,0.52, 0.9, 2.0); //violet like color
CGContextMoveToPoint(gContext, gxCoordOld, gyCoordOld); // use last coordinates
CGContextAddLineToPoint(gContext, gxCoord , gyCoord );
CGContextStrokePath(gContext);

if (gyCoord == SPO2_WAVE_START_COORD_Y - gHighestArrayValue ) // compare to see if we beep or not
[self PlayHeartSound];
}
else
{
CGContextClearRect(gContext, rect); //time to start again at the left part of the screen
gxCoord = SPO2_WAVE_START_COORD_X;
}
}

- (void)HeartbeatTimer{
NSTimer *HBtimer;
HBtimer = [NSTimer scheduledTimerWithTimeInterval:1.0/40.0
target:self
selector:@selector(onHeartbeat:)
userInfo:nil
repeats:YES];

}

- (void)onHeartbeat: (NSTimer *) HBtimer
{
gHighestArrayValue = gMonitorData[0]; // find highest value in current array so as to send beep along with it...
for (int i=0; i<SPO2_BUFFER;i++)
{
if (gMonitorData[i] > gHighestArrayValue)
{
gHighestArrayValue = gMonitorData[i]; //got it - store it for later use in gVar
}
}
gxCoordOld = gxCoord; //save last coordinates for use later with movetopoint
gyCoordOld = gyCoord;
gCounter = gCounter + 1; //gCounter is used as counter for the array cells - in this case 40, its size
if(gCounter <= SPO2_BUFFER-1)
{
gyCoord = -gMonitorData[gCounter]; // invert so as to draw up...
gyCoord = gyCoord + SPO2_WAVE_START_COORD_Y; // start curve almost half way down
gxCoord = gxCoord + CURVE_FATNESS; // number of x moves per each y move
[self setNeedsDisplay]; // send redisplay to screen
}
else
gCounter = 0;
}

lucasgladding
Apr 26, 2008, 10:57 AM
This code generates and oscilloscope, but it still FLICKERS. I suspect it is the MOVETOPOINT that needs to be called all the time. Am I right in my assumption? Can anyone suggest fixes?

int gxCoord = SPO2_WAVE_START_COORD_X;
int gyCoord= SPO2_WAVE_START_COORD_Y;
int gxCoordOld = SPO2_WAVE_START_COORD_X;
int gyCoordOld= SPO2_WAVE_START_COORD_Y;
int gCounter = 0;
float gHighestArrayValue;

float gMonitorData[SPO2_BUFFER]=
{
10.0, 10.0, 10.0, 10.0, 11.0, 13.0, 16.0, 21.0, 30.0, 42.0,
54.0, 66.0, 78.0, 88.0, 92.0, 94.0, 95.0, 94.0, 93.0, 88.0,
82.0, 75.0, 66.0, 67.0, 72.0, 72.0, 67.0, 60.0, 55.0, 48.0,
41.0, 35.0, 29.0, 23.0, 18.0, 14.0, 12.0, 10.0, 10.0, 10.0
};


- (id)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame])
{
[self HeartbeatTimer]; //initiate timer - maybe not the best place, but it works...
}

return self;
}

- (void)drawRect:(CGRect)rect
{
if (gCounter < 1)
return;

// if (gDemoModeSwitchStatus == 0)
// return;

if(gxCoord == rect.size.width - SPO2_WAVE_START_COORD_X) // do this here - if I don't clear before I start, nothing happens
CGContextClearRect(gContext, rect);

if (gContext == NULL) // do this so as to not got through this code all the time - I thought this could be an issue
{
gContext = UIGraphicsGetCurrentContext();
CGContextBeginPath(gContext);
CGContextSetLineWidth(gContext,1.0);
}

if(gxCoord <= rect.size.width - SPO2_WAVE_START_COORD_X) //stop drawing at end of screen
{
CGContextSetRGBStrokeColor(gContext,0.51,0.52, 0.9, 2.0); //violet like color
CGContextMoveToPoint(gContext, gxCoordOld, gyCoordOld); // use last coordinates
CGContextAddLineToPoint(gContext, gxCoord , gyCoord );
CGContextStrokePath(gContext);

if (gyCoord == SPO2_WAVE_START_COORD_Y - gHighestArrayValue ) // compare to see if we beep or not
[self PlayHeartSound];
}
else
{
CGContextClearRect(gContext, rect); //time to start again at the left part of the screen
gxCoord = SPO2_WAVE_START_COORD_X;
}
}

- (void)HeartbeatTimer{
NSTimer *HBtimer;
HBtimer = [NSTimer scheduledTimerWithTimeInterval:1.0/40.0
target:self
selector:@selector(onHeartbeat:)
userInfo:nil
repeats:YES];

}

- (void)onHeartbeat: (NSTimer *) HBtimer
{
gHighestArrayValue = gMonitorData[0]; // find highest value in current array so as to send beep along with it...
for (int i=0; i<SPO2_BUFFER;i++)
{
if (gMonitorData[i] > gHighestArrayValue)
{
gHighestArrayValue = gMonitorData[i]; //got it - store it for later use in gVar
}
}
gxCoordOld = gxCoord; //save last coordinates for use later with movetopoint
gyCoordOld = gyCoord;
gCounter = gCounter + 1; //gCounter is used as counter for the array cells - in this case 40, its size
if(gCounter <= SPO2_BUFFER-1)
{
gyCoord = -gMonitorData[gCounter]; // invert so as to draw up...
gyCoord = gyCoord + SPO2_WAVE_START_COORD_Y; // start curve almost half way down
gxCoord = gxCoord + CURVE_FATNESS; // number of x moves per each y move
[self setNeedsDisplay]; // send redisplay to screen
}
else
gCounter = 0;
}

Does the flicker occur if the CGContextClearRect is removed? That would be my first guess. Without CGContextClearRect, is the view not drawn or is it simply drawn once and redrawn over the same path (making it appear as though nothing is animated)? I would try filling the rect with the desired background colour instead of clearing personally.

Best of luck

Lucas Gladding

kaius
Apr 27, 2008, 05:11 PM
Lucas,

Without the CGContextClearRect nothing is drawn. But the flickering occurs in an awkward way because the data from the array does not seem to be drawn in an ordered fashion (that is, the way that it is coming from the array) but what you see when you slow the timer down is some "dancing" of the lines back and forth...

I'll try what you suggest and let you know.

Thanks for the input.

lucasgladding
Apr 27, 2008, 09:39 PM
Lucas,

Without the CGContextClearRect nothing is drawn. But the flickering occurs in an awkward way because the data from the array does not seem to be drawn in an ordered fashion (that is, the way that it is coming from the array) but what you see when you slow the timer down is some "dancing" of the lines back and forth...

I'll try what you suggest and let you know.

Thanks for the input.

kaius

I just looked through your code in more detail and realized that only one segment is being drawn in drawRect each time the timer fires (unless I am missing something). Someone else may want to chime in here, but I have always drawn the entire view with drawRect rather than the changes alone. I would move the coordinate setting code into the drawRect method and use a for loop to step through each visible point.

This is little more than a guess on my part, but the selective drawing may explain the flicker that you are seeing. If anyone else has ideas to the contrary here, please feel free to correct me. Remember, however, that drawRect can be called automatically by other objects. That unpredictable nature could cause problems with the approach you have taken.

Good luck again

Luke

lucasgladding.wordpress.com (http://lucasgladding.wordpress.com)

kaius
Apr 27, 2008, 10:12 PM
In fact, the CGContextClearRect(gContext, gRect); needs to be called the TWO times so that when the tracing reaches the end (right) and restarts on the left, it does NOT overwrite the previous tracing

Here is a screen pic of the tracing. Note the discontinuous lines and now add to this a display that shows each segment every other time, thus flickering.

I have reviewed the code time and time again, and I am at a loss as to what is going on here.

SEE THIS LINK TO VIEW IMAGE: http://mindensoaringclub.com/int2/upfiles/oscilloscope3.tiff

kaius
Apr 27, 2008, 11:38 PM
Hi Lucas,

OK, you're idea half works... I redraw everything all the time as you suggested, and this half works. But now, when the oscilloscope reaches the right, the CGContextClearRect does not work and does not erase the previous tracing...

I have tried using ContextClosePath after the ClearRect, but this also does not work...

Here's my code after your suggestion:

- (void)drawRect:(CGRect)oldrect
{
if (gCounter < 1)
return;

// if (gDemoModeSwitchStatus == 0)
// return;

gContext = UIGraphicsGetCurrentContext();
CGContextBeginPath(gContext);
CGContextSetLineWidth(gContext,1.0);

if(gCounter == SPO2_BUFFER-2)
{
CGContextClearRect(gContext, oldrect);
}

if(gCounter <= SPO2_BUFFER-2) //stop drawing at end of screen
{
CGContextSetRGBStrokeColor(gContext,0.51,0.52, 0.9, 2.0);
gxCoord = SPO2_WAVE_START_COORD_X;
gyCoord = SPO2_WAVE_START_COORD_Y;
CGContextMoveToPoint(gContext, gxCoord, gyCoord);
for (int bisCounter = 1; bisCounter <= gCounter; ++bisCounter)
{
gyCoord = -gMonitorData[bisCounter]; // invert so as to draw up...
gyCoord = gyCoord + SPO2_WAVE_START_COORD_Y;
gxCoord = gxCoord + CURVE_FATNESS;
CGContextAddLineToPoint(gContext, gxCoord, gyCoord);
}
CGContextStrokePath(gContext);

if (gyCoord == SPO2_WAVE_START_COORD_Y - gHighestArrayValue )
[self PlayHeartSound];
}
else
{
CGContextClearRect(gContext, oldrect);
CGContextClosePath(gContext);
}

}

- (void)HeartbeatTimer{
NSTimer *HBtimer;
HBtimer = [NSTimer scheduledTimerWithTimeInterval:1.0/40.0
target:self
selector:@selector(onHeartbeat:)
userInfo:nil
repeats:YES];
gRect = CGRectMake(0, 0, 340, 480);
}

- (void)onHeartbeat: (NSTimer *) HBtimer
{

gHighestArrayValue = gMonitorData[0]; // find highest value in current array so as to send beep along with it...
for (int i=0; i<SPO2_BUFFER;i++)
{
if (gMonitorData[i] > gHighestArrayValue)
gHighestArrayValue = gMonitorData[i]; //got it
}
gCounter = gCounter + 1;
if(gCounter <= SPO2_BUFFER-1)
[self setNeedsDisplayInRect:gRect]; // send redisplay to screen
else
gCounter = 0;
}

lucasgladding
Apr 28, 2008, 12:45 AM
Hi Lucas,

OK, you're idea half works... I redraw everything all the time as you suggested, and this half works. But now, when the oscilloscope reaches the right, the CGContextClearRect does not work and does not erase the previous tracing...


kaius

Try replacing the CGContextClearRect with CGContextFillRect and moving it to the beginning of your drawRect method. You should first set the context fill colour to white (or the background colour for your view).

I'm a little unclear about CGContextClearRect, but here is what I understand from my reading:

Think about onion-skinning in traditional animation. You're currently doing something similar by using CGContextClearRect. If you layer a transparent image over another image, no change is visible. The API documentation described CGContextClearRect as "paints a transparent rectangle".

If you want to use CGContextClearRect, you will first need to redraw the nearest opaque ancestor. This redraw is what actually "clears" the canvas. If you look in the NSView class documentation, you will see multiple references to opaqueAncestor. In rectangular views with solid background colours, filling the background is usually the easiest approach.

Views behave as stencils on a canvas. Once you understand some of the technical details, most issues are pretty easy to understand.

kaius
Apr 28, 2008, 08:24 AM
Lucas,

YOU WERE RIGHT! It works...

I guess my surprise with working with all this is that it requires some logic, but mostly, like in medicine, an apprenticeship in little tricks which are hardly documented. Thanks to this forum and you, the learning curve is much faster.

BTW, I know somebody in Kitchnener - an old GF - small world!

lucasgladding
Apr 28, 2008, 09:10 AM
Lucas,

YOU WERE RIGHT! It works...

I guess my surprise with working with all this is that it requires some logic, but mostly, like in medicine, an apprenticeship in little tricks which are hardly documented. Thanks to this forum and you, the learning curve is much faster.

BTW, I know somebody in Kitchnener - an old GF - small world!

Congrats on the success. The little tricks become more and more obvious as you learn. Sometimes the answers lie in the space between the lines in the documentation.

It definitely is a small world. Let me know if you are ever in the KW area visiting.