Introduction
There are already some good articles on clipping out there, so why another? One of the really cool things I like about cropping in Photoshop is that the important part of the image (the part left after the crop) is the unobscured portion and the unimportant part (the part to be discarded) is the obscured portion. This makes it easier to see what the final result will be. The croppers I've seen so far do the precise opposite of this. It's easier that way and for example purposes, probably the best way to handle things, but I really wanted to make a useful cropper so I made mine work ala Photoshop. Some of the other croppers (in fact, all as far as I know) work not as general adorners, but as special purpose programs built on top of a specific bitmap. I wanted something that would clip anything, including containers and controls in WPF so I made my cropper as a general adorner and produce a bitmap of whatever is beneath the adorner.
WPF is great at vector graphics, but dealing with bitmaps can be much more tricky. Some of the other croppers have gone to the extent to write out temporary files to produce their cropped bitmaps. I've used the RenderTargetBitmap
and CroppedBitmap
to do this very quickly in memory so that the cropped portion can be shown interactively as you manipulate the cropping region.
I've included a routed event for the clipping area changing and the color is a standard WPF dependency property. The clipper could also be used without ever retrieving the bitmap underneath it to indicate a portion of a bitmap or even in a container, although it would seem like the cropping would be the normal usage. Finally, it includes as a bonus a PuncturedRect
shape which is just a rect with another rect poked out of the middle.
Shapes, Adorners and Other Trivia
There are two potentially usable products in this package - PuncturedRect
, a custom shape used for the masking portion of the clip, and CroppingAdorner
, the actual adorner.
Initially, a custom shape sounds like nothing special. After all, you can always create shapes of any sort by using paths. True enough, but if you've got a general class of shapes, it can be much nicer to package the functionality into a custom shape than to constantly reform them from a series of paths. More importantly, by creating custom shapes, you can expose dependency properties and use them much more easily in XAML and data bindings. PuncturedRect
is a fairly simple shape and perhaps serves as a good example of a custom shape and shape creation. It exposes two dependency properties, ExteriorRect
and InteriorRect
. The result is a rectangle given by ExteriorRect
with a hole "punctured" in it by the rectangle given in InteriorRect
. In the CroppingAdorner
, this shape is used for the cropping mask. While it served its purpose well in that regard, its usage outside that is probably more pedagogical than useful in actual products. Still, it could potentially be used, for instance, as a frame around underlying controls/images.
The main feature of this article, however, is the CroppingAdorner
. It's the first adorner I've written and I've learned much in its implementation. While it superficially resembles the classic resizing adorner as outlined very well in this article (whose author I would like to credit, but is listed only as "Me" in the blog entries), it actually turned out to be much different and much more difficult. Since any manipulation of the thumbs in the resizing container causes a resize on the adorned element (duh) they also result in a new layout for that element. Since the thumbs are placed during this layout phase, they can be reliably positioned. Therein lies the rub. In the cropping adorner, manipulating the thumbs does not cause a new call to layout the control, hence no hook at layout time suffices to move the thumbs interactively, but we can't normally just direct the thumbs to place themselves "just anywhere" on the adorned control. WPF decides where the controls go. The only place you can set control positions where you like outside of the arrangement phase is on a canvas. Consequently, instead of having all the thumbs be in the visual tree of the adorner, I placed a single unmoving canvas in the tree and placed all the thumbs on the canvas.
Thus, the adorner is actually a control with two children in its visual tree - the PuncturedRect
which forms the mask and the canvas which all the thumbs sit on. Once you realize that this is the way things need to be set up, the rest of displaying the control is pretty straightforward.
Using the Code
As discussed above, the adorner is composed of two children in its visual tree. The first is the PuncturedRect
representing the crop mask and the second is the canvas which contains the thumbs. There is a separate class for the thumbs called CropThumbs
and derived from Thumb
which mainly sets the appearance of the thumbs. Their behavior is unmodified from Thumb
. When a thumb is moved, it produces a message which gives the delta by which the thumb was moved. Manipulation of the cropping rectangle can be arranged by simply adding multiples of this delta to the sides of the rectangle. Thus, the behavior for a particular thumb can be characterized by the multiples it adds to each of these sides. The ThumbMultipliers
structure is designed to hold these multiples. Each thumb stores a ThumbMultipler
in its tag. For instance, the top right thumb has a ThumbMultiplier
of (0, 1, 1, -1)
meaning that its x
delta should be multiplied by 0
and added to the left side, its y
delta should be multiplied by 1
and added to the top, its x
delta should be multiplied by 1
and added to the width and its y
delta should be multiplied by -1
and added to the height. By doing this, we can handle all the thumb movements in one handler which references this tag:
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);
_prCropMask.RectInterior = rcCrop;
SetThumbs(_prCropMask.RectInterior);
RaiseEvent(new RoutedEventArgs(CropChangedEvent, this));
}
}
The only public
method on the CropAdorner
other than its constructor is the routine to actually extract a BitmapSource
representing the crop area. In order to do this, we retrieve a bitmap of the adorned element using a RenderTargetBitmap
. We need to know the width and height of the RenderTargetBitmap
in pixels, so we need to convert from WPF units to pixels. Once we have retrieved a RenderTargetBitmap
with the adorned element's bitmap image, we need to extract the cropped part we're interested in. We use the CroppedBitmap
object for this. CroppedBitmap
needs also to have pixel coordinates for the rectangle it's going to crop. CroppedBitmap
derives from BitmapSource
so we can return it as our final result. The method to achieve all this is given below:
public BitmapSource BpsCrop()
{
Thickness margin = AdornedElementMargin();
Rect rcInterior = _prCropMask.RectInterior;
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);
}
Adorners are a bit trickier to install than you might think. For one thing, adorners live where nobody else lives - in AdornerLayer
objects. The AdornerLayer
objects are invisible things that sit above the item being adorned. In order to adorn an object, you have to find its AdornerLayer
and install the adorner there. The code looks like this:
AdornerLayer aly = AdornerLayer.GetAdornerLayer(fel);
_crp = new CroppingAdorner(fel, rcInterior);
aly.Add(_crp);
CroppingAdorner
also supplies a routed event, CropChanged
which fires whenever the cropping area changes, and a readonly property ClippingRectangle
which gives the current clipping rectangle.
Ultimately, the best way of understanding how to use the adorner is to check out the code in the test project.
History
- 24th January, 2008: Initial submission