Click here to Skip to main content
Click here to Skip to main content

WPF DrawTools

By , 8 Jan 2008
 
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)

About the Author

Alex Fr
Software Developer
Israel Israel
Member
No Biography provided

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
QuestionSave background image with visuals to a filememberTodd Cherry25 Mar '13 - 3:09 
Alex,
 
I see how your printing the drawing visuals along with the background image in the demo app. How would you go about saving them all to a file, say a bitmap for example?
 
I'm trying to use RenderTargetBitmap (see below), but I just get the visuals and not the background image.
Any tips would be welcome.
 
Here is my code:
 
if (drawingCanvas.GetListOfGraphicObjects().Any())
{
     var vs = new DrawingVisual();
     var dc = vs.RenderOpen();
     dc.DrawImage(image.Source, 
                  new Rect(0,0, image.Source.PixelWidth, image.Source.PixelHeight));
     dc.Close();
 
     drawingCanvas.Draw(dc);
  
     var size = new Size(drawingCanvas.ActualWidth, drawingCanvas.ActualHeight);
 
     drawingCanvas.Measure(size);
     drawingCanvas.Arrange(new Rect(size));
 
      var bmp = new RenderTargetBitmap((int)drawingCanvas.ActualWidth,
                                      (int)(drawingCanvas.ActualHeight),
                                      300,
                                      300,
                                      PixelFormats.Default);
 
      bmp.Render(drawingCanvas);
      var enc = new PngBitmapEncoder();
      enc.Frames.Add(BitmapFrame.Create(bmp));
 
      using (var stm = File.Create(@"c:\temp\test.png"))
      {
          enc.Save(stm);
      }
}

QuestionHow to ADD tables to the DrawTools.memberAbhinavkpit00721 Mar '13 - 20:24 
Hi Alex,
 
This is a wonderful article by you and it helped me a lot on Drawing.Actually I want to add tables to the Draw tools, I have tried to draw the required tables and succeded but unable to add text in the cells created.
 
Can you suggest me a approach.....
 

 
Again Thnks For this Article Smile | :)
 
Abhinav
QuestionHow to recalculate the rotation Center after the Object resizemembergnanamu26 Feb '13 - 4:19 
I tried to implement rotation by adding a rotation handle. Rotation works fine. I am using the DrawingVisual.Transform for rotation.
I re sized the object after rotation and i tried to rotate the but the object moves to some location. There is a problem in the Center calculation. This is only happening when i resize the object. but when i translate the object after rotation there is no problem in further rotation.
 
Please help me. How to recalculate the rotation Center after the Object resize
GeneralMy vote of 5memberJason Fang_10 Jan '13 - 21:06 
Perfect.
SuggestionRotating text possible?memberBlackMilan9 Nov '12 - 3:15 
It looks great, and I think ...
 
Is it possible to rotate the text in the direction, so for example vertically from bottom to top, or vice versa, or even upside down?
GeneralRe: Rotating text possible?memberAlex Fr13 Nov '12 - 4:59 
I didn't think about this. Actually, drawing rotated text is possible using coordinate transformations, but in-place editing looks somewhat exciting...
GeneralRe: Rotating text possible?memberBlackMilan14 Nov '12 - 20:56 
Ja selbstverständlich. Aber man könnte vielleicht ein separates Feld zur Text Eingabe nehmen. Ich habe schon kurz mit der Koordinaten Transformation herum gespielt, aber dann verschwindet der Text immer komplett. Keine Ahnung was ich da schief läuft.
GeneralRe: Rotating text possible?memberAlex Fr15 Nov '12 - 3:55 
Halik pen partia birtutas.
GeneralRe: Rotating text possible?memberBlackMilan15 Nov '12 - 21:04 
Sorry, the translator didn't play with me ...
 
Yes, of course. But you could maybe take a separate field for entering text. I've played briefly with the coordinate transformation around, but then the text is disappearing completely. I do not know what am I going wrong.
GeneralMy vote of 5memberssavkin6 Sep '12 - 22:33 
Thanks, Alex! Great article!
QuestionGraphicsSelectionRectanglememberPhilip Stuyck7 Aug '12 - 2:19 
I see that when the user is selecting elements on the canvas by clicking and moving the mouse that a graphicsSelectionRectangle is created. Basically this are two rectangles a white one, which is solid, and a black one that is dashed and black. However this rectangle does not appear to be completely opaque but I cannot seem to find the code to give it some kind of transparency. Where in the code are doing that ?
QuestionLayer like in PhotoshopmemberCetin AKbulut22 Jul '12 - 5:43 
Hello,
 
how is possible to implement a layer managerment in WPF like in Photoshop.
Are there common technique.
 
Best regards
cakbulut
QuestionJPEG/BMP exportmemberMember 851326118 Jul '12 - 3:20 
Hello!
 
Can anyone help me with exporting to bitmap or JPEG?
 
I would like to convert all the objects (xml data) to bitmap. I was successful with background implementation. Drawing image data from XML back to the canvas, but I wasnt successful at xml to jpeg complete conversion -_-
GeneralMy vote of 5memberFarhan Ghumra13 Jun '12 - 2:51 
Excellent
QuestionGetting nearly 400+ errors [modified]memberFarhan Ghumra13 Jun '12 - 2:31 
Getting error symbol in this assemblies "WindowsBase, PresentationCore, PresentationFramework"
 
I have also tried by adding those assembly references.

modified 13 Jun '12 - 8:42.

AnswerRe: Getting nearly 400+ errorsmemberFarhan Ghumra29 Jun '12 - 1:21 
Yippi, Here [^] is the solution
Windows 8 Metro Style App Developer
Silverlight Developer
 
My Blog on Windows 8

QuestionYour comment on this will be a great help.memberakohan6 Jun '12 - 8:25 
Alex,
 
One thing I'm adding on this source is adding a toolbox where end user could drag/drop his custom control (adorners). However, when I drop something on canvas nothing shows up.
 
What portion of Canvas code must be modified to allow dropping objects on it?
 
if Adorners is not a good approach then what do you suggest? long story short I need to drop and use some user controls on this canvas. Please advise.
 

 
Thank you in advance,
Amit
AnswerRe: Your comment on this will be a great help.memberAlex Fr13 Jun '12 - 6:54 
DrawingCanvas accepts only DrawingVisual-derived children. This restricts this solution to simple graphic objects like presented in the sample. Actually, WPF DrawTools provides Winforms-style solution with manual coding. If you want to place some high-level objects, the whole approach should be changed. There are several more "WPF-style" solutions available in CodeProject, consider using one of them.
Questionabout PolyLine and Line [modified]memberakohan16 May '12 - 7:45 
Hello Alex,
 
No need to say more about this application, however, let me say it is a unique one. Thank you for sharing it.
 
Question 1:
 
Here, I like to ask a question and I will appreciate it if you give me some hints. I like to implement a feature
where 2 lines or more can be merged at their ends. You have line AB and CD and now when user moves node B and drops
it on node C then we will see a line (either straight or broken) as AD or ... so it will be treated as one single
line object rather two line objects.
 
I can see part of it is implemented in Polyline behavior. right?
 
In fact, all I need to implement/add here is to merging lines (maybe CombinedGeometry term (Union) applies to it - correct me if I'm wrong).
If so, can you give me some direction on your code or online resources?
 
Few questions:
 
2: In PolyLine where do you specify the length of each point/node? it seems it has been defined very short.
3: Which part of code highlights the nodes on lines, polyline and other shapes as small blue boxes?
 
---- updates:
 
I looked at the code and found ToolPorinters.cs and GraphicsLine.cs have methods such as
 
PathGeometry p = Geometry.Combine(rg, widen, GeometryCombineMode.Intersect, null);
 
I guess I must call the same function to join/unite two different such a line and rect get merged such as
 
PathGeometry p = Geometry.Combine(rg, widen, GeometryCombineMode.Union, null);
 
Is this right?
 

 
Thanks
AKOHAN

modified 16 May '12 - 19:04.

AnswerRe: about PolyLine and LinememberAlex Fr18 May '12 - 9:09 
I don't remember exactly, sorry, this very old project.
For polyline, it is enough to keep coordinates of every point, there is no need for a line length.
Small blue boxes - I think you are talking about selection. So, every object should have something like this: if ( Selected ) { draw selection handles }
GeneralRe: about PolyLine and Linememberakohan6 Jun '12 - 8:28 
Thanks for your response. I might not been clear, all I need is allowing lines (line A and B) to snap in so when I drop one end of line A on another end of line B then they will be one piece and attached to each other.
 
Regards,
Amit
GeneralRe: about PolyLine and Linememberjudasizm11 Jun '12 - 12:59 
Hi akohan,
i am trying to add this tool too for drawing polyline or polygon. but it is hard to figure out the project that made by somebody. if i find something i will write here...
best regards,
GeneralRe: about PolyLine and Linememberakohan12 Jun '12 - 5:59 
Hi Judasizm,
 
That would be great. I will do the same as soon as I find out how it is.
 
Regards,
Amit
QuestionMore ToolsmemberhosenHashefi20 Apr '12 - 3:02 
Hi
Thank you for Your So UseFull Articel About Draw Tool By Wpf.
I am Developer I use Your Great Article In My Project
I Need Some More Feature
I Would Be appreciated if You Help With This
 
1) How To Custome Rotate Shapes And Save Like Other Shape Posotion.
2) How To Fill With A color Some Custome Shape Like Paint Fill With A Color
 
My Email: Kashefi.mail@gmail.Com
Thanks Agian.
Rose | [Rose]
Big

QuestionObject rotationmemberthefox8518 Apr '12 - 8:31 
How to implement rotation?

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

Permalink | Advertise | Privacy | Mobile
Web03 | 2.6.130523.1 | Last Updated 8 Jan 2008
Article Copyright 2008 by Alex Fr
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid