![]() |
Platforms, Frameworks & Libraries »
Windows Presentation Foundation »
General
Intermediate
License: The Code Project Open License (CPOL)
WPF DrawToolsBy Alex FrWPF application for drawing graphic objects in a window client area using drawing tools and mouse |
C# (C# 3.0), Windows (WinXP, Vista), WPF, Dev, Design
|
|
Advanced Search Add to IE Search |
|
|
|
||||||||||||||||
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:
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:
ActualWidth and ActualHeight. This creates problems when the canvas is used to draw overlays on an image. 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.
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. 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.
| 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 |
| 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.
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 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.
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.
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:
DrawingVisual; I want to serialize only my own properties like color, line width, etc. and not the whole DrawingVisual instance. 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. 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.
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.
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.
General
News
Question
Answer
Joke
Rant
Admin
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 8 Jan 2008 Editor: Genevieve Sovereign |
Copyright 2008 by Alex Fr Everything else Copyright © CodeProject, 1999-2009 Web21 | Advertise on the Code Project |