- Page 500
Chapter 12 gave an idea of what we could do in terms of graphics using a set of simple interface classes, and how we can do it. This chapter presents many of the classes offered. The focus here is on the design, use, and implementation of individual interface classes such as Point, Color, Polygon, and Open_polyline and their uses. The following chapter will present ideas for designing sets of related classes and will also present more implementation techniques.
13.1 Overview of graphics classes
13.2 Point and Line
13.3 Lines
13.4 Color
13.5 Line_style
13.6 Open_polyline
13.7 Closed_polyline
13.8 Polygon
13.9 Rectangle
13.10 Managing unnamed objects
13.11 Text
13.12 Circle
13.13 Ellipse
13.14 Marked_polyline
13.15 Marks
13.16 Mark
13.17 Images
- Page 502
13.1 Overview of graphics classes
-- One purpose of our interface library is to reduce the shock delivered by the complexity of a full-blown graphics/GUI library. We present just two dozen classes with hardly any operations. A closely related goal is to introduce key graphics and GUI concepts through those classes. Already, you can write programs displaying results as simple graphics. After this chapter, your range of graphics programs will have increased to exceed most people's initial requirements. After Chapter 14, you'll understand most of the design techniques and ideas involved so that you can deepen your understanding and extend your range of graphics expression as needed. You can do so either by adding to the facilities described here or by adopting another C++ graphics/GUI library.
The key interface classes are:
- Color
- Line_style
- Point
- Line
- Open_polyline
- Closed_polyline
- Polygon
- Text
- Lines
- Rectangle
- Circle
- Ellipse
- Function
- Axis
- Mark
- Marks
- Marked_polyline
- Image
Chapter 15 examines Function and Axis. CHapter 16 presents the main GUI interface classes:
- Window : an area of the screen in which we display our graphics objects
- Simple_window : a window with a "Next" button
- Button : a rectangle, usually labeled, in a window that we can press to run one of our functions
- In_box : a box, usually labeled, in a window into which a user can type a string
- Out_box : a box, usually labeled, in a window into which our program can write a string
- Menu : a vector of Buttons
The source code is organized into files like this:
- Point.h : Point
- Graph.h : all other graphics interface classes
- Window.h : Window
- Simple_window.h : Simple_window
- GUI.h : Button and the other GUI classes
- Graph.cpp : definitions of functions from Graph.h
- Window.cpp : definitions of functions from Window.h
- GUI.cpp : definitions of functions from GUI.h
In addition to the graphics classes, we present a class that happens to be useful for holding collections for Shapes or Widgets:
Vector_ref : a vector with an interface that maeks it convenient for holding unnamed elements
The main points of this chapter are
- To show the correspondence between code and the pictures produced.
- To get you used to reading code and thinking about how it works
- To get you to think about the design of code - in particular to think about how to represent concepts as classes in code. Why do those classes look the way they do? How else could they have looked? We made many, many design decisions, most of which could reasonably have been made differently, in some cases radically differently.
- Page 505
13.2 Point and Line
The most basic part of any graphics system is the point. To define point is to define how we organize our geometric space. Here, we use a conventional, computer-oriented layout of two-dimensional points defined by (x,y) integer coordinates. As described in 12.5, x coordinates go from 0 (representing the left-hand side of the screen) to x_max() (representing the right-hand side of the screen); y coordintaes go from 0 (representing the top of the screen) to y_max() (representing the bottom of the screen).
In Graph.h, we find Shape, which we describe in detail in Chapter 14, and Line:
struct Line : Shape { // a Line is a Shape defined by two Points
Line(Point p1, Point p2); // construct a Line from two Points
};
A Line is a kind of Shape. That's what : Shape means. Shape is called a base class for Line or simply a base of Line. Shape provides the facilities needed to make the definition of Line simple. Once we have a feel for the particular shapes, such as Line and Open_polyline, we'll explain what that implies (Chapter 14).
A Line is defined by two Points. Leaving out the "scaffolding" (#includes, etc. as describe in 12.3), we can create lines and cause them to be drawn like this:
struct Point { int x, y; Point(int xx, int yy) : x(xx), y(yy) { } Point() :x(0), y(0) { } }; inline bool operator==(Point a, Point b) { return a.x==b.x && a.y==b.y; } inline bool operator!=(Point a, Point b) { return !(a==b); }
In Graph.h, we find Shape, which we describe in detail in Chapter 14, and Line:
struct Line : Shape { // a Line is a Shape defined by two Points
Line(Point p1, Point p2); // construct a Line from two Points
};
A Line is a kind of Shape. That's what : Shape means. Shape is called a base class for Line or simply a base of Line. Shape provides the facilities needed to make the definition of Line simple. Once we have a feel for the particular shapes, such as Line and Open_polyline, we'll explain what that implies (Chapter 14).
A Line is defined by two Points. Leaving out the "scaffolding" (#includes, etc. as describe in 12.3), we can create lines and cause them to be drawn like this:
const Point x{ 100, 100 };
Simple_window win1{ x, 600, 400, "two lines" };
Line horizontal{ x, Point{200,100} };
Line vertical{ Point{150,50}, Point{150,150} };
win1.attach(horizontal);
win1.attach(vertical);
win1.wait_for_button();
- Page 507
The implementation of Line's constructor is correspondingly simple:
Line::Line(Point p1, Point p2) // construct a line from two points { add(p1); // add p1 to this shape add(p2); // add p2 to this shape }
That is, it simply "adds" two points. Adds to what? And how does a Line get drawn in a window? The answer lies in the Shape class. As we'll describe in Chapter 14, Shape can hold points defining lines, knows how to draw lines defined by pairs of Points, and provides a function add() that allows an object to add a Point to its Shape. The key point(sic!) here is that defining Line is trivial. Most of the implementation work is done by "the system" so that we can concentrate on writing simple classes that are easy to use.
From now on we'll leave out the definition of the Simple_window and the calls of attach(). Those are just more "scaffolding" that we need for a complete program but that adds little to the discussion of specific Shapes.
struct Lines : Shape // related lines { Lines() {} // empty Lines(initializer_list<Point> lst); // initialize from a list of Points void draw_lines() const; void add(Point p1, Point p2); // add a line defined by two points };
Lines 오브젝트는 간단히 line들의 집합이다. 그리고 점들의 한 쌍에 의해 각각이 정의된다.
예를들어, 우리가 13.2의 Line 예제에서 한 개의 그래픽 오브젝트의 일부로서 두 직선을 고려한다면, 우리는 그것들을 이렇게 정의할 수 있다:
Lines x;
x.add(Point{100,100}, Point{200,100}); // first line : horizontal
x.add(Point{150,50}, Point{150,150}); // second line : vertical
- Page 508
By using Lines, we have expressed our opinion that the two lines belong together and should be manipulated together. For example, we can change the color of all lines that are part of a Lines object with a single command. On the other hand, we can give lines that are individual Line objects different colors. As a more realistic example, consider how to define a grid. A grid consists of a number of evenly spaced horizontal and vertical lines. However, we think of a grid as one "thing," so we define those lines as part of a Lines object, which we call grid:
{ int x_size = win1.x_max(); int y_size = win1.y_max(); int x_grid = 80; int y_grid = 40; Lines grid; for (int x = x_grid; x < x_size; x += x_grid) grid.add(Point{ x, 0 }, Point{ x,y_size }); // vertical line for (int y = y_grid; y < y_size; y += y_grid) grid.add(Point{ 0, y }, Point{ x_size,y }); }
- Page 509
This is also the first example where we are writing code that computes which objects we want to display. It would have been unbearably tedious to define this grid by defining one name variable for each grid line.
Let's return to the design of Lines. How are the member functions of class Lines implemented? Lines provides just two constructors and two operations.
The add() function simply adds a line defined by a pair of points to the set of lines to be displayed:
void Lines::add(Point p1, Point p2) { Shape::add(p1); Shape::add(p2); }
- Page 510
Yes, the Shape:: qualification is needed because otherwise the compiler would see add(p1) as an (illegal) attempt to call Lines'add() rather than Shape's add().
The draw_lines() function draws the lines defined using add():
void Lines::draw_lines() const { if (color().visibility()) for (int i=1; i<number_of_points(); i+=2) fl_line(point(i-1).x,point(i-1).y,point(i).x,point(i).y); }
That is, Lines::draw_lines() takes two points at a time (starting with points 0 and 1) and draws the line between them using the underlying library's line-drawing function (fl_line()). Visibility is a property of the Lines'Color object (13.4), so we have to check that the lines are meant to be visible before drawing them.
As we explain in Chapter 14, draw_lines() is called by "the system." We don't need to check that the number of points is even - Lines'add() can add only pairs of points.
The functions number_of_points() and point() are defined in class Shape (!4.2) and have their obvious meaning. These two functions provide read-only access to a Shape's points. The member function draw_lines() is defined to be const (See 9.7.4) because it doesn't modify the shape.
The default constructor for Lines simply creates an empty object (containing no lines): the model of starting out with no points and then add()ing points as needed is more flexible than any constructor could be. However, we also added a constructor taking an initializer_list of pairs of Points, each defining a line. Given that initializer-list constructor (18.2), we can simply define Lines starting out with 0, 1, 2, 3,... lines. For example, the first Lines example could be written like this:
Lines t1 = { {Point{100,100}, Point{200,100}}, {Point{150,50}, Point{150,150}} };
or even like this:
Lines t1 = { {{100,100}, {200,100}}, {{150,50}, {150,150}} };
The initializer-list constructor is easily defined:
Lines::Lines(initializer_list<pair<Point, Point>> lst) { for (auto p : lst) add(p.first, p.second); }
The auto is a placeholder for the type pair<Point,Point>, and first and second are the names of a pair's first and second members. They type initializer_list and pair are defined in the standard library (B.6.4, B.6.3).
- Page 511
13.4 Color
The purpose of Color is
- To hide the implementation's notion of color, FLTK's Fl_Color type
- To map between Fl_Color and Color_type values
- To give the color constants a scope
- To provide a simple version of transparency (visible and invisible)
You can pick colors
- From the list of names colors, for example, Color::dark_blue.
- By picking from a small "palette" of colors that most screens display well by specifying a value in the range 0-255; for example, Color(99) is a dark green. For a code example, see 13.9
- RGB model
- Page 513
Note the use of constructors to allow Colors to be created either from the Color_type or from a plain int. The member c is initialized by each constructor. You could argue that c is too short and too obscure a name to use, but since it is used only within the small scope of Color and not intended for general use, that's probably OK. We made the member c private to protect it from direct use from our users. For our representation of the data member c we use the FLTK type Fl_Color that we don't really want to expose to our users. However, looking at a color as an int representing its RGB (or other) value is very common, so we supplied as_int() for that. Note that as_int() is a const member because it doesn't actually change the Color object that it is used for.
13.5 Line_style
A line style is the pattern used to outline the line. We can use Line_style like this:
grid.set_style(Line_style::dot);
That "thinned out" the grid a bit, making it more discreet. By adjusting the width (thickness), we can adjust the grid lines to suit our taste and needs.
The programming techniques for defining Line_style are exactly the same as the ones we used for Color. Here, we hide the fact that FLTK uses plain ints to represent line styles. Why is something like that worth hiding? Because it is exactly such a detail that might change as a library evolves. The next FLTK release might very well have a Fl_linestyle type, or we might retarget our interface classes to some other GUI library. In either case, we wouldn't like to have our code and our user's code littered with plain ints that we just happend to know represent line styles.
- Page 515
Most of the time, we don't worry about style at all; we just rely on the default (default width and solid lines). This default line width is defined by the constructor in the cases where we don't specify one explicitly. Setting defaults is one of the things that constructors are good for, and good defaults can significantly help users of a class.
Note that Line_style has two "components": the style proper (e.g., use dashed or solid lines) and width (the thickness of the line used). The width is measured in integers. The default width is 1. We can request a fat dashed line like this:
grid.set_style(Line_style{Line_style::dash, 2});
- Page 517
13.6 Open_polyline
An Open_polyline is a shape that is composed of a series of connected line segments defined by a series of points. Poly is the Greek word for "many," and polyline is a fairly conventional name for a shape composed of many lines.
- Page 518
Class Open_polyline is defined like this:
struct Open_polyline : Shape { // open sequence of lines using Shape::Shape; // use Shape's constructors void add(Point p) { Shape::add(p); } void draw_lines() const; };
Open_polyline inherits from Shape. Open_polyline's add() function is there to allow the users of an Open_polyline to access the add() from Shape (that is, Shape::add()). We don't even need to define a draw_lines() because Shape by default interprets the Points add()ed as a sequence of connected lines.
The declaration using Shape::Shape is a using declaration. It says that an Open_polyline can use the constructors defined for Shape. Shape has a default constructor (9.7.3) and an initializer-list constructor (18.2), so the using declaration is simply a shorthand for defining those two constructors for Open_polylines. As for Lines, the initializer-list constructor is there as a shorthand for an initiali sequence of add()s.
- Page 519
13.7 Closed_polyline
A Closed_polyline is just like an Open_polyline, except that we also draw a line from the last from the last point to the first. For example, we could use the same points we used for the Open_polyline in 13.6 for a Closed_polyline:
------------------------------------------------------------------------------
A.16 Aliases (Page 1247)
We can define an alias for a name; that is, we can define a symbolic name that means exactly the same as what it refers to (for most uses of the name):
A reference(8.5., A.8.3) is a run-time mechanism, referring to objects. The using(20.5) and namespace aliases are compile-time mechanisms, referring to names. In particular, a using does not introduce a new type, just a new name for a type. For example:
Older codes uses the keyword typedef (27.3.1) rather than the (C++) using notation to define a type alias. For example:
typedef char* Pchar; // P char is a name for char*
------------------------------------------------------------------------------
The using declaration (A.16) says that Closed_polyline has the same constructors as Open_polyline. Closed_polyline needs its own draw_lines() to draw that closing line connecting the last to the first.
We only have to do the little detail where Closed_polyline differs from what Open_polyline offers. That's important and is sometimes called "programming by difference." We need to program only what's different about our derived class (here, Closed_polyline) compared to what a base class (here, Open_polyline) offers
So how do we draw that closing line? We use the FLTK line-drawing function fl_line().
13.8 Polygon
A Polygon is very similar to a Closed_polyline. The only difference is that for Polygons we don't allow lines to cross.
A Polygon is a Closed_polyline where lines do not cross. Alternatively, we could emphasize the way a shape is built out of points and say that a Polygon is a Closed_polyline where we cannot add a Point that defines a line segment that intersects one of the existing lines of the Polygon.
- Page 522
Here we inherit Closed_polyline's definition of draw_lines(), thus saving a fair bit of work and avoiding duplication of code. Unfortunately, we have to check each add(). That yields an inefficient (order N-squared) algorithm - defining a Polygon with N points requires N * (N-1) / 2 calls of intersect(). In effect, we have made the assumption that the Polygon class will be used for polygons of a low number of points. For example, creating a Polygon with 24 Points involves 24 * (24-1) / 2 = 276 calls of intersect(). That's probably acceptable, but if we wanted a polygon with 2000points it would cost us about 2,000,000 calls, and we might look for a better algorithm, which might require a modified interface.
- Page 523
The trouble is that Polygon's invariant "the points represent a polygon" can't be verified until all points have been defined; that is, we are not - as strongly recommended - establishing Polygon's invariant in its constructor. We considered removing add() and requiring that a Polygon be completely specified by an initializer list with at least three points, but that would have complicated uses where a program generated a sequence of points.
13.9 Rectangle
Anyway, rectangles are so common that GUI systems support them directly rather than treating them simply as polygons that happen to have four corners and right angles.
We can specify a rectangle by two points (top left and bottom right) or by one point (top left) and a width and a height.
- Page 524
One of the reasons that some graphics/GUI systems treat rectangles as special is that the algorithm for determining which pixels are inside a rectangle is far simpler - and therefore far faster - than for other shapes, such as Polygons and Circles. Consequently, the notion of "fill color" - that is, the color of the space inside the rectangle - is more commonly used for rectangles that for other shapes. We can set the fill color in a constructor or by the operation set_fill_color() (provided by Shape together with the other services related to color):
- Page 526
Our Window(E.3) provides a simple way of reordering shapes. You can tell a window to put ashape on top (using Window::put_on_top()).
- Page 529
13.10 Managing unnamed objects
So far, we have named all our graphical objects. When we want lots of objects, this becomes infeasible. As an example, let us draw a simple color chart of the 256 colors in LTK's palette; that is, let's make 256 colored squares and draw them in a 16-by-16 matrix that shows how colors with similar color values relate.
-- For example, it can be useful to have a collection of unnamed objects (elements) that are not all of the same type. We discuss that flexibility issue in 14.3. Here, we'll just present our solution: a vector type that can hold named and unnamed objects
We explain the new operator in Chapter 17, and the implemenation of Vector_ref is presented in Appendix E. For now, it is sufficient to know that we can use it to hold unnamed objects. Operator new is followed by the name of type (here, Rectangle) optionally followed by an initializer list.
Given Rectangle and Vector_ref, we can play with colors. For example, we can draw a simple color chart of the 256 colors shown above:
We make a Vector_ref of 256 Rectangles, organized graphically in the Window as a 16-by-16 matrix. We give the Rectangles the colors 0, 1, 2, 3, 4, and so on. After each Rectangle is created, we attach it to the window, we attach it to the window, so that it will be displayed.
13.11 Text
Obviously, we want to be able to add text to our displays. For example, we might want to label our "odd" Closed_polyline from 13.8:
Text t {Point{20,,200}, "A closed polyline that isn't a polygon"};
t.set_color(Color::blue);
Basically, a Text object defines a line of text starting at a Point. The Point will be the bottom left corner of the text. The reason for restricting the string to be a single line is to ensure portability across systems. Don't try to put in a newline character; it may or may not be represented as a newline in your window. Stringstreams(11.4) are useful for composing strings for display in Text objects (examples in 12.7.7 and 12.7.8).
- Page 534
13.12 Circle
Just to show that the world isn't completely rectangular, we provide class Circle and class Ellipse.
- Page 535
The main peculiarity of Circle's implementation is that the point stored is not the center, but the top left corner of the square bouding the circle. We could have stored either but chose the on FLTK uses for its optimized circle-drawing routine. That way, Circle provides another example of how a class can be used to present a different (and supposedly nicer) view of a concept than its implementation.
- Page 536
13.13 Ellipse
An ellipse is similar to Circle but is defined with both a major and a minor axis, instead of a radius; that is, to define an ellipse, we give the center's coordinates, the distance from the center to a point on the x axis, and the distance from the center to a point on the y axis.
Note that an Ellipse with major() == minor() looks exactly like a circle.
Another popular view of an ellipse specifies two foci plus a sum of distance from a point to the foci. Given an Ellipse, we can compute a focus.
- Page 537
Why is a Circle not an Ellipse? Geometrically, every circle is an ellipse, but not every ellipse is a circle. In particular, a circle is an ellipse where the two foci are equal.
- Page 538
When we design classes, we have to be careful not to be too clever and not to be deceived by our "intuition" into defining classes that don't make sense as classes in our code. Conversely, we have to take care that our classes represent some coherent concept and are not just a collection of data and function members. Just throwing code together without thinking about what ideas/concepts we are representing is "hacking" and leads to code that we can't explain and that others can't maintain. If you don't feel altruistic, remember that "others" might be you in a few months' time. Such code is also harder to debug.
- Page 543
The string{1,c} is a constructor for string, initializing the string to contain the single character c.
13.17 Images
The average personal computer holds thousands of images in files and can access millions more over the web. Naturally, we want to display some of those images in even quite simple programs.
- Page 547
Opening a file and then closing it again is a fairly clumsy way of portably separating errors related to "can't open the file" from errors related to the format of the data in the file.
Drill
1. Make an 800-by-1000 Simple_window.
2. Put an 8-by-8 grid on the leftmost 800-by-800 par tof that window (so that each square is 100 by 100).
3. Make the eight squares on the diagonal starting from the top left corner red (use Rectangle).
4. Find a 200-by-200-pixel image (JPEG or GIF) and place three copies of it on the grid (each image covering four squares). If you can't find an image that is exactly 200 by 200, use set_mask() to pick a 200-by-200 section of a larger image. Don't obscure the red squares
5. Add a 100-by-100 image. Have it move around from square to square when you click the"Next" button. Just put wait_for_button() in a loop with some code that picks a new square for your image.
The declaration using Shape::Shape is a using declaration. It says that an Open_polyline can use the constructors defined for Shape. Shape has a default constructor (9.7.3) and an initializer-list constructor (18.2), so the using declaration is simply a shorthand for defining those two constructors for Open_polylines. As for Lines, the initializer-list constructor is there as a shorthand for an initiali sequence of add()s.
- Page 519
13.7 Closed_polyline
A Closed_polyline is just like an Open_polyline, except that we also draw a line from the last from the last point to the first. For example, we could use the same points we used for the Open_polyline in 13.6 for a Closed_polyline:
{ Closed_polyline cpl = { {100, 100}, {150, 200}, {250, 250}, {300, 200} }; }
------------------------------------------------------------------------------
A.16 Aliases (Page 1247)
We can define an alias for a name; that is, we can define a symbolic name that means exactly the same as what it refers to (for most uses of the name):
using Pint = int*; // Pint means pointer to int namespace Long_library_name {/* ...*/} namespace Lib = Long_library_name; // Lib means Long_libary_name int x = 7; int& r = x; // r means x
A reference(8.5., A.8.3) is a run-time mechanism, referring to objects. The using(20.5) and namespace aliases are compile-time mechanisms, referring to names. In particular, a using does not introduce a new type, just a new name for a type. For example:
using Pchar = char*; // Pchar is a name for char* Pchar p = "Idelfix"; // OK : p is a char* char* q = p; // OK: p and q are both char*s int x = strlen(p); // OK: p is a char*
Older codes uses the keyword typedef (27.3.1) rather than the (C++) using notation to define a type alias. For example:
typedef char* Pchar; // P char is a name for char*
------------------------------------------------------------------------------
The using declaration (A.16) says that Closed_polyline has the same constructors as Open_polyline. Closed_polyline needs its own draw_lines() to draw that closing line connecting the last to the first.
We only have to do the little detail where Closed_polyline differs from what Open_polyline offers. That's important and is sometimes called "programming by difference." We need to program only what's different about our derived class (here, Closed_polyline) compared to what a base class (here, Open_polyline) offers
So how do we draw that closing line? We use the FLTK line-drawing function fl_line().
13.8 Polygon
A Polygon is very similar to a Closed_polyline. The only difference is that for Polygons we don't allow lines to cross.
A Polygon is a Closed_polyline where lines do not cross. Alternatively, we could emphasize the way a shape is built out of points and say that a Polygon is a Closed_polyline where we cannot add a Point that defines a line segment that intersects one of the existing lines of the Polygon.
- Page 522
Here we inherit Closed_polyline's definition of draw_lines(), thus saving a fair bit of work and avoiding duplication of code. Unfortunately, we have to check each add(). That yields an inefficient (order N-squared) algorithm - defining a Polygon with N points requires N * (N-1) / 2 calls of intersect(). In effect, we have made the assumption that the Polygon class will be used for polygons of a low number of points. For example, creating a Polygon with 24 Points involves 24 * (24-1) / 2 = 276 calls of intersect(). That's probably acceptable, but if we wanted a polygon with 2000points it would cost us about 2,000,000 calls, and we might look for a better algorithm, which might require a modified interface.
- Page 523
The trouble is that Polygon's invariant "the points represent a polygon" can't be verified until all points have been defined; that is, we are not - as strongly recommended - establishing Polygon's invariant in its constructor. We considered removing add() and requiring that a Polygon be completely specified by an initializer list with at least three points, but that would have complicated uses where a program generated a sequence of points.
13.9 Rectangle
Anyway, rectangles are so common that GUI systems support them directly rather than treating them simply as polygons that happen to have four corners and right angles.
We can specify a rectangle by two points (top left and bottom right) or by one point (top left) and a width and a height.
- Page 524
One of the reasons that some graphics/GUI systems treat rectangles as special is that the algorithm for determining which pixels are inside a rectangle is far simpler - and therefore far faster - than for other shapes, such as Polygons and Circles. Consequently, the notion of "fill color" - that is, the color of the space inside the rectangle - is more commonly used for rectangles that for other shapes. We can set the fill color in a constructor or by the operation set_fill_color() (provided by Shape together with the other services related to color):
- Page 526
Our Window(E.3) provides a simple way of reordering shapes. You can tell a window to put ashape on top (using Window::put_on_top()).
- Page 529
13.10 Managing unnamed objects
So far, we have named all our graphical objects. When we want lots of objects, this becomes infeasible. As an example, let us draw a simple color chart of the 256 colors in LTK's palette; that is, let's make 256 colored squares and draw them in a 16-by-16 matrix that shows how colors with similar color values relate.
-- For example, it can be useful to have a collection of unnamed objects (elements) that are not all of the same type. We discuss that flexibility issue in 14.3. Here, we'll just present our solution: a vector type that can hold named and unnamed objects
Vectror_ref<Rectangle> rect; Rectangle x {Point{100,200}, Point{200,300}}; rect.push_back(x); // add named rect.push_back(new Rectangle{Point{50,60}.Point{80,90}}); // add unnamed for(int i = 0; i < rect.size(); ++i) rect[i].move(10,10); // use rect
We explain the new operator in Chapter 17, and the implemenation of Vector_ref is presented in Appendix E. For now, it is sufficient to know that we can use it to hold unnamed objects. Operator new is followed by the name of type (here, Rectangle) optionally followed by an initializer list.
Given Rectangle and Vector_ref, we can play with colors. For example, we can draw a simple color chart of the 256 colors shown above:
Vector_ref<Rectangle> vr; for(int i = 0; i < 16; ++i) for (int j = 0; j < 16; ++j) { vr.push_back(new Rectangle{ Point{i * 20, j * 20}, 20, 20 }); vr[vr.size() - 1].set_fill_color(Color(i * 16 + j)); win1.attach(vr[vr.size() - 1]); }
We make a Vector_ref of 256 Rectangles, organized graphically in the Window as a 16-by-16 matrix. We give the Rectangles the colors 0, 1, 2, 3, 4, and so on. After each Rectangle is created, we attach it to the window, we attach it to the window, so that it will be displayed.
13.11 Text
Obviously, we want to be able to add text to our displays. For example, we might want to label our "odd" Closed_polyline from 13.8:
Text t {Point{20,,200}, "A closed polyline that isn't a polygon"};
t.set_color(Color::blue);
Basically, a Text object defines a line of text starting at a Point. The Point will be the bottom left corner of the text. The reason for restricting the string to be a single line is to ensure portability across systems. Don't try to put in a newline character; it may or may not be represented as a newline in your window. Stringstreams(11.4) are useful for composing strings for display in Text objects (examples in 12.7.7 and 12.7.8).
- Page 534
13.12 Circle
Just to show that the world isn't completely rectangular, we provide class Circle and class Ellipse.
- Page 535
The main peculiarity of Circle's implementation is that the point stored is not the center, but the top left corner of the square bouding the circle. We could have stored either but chose the on FLTK uses for its optimized circle-drawing routine. That way, Circle provides another example of how a class can be used to present a different (and supposedly nicer) view of a concept than its implementation.
- Page 536
13.13 Ellipse
An ellipse is similar to Circle but is defined with both a major and a minor axis, instead of a radius; that is, to define an ellipse, we give the center's coordinates, the distance from the center to a point on the x axis, and the distance from the center to a point on the y axis.
Note that an Ellipse with major() == minor() looks exactly like a circle.
Another popular view of an ellipse specifies two foci plus a sum of distance from a point to the foci. Given an Ellipse, we can compute a focus.
- Page 537
Why is a Circle not an Ellipse? Geometrically, every circle is an ellipse, but not every ellipse is a circle. In particular, a circle is an ellipse where the two foci are equal.
- Page 538
When we design classes, we have to be careful not to be too clever and not to be deceived by our "intuition" into defining classes that don't make sense as classes in our code. Conversely, we have to take care that our classes represent some coherent concept and are not just a collection of data and function members. Just throwing code together without thinking about what ideas/concepts we are representing is "hacking" and leads to code that we can't explain and that others can't maintain. If you don't feel altruistic, remember that "others" might be you in a few months' time. Such code is also harder to debug.
- Page 543
The string{1,c} is a constructor for string, initializing the string to contain the single character c.
13.17 Images
The average personal computer holds thousands of images in files and can access millions more over the web. Naturally, we want to display some of those images in even quite simple programs.
- Page 547
Opening a file and then closing it again is a fairly clumsy way of portably separating errors related to "can't open the file" from errors related to the format of the data in the file.
Drill
1. Make an 800-by-1000 Simple_window.
2. Put an 8-by-8 grid on the leftmost 800-by-800 par tof that window (so that each square is 100 by 100).
3. Make the eight squares on the diagonal starting from the top left corner red (use Rectangle).
4. Find a 200-by-200-pixel image (JPEG or GIF) and place three copies of it on the grid (each image covering four squares). If you can't find an image that is exactly 200 by 200, use set_mask() to pick a 200-by-200 section of a larger image. Don't obscure the red squares
5. Add a 100-by-100 image. Have it move around from square to square when you click the"Next" button. Just put wait_for_button() in a loop with some code that picks a new square for your image.
#include "Simple_window.h" #include "Graph.h" #include "std_lib_facilities.h" using namespace Graph_lib; int main() try { const Point x{ 100, 100 }; Simple_window win1{ x, 800, 1000, "Chapter 13 Drill" }; int xm = win1.x_max(); int ym = win1.y_max(); Lines grid; for (int i = 0; i < 8; ++i) { grid.add(Point{ i * 100, 0 }, Point{ i * 100, ym }); grid.add(Point{ 0, i * 100 }, Point{ xm, i * 100 }); } grid.set_color(Color::dark_cyan); grid.set_style(Line_style{ Line_style::dot, 2 }); win1.attach(grid); Vector_ref<Rectangle> vRect; for (int i = 0; i < 8; ++i) { vRect.push_back(new Rectangle{ {i * 100, i * 100}, 100, 100 }); vRect[vRect.size() - 1].set_fill_color(Color::red); win1.attach(vRect[vRect.size() - 1]); } Vector_ref<Image> vImg; for (int i = 0; i < 3; ++i) { vImg.push_back(new Image{ Point{200 * (i + 1), 0}, "test.jpg" }); vImg[vImg.size() - 1].set_mask(200, 200); win1.attach(vImg[vImg.size() -1]); } int row = 0, col = 0; Image movingImg{ Point{ 0,0 }, "test2.gif" }; movingImg.set_mask(100, 100); for(int i = 0; i < 8; ++i) for (int j = 0; j < 8; ++j) { movingImg.set_position(j * 100, i * 100); win1.attach(movingImg); win1.wait_for_button(); } return 0; } catch (exception& e) { cerr << e.what() << '\n'; return 1; } catch (...) { }
Exercises
For each "define a class" exercise, display a couple of objects of the class to demonstrate that they work.
1. Define a class Arc, which draws a part of an ellipse. Hint: fl_arc().
----------------------------------------------------------------------------------
Chapter 13 Graphics class
Chapter 14 Graphics class Design
Chapter 15 Graphing Functions and Data
Chapter 16 Graphical User Interfaces
지금 하고 있는 챕터들이 class 관련 모든 기능들에 대한 설명을 해주고 있으니, 여기 연습문제부터는 하나하나씩 그 의미를 알고 열심히 해야 한다.
--------------------------------------------------------------------------------------
struct Arc : Shape { public: // center, min, and max distance from center, start angle, end angle Arc(Point p, int ww, int hh, double sa, double ea) :w{ ww }, h{ hh }, sAngle{ sa }, eAngle{ ea } { add(Point{ p.x - ww, p.y - hh }); } void draw_lines() const { if (fill_color().visibility()) { // fill fl_color(fill_color().as_int()); fl_pie(point(0).x, point(0).y, w + w - 1, h + h - 1, sAngle, eAngle); fl_color(color().as_int()); // reset color } if (color().visibility()) { fl_color(color().as_int()); fl_arc(point(0).x, point(0).y, w + w, h + h, sAngle, eAngle); } } Point center() const { return{ point(0).x + w, point(0).y + h }; } Point focus1() const { return{ center().x + int(sqrt(double(w*w - h * h))), center().y }; } Point focus2() const { return{ center().x - int(sqrt(double(w*w - h * h))), center().y }; } void set_major(int ww) { set_point(0, Point{ center().x - ww, center().y - h }); w = ww; } int major() const { return w; } void set_minor(int hh) { set_point(0, Point{ center().x - w, center().y - hh }); h = hh; } int minor() const { return h; } void set_startAngle(double sa, double ea) { if (sa >= ea) error("startAngle should be smaller than endAngle"); sAngle = sa; eAngle = ea; } double startAngle() const { return sAngle; } double endAngle() const { return eAngle; } private: int w; int h; double sAngle; double eAngle; };
2. Draw a box with rounded corners. Define a class Box, consisting of four lines and four arcs.
I did it.
struct Box : Shape { public: // Center Point of the Rounded Box Box(Point xy, int ww, int hh, int ddx, int ddy) : w{ ww }, h{ hh }, dx{ ddx }, dy{ ddy } { if (h <= 0 || w <= 0 || dx <= 0 || dy <= 0) error("Bad rectangle: non-positive side"); add(xy); } void draw_lines() const; int height() const { return h; } int width() const { return w; } private: int dx; int dy; int h; int w; };
void Box::draw_lines() const { int halfw = w / 2; int halfh = h / 2; if (fill_color().visibility()) { // fill fl_color(fill_color().as_int()); fl_rectf(point(0).x - halfw, point(0).y - halfh - dy, w, h + dy * 2); fl_rectf(point(0).x - halfw - dx, point(0).y - halfh, w + dx * 2, h); fl_pie(point(0).x - halfw - dx, point(0).y - halfh - dy, dx * 2, dy * 2, 90, 180); fl_pie(point(0).x + halfw - dx, point(0).y - halfh - dy, dx * 2, dy * 2, 0, 90); fl_pie(point(0).x - halfw - dx, point(0).y + halfh - dy, dx * 2, dy * 2, 180, 270); fl_pie(point(0).x + halfw - dx, point(0).y + halfh - dy, dx * 2, dy * 2, 270, 360); fl_color(color().as_int()); // reset color } if (color().visibility()) { // edge on top of fill fl_color(color().as_int()); // Left Line fl_line(point(0).x - halfw - dx, point(0).y - halfh, point(0).x - halfw - dx, point(0).y + halfh); // Right Line fl_line(point(0).x + halfw + dx, point(0).y - halfh, point(0).x + halfw + dx, point(0).y + halfh); // Upper Line fl_line(point(0).x - halfw, point(0).y + halfh + dy, point(0).x + halfw, point(0).y + halfh + dy); // Lower Line fl_line(point(0).x - halfw, point(0).y - halfh - dy, point(0).x + halfw, point(0).y - halfh - dy); // Upper Left Arc fl_arc(point(0).x - halfw - dx, point(0).y - halfh - dy, dx * 2, dy * 2, 90, 180); // Upper Right Arc fl_arc(point(0).x + halfw - dx, point(0).y - halfh - dy, dx * 2, dy * 2, 0, 90); // Lower Left Arc fl_arc(point(0).x - halfw - dx, point(0).y + halfh - dy, dx * 2, dy * 2, 180, 270); // Lower Right Arc fl_arc(point(0).x + halfw - dx, point(0).y + halfh - dy, dx * 2, dy * 2, 270, 360); } }
3. Define a class Arrow, which draws a line with an arrowhead.
I did it!
struct Arrow : Line { enum headDirection { headOnA, headOnB }; Arrow(Point a, Point b, headDirection aorb, int wingLen) : Line(a, b), wingLength(wingLen) { pair<float, float> reverseDirection; if (aorb == headOnA) { starter = a; reverseDirection.first = b.x - a.x; reverseDirection.second = b.y - a.y; } else if (aorb == headOnB) { starter = b; reverseDirection.first = a.x - b.x; reverseDirection.second = a.y - b.y; } // Direction Normalization float inverselength = 1.f / sqrt(reverseDirection.first * reverseDirection.first + reverseDirection.second * reverseDirection.second); reverseDirection.first *= inverselength; reverseDirection.second *= inverselength; float rA = sqrt(2) / 2; // rotateAngle // rotate 45 degrees pair<float, float> leftHeadWingDirection; leftHeadWingDirection.first = cos(rA) * reverseDirection.first - sin(rA) * reverseDirection.second; leftHeadWingDirection.second = sin(rA) * reverseDirection.first + cos(rA) * reverseDirection.second; rA *= -1; // rotate -45 degrees pair<float, float> rightHeadWingDirection; rightHeadWingDirection.first = cos(rA) * reverseDirection.first - sin(rA) * reverseDirection.second; rightHeadWingDirection.second = sin(rA) * reverseDirection.first + cos(rA) * reverseDirection.second; // Calculate the position of the wings leftWing.x = starter.x + leftHeadWingDirection.first * wingLength; leftWing.y = starter.y + leftHeadWingDirection.second * wingLength; rightWing.x = starter.x + rightHeadWingDirection.first * wingLength; rightWing.y = starter.y + rightHeadWingDirection.second * wingLength; } void draw_lines() const; private: int wingLength; Point starter; Point leftWing; Point rightWing; };
void Arrow::draw_lines() const { Line::draw_lines(); fl_line(starter.x, starter.y, leftWing.x, leftWing.y); fl_line(starter.x, starter.y, rightWing.x, rightWing.y); }
It was exciting for me to implement this. I used the vector mathematics and rotation matrix expression for the rotation
First, You have to take the direction where you want to put the arrow head between two points.
Second, you should calculate the reverse direction for calculating head wing direction by rotating. (Don't forget to normalize vectors when you deal with the direction) the reason why you get the reverse direction is that, if you imagine rotating the line to make the arrow head, you will know we need the reverse direction.
Third, Get the two direction rotated by 45 and -45 degrees, using the rotation matrix expression.
Lastly, you can use the wing direction to get the wings' positions. the starter point variable is the position where you put the arrow head. So, the leftWing and the rightWing Points have the positions where you draw line from the arrow head to arrow wing.
4. Define functions n(), s(), e(), w(), center(), ne(), se(), sw(), and nw(). Each takes a Rectangle argument and returns a Point. These functions define "connection points" on and in the rectangle. For example, nw(r) is the northwest (top left) corner of a Rectangle called r.
Point n(const Rectangle& r) { return Point{ r.point(0).x + r.width() / 2, r.point(0).y }; } Point s(const Rectangle& r) { return Point{ r.point(0).x + r.width() / 2, r.point(0).y + r.height }; } Point e(const Rectangle& r) { return Point{ r.point(0).x + r.width(), r.point(0).y + r.height / 2 }; } Point w(const Rectangle& r) { return Point{ r.point(0).x, r.point(0).y + r.height / 2 }; } Point center(const Rectangle& r) { return Point{ r.point(0) + r.width / 2, r.point(0).y + r.height / 2 }; } Point ne(const Rectangle& r) { return Point{ r.point(0).x + r.width, r.point(0).y }; } Point se(const Rectangle& r) { return Point{ r.point(0).x + r.width, r.point(0).y + r.height }; } Point sw(const Rectangle& r) { return Point{ r.point(0).x, r.point(0).y + r.height }; } Point nw(const Rectangle& r) { return r.point(0); }
5. Define the functions from exercise 4 for a Circle and Ellipse. Place the connection points on or outside the shape but not outside the bounding rectangle.
댓글 없음:
댓글 쓰기