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

A Photoshop-like Cropping Adorner for WPF

By , 24 Jan 2008
 

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)

About the Author

darrellp
Web Developer
United States United States
Member
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.

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
QuestionUniform Resizingmemberadmdev7 Sep '12 - 9:01 
Darrellp,
 
You did an awesome job.
 
I have one question. I would like the adorner to resize uniformly. In other words, like a square.
 
I would appreciate your input on this.
 
Thanks in advance.
AnswerRe: Uniform Resizingmemberdarrellp10 Sep '12 - 7:43 
I think I'd probably subclass PuncturedRect to make Punctured Square and ensure that the CoerceRectInterior forced the width and height to be the same - probably the min of the width/height in the passed in rectangle. I think you're still going to have to fiddle with the placing of the upper left corner properly in the main code. Shouldn't be too awful hard.
GeneralRe: Uniform Resizingmemberadmdev10 Sep '12 - 17:22 
Thank you.
 
While trying the code, I noticed something. I'm loading my images onto a square image container. If I load an image, which is not square, in other words, it's hight is bigger, the adorned does not crop the image properly. It cuts one of the sides. Any ideas.
 
Thank you for your reply.
QuestionImage quality reducesmemberUday P.Singh15 Jul '12 - 21:06 
Hi, its a great article, I have implemented this in one of my projects. But I have a question when I crop the image the image quality reduces. Is there anything I can do to maintain the image quality?
AnswerRe: Image quality reducesmemberdarrellp16 Jul '12 - 3:41 
Any time you increase the size of an image you inevitably lose image quality and there's no way around it. I can only imagine that this is the degradation you're referring to. If the cropped area and the area you're displaying it in are exactly the same size there should be no degradation. This can't be eliminated, but there are ways of mitigating it using antialiasing. Look at this site: http://msdn.microsoft.com/en-us/library/k0fsyd4e.aspx[^]. Hope this helps.
GeneralMy vote of 5membercoderprime26 Jan '12 - 4:33 
Simple to understand and a great piace of work done!
QuestionDraggable crop rectangle [modified]membercoderprime26 Jan '12 - 4:32 
Thank you for the nice article!

modified 26 Jan '12 - 10:56.

QuestionWinForms Version to help with cropping.memberbennee15 Jan '12 - 16:25 
Is there a way I could use this on a winform project? Amazing work BTW! Smile | :)
AnswerRe: WinForms Version to help with cropping.memberdarrellp15 Jan '12 - 17:00 
No - not really. It uses a lot of WPF specific stuff - decorators, pens which copy from arbitrary bitmaps on a contrl, etc.. I don't think there's any good way of transferring all of that stuff over.
GeneralSuperbmemberneil bright14 Apr '11 - 22:58 
Exactly what I wanted, thanks very muchly.
 
Smile | :)
GeneralImage Stretch="Uniform " issuememberMember 467731212 Mar '11 - 10:55 
I have an issue when I set my Image container to Stretch="Uniform".
The cropped portion is not the same as what is in the selection. any ideas?
GeneralRe: Image Stretch="Uniform " issuemembersteve_hocking5 Apr '11 - 10:49 
I had the same problem. On a whim and being slightly peeved I wrapped my Image element in a Canvas element, and set the MaxHeight and MaxWidth attributes on the Image. This fixed the problem. Might be worth a try. Smile | :)
GeneralMy vote of 5memberJoe Sonderegger15 Feb '11 - 19:16 
Brilliant Work.
I have just added the functionality for Maintaining Aspect Ratio if anyone is interested...

modified on Monday, March 14, 2011 2:31 AM

GeneralRe: My vote of 5memberMember 467731212 Mar '11 - 9:28 
Yes please, that would be very useful
GeneralRe: My vote of 5 Maintaining Aspect RatiomemberJoe Sonderegger13 Mar '11 - 20:48 
Here I define a target size: Only the ratio of the two sides will be used.
 
private Size m_TargetSize = new Size(640, 480);
        public Size TargetSize
        {
            get { return m_TargetSize; }
            set { m_TargetSize = value; }
        }   
 
Here I define which edge is used.
public enum Edge
    {
        None = 0,
        Horizontal = 0x10,
        Vertical = 0x20,
 
        NotCorner = 0x100,
 
        Top = 0x01 | Vertical,
        Bottom = 0x02 | Vertical,
        Left = 0x04 | Horizontal,
        Right = 0x08 | Horizontal,
 
        TopLeft = Top | Left | NotCorner,
        TopRight = Top | Right | NotCorner,
        BottomLeft = Bottom | Left | NotCorner,
        BottomRight = Bottom | Right | NotCorner,
    };  
 

Calculate new size based on the input size and target size.
The dominiant edge is used as master.
 
public static Size CalculateSize(Size targetSize, Size input, Edge edge)
        {
            double outputWidth = 0;
            double outputHeight = 0;
            if ((edge & Edge.Horizontal) == Edge.Horizontal)
            {
                double ratio = targetSize.Height / targetSize.Width;
                outputWidth = input.Width;
                outputHeight = input.Width * ratio;
            }
 
            else if ((edge & Edge.Vertical) == Edge.Vertical)
            {
                double ratio = targetSize.Height / targetSize.Width;
                outputWidth = input.Height / ratio;
                outputHeight = input.Height;
            }
 
            return new Size(outputWidth, outputHeight);
        }
 
And here I have modified the HandleThumb function to maintain aspect ratio.
private void HandleThumb(
            double drcL,
            double drcT,
            double drcW,
            double drcH,
            double dx,
            double dy, Edge edge)
        {
            Rect rcInterior = _prCropMask.RectInterior;
 
            if (rcInterior.Width + drcW * dx < 0)
            {
                dx = -rcInterior.Width / drcW;
            }
 
            if (rcInterior.Height + drcH * dy < 0)
            {
                dy = -rcInterior.Height / drcH;
            }
 
            double differenceLeft = drcL * dx;
            double differenceTop = drcT * dy;
            double differenceWidth = drcW * dx;
            double differenceHeight = drcH * dy;
 

            rcInterior = new Rect(
                rcInterior.Left + differenceLeft,
                rcInterior.Top + differenceTop,
                rcInterior.Width + differenceWidth,
                rcInterior.Height + differenceHeight);
            Console.WriteLine(rcInterior);
            if (MaintainAspectRatio)
            {
                Edge dominantEdge = Edge.None;
 
                if ((edge & Edge.NotCorner) == Edge.NotCorner)
                    dominantEdge = Edge.Horizontal;
                else
                    dominantEdge = edge;
 
                Size newSize = CalculateSize(TargetSize, rcInterior.Size, dominantEdge);
 
                if ((edge & Edge.Top) == Edge.Top)
                {
                    double newTop = rcInterior.Top + rcInterior.Height - newSize.Height;
                    rcInterior.Location = new System.Windows.Point(rcInterior.Left, newTop);
                }
                if ((edge & Edge.Left) == Edge.Left)
                {
                    double newLeft = rcInterior.Left + rcInterior.Width - newSize.Width;
                    rcInterior.Location = new System.Windows.Point(newLeft, rcInterior.Top);
                }
 
                rcInterior.Height = newSize.Height;
                rcInterior.Width = newSize.Width;
            }
 
            _prCropMask.RectInterior = rcInterior;
            SetThumbs(_prCropMask.RectInterior);
            RaiseEvent(new RoutedEventArgs(CropChangedEvent, this));
        }
 
That's all.
I hope it helps to make this great control even better.
Have a nice life!!

GeneralRe: My vote of 5 Maintaining Aspect Ratiomemberdarrellp13 Mar '11 - 20:50 
Hey Joe! Thanks for the addition and good job!
GeneralTHIS IS AWESOME!membersong song song23 Nov '10 - 15:58 
THIS PROJECT IS AWESOME. THANK YOU SO MUCHHHHHIEEEEE Smile | :) btw, There's one bug in the project done by me that I couldn't solve. How to fix a border around the rectangle9for cropping) so that it doesn't exceed the size of the image box during zoomming?
 
Thanks!
GeneralResize on image resizememberGilad Kapelushnik1 Jan '10 - 0:48 
Hi,
 
Very nice work, got it working in a snap.
 
I have one problem though, when the image is resized the adoner stays at its own location. I've looked for a way to change the adoner size pragmatically but there aren't much public methods. As the adoner layer is part of the image I can only guess that there is an integrated way to handle the resize issue without too much hassle, any idea?
 
Gilad.
GeneralRe: Resize on image resizememberdarrellp1 Jan '10 - 6:17 
There is the AdornedElement_SizeChanged method which gets called whenever the element underneath changes size. Right now it's used to make sure that the adorner stays within the bounds of it's parent element, but I'm sure you could add some code to change the crop region however you'd like. I'm not sure what you want it to do. I think when it's over an image, if the image changes proportionately, it may make sense to change the adorner size proportionately also, but images don't necessarily change size proportionally (though, admittedly, mine does). Also, when it's over a random Framework element, it's impossible to tell how the underlying element may react to size changes (since it can override them and do whatever it likes) so in general, it's impossible to know what to do here. Perhaps the generic solution would be to make an overridable procedure for size changes and allow the user to subclass the adorner, overriding the size change routine. If I were to rewrite it to solve that problem, that's probably what I'd do. I'd rather make my code more general and then override that generality in special cases than to make it more specific for a particular special case. If you've got only one thing you'll ever use it for, though, I suppose just modifying AdornedElement_SizeChanged would work too.
GeneralRe: Resize on image resizememberGilad Kapelushnik2 Jan '10 - 1:02 
Thanks for your help.. I did the change as you suggested and attached it so maybe someone else is interested.
 
private void AdornedElement_SizeChanged(object sender, SizeChangedEventArgs e) {
            FrameworkElement fel = sender as FrameworkElement;
            Rect rcInterior = _prCropMask.RectInterior;
            bool fFixupRequired = false;
            double
                intLeft = rcInterior.Left,
                intTop = rcInterior.Top,
                intWidth = rcInterior.Width,
                intHeight = rcInterior.Height;
 
            double HeightRatio = e.NewSize.Height / e.PreviousSize.Height;
            double WidthRatio = e.NewSize.Width / e.PreviousSize.Width;
 
            if (HeightRatio != 1.0) {                
                intTop *= HeightRatio;
                intHeight *= HeightRatio;
                fFixupRequired = true;
            }
 
            if (WidthRatio != 1.0) {                
                intLeft *= WidthRatio;
                intWidth *= WidthRatio;
                fFixupRequired = true;
            }
 
            if (rcInterior.Left > fel.RenderSize.Width) {
                intLeft = fel.RenderSize.Width;
                intWidth = 0;
                fFixupRequired = true;
            }
 
            if (rcInterior.Top > fel.RenderSize.Height) {
                intTop = fel.RenderSize.Height;
                intHeight = 0;
                fFixupRequired = true;
            }
 
            if (rcInterior.Right > fel.RenderSize.Width) {
                intWidth = Math.Max(0, fel.RenderSize.Width - intLeft);
                fFixupRequired = true;
            }
 
            if (rcInterior.Bottom > fel.RenderSize.Height) {
                intHeight = Math.Max(0, fel.RenderSize.Height - intTop);
                fFixupRequired = true;
            }
            if (fFixupRequired) {
                _prCropMask.RectInterior = new Rect(intLeft, intTop, intWidth, intHeight);
            }
        }

GeneralDraggablememberverxailes4 Aug '09 - 5:37 
man, I've been studying your codes for almost a week now but I can't still figure out how I can make it draggable. Can you teach me how?
GeneralRe: Draggablememberdarrellp5 Aug '09 - 6:49 
I'm not sure what you mean by "draggable". You can drag out the corners and sides. Do you want to click in the middle and drag the cropping area around? It shouldn't be that hard - just simulate what I'm doing to translate two opposite corners and it should drag. I haven't looked at the code in a million years (despite the fact that I wrote it only a few years ago - imagine that!) but that's certainly where I'd start.
GeneralRe: DraggablememberJose Maldonado9 Mar '10 - 1:07 
I also need to make the selection "Draggable"... I have tried to implement it, but it is proving very difficult to understand. Can you help with this task? How can I RectInterior draggable?
GeneralRe: DraggablememberJose Maldonado10 Mar '10 - 1:11 
eureka!
"drag and drop" in selector is working...
 
public CroppingAdorner(UIElement adornedElement, Rect rcInit)
			: base(adornedElement)
		{
			_vc = new VisualCollection(this);
			_prCropMask = new PuncturedRect();
			_prCropMask.IsHitTestVisible = false;
			_prCropMask.RectInterior = rcInit;
			_prCropMask.Fill = Fill;
 
			_vc.Add(_prCropMask);
			_cnvThumbs = new Canvas();
			_cnvThumbs.HorizontalAlignment = HorizontalAlignment.Stretch;
			_cnvThumbs.VerticalAlignment = VerticalAlignment.Stretch;
 
			_vc.Add(_cnvThumbs);
			BuildCorner(ref _crtTop, Cursors.SizeNS);
			BuildCorner(ref _crtBottom, Cursors.SizeNS);
			BuildCorner(ref _crtLeft, Cursors.SizeWE);
			BuildCorner(ref _crtRight, Cursors.SizeWE);
			BuildCorner(ref _crtTopLeft, Cursors.SizeNWSE);
			BuildCorner(ref _crtTopRight, Cursors.SizeNESW);
			BuildCorner(ref _crtBottomLeft, Cursors.SizeNESW);
			BuildCorner(ref _crtBottomRight, Cursors.SizeNWSE);
 
			// Add handlers for Cropping.
			_crtBottomLeft.DragDelta += new DragDeltaEventHandler(HandleBottomLeft);
			_crtBottomRight.DragDelta += new DragDeltaEventHandler(HandleBottomRight);
			_crtTopLeft.DragDelta += new DragDeltaEventHandler(HandleTopLeft);
			_crtTopRight.DragDelta += new DragDeltaEventHandler(HandleTopRight);
			_crtTop.DragDelta += new DragDeltaEventHandler(HandleTop);
			_crtBottom.DragDelta += new DragDeltaEventHandler(HandleBottom);
			_crtRight.DragDelta += new DragDeltaEventHandler(HandleRight);
			_crtLeft.DragDelta += new DragDeltaEventHandler(HandleLeft);
 
            //add eventhandler to drag and drop 
            adornedElement.MouseLeftButtonDown += new MouseButtonEventHandler(Handle_MouseLeftButtonDown);
            adornedElement.MouseLeftButtonUp += new MouseButtonEventHandler(Handle_MouseLeftButtonUp);
            adornedElement.MouseMove += new MouseEventHandler(Handle_MouseMove);
 

			// We have to keep the clipping interior withing the bounds of the adorned element
			// so we have to track it's size to guarantee that...
			FrameworkElement fel = adornedElement as FrameworkElement;
 
			if (fel != null)
			{
				fel.SizeChanged += new SizeChangedEventHandler(AdornedElement_SizeChanged);
			}
		}
 
        #region Drag and drop handlers
 
        Double OrigenX;
        Double OrigenY;
 
        //  generic handler move selection with Drag'n'Drop
        private void HandleDrag(double dx, double dy)
        {
            Rect rcInterior = _prCropMask.RectInterior;
            rcInterior = new Rect(
               dx ,
               dy ,
                rcInterior.Width ,
                rcInterior.Height);
 
            _prCropMask.RectInterior = rcInterior;
            SetThumbs(_prCropMask.RectInterior);
            RaiseEvent(new RoutedEventArgs(CropChangedEvent, this));
        }
 
        private void Handle_MouseMove(object sender, MouseEventArgs args)
        {
            Image Marco = sender as Image;
            if (Marco != null && Marco.IsMouseCaptured)
            {
                Double x = args.GetPosition(Marco).X; //posición actual cursor
                Double y = args.GetPosition(Marco).Y;
                Double _x = _prCropMask.RectInterior.X; // posición actual esquina superior izq del marco interior
                Double _y = _prCropMask.RectInterior.Y;
                Double _width = _prCropMask.RectInterior.Width; //dimensiones del marco interior
                Double _height = _prCropMask.RectInterior.Height;
 
                //si el click es dentro del marco interior
                if (((x > _x) && (x < (_x + _width))) && ((y > _y) && (y < (_y + _height))))
                {
                    //calculamos la diferencia de la posición actual del cursor con respecto al punto de origen del arrastre
                    //y se la añadimos a la esquina sup. izq. del marco interior.
                    _x =_x + ( x - OrigenX);
                    _y = _y + (y - OrigenY );
 
                    //comprobamos si es posible mover sin salirse del marco exterior por ninguna de sus dimensiones
                    //no supera el borde izquierdo de la imagen: !(_x < 0)
                    if (_x < 0) 
                    {
                        _x = 0;
                    }
                    //no supera el borde derecho de la imagen: !((_x + _width) > Marco.Width)
                    if ((_x + _width) > Marco.ActualWidth)
                    {
                        _x = Marco.ActualWidth - _width;
                    }
                    //no supera el borde superior de la imagen: !(_y<0)
                    if (_y < 0)
                    {
                        _y = 0;
                    }
                    //no supera el borde inferior de la imagen: !((_y + _height) > Marco.Height)
                    if ((_y + _height) > Marco.ActualHeight)
                    {
                        _y = Marco.ActualHeight - _height;
                    }
                    
                    //asignamos nuevo punto origen del arrastre y movemos el marco interior
                    OrigenX =  x;
                    OrigenY = y;
                    HandleDrag(_x, _y );
                    
                }
            }
 
        }
          
        private void Handle_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            Image Marco = sender as Image;
            if (Marco != null)
            {
                Marco.CaptureMouse();
                OrigenX = e.GetPosition(Marco).X; //iniciamos las variables en el punto de origen del arrastre
                OrigenY = e.GetPosition(Marco).Y;
            }
        }
 
        private void Handle_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            Image Marco = sender as Image;
            if (Marco != null)
            {
                Marco.ReleaseMouseCapture();
            }
        }
        #endregion
 

QuestionHas anyone added a border to this?memberkidpurple9918 Jun '09 - 4:41 
I'm trying to add a border around the crop zone but I don't see exactly where this would fit into the code. My first idea was to create a border and add it to _vc inside the CroppingAdorner constructor. That didn't quite work.
 
Has anyone done this? Where did you add the border?
AnswerRe: Has anyone added a border to this?memberdarrellp18 Jun '09 - 8:49 
See my second response to SHartmann's question below. It tells precisely how to add a border.
GeneralMoving the selectionmemberrickengle16 Oct '08 - 16:36 
This is a terrific solution and one I'd actually been looking to write since I've seen this style selection in other graphics applications.
Do you know how you would enhance yuor code so that the selection window could be moved?
Since a selection is almost never perfectly aligned its great to be able to nudge it around to get it perfectly positioned.
Any ideas how to mod your code for this enhancement?
 
Thanks for this great sample!
Rick
QuestionUsing in 3D with Viewport2DVisual3DmemberSHartmann3 Aug '08 - 22:08 
Hello,
 
when I use this component in 2D mode everything is fine.
When I want to use it in 3D using Viewport2DVisual3D it crashes.
Doesn't Viewport2DVisual3D support adorner layer?
In the following code (proc AddCropToElement)
AdornerLayer aly = AdornerLayer.GetAdornerLayer(fel);
the adorner layer aly is allways null ...
 
best regards
Stephan
AnswerRe: Using in 3D with Viewport2DVisual3Dmemberdarrellp3 Aug '08 - 22:45 
Man - you're giving this thing a workout! That's a good thing, though, so thanks!
 
Don't really know on this one. If I was guessing I'd guess RenderTargetBitmap() which is supposed to render anything in a control. Perhaps it doesn't like the straight Direct3D output that comes from the 3D stuff in WPF. I haven't messed around enough with 3D yet to know. That's really the place where we try to deal with the pixels on the underlying control and in fact, about the only place we deal with the underlying control at all. Other than that, it's a matter of drawing the cropping shape right, dealing with the thumbs and manipulating the bitmap that gets returned from RenderTargetBitmap().
 
Figuring out all the details on 3D is definitely something I want to do. Right now, though, it's a task I have in my horizon but I'm doing some other stuff which keeps me away from it for the moment. If you figure anything out, please let me know!
AnswerRe: Using in 3D with Viewport2DVisual3Dmemberdarrellp3 Aug '08 - 22:51 
By the way, are you using this for a "real" project? Of course, that's fine - I was just curious. If it's not a problem with revealing trade secrets, it'd be kinda cool to find out how it's being used. Of course, if there are trade secrets that need to be kept, I don't want anyone to spill the beans.
GeneralRe: Using in 3D with Viewport2DVisual3DmemberSHartmann3 Aug '08 - 23:06 
I'm trying to use it in a "real" project, but for the moment it's more a playground to see what is possible with WPF, there is a lot to learn Wink | ;)
 
I found a work around for my problem, if I don't get an adorner layer I add the CroppingAdorner direct to children of my working canvas. This is no universal solution, but it fits my needs Wink | ;)
 

best regards
Stephan
Generalanother enhancement proposalmemberSHartmann27 Jul '08 - 15:56 
I also linked to the Marching Ants solution here
http://www.codeproject.com/KB/WPF/wpfmarchingants.aspx[^]
in my last message.
 
I think it would be great to merge the functionality of this project with your solution.
 
the other project does the first part:
a user wants to select a region. he starts in the one corner and moves with pressed mouse button to the opposite corner.
Your solution starts here, when an area is selected you can easily resize it, that is something the other solution is missing.
 
I also like the way you put it together in the adorner Smile | :)
Is it possible to enhance your solution with the missing parts?
GeneralRe: another enhancement proposalmemberSHartmann27 Jul '08 - 17:46 
I still belive enhancing component is a good thing,
but I found a workaround where I can live with (at least for the moment)
 
If you set the size of the rectangle in the creator to 0, all thumbs are placed on a single point. When you click again you get the lower right thumb and can begin with sizing.
 
This is not exactly the typical behaviour of marking something, but not bad at all Smile | :)
Generalgreat, but questionmemberSHartmann27 Jul '08 - 15:41 
Hi,
 
this is a really great article, but I have a question.
I tried to enhance your control with marchings ants
(taken from this article http://www.codeproject.com/KB/WPF/wpfmarchingants.aspx[^] )
 
In the constructor of CroppingAdorner I added this code:
_prCropMask.StrokeThickness = 1;
_prCropMask.StrokeDashOffset = 0;
_prCropMask.Stroke = new SolidColorBrush(Colors.Black);
_prCropMask.StrokeDashArray = new DoubleCollection();
_prCropMask.StrokeDashArray.Add(5);
DoubleAnimationUsingKeyFrames ani = new DoubleAnimationUsingKeyFrames();
ani.RepeatBehavior = RepeatBehavior.Forever;
ani.KeyFrames.Add(new LinearDoubleKeyFrame(0, KeyTime.FromTimeSpan (TimeSpan.FromSeconds(0))));
ani.KeyFrames.Add(new LinearDoubleKeyFrame(10, KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(500))));
_prCropMask.BeginAnimation(Shape.StrokeDashOffsetProperty, ani);
 
It works fine with a little bug.
the marchings ants are not only around the selected area, but also around the whole (greyed out) area.
Is there any chance to fix this?
GeneralRe: great, but questionmemberdarrellp27 Jul '08 - 16:38 
I'll take a look. For my tastes, I'd rather have the Photoshop like version without the ants - I think they'd be a bit distracting in this situation and as it is, it highlights pretty much what you want to see so it's not like the ants would really mark anything out that's not already plainly visible. That's only my opinion, though. I'll take a quick look but it's been a long time since I did this so I can't guarantee anything.
 
Darrell
GeneralRe: great, but questionmemberdarrellp27 Jul '08 - 16:59 
I think it will work if you replace the rcExterior definition in ArrangeOverride with the following:
 
Rect rcExterior = new Rect(-1, -1, AdornedElement.RenderSize.Width + 3, AdornedElement.RenderSize.Height + 3);
 
This makes the punctured rect that contains the mask a little larger than the adorned item so that the border on the outside is clipped off. I'm not sure why I had to add 3 to the width - seems like it ought to have been 2. Perhaps because of antialiassing within WPF or something.
 
I've done zero testing on this - just threw it in to my test project and it seemed to work.
GeneralRe: great, but questionmemberSHartmann27 Jul '08 - 17:17 
I also tested it in my project, worked fine Smile | :)
sometimes the simplest solutions are the best Laugh | :laugh:
GeneralRe: great, but questionmemberdarrellp27 Jul '08 - 17:27 
Good. Not sure that there really is another better solution - at least not if you plan to implement the marching ants as a "border" on the punctured rect. You could try adding a transparent rect with the marching ants border as another element to the adorner. That would probably work also, but I think this solution is simpler and very possibly, more efficient since it doesn't increase the size of the visual elements in the adorner. Glad it worked for you.
Generalquestionmemberanamikaahirwar15 Jul '08 - 20:13 
hi
i want 2 ask that how this croped image convert into matrix form.
GeneralRe: questionmemberdarrellp15 Jul '08 - 21:55 
I think you're asking how I converted the cropped image into individual pixels. The answer is...I didn't. Look at the code for BpsCrop() listed above. I use RenderTargetBitmap to render the adorned item into a WPF bitmap (no conversion to individual pixels here) and then use CroppedBitmap (in the return statement) to actually crop out the part of the bitmap I want. This returns a BitmapSource and again - no direct conversion to a matrix of individual pixels.
 
So I never actually convert to a matrix of pixels, however I'm only one step away. You can call the CopyPixels() member function on BitmapSource and it will give you the matrix you're looking for. So essentially, call CopyPixels on the return value from the code in BpsCrop() and you'll get the matrix of the individual pixels which is (I believe) what you're looking for.
 
Darrell
GeneralGreat Workmemberaquamoon24 Apr '08 - 5:41 
Hello
 
this is great work. I noticed in the
private Point UnitsToPx(double x, double y)
{
return new Point((int)(x * s_dpiX / 96), (int)(y * s_dpiY / 96));
}
Should it not be
private Point UnitsToPx(double x, double y)
{
return new Point((int)(x * (image.Source as WriteableBitmap).DpiX / 96), (int)(y * (image.Source as WriteableBitmap).DpiY / 96));
}
since in wpf the size of the image drawn is determined by the image dpi
 
V
GeneralRe: Great Workmemberdarrellp25 Apr '08 - 20:17 
Thanks for the kind words.
 
I've gotta say, if any part of this stuff will drive you batty it's the conversions to and from pixels and determining which types of which pixels are being used. Sadly, WPF is so concentrated on eliminating pixels from their worldview that they give them very short shrift any time they inadvertently allowed them to turn up in the docs. I had to write a separate test app just to infer how CroppedBitmap and RenderTargetBitmap really work. What's that got to do with the question, you ask?
 
Well, it gives me some small excuse to say that it's been long enough that I'm not 100% sure regarding the answer to your question, but I believe that it's correct as is since I think we're really working at all times with screen bitmaps as returned by "CroppedBitmap". That's why we work fine with vector controls as well as bitmaps if we so desire. Those screen bitmaps, I think (and "think" is the operative word here) will all use the s_dpiX/s_dpiY dots per inch so in the adorner we're not really working directly with the original bitmaps but with their representation on the screen.
 
I believe if I recall correctly, I had bitmaps with different DPIs and while there was something funny about them in my test app, it all works out in the adorner. Like I say, though, you kind of hit me in my soft spot there, especially after a few months of happily avoiding the subject, so I wouldn't stake my life on it. If it comes up again, I daresay that I'll probably have to rewrite the same test app to recall all the maddening little details about how all that stuff works together.
 
Thanks again for the response!
 
Darrell

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

Permalink | Advertise | Privacy | Mobile
Web02 | 2.6.130516.1 | Last Updated 24 Jan 2008
Article Copyright 2008 by darrellp
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid