I'm new to Xamarin Android and currently working on a floating action button, I implemented View.IOnTouchListener and normal button click event (faButton.Click += floatButtonPressed;) to carry out my actions. But for the case MotionEventActions.Move, it doesn't work as I wanted. Moving left and right it works fine but for top and bottom it will move downwards a little bit whenever I start moving it. Besides, when I move the button to screen border it will be able to exceed the screen. Hence, I tried detect screen size and restrict it but it still not good enough, is there any other available solution or settings?
public bool OnTouch(View v, MotionEvent e)
{
switch (e.Action)
{
case MotionEventActions.Down:
oldXvalue = e.GetX();
oldYvalue = e.GetY();
if (oldXvalue == e.GetX() && oldYvalue == e.GetY())
{
return false;
}
break;
case MotionEventActions.Up:
if (oldXvalue == e.GetX() && oldYvalue == e.GetY())
{
return false;
}
break;
case MotionEventActions.Move:
var xleft = (int)(e.RawX - oldXvalue);
var xright = xleft + v.Width;
var ytop = (int)(e.RawY - oldYvalue);
var ybtm = (ytop + v.Height);
if (xleft + v.Width >= intWidth)
{
break;
}
if (xleft <= 0)
{
break;
}
if (ytop + v.Height >= intHeight)
{
break;
}
if (ytop <= 0)
{
break;
}
v.Layout(xleft, ytop, xright, ybtm);
break;
}
return true;
}
You can get the screen height and screen width first. And when the button exceed the screen you need to reset the button position.
Try the following code:
public class MainActivity : Activity, IOnTouchListener
{
Button dragAbleBt;
int screenWidth = 0;
int screenHeight = 0;
int lastX = 0, lastY = 0;
public bool OnTouch(View v, MotionEvent e)
{
MotionEventActions ea = e.Action;
switch (ea) {
case MotionEventActions.Down:
lastX = (int)e.RawX;
lastY = (int)e.RawY;
break;
case MotionEventActions.Move:
int dx = (int)e.RawX - lastX;
int dy = (int)e.RawY - lastY;
int left = v.Left + dx;
int right = v.Right + dx;
int top = v.Top + dy;
int bottom = v.Bottom + dy;
if (left < 0)
{
left = 0;
right = left + v.Width;
}
if (right > screenWidth)
{
right = screenWidth;
left = right - v.Width;
}
if (top < 0)
{
top = 0;
bottom = top + v.Height;
}
if (bottom > screenHeight)
{
bottom = screenHeight;
top = bottom - v.Height;
}
v.Layout(left, top, right, bottom);
lastX = (int) e.RawX;
lastY = (int) e.RawY;
v.PostInvalidate();
break;
case MotionEventActions.Up:
break;
}
return false;
}
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
SetContentView (Resource.Layout.Main);
//DisplayMetrics dm = Resources.DisplayMetrics;
//screenWidth = dm.WidthPixels;
//screenHeight = dm.HeightPixels;
dragAbleBt = FindViewById<Button>(Resource.Id.button1);
dragAbleBt.SetOnTouchListener(this);
}
public override void OnWindowFocusChanged(bool hasFocus)
{
base.OnWindowFocusChanged(hasFocus);
if (hasFocus)
{
Rect outRect = new Rect();
this.Window.FindViewById(Window.IdAndroidContent).GetDrawingRect(outRect);
screenWidth = outRect.Width();
screenHeight = outRect.Height();
}
}
}
I tried detect screen size and restrict it but it still not good enough, is there any other available solution or settings?
You may got the whole screen width and height, the button will exceed the screen height, In this kind of situation you need get the view drawing area by this.Window.FindViewById(Window.IdAndroidContent).GetDrawingRect(outRect)
Screen Shot:
Note: I am using the emulator that is slow, If you are using the real device it will be faster.
Related
I wrote an EditorPanel container, which orders controls in a label/editor order. The complete source of the control follows:
public class EditorPanel : Panel
{
private enum GeneralAlignment
{
Begin,
Center,
End,
Stretch
}
private static GeneralAlignment ToGeneralAlignment(VerticalAlignment verticalAlignment)
{
switch (verticalAlignment)
{
case VerticalAlignment.Top:
return GeneralAlignment.Begin;
case VerticalAlignment.Center:
return GeneralAlignment.Center;
case VerticalAlignment.Bottom:
return GeneralAlignment.End;
case VerticalAlignment.Stretch:
return GeneralAlignment.Stretch;
default:
throw new InvalidEnumArgumentException("Unsupported vertical alignment!");
}
}
private static GeneralAlignment ToGeneralAlignment(HorizontalAlignment horizontalAlignment)
{
switch (horizontalAlignment)
{
case HorizontalAlignment.Left:
return GeneralAlignment.Begin;
case HorizontalAlignment.Center:
return GeneralAlignment.Center;
case HorizontalAlignment.Right:
return GeneralAlignment.End;
case HorizontalAlignment.Stretch:
return GeneralAlignment.Stretch;
default:
throw new InvalidEnumArgumentException("Unsupported horizontal alignment!");
}
}
private Size DesiredSizeWithMargin(UIElement element)
{
if (element == null)
return Size.Empty;
if (element is FrameworkElement frameworkElement)
return new Size(frameworkElement.DesiredSize.Width + frameworkElement.Margin.Left + frameworkElement.Margin.Right,
frameworkElement.DesiredSize.Height + frameworkElement.Margin.Top + frameworkElement.Margin.Bottom);
else
return element.DesiredSize;
}
private static (double elementStart, double elementSize) EvalPlacement(UIElement element,
double placementRectStart,
double placementRectSize,
double elementMarginBegin,
double elementMarginEnd,
double elementDesiredSize,
GeneralAlignment elementAlignment)
{
double resultSize;
double resultStart;
switch (elementAlignment)
{
case GeneralAlignment.Begin:
resultSize = Math.Max(0, Math.Min(elementDesiredSize, placementRectSize - (elementMarginBegin + elementMarginEnd)));
resultStart = placementRectStart + elementMarginBegin;
break;
case GeneralAlignment.Center:
resultSize = Math.Max(0, Math.Min(elementDesiredSize, placementRectSize - (elementMarginBegin + elementMarginEnd)));
resultStart = placementRectStart + (placementRectSize - (resultSize + elementMarginBegin + elementMarginEnd)) / 2 + elementMarginBegin;
break;
case GeneralAlignment.End:
resultSize = Math.Max(0, Math.Min(elementDesiredSize, placementRectSize - (elementMarginBegin + elementMarginEnd)));
resultStart = placementRectStart + placementRectSize - elementMarginEnd - resultSize;
break;
case GeneralAlignment.Stretch:
resultSize = Math.Max(0, placementRectSize - (elementMarginBegin + elementMarginEnd));
resultStart = placementRectStart + elementMarginBegin;
break;
default:
throw new InvalidEnumArgumentException("Unsupported alignment!");
}
return (resultStart, resultSize);
}
private void ArrangeWithAlignment(UIElement element, Rect placementRect, Size cachedDesiredSize)
{
if (cachedDesiredSize == Size.Empty)
cachedDesiredSize = DesiredSizeWithMargin(element);
Thickness elementMargin = new Thickness();
HorizontalAlignment elementHorizontalAlignment = HorizontalAlignment.Stretch;
VerticalAlignment elementVerticalAlignment = VerticalAlignment.Top;
if (element is FrameworkElement frameworkElement)
{
elementMargin = frameworkElement.Margin;
elementHorizontalAlignment = frameworkElement.HorizontalAlignment;
elementVerticalAlignment = frameworkElement.VerticalAlignment;
}
(double elementTop, double elementHeight) = EvalPlacement(element,
placementRect.Top,
placementRect.Height,
elementMargin.Top,
elementMargin.Bottom,
cachedDesiredSize.Height,
ToGeneralAlignment(elementVerticalAlignment));
(double elementLeft, double elementWidth) = EvalPlacement(element,
placementRect.Left,
placementRect.Width,
elementMargin.Left,
elementMargin.Right,
cachedDesiredSize.Width,
ToGeneralAlignment(elementHorizontalAlignment));
element.Arrange(new Rect(elementLeft, elementTop, elementWidth, elementHeight));
}
protected override Size MeasureOverride(Size availableSize)
{
double maxLabelWidth = 0.0;
double maxEditorWidth = 0.0;
double totalLabelEditorPairHeight = 0.0;
for (int i = 0; i < InternalChildren.Count; i += 2)
{
// Measure label
InternalChildren[i].Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
Size labelDesiredSize = DesiredSizeWithMargin(InternalChildren[i]);
// Measure editor (if any)
Size editorDesiredSize = Size.Empty;
if (i + 1 < InternalChildren.Count)
{
InternalChildren[i + 1].Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
editorDesiredSize = DesiredSizeWithMargin(InternalChildren[i + 1]);
}
maxLabelWidth = Math.Max(maxLabelWidth, labelDesiredSize.Width);
maxEditorWidth = Math.Max(maxEditorWidth, editorDesiredSize.Width);
totalLabelEditorPairHeight += Math.Max(labelDesiredSize.Height, editorDesiredSize.Height);
}
// This is required height, regardless of how much space is available
double resultHeight = totalLabelEditorPairHeight;
// If space is not constrained, pick as much as labels & editors want. Else, use
// as much, as is given.
double resultWidth = double.IsInfinity(availableSize.Width) ? maxLabelWidth + maxEditorWidth : availableSize.Width;
return new Size(resultWidth, resultHeight);
}
protected override Size ArrangeOverride(Size finalSize)
{
// Label area width
double labelAreaWidth = 0;
for (int i = 0; i < InternalChildren.Count; i += 2)
labelAreaWidth = Math.Max(labelAreaWidth, DesiredSizeWithMargin(InternalChildren[i]).Width);
labelAreaWidth = Math.Min(labelAreaWidth, finalSize.Width);
// Editor area width
double editorAreaWidth = Math.Max(0, finalSize.Width - labelAreaWidth);
// Arranging controls
double y = 0;
int controlIndex = 0;
while (controlIndex < InternalChildren.Count)
{
// Retrieve label and editor
UIElement label = InternalChildren[controlIndex++];
Size labelDesiredSize = DesiredSizeWithMargin(label);
UIElement editor = (controlIndex < InternalChildren.Count) ? InternalChildren[controlIndex++] : null;
Size editorDesiredSize = DesiredSizeWithMargin(editor);
double rowHeight = Math.Max(labelDesiredSize.Height, editorDesiredSize.Height);
var labelArea = new Rect(0, y, labelAreaWidth, rowHeight);
ArrangeWithAlignment(label, labelArea, label.DesiredSize);
// Arrange editor
if (editor != null)
{
var editorArea = new Rect(labelAreaWidth, y, editorAreaWidth, rowHeight);
ArrangeWithAlignment(editor, editorArea, editor.DesiredSize);
}
y += Math.Max(labelDesiredSize.Height, editorDesiredSize.Height);
}
return finalSize;
}
}
Example of usage may look like following:
<Border BorderBrush="Black" BorderThickness="1"
Width="400" Height="100">
<controls:EditorPanel>
<Label>First label</Label>
<TextBox />
<Label>Second label</Label>
<TextBox />
</controls:EditorPanel>
</Border>
When I run the application, it looks like following:
However, if I write more text in the textbox, it starts to escape the boundaries set by the EditorPanel itself.
What is interesting is that the container seems to work properly and allocates proper amount of space for the control:
But the control seems to ignore it completely and ends up bigger than it actually should be:
That leads to my question: why the child control ignores the space it was given through the Arrange call?
It turns out, that I was too generous during the Measure stage. I put more effort into checking, how much space labels and editors can occupy and now everything works.
protected override Size MeasureOverride(Size availableSize)
{
// Measure labels
List<Size> labelSizes = new List<Size>();
for (int i = 0; i < InternalChildren.Count; i += 2)
{
// Measure label
Thickness labelMargin = new Thickness(0);
if (InternalChildren[i] is FrameworkElement frameworkElement)
labelMargin = frameworkElement.Margin;
InternalChildren[i].Measure(new Size(availableSize.Width - (labelMargin.Left + labelMargin.Right), availableSize.Height - (labelMargin.Top + labelMargin.Bottom)));
labelSizes.Add(DesiredSizeWithMargin(InternalChildren[i]));
}
double maxLabelWidth = labelSizes.Max(ls => ls.Width);
// Measure editors
List<Size> editorSizes = new List<Size>();
for (int i = 1; i < InternalChildren.Count; i += 2)
{
Thickness editorMargin = new Thickness(0);
if (InternalChildren[i] is FrameworkElement frameworkElement)
editorMargin = frameworkElement.Margin;
InternalChildren[i].Measure(new Size(availableSize.Width - maxLabelWidth - (editorMargin.Left + editorMargin.Right), availableSize.Height - (editorMargin.Top + editorMargin.Bottom)));
editorSizes.Add(DesiredSizeWithMargin(InternalChildren[i]));
}
double maxEditorWidth = editorSizes.Max(es => es.Width);
// Equalize count
while (editorSizes.Count < labelSizes.Count)
editorSizes.Add(Size.Empty);
// Evaluate total height
double totalLabelEditorPairHeight = labelSizes.Zip(editorSizes)
.Select(sizes => Math.Max(sizes.First.Height, sizes.Second.Height))
.Sum();
// This is required height, regardless of how much space is available
double resultHeight = totalLabelEditorPairHeight;
// If space is not constrained, pick as much as labels & editors want. Else, use
// as much, as is given.
double resultWidth = double.IsInfinity(availableSize.Width) ? maxLabelWidth + maxEditorWidth : availableSize.Width;
return new Size(resultWidth, resultHeight);
}
I want to fit my VideoView to screen size in xamarin forms ios,
I use it but i can't get fit size
MediaPlayer = new MediaPlayer(LibVLC)
{
Media = media,
EnableHardwareDecoding = true,
AspectRatio = $"{App.ScreenWidth}:{App.ScreenHeight}"
};
so I used fullscreen property
MediaPlayer = new MediaPlayer(LibVLC)
{
Media = media,
EnableHardwareDecoding = true,
Fullscreen = true
};
It couldn't give my want result
i made custom renderer and overridden OnSizeAllocated() Method,
I guessed that changing the videoview size would change the video size as well.
protected override void OnSizeAllocated(double width, double height)
{
base.OnSizeAllocated(width, height);
if (width > height)
{
if (height * 16 / 9 > width)
{
VideoView.WidthRequest = width;
VideoView.HeightRequest = width * 9 / 16;
}
else
{
VideoView.HeightRequest = height;
VideoView.WidthRequest = height * 16 / 9;
}
}
else
{
if (width * 9 / 16 > height)
{
VideoView.HeightRequest = height;
VideoView.WidthRequest = height * 16 / 9;
}
else
{
VideoView.WidthRequest = width;
VideoView.HeightRequest = width * 9 / 16;
}
}
}
Likewise I didn't get the desired result
In addition, I tried various methods, for example, --fullscreen option, etc.
if i has some mistake, please advice for me
https://github.com/Sunday5214/vlcExample
above link is my example
after I checked answer, i tried solution
MediaPlayer = new MediaPlayer(LibVLC)
{
Media = media,
EnableHardwareDecoding = true,
Scale = 0,
AspectRatio = $"{App.ScreenHeight}:{App.ScreenWidth}"
};
still i couldn't get fullscreen,
i got to know some interest things, if i rotate my phone to landscape then rotate to portrait, i can get full screen
before rotating,
after rotating
This is how you are supposed to change aspect ratio
private void UpdateAspectRatio(AspectRatio? aspectRatio = null)
{
var mediaPlayer = MediaPlayer;
var videoView = VideoView;
if (aspectRatio == null)
{
aspectRatio = _aspectRatio ?? GetAspectRatio(mediaPlayer);
if (aspectRatio == null)
{
return;
}
}
if (videoView != null && mediaPlayer != null)
{
switch (aspectRatio)
{
case AspectRatio.Original:
mediaPlayer.AspectRatio = null;
mediaPlayer.Scale = 1;
break;
case AspectRatio.Fill:
var videoTrack = GetVideoTrack(mediaPlayer);
if (videoTrack == null)
{
break;
}
mediaPlayer.Scale = 0;
mediaPlayer.AspectRatio = IsVideoSwapped((VideoTrack)videoTrack) ? $"{videoView.Height}:{videoView.Width}" :
$"{videoView.Width}:{videoView.Height}";
break;
case AspectRatio.BestFit:
mediaPlayer.AspectRatio = null;
mediaPlayer.Scale = 0;
break;
case AspectRatio.FitScreen:
videoTrack = GetVideoTrack(mediaPlayer);
if (videoTrack == null)
{
break;
}
var track = (VideoTrack)videoTrack;
var videoSwapped = IsVideoSwapped(track);
var videoWidth = videoSwapped ? track.Height : track.Width;
var videoHeigth = videoSwapped ? track.Width : track.Height;
if (videoWidth == 0 || videoHeigth == 0)
{
mediaPlayer.Scale = 0;
}
else
{
if (track.SarNum != track.SarDen)
{
videoWidth = videoWidth * track.SarNum / track.SarDen;
}
var ar = videoWidth / (double)videoHeigth;
var videoViewWidth = videoView.Width;
var videoViewHeight = videoView.Height;
var dar = videoViewWidth / videoViewHeight;
var rawPixelsPerViewPixel = DisplayInformation.ScalingFactor;
var displayWidth = videoViewWidth * rawPixelsPerViewPixel;
var displayHeight = videoViewHeight * rawPixelsPerViewPixel;
mediaPlayer.Scale = (float)(dar >= ar ? (displayWidth / videoWidth) : (displayHeight / videoHeigth));
}
mediaPlayer.AspectRatio = null;
break;
case AspectRatio._16_9:
mediaPlayer.AspectRatio = "16:9";
mediaPlayer.Scale = 0;
break;
case AspectRatio._4_3:
mediaPlayer.AspectRatio = "4:3";
mediaPlayer.Scale = 0;
break;
}
}
if (_aspectRatio != aspectRatio)
{
_aspectRatio = aspectRatio;
AspectRatioChanged?.Invoke(this, EventArgs.Empty);
}
}
You need to set both the Scale and AspectRatio properties. This is a helper only available from the MediaPlayerElement when using Xamarin.Forms, but you can copy that behavior in your app.
I'm using carousel view to display a number of images, changeable upon sliding. The problem I'm having is that these images are not zoom-able. Is there any way to enable zooming within carousel view? Thanks.
Ok so the First Thing first Carousel View is not a gud thing which you want to make zoom (for Practical Case)
and as client demands out of the Galaxy things so you can make things work out with some logic
1st Way:
Create a Class Name ZoomImage in Your Shared Xamarin.forms Solution and Add this code in it
public class ZoomImage : Image {
private const double MIN_SCALE = 1;
private const double MAX_SCALE = 4;
private const double OVERSHOOT = 0.15;
private double StartScale;
private double LastX, LastY;
public ZoomImage() {
var pinch = new PinchGestureRecognizer();
pinch.PinchUpdated += OnPinchUpdated;
GestureRecognizers.Add(pinch);
var pan = new PanGestureRecognizer();
pan.PanUpdated += OnPanUpdated;
GestureRecognizers.Add(pan);
var tap = new TapGestureRecognizer { NumberOfTapsRequired = 2 };
tap.Tapped += OnTapped;
GestureRecognizers.Add(tap);
Scale = MIN_SCALE;
TranslationX = TranslationY = 0;
AnchorX = AnchorY = 0;
}
protected override SizeRequest OnMeasure(double widthConstraint, double heightConstraint) {
Scale = MIN_SCALE;
TranslationX = TranslationY = 0;
AnchorX = AnchorY = 0;
return base.OnMeasure(widthConstraint, heightConstraint);
}
private void OnTapped(object sender, EventArgs e) {
if (Scale > MIN_SCALE) {
this.ScaleTo(MIN_SCALE, 250, Easing.CubicInOut);
this.TranslateTo(0, 0, 250, Easing.CubicInOut);
}
else {
AnchorX = AnchorY = 0.5; //TODO tapped position
this.ScaleTo(MAX_SCALE, 250, Easing.CubicInOut);
}
}
private void OnPanUpdated(object sender, PanUpdatedEventArgs e) {
if (Scale > MIN_SCALE)
switch (e.StatusType) {
case GestureStatus.Started:
LastX = TranslationX;
LastY = TranslationY;
break;
case GestureStatus.Running:
TranslationX = Clamp(LastX + e.TotalX * Scale, -Width / 2, Width / 2);
TranslationY = Clamp(LastY + e.TotalY * Scale, -Height / 2, Height / 2);
break;
}
}
private void OnPinchUpdated(object sender, PinchGestureUpdatedEventArgs e) {
switch (e.Status) {
case GestureStatus.Started:
StartScale = Scale;
AnchorX = e.ScaleOrigin.X;
AnchorY = e.ScaleOrigin.Y;
break;
case GestureStatus.Running:
double current = Scale + (e.Scale - 1) * StartScale;
Scale = Clamp(current, MIN_SCALE * (1 - OVERSHOOT), MAX_SCALE * (1 + OVERSHOOT));
break;
case GestureStatus.Completed:
if (Scale > MAX_SCALE)
this.ScaleTo(MAX_SCALE, 250, Easing.SpringOut);
else if (Scale < MIN_SCALE)
this.ScaleTo(MIN_SCALE, 250, Easing.SpringOut);
break;
}
}
private T Clamp<T>(T value, T minimum, T maximum) where T : IComparable {
if (value.CompareTo(minimum) < 0)
return minimum;
else if (value.CompareTo(maximum) > 0)
return maximum;
else
return value;
}
}
and now In Xaml within carousel view Use Image Like This
<cv:CarouselView x:Name="itemPictureGallery" Grid.Column="0" Grid.Row="0">
<cv:CarouselView.ItemTemplate>
<DataTemplate>
<local:image source="YOUR_PIC_SOURCE" />
</DataTemplate>
</cv:CarouselView.ItemTemplate>
</cv:CarouselView>
Dont Forget to add nameSpace in Xamal
xmlns:local="clr-namespace:YOUR_APP_NAME"
so this is how the image can zoom etiher
Or Second Way is
add an Item Tapped on carousel view and pass the picture source in command parameter and open a new page with that picture in it and apply zoom with in it
i use that logic for figure out my problem
Hope this Helps :)
This can be achieved by using
alexrainman/CarouselView for the carousal view.
https://github.com/alexrainman/CarouselView
and add Xamarin forms pinch gesture recognizer according to the forms sample to the image used in carousal view
https://learn.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/gestures/pinch
I looked everywhere and haven't come across anything but I would like to know the best way to contain a selection rectangle so that it wouldn't go out of bounds. I have an application were user draws a selection rectangle on top of an image. The rectangle can be also be moved and resized. Currently I just use an exception handler which when an out of range exception is catched it would alert the user. The out of range exception only occurs when moving the drawn rectangle and I would like to make it more streamlined that the actual rectangle cant be dragged or resized outside of the image. Below is the xaml and code behind for my crop control.
Crop Control Code Behind:
public partial class CropControl : UserControl
{
#region Data's
private bool isDragging = false;
private Point anchorPoint = new Point();
private bool MoveRect = false; //flag which intially set to false which means a crop rectangle is not moved but created.
private bool MoveInProgress = false; //flag that is set to true if the crop rect is moving, otherwise false.
private Point LastPoint; // The drag's last point
HitType MouseHitType = HitType.None; //part of the rectangle under the mouse
private enum HitType { None, Body, UL, UR, LR, LL, L, R, T, B }; //Enum for the part of the rectangle the mouse is over.
#endregion
#region Constructor
public CropControl()
{
InitializeComponent();
}
#endregion
#region Dependency Property
//Register the Dependency Property
public static readonly DependencyProperty SelectionProperty =
DependencyProperty.Register("Selection", typeof(Rect), typeof(CropControl), new PropertyMetadata(default(Rect)));
public Rect Selection
{
get { return (Rect)GetValue(SelectionProperty); }
set { SetValue(SelectionProperty, value); }
}
// this is used, to react on changes from ViewModel. If you assign a
// new Rect in your ViewModel you will have to redraw your Rect here
private static void OnSelectionChanged(System.Windows.DependencyObject d, System.Windows.DependencyPropertyChangedEventArgs e)
{
Rect newRect = (Rect)e.NewValue;
Rectangle selectionRectangle = d as Rectangle;
if (selectionRectangle != null)
return;
selectionRectangle.SetValue(Canvas.LeftProperty, newRect.X);
selectionRectangle.SetValue(Canvas.TopProperty, newRect.Y);
selectionRectangle.Width = newRect.Width;
selectionRectangle.Height = newRect.Height;
}
#endregion
private Point lastLoc;
#region MouseLeftButtonDown Event
private void LoadedImage_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
lastLoc = new Point(Canvas.GetLeft(selectionRectangle), Canvas.GetTop(selectionRectangle));
//This statement will enable the creation of a new rectangle only if the mouse left
//button press is outside of a created rectangle and that crop rectangle was initially created.
//This is known since the HitType if outside the rectangle will always be set to None and the crop rect width > 0.
//The previous cropping rect will be removed by setting its value to null.
if (MouseHitType== HitType.None && selectionRectangle.Width>0)
{
selectionRectangle.Width = 0; //set crop rectangle's width to 0
selectionRectangle.Height = 0; //set crop rectangle's height to 0
SetMouseCursor();
MoveRect = false; //flag that crop rectangle is not being moved but drawn.
}
//This statement test if the crop rectangle is not being dragged and moved. If true it would
//set the x and y position of the crop rect in accordance to Canvas. If false it means that
//crop rectangle was already created and is now being moved to different position in the canvas.
if (!isDragging && !MoveRect)
{
anchorPoint.X = e.GetPosition(BackPanel).X; //get the x position of the mouse
anchorPoint.Y = e.GetPosition(BackPanel).Y; //get the y position of the mouse
isDragging = true; //flag that the user is dragging the mouse to create a rectangle
BackPanel.Cursor = Cursors.Cross; //change the cursor to a cross while left button is held down
}
else
{
MouseHitType = SetHitType(selectionRectangle, e.GetPosition(BackPanel)); //get hittype
SetMouseCursor(); //set the mouse cursor based on the hittype
if (MouseHitType == HitType.None) return;
LastPoint = e.GetPosition(BackPanel);
MoveInProgress = true; //flag true since rectangle is being moved
}
}
#endregion
private double CanvasTop, CanvasLeft;
#region MouseMove Event
private void LoadedImage_MouseMove(object sender, MouseEventArgs e)
{
Point offset = new Point((anchorPoint.X-lastLoc.X),(anchorPoint.Y-lastLoc.Y));
var newX=(anchorPoint.X+(e.GetPosition(BackPanel).X)-anchorPoint.X);
var newY=(anchorPoint.Y+(e.GetPosition(BackPanel).Y)-anchorPoint.Y);
CanvasTop = newX - offset.X;
CanvasLeft = newY - offset.Y;
//Statement that checks if crop rect is being created or moved. If moved it will set the
//dimension of the rectanlge and if not it would set the location of the new rectangle.
if (isDragging && !MoveRect)
{
double x = e.GetPosition(BackPanel).X; //get x position of mouse
double y = e.GetPosition(BackPanel).Y; //get y position of mouse
selectionRectangle.SetValue(Canvas.LeftProperty, Math.Min(x, anchorPoint.X)); //set the bottom
selectionRectangle.SetValue(Canvas.TopProperty, Math.Min(y, anchorPoint.Y)); //set the top
selectionRectangle.Width = Math.Abs(x - anchorPoint.X); //set the width
selectionRectangle.Height = Math.Abs(y - anchorPoint.Y); //set the height
if (selectionRectangle.Visibility != Visibility.Visible) //make crop rectangle visible if its not.
selectionRectangle.Visibility = Visibility.Visible;
}
else if (!isDragging && MoveRect)
{
if (!MoveInProgress)
{
MouseHitType = SetHitType(selectionRectangle, e.GetPosition(BackPanel));
SetMouseCursor();
}
else
{
// See how much the mouse has moved.
Point point = e.GetPosition(BackPanel);
double offset_x = point.X - LastPoint.X;
double offset_y = point.Y - LastPoint.Y;
// Get the rectangle's current position.
double new_x = Canvas.GetLeft(selectionRectangle);
double new_y = Canvas.GetTop(selectionRectangle);
double new_width = selectionRectangle.Width;
double new_height = selectionRectangle.Height;
// Update the rectangle.
switch (MouseHitType)
{
case HitType.Body:
new_x += offset_x;
new_y += offset_y;
break;
case HitType.UL:
new_x += offset_x;
new_y += offset_y;
new_width -= offset_x;
new_height -= offset_y;
break;
case HitType.UR:
new_y += offset_y;
new_width += offset_x;
new_height -= offset_y;
break;
case HitType.LR:
new_width += offset_x;
new_height += offset_y;
break;
case HitType.LL:
new_x += offset_x;
new_width -= offset_x;
new_height += offset_y;
break;
case HitType.L:
new_x += offset_x;
new_width -= offset_x;
break;
case HitType.R:
new_width += offset_x;
break;
case HitType.B:
new_height += offset_y;
break;
case HitType.T:
new_y += offset_y;
new_height -= offset_y;
break;
}
// Don't use negative width or height.
if ((new_width > 0) && (new_height > 0))
{
// Update the rectangle.
Canvas.SetLeft(selectionRectangle, new_x);
Canvas.SetTop(selectionRectangle, new_y);
selectionRectangle.Width = new_width;
selectionRectangle.Height = new_height;
// Save the mouse's new location.
LastPoint = point;
}
}
}
}
#endregion
#region MouseLeftButtonUp Event
private void LoadedImage_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
//statement which checks if the mouse left button action is for either creating or
//moving the crop rectangle. If true, isDragging=false since the crop rect is created
//and moverect = true since the created rectangle is ready to be moved.
if (isDragging && !MoveRect)
{
isDragging = false;
if (selectionRectangle.Width > 0)
{
MoveRect = true;
}
}
else
{
MoveInProgress = false; //flags Move in progress as false since rect move action is done.
}
// Set the Selection to the new rect, when the mouse button has been released
Selection = new Rect(
(double)selectionRectangle.GetValue(Canvas.LeftProperty),
(double)selectionRectangle.GetValue(Canvas.TopProperty),
selectionRectangle.Width,
selectionRectangle.Height);
}
#endregion
#region Mutator's
// Return a HitType value to indicate what is at the point.
private HitType SetHitType(Rectangle rect, Point point)
{
double left = Canvas.GetLeft(selectionRectangle);
double top = Canvas.GetTop(selectionRectangle);
double right = left + selectionRectangle.Width;
double bottom = top + selectionRectangle.Height;
//statement that checks if cursor is outside the area of the crop rectangle
//and returns HitType.None.
if (point.X < left) return HitType.None;
if (point.X > right) return HitType.None;
if (point.Y < top) return HitType.None;
if (point.Y > bottom) return HitType.None;
const double GAP = 10; //sets the gap which when mouse over a cursor change is triggered
//statement that checks where the mouse is located within the rectangle.
if (point.X - left < GAP)
{
// Left edge.
if (point.Y - top < GAP) return HitType.UL;
if (bottom - point.Y < GAP) return HitType.LL;
return HitType.L;
}
if (right - point.X < GAP)
{
// Right edge.
if (point.Y - top < GAP) return HitType.UR;
if (bottom - point.Y < GAP) return HitType.LR;
return HitType.R;
}
if (point.Y - top < GAP) return HitType.T;
if (bottom - point.Y < GAP) return HitType.B;
return HitType.Body;
}
// Set a mouse cursor appropriate for the current hit type.
private void SetMouseCursor()
{
// See what cursor we should display.
Cursor desired_cursor = Cursors.Arrow;
switch (MouseHitType)
{
case HitType.None:
desired_cursor = Cursors.Arrow;
break;
case HitType.Body:
desired_cursor = Cursors.ScrollAll;
break;
case HitType.UL:
case HitType.LR:
desired_cursor = Cursors.SizeNWSE;
break;
case HitType.LL:
case HitType.UR:
desired_cursor = Cursors.SizeNESW;
break;
case HitType.T:
case HitType.B:
desired_cursor = Cursors.SizeNS;
break;
case HitType.L:
case HitType.R:
desired_cursor = Cursors.SizeWE;
break;
}
// Display the desired cursor.
if (BackPanel.Cursor != desired_cursor)
BackPanel.Cursor = desired_cursor;
}
#endregion
}
Crop Control XAML:
<UserControl.Resources>
<Storyboard x:Key="MarchingAnts">
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
Storyboard.TargetName="selectionRectangle"
Storyboard.TargetProperty="(Shape.StrokeDashOffset)"
RepeatBehavior="Forever">
<SplineDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
<SplineDoubleKeyFrame KeyTime="00:00:00.3000000"
Value="10"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</UserControl.Resources>
<UserControl.Triggers>
<EventTrigger RoutedEvent="FrameworkElement.Loaded">
<BeginStoryboard Storyboard="{StaticResource MarchingAnts}"/>
</EventTrigger>
</UserControl.Triggers>
<Canvas Name="BackPanel" Background="Transparent" MouseLeftButtonDown="LoadedImage_MouseLeftButtonDown" MouseMove="LoadedImage_MouseMove" MouseLeftButtonUp="LoadedImage_MouseLeftButtonUp">
<Rectangle Name="selectionRectangle" Stroke="#FFFFFFFF"
StrokeThickness="1" StrokeDashOffset="0"
Fill="#220000FF" Visibility="Collapsed"
StrokeDashArray="5"/>
</Canvas>
Sorry for the confusion but i changed my explanation. The rectangle don't go out of bounds when drawn but only happens if the drawn rectangle is moved. Also the exception is caught in my view model's crop method which is shown below:
public void Crop()
{
////Get a copy of the selection in case it changes during execution
Rect cropSelection = Selection;
//// use it to crop your image
Int32Rect rcFrom = new Int32Rect();
rcFrom.X = (int)((cropSelection.X) * (ImagePath.Width) / (ImagePath.Width));
rcFrom.Y = (int)((cropSelection.Y) * (ImagePath.Height) / (ImagePath.Height));
rcFrom.Width = (int)cropSelection.Width;
rcFrom.Height = (int)cropSelection.Height;
try
{
BitmapSource bs = new CroppedBitmap(ImagePath as BitmapSource, rcFrom);
CroppedImage = bs;
}
catch (Exception e)
{
MessageBox.Show("Selection Rectangle is outside the image." + "\n" + "Adjust the cropping rectangle so it's within the boundaries of the Image ", " Error Message", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
Update:
I was able to get it working by calculating the size and position of the selection rectangle against its parent (Canvas) which takes takes the same size as the image. Below is what I added to my code.
double bottom = new_y + selectionRectangle.Height;
double right = new_x+selectionRectangle.Width;
if (new_y< 0)
new_y = 0;
if (new_x < 0)
new_x = 0;
if (bottom > BackPanel.ActualHeight)
new_y = BackPanel.ActualHeight-selectionRectangle.Height;
if (right > BackPanel.ActualWidth)
new_x = BackPanel.ActualWidth - selectionRectangle.Width;
if (new_height > BackPanel.ActualHeight)
new_height = BackPanel.ActualHeight;
if (new_width > BackPanel.ActualWidth)
new_width = BackPanel.ActualWidth;
The new_height and new_width was added because an exception is still thrown if the rectangle occupies the entire image.
If i correctly understood question - you shows message to the user when selection rect goes outside of image rect? If so, why not check that: if new selection state will out of image area - then just not move selection and keep it in old state? I mean - compare borders positions of new selection rect with image rect and make decision: move or not your selection rect in new position (or change or not it's size).
I have a custom control that zooms on a custom drawn document canvas.
I tried using AutoScroll but it was not giving satisfactory results. When I would set AutoScrollPosition and AutoScrollMinSize back to back (in any order) it would force a paint and cause jitter each time the zoom changes. I assume this was because it was calling an Update and not Invalidate when I modified both properties.
I am now manually setting the HorizontalScroll and VerticalScroll properties with AutoScroll set to false like so each time the Zoom level or the client size changes:
int canvasWidth = (int)Math.Ceiling(Image.Width * Zoom) + PageMargins.Horizontal;
int canvasHeight = (int)Math.Ceiling(Image.Height * Zoom) + PageMargins.Vertical;
HorizontalScroll.Maximum = canvasWidth;
HorizontalScroll.LargeChange = ClientSize.Width;
VerticalScroll.Maximum = canvasHeight;
VerticalScroll.LargeChange = ClientSize.Height;
if (canvasWidth > ClientSize.Width)
{
HorizontalScroll.Visible = true;
}
else
{
HorizontalScroll.Visible = false;
HorizontalScroll.Value = 0;
}
if (canvasHeight > ClientSize.Height)
{
VerticalScroll.Visible = true;
}
else
{
VerticalScroll.Visible = false;
VerticalScroll.Value = 0;
}
int focusX = (int)Math.Floor((FocusPoint.X * Zoom) + PageMargins.Left);
int focusY = (int)Math.Floor((FocusPoint.Y * Zoom) + PageMargins.Top);
focusX = focusX - ClientSize.Width / 2;
focusY = focusY - ClientSize.Height / 2;
if (focusX < 0)
focusX = 0;
if (focusX > canvasWidth - ClientSize.Width)
focusX = canvasWidth - ClientSize.Width;
if (focusY < 0)
focusY = 0;
if (focusY > canvasHeight - ClientSize.Height)
focusY = canvasHeight - ClientSize.Height;
if (HorizontalScroll.Visible)
HorizontalScroll.Value = focusX;
if (VerticalScroll.Visible)
VerticalScroll.Value = focusY;
In this case, FocusPoint is a PointF structure that holds the coordinates in the bitmap which the user is focused on (for example, when they mouse wheel to zoom in they are focusing on the current mouse location at that time). This functionality works for the most part.
What does not work is the scroll bars. If the user tries to manually scroll by clicking on either scroll bar, they both keep returning to 0. I do not set them anywhere else in my code. I have tried writing the following in the OnScroll() method:
if (se.ScrollOrientation == ScrollOrientation.VerticalScroll)
{
VerticalScroll.Value = se.NewValue;
}
else
{
HorizontalScroll.Value = se.NewValue;
}
Invalidate();
But this causes some very erratic behavior including flicking and scrolling out of bounds.
How am I supposed to write the code for OnScroll? I've tried the base.OnScroll but it didn't do anything while AutoScroll is set to false.
I ended up implementing my own custom scrolling by creating 3 child controls: an HScrollBar, a VScrollBar, and a Panel.
I hide ClientSize and ClientRectangle like so:
public new Rectangle ClientRectangle
{
get
{
return new Rectangle(new Point(0, 0), ClientSize);
}
}
public new Size ClientSize
{
get
{
return new Size(
base.ClientSize.Width - VScrollBar.Width,
base.ClientSize.Height - HScrollBar.Height
);
}
}
The layout is done in OnClientSizeChanged:
protected override void OnClientSizeChanged(EventArgs e)
{
base.OnClientSizeChanged(e);
HScrollBar.Location = new Point(0, base.ClientSize.Height - HScrollBar.Height);
HScrollBar.Width = base.ClientSize.Width - VScrollBar.Width;
VScrollBar.Location = new Point(base.ClientSize.Width - VScrollBar.Width, 0);
VScrollBar.Height = base.ClientSize.Height - HScrollBar.Height;
cornerPanel.Size = new Size(VScrollBar.Width, HScrollBar.Height);
cornerPanel.Location = new Point(base.ClientSize.Width - cornerPanel.Width, base.ClientSize.Height - cornerPanel.Height);
}
Each ScrollBar has their Scroll event subscribed to the following:
private void ScrollBar_Scroll(object sender, ScrollEventArgs e)
{
OnScroll(e);
}
And finally we can allow MouseWheel events to scroll with the following:
protected override void OnMouseWheel(MouseEventArgs e)
{
int xOldValue = VScrollBar.Value;
if (e.Delta > 0)
{
VScrollBar.Value = (int)Math.Max(VScrollBar.Value - (VScrollBar.SmallChange * e.Delta), 0);
OnScroll(new ScrollEventArgs(ScrollEventType.ThumbPosition, xOldValue, VScrollBar.Value, ScrollOrientation.VerticalScroll));
}
else
{
VScrollBar.Value = (int)Math.Min(VScrollBar.Value - (VScrollBar.SmallChange * e.Delta), VScrollBar.Maximum - (VScrollBar.LargeChange - 1));
OnScroll(new ScrollEventArgs(ScrollEventType.ThumbPosition, xOldValue, VScrollBar.Value, ScrollOrientation.VerticalScroll));
}
}
For custom painting, you would use the following statement:
e.Graphics.TranslateTransform(-HScrollBar.Value, -VScrollBar.Value);
This worked perfectly without the glitches present when using AutoScroll.