65.9K
CodeProject 正在变化。 阅读更多。
Home

在 WPF 中注解图像

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (41投票s)

2007年9月12日

CPOL

4分钟阅读

viewsIcon

289056

downloadIcon

5401

演示如何向 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 日 – 文章创建完成
© . All rights reserved.