Click here to Skip to main content
Click here to Skip to main content
Go to top

WPF DrawTools

, 8 Jan 2008
Rate this:
Please Sign up or sign in to vote.
WPF application for drawing graphic objects in a window client area using drawing tools and mouse
DT_Default.jpg

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:

DT_Viewbox.jpg

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:

DT_Scroll.jpg

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

Graphics.jpg

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.jpg

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

Commands.jpg

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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Alex Fr
Software Developer
Israel Israel
No Biography provided

Comments and Discussions

 
QuestionJPEG/BMP export PinmemberMember 851326118-Jul-12 3:20 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140916.1 | Last Updated 8 Jan 2008
Article Copyright 2008 by Alex Fr
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid