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

A Photoshop-like Cropping Adorner for WPF

, 24 Jan 2008 CPOL
Rate this:
Please Sign up or sign in to vote.
A cropping adorner which darkens everything except the selected portion

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);

        // 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));
    }
}

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;

    // 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);
}

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

License

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

Share

About the Author

darrellp
Web Developer
United States United States
I've been doing programming for close to 30 years now. I started with C at Bell Labs and later went to work for Microsoft for many years. I currently am a partner in Suckerpunch LLC where we produce PlayStation games (www.suckerpunch.com).
 
I am mainly interested in AI, graphics and more mathematical stuff. Dealing with client/server architectures and business software doesn't do that much for me. I love math, computers, hiking, travel, reading, playing piano, fruit jars (yes - I said fruit jars) and photography.

Comments and Discussions

 
Questionnot working when adding another crop rectangle to the code PinmemberMember 1088570316-Aug-14 7:42 
GeneralMy vote of 5 Pinmemberkarthikin5-May-14 6:07 
QuestionIs it possible to move the adorners? [modified] Pinmemberkarthikin5-May-14 2:17 
QuestionAwesome stuff! PinmemberSiddhartha S.25-Apr-14 19:12 
QuestionUniform Resizing Pinmemberadmdev7-Sep-12 10:01 
AnswerRe: Uniform Resizing Pinmemberdarrellp10-Sep-12 8:43 
GeneralRe: Uniform Resizing Pinmemberadmdev10-Sep-12 18:22 
QuestionImage quality reduces PinmemberUday P.Singh15-Jul-12 22:06 
AnswerRe: Image quality reduces Pinmemberdarrellp16-Jul-12 4:41 
GeneralMy vote of 5 Pinmembercoderprime26-Jan-12 5:33 
QuestionDraggable crop rectangle [modified] Pinmembercoderprime26-Jan-12 5:32 
QuestionWinForms Version to help with cropping. Pinmemberbennee15-Jan-12 17:25 
AnswerRe: WinForms Version to help with cropping. Pinmemberdarrellp15-Jan-12 18:00 
GeneralSuperb Pinmemberneil bright14-Apr-11 23:58 
Exactly what I wanted, thanks very muchly.
 
Smile | :)
GeneralImage Stretch="Uniform " issue PinmemberMember 467731212-Mar-11 11:55 
GeneralRe: Image Stretch="Uniform " issue Pinmembersteve_hocking5-Apr-11 11:49 
GeneralMy vote of 5 PinmemberJoe Sonderegger15-Feb-11 20:16 
GeneralRe: My vote of 5 PinmemberMember 467731212-Mar-11 10:28 
GeneralRe: My vote of 5 Maintaining Aspect Ratio PinmemberJoe Sonderegger13-Mar-11 21:48 
GeneralRe: My vote of 5 Maintaining Aspect Ratio Pinmemberdarrellp13-Mar-11 21:50 
GeneralTHIS IS AWESOME! Pinmembersong song song23-Nov-10 16:58 
GeneralResize on image resize PinmemberGilad Kapelushnik1-Jan-10 1:48 
GeneralRe: Resize on image resize Pinmemberdarrellp1-Jan-10 7:17 
GeneralRe: Resize on image resize PinmemberGilad Kapelushnik2-Jan-10 2:02 
GeneralDraggable Pinmemberverxailes4-Aug-09 6:37 

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

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web01 | 2.8.141216.1 | Last Updated 24 Jan 2008
Article Copyright 2008 by darrellp
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid