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

Annotating an Image in WPF

By , 12 Sep 2007
 

Introduction

This article shows how to create text annotations over an image, in a WPF application. The technique involves rendering a custom control in the adorner layer of an Image element, allowing for in-place editing of annotations. The classes used in the demo application can be easily and freely used in other applications to achieve the same functionality.

Background

When you read the newspaper and scribble a thought on the page, you are creating an annotation. The term "annotation" refers to a note which describes or explains part of another document. The Windows Presentation Foundation has built-in support for document annotations, as described here. It does not, however, provide out-of-the-box support for annotating images.

A while back I wrote a blog post about how to annotate an Image element which happens to reside in a Viewbox. This article takes that idea and generalizes it so that any Image can be annotated, not just one contained within a Viewbox. Another improvement seen in this article's demo application is that the annotations are created "in-place", as opposed to typing the annotation text in a TextBox somewhere else in the user interface.

The demo app

This article is accompanied by a demo application, available for download at the top of this page. The demo app allows you to create annotations on two images. It contains explanatory text about how to create, modify, and delete annotations.

Here is a screenshot of the demo application, after a few annotations have been created:

Notice the location of the various annotations, relative to entities in the picture. After the Window is made smaller, you will see that the annotations remain "pinned" to those entities:

Even though the dimensions of the Image element have changed, the annotations remain in the same meaningful locations over the picture. This is an important aspect of image annotations, because the location of an annotation is just as meaningful as its text.

The demo app lets the user delete annotations in several ways. If an annotation loses input focus and has no text, it is automatically deleted. Also, aside from the glaringly obvious 'Delete Annotations' button seen above, you can also delete an annotation by right-clicking on it, to pull up a context menu. For example:

Limitations

The demo app is not a "complete" solution. It does not provide any means of persisting annotations across runs of the application. I did not write annotation persistence code because there are so many different ways that this functionality might be used, that writing my own implementation seemed like a shot in the dark. I did, however, try to write the classes in such a way that it will be straightforward to implement saving and loading of annotations.

The demo app also does not provide any fancy UI features like drag-drop of annotations. That might be a useful feature, but I wanted to keep this simple. Drag-drop in WPF is pretty well documented on the Web, so if you need to add that feature you should be able to find some good reference material out there.

How it works

There are four main participants involved, as seen below:

The ImageAnnotationControl is what you actually see on the screen which displays, and allows you to edit, annotations. ImageAnnotationControl is a ContentControl which exposes one interesting public dependency property, called IsInEditMode. When that property is true, a DataTemplate is applied to the ContentTemplate property which renders the annotation text in a TextBox. When IsInEditMode is false, the annotation text is rendered in a TextBlock. The complete XAML for ImageAnnotationControl is seen below:

<ContentControl
  x:Class="ImageAnnotationDemo.ImageAnnotationControl"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:local="clr-namespace:ImageAnnotationDemo"
  x:Name="mainControl"
  >
  <ContentControl.Resources>
    <!-- The template used to create a TextBox 
         for the user to edit an annotation. -->
    <DataTemplate x:Key="EditModeTemplate">
      <TextBox
        KeyDown="OnTextBoxKeyDown"
        Loaded="OnTextBoxLoaded"
        LostFocus="OnTextBoxLostFocus"
        Style="{DynamicResource STYLE_AnnotationEditor}"
        Text="{Binding         
                ElementName=mainControl, 
                Path=Content,
                UpdateSourceTrigger=PropertyChanged}"
        />
    </DataTemplate>

    <!-- The template used to create a TextBlock 
         for the user to read an annotation. -->
    <DataTemplate x:Key="DisplayModeTemplate">
      <Border>
        <TextBlock
          MouseLeftButtonDown="OnTextBlockMouseLeftButtonDown"
          Style="{DynamicResource STYLE_Annotation}"
          Text="{Binding ElementName=mainControl, Path=Content}"
          >
          <TextBlock.ContextMenu>
            <ContextMenu>
              <MenuItem 
                Header="Delete" 
                Click="OnDeleteAnnotation"
                >
                <MenuItem.Icon>
                  <Image Source="delete.ico" />
                </MenuItem.Icon>
              </MenuItem>
            </ContextMenu>
          </TextBlock.ContextMenu>
        </TextBlock>
      </Border>
    </DataTemplate>

    <Style TargetType="{x:Type local:ImageAnnotationControl}">
      <Style.Triggers>
        <!-- Applies the 'edit mode' template 
             to the Content property. -->
        <Trigger Property="IsInEditMode" Value="True">
          <Setter
            Property="ContentTemplate" 
            Value="{StaticResource EditModeTemplate}" 
            />
        </Trigger>

        <!-- Applies the 'display mode' template 
             to the Content property. -->
        <Trigger Property="IsInEditMode" Value="False">
          <Setter
            Property="ContentTemplate" 
            Value="{StaticResource DisplayModeTemplate}" 
            />
        </Trigger>
      </Style.Triggers>
    </Style>
  </ContentControl.Resources>
</ContentControl>

ImageAnnotationAdorner is an adorner which is responsible for hosting an instance of ImageAnnotationControl. It is added to the adorner layer of the Image being annotated. ImageAnnotationAdorner is created and positioned by the ImageAnnotation class. That class has no visual representation, but simply serves as a handle to an annotation for the consumer (i.e. the demo app's main Window).

When an ImageAnnotation is created, it installs an adorner in the annotated Image's adorner layer, as seen below:

void InstallAdorner()
{
    if (_isDeleted)
        return;

    _adornerLayer = AdornerLayer.GetAdornerLayer(_image);

    _adornerLayer.Add(_adorner);
}

When the Image element is resized and an annotation must be moved to its new location, these methods in ImageAnnotation are invoked:

void OnImageSizeChanged(object sender, SizeChangedEventArgs e)
{
    Point newLocation = this.CalculateEquivalentTextLocation();
    _adorner.UpdateTextLocation(newLocation);
}

Point CalculateEquivalentTextLocation()
{
    double x = _image.RenderSize.Width * _horizPercent;
    double y = _image.RenderSize.Height * _vertPercent;
    return new Point(x, y);
}

The _horizPercent and _vertPercent fields represent the relative location of an annotation over an picture. Those values are calculated in the ImageAnnotation constructor, as seen below:

private ImageAnnotation(
 Point textLocation, Image image, 
 Style annontationStyle, Style annotationEditorStyle)
{
    if (image == null)
        throw new ArgumentNullException("image");

    _image = image;
    this.HookImageEvents(true);

    Size imageSize = _image.RenderSize;
    if (imageSize.Height == 0 || imageSize.Width == 0)
        throw new ArgumentException("image has invalid dimensions");

    // Determine the relative location of the TextBlock.
    _horizPercent = textLocation.X / imageSize.Width;
    _vertPercent = textLocation.Y / imageSize.Height;

    // Create the adorner which displays the annotation.
    _adorner = new ImageAnnotationAdorner(
       this, 
       _image, 
       annontationStyle, 
       annotationEditorStyle, 
       textLocation);

    this.InstallAdorner();
}

The Window in the demo app asks ImageAnnotation to create instances of itself when the user clicks on an Image. In addition to informing the annotation where it should exist over the Image, it also specifies two Styles for the ImageAnnotationControl, as seen below:

void image_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    Image image = sender as Image;

    // Get the location of the mouse cursor relative to the Image. Offset the
    // location a bit so that the annotation placement feels more natural.
    Point textLocation = e.GetPosition(image);
    textLocation.Offset(-4, -4);

    // Get the Style applied to the annotation's TextBlock.
    Style annotationStyle = base.FindResource("AnnotationStyle") as Style;

    // Get the Style applied to the annotations's TextBox.
    Style annotationEdtiorStyle =
       base.FindResource("AnnotationEditorStyle") as Style;

    // Create an annotationwhere the mouse cursor is located.
    ImageAnnotation imgAnn = ImageAnnotation.Create(
       image,
       textLocation,
       annotationStyle,
       annotationEdtiorStyle);

    this.CurrentAnnotations.Add(imgAnn);
}

Those two Style objects allow the annotation consumer to specify how annotations should be rendered, both when in edit mode and display mode. The demo app's Styles, which exist in the main Window's resources, are seen below:

<!-- This is the Style applied to the TextBlock within 
     an ImageAnnotationControl. -->
<Style x:Key="AnnotationStyle" TargetType="TextBlock">
  <Setter Property="Background" Value="#AAFFFFFF" />
  <Setter Property="FontWeight" Value="Bold" />      
  <Style.Triggers>
    <Trigger Property="IsMouseOver" Value="True">
      <Setter Property="Background" Value="#CCFFFFFF" />
    </Trigger>
  </Style.Triggers>
</Style>

<!-- This is the Style applied to the TextBox within 
     an ImageAnnotationControl. -->
<Style x:Key="AnnotationEditorStyle" TargetType="TextBox">
  <Setter Property="Background" Value="#FFFFFFFF" />
  <Setter Property="BorderThickness" Value="0" />
  <Setter Property="FontWeight" Value="Bold" />
  <Setter Property="Padding" Value="-2,0,-1,0" />
</Style>

Revision history

  • September 12, 2007 – Created the article

License

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

About the Author

Josh Smith
Software Developer (Senior) Cynergy Systems
United States United States
Member
Josh creates software, for iOS and Windows.
 
He works at Cynergy Systems as a Senior Experience Developer.
 
Read his iOS Programming for .NET Developers[^] book to learn how to write iPhone and iPad apps by leveraging your existing .NET skills.
 
Use his Master WPF[^] app on your iPhone to sharpen your WPF skills on the go.
 
Check out his Advanced MVVM[^] book.
 
Visit his WPF blog[^] or stop by his iOS blog[^].

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   
QuestionClipToBounds challenge when zooming [modified]memberMember 193185521 Dec '08 - 10:37 
Very useful code sample, but I have the following challenge : When I allow for zooming into the image, the image itself sticks to its boudaries since I use ClipToBounds="True". My problem is that the annotations are still visible outside the boundaries...
 
Does anyone have an idea how to handle this?
 
modified on Monday, December 22, 2008 6:27 PM

QuestionCreate single instance of annotation TextBox and TextBlock?memberMark C Newman11 Dec '08 - 9:47 
Thanks for providing this demo code, I have found it very helpful.
 
I notice that each time I switch between IsInEditMode=true and IsInEditMode=false, a new instance of the TextBox or TextBlock is created.
 
Is there a way to modify your XAML so that a single TextBox and a single TextBlock is created and reused throughout the life of the ImageAnnotationControl?
 
Thanks for your help.
 
Regards,
 
Mark Newman
Generalproblem opening projectmemberTim Gee13 Nov '08 - 12:04 
It looks interesting, but I was unable to open the project. I have Visual Studio 2008 with SP1 running on Vista. I also have .NET 3.5
GeneralRe: problem opening projectmemberTim Gee13 Nov '08 - 12:16 
I meant to describe the error. It says:
"The project file '___' cannot be opened"
"The project type is not supported by this installation"
GeneralRe: problem opening projectmvpJosh Smith14 Nov '08 - 1:31 
Hi Tim,
 
I downloaded the ZIP file, extracted it, opened the SLN file, let the Visual Studio Conversion Wizard run, and the application ran properly for me. I'm using VS2008 SP1. If those steps don't work for you, I suppose you'll have to create a new WPF project, and then add in the files manually.
 
:josh:
Try Crack![^]
Sleep is overrated.

GeneralRe: problem opening projectmemberTim Gee14 Nov '08 - 3:18 
Thanks very much for checking. Maybe I don't have Studio set up as well as I thought.
QuestionDrawing annotationsmemberalanp520 May '08 - 8:28 
Great example! I was wondering if you had something written that would allow a user to dynamically draw an annotation? For example, they click and draw a rectangle that is 100px by 100px.
AnswerRe: Drawing annotationsmvpJosh Smith20 May '08 - 8:32 
Hi Alan,
 
I have not tried that out, but you might find a way to combine my work with sukram's WPF Diagram Designer[^]. If you get this working, please write an article about it here on CodeProject! Smile | :)
 
:josh:
My WPF Blog[^]
All of life is just a big rambling blog post.

GeneralRe: Drawing annotationsmemberalanp520 May '08 - 8:46 
Awesome thanks!
Generalhimemberalireza45619 Apr '08 - 9:13 
does anyone knows that how we can set image at the background of XBAP interface?!!.
 
regards
GeneralGreat Idea!memberantecedents6 Feb '08 - 5:33 
Nice work Josh!
GeneralRe: Great Idea!mvpJosh Smith6 Feb '08 - 5:34 
Thanks! This is one of my favorite WPF articles that I've written. Maybe it's just the picture of homer... Smile | :)
 
:josh:
My WPF Blog[^]
All of life is just a big rambling blog post.

GeneralSave the annotated imagememberabir1 Jan '08 - 23:59 
how do I go about Saving the annotated image
GeneralRe: Save the annotated imagemvpJosh Smith2 Jan '08 - 2:58 
I don't know since I have never tried to do it before, but I can venture a guess. You will need to use RenderTargetBitmap to draw the Image element and associated annotation adorners to disk. First I'd try just rendering the Image to the RTB and hope it draws the annotations too. If it does, you're done. If it does not, then you will need to figure out a way to loop over the annotation adorner controls and render them after you've rendered the Image element.
 
:josh:
My WPF Blog[^]
Without a strive for perfection I would be terribly bored.

GeneralRe: Save the annotated imagememberabir10 Jan '08 - 23:07 
Thanks ,
any ideas ? If i want to annotate images based on Compact Framework for smart devices? D'Oh! | :doh:
GeneralRe: Save the annotated imagemvpJosh Smith11 Jan '08 - 2:23 
abir wrote:
any ideas ? If i want to annotate images based on Compact Framework for smart devices?

 
I have no experience programming the Compact Framework. You'll have to look elsewhere for advice on that.
 
:josh:
My WPF Blog[^]
All of life is just a big rambling blog post.

GeneralRe: Save the annotated imagememberantecedents6 Feb '08 - 5:31 
You could make a simple screen capture too! Cool | :cool:
In code of course Poke tongue | ;-P
Generalvery greatmembervenkatrajagopal11 Dec '07 - 6:24 
verry nice quality idea
GeneralWho's the daddy.memberPete O'Hanlon21 Sep '07 - 4:45 
Josh - is your life just one long round of apartment hunting and WPF articles? I assume that you take sustenance from the air because you obviously don't need (or have time) to eat.
 
5 vote as usual my man.
 
Deja View - the feeling that you've seen this post before.

GeneralRe: Who's the daddy.mvpJosh Smith21 Sep '07 - 4:57 
Thanks Pete! I actually do eat. I enjoy barbecued XAML, roasted leg of Vector, and a glass of ice cold BAML fresh from a resource stream. Roll eyes | :rolleyes:
 
:josh:
My WPF Blog[^]
Without a strive for perfection I would be terribly bored.

QuestionHey, does this sample use AJAX?memberntense9918 Sep '07 - 3:23 
HAHA hey, just kidding Josh! It's me, Tom P from IG! Do you recall the good old days in support? HA, Great to see you again!
AnswerRe: Hey, does this sample use AJAX?mvpJosh Smith18 Sep '07 - 3:28 
Laugh | :laugh: When I read the title of your post my jaw dropped! Laugh | :laugh:
 
:josh:
My WPF Blog[^]
Without a strive for perfection I would be terribly bored.

GeneralNice jobmemberPatrick Sears13 Sep '07 - 14:47 
Got my 5. I like how you took Christian's problem and ran with it and created something quite reusable as a result.
 

GeneralRe: Nice jobmvpJosh Smith13 Sep '07 - 16:37 
Thanks Patrick. The problem Christian posed really piqued my interest. Generalizing a solution for a specific problem, so that it can be reused, is always an interesting challenge.
 
:josh:
My WPF Blog[^]
Without a strive for perfection I would be terribly bored.

GeneralSystem.TypeLoadException [modified]memberDaniel Strigl12 Sep '07 - 22:31 
Hi Josh!
 
When I run your sample code and click on the image I receive the following exception:
 
An unhandled exception of type 'System.TypeLoadException' occurred in PresentationFramework.dll
 
The problem occurs on line 125 in file ImageAnnotationControl.xaml.cs.
 
I am using VS2005, running on Windows XP Pro.
 
Do you have any idea what's the problem?
 
Regards,
Daniel.
--
FIND A JOB YOU LOVE, AND YOU'LL NEVER HAVE TO WORK A DAY OF YOUR LIFE. Wink | ;)

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

Permalink | Advertise | Privacy | Mobile
Web02 | 2.6.130523.1 | Last Updated 12 Sep 2007
Article Copyright 2007 by Josh Smith
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid