C++ Windows Programming
上QQ阅读APP看书,第一时间看更新

The circle application

In this section, we look into a simple circle application. As the name implies, it enables the user to handle circles in a graphical application. The user can add a new circle by pressing the left mouse button. The user can also move an existing circle by dragging it. Moreover, the user can change the color of a circle as well as save and open the document:

The main window

As we will see throughout this book, the MainWindow function always does the same thing: it sets the application name and creates the main window of the application. The name is used by the Save and Open standard dialogs, the About menu item, and the registry.

The difference between the main window and other windows of the application is that, when the user closes the main window, the application exits. Moreover, when the user selects the Exit menu item, the main window is closed, and its destructor is called:

MainWindow.cpp

#include "..\\SmallWindows\\SmallWindows.h" 
#include "Circle.h" 
#include "CircleDocument.h" 
 
void MainWindow(vector<String> /* argumentList */, 
                WindowShow windowShow) { 
  Application::ApplicationName() = TEXT("Circle"); 
  Application::MainWindowPtr() = 
    new CircleDocument(windowShow); 
} 

The CircleDocument class

The CircleDocument class extends the Small Windows StandardDocument class, which, in turn, extends the Document and Window classes. In fact, the StandardDocument class constitutes a framework, that is, a base class with a set of virtual methods with functionality that we can override and further specify.

The OnMouseDown and OnMouseUp methods are overridden from the Window class and are called when the user presses or releases one of the mouse buttons. The OnMouseMove method is called when the user moves the mouse. The OnDraw method is also overridden from the Window class and is called every time the window needs to be redrawn.

The ClearDocument, ReadDocumentFromStream, and WriteDocumentToStream methods are overridden from the Standard­Document class and are called when the user creates a new file, opens a file, or saves a file:

CircleDocument.h

class CircleDocument : public StandardDocument { 
  public: 
    CircleDocument(WindowShow windowShow); 
    ~CircleDocument(); 
 
    void OnMouseDown(MouseButton mouseButtons, 
                    Point mousePoint, 
                    bool shiftPressed, 
                    bool controlPressed); 
    void OnMouseUp(MouseButton mouseButtons, 
                    Point mousePoint, 
                    bool shiftPressed, 
                    bool controlPressed); 
    void OnMouseMove(MouseButton mouseButtons, 
                    Point mousePoint, 
                    bool shiftPressed, 
                    bool controlPressed); 
 
    void OnDraw(Graphics& graphics, DrawMode drawMode) const; 
 
    bool ReadDocumentFromStream(String name, 
                           istream& inStream); 
    bool WriteDocumentToStream(String name, 
                           ostream& outStream) const; 
    void ClearDocument(); 

The DEFINE_BOOL_LISTENER and DEFINE_VOID_LISTENER macros define listeners which are methods without parameters that are called when the user selects a menu item. The only difference between the macros is the return type of the defined methods: bool or void.

In the applications of this book, we use the common standard whereby listeners called in response to user actions are prefixed with On, for instance, OnRed, as shown in the following code snippet. The methods that decide whether the menu item should be enabled are suffixed with Enable, and the methods that decide whether the menu item should be marked with a check mark or a radio button are suffixed with Check or Radio.

In the following application, we define menu items for the red, green, and blue colors. We also define a menu item for the color standard dialog:

    DEFINE_VOID_LISTENER(CircleDocument,OnRed); 
    DEFINE_VOID_LISTENER(CircleDocument,OnGreen); 
    DEFINE_VOID_LISTENER(CircleDocument,OnBlue); 
    DEFINE_VOID_LISTENER(CircleDocument,OnColorDialog); 

When the user has chosen one of the colors, red, green, or blue, its corresponding menu item is checked with a radio button. The RedRadio, GreenRadio, and BlueRadio parameters are called before the menu items become visible and return a Boolean value indicating whether the menu item should be marked with a radio button:

    DEFINE_BOOL_LISTENER(CircleDocument, RedRadio); 
    DEFINE_BOOL_LISTENER(CircleDocument, GreenRadio); 
    DEFINE_BOOL_LISTENER(CircleDocument, BlueRadio); 

The circle radius is always 500 units, which corresponds to 5 mm:

    static const int CircleRadius = 500; 

The circleList field holds the circles, where the topmost circle is located at the beginning of the list. The nextColor field holds the color of the next circle to be added by the user. It is initialized to minus 0ne to indicate that no circle is being moved at the beginning. The moveIndex and movePoint fields are used by the OnMouseDown and OnMouseMove methods to keep track of the circle being moved by the user:

  private: 
    vector<Circle> circleList; 
    Color nextColor; 
    int moveIndex = -1; 
    Point movePoint; 
}; 

In the StandardDocument constructor call, the first two parameters are LogicalWithScroll and USLetterPortrait. They indicate that the logical size is hundredths of millimeters and that the client area holds the logical size of a US letter: 215.9*279.4 millimeters (8.5*11 inches). If the window is resized so that the client area becomes smaller than a US letter, scroll bars are added to the window.

The third parameter sets the file information used by the standard save and open dialogs; the text description is set to Circle Files and the file suffix is set to cle. The nullptr parameter indicates that the window does not have a parent window. The OverlappedWindow constant parameter indicates that the window should overlap other windows, and the windowShow parameter is the window's initial appearance passed on from the surrounding system by the MainWindow class:

CircleDocument.cpp

#include "..\\SmallWindows\\SmallWindows.h" 
#include "Circle.h" 
#include "CircleDocument.h" 
 
CircleDocument::CircleDocument(WindowShow windowShow) 
 :StandardDocument(LogicalWithScroll, USLetterPortrait, 
                   TEXT("Circle Files, cle"), nullptr, 
                   OverlappedWindow, windowShow) { 

The StandardDocument class adds the standard File, Edit, and Help menus to the window menu bar. The File menu holds the New, Open, Save, Save As, Page Setup, Print Preview, and Exit items. Page Setup and Print Preview are optional. The seventh parameter of the StandardDocument constructor (the default value is false) indicates their presence. The Edit menu holds the Cut, Copy, Paste, and Delete items. They are disabled by default; we will not use them in this application. The Help menu holds the About item, and the application name set in MainWindow is used to display a message box with a standard message Circle, version 1.0.

We add the standard File and Edit menus to the menu bar. Then we add the Color menu, which is the application-specific menu of this application. Finally, we add the standard Help menu and set the menu bar of the document.

The Color menu holds the menu items used to set the circle colors. The OnRed, OnGreen, and OnBlue methods are called when the user selects the menu item, and the RedRadio, GreenRadio, and BlueRadio methods are called before the user selects the Color menu in order to decide if the items should be marked with a radio button. The OnColorDialog method opens a standard color dialog.

In the &Red\tCtrl+R text in the following code snippet, the ampersand (&) indicates that the menu item has a mnemonic; that is, the letter R will be underlined and it is possible to select the menu item by pressing R after the menu has been opened. The tabulator character (\t) indicates that the second part of the text defines an accelerator; that is, the text Ctrl+R will occur right-justified in the menu item and the item can be selected by pressing Ctrl+R:

  Menu menuBar(this);

The false parameter to StandardFileMenu indicates that we do not want to include the file menu items.

  menuBar.AddMenu(StandardFileMenu(false));

The AddItem method in the Menu class also takes two more parameters for enabling the menu item and setting a checkbox. However, we do not use them in this application. Therefore, we send null pointers:

  Menu colorMenu(this, TEXT("&Color")); 
  colorMenu.AddItem(TEXT("&Red\tCtrl+R"), OnRed, 
                    nullptr, nullptr, RedRadio); 
  colorMenu.AddItem(TEXT("&Green\tCtrl+G"), OnGreen, 
                    nullptr, nullptr, GreenRadio); 
  colorMenu.AddItem(TEXT("&Blue\tCtrl+B"), OnBlue, 
                    nullptr, nullptr, BlueRadio); 
  colorMenu.AddSeparator(); 
  colorMenu.AddItem(TEXT("&Dialog ..."), OnColorDialog); 
  menuBar.AddMenu(colorMenu); 
 
  menuBar.AddMenu(StandardHelpMenu()); 
  SetMenuBar(menuBar); 

Finally, we read the current color (the color of the next circle to be added) from the registry; red is the default color in case there is no color stored in the registry:

  nextColor.ReadColorFromRegistry(TEXT("NextColor"), Red); 
} 

The destructor saves the current color in the registry. In this application, we do not need to perform the destructor's normal tasks such as deallocating memory or closing files:

CircleDocument::~CircleDocument() { 
  nextColor.WriteColorToRegistry(TEXT("NextColor")); 
} 

The ClearDocument method is called when the user selects the New menu item. In this case, we just clear the circle list. Every other action, such as redrawing the window or changing its title, is taken care of by the StandardDocument class:

void CircleDocument::ClearDocument() { 
  circleList.clear(); 
} 

The WriteDocumentToStream method is called by the StandardDocument class when the user saves a file (by selecting Save or Save As). It writes the number of circles (the size of the circle list) to the output stream and calls the WriteCircle method for each circle in order to write their states to the stream:

bool CircleDocument::WriteDocumentToStream(String name, 
                          ostream& outStream) const { 
  int size = circleList.size(); 
  outStream.write((char*) &size, sizeof size); 
 
  for (Circle circle : circleList) { 
    circle.WriteCircle(outStream); 
  } 
 
  return ((bool) outStream); 
} 

The ReadDocumentFromStream method is called by the StandardDocument method when the user opens a file by selecting the Open menu item. It reads the number of circles (the size of the circle list) and for each circle it creates a new object of the Circle class, calls the ReadCircle method in order to read the state of the circle, and adds the circle object to the circleList method:

bool CircleDocument::ReadDocumentFromStream(String name, 
                                 istream& inStream) { 
  int size; 
  inStream.read((char*) &size, sizeof size); 



 
  for (int count = 0; count < size; ++count) { 
    Circle circle; 
    circle.ReadCircle(inStream); 
    circleList.push_back(circle); 
  } 
 
  return ((bool) inStream); 
} 

The OnMouseDown method is called when the user presses one of the mouse buttons. First we need to check that they have pressed the left mouse button. If they have, we loop through the circle list and call the IsClick method for each circle in order to decide whether they have clicked on a circle. Note that the topmost circle is located at the beginning of the list; therefore, we loop from the beginning of the list. If we find a clicked circle, we break the loop.

If the user has clicked on a circle, we store its index moveIndex and the current mouse position in movePoint. Both values are needed by that OnMouseMove method that will be called when the user moves the mouse:

void CircleDocument::OnMouseDown 
           (MouseButton mouseButtons, Point mousePoint, 
            bool shiftPressed /* = false */, 
            bool controlPressed /* = false */) { 
  if (mouseButtons == LeftButton) { 
    moveIndex = -1; 
    int size = circleList.size(); 
 
    for (int index = 0; index < size; ++index) { 
      if (circleList[index].IsClick(mousePoint)) { 
        moveIndex = index; 
        movePoint = mousePoint; 
        break; 
      } 
    } 

However, if the user has not clicked on a circle, we add a new circle. A circle is defined by its center position (mousePoint), radius (CircleRadius), and color (nextColor).

An invalidated area is a part of the client area that needs to be redrawn. Remember that in Windows, we normally do not draw figures directly. Instead, we call the Invalidate method to tell the system that an area needs to be redrawn and force the actual redrawing by calling the UpdateWindow method, which eventually results in a call to the OnDraw method. The invalidated area is always a rectangle. The Invalidate method has a second parameter (the default value is true) indicating that the invalidated area should be cleared.

Technically, it is painted in the window's client color, which in this case is white. In this way, the previous location of the circle is cleared and the circle is drawn at its new location.

The SetDirty method tells the framework that the document has been altered (the document has become dirty), which causes the Save menu item to be enabled and the user to be warned if he/she tries to close the window without saving it:

    if (moveIndex == -1) { 
      Circle newCircle(mousePoint, CircleRadius, 
                       nextColor); 
      circleList.push_back(newCircle); 
      Invalidate(newCircle.Area()); 
      UpdateWindow(); 
      SetDirty(true); 
    } 
  } 
} 

The OnMouseMove method is called every time the user moves the mouse with at least one mouse button pressed. We first need to check whether the user is pressing the left mouse button and is clicking on a circle (whether the moveIndex method does not equal -1). If the user is, we calculate the distance from the previous mouse event (OnMouseDown or OnMouseMove) by comparing the previous and the current mouse position using the mousePoint method. We update the circle position, invalidate both the old and new area, forcing a redrawing of the invalidated areas with the UpdateWindow method, and set the dirty flag:

void CircleDocument::OnMouseMove 
           (MouseButton mouseButtons, Point mousePoint, 
            bool shiftPressed /* = false */, 
            bool controlPressed /* = false */) { 
  if ((mouseButtons == LeftButton)&&(moveIndex != -1)) { 
    Size distanceSize = mousePoint - movePoint; 
    movePoint = mousePoint; 
 
    Circle& movedCircle = circleList[moveIndex]; 
    Invalidate(movedCircle.Area()); 
    movedCircle.Center() += distanceSize; 
    Invalidate(movedCircle.Area()); 
 
    UpdateWindow(); 
    SetDirty(true); 
  } 
} 

Strictly speaking, the OnMouseUp method could be excluded since the moveIndex method is set to minus one in the OnMouseDown method, which is always called before the OnMouseMove method. However, it has been included for the sake of completeness:

void CircleDocument::OnMouseUp 
           (MouseButton mouseButtons, Point mousePoint, 
            bool shiftPressed /* = false */, 
            bool controlPressed /* = false */) { 
  moveIndex = -1; 
} 

The OnDraw method is called every time the window needs to be (partly or completely) redrawn. The call can be initialized by the system as a response to an event (for instance, the window has been resized) or by an earlier call to the UpdateWindow method. The Graphics reference parameter has been created by the framework and can be considered as a toolbox for drawing lines, painting areas, and writing text. However, in this application, we do not write text.

We iterate through the circle list and, for each circle, call the Draw method. Note that we do not care about which circles are to be physically redrawn. We simple redraw all circles. However, only the circles located in an area that has been invalidated by a previous call to the Invalidate method will be physically redrawn.

The Draw method has a second parameter indicating the draw mode, which can be Paint or Print. The Paint method indicates that the OnDraw method is called by the OnPaint method in the Window class and that the painting is performed in the window's client area. The Print method indicates that the OnDraw method is called by the OnPrint method and that the painting is sent to a printer. However, in this application, we do not use that parameter:

void CircleDocument::OnDraw(Graphics& graphics, 
                            DrawMode /* drawMode */) const { 
  for (Circle circle : circleList) { 
    circle.Draw(graphics); 
  } 
} 

The RedRadio, GreenRadio, and BlueRadio methods are called before the menu items are shown, and the items will be marked with a radio button if they return true. The Red, Green, and Blue constants are defined in the Color class:

bool CircleDocument::RedRadio() const { 
  return (nextColor == Red); 
} 
 
bool CircleDocument::GreenRadio() const { 
  return (nextColor == Green); 
} 
 
bool CircleDocument::BlueRadio() const { 
  return (nextColor == Blue); 
} 

The OnRed, OnGreen, and OnBlue methods are called when the user selects the corresponding menu item. They all set the nextColor field to an appropriate value:

void CircleDocument::OnRed() { 
  nextColor = Red; 
} 
 
void CircleDocument::OnGreen() { 
  nextColor = Green; 
} 
 
void CircleDocument::OnBlue() { 
  nextColor = Blue; 
} 

The OnColorDialog method is called when the user selects the Color dialog menu item and displays the standard color dialog. If the user chooses a new color, the nextcolor method will be given the chosen color value:

void CircleDocument::OnColorDialog() { 
  StandardDialog(this, nextColor); 
} 

The Circle class

Circle is a class holding the information about a single circle. The default constructor is used when reading a circle from a file. The second constructor is used when creating a new circle. The IsClick method returns true if the given point is located inside the circle (to check whether the user has clicked in the circle), the Area method returns the circle's surrounding rectangle (for invalidation), and the Draw method is called to redraw the circle:

Circle.h

class Circle { 
  public: 
    Circle(); 
    Circle(Point center, int radius, Color color); 
 
    bool WriteCircle(ostream& outStream) const; 
    bool ReadCircle(istream& inStream); 
 
    bool IsClick(Point point) const; 
    Rect Area() const; 
    void Draw(Graphics& graphics) const; 
 
    Point Center() const {return center;} 
    Point& Center() {return center;} 
    Color GetColor() {return color;} 

As mentioned in the previous section, a circle is defined by its center position (center), radius (radius), and color (color):

  private: 
    Point center; 
    int radius; 
    Color color; 
}; 

The default constructor does not need to initialize the fields since it is called when the user opens a file and the values are read from the file. The second constructor, however, initializes the center point, radius, and color of the circle:

Circle.cpp

#include "..\\SmallWindows\\SmallWindows.h" 
#include "Circle.h" 
 
Circle::Circle() { 
  // Empty. 
} 
 
Circle::Circle(Point center, int radius, Color color) 
 :color(color), 
  center(center), 
  radius(radius) { 
  // Empty. 
} 

The WriteCircle method writes the color, center point, and radius to the stream. Since radius is a regular integer, we simply use the C standard function write, while Color and Point have their own methods to write their values to a stream. In the ReadCircle method, we read the color, center point, and radius from the stream in a similar manner:

bool Circle::WriteCircle(ostream& outStream) const { 
  color.WriteColorToStream(outStream); 
  center.WritePointToStream(outStream); 
  outStream.write((char*) &radius, sizeof radius); 
  return ((bool) outStream); 
} 
 
bool Circle::ReadCircle(istream& inStream) { 
  color.ReadColorFromStream(inStream); 
  center.ReadPointFromStream(inStream); 
  inStream.read((char*) &radius, sizeof radius); 
  return ((bool) inStream); 
} 

The IsClick method uses Pythagoras' theorem to calculate the distance between the given point and the circle's center point and returns true if the point is located inside the circle (if the distance is less than or equal to the circle radius):

 Circle::IsClick(Point point) const { 
  int width = point.X() - center.X(), 
      height = point.Y() - center.Y(); 
  int distance = (int) sqrt((width * width) + 
                            (height * height)); 
  return (distance <= radius); 
} 

The top-left corner of the resulting rectangle is the center point minus the radius and the bottom-right corner is the center point plus the radius:

Rect Circle::Area() const { 
  Point topLeft = center - radius, 
        bottomRight = center + radius; 
  return Rect(topLeft, bottomRight); 
} 

We use the FillEllipse method (there is no FillCircle method) of the Small Windows Graphics class to draw the circle. The circle's border is always black, while its interior color is given by the color field:

void Circle::Draw(Graphics& graphics) const { 
  Point topLeft = center - radius, 
        bottomRight = center + radius; 
  Rect circleRect(topLeft, bottomRight); 
  graphics.FillEllipse(circleRect, Black, color); 
}