In this lab the topics you will become familiar with:
We will speak about how graphics libraries represent points and rectangles. CoreTools is based on the Mac graphics system, so I will speak of these interchangibly. By Win32 I mean the MS Windows APIs (system calls).
Most graphics libraries (Mac, CoreTools, Win32) depend on Point and Rectangle (Rect) as their basic datatypes. These are called Point and Rect in Mac and CoreTools (which builds on the Mac system), POINT and RECT in Win32 etc.
Assume the following declarations:
struct Rect { int left; // x-coordinate of the upper-left corner int top; // y-coordinate of the upper-left corner int right; // x-coordinate of the lower-right corner int bottom; // y-coordinate of the lower-right corner }; struct Point { int h; // the x-coordinate is called "h" instead of "x" on Mac int v; // the x-coordinate is called "v" instead of "y" on Mac };
These serve as arguments to basic graphics functions. For example, CoreTools contains the following:
void DrawLine( int x1, int y1, int x2, int y2); void DrawLine( Point P1, Point P2 ); void PaintRect( int x1, int y1, int x2, int y2); // paints a solid rectangle void PaintRect( Point P1, Point P2 ); // P1, P2 are the upper-left and lower-right corners void PaintRect( Rect R ); void FrameRect( int x1, int y1, int x2, int y2); // paints a frame rectangle, i.e. doesn't fill its interior void FrameRect( Point P1, Point P2 ); void FrameRect( Rect R ); void PaintCircle(Point center, int radius);and so on. There are also functions to fill these structs, such as
void SetPoint(Point& P, int x, int y) { P.h = x; P.v = y; } void SetRect(Rect& R, int x1, int y1, int x2, int y2) { R.left = x1; R.top = y1; R.right = x2; R.bottom = y2; }-- Notice that the first argument, a struct, is taken by reference! -- and these two functions:
Point MakePoint(int x, int y) { Point P; P.h = x; P.v = y; return P; } Rect MakeRect(int x1, int y1, int x2, int y2) { Rect R; R.left = x1; R.top = y1; R.right = x2; R.bottom = y2; return R; }-- Notice that these functions return a struct Point (struct Rect), so that the local variables P and R get copied outside the function. Therefore either of the following will draw the same rectangle for you:
PaintRect( 100, 100, 200, 300 ); Rect r; SetRect( r, 100, 100, 200, 300 ); PaintRect(r); PaintRect( MakeRect( 100, 100, 200, 300 ) ); Point p1, p2; // two points, the upper-left and lower-right corners SetPoint( p1, 100, 100 ); SetPoint( p1, 200, 300 ); PaintRect( p1, p2 );Notice that once you have defined and set a Point or a Rect, you can use it as a parameter to drawings functions as many times as you like -- the coords need to be specified only once.
This will draw a triangle:
Point p1, p2, p3; SetPoint( p1, 200, 50 ); SetPoint( p2, 100, 300 ); SetPoint( p3, 300, 250 ); DrawLine( p1, p2 ); DrawLine( p2, p3 ); DrawLine( p3, p1 ); // etc.
Since Point and Rect are now basic types, you can create arrays of them, just as with ints or doubles:
Point pts[10]; // array of 10 Points pts[0].h = 10; // set the first one to (10,20) pts[0].v = 20; pts[9].h = 100; // set the last one to (100,200) pts[9].v = 200; SetPoint( pts[1], 50, 60 ); // better than above DrawLine( pts[1], pts[9] ); // draw a line segment, etc.Consider this code that remembers the 10 consecutive points at which you click your mouse. It uses the CoreTools function
void GetClickPoint(Point& UpSpot)which waits for you to press the mouse button, then to release it, and fills its agrumnet, UpSpot, with the coordinates of the point where the button was released.
Point pts[10]; for( int i=0; i<10; i++ ){ GetClickPoint( pts[i] ); PaintCircle( pts[i], 5 ); // draw a little circle at click point } // now draw the trajectory for( int i=0; i<9; i++ ) // why 9, not 10? DrawLine( pts[i], pts[i+1] );You can now imagine the code that lets you draw any shape and then allows to move it around with the mouse, change its color, and save it to disk, like any good graphics editor does.
So far, we have seen only examples in which Point and Rect were used as containers (or "envelopes") to store data. A struct (as well as a class) in C++ can have more "behavior" to it than just being a container, and this is what Object Oriented Programming is all about.
A struct in C++ can have METHODS, its very own functions that work on its data members. In particular, you can modify a Rect so that it can draw itself, erase itself, answer questions about itself etc. Also, instead of using other function, such as SetRect, to initialize Rects, we can make the Rect initialize itself during its creation.
For compatibility with C, the standard Mac library keeps its Rect as a container only, without any methods. We will create a new type myRect that draws itself, initializes itself etc.
struct myRect { int left; // same data members int top; int right; int bottom; myRect(){ // "default constructor" left = right = top = bottom = 0; } myRect( int x1, int y1, int x2, int y2 ){ // "constructor with args" left = x1; top = y1; right = x2; bottom = y2; } void DrawSolid(){ PaintRect( left, top, right, bottom ); } void DrawFrame(){ FrameRect( left, top, right, bottom ); } bool isEmpty(){ // i.e. not really set to anything interesting if( left == 0 && right == 0 && top == 0 && bottom == 0 ) return true; else return false; } // etc. }; // end of struct definition // NOTE: if you forget the semicolon after the closing brace, you will // get weird compiler errors!Let us look at these functions one by one.
The first two, the "default constructor" and the "constructor with arguments", have the same name as the struct itself, and have no return type. These are the C++ syntax rules for constructors.
The default constructor is called every time a myRect is declared and no arguments are given, like
myRect r1, r2, r3; // default constructor is called 3 timesIt specifies what values the data members have right after an object of type myRect is created. The way I wrote it, they are all set to 0.
The constructor with arguments sets the data members to the provided values. It is called when a myRect is declared like this:
myRect r4( 100, 100, 200, 300 ); // calls the constructor with args myRect r5( 2, 57, 43, 91 ); // another oneRight after its creation, r4 is a rectangle with the corners (100,100) and (200,300). Thus myRect type does not need functions like MakeRect or SetRect, as the "pure container" type Rect does.
The other three functions, DrawSolid(), DrawFrame() and isEmpty() take no arguments. You can think of them as "belonging" to a particular myRect, and always invoked for a particular variable of type myRect, whose left, top, right and bottom data members they know without telling. These functions are invoked through the "dot" operator, like this:
r4.DrawSolid(); // draw r4 as solid, etc. if( r1.isEmpty() ) cout << "it's empty"; else r1.DrawSolid(); r5.DrawFrame();in each case, the METHODS above are invoked for a particular myRect, so there is no question which values of left, ... , bottom to use.
With each struct you declare, you get the assignment operator =, which copies one existing struct to another of the same type, e.g.:
r1 = r4; // sets r1's data members equal to these of r4You can still write a function that cooks up a myRect, like this:
myRect MakeMyRect( int x1, int y1, int x2, int y2 ){ myRect r ( x1, y1, x2, y2 ); // calls myRect's constructor with args return r; }Then you can use the returned value:
r2 = MakeMyRect( 100, 100, 200, 300 ); // sets r2or even write
MakeMyRect( 2, 57, 43, 91 ).DrawSolid();which draws the rectangle with corners (2,57) and (43,91). Notice that you cannot re-use that rectangle, because it is not saved in any variable, and has no name. The memory used by this struct will be de-allocated shortly after the rectangle is drawn. Such unnamed objects are called TEMPORARIES. Of course, there is nothing new about them: in the expression 3 + 5 - 1 the result of 3+5 is stored only as long as it is needed for the next operation, and you cannot refer to it later, you can only re-compute it.
By the way, just writing
r2 = myRect( 100, 100, 200, 300 );and
myRect( 2, 43, 57, 91 ).DrawSolid();will have the same effect, i.e. will construct a new object using myRect's constructor with args, and will de-allocate it some time soon. In the first case its contents will by then be copied into r2, in the second case they will be lost forever a short while after DrawSolid() does its work with them.
Now you see that METHODS (aka MEMBER FUNCTIONS) can do the same work as outside functions (such as SetRect etc.)
You can now add color to myRect, so that a myRect rectangle remembers its color and is always drawn in that color, not in the color of the current pen. For that, you only need to add the data members to keep the red, green and blue components of the RGB color, and then call the SetForeColor(..) function inside DrawSolid() and DrawFrame() methods to set the right color before you draw. You would also want to add these additional arguments to the constructor with arguments.
When the number of methods in a struct grows, keeping all the member functions inside its declaration is not good for readability. Instead, it is considered good style to give only prototypes of the member functions inside the class, and write the actual code for the member functions afterwards. Using this syntax with "outside function definitions", myRect can be re-written as
struct myRect { int left; // same data members int top; int right; int bottom; myRect(); myRect( int x1, int y1, int x2, int y2 ); void DrawSolid(); void DrawFrame(); bool isEmpty(); }; // end of struct definition // Now comes the code. Each member function is preceded by the struct // name (and the special marker ::), so that we know who it belongs to. myRect::myRect(){ // "default constructor" left = right = top = bottom = 0; } myRect::myRect( int x1, int y1, int x2, int y2 ){ // "constructor with args" left = x1; top = y1; right = x2; bottom = y2; } void myRect::DrawSolid(){ PaintRect( left, top, right, bottom ); } void myRect::DrawFrame(){ FrameRect( left, top, right, bottom ); } bool myRect::isEmpty(){ if( left == 0 && right == 0 && top == 0 && bottom == 0 ) return true; else return false; }This is mere syntax, the functionality did not change at all.
Practice this syntax with your myRect.
By the way, the new C++ standard encourages you to write the constructors differently. Instead of assignments to data members in the body of the constructor, as in
myRect::myRect( int x1, int y1, int x2, int y2 ){ // "constructor with args" left = x1; top = y1; right = x2; bottom = y2; }you should write
myRect::myRect( int x1, int y1, int x2, int y2 ) : left(x1), top(y1), right(x2), bottom(y2) { // empty body! }
This is better code, because what you really want done is not just assignments to data members, it is creation and simultaneous initialization of those data members. The empty constructor body shows that nothing else is happening. It is considered good style to have constructors with empty bodies, is possible, and do all initializations in the so-called initialization list, between : and { } .
Practice this syntax once you get your basic code to work.