I'm trying to extend crop control from CodeProject with ability to choose image from disk, display it with Stretch='Uniform'
and ability to resize crop area with aspect ratio.
I've done all the modifications, but I have a problem - i must load same image twice to get ActualWidth
of Image control.
I've searched over SO (Why are ActualWidth and ActualHeight 0.0 in this case?) for solutions, but I wasn't able to get this working.
Below is my full code:
windows.xaml:
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="CroppingTest.WndCroppingTest"
Title="CroppingTest"
Width="900" Height="600" Background="OliveDrab"
SizeChanged="Window_SizeChanged" Loaded="WndCroppingTest_OnLoaded"
>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="10"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid HorizontalAlignment="Center" VerticalAlignment="Center">
<Grid>
<Rectangle Fill="White">
<Rectangle.Effect>
<DropShadowEffect Opacity="0.5" />
</Rectangle.Effect>
</Rectangle>
<Image x:Name="Crop" Stretch="Uniform" VerticalAlignment="Center" HorizontalAlignment="Center" />
</Grid>
</Grid>
<StackPanel HorizontalAlignment="Left" VerticalAlignment="Stretch" Width="Auto" Height="Auto" Grid.Column="2">
<StackPanel.Resources>
<Style TargetType="CheckBox">
<Setter Property="Margin" Value="5,5,5,5"/>
</Style>
</StackPanel.Resources>
<Image x:Name="Preview" Width="130" Height="100" Margin="0,5,5,0"/>
<Button Content="Open" HorizontalAlignment="Stretch" Margin="0,10" Click="OnOpen"/>
<Button Content="Save" HorizontalAlignment="Stretch" Margin="0,10" Click="OnSave"/>
</StackPanel>
<TextBlock HorizontalAlignment="Stretch" Margin="5,0,0,5" x:Name="tblkClippingRectangle" VerticalAlignment="Top" Width="Auto" Height="Auto" Grid.Row="1" Foreground="#FFFFFFFF" Text="ClippingRectangle" TextWrapping="Wrap"/>
</Grid>
</Window>
code behind:
using System;
using System.Diagnostics;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media.Imaging;
using DAP.Adorners;
using Microsoft.Win32;
namespace CroppingTest
{
/// <summary>
/// Interaction logic for Window1.xaml
/// </summary>
public partial class WndCroppingTest
{
CroppingAdorner _clp;
FrameworkElement _felCur;
public WndCroppingTest()
{
InitializeComponent();
}
private string _s;
public WndCroppingTest(string source)
{
_s = source;
InitializeComponent();
}
private void RemoveCropFromCur()
{
AdornerLayer aly = AdornerLayer.GetAdornerLayer(_felCur);
aly.Remove(_clp);
}
private void AddCropToImage(Image fel)
{
if (_felCur != null)
{
RemoveCropFromCur();
}
Size s = new Size(80,120);
double ratio = s.Width/s.Height;
Rect r = new Rect();
if (ratio < 1)
{
r.Height = fel.ActualHeight;
r.Width = fel.ActualHeight*ratio;
r.Y = 0;
r.X = (fel.ActualWidth - r.Width)/2;
}
else
{
r.Width = fel.ActualWidth;
r.Height = fel.ActualWidth / ratio;
r.X = 0;
r.Y = (fel.ActualHeight - r.Height) / 2;
}
AdornerLayer aly = AdornerLayer.GetAdornerLayer(fel);
_clp = new CroppingAdorner(fel, r,true);
aly.Add(_clp);
Preview.Source = _clp.BpsCrop();
_clp.CropChanged += CropChanged;
_felCur = fel;
}
private void RefreshCropImage()
{
if (_clp != null)
{
Rect rc = _clp.ClippingRectangle;
tblkClippingRectangle.Text = string.Format(
"Clipping Rectangle: ({0:N1}, {1:N1}, {2:N1}, {3:N1})",
rc.Left,
rc.Top,
rc.Right,
rc.Bottom);
Preview.Source = _clp.BpsCrop();
}
}
private void CropChanged(Object sender, RoutedEventArgs rea)
{
RefreshCropImage();
}
private void Window_SizeChanged(object sender, SizeChangedEventArgs e)
{
RefreshCropImage();
}
private void OnOpen(object sender, RoutedEventArgs e)
{
OpenFileDialog openfile = new OpenFileDialog
{
//Filter = "JPEG (*.jpeg)|*.jpeg|PNG (*.png)|*.png|JPG (*.jpg)|*.jpg"
Filter = "Obrazy (*.jpeg, *.png, *.jpg)|*.jpeg;*.png;*.jpg"
};
bool? result = openfile.ShowDialog();
if (result == true)
{
//MessageBox.Show(openfile.FileName);
var source = openfile.FileName;
Crop.Source= new BitmapImage(new Uri(source));
AddCropToImage(Crop);
RefreshCropImage();
}
}
private void OnSave(object sender, RoutedEventArgs e)
{
SaveFileDialog dlg = new SaveFileDialog
{
FileName = "Avatar",
DefaultExt = ".png",
Filter = "PNGi (.png)|*.png"
};
bool? result = dlg.ShowDialog();
if (result == true)
{
string filename = dlg.FileName;
using (var fileStream = new FileStream(filename, FileMode.Create))
{
BitmapEncoder encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(_clp.BpsCrop()));
encoder.Save(fileStream);
}
}
}
private void WndCroppingTest_OnLoaded(object sender, RoutedEventArgs e)
{
if (_s != null)
{
Crop.Source = new BitmapImage(new Uri(_s));
AddCropToImage(Crop);
RefreshCropImage();
}
}
}
}
CroppingAdorner:
using System;
using System.Diagnostics;
using System.Drawing;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using Brush = System.Windows.Media.Brush;
using Brushes = System.Windows.Media.Brushes;
using Color = System.Windows.Media.Color;
using Image = System.Windows.Controls.Image;
using Pen = System.Windows.Media.Pen;
using Point = System.Drawing.Point;
using Size = System.Windows.Size;
namespace DAP.Adorners
{
public class CroppingAdorner : Adorner
{
#region Private variables
// Width of the thumbs. I know these really aren't "pixels", but px
// is still a good mnemonic.
private const int _cpxThumbWidth = 6;
// PuncturedRect to hold the "Cropping" portion of the adorner
private PuncturedRect _prCropMask;
// Canvas to hold the thumbs so they can be moved in response to the user
private Canvas _cnvThumbs;
// Cropping adorner uses Thumbs for visual elements.
// The Thumbs have built-in mouse input handling.
private CropThumb _crtTopLeft, _crtTopRight, _crtBottomLeft, _crtBottomRight;
//private CropThumb _crtTop, _crtLeft, _crtBottom, _crtRight;
// To store and manage the adorner's visual children.
private VisualCollection _vc;
// DPI for screen
private static double s_dpiX, s_dpiY;
private Size _originalSize, _controlSize;
private Image _i;
private ImageSource _s;
private BitmapImage _b;
#endregion
#region Properties
public Rect ClippingRectangle
{
get
{
return _prCropMask.RectInterior;
}
}
#endregion
#region Routed Events
public static readonly RoutedEvent CropChangedEvent = EventManager.RegisterRoutedEvent(
"CropChanged",
RoutingStrategy.Bubble,
typeof(RoutedEventHandler),
typeof(CroppingAdorner));
public event RoutedEventHandler CropChanged
{
add
{
AddHandler(CropChangedEvent, value);
}
remove
{
RemoveHandler(CropChangedEvent, value);
}
}
#endregion
#region Dependency Properties
static public DependencyProperty FillProperty = Shape.FillProperty.AddOwner(typeof(CroppingAdorner));
public Brush Fill
{
get { return (Brush)GetValue(FillProperty); }
set { SetValue(FillProperty, value); }
}
private static void FillPropChanged(DependencyObject d, DependencyPropertyChangedEventArgs args)
{
CroppingAdorner crp = d as CroppingAdorner;
if (crp != null)
{
crp._prCropMask.Fill = (Brush)args.NewValue;
}
}
#endregion
#region Constructor
static CroppingAdorner()
{
Color clr = Colors.Black;
Graphics g = Graphics.FromHwnd((IntPtr)0);
s_dpiX = g.DpiX;
s_dpiY = g.DpiY;
clr.A = 80;
FillProperty.OverrideMetadata(typeof(CroppingAdorner),
new PropertyMetadata(
new SolidColorBrush(clr),
FillPropChanged));
}
public CroppingAdorner(Image sourceImage, Rect rcInit, bool fixedRatio = false)
: base(sourceImage)
{
_fixedRatio = fixedRatio;
_ratio = rcInit.Width/rcInit.Height;
_i = sourceImage;
_s = sourceImage.Source;
try
{
_b = (BitmapImage) sourceImage.Source;
}
catch (Exception e)
{
Debug.WriteLine(e);
}
try
{
_originalSize = new Size(_b.PixelWidth, _b.PixelHeight);
}
catch (Exception e)
{
_originalSize = new Size(1,1);
}
_controlSize = new Size(sourceImage.ActualWidth, sourceImage.ActualHeight);
_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 += HandleBottomLeft;
_crtBottomRight.DragDelta += HandleBottomRight;
_crtTopLeft.DragDelta += HandleTopLeft;
_crtTopRight.DragDelta += HandleTopRight;
//_crtTop.DragDelta += HandleTop;
//_crtBottom.DragDelta += HandleBottom;
//_crtRight.DragDelta += HandleRight;
//_crtLeft.DragDelta += HandleLeft;
//add eventhandler to drag and drop
sourceImage.MouseLeftButtonDown += Handle_MouseLeftButtonDown;
sourceImage.MouseLeftButtonUp += Handle_MouseLeftButtonUp;
sourceImage.MouseMove += 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 = sourceImage;
fel.SizeChanged += AdornedElement_SizeChanged;
}
#endregion
#region Drag and drop handlers
Double OrigenX;
Double OrigenY;
private readonly bool _fixedRatio;
private double _ratio;
// 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
#region Thumb handlers
// Generic handler for Cropping
private void HandleThumb(
double drcL,
double drcT,
double drcW,
double drcH,
double dx,
double dy)
{
Rect rcInterior = _prCropMask.RectInterior;
if (rcInterior.Width + drcW * dx < 0)
{
dx = -rcInterior.Width / drcW;
}
if (rcInterior.Height + drcH * dy < 0)
{
dy = -rcInterior.Height / drcH;
}
rcInterior = new Rect(
rcInterior.Left + drcL * dx,
rcInterior.Top + drcT * dy,
rcInterior.Width + drcW * dx,
rcInterior.Height + drcH * dy);
if (_fixedRatio)
{
if (_ratio < 1)
{
if (rcInterior.Height > _i.ActualHeight)
{
rcInterior.Height = _i.ActualHeight;
}
rcInterior.Width = rcInterior.Height * _ratio;
}
else
{
if (rcInterior.Width > _i.ActualWidth)
{
rcInterior.Width = _i.ActualWidth;
}
rcInterior.Height = rcInterior.Width / _ratio;
}
}
_prCropMask.RectInterior = rcInterior;
SetThumbs(_prCropMask.RectInterior);
RaiseEvent( new RoutedEventArgs(CropChangedEvent, this));
}
// Handler for Cropping from the bottom-left.
private void HandleBottomLeft(object sender, DragDeltaEventArgs args)
{
if (sender is CropThumb)
{
HandleThumb(
1, 0, -1, 1,
args.HorizontalChange,
args.VerticalChange);
}
}
// Handler for Cropping from the bottom-right.
private void HandleBottomRight(object sender, DragDeltaEventArgs args)
{
if (sender is CropThumb)
{
HandleThumb(
0, 0, 1, 1,
args.HorizontalChange,
args.VerticalChange);
}
}
// Handler for Cropping from the top-right.
private void HandleTopRight(object sender, DragDeltaEventArgs args)
{
if (sender is CropThumb)
{
HandleThumb(
0, 1, 1, -1,
args.HorizontalChange,
args.VerticalChange);
}
}
// Handler for Cropping from the top-left.
private void HandleTopLeft(object sender, DragDeltaEventArgs args)
{
if (sender is CropThumb)
{
HandleThumb(
1, 1, -1, -1,
args.HorizontalChange,
args.VerticalChange);
}
}
#endregion
#region Other handlers
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;
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);
}
}
#endregion
#region Arranging/positioning
private void SetThumbs(Rect rc)
{
_crtBottomRight.SetPos(rc.Right, rc.Bottom);
_crtTopLeft.SetPos(rc.Left, rc.Top);
_crtTopRight.SetPos(rc.Right, rc.Top);
_crtBottomLeft.SetPos(rc.Left, rc.Bottom);
//_crtTop.SetPos(rc.Left + rc.Width / 2, rc.Top);
//_crtBottom.SetPos(rc.Left + rc.Width / 2, rc.Bottom);
//_crtLeft.SetPos(rc.Left, rc.Top + rc.Height / 2);
//_crtRight.SetPos(rc.Right, rc.Top + rc.Height / 2);
}
// Arrange the Adorners.
protected override Size ArrangeOverride(Size finalSize)
{
Rect rcExterior = new Rect(0, 0, AdornedElement.RenderSize.Width, AdornedElement.RenderSize.Height);
_prCropMask.RectExterior = rcExterior;
Rect rcInterior = _prCropMask.RectInterior;
_prCropMask.Arrange(rcExterior);
SetThumbs(rcInterior);
_cnvThumbs.Arrange(rcExterior);
return finalSize;
}
#endregion
#region Public interface
public BitmapSource BpsCrop()
{
Thickness margin = AdornerMargin();
Rect rcInterior = _prCropMask.RectInterior;
Point pxFromSize = UnitsToPx(rcInterior.Width, rcInterior.Height);
// 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, rcInterior.Top);
Point pxWhole = UnitsToPx(AdornedElement.RenderSize.Width, AdornedElement.RenderSize.Height);
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;
}
var Width = _i.ActualWidth;
var Height = _i.ActualHeight;
int x = (int)(rcInterior.Left * _originalSize.Width / Width);
int y = (int)(rcInterior.Top * _originalSize.Height / Height);
int xx = (int)((rcInterior.Width) * _originalSize.Width / Width);
int yy = (int)((rcInterior.Height) * _originalSize.Height / Height);
Int32Rect rcFrom = new Int32Rect(x, y, xx, yy);
//Int32Rect rcFrom = new 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);
try
{
return new CroppedBitmap(_b, rcFrom);
}
catch (Exception e)
{
Debug.WriteLine(e);
return new CroppedBitmap(rtb, new Int32Rect(0,0, 100,100));
}
}
public static Size RelativeSize(double aspectRatio)
{
return (aspectRatio > 1)
? new Size(1, 1 / aspectRatio)
: new Size(aspectRatio, 1);
}
#endregion
#region Helper functions
private Thickness AdornerMargin()
{
Thickness thick = new Thickness(0);
if (AdornedElement is FrameworkElement)
{
thick = ((FrameworkElement)AdornedElement).Margin;
}
return thick;
}
private void BuildCorner(ref CropThumb crt, Cursor crs)
{
if (crt != null) return;
crt = new CropThumb(_cpxThumbWidth);
// Set some arbitrary visual characteristics.
crt.Cursor = crs;
_cnvThumbs.Children.Add(crt);
}
private Point UnitsToPx(double x, double y)
{
return new Point((int)(x * s_dpiX / 96), (int)(y * s_dpiY / 96));
}
#endregion
#region Visual tree overrides
// Override the VisualChildrenCount and GetVisualChild properties to interface with
// the adorner's visual collection.
protected override int VisualChildrenCount { get { return _vc.Count; } }
protected override Visual GetVisualChild(int index) { return _vc[index]; }
#endregion
#region Internal Classes
class CropThumb : Thumb
{
#region Private variables
int _cpx;
#endregion
#region Constructor
internal CropThumb(int cpx)
: base()
{
_cpx = cpx;
}
#endregion
#region Overrides
protected override Visual GetVisualChild(int index)
{
return null;
}
protected override void OnRender(DrawingContext drawingContext)
{
drawingContext.DrawRoundedRectangle(Brushes.White, new Pen(Brushes.Black, 1), new Rect(new Size(_cpx, _cpx)), 1, 1);
}
#endregion
#region Positioning
internal void SetPos(double x, double y)
{
Canvas.SetTop(this, y - _cpx / 2);
Canvas.SetLeft(this, x - _cpx / 2);
}
#endregion
}
#endregion
}
}
and PunctedRect (I can't include code here because it exceeds question length limit, sorry for adding link)
What I'm trying to create is cropping tool that will work on Win7 and will allow me to select portion of image with aspect ratio.
As I wrote before I've tried fixing that ActualWidth problem, but I wasn't able to. How this can be fixed?
Can anyone suggest alternative (free) control that will have described functionality? There are many WUP (Windows Universal Platform) apps and controls, but I need Win7 compatible.