4.2. Adding Graphical Objects

The examples to this point have drawn graphic images within windows. The sgl::fill_rectangle function, for example, draws a solid rectangle in a window, but the image has no existence other than its visual appearance on the window's canvas. It works like a picture drawn on a whiteboard; to move the rectangle within a window, the program must erase the window where the rectangle appears and redraw a similar rectangle elsewhere. A programmer cannot modify a rectangle image once it is drawn, as it is not a persistent software object that maintains a certain state and that provides operations that can change its state.

The SGL supports graphical objects which are software objects that can present a visual image within a window. As true software objects, graphical objects can provide methods that allow programmers to move them, resize them, alter their color, etc. Programmers can design graphical objects within an application that allow users to interact with them in ways that simulate manipulating physical objects in the real world.

In order to use graphical objects within a program, programmers must subclass the sgl::ObjectWindow class. An sgl::ObjectWindow instance serves as a container for graphical objects. A programmer then would subclass sgl::GraphicalObject to provide the needed application-specific functionality of the graphical object. The SGL provides the framework that makes it easy for programmers to design graphical objects that the user can move around within the application's window. These graphical objects can receive and process events such as mouse clicks, mouse motion notifications, and key presses. A graphical object can present an image of itself within its containing window, and this image can change as its state changes.

An application adds a graphical object to the window via the sgl::ObjectWindow::add method.

Example 4.2 illustrates some of the capabilities of graphical objects.

Example 4.2. Adding Various Graphical Objects

#include <cmath>
#include <GL/sgl.hpp>
#include <iostream>

//  Some constants to compute the points of a 5-point star
static const double DEGREES_TO_RADIANS = 3.14159/180.0,
                    ANGLE = 360.0/5.0,
                    SIN = sin(ANGLE*DEGREES_TO_RADIANS),
                    COS = cos(ANGLE*DEGREES_TO_RADIANS);

class VariousObjectsWindow: public sgl::ObjectWindow {
protected:
    class Shape: public sgl::GraphicalObject {
    protected:
        sgl::Color color;
    public:
        Shape(double x, double y, double width, double height, sgl::Color color):
              sgl::GraphicalObject(x - width/2.0, y - height/2.0, width, height), color(color) {
            //  Shapes by default use a crosshairs-shaped cursor
            set_cursor(sgl::CursorShape::Crosshair);
        }
        //  Shape is abstract because pure virtual paint method is not overridden
    };

    class CircularShape: public Shape {
    protected:
        //  Controls the user's ability to move the object 
        bool is_locked;
    public:
        CircularShape(double x, double y, double diameter, sgl::Color color):
          Shape(x, y, diameter, diameter, color), is_locked(false) {} 
  
        void paint() const override {
            set_color(color);
            //  Locked circles drawn as outlines
            if (is_locked)
                sgl::draw_circle(left + width/2.0, bottom + height/2.0, width/2.0);
            else
                sgl::fill_circle(left + width/2.0, bottom + height/2.0, width/2.0);
            //  When the mouse is over this circle, draw a ring  around it
            if (mouse_over)
                sgl::draw_circle(left + width/2.0, bottom + height/2.0, width/2.0 + 5.0);
        }

        //  Pressing the 'L' key toggles locking and unlocking
        //  this circle; the user cannot move a locked circle
        void key_pressed(int key, double x, double y) override {
            if (key == 'L' || key == 'l') {
                is_locked = !is_locked;
                Shape::key_pressed(key, x, y);
                window->repaint();
            }
        }

        //  Moves a circle object if it currently is not locked
        void move_to(double x, double y) override {
            if (!is_locked)
                Shape::move_to(x, y);
        }
    };

    class RectangularShape: public Shape {
    public:
        RectangularShape(double x, double y, double width, double height, sgl::Color color):
                            Shape(x, y, width, height, color) {
            //  Rectangle objects use the standard cursor instead of the default crosshairs cursor 
            //  inherited from Shape 
            set_cursor(sgl::CursorShape::Right_arrow);
        } 
  
        void paint() const override {
            sgl::set_color(color);
            //  When the mouse is over a rectangle, the rectangle is rendered as an outline
            if (mouse_over)
                sgl::draw_rectangle(left, bottom, width, height);
            else
                sgl::fill_rectangle(left, bottom, width, height);
        }
    };

    class StarShape: public Shape {
    protected:
        //  Points locating the tips of the star points
        sgl::Point p1, p2, p3, p4, p5;
    public:
        //  The constructor computes the relative locations of the star's tips
        //  Since stars cannot be resized, the relative locations
        //  are based on the star's size.  Since the user can move
        //  a star to a new location within the viewport, the
        //  constructor cannot precompute 
        StarShape(double x, double y, double diameter, sgl::Color color):
                         Shape(x, y, diameter, diameter, color),
                         p1(0.0, diameter/2.0), 
                         p2(p1.x*COS - p1.y*SIN, p1.x*SIN + p1.y*COS),
                         p3(p2.x*COS - p2.y*SIN, p2.x*SIN + p2.y*COS),
                         p4(p3.x*COS - p3.y*SIN, p3.x*SIN + p3.y*COS),
                         p5(p4.x*COS - p4.y*SIN, p4.x*SIN + p4.y*COS) {}

        //  The paint method computes the absolute locations of the star's 
        //  tips based on the star's location
        void paint() const override {
            sgl::set_color(color);
            double x_offset = left + width/2.0,
                   y_offset = bottom + height/2.0;
            sgl::draw_line(p1.x + x_offset, p1.y + y_offset, 
                           p3.x + x_offset, p3.y + y_offset);
            sgl::draw_line(p3.x + x_offset, p3.y + y_offset, 
                           p5.x + x_offset, p5.y + y_offset);
            sgl::draw_line(p5.x + x_offset, p5.y + y_offset, 
                           p2.x + x_offset, p2.y + y_offset);
            sgl::draw_line(p2.x + x_offset, p2.y + y_offset, 
                           p4.x + x_offset, p4.y + y_offset);
            sgl::draw_line(p4.x + x_offset, p4.y + y_offset, 
                           p1.x + x_offset, p1.y + y_offset);

            //  Uncomment for accessory drawing
            //set_color(LIGHT_GRAY);
            //draw_rectangle(left, bottom, width, height);
            //draw_circle(left + width/2.0, bottom + height/2.0, width/2.0);
        }
    };

public:
    VariousObjectsWindow(): sgl::ObjectWindow("Graphical Objects", 100, 100, 800, 600,
                                              0.0, 799.0, 0.0, 599.0) {
        sgl::set_random_seed();  //  Use a seed based on system time
    }

    //  Provide a concrete paint that does nothing; the added shapes paint themselves
    void paint() override {}  
    
    void mouse_pressed(double x, double y, sgl::MouseButton button) {
        
        if (button == sgl::MouseButton::Right) {
            int shape = sgl::random(0, 2),               //  0 = circle, 1 = rectangle, 2 = star
                obj_w = sgl::random(2, get_width()/5),   //  Random width
                obj_h = sgl::random(2, get_height()/5);  //  Random height
            double red = sgl::random(0, 1000)/1000.0,    //  Random color: red component
                   green = sgl::random(0, 1000)/1000.0,  //  green component
                   blue = sgl::random(0, 1000)/1000.0;   //  blue component
            sgl::Color obj_color(red, green, blue);
            if (shape == 0)
                add<CircularShape>(x, y, obj_w, obj_color);
            else if ( shape == 1 )
                add<RectangularShape>(x, y, obj_w, obj_h, obj_color);
            else
                add<StarShape>(x, y, obj_w, obj_color);
        }
        sgl::ObjectWindow::mouse_pressed(x, y, button);
    }
};

int main() {
    sgl::run<VariousObjectsWindow>();
}

Figure 4.3 provides a screenshot of Example 4.2 in action.

Figure 4.3. Object Window Application

Object Window Application

In Example 4.2, users can add various shapes (rectangles, circles, and five-pointed stars) by clicking the right mouse button. The program generates a random instance of one of the three basic shapes. The program determines the size and color of the shape randomly as well. The user can move a shape around within the window by pressing and holding the left mouse button and dragging it with the mouse.

Each shape corresponds to a subclass of sgl::GraphicalObject (indirectly via the application's VariousObjectsWindow::Shape class). Observe how the implementations of these classes allow these shapes to respond differently to user input:

When the mouse cursor is over a circle or star, its shape becomes a cross symbol (sgl::CursorShape::Crosshair). The VariousObjectsWindow::Shape class dictates this behavior. The rectangle shape opts for the standard arrow cursor shape (CursorShape::Right_arrow).


Copyright  ©2019 Richard L. HaltermanVersion 0.9.5February 17, 2019
Creative Commons License This work is licensed under a Creative Commons License.