I have the following code:
<Window x:Class="Demo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Canvas Name="Canvas_Main" />
</Window>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Loaded += MainWindow_Loaded;
}
private void MainWindow_Loaded(Object sender, RoutedEventArgs e)
{
Rectangle lastRectangle = null;
Random random = new Random(0);
for (Int32 counter = 0; counter < 5; counter++)
{
Rectangle rectangle = new Rectangle();
rectangle.Fill = Brushes.Blue;
rectangle.Width = random.Next(100, 200);
rectangle.Height = counter * 100;
Canvas_Main.Children.Add(rectangle);
if (lastRectangle == null) {
Canvas.SetLeft(rectangle, 0);
Canvas.SetTop(rectangle, 0);
}
else
{
Canvas.SetLeft(rectangle, lastRectangle.ActualWidth);
Canvas.SetTop(rectangle, 0);
}
lastRectangle = rectangle;
}
}
}
This isn't working as expected (laying each rectangle diagonally next to each other), as lastRectangle.ActualWidth is 0. As I understand things from this answer, it is because lastRectangle has not been measured and arranged.
I am curious, at what point would the measuring and arranging be done, if not when added to a container that is already visible and loaded?
The Framework.Loaded event of an element is raised after the measure and arrange layout pass, but before the rendering of the element.
The complete layout pass is initialized when UIElement.InvalidateMeasure for an asynchronous layout pass or UIElement.UpdateLayout for a synchronous layout pass was invoked on the element.
In your scenario the Window.Loadedevent handler is invoked, which means that the Window and the Canvas are both loaded (but not rendered).
Now you start to add new UIElement elements to the Canvas.
Canvas.Children.Add should invoke the InvalidateMeasure method. Because InvalidateMeasure triggers an asynchronous layout pass, the Canvas and therefore the current child element will be enqueued into the layout queue and the context continues execution (adding more rectangles).
Because there is already a pending layout pass due to the freshly added element, you should avoid calling UIElement.Measure manually (this are recursive calls and quite expensive when considering performance).
Once the context has completed, the elements in the queue, that are waiting for their layout pass and final rendering, will be handled and MeasureOverride and ArrangeOverride are invoked recursively on those elements (Canvas and its children).
As a result, UIElement.RenderSize can be calculated by the layout system.
At this moment, the new FrameworkElement.ActualWidth will be available.
This is the moment the FrameworkElement.Loaded event of the added elements (the rectangles) is finally raised.
To solve your problem you have to either use Rectangle.Width instead
or wait for each Rectangle.Laoded event before adding the next:
private int ShapeCount { get; set; }
private const int MaxShapes = 5;
private Point ShapeBPosition { get; set; }
private void MainWindow_Loaded(Object sender, RoutedEventArgs e)
{
this.ShapePosition = new Point();
AddRectangle(this.ShapePosition);
}
private void AddRectangle(Point position)
{
Random random = new Random();
Rectangle rectangle = new Rectangle();
rectangle.Fill = Brushes.Blue;
rectangle.Width = random.Next(100, 200);
rectangle.Height = ++this.ShapeCount * 100;
Canvas_Main.Children.Add(rectangle);
Canvas.SetLeft(rectangle, position.X);
Canvas.SetTop(rectangle, position.Y);
rectangle.Loaded += OnRectangleLoaded;
}
private void OnRectangleLoaded(object sender, RoutedEventArgs e)
{
var rectangle = sender as Rectangle;
rectangle.Loaded -= OnRectangleLoaded;
if (this.ShapeCount == MainWindow.MaxShapes)
{
return;
}
// this.ShapePosition is struct => modify copy
Point position = this.ShapePosition;
position.Offset(rectangle.ActualWidth, 0);
this.ShapePosition = position;
AddRectangle(this.ShapePosition);
}
The measuring and arranging has been completed for the window. But you are now creating new child controls, which will ordinarily not be measured/arranged until the window's next layout pass.
You can force this to happen immediately by calling the Measure method of each rectangle at the end of the loop.
Related
I am struggling with making both touch events and manipulation work properly in a WPF project. I have a ScrollViewer which contains a picture and I would like to scroll both horizontally and vertically using a swipe gestures. Additionally, I would like to zoom in/out in the center of the pinch gesture. The code below achieves what I wish, but it has the following problems:
Sometimes the scrolling is laggy;
The scrolling does not work on the first try, only when attempting the same gesture a second time;
The zoom in/out does not work on the first try, only when attempting the same gesture a second time.
I enabled the IsManipulationEnabled and I implemented the code for zoom in/out functionality. However, I was not able to combine it with the scrolling functionality (by setting the PanningMode in the ScrollViewer only). Therefore, I created a custom control which inherits from Image control and I overwritten the OnTouchDown and OnTouchUp event handlers. Basically, what I am doing in these overwritten handlers is counting the number of touches on the screen and enabling/disabling manipulation. I also tried setting the PanningMode for the ScrollViewer, but it did not do the trick.
Below is the XAML:
<Grid>
<ScrollViewer
x:Name="ScrollViewerParent"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto"
PanningMode="Both">
<local:CustomImage
x:Name="MainImage"
Source="{Binding Source={x:Static local:Constants.ImagePath}}"
IsManipulationEnabled="True"
ManipulationStarting="MainImage_ManipulationStarting"
ManipulationDelta="MainImage_ManipulationDelta">
</local:CustomImage>
</ScrollViewer>
</Grid>
Here is the code-behind:
public partial class MainWindow : Window
{
private void MainImage_ManipulationStarting(object sender, ManipulationStartingEventArgs e)
{
e.ManipulationContainer = ScrollViewerParent;
e.Handled = true;
}
private void MainImage_ManipulationDelta(object sender, ManipulationDeltaEventArgs e)
{
var matrix = MainImage.LayoutTransform.Value;
Point? centerOfPinch = (e.ManipulationContainer as FrameworkElement)?.TranslatePoint(e.ManipulationOrigin, ScrollViewerParent);
if (centerOfPinch == null)
{
return;
}
var deltaManipulation = e.DeltaManipulation;
matrix.ScaleAt(deltaManipulation.Scale.X, deltaManipulation.Scale.Y, centerOfPinch.Value.X, centerOfPinch.Value.Y);
MainImage.LayoutTransform = new MatrixTransform(matrix);
Point? originOfManipulation = (e.ManipulationContainer as FrameworkElement)?.TranslatePoint(e.ManipulationOrigin, MainImage);
double scrollViewerOffsetX = ScrollViewerParent.HorizontalOffset;
double scrollViewerOffsetY = ScrollViewerParent.VerticalOffset;
double pointMovedOnXOffset = originOfManipulation.Value.X - originOfManipulation.Value.X * deltaManipulation.Scale.X;
double pointMovedOnYOffset = originOfManipulation.Value.Y - originOfManipulation.Value.Y * deltaManipulation.Scale.Y;
double multiplicatorX = ScrollViewerParent.ExtentWidth / MainImage.ActualWidth;
double multiplicatorY = ScrollViewerParent.ExtentHeight / MainImage.ActualHeight;
ScrollViewerParent.ScrollToHorizontalOffset(scrollViewerOffsetX - pointMovedOnXOffset * multiplicatorX);
ScrollViewerParent.ScrollToVerticalOffset(scrollViewerOffsetY - pointMovedOnYOffset * multiplicatorY);
e.Handled = true;
}
}
The XAML for the custom control:
<Style TargetType="{x:Type local:CustomImage}" />
Here is where I override the OnTouchDown and OnTouchUp event handlers:
public class CustomImage : Image
{
private volatile int nrOfTouchPoints;
private volatile bool isManipulationReset;
private object mutex = new object();
static CustomImage()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomImage), new FrameworkPropertyMetadata(typeof(CustomImage)));
}
protected override void OnTouchDown(TouchEventArgs e)
{
lock (mutex)
{
nrOfTouchPoints++;
if (nrOfTouchPoints >= 2)
{
IsManipulationEnabled = true;
isManipulationReset = false;
}
}
base.OnTouchDown(e);
}
protected override void OnTouchUp(TouchEventArgs e)
{
lock (mutex)
{
if (!isManipulationReset)
{
IsManipulationEnabled = false;
isManipulationReset = true;
nrOfTouchPoints = 0;
}
}
base.OnTouchUp(e);
}
}
What I expect from this code is the following:
When using one finger to swipe horizontally or vertically across the touchscreen, the image should be scrolled accordingly;
When I use a pinch gesture on the touch screen, the image should be zoomed in/out in the center of the pinch.
Fortunately, I managed to find the perfect solution. Therefore, I am going to post the answer in the case that someone is working on a similar problem and needs some help.
What I did:
Got rid of the custom control as it was not necessary;
Create a field which counts the number of the touch points;
Implemented the TouchDown event handler, which increases the number of touch points by 1 (this method is called each time there is a touch down gesture on the device);
Implemented the TouchUp event handler, which decreases the number of touch points by 1 (this method is called each time there is a touch up gesture on the device);
In the Image_ManipulationDelta event handler, I check the number of touch points:
if the number of touch points < 2, then the translation value is added to the current offset of the scrollbars, thus achieving scrolling;
otherwise, the center of the pinch is calculated and a scale gesture is applied.
Here is the full XAML:
<Grid
x:Name="GridParent">
<ScrollViewer
x:Name="ScrollViewerParent"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto"
PanningMode="Both">
<Image
x:Name="MainImage"
Source="{Binding Source={x:Static local:Constants.ImagePath}}"
IsManipulationEnabled="True"
TouchDown="MainImage_TouchDown"
TouchUp="MainImage_TouchUp"
ManipulationDelta="Image_ManipulationDelta"
ManipulationStarting="Image_ManipulationStarting"/>
</ScrollViewer>
</Grid>
Here is the entire code discussed above:
public partial class MainWindow : Window
{
private volatile int nrOfTouchPoints;
private object mutex = new object();
public MainWindow()
{
InitializeComponent();
DataContext = this;
}
private void Image_ManipulationStarting(object sender, ManipulationStartingEventArgs e)
{
e.ManipulationContainer = ScrollViewerParent;
e.Handled = true;
}
private void Image_ManipulationDelta(object sender, ManipulationDeltaEventArgs e)
{
int nrOfPoints = 0;
lock (mutex)
{
nrOfPoints = nrOfTouchPoints;
}
if (nrOfPoints >= 2)
{
DataLogger.LogActionDescription($"Executed {nameof(Image_ManipulationDelta)}");
var matrix = MainImage.LayoutTransform.Value;
Point? centerOfPinch = (e.ManipulationContainer as FrameworkElement)?.TranslatePoint(e.ManipulationOrigin, ScrollViewerParent);
if (centerOfPinch == null)
{
return;
}
var deltaManipulation = e.DeltaManipulation;
matrix.ScaleAt(deltaManipulation.Scale.X, deltaManipulation.Scale.Y, centerOfPinch.Value.X, centerOfPinch.Value.Y);
MainImage.LayoutTransform = new MatrixTransform(matrix);
Point? originOfManipulation = (e.ManipulationContainer as FrameworkElement)?.TranslatePoint(e.ManipulationOrigin, MainImage);
double scrollViewerOffsetX = ScrollViewerParent.HorizontalOffset;
double scrollViewerOffsetY = ScrollViewerParent.VerticalOffset;
double pointMovedOnXOffset = originOfManipulation.Value.X - originOfManipulation.Value.X * deltaManipulation.Scale.X;
double pointMovedOnYOffset = originOfManipulation.Value.Y - originOfManipulation.Value.Y * deltaManipulation.Scale.Y;
double multiplicatorX = ScrollViewerParent.ExtentWidth / MainImage.ActualWidth;
double multiplicatorY = ScrollViewerParent.ExtentHeight / MainImage.ActualHeight;
ScrollViewerParent.ScrollToHorizontalOffset(scrollViewerOffsetX - pointMovedOnXOffset * multiplicatorX);
ScrollViewerParent.ScrollToVerticalOffset(scrollViewerOffsetY - pointMovedOnYOffset * multiplicatorY);
e.Handled = true;
}
else
{
ScrollViewerParent.ScrollToHorizontalOffset(ScrollViewerParent.HorizontalOffset - e.DeltaManipulation.Translation.X);
ScrollViewerParent.ScrollToVerticalOffset(ScrollViewerParent.VerticalOffset - e.DeltaManipulation.Translation.Y);
}
}
private void MainImage_TouchDown(object sender, TouchEventArgs e)
{
lock (mutex)
{
nrOfTouchPoints++;
}
}
private void MainImage_TouchUp(object sender, TouchEventArgs e)
{
lock (mutex)
{
nrOfTouchPoints--;
}
}
}
}
I'm attempting to emulate the behavior of Windows 10's Virtual Touchpad, ie when a user touches a control inside the app and moves their finger around, the system cursor will mirror their movement. However, I notice that there seems to be some conflict between the system processing touch and mouse inputs simultaneously, where the touch input wants to hide the cursor, and the mouse input wants to show the cursor.
I started by building a boilerplate UWP App in VS-2019, here's my MainPage.xaml, where the only thing I'm changing is giving my Grid the Name="Touchpad" property so I can track pointer events within it:
<Page
x:Class="Playground.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Playground"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid Background="Black" Name="Touchpad">
</Grid>
</Page>
I'm using Windows.UI.Input.Preview.Injection to move the mouse cursor. Because this is a Restricted Capability I made changes to my project as defined here: https://learn.microsoft.com/en-us/uwp/api/windows.ui.input.preview.injection#remarks
public sealed partial class MainPage : Page
{
private Point lastPosition;
private InputInjector inputInjector = InputInjector.TryCreate();
public MainPage()
{
this.InitializeComponent();
ApplicationView.PreferredLaunchViewSize = new Size(480, 480);
ApplicationView.PreferredLaunchWindowingMode = ApplicationViewWindowingMode.PreferredLaunchViewSize;
Touchpad.PointerPressed += new PointerEventHandler(Touchpad_PointerPressed);
Touchpad.PointerReleased += new PointerEventHandler(Touchpad_PointerReleased);
Touchpad.PointerMoved += new PointerEventHandler(Touchpad_PointerMoved);
}
private void Touchpad_PointerMoved(object sender, PointerRoutedEventArgs e)
{
e.Handled = true;
PointerPoint pointer = e.GetCurrentPoint(Touchpad);
Point currentPosition = pointer.Position;
if (pointer.PointerDevice.PointerDeviceType == Windows.Devices.Input.PointerDeviceType.Touch &&
lastPosition != currentPosition &&
pointer.Properties.IsPrimary == true)
{
Point delta = new Point(currentPosition.X - lastPosition.X, currentPosition.Y - lastPosition.Y);
InjectedInputMouseInfo mouseInfo = new InjectedInputMouseInfo();
mouseInfo.MouseOptions = InjectedInputMouseOptions.Move;
mouseInfo.DeltaX = (int)delta.X;
mouseInfo.DeltaY = (int)delta.Y;
if (inputInjector != null)
{
inputInjector.InjectMouseInput(new[] { mouseInfo });
}
lastPosition = currentPosition;
}
}
private void Touchpad_PointerReleased(object sender, PointerRoutedEventArgs e)
{
e.Handled = true;
lastPosition = new Point(0,0);
}
private void Touchpad_PointerPressed(object sender, PointerRoutedEventArgs e)
{
e.Handled = true;
PointerPoint pointer = e.GetCurrentPoint(Touchpad);
if (pointer.PointerDevice.PointerDeviceType == Windows.Devices.Input.PointerDeviceType.Touch &&
pointer.Properties.IsPrimary == true)
{
lastPosition = pointer.Position;
Window.Current.CoreWindow.PointerCursor = null;
}
}
}
I noticed that while moving my finger around, there seems to be two cursors (or one that's bouncing back and forth really fast) - one in the app itself, and one in the system/desktop. I added Window.Current.CoreWindow.PointerCursor = null; which successfully hides the cursor inside the app, but the one that's being driven by the injection is blinking/flickering.
I feel like I'm missing something, as the feedback on the Virtual Touchpad in Win10 is buttery smooth and is also a UWP app, as far as I know...
I have made a program where you ask the number of ellipses and it makes them in a different window in c#,but I want to have a mouse over effect-which I understood is called : MouseEnter and an onclick event,which I understood is called MouseDown, but I made an array of ellipses and I tried the following :
namespace WpfApp1
{
/// <summary>
/// Interaction logic for Window2.xaml
/// </summary>
public partial class Window2 : Window
{
int numOfElipses;
public Window2()
{
InitializeComponent();
numOfElipses= MainWindow.numOfElipse;
Ellipse[] ellipsePoints = new Ellipse[numOfElipses];
Random rnd = new Random();
for (int i=0;i<numOfElipses; i++)
{
SolidColorBrush brush =
new SolidColorBrush(
Color.FromRgb(
(byte)rnd.Next(255),
(byte)rnd.Next(255),
(byte)rnd.Next(255)
));
var top = rnd.Next(0, 280);
var left = rnd.Next(0, 450);
ellipsePoints[i] = new Ellipse();
ellipsePoints[i].Width = 40;
ellipsePoints[i].Height = 40;
Canvas.SetTop(ellipsePoints[i], i);
Canvas.SetLeft(ellipsePoints[i], i*45);
ellipsePoints[i].Fill = brush;
c1.Children.Add(ellipsePoints[i]);
}
}
private void E1_MouseEnter(object sender, MouseEventArgs e)
{
Random r = new Random();
Ellipse ellipsePoints = (Ellipse)sender;
ellipsePoints.Fill = new
SolidColorBrush(Color.FromRgb((byte)r.Next(255), (byte)r.Next(255),
(byte)r.Next(255)));
}
private void E1_MouseDown(object sender, MouseButtonEventArgs e)
{
c1.Children.Remove((Ellipse)sender);
}
}
}
but it doesn't work.Can anyone explain why and how do I make it change color on a mouse over(hover) randomly,and disappear/be removed on a mouse click?
I would really appreciate any help!
As mentioned in the comments, you need to actually hook up the events to the ellipses you are creating:
...
ellipsePoints[i].MouseEnter += E1_MouseEnter; // "hook up" the Mouse Enter event
ellipsePoints[i].MouseDown += E1_MouseDown; // "hook up" the Mouse Down event
c1.Children.Add(ellipsePoints[i]);
...
Simply creating the E1_MouseEnter and E1_MouseDown methods does not automatically wire them up, and that makes sense when we think about it. There could be any number of objects on the Window that have those events - how is the code supposed to know who should listen to?
I try to make a graphics.
When I click on my label, I want to draw a line. It works, it draw my line but at the last point there is another line going at the left top corner.. I don't know why.
(It's useless, but it's for another project, I try to understand how works the drawing)
Here's my code :
public partial class Form1 : Form
{
Pen myPen = new Pen(Color.Blue);
Graphics g = null;
int start_x = 0, start_y;
Point[] points = new Point[1000];
int test = 0;
public Form1()
{
InitializeComponent();
start_y = canvas.Height / 2;
points[0] = new Point (start_x,start_y);
myPen.Width = 1;
}
private void drawLine()
{
g.DrawLines(myPen, points);
}
private void incrementation_Click(object sender, EventArgs e)
{
test = test + 1;
incrementation.Text = test.ToString();
if(test == 1)
{
points[1] = new Point(100, start_y);
}
if (test == 2)
{
points[test] = new Point(200, 90),new Point(220, 10);
}
if (test == 3)
{
points[test] = new Point(220, 10);
drawLine();
}
}
private void canvas_Paint(object sender, PaintEventArgs e)
{
g = canvas.CreateGraphics();
}
}
A couple of issues.
You don't assign any values to points after points[3].
Point is a structure and will have a value of [0,0] at all further elements
so your lines go there.. (all 996 of them ;-)
There is more you should change:
Do the drawing in the Paint event or trigger it from there.
Do not store the Paint e.Grahpics object. You can pass it out to use it, but don't try to hold on to it.
After adding or changing the points, write canvas.Invalidate() to trigger the Paint event. This will make your drawing persistent.
To learn about persistent drawing minimize & restore the form!
Also you should use a List<Point> instead of an array. This lets you add Points without having to decide on the number of Points you want to support..
To create a new Point you write something like this:
points.Add(new Point(100, start_y) );
To draw you then use this format in the Paint event::
e.Graphics.DrawLines(myPen, points.toArray());
In the constructor you're filling first point as
points[0] = new Point (start_x,start_y);
At this moment, start_x = 0 (since you're not assigned anything else to it after declaration int start_x = 0).
Then in incrementation_Click you're assigning points[1], points[2] and points[3], but you don't changing anywhere in your code points[0].
So when you calling g.DrawLines - first point will always be (0, canvas.Height / 2)
Aside from this:
You don't need to create graphics explicitly in _Paint event handler since it can accessed as e.Graphics.
It's better to move all paintings into canvas_Paint like:
private void canvas_Paint(object sender, PaintEventArgs e)
{
e.Graphics.DrawLines(myPen, points);
}
and in your _Click handler instead of calling drawLine you should only call canvas.Refresh()
I'm trying to implement a WPF application with drag&drop functionality using MouseDragElementBehavior.
But I just can't find a way to get the dropped elements position relative to its parent Canavs.
Example code:
namespace DragTest {
public partial class MainWindow : Window {
private Canvas _child;
public MainWindow() {
InitializeComponent();
Canvas parent = new Canvas();
parent.Width = 400;
parent.Height = 300;
parent.Background = new SolidColorBrush(Colors.LightGray);
_child = new Canvas();
_child.Width = 50;
_child.Height = 50;
_child.Background = new SolidColorBrush(Colors.Black);
MouseDragElementBehavior dragBehavior = new MouseDragElementBehavior();
dragBehavior.Attach(_child);
dragBehavior.DragBegun += onDragBegun;
dragBehavior.DragFinished += onDragFinished;
Canvas.SetLeft(_child, 0);
Canvas.SetTop(_child, 0);
parent.Children.Add(_child);
Content = parent;
}
private void onDragBegun(object sender, MouseEventArgs args) {
Debug.WriteLine(Canvas.GetLeft(_child));
}
private void onDragFinished(object sender, MouseEventArgs args) {
Debug.WriteLine(Canvas.GetLeft(_child));
}
}
}
After Dropping the child Canvas the value of Canvas.GetLeft(_child) is still 0.
Why that? Why doesn't it change?
Sure, I can get the new position by using dragBehavior.X, but that's the child Canvas' position in the main window, not the position relative to the parent Canvas. There must be a way to get it...
I just found a workaround:
private void onDragFinished(object sender, MouseEventArgs args) {
Point windowCoordinates = new Point(((MouseDragElementBehavior)sender).X, ((MouseDragElementBehavior)sender).Y);
Point screenCoordinates = this.PointToScreen(windowCoordinates);
Point parentCoordinates = _parent.PointFromScreen(screenCoordinates);
Debug.WriteLine(parentCoordinates);
}
So I simple convert the point to screen coordinates, then from screen coordinates to the parents coordinates.
Nevertheless there will be problems if the parent Canvas is in some ScrollView or somthing.
There doesn't seem to be a simple solution with this Drag&Drop approach...