在 WPF 中注解图像






4.89/5 (41投票s)
演示如何向 Image 元素添加文本注解

引言
本文介绍了如何在 WPF 应用程序中创建图像上的文本注释。该技术涉及在 `Image` 元素的附加层中渲染自定义控件,从而允许就地编辑注释。演示应用程序中使用的类可以轻松免费地在其他应用程序中使用,以实现相同的功能。
背景
当你阅读报纸并在页面上涂鸦写下想法时,你就是在创建注释。术语“注释”是指描述或解释另一文档一部分的笔记。Windows Presentation Foundation 内置了对文档注释的支持,如此处所述。但是,它并没有提供开箱即用的注释图像的功能。
不久前,我写了一篇博客文章,介绍了如何注释一个碰巧位于 `Viewbox` 中的 `Image` 元素。本文将这一想法推广,使其可以注释任何 `Image`,而不仅仅是包含在 `Viewbox` 中的。本文演示应用程序中看到的另一个改进是,注释是“就地”创建的,而不是在用户界面的其他地方的 `TextBox` 中键入注释文本。
演示应用程序
本文附带一个演示应用程序,可在本页顶部下载。该演示应用程序允许您在两个图像上创建注释。它包含有关如何创建、修改和删除注释的说明性文本。
这是演示应用程序的屏幕截图,已创建一些注释:

请注意各种注释相对于图片中实体的位置。当窗口变小时,您将看到注释仍然“固定”在那些实体上。

即使 `Image` 元素的尺寸发生了变化,注释仍然保留在图片中具有意义的相同位置。这是图像注释的一个重要方面,因为注释的位置与其文本一样有意义。
演示应用程序允许用户通过多种方式删除注释。如果注释失去了输入焦点且没有文本,它会自动删除。此外,除了上面显而易见的“删除注释”按钮外,您还可以通过右键单击注释来弹出上下文菜单来删除注释。例如:

限制
演示应用程序不是一个“完整”的解决方案。它不提供在应用程序运行时持久化注释的任何方法。我没有编写注释持久化代码,因为该功能可能有多种使用方式,编写我自己的实现似乎是盲目摸索。不过,我确实尝试以一种可以轻松实现保存和加载注释的方式编写类。
演示应用程序也不提供任何花哨的 UI 功能,如注释的拖放。这可能是一个有用的功能,但我希望保持简单。在 WPF 中进行拖放的功能在网上文档记录得很完善,因此如果您需要添加该功能,应该能够找到一些不错的参考资料。
工作原理
有四个主要参与者,如下所示:

`ImageAnnotationControl` 是您实际在屏幕上看到的内容,它显示注释并允许您编辑注释。`ImageAnnotationControl` 是一个 `ContentControl`,它公开了一个有趣的公共依赖项属性,称为 `IsInEditMode`。当该属性为 `true` 时,`DataTemplate` 会应用于 `ContentTemplate` 属性,该属性将注释文本呈现为 `TextBox`。当 `IsInEditMode` 为 `false` 时,注释文本将呈现为 `TextBlock`。`ImageAnnotationControl` 的完整 XAML 如下所示:
<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` 是一个附加控件,负责托管 `ImageAnnotationControl` 的实例。它被添加到被注释的 `Image` 的附加层中。`ImageAnnotationAdorner` 由 `ImageAnnotation` 类创建和定位。该类没有视觉表示,但仅作为注释的句柄提供给消费者(即演示应用程序的主 `Window`)。
创建 `ImageAnnotation` 时,它会在被注释的 `Image` 的附加层中安装一个附加控件,如下所示:
void InstallAdorner()
{
if (_isDeleted)
return;
_adornerLayer = AdornerLayer.GetAdornerLayer(_image);
_adornerLayer.Add(_adorner);
}
当 `Image` 元素调整大小时,如果注释必须移动到新位置,则会调用 `ImageAnnotation` 中的这些方法。
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);
}
`_horizPercent` 和 `_vertPercent` 字段表示注释在图片上的相对位置。这些值在 `ImageAnnotation` 构造函数中计算,如下所示:
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();
}
当用户单击 `Image` 时,演示应用程序中的 `Window` 会要求 `ImageAnnotation` 创建自己的实例。除了告知注释在 `Image` 上的应存在位置外,它还为 `ImageAnnotationControl` 指定了两个 `Style`,如下所示:
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);
}
这两个 `Style` 对象允许注释消费者指定注释在编辑模式和显示模式下的呈现方式。演示应用程序的 `Style` 存在于主 `Window` 的资源中,如下所示:
<!-- 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>
修订历史
- 2007 年 9 月 12 日 – 文章创建完成