In this article by Stefan Björnander, the author of the book C++ Windows Programming, we will see how to create Windows applications using C++. This article introduces Small Windows by presenting two small applications:
(For more resources related to this topic, see here.)
In The C Programming Language by Brian Kernighan and Dennis Richie, the hello-world example was introduced. It was a small program that wrote hello, world on the screen. In this section, we shall write a similar program for Small Windows.
In regular C++, the execution of the application starts with the main function. In Small Windows, however, main is hidden in the framework and has been replaced by MainWindow, which task is to define the application name and create the main window object. The argumentList parameter corresponds to argc and argv in main. The commandShow parameter forwards the system's request regarding the window's appearance.
MainWindow.cpp
#include "..\SmallWindows\SmallWindows.h"
#include "HelloWindow.h"
void MainWindow(vector<String> /* argumentList */,
WindowShow windowShow) {
Application::ApplicationName() = TEXT("Hello");
Application::MainWindowPtr() =
new HelloWindow(windowShow);
}
In C++, there are to two character types: char and wchar_t, where char holds a regular character of one byte and wchar_t holds a wide character of larger size, usually two bytes. There is also the string class that holds a string of char values and the wstring class that holds a string of wchar_t values.
However, in Windows there is also the generic character type TCHAR that is char or wchar_t, depending on system settings. There is also the String class holds a string of TCHAR values. Moreover, TEXT is a macro that translates a character value to TCHAR and a text value to an array of TCHAR values.
To sum it up, following is a table with the character types and string classes:
Regular character |
Wide character |
Generic character |
char |
wchar_t |
TCHAR |
string |
wstring |
String |
In the applications of this book, we always use the TCHAR type, the String class, and the TEXT macro. The only exception to that rule is the clipboard handling.
Our version of the hello-world program writes Hello, Small Windows! in the center of the client area. The client area of the window is the part of the window where it is possible to draw graphical objects. In the following window, the client area is the white area.
The HelloWindow class extends the Small Windows Window class. It holds a constructor and the Draw method. The constructor calls the Window constructor with suitable information regarding the appearance of the window. Draw is called every time the client area of the window needs to be redrawn.
HelloWindow.h
class HelloWindow : public Window {
public:
HelloWindow(WindowShow windowShow);
void OnDraw(Graphics& graphics, DrawMode drawMode);
};
The constructor of HelloWindow calls the constructor of Window with the following parameter:
HelloWindow.cpp
#include "..\SmallWindows\SmallWindows.h"
#include "HelloWindow.h"
HelloWindow::HelloWindow(WindowShow windowShow)
:Window(LogicalWithScroll, ZeroSize, nullptr,
OverlappedWindow, NoStyle, windowShow) {
SetHeader(TEXT("Hello Window"));
}
The OnDraw method is called every time the client area of the window needs to be redrawn. It obtains the size of the client area and draws the text in its center with black text on white background. The SystemFont parameter will make the text appear in the default system font.
The Small Windows Color class holds the constants Black and White. Point holds a 2-dimensional point. Size holds a width and a height. The Rect class holds a rectangle. More specifically, it holds the four corners of a rectangle.
void HelloWindow::OnDraw(Graphics& graphics,
DrawMode /* drawMode */) {
Size clientSize = GetClientSize();
Rect clientRect(Point(0, 0), clientSize);
Font textFont("New Times Roman", 12, true);
graphics.DrawText(clientRect, TEXT("Hello, Small Windows!"),
textFont , Black, White);
}
In this section, we look into a simple circle application. As the name implies, it provides the user the possibility to handle circles in a graphical application. The user can add a new circle by clicking the left mouse button. They 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.
As we will see thought out this book, MainWindow does always do 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 CircleDocumentclass extends the Small Windows class StandardDocument, which in turn extends Document and Window. In fact, StandardDocument constitutes of a framework; that is, a base class with a set of virtual methods with functionality we can override and further specify.
The OnMouseDown and OnMouseUp methods are overridden from Window and are called when the user presses or releases one of the mouse buttons. OnMouseMove is called when the user moves the mouse. The OnDraw method is also overridden from Window and is called every time the window needs to be redrawn.
The ClearDocument, ReadDocumentFromStream, and WriteDocumentToStream methods are overridden from StandardDocument 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);
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: 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 that the listeners called in response to user actions are prefixed with On, for instance OnRed. The methods that decide whether the menu item shall be enabled are suffixed with Enable, and the methods that decide whether the menu item shall be marked with a check mark or a radio button are suffixed with Check or Radio.
In this 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 color red, green, or blue, its corresponding menu item shall be checked with a radio button. RedRadio, GreenRadio, and BlueRadio are called before the menu items become visible and return a Boolean value indicating whether the menu item shall 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 correspond to 5 millimeters.
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 one to indicate that no circle is being moved at the beginning. The moveIndex and movePoint fields are used by OnMouseDown and OnMouseMove 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 null pointer parameter indicates that the window does not have a parent window. The OverlappedWindow constant parameter indicates that the window shall overlap other windows and the windowShow parameter is the window's initial appearance passed on from the surrounding system by MainWindow.
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 framework 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. The Page Setup and Print Preview items are optional. The seventh parameter of the StandardDocument constructor (default 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, 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, BlueRadio are called before the user selects the color menu in order to decide if the items shall be marked with a radio button. OnColorDialog opens a standard color dialog.
In the text &RedtCtrl+R, 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);
menuBar.AddMenu(StandardFileMenu(false));
The AddItem method in the Menu class also takes two more parameters for enabling the menu item and setting a check box. However, we do not use them in this application. Therefore, we send null pointers.
Menu colorMenu(this, TEXT("&Color"));
colorMenu.AddItem(TEXT("&RedtCtrl+R"), OnRed,
nullptr, nullptr, RedRadio);
colorMenu.AddItem(TEXT("&GreentCtrl+G"), OnGreen,
nullptr, nullptr, GreenRadio);
colorMenu.AddItem(TEXT("&BluetCtrl+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 deallocate 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 StandardDocument.
void CircleDocument::ClearDocument() {
circleList.clear();
}
The WriteDocumentToStream method is called by StandardDocument 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 WriteCircle 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 StandardDocument 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 ReadCircle in order to read the state of the circle, and adds the circle object to circleList.
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 IsClick for each circle in order to decide whether they have clicked at a circle. Note that the top-most 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 at a circle, we store its index moveIndex and the current mouse position in movePoint. Both values are needed by 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 at 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 Invalidate to tell the system that an area needs to be redrawn and forces the actually redrawing by calling UpdateWindow, which eventually results in a call to OnDraw. The invalidated area is always a rectangle. Invalidate has a second parameter (default true) indicating that the invalidated area shall 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 becomes 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 they try 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 at a circle (whether moveIndex does not equal minus one). If they have, we calculate the distance from the previous mouse event (OnMouseDown or OnMouseMove) by comparing the previous mouse position movePoint by the current mouse position mousePoint. We update the circle position, invalidate both the old and new area, forcing a redrawing of the invalidated areas with UpdateWindow, 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, OnMouseUp could be excluded since moveIndex is set to minus one in OnMouseDown, which is always called before OnMouseMove. 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 have been initialized by the system as a response to an event (for instance, the window has been resized) or by an earlier call to UpdateWindow. The Graphics reference parameter has been created by the framework and can be considered a toolbox for drawing lines, painting areas and writing text. However, in this application we do not write text.
We iterate throw 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 Invalidate will be physically redrawn.
The Draw method has a second parameter indicating the draw mode, which can be Paint or Print. Paint indicates that OnDraw is called by OnPaint in Window and that the painting is performed in the windows' client area. The Print method indicates that OnDraw is called by OnPrint 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 */) {
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 in case 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 choses a new color, nextcolor will be given the chosen color value.
void CircleDocument::OnColorDialog() {
ColorDialog(this, nextColor);
}
The Circle class 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), Area returns the circle's surrounding rectangle (for invalidating), and Draw 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 the 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 ReadCircle 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 the Pythagoras theorem to calculate the distance between the given point and the circle's center point, and return 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);
}
In this article, we have looked into two applications in Small Windows: a simple hello-world application and a slightly more advance circle application, which has introduced the framework. We have looked into menus, circle drawing, and mouse handling.
Further resources on this subject: