
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 StandardDocument
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); }