Introduction
This article describes the program that allows one to draw graphic objects on a WPF window using the mouse and certain drawing tools. The program supports the following tools: rectangle, ellipse, line, pencil and text. My first DrawTools article shows how to do this using Windows Forms. In the WPF version, I implemented a number of additional features requested by previous article readers:
- Text tool
- XML serialization
- Drawing overlays on a background image
- Printing
- Zoom
What Technology to Use
The first question was of what WPF features to use for implementing drawing functionality. Obviously, the host class should be derived from the Canvas
. First, I wanted to use Shape
-derived classes as Canvas
children. This allows the use of high-level AdornerDecorator
and Thumb
classes for object moving and resizing. However, when writing simple prototypes I encountered a number of problems:
- WPF adorners allow one to move and resize single objects. Group selection and moving still require manual coding.
- Moving a graphic object outside of the hosting canvas changes the canvas'
ActualWidth
and ActualHeight
. This creates problems when the canvas is used to draw overlays on an image.
- The size of some elements, like line width and selection thumbs, should remain unchanged while the canvas is resized by a container like
Viewbox
. This requires full control over graphics' appearance.
My final choice was a Canvas
-derived class that is called DrawingCanvas
. It hosts VisualCollection
, which contains instances of DrawingVisual
-derived classes. This requires more coding, but keeps things under full control. The result of this approach is that some code in the project is similar to the Windows Forms DrawTools version.
Solution Structure
The DrawToolsWPF solution contains three projects:
DrawTools
: the hosting application.
Utilities
: general purpose classes like converters, persisting window state support, MRU files list, etc.
DrawToolsLib
: the generic part of the solution, this is a Class Library which exports the DrawingCanvas
class. This class can be placed on a XAML page of the client host application and used to add drawing functionality.
Different Ways to Use DrawingCanvas
DrawingCanvas
can be used in standalone mode. The image in the beginning of this article shows this mode. It is also possible to make DrawingCanvas
completely transparent and place it over some image, which allows one to draw graphic overlays on the image.
The DrawTools
hosting application uses three different methods of employing DrawingCanvas
. The MainWindow.xaml file contains three code fragments, but only one of them is active. The two others should be commented out. The first version is standalone mode:
<lib:DrawingCanvas x:Name="drawingCanvas" Background="White" />
Comment this line and uncomment the second version:
<Viewbox Name="viewBoxContainer">
<Grid Name="gridContainer">
<Image Name="imageBackground" Source="Images/background.jpg" Stretch="None"/>
<lib:DrawingCanvas x:Name="drawingCanvas" Background="#00000000"
Width="{Binding ElementName=imageBackground, Path=ActualWidth, Mode=OneWay}"
Height="{Binding ElementName=imageBackground,
Path=ActualHeight, Mode=OneWay}"
/>
</Grid>
</Viewbox>
Compile the program and run it. It looks like this:
Transparent DrawingCanvas
is placed over the Image
control. It is resized together with an Image
by container Viewbox
. Although DrawingCanvas
itself is transparent, members of its VisualCollection
are visible. DrawingCanvas
handles mouse events and draws graphic overlays. Mouse coordinates handled by DrawingCanvas
are always compatible with the size of the image shown in the Image
control. Now comment out these lines and uncomment the third version:
<Grid Name="gridContainer">
<Grid.RowDefinitions>
<RowDefinition Height="5*"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ScrollViewer Grid.Row="0" HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<Grid>
<Grid.LayoutTransform>
<ScaleTransform
ScaleX="{Binding ElementName=sliderScale, Path=Value, Mode=OneWay}"
ScaleY="{Binding ElementName=sliderScale, Path=Value, Mode=OneWay}"
/>
</Grid.LayoutTransform>
<Image Name="imageBackground"
Source="Images/background.jpg" Stretch="None"/>
<lib:DrawingCanvas Name="drawingCanvas" Background="#00000000"
Width="{Binding ElementName=imageBackground,
Path=ActualWidth, Mode=OneWay}"
Height="{Binding ElementName=imageBackground,
Path=ActualHeight, Mode=OneWay}"
ActualScale="{Binding ElementName=sliderScale,
Path=Value, Mode=OneWay}"
</lib:DrawingCanvas>
</Grid>
</ScrollViewer>
<GridSplitter Grid.Row="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Top" ResizeDirection="Rows"
/>
<Grid Grid.Row="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="9*"/>
<ColumnDefinition Width="2*"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0">
Scale
</Label>
<Slider Grid.Column="1" Name="sliderScale"
Orientation="Horizontal" Minimum="0.2" Maximum="5.0"
Margin="10,5,10,5"
Value="1.0"
/>
<Label Grid.Column="2" Content="{Binding ElementName=sliderScale,
Path=Value, Mode=OneWay,
Converter={StaticResource convDoubleDecimal}, ConverterParameter=2}"/>
</Grid>
</Grid>
Compile the program and run it. Now it looks like this:
Dependency property DrawingCanvas.ActualScale
is bound to the scale coefficient applied to the image and canvas. Since DrawingCanvas
itself doesn't know what transformation is applied to it, the host's responsibility is to supply this information. This allows one to draw the line width with a constant size when the underlying image is resized, which is expected behavior for graphic overlays. In the second XAML version, DrawingCanvas.ActualScale
is set from the code.
The host program cannot handle mouse events in the Image
control. Notice also that Preview versions of mouse events are not available. This is because the canvas and the image are on the same visual tree level. If the host program needs to implement its own mouse handling logic, it should handle mouse messages in the gridContainer
control. Mouse coordinates are the same as in the image. Set the DrawingCanvas.Tool
property to ToolType.None
; in this case, DrawingCanvas
doesn't handle any mouse events.
The demo executables file available for download at the beginning of this article contains three executable files, one for every XAML version described here.
The DrawTools
sample shows the background image from the project's resources. This is done for demonstration purposes. Graphic overlays are saved in an XML file without any relation to underlying images. The actual application which uses DrawingCanvas
should solve the problem of keeping graphic overlays together with an image. This can be done by creating an XML file which is kept aside of the image or by using an image format which supports graphic overlays. This issue is out of scope of this article.
DrawingCanvas Interface
Dependency Properties
Name |
Type |
Description |
Tool |
ToolType |
Active drawing tool |
ActualScale |
double |
Sets scale coefficient applied to image and canvas; allows one to keep the line width unchanged while the image is resized |
IsDirty |
bool |
True if document was changed after last Clear, Save or Load operation |
CanUndo
CanRedo
CanSelectAll
CanUnselectAll
CanDelete
CanDeleteAll
CanMoveToFront
CanMoveToBack
CanSetProperties |
bool |
True if operation is available; allows one to enable/disable controls in a host application |
LineWidth |
double |
Line width: when the client sets LineWidth or any other object property, it is kept in the DrawingCanvas and applied to every new object created after this. If there are selected objects, this value is also applied to them. It is a good idea for client application to persist the last selected object properties between program sessions. These notes apply to all subsequent object properties. |
ObjectColor |
Color |
Graphic object color |
TextFontFamilyName |
string |
Font Family name of Text object |
TextFontStyle |
FontStyle |
Font Style of Text object |
TextFontWeight |
FontWeight |
Font Weight of Text object |
TextFontStretch |
FontStretch |
Font Stretch of Text object |
TextFontSize |
double |
Font size of Text object |
Methods
Prototype |
Description |
PropertiesGraphicsBase[] GetListOfGraphicObjects() |
Returns an array of light-weight objects containing properties of graphic overlays; used if the client program needs to make its own usage of graphics objects, like saving them in some persistent storage |
void Draw(DrawingContext drawingContext) |
Draws all graphics to DrawingContext; can be used for printing or saving an image together with graphics as single bitmap |
void Draw(DrawingContext drawingContext, bool withSelection) |
Draw function overlay which allows one to draw selected objects with or without a tracker |
void Clear() |
Clears all objects |
void Save(string fileName) |
Saves graphics to XML file; throws DrawingCanvasException |
void Load(string fileName) |
Loads graphics from XML file; throws DrawingCanvasException |
void SelectAll() |
Selects all objects |
void UnselectAll() |
Unselects all objects |
void Delete() |
Deletes selection |
void DeleteAll() |
Deletes all |
void MoveToFront() |
Moves selection to front of Z-order |
void MoveToBack() |
Moves selection to back of Z-order |
void Undo() |
Makes Undo operation |
void Redo() |
Makes Redo operation |
void SetProperties() |
Applies currently active properties to selection |
void RefreshClip() |
Refreshes clipping area; used for printing: see sample code |
void RemoveClip() |
Removes clipping area; used for printing: see sample code |
The class also exposes routed event IsDirtyChanged
, which is raised when the IsDirty
property is changed.
Other DrawToolsLib Classes
Graphic Objects
All graphic objects are derived from DrawingVisual
and kept in the VisualCollection
instance hosted by DrawingCanvas
.
GraphicsBase
is an abstract base class for all graphic objects. GraphicsRectangleBase
is an abstract base class for all rectangle-based objects: rectangle, ellipse and text. Every class is responsible for keeping its properties -- like coordinates, color, line width, etc. -- as well as drawing itself in DrawingContext
and providing information for handling mouse events, like hit test and cursor type. GraphicsSelectionRectangle
is ca lass which draws a group selection rectangle. It is created when the user clicks on an empty canvas place and is deleted immediately after user releases the mouse button.
Tools
Tools handle mouse events and make different actions with graphic objects. DrawingCanvas
redirects all mouse events to the currently active tool. Tool
is the abstract base class for all tools. ToolObject
is the abstract base class for all tools creating a new object. Every tool derived from ToolObject
is responsible for creating and resizing a new graphic object. ToolText
also manages the creation of TextBox
for in-place editing. ToolPointer
is the most complicated class and is responsible for the moving and resizing of existing objects, group selection and moving.
Commands
The UndoManager
class manages Undo-Redo operations. It contains the command history, a list of CommandBase
-derived classes. Every command class keeps enough information to allow it to undo or redo the command. CommandChangeState
is used for every change to existing objects: Move, Resize, Set Properties. An instance of this class is added to the History when one of these actions is executed. CommandAdd
is used when a new object is added to the canvas. CommandDelete
and CommandDeleteAll
are used for deleting.
Property Containers
For every non-abstract graphic class, there is light-weight property container class with its name starting with the Properties prefix: PropertiesGraphicsRectangle
, PropertiesGraphicsEllipse
, PropertiesGraphicsLine
, PropertiesGraphicsPolyLine
, PropertiesGraphicsText
. They don't perform any action and only keep properties. These classes are used for the following purposes:
- Serialization: the original graphic classes are derived from
DrawingVisual
; I want to serialize only my own properties like color, line width, etc. and not the whole DrawingVisual
instance.
- Property containers are used as light-weight clones kept in the command history.
DrawingCanvas.GetListOfGraphicObjects
returns an array of property containers for the client which needs to make its own handling, like saving graphics in some persistent storage.
Helper Classes
There are also two helper public classes. FontConversions
contains static functions for the conversion of different font properties to strings and creating them from strings. It helps to serialize font properties. ToolTypeConverter
is a WPF converter used to check/unckeck tools' controls (buttons, menu items) in a host application.
Different Ways to Implement Drawing Functionality
Text
There are a number of ways to write text objects and they can be single-line or multi-line. The bounding rectangle of multi-line text can be completely under user control or automatically adjusted according to the control content. I implemented it in the following way: as a multi-line TextBox with a bounding rectangle defined by the user. An in-place edit box is opened when new text is created or existing text is double-clicked. It is closed when the user clicks anywhere outside of the edit box, presses Enter or presses Esc. Esc cancels editing results. A new line can be started with Shift+Enter.
Properties
There are two kinds of properties: new object properties which are applied to every object created in the document, and selected object properties. It is possibly to combine them, when every property set to existing object becomes current property applied to a new object. I decided to keep only one set of properties used for both purposes. There are also two ways to set properties: via a Properties dialog or with Word-style controls placed on the toolbar. I prefer the second way because it looks more attractive.
Should Properties dialog controls placed on the toolbar follow the current selection, like in Microsoft Word? For example, say the current color is black. If the user selects a red rectangle, should the current color be changed to red? Since current properties are used for creating new objects, they are actually user preferences. For that reason, I decided to keep them unchanged and not follow the current selection.
However, there is one hole in this model. Suppose that if the current color is black, the user selects a red rectangle and wants to make it black. How can this be done? The answer is: open the Colors dialog and click OK to apply current color, but this looks stupid. I use the Set Properties command, which means: apply the current properties from the toolbar to the selected objects. This behavior can be changed according to specific client program requirements.
Sources
History
- 8 January, 2008 -- Original version posted