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

WPF 的 Photoshop 式裁剪装饰器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (23投票s)

2008年1月24日

CPOL

6分钟阅读

viewsIcon

200073

downloadIcon

8019

一个裁剪装饰器,使除选定部分之外的所有内容变暗。

引言

市面上已经有一些关于裁剪的好文章了,那为什么还要写一篇呢? 我喜欢 Photoshop 中裁剪的一个很酷的地方是,图像的重要部分(裁剪后留下的部分)是未被遮挡的,而不重要的部分(将被丢弃的部分)是被遮挡的部分。这使得更容易看到最终结果。到目前为止,我看到的裁剪器都做的恰恰相反。这样更容易,而且例如,可能是处理事情的最佳方式,但我真的想做一个有用的裁剪器,所以我的裁剪器工作方式是类似 Photoshop 的。其他一些裁剪器(事实上,据我所知,所有裁剪器)并非作为通用的装饰器,而是作为构建在特定位图之上的专用程序。我想要一个可以裁剪任何东西的东西,包括 WPF 中的容器和控件,所以我将我的裁剪器作为一个通用的装饰器,并生成一个位图,显示装饰器下方的任何内容。

WPF 在矢量图形方面很出色,但处理位图可能更棘手。其他一些裁剪器甚至写出了临时文件来生成它们裁剪的位图。我使用了 RenderTargetBitmapCroppedBitmap 在内存中非常快速地完成这项工作,这样在您操作裁剪区域时,裁剪的部分就可以交互式地显示出来。

我包含了一个用于裁剪区域变化的路由事件,颜色是标准的 WPF 依赖属性。该裁剪器也可以在不检索下方位图的情况下使用,以指示位图的一部分,甚至是在容器中,尽管裁剪似乎是正常的使用方式。最后,它还包含一个作为奖励的 PuncturedRect 形状,它只是一个带有中间打孔的矩形。

形状、装饰器和其他琐事

此包中有两个可能可用的产品 - PuncturedRect,一个用于裁剪遮罩的自定义形状,以及 CroppingAdorner,实际的装饰器。

起初,自定义形状听起来没什么特别的。毕竟,您总是可以使用路径创建任何类型的形状。确实如此,但如果您有一个通用的形状类,将其功能打包到自定义形状中会比不断地用一系列路径重新创建它们要好得多。更重要的是,通过创建自定义形状,您可以公开依赖属性,并在 XAML 和数据绑定中使用它们,这会更容易。PuncturedRect 是一个相当简单的形状,也许可以作为自定义形状和形状创建的一个好例子。它公开了两个依赖属性,ExteriorRect InteriorRect。结果是 ExteriorRect 给出的矩形,并由 InteriorRect 给出的矩形在其中“打孔”。在 CroppingAdorner 中,此形状用于裁剪遮罩。虽然它在这方面很好地发挥了作用,但它在外部的使用可能更多地是教学性的,而不是实际产品中有用的。尽管如此,它仍有可能被用作围绕底层控件/图像的框架。

然而,本文的主要特色是 CroppingAdorner。这是我写的第一款装饰器,我在实现过程中学到了很多。虽然它表面上与经典的缩放装饰器相似,而经典的缩放装饰器在这篇文章中有很好的概述(我想感谢其作者,但该作者在博客条目中仅列为“我”),但实际上它却大不相同,也更难。由于在调整缩放容器中的拇指(thumb)时会导致对所装饰元素的缩放( duh),因此它们也会导致该元素进行新的布局。由于拇指是在布局阶段放置的,因此可以可靠地定位它们。这就是问题的关键。在裁剪装饰器中,调整拇指的移动不会导致新的布局调用,因此在布局时无法通过挂钩来交互式地移动拇指,但我们通常不能简单地将拇指放置在所装饰控件的“任何地方”。WPF 会决定控件的位置。在排列阶段之外,您唯一可以随意设置控件位置的地方是画布。因此,我没有将所有拇指都放在装饰器的视觉树中,而是将一个不可移动的画布放在树中,并将所有拇指都放在画布上。

因此,装饰器实际上是一个控件,其视觉树中有两个子元素 - PuncturedRect 形成遮罩,以及包含所有拇指的画布。一旦您意识到需要这样设置,显示控件的其余部分就相当直接了。

Using the Code

如上所述,装饰器由其视觉树中的两个子元素组成。第一个是表示裁剪遮罩的 PuncturedRect,第二个是包含拇指的画布。拇指有一个单独的类 CropThumbs,它派生自 Thumb,主要设置拇指的外观。它们的行为未从 Thumb 修改。当移动拇指时,它会生成一条消息,指示拇指移动的增量。可以通过简单地将此增量的倍数加到矩形边上来安排裁剪矩形的移动。因此,特定拇指的行为可以通过它添加到这些边的倍数来表征。ThumbMultipliers 结构旨在保存这些倍数。每个拇指在其标签中存储一个 ThumbMultipler。例如,右上角的拇指有一个 ThumbMultiplier (0, 1, 1, -1),这意味着它的 x 增量应乘以 0 并加到左侧,其 y 增量应乘以 1 并加到顶部,其 x 增量应乘以 1 并加到宽度,其 y 增量应乘以 -1 并加到高度。通过这样做,我们可以在一个引用此标签的处理程序中处理所有拇指的移动。

private void HandleThumb(object sender, DragDeltaEventArgs args)
{
    CropThumb crt = sender as CropThumb;
    if (crt != null)
    {
        Rect rcCrop = _prCropMask.RectInterior;
        ThumbMultipliers tmlt = (ThumbMultipliers)crt.Tag;

        rcCrop = tmlt.Apply(rcCrop, args.HorizontalChange, args.VerticalChange);

        // Reflect new cropping rectangle in mask
        _prCropMask.RectInterior = rcCrop;

        // Reflect new cropping rectangle in thumb positions
        SetThumbs(_prCropMask.RectInterior);

        // Alert anybody who might be interested
        RaiseEvent(new RoutedEventArgs(CropChangedEvent, this));
    }
}

CropAdorner 上除构造函数外唯一的 public 方法是实际提取代表裁剪区域的 BitmapSource 的例程。为了做到这一点,我们使用 RenderTargetBitmap 检索所装饰元素的位图。我们需要知道 RenderTargetBitmap 的宽度和高度(以像素为单位),因此我们需要将 WPF 单位转换为像素。一旦我们检索到一个包含所装饰元素位图图像的 RenderTargetBitmap,我们就需要提取我们感兴趣的裁剪部分。我们为此使用 CroppedBitmap 对象。CroppedBitmap 也需要矩形的像素坐标。CroppedBitmap 派生自 BitmapSource,因此我们可以将其作为最终结果返回。实现这一切的方法如下所示

public BitmapSource BpsCrop()
{
    Thickness margin = AdornedElementMargin();
    Rect rcInterior = _prCropMask.RectInterior;

    // It appears that CroppedBitmap indexes from the upper left of the margin 
    // whereas RenderTargetBitmap renders the
    // control exclusive of the margin.  
    // Hence our need to take the margins into account here...

    Point pxFromPos = UnitsToPx(rcInterior.Left + 
                margin.Left, rcInterior.Top + margin.Top);
    Point pxFromSize = UnitsToPx(rcInterior.Width, rcInterior.Height);
    Point pxWhole = UnitsToPx(AdornedElement.RenderSize.Width + 
            margin.Left, AdornedElement.RenderSize.Height + margin.Left);
    pxFromSize.X = Math.Max(Math.Min(pxWhole.X - pxFromPos.X, pxFromSize.X), 0);
    pxFromSize.Y = Math.Max(Math.Min(pxWhole.Y - pxFromPos.Y, pxFromSize.Y), 0);
    if (pxFromSize.X == 0 || pxFromSize.Y == 0)
    {
        return null;
    }
    System.Windows.Int32Rect rcFrom = new System.Windows.Int32Rect
            (pxFromPos.X, pxFromPos.Y, pxFromSize.X, pxFromSize.Y);

    RenderTargetBitmap rtb = new RenderTargetBitmap
            (pxWhole.X, pxWhole.Y, s_dpiX, s_dpiY, PixelFormats.Default);
    rtb.Render(AdornedElement);
    return new CroppedBitmap(rtb, rcFrom);
}

安装装饰器比您想象的要棘手一些。首先,装饰器存在于无人居住的地方——在 AdornerLayer 对象中。AdornerLayer 对象是不可见的,它们位于被装饰的项之上。为了装饰一个对象,您必须找到它的 AdornerLayer 并在那里安装装饰器。代码如下所示

AdornerLayer aly = AdornerLayer.GetAdornerLayer(fel);
_crp = new CroppingAdorner(fel, rcInterior);
aly.Add(_crp);

CroppingAdorner 还提供了一个路由事件 CropChanged,每当裁剪区域发生变化时就会触发,还有一个只读属性 ClippingRectangle,它提供了当前的裁剪矩形。

最终,理解如何使用装饰器的最好方法是查看测试项目中的代码。

历史

  • 2008 年 1 月 24 日:首次提交
© . All rights reserved.