- C++ Windows Programming
- Stefan Bj?rnander
- 5851字
- 2021-07-14 10:03:20
The DrawDocument class
The DrawDocument
class extends the StandardDocument
framework, similar to the circle application. It catches the mouse events, overrides the file methods, implements cut, copy, and paste, as well as cursor handling:
DrawDocument.h
class DrawDocument : public StandardDocument { public: DrawDocument(WindowShow windowShow); ~DrawDocument();
Similar to the circle application, we catch mouse action with the OnMouseDown
, OnMouseMove
, and OnMouseUp
methods. However, in this application, we also catch double-clicks with the OnDoubleClick
method. When the user double-clicks on a figure, it takes individual actions:
void OnMouseDown(MouseButton mouseButtons, Point mousePoint, bool shiftPressed, bool controlPressed); void OnMouseMove(MouseButton mouseButtons, Point mousePoint, bool shiftPressed, bool controlPressed); void OnDoubleClick(MouseButton mouseButtons, Point mousePoint, bool shiftPressed, bool controlPressed); void OnMouseUp(MouseButton mouseButtons, Point mousePoint, bool shiftPressed, bool controlPressed);
The OnDraw
method is called when the window's client area needs to be redrawn. It draws the figures, and the rectangle enclosing the figures, if the user is in the process of marking figures with a rectangle:
void OnDraw(Graphics& graphics, DrawMode drawMode) const;
The ClearDocument
method is called when the user selects the New menu item, the ReadDocumentFromStream
method is called when they select the Open menu item, and the WriteDocumentToStream
method is called when they select the Save or Save As menu item:
void ClearDocument(); bool WriteDocumentToStream(String name, ostream& outstream) const;
Each figure has an integer identity value that is written by the WriteDocumentToStream
method and read by the ReadDocumentFromStream
method to decide which figure has to be created. Given the identity value, the CreateFigure
method creates the new figure:
bool ReadDocumentFromStream(String name, istream& instream); DrawFigure* CreateFigure(FigureId figureId) const;
In this application, we introduce functionality for cut, copy, and paste. The CopyGeneric
method is called when the user selects the Cut or Copy menu item in the Edit menu and the PasteGeneric
method is called when the user selects the Paste menu item. In the StandardDocument
framework, there are methods for cutting, copying, and pasting ASCII and Unicode text as well. However, we do not use them in this application:
bool IsCopyGenericReady(int /* format */) const; void CopyGeneric(int format, InfoList& infoList) const; void PasteGeneric(int format, InfoList& infoList);
The CopyEnable
method returns true
if information is ready to be copied. In that case, the Cut, Copy, and Delete menu items are enabled. In this application, we do not override the PasteEnable
method, since the StandardDocument
framework looks up whether there is a memory buffer in the global clipboard suitable to paste. The OnDelete
method is called when the user selects the Delete menu item:
bool CopyEnable() const; void OnDelete();
Similar to the circle application, we have a set of listeners, even though the set is larger in this case. Each listener is added to the menus in the constructor. Unlike the circle application, we also use enable methods: methods that are called before the menu item becomes visible. If the methods return false
, the menu items become disabled and grayed. If the menu item is connected to an accelerator, the accelerator also becomes disabled. We place the Modify, Color, and Fill items in the Modify menu, and the Line, Arrow, Rectangle, and Ellipse items in the Add menu:
DEFINE_BOOL_LISTENER(DrawDocument, ModifyEnable) DEFINE_BOOL_LISTENER(DrawDocument, ModifyRadio) DEFINE_VOID_LISTENER(DrawDocument, OnModify) DEFINE_BOOL_LISTENER(DrawDocument, ColorEnable) DEFINE_VOID_LISTENER(DrawDocument, OnColor) DEFINE_BOOL_LISTENER(DrawDocument, FillEnable) DEFINE_BOOL_LISTENER(DrawDocument, FillCheck) DEFINE_VOID_LISTENER(DrawDocument, OnFill) DEFINE_BOOL_LISTENER(DrawDocument, LineEnable); DEFINE_BOOL_LISTENER(DrawDocument, ArrowEnable); DEFINE_BOOL_LISTENER(DrawDocument, RectangleEnable); DEFINE_BOOL_LISTENER(DrawDocument, EllipseEnable); DEFINE_BOOL_LISTENER(DrawDocument, LineRadio); DEFINE_BOOL_LISTENER(DrawDocument, ArrowRadio); DEFINE_BOOL_LISTENER(DrawDocument, RectangleRadio); DEFINE_BOOL_LISTENER(DrawDocument, EllipseRadio); DEFINE_VOID_LISTENER(DrawDocument, OnLine); DEFINE_VOID_LISTENER(DrawDocument, OnArrow); DEFINE_VOID_LISTENER(DrawDocument, OnRectangle); DEFINE_VOID_LISTENER(DrawDocument, OnEllipse);
In this application, we also introduce cursor control. The UpdateCursor
method sets the cursor to an appropriate appearance depending on whether the user is creating, modifying, or moving figures:
void UpdateCursor();
One central point of this application is its mode: the applicationMode
method keeps track of the actions when the user presses the left mouse button. It holds the following modes:
Idle
: The application waits for input from the user. This is always the mode as long as the user does not press the left mouse button. However, when the user presses the mouse button, until they release it, theapplicationMode
method holds one value. The user presses the Ctrl key and clicks on an already marked figure. The figure becomes unmarked, nothing more happens.ModifySingle
: The user grabs one single figure that is being modified (if the user clicks on one of its endpoints) or moved (if the user clicks on any other part of the figure).ModifyRectangle
: The user has clicked on the client area without hitting a figure, resulting in a rectangle being drawn. When the user releases the mouse button, every figure completely enclosed by the rectangle is marked.MoveMultiple
: The user presses the Ctrl key and clicks on an unmarked figure. It is not possible to modify more than one figure at the same time.
Note that the applicationMode
method is relevant only as long as the user presses the left mouse button. As soon as they release the mouse button, the applicationMode
method is always Idle
:
private: enum ApplicationMode {Idle, ModifySingle, MoveMultiple, ModifyRectangle}; ApplicationMode applicationMode = Idle;
When the applicationMode
method holds the Idle
mode, the application waits for further input from the user. The actionMode
field defines the next action, which may hold the following values:
Modify
: When the user presses the mouse button, theapplicationMode
method is set to theModifySingle
mode if they click on a figure, theMoveMultiple
mode if they click on an unmarked figure while pressing the Ctrl key, theIdle
mode if the figure is already marked, or theModifyRectangle
mode if they click on the client area without hitting a figure.Add
: When the user presses the left mouse button, a new figure is created at the location, regardless of whether there already is a figure at the location. The value of theaddFigureId
method decides which kind of figure should be added; it can hold any of the valuesLineId
,ArrowId
,RectangleId
, orEllipseId
.
enum ActionMode {Modify, Add}; ActionMode actionMode = Add; FigureId addFigureId = LineId;
Later in the chapter, we will encounter expressions such as in Modify mode and in Add mode, which refer to the value of the actionMode
variable: Modify
or Add
.
The nextColor
and nextFill
fields hold the figure's color and fill status (in the case of a rectangle or ellipse), respectively, of the next figure to be added:
Color nextColor; bool nextFill;
Similar to the circle application, when the user adds or modifies a figure, we need to store the previous mouse position in the prevMousePoint
method in order to keep track of the distance the mouse has been moved since the last mouse action:
Point prevMousePoint;
When the applicationMode
method holds the ModifySingle
value, the figure being modified is always placed at the beginning of the figure pointer list (figurePtrList[0]
) in order for it to appear on top of the figures. When the applicationMode
method holds the ModifyRectangle
mode, the insideRectangle
method keeps track of the rectangle enclosing the figures:
Rect insideRectangle;
The static DrawFormat
constant is used to identify data to be cut, copied, or pasted in the global clipboard. It is arbitrarily set to 1000:
static const unsigned int DrawFormat = 1000;
As the user adds and removes figures from the drawing, the figures are dynamically created and deleted; their addresses are stored in the figurePtrList
list. The DynamicList
class is a Small Windows class that is a more advanced version of the C++ standard classes list
and vector
.
The values of the figure list are pointers to the DrawFigure
class, which is the root class of the figure hierarchy used in this application (described in Chapter 5, The Figure Hierarchy). Unlike the circle and Tetris applications in the previous chapters, we do not store the figure objects directly in the list, but rather their pointers. This is necessary, since we use class hierarchy holds with pure virtual methods, which makes the DrawWindow
class abstract and not possible to store directly in the list. It is also necessary in order to take advantage of dynamic binding of the class hierarchy:
DynamicList<DrawFigure*> figurePtrList; };
The application modes
This section holds a further description of the applicationMode
field. It is closely connected to the mouse input cycle. When the user is not pressing the left mouse button, the applicationMode
method is always in the Idle
mode. When the user presses the left mouse button in modify mode, they can choose to press the Ctrl key at the same time:
- If they do not press the Ctrl key, the
applicationMode
method is set to theModifySingle
mode if they hit a figure. That figure becomes marked and other figures become unmarked. - If they do press the Ctrl key, the
applicationMode
method is set to theMoveMultiple
mode if they hit a figure that is not marked and to theIdle
mode if it is marked. The figure becomes marked if it is unmarked and unmarked if it is marked. The rest of the figures are unaffected. - If they do not hit a figure, the
applicationMode
method is set to theModifyRectangle
mode regardless of whether they pressed the Ctrl key and the inside rectangle (insideRectangle
) is being initialized. All figures become unmarked. All figures that are completely enclosed by the rectangle when the user releases the left button are marked.
When the user moves the mouse with the left button pressed in modify mode, there are four possible values of the applicationMode
method to consider:
Idle
: We do nothing.ModifySingle
: We call theModify
method on the single figure. This may result in the single hit figure being modified or moved, depending on where the user hit the figure.MoveMultiple
: We call theMove
method on all marked figures. This always results in the marked figures being moved, not modified.ModifyRectangle
: We modify the inside rectangle.
Finally, when the user releases the left mouse button, we again look into the four modes of the applicationMode
method:
Idle
,ModifySingle
, orMoveMultiple
: We do nothing since everything has already been done when the user moved the mouse. The marked figures have been moved or modified.ModifyRectangle
: We mark all figures completely enclosed by the rectangle.
The DynamicList class
In this chapter, we use a subset of the methods of the auxiliary DynamicList
class. It holds a set of methods that take callback functions, that is, functions that are sent as parameters to methods and called by the methods:
template <class Type> class DynamicList { public:
IfFuncPtr
and DoFuncPtr
are pointers to callback functions. The difference between them is that the IfFuncPtr
pointer is intended for methods that only inspect the values of the list. Therefore, the value
parameter is constant. The DoFuncPtr
pointer is intended for methods that modify the values. Consequently, the value
parameter is not constant:
typedef bool (*IfFuncPtr) (const Type& value, void* voidPtr); typedef void (*DoFuncPtr) (Type& value, void* voidPtr);
The AnyOf
method takes the ifFuncPtr
pointer and applies it to each value of the array. The methods return true
if at least one of the values satisfies the ifFunctPtr
pointer (if the ifFuncPtr
pointer returns true
for the value). The ifVoidPtr
parameter is sent as the second parameter to the ifFuncPtr
pointer:
bool AnyOf(IfFuncPtr ifFuncPtr, void* ifVoidPtr = nullptr) const;
The FirstOf
method also returns true
if at least one value satisfies the ifFuncPtr
pointer. In that case, the first satisfied value is copied to the value
parameter:
bool FirstOf(IfFuncPtr ifFuncPtr,Type& value, void* ifVoidPtr = nullptr) const;
The Apply
method calls the doFunctPtr
pointer to every value of the list. The ApplyIf
method calls the doFuncPtr
pointer to all values that satisfy the ifFuncPtr
pointer:
void Apply(DoFuncPtr doFuncPtr, void* ifVoidPtr = nullptr); void ApplyIf(IfFuncPtr ifFuncPtr, DoFuncPtr doFuncPtr, void* ifVoidPtr = nullptr, void* doVoidPtr = nullptr);
The CopyIf
method copies the values satisfying the ifFuncPtr
pointer into the copyArray
method. The RemoveIf
method removes every value satisfying the ifFuncPtr
pointer:
void CopyIf(IfFuncPtr ifFuncPtr, DynamicList& copyArray, void* ifVoidPtr = nullptr) const; void RemoveIf(IfFuncPtr ifFuncPtr, void* ifVoidPtr = nullptr);
The ApplyRemoveIf
method calls the doFuncPtr
pointer and then removes every value satisfying the ifFuncPtr
pointer, which comes in handy when we want to deallocate and remove pointers from the list:
void ApplyRemoveIf(IfFuncPtr ifFuncPtr, DoFuncPtr doFuncPtr, void* ifVoidPtr = nullptr, void* doVoidPtr=nullptr); };
Initialization
The constructor of the DrawDocument
class is similar to the constructor of the CircleDocument
class. We use the LogicalWithScroll
coordinate system with US letter size. The file description Draw Files
and the suffix drw
are used to filter drawing files in the open and save dialogs. The null pointer indicates that the document does not have a parent window, and the false
parameter indicates that the Print and Print Preview items in the File menu are omitted. Finally, the initiation lists holding the DrawFormat
parameter indicates the format used to identify data to be copied and pasted. In this case, we use the same format for both copying and pasting:
DrawDocument.cpp
#include "..\\SmallWindows\\SmallWindows.h" #include "DrawFigure.h" #include "LineFigure.h" #include "ArrowFigure.h" #include "RectangleFigure.h" #include "EllipseFigure.h" #include "TextFigure.h" #include "DrawDocument.h" DrawDocument::DrawDocument(WindowShow windowShow) :StandardDocument(LogicalWithScroll, USLetterPortrait, TEXT("Draw Files, drw"), nullptr, OverlappedWindow, windowShow, {DrawFormat}, {DrawFormat}) {
Since we extend the StandardDocument
framework, the window has a standard menu bar with the File menu holding New, Open, Save, Save As, and Exit (the Print and Print Preview items are omitted due to the false
parameter in the constructor call) items, the Edit menu holding Cut, Copy, Paste, and Delete, and the Help items, and About.
We also add two application-specific menus: Format and Add. The Format menu holds the menu items Modify, Color, and Fill. Similar to the circle application, we mark the menu items with mnemonics and accelerators. However, we also use the enable parameters; the ModifyEnable
, ColorEnable
, and FillEnable
methods are called before the menu items become visible. If they return false
, the menu item is disabled and grayed:
Menu menuBar(this); menuBar.AddMenu(StandardFileMenu(false)); menuBar.AddMenu(StandardEditMenu()); Menu formatMenu(this, TEXT("F&ormat")); formatMenu.AddItem(TEXT("&Modify\tCtrl+M"), OnModify, ModifyEnable, nullptr, ModifyRadio); formatMenu.AddItem(TEXT("&Color\tAlt+C"), OnColor, ColorEnable); formatMenu.AddItem(TEXT("F&ill\tCtrl+I"), OnFill, FillEnable FillCheck, nullptr); menuBar.AddMenu(formatMenu);
The Add menu holds one item for each kind of figure to be added:
Menu addMenu(this, TEXT("&Add")); addMenu.AddItem(TEXT("&Line\tCtrl+L"), OnLine, LineEnable, nullptr, LineRadio); addMenu.AddItem(TEXT("&Arrow\tAlt+A"), OnArrow, ArrowEnable, nullptr, ArrowRadio); addMenu.AddItem(TEXT("&Rectangle\tCtrl+R"), OnRectangle, RectangleEnable, nullptr, RectangleRadio); addMenu.AddItem(TEXT("&Ellipse\tCtrl+E"), OnEllipse, EllipseEnable, nullptr, EllipseRadio); menuBar.AddMenu(addMenu); menuBar.AddMenu(StandardHelpMenu()); SetMenuBar(menuBar);
Finally, we read values from the Windows Registry, which is a database in the Windows system that we can use to store values between the executions of our applications. The Small Windows auxiliary classes Color
, Font
, Point
, Size
, and Rect
have their own registry methods. The Small Windows Registry
class holds static methods for reading and writing text as well as numerical and integer values:
actionMode = (ActionMode) Registry::ReadInteger(TEXT("actionMode"), Modify); addFigureId = (FigureId) Registry::ReadInteger(TEXT("addFigureId"), LineId); nextColor.ReadColorFromRegistry(TEXT("nextColor")); nextFill = Registry::ReadBoolean(TEXT("nextFill"), false); }
The destructor writes the values to the registry. In this application, it is not necessary to provide any common destructor actions such as deallocating memory or closing files:
DrawDocument::~DrawDocument() { Registry::WriteInteger(TEXT("actionMode"), actionMode); Registry::WriteInteger(TEXT("addFigureId "), addFigureId); nextColor.WriteColorToRegistry(TEXT("nextColor")); Registry::WriteBoolean(TEXT("nextFill"), nextFill); }
Mouse input
IsFigureMarked
, IsFigureClicked
, and UnmarkFigure
are callback functions that are called by the DynamicList
methods AnyOf
, FirstOf
, CopyIf
, ApplyIf
, and ApplyRemoveIf
. These methods take the pointer to a figure and an optional void pointer that holds additional information.
The IsFigureMarked
function returns true
if the figure is marked, the IsFigureClicked
function returns true
if the mouse point given in the voidPtr
pointer hits the figure, and the IsFigureClicked
function unmarks the figure if it is marked. As you can see, the IsFigureMarked
function is defined as a lambda function, while the IsFigureClicked
function is defined as a regular function.
There is no rational reason for this, other than that I would like to demonstrate both ways to define functions:
auto IsFigureMarked = [](DrawFigure* const& figurePtr, void* /* voidPtr */) { return figurePtr->IsMarked(); }; bool IsFigureClicked(DrawFigure* const& figurePtr, void* voidPtr) { Point* mousePointPtr = (Point*) voidPtr; return figurePtr->IsClick(*mousePointPtr); } void UnmarkFigure(DrawFigure*& figurePtr, void* /* voidPtr */) { if (figurePtr->IsMarked()) { figurePtr->Mark(false); } }
In the OnMouseDown
method, we first check that the user presses the left mouse button. If so, we save the mouse position in the prevMousePoint
field so that we can calculate the distance the figure has moved in subsequent calls to the OnMouseMove
method:
void DrawDocument::OnMouseDown(MouseButton mouseButtons, Point mousePoint, bool shiftPressed, bool controlPressed) { if (mouseButtons == LeftButton) { prevMousePoint = mousePoint;
As mentioned earlier, the mouse click will result in different actions depending on the value of the actionMode
method. In case of the Modify
method, we call the FirstOf
parameter on the figure pointer list to extract the first clicked figure. The figures can overlap, and the click may hit more than one figure. In that case, we want the topmost figure located at the beginning of the list. The FirstOf
method returns true
if there is at least one clicked figure, which is copied into the topClickedFigurePtr
reference parameter. The address of the mousePoint
method is given as the second parameter to the FirstOf
method and is, in turn, given to the IsFigureClicked
function as its second parameter:
switch (actionMode) { case Modify: { DrawFigure* topClickedFigurePtr; if (figurePtrList.FirstOf(IsFigureClicked, topClickedFigurePtr, &mousePoint)) {
We have two cases to consider, depending on whether the user presses the Ctrl key. If they do so, the figure will be marked if it is unmarked and vice versa, and other marked figures will remain marked.
However, in the other case, when the user does not press the Ctrl key, the figure becomes marked regardless of whether it is already marked, all other marked figures become unmarked, and the application is set to the ModifySingle
mode. The figures are removed from the list and inserted at the beginning (front) in order to appear on top of the drawing:
if (!controlPressed) { figurePtrList.ApplyIf(IsFigureMarked, UnmarkFigure); topClickedFigurePtr->Mark(true); applicationMode = ModifySingle; int topFigureIndex = figurePtrList.IndexOf(topClickedFigurePtr); figurePtrList.Erase(topFigureIndex); figurePtrList.PushFront(topClickedFigurePtr); }
If the user presses the Ctrl key, we have another two cases. If the clicked figure is already marked, we unmark it and set the applicationMode
method to the Idle
mode. If the clicked figure is not already marked, we mark it and set the applicationMode
method to the MoveMultiple
mode. In this way, we have at least one marked figure to be moved in the OnMouseMove
method when the user moves the mouse. Note that if the user presses the Ctrl key, one or several figures can be moved but not modified. It would be illogical to modify more than one figure at the same time:
else { if (topClickedFigurePtr->IsMarked()) { applicationMode = Idle; topClickedFigurePtr->Mark(false); } else { applicationMode = MoveMultiple; topClickedFigurePtr->Mark(true); } } }
If the user hits a point where no figure is located (the figurePtrList.FirstOf
method returns false
), we unmark all marked figures, initialize the insideRectangle
method, and set the applicationMode
method to the ModifyRectangle
mode.
else { figurePtrList.ApplyIf(IsFigureMarked, UnmarkFigure); insideRectangle = Rect(mousePoint, mousePoint); applicationMode = ModifyRectangle; } } break;
All the aforementioned cases in this method takes place when the actionMode
method is Modify
. However, it can also be Add
, in which case a new figure will be added to the drawing. We use the addFigureId
method to decide which kind of figure to add when calling the CreateFigure
method. We set the dirty flag, since we have added a figure and the document has been modified. Finally, we add the address of the new figure to the beginning of the figure list (so that it appears on top) and set the applicationMode
method to the ModifySingle
mode:
case Add: { DrawFigure* newFigurePtr = CreateFigure(addFigureId); newFigurePtr->SetColor(nextColor); newFigurePtr->Fill(nextFill); newFigurePtr->SetFirstPoint(mousePoint); SetDirty(true); figurePtrList.PushFront(newFigurePtr); applicationMode = ModifySingle; } break; }
Depending on the action and modes, the window and cursor may need to be updated:
UpdateWindow(); UpdateCursor(); } }
The MoveMarkFigure
method is a callback function that is called by the Apply
method on figurePtrList
in the OnMouseMove
method. It moves the figure that is marked. The address of the moving distance is given in the voidPtr
parameter:
void MoveMarkedFigure(DrawFigure*& figurePtr, void* voidPtr) { if (figurePtr->IsMarked()) { figurePtr->Invalidate(); Size* distanzeSizePtr = (Size*) voidPtr; figurePtr->Move(*distanzeSizePtr); figurePtr->Invalidate(); } }
In the OnMouseMove
method, we start by calculating the distance since the previous call to the OnMouseDown
or OnMouseMove
method. We also set the prevMousePoint
method to the mouse position:
void DrawDocument::OnMouseMove(MouseButton mouseButtons, Point mousePoint,bool shiftPressed, bool controlPressed) { if (mouseButtons == LeftButton) { Size distanceSize = mousePoint - prevMousePoint; prevMousePoint = mousePoint;
Depending on the applicationMode
method, we perform different tasks. In case of the Modify
method on a single figure, we call the MoveOrModify
method on that figure. The figure is placed at the beginning of the figure pointer list (figurePtrList[0]
), since we placed it there in the OnMouseDown
method. The idea is that the figure itself, depending on where the user clicked, decides whether it is moved or modified. The state of the figure is set when the user clicks on it, and depends on whether they click on any of the endpoints of the figure:
switch (applicationMode) { case ModifySingle: figurePtrList[0]->Modify(distanceSize); SetDirty(true); break;
In case of multiple movements, we move every marked figure the distance since the last mouse message. Note that we do not modify the figures in the multiple cases as we do in the single case:
case MoveMultiple: figurePtrList.Apply(MoveMarkedFigure, &distanceSize); SetDirty(true); break;
In the rectangle case, we set its bottom-right corner and redraw it:
case ModifyRectangle: Invalidate(insideRectangle); insideRectangle.SetBottomRight(mousePoint); Invalidate(insideRectangle); UpdateWindow(); break; } UpdateWindow(); UpdateCursor(); } }
The IsFigureInside
and MarkFigure
methods are callback functions that are called by the DynamicList
methods CopyIf
, RemoveIf
, and Apply
on figurePtrList
in the OnMouseUp
method. The IsFigureInside
method returns true
if the figure is located inside the given rectangle, while the MarkFigure
method simply marks the figure:
bool IsFigureInside(DrawFigure* const& figurePtr, void* voidPtr) { Rect* insideRectanglePtr = (Rect*) voidPtr; return figurePtr->IsInside(*insideRectanglePtr); } void MarkFigure(DrawFigure*& figurePtr, void* /* voidPtr */) { figurePtr->Mark(true); }
In the OnMouseUp
method, we only need to take the ModifyRectangle
case into consideration. We need to decide which figures are totally enclosed by the rectangle. In order for them to appear on top of the drawing, we first call the CopyIf
method on the figurePtrList
list to temporarily copy the figures located completely inside the rectangle to the insideList
list.
Then we remove the figures from the figurePtrList
list and insert them from the insideList
list at the beginning of the figurePtrList
list. This makes them appear at the top of the drawing. Finally, we mark the figure inside the rectangle by calling Apply
on the insideList
list:
void DrawDocument::OnMouseUp(MouseButton mouseButtons, Point mousePoint, bool shiftPressed, bool controlPressed) { if (mouseButtons == LeftButton) { switch (applicationMode) { case ModifyRectangle: { insideRectangle.Normalize(); DynamicList<DrawFigure*> insideList; figurePtrList.CopyIf(IsFigureInside, insideList, &insideRectangle); figurePtrList.RemoveIf(IsFigureInside, &insideRectangle); figurePtrList.PushFront(insideList); insideList.Apply(MarkFigure); Invalidate(insideRectangle); insideRectangle.Clear(); UpdateWindow(); } break; }
After the user has released the left mouse button, the application holds the Idle
mode, which it always holds as long as the user does not press the left mouse button:
applicationMode = Idle; } }
The OnDoubleClick
method is called when the user double-clicks on the mouse button. The difference between a double-click and two consecutive clicks is decided by the Windows system, and can be adjusted in the Windows control panel. In case of a double-click, the OnMouseDown
and OnMouseUp
methods are called before the OnDoubleClick
method. We extract the topmost clicked figure, if any, and call the DoubleClick
method. The result depends on the type of figure: the head of an arrow is reversed, a rectangle or ellipse is filled if unfilled and vice versa, and a line is not affected at all:
void DrawDocument::OnDoubleClick(MouseButton mouseButtons, Point mousePoint, bool shiftPressed, bool controlPressed) { if ((mouseButtons == LeftButton) && !controlPressed) { DrawFigure* topClickedFigurePtr; if (figurePtrList.FirstOf(IsFigureClicked,topClickedFigurePtr, &mousePoint)) { topClickedFigurePtr->DoubleClick(mousePoint); } } }
Painting
In Small Windows, there are three general painting methods: OnPaint
, OnPrint
, and OnDraw
. The Windows system indirectly calls the OnPaint
and OnPrint
methods for painting a window or printing a paper, respectively. Their default behavior is to call the OnDraw
method. Remember that we do not take any initiatives to paint the window, we just wait for the right message. The idea is that in cases when we need to distinguish between painting and printing, we override the OnPaint
and OnPrint
methods, and when we do not need that distinction, we override the OnDraw
method instead.
In the word processor, which is discussed later in this book, we will look into the difference between painting and printing. However, in this application, we just override the OnDraw
method. As mentioned in Chapter 3, Building a Tetris Application, the Graphics
class reference is created by the framework and can be considered a toolbox equipped with pens and brushes. In this case, we just call the DrawFigure
method for each figure with the Graphics
reference as a parameter. In case of the ModifyRectangle
mode, we also draw the rectangle:
void DrawDocument::OnDraw(Graphics& graphics, DrawMode /* drawMode */) const { int size = figurePtrList.Size(); for (int index = (size - 1); index >= 0; --index) { DrawFigure* figurePtr := figurePtrList) {[index]; figurePtr->Draw(graphics); } if (applicationMode == ModifyRectangle) { graphics.DrawRectangle(insideRectangle, Gray); } }
The File menu
Thanks to the framework in the StandardDocument
class, the file management is quite easy. The ClearDocument
method is called when the user selects the New menu item, we just delete the figures and clear the figure list:
void DrawDocument::ClearDocument() { for (DrawFigure* figurePtr : figurePtrList) { delete figurePtr; } figurePtrList.Clear(); }
The WriteDocumentToStream
method is called when the user selects the Save or Save As menu item. It first writes the size of the figure list, and for each figure it writes its identity number (which is necessary when reading the figure in the ReadDocumentFromStream
method shown as follows), and then writes the figure itself by calling its WriteFigureToStream
method:
bool DrawDocument::WriteDocumentToStream(String name, ostream& outStream)const{ int listSize = figurePtrList.Size(); outStream.write((char*) &listSize, sizeof listSize); for (DrawFigure* figurePtr : figurePtrList) { FigureId figureId = figurePtr->GetId(); outStream.write((char*) &figureId, sizeof figureId); figurePtr->WriteFigureToStream(outStream); } return ((bool) outStream); }
The ReadDocumentFromStream
method is called when the user selects the Open menu item. It starts by reading the number of figures in the figure list. We need to read the identity number for the next figure and call the CreateFigure
method to receive a pointer to the created figure. Then we just call the ReadFigureFromStream
method for the figure and add the figure's address to the figure pointer list:
bool DrawDocument::ReadDocumentFromStream(String name, istream& inStream) { int listSize; inStream.read((char*) &listSize, sizeof listSize); for (int index = 0; index < listSize; ++index) { FigureId figureId; inStream.read((char*) &figureId, sizeof figureId); DrawFigure* figurePtr = CreateFigure(figureId); figurePtr->ReadFigureFromStream(inStream); figurePtrList.PushBack(figurePtr); } return ((bool) inStream); }
The CreateFigure
method is called by the ReadFigureFromStream
and ReadFigureFromClipboard
method and creates a figure of the given type:
DrawFigure* DrawDocument::CreateFigure(FigureId figureId) const { switch (figureId) { case LineId: return (new LineFigure(this)); case ArrowId: return (new ArrowFigure(this)); case RectangleId: return (new RectangleFigure(this)); case EllipseId: return (new EllipseFigure(this)); } return nullptr; }
Cut, copy, and paste
Similar to the aforementioned file management case, the framework also takes care of the details of cut, copy, and paste. First, we do need to decide when the cut and copy menu items and accelerators will be enabled. In Modify
mode, it is enough that at least one figure is marked. We use the DynamicList
method AnyOf
to decide whether at least one figure is marked. In Add
mode, cut or copy is never allowed. We do not need to override the CutEnable
method, since its default behavior in the StandardDocument
framework is to call the CopyEnable
method:
bool DrawDocument::CopyEnable() const { if (applicationMode == Idle) { switch (actionMode) { case Modify: return figurePtrList.AnyOf(IsFigureMarked); case Add: return false; } } return false; }
There is a PasteEnable
method in the StandardDocument
framework. However, in this application we do not need to override it, since the framework decides when to enable pasting or, more specifically, when there is data on the global clipboard with the format code given in the StandardDocument
constructor, in this case the DrawFormat
field. The global clipboard is a Windows resource intended for short-term storing of information that has been copied.
The CopyGeneric
method takes a list of characters that are intended to be filled with application-specific information. We save the number of marked figures, and for each marked figure, we write its identity number and call the WriteFigureToClipboard
method, which writes the figure-specific information to the infoList
parameter:
bool DrawDocument::IsCopyGenericReady(int /* format */) const { return true; } void DrawDocument::CopyGeneric(int format, InfoList& infoList) const { DynamicList<DrawFigure*> markedList; figurePtrList.CopyIf(IsFigureMarked, markedList); infoList.AddValue<int>(markedList.Size()); for (DrawFigure* figurePtr : markedList) { infoList.AddValue<FigureId>(figurePtr->GetId()); figurePtr->WriteFigureToClipboard(infoList); } }
The PasteGeneric
method pastes the figures in a way similar to the aforementioned the ReadDocumentFromStream
method:
void DrawDocument::PasteGeneric(int format, InfoList& infoList) { figurePtrList.ApplyIf(IsFigureMarked, UnmarkFigure); int pasteSize; infoList.GetValue<int>(pasteSize); for (int count = 0; count < pasteSize; ++count) { FigureId figureId; infoList.GetValue<FigureId>(figureId); DrawFigure* figurePtr = CreateFigure(figureId); figurePtr->ReadFigureFromClipboard(infoList); figurePtr->Move(Size(1000, 1000)); figurePtrList.PushBack(figurePtr); figurePtr->Mark(true); } UpdateWindow(); }
There is a DeleteEnable
method in the StandardDocument
framework, which we do not need to override since its default behavior is to call the CopyEnable
method. The OnDelete
method goes through the figure list, invalidating and deleting the marked figures. We use the DynamicList
method ApplyRemoveIf
to remove and delete marked figures.
We cannot simply use the ApplyIf
and RemoveIf
methods to deallocate and remove the figures, since it would result in memory errors (dangling pointers):
void DeleteFigure(DrawFigure*& figurePtr, void* /* voidPtr */) { figurePtr->Invalidate(); delete figurePtr; } void DrawDocument::OnDelete() { figurePtrList.ApplyRemoveIf(IsFigureMarked, DeleteFigure, nullptr, this); UpdateWindow(); SetDirty(true); }
The Modify menu
The Modify menu item is quite easy to handle. It is enabled in case the application is in the Idle
mode, which it is in when the user does not press the left mouse button. The radio button is also present if the actionMode
method is Modify
, and the menu item listener just sets the actionMode
method to Modify
:
bool DrawDocument::ModifyEnable() const { return (applicationMode == Idle); } bool DrawDocument::ModifyRadio() const { return ((applicationMode == Idle) && (actionMode == Modify)); } void DrawDocument::OnModify() { actionMode = Modify; }
For the Color and Fill menu items, there are enable methods that are rather easy and listeners that are a little bit more complicated. It is possible to change the color in Modify
mode if at least one figure is marked. In Add
mode, it is always possible to change the color:
bool DrawDocument::ColorEnable() const { if (applicationMode == Idle) { switch (actionMode) { case Modify: return figurePtrList.AnyOf(IsFigureMarked); case Add: return true; } } return false; }
The SetFigureColor
method is a callback function that is called by the ApplyIf
method on the figurePtrList
list in the OnColor
method:
void SetFigureColor(DrawFigure*& figurePtr, void* voidPtr) { Color* colorPtr = (Color*) voidPtr; if (figurePtr->IsMarked() && (figurePtr->GetColor() != *colorPtr)) { figurePtr->SetColor(*colorPtr); } }
The OnColor
method is called when the user selects the Color menu item. In Modify
mode, we extract the marked figures and choose the color of the topmost of them. We know that at least one figure is marked, otherwise the preceding ColorEnable
method would return false
and the Color menu item would be disabled. If the ColorDialog
call returns true
, we set the new color of all marked figures by calling the ApplyIf
method on the figurePtrList
list:
void DrawDocument::OnColor() { switch (actionMode) { case Modify: { DynamicList<DrawFigure*> markedList; figurePtrList.CopyIf(IsFigureMarked, markedList); DrawFigure* topFigurePtr = markedList[0]; Color topColor = topFigurePtr->GetColor(); if (StandardDialog::ColorDialog(this, topColor)) { nextColor = topColor; figurePtrList.ApplyIf(IsFigureMarked, SetFigureColor, nullptr, &topColor); UpdateWindow(); SetDirty(true); } } break;
If the actionMode
method is Add
, we just display a color dialog to set the next color:
case Add: StandardDialog::ColorDialog(this, nextColor); break; } }
The IsFigureMarkedAndFilled
method is a callback function that is called by the AnyOf
method on the figurePtrList
list in the FillCheck
method. The Fill menu item is checked with a radio mark if at least one figure is marked and filled:
bool IsFigureMarkedAndFilled(DrawFigure* const& figurePtr, void* /* voidPtr */) { return (figurePtr->IsMarked() && figurePtr->IsFilled()); } bool DrawDocument::FillCheck() const { if (applicationMode == Idle) { switch (actionMode) { case Modify: return figurePtrList.AnyOf(IsFigureMarkedAndFilled); case Add: return nextFill; } } return false; }
The IsFigureMarkedAndFillable
method is a callback function that is called by the AnyOf
method on the figurePtrList
list in the FillEnable
method. The Fill menu item is enabled if at least one fillable figure (rectangle or ellipse) is marked, or if the user is about to add a rectangle or ellipse:
bool IsFigureMarkedAndFillable(DrawFigure* const& figurePtr, void* /* voidPtr */){ return (figurePtr->IsMarked() && figurePtr->IsFillable()); } bool DrawDocument::FillEnable() const { if (applicationMode == Idle) { switch (actionMode) { case Modify: return figurePtrList.AnyOf(IsFigureMarkedAndFillable);
In order to test whether the figure type of the next figure to be added is fillable, we create and delete such a figure:
case Add: { DrawFigure* addFigurePtr = CreateFigure(addFigureId); bool fillable = addFigurePtr->IsFillable(); delete addFigurePtr; return fillable; } } } return false; }
The InverseFill
method is a callback function that is called by the AnyOf
method on the figurePtrList
list in the OnFill
method, which is called when the user selects the Fill menu item. The OnFill
method inverts the fill status of all marked figures in Modify
mode. In Add
mode, it just inverts the value of nextFill
, indicating that the next figure to be added will have the inverted fill status:
void InverseFill(DrawFigure*& figurePtr, void* /* voidPtr */) { if (figurePtr->IsMarked()) { figurePtr->Fill(!figurePtr->IsFilled()); } } void DrawDocument::OnFill() { switch (actionMode) { case Modify: figurePtrList.ApplyIf(IsFigureMarked, InverseFill); UpdateWindow(); break; case Add: nextFill = !nextFill; break; } }
The Add menu
The listeners for the items of the Add
menu are rather straightforward. The enable methods are simple, for the menu item to be enabled it is enough if the applicationMode
method is in the Idle
mode:
bool DrawDocument::LineEnable() const { return (applicationMode == Idle); } bool DrawDocument::ArrowEnable() const { return (applicationMode == Idle); } bool DrawDocument::RectangleEnable() const { return (applicationMode == Idle); } bool DrawDocument::EllipseEnable() const { return (applicationMode == Idle); }
The radio methods return true
in Add
mode if the figure to be added matches the figure of the radio method:
bool DrawDocument::LineRadio() const { return ((actionMode == Add) && (addFigureId == LineId)); } bool DrawDocument::ArrowRadio() const { return ((actionMode == Add) && (addFigureId == ArrowId)); } bool DrawDocument::RectangleRadio() const { return ((actionMode == Add) && (addFigureId == RectangleId)); } bool DrawDocument::EllipseRadio() const { return ((actionMode == Add) && (addFigureId == EllipseId)); }
Finally, the methods responding to the menu item and accelerator selections sets the actionMode
to Add
and the figure to be added:
void DrawDocument::OnLine() { actionMode = Add; addFigureId = LineId; } void DrawDocument::OnArrow() { actionMode = Add; addFigureId = ArrowId; } void DrawDocument::OnRectangle() { actionMode = Add; addFigureId = RectangleId; } void DrawDocument::OnEllipse() { actionMode = Add; addFigureId = EllipseId; }
The cursor
The Set
method in the Cursor
class sets the cursor to an appropriate value. If the application mode is Idle
mode, we wait for the user to press the mouse button. In that case, we use the well-known arrow cursor image. If the user is in the process of enclosing figures with a rectangle, we use the cross-hair. If the user is in the process of moving several figures, we use the cursor with four arrows (size all). Finally, if they are in the process of modifying a single figure, the figure (whose address is located in the figurePtrList[0]
list) itself is deciding which cursor to use:
void DrawDocument::UpdateCursor() { switch (applicationMode) { case Idle: Cursor::Set(Cursor::Arrow); break; case ModifyRectangle: Cursor::Set(Cursor::Crosshair); break; case MoveMultiple: Cursor::Set(Cursor::SizeAll); break; case ModifySingle: Cursor::Set(figurePtrList[0]->GetCursor()); break; } }
- Docker技術入門與實戰(第3版)
- 軟件界面交互設計基礎
- JavaScript+jQuery網頁特效設計任務驅動教程(第2版)
- Vue.js 2 and Bootstrap 4 Web Development
- Mastering Rust
- Learning Vaadin 7(Second Edition)
- Building RESTful Python Web Services
- ANSYS Fluent 二次開發指南
- 機器學習與R語言實戰
- Java程序設計案例教程
- Julia 1.0 Programming Complete Reference Guide
- 汽車人機交互界面整合設計
- Django Design Patterns and Best Practices
- ROS機器人編程實戰
- Beginning PHP