So I am just trying to draw a x and y axis on a canvas in a WPF window. I had the canvas in my main window, and my draw method worked, however I tried to put this canvas in a new window and my program crashed. (When in a new window) The program crashes when it reaches a call to gCanvas.Height or gCanvas.Width, but if I change these to gCanvas.ActualHeight and gCanvas.ActualWidth the program runs, but it does not draw the x and y axis. Does anyone know why the method works when the canvas is in my main window but not in my new window, here is the code :
private void draw()
{
Line xAxis = new Line();
Line yAxis = new Line();
xAxis.Stroke = System.Windows.Media.Brushes.LightSteelBlue;
xAxis.X1 = 0;
xAxis.X2 = gCanvas.Width;
xAxis.Y1 = (gCanvas.Height / 2);
xAxis.Y2 = (gCanvas.Height / 2);
xAxis.HorizontalAlignment = HorizontalAlignment.Left;
xAxis.VerticalAlignment = VerticalAlignment.Center;
xAxis.StrokeThickness = 2;
gCanvas.Children.Add(xAxis);
yAxis.Stroke = System.Windows.Media.Brushes.LightSteelBlue;
yAxis.X1 = (gCanvas.Width / 2);
yAxis.X2 = (gCanvas.Width / 2);
yAxis.Y1 = 0;
yAxis.Y2 = gCanvas.Height;
yAxis.HorizontalAlignment = HorizontalAlignment.Left;
yAxis.VerticalAlignment = VerticalAlignment.Center;
yAxis.StrokeThickness = 2;
gCanvas.Children.Add(yAxis);
}
Here is the XAML for the 2nd window
<Window x:Class="Control.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="362" Width="412">
<Grid>
<Canvas Name="gCanvas" Margin="5" Background="White"/>
</Grid>
</Window>
and i call my draw function like this
public Window1()
{
InitializeComponent();
if (this.IsLoaded)
draw();
}
So, your question is
Does anyone know why the method works when the canvas is in my main window but not in my new window?
Well, no. You are the only person that can know that. However, from your problem description, I can tell that your gCanvas control has not been rendered at the time that you have called your draw method. I can tell this because your Exception was thrown at the gCanvas.Width call, so that must have had a value of Nan, or Not a Number.
The ActualWidth property will not return that value, so that is why that worked. As the shape was not drawn, I could therefore assume that the ActualWidth property returned 0. So if this is the case, then that points to the fact that you must be calling the draw method too early and if you're doing it in the constructor, then that is too early. Instead, handle the FrameworkElement.Loaded Event on the Window:
public YourWindow()
{
...
Loaded += YourWindow_Loaded;
}
private void YourWindow_Loaded(object sender, RoutedEventArgs e)
{
draw();
}
Related
I have a WPF window containing a Canvas which is populated with rotated Rectangles in code. The rectangles each have a MouseDown event and their positions will be distributed according to coordinates provided by the user. Often two or more will overlap, partially obstructing the rectangle beneath it.
I need the MouseDown event to fire for each rectangle that is under the mouse when it is pressed, even if that rectangle is obstructed by another rectangle, but I am only getting the MouseDown event for the topmost rectangle.
I have tried setting e.Handled for the clicked rectangle, and routing the events through the Canvas with no luck, and even gone as far as trying to locate the objects beneath the mouse based on their coordinates, but the rotation of the rectangles make that difficult to calculate.
public MainWindow()
{
InitializeComponent();
Rectangle r1 = new Rectangle() {Width = 80, Height = 120, Fill = Brushes.Blue };
r1.MouseDown += r_MouseDown;
RotateTransform rt1 = new RotateTransform(60);
r1.RenderTransform = rt1;
Canvas.SetLeft(r1, 150);
Canvas.SetTop(r1, 50);
canvas1.Children.Add(r1);
Rectangle r2 = new Rectangle() { Width = 150, Height = 50, Fill = Brushes.Green };
r2.MouseDown += r_MouseDown;
RotateTransform rt2 = new RotateTransform(15);
r2.RenderTransform = rt2;
Canvas.SetLeft(r2, 100);
Canvas.SetTop(r2, 100);
canvas1.Children.Add(r2);
}
private void r_MouseDown(object sender, MouseButtonEventArgs e)
{
Console.WriteLine("Rectangle Clicked");
}
}
There is another question that is similar to this, but it has no accepted answer and it is quite unclear as to what the final solution should be to resolve this issue. Let's see if we can be a little more clear.
First off, the solution outlined below will use the VisualTreeHelper.HitTest method in order to identify if the mouse has clicked your rectangles. The VisualTreeHelper allows us to find the rectangles even if they have moved around due to things like Canvas.SetTop and various .RenderTransform operations.
Secondly, we are going to be capturing the click event on your canvas element rather than on the individual rectangles. This allows us to handle things at the canvas level and check all the rectangles at once, as it were.
public MainWindow()
{
InitializeComponent();
//Additional rectangle for testing.
Rectangle r3 = new Rectangle() { Width = 175, Height = 80, Fill = Brushes.Goldenrod };
Canvas.SetLeft(r3, 80);
Canvas.SetTop(r3, 80);
canvas1.Children.Add(r3);
Rectangle r1 = new Rectangle() { Width = 80, Height = 120, Fill = Brushes.Blue };
RotateTransform rt1 = new RotateTransform(60);
r1.RenderTransform = rt1;
Canvas.SetLeft(r1, 100);
Canvas.SetTop(r1, 100);
canvas1.Children.Add(r1);
Rectangle r2 = new Rectangle() { Width = 150, Height = 50, Fill = Brushes.Green };
RotateTransform rt2 = new RotateTransform(15);
r2.LayoutTransform = rt2;
Canvas.SetLeft(r2, 100);
Canvas.SetTop(r2, 100);
canvas1.Children.Add(r2);
//Mouse 'click' event.
canvas1.PreviewMouseDown += canvasMouseDown;
}
//list to store the hit test results
private List<HitTestResult> hitResultsList = new List<HitTestResult>();
The HitTest method being used is the more complicated one, because the simplest version of that method only returns "the topmost" item. And by topmost, they mean the first item drawn, so it's actually visually the one on the bottom of the stack of rectangles. In order to get all of the rectangles, we need to use the complicated version of the HitTest method shown below.
private void canvasMouseDown(object sender, MouseButtonEventArgs e)
{
if (canvas1.Children.Count > 0)
{
// Retrieve the coordinates of the mouse position.
Point pt = e.GetPosition((UIElement)sender);
// Clear the contents of the list used for hit test results.
hitResultsList.Clear();
// Set up a callback to receive the hit test result enumeration.
VisualTreeHelper.HitTest(canvas1,
new HitTestFilterCallback(MyHitTestFilter),
new HitTestResultCallback(MyHitTestResult),
new PointHitTestParameters(pt));
// Perform actions on the hit test results list.
if (hitResultsList.Count > 0)
{
string msg = null;
foreach (HitTestResult htr in hitResultsList)
{
Rectangle r = (Rectangle)htr.VisualHit;
msg += r.Fill.ToString() + "\n";
}
//Message displaying the fill colors of all the rectangles
//under the mouse when it was clicked.
MessageBox.Show(msg);
}
}
}
// Filter the hit test values for each object in the enumeration.
private HitTestFilterBehavior MyHitTestFilter(DependencyObject o)
{
// Test for the object value you want to filter.
if (o.GetType() == typeof(Label))
{
// Visual object and descendants are NOT part of hit test results enumeration.
return HitTestFilterBehavior.ContinueSkipSelfAndChildren;
}
else
{
// Visual object is part of hit test results enumeration.
return HitTestFilterBehavior.Continue;
}
}
// Add the hit test result to the list of results.
private HitTestResultBehavior MyHitTestResult(HitTestResult result)
{
//Filter out the canvas object.
if (!result.VisualHit.ToString().Contains("Canvas"))
{
hitResultsList.Add(result);
}
// Set the behavior to return visuals at all z-order levels.
return HitTestResultBehavior.Continue;
}
The test example above just displays a message box showing the fill colors of all rectangles under the mouse pointer when it was clicked; verifying that VisualTreeHelper did in fact retrieve all the rectangles in the stack.
I have a canvas (not InkCanvas!) and I am able to draw Polylines on it. This is working just fine but there is a huge problem with drawing out of bounds like shown in the GIF below.
My canvas is inside a ScrollViewer and the ScrollViewer is inside a GridView.
I tried to catch the pointer leaving the canvas with the following event handlers:
canvas.PointerExited += Canvas_PointerExited;
canvas.PointerCaptureLost += Canvas_PointerCaptureLost;
But it seems those events are fired way too slow.
I tried to use the Clip property of my canvas but there is no change in behaviour. And there is no "ClipToBound" property for the UWP canvas.
My whole view is generated in Code-Behind because I have to generate multiple canvases on one view.
Is there a way to stop this behaviour?
EDIT1:
As requested: more insight of my code.
The XAML Page looks like this:
<Grid x:Name="BoundingGrid">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="15*"/>
</Grid.RowDefinitions>
<Grid x:Name="InkGrid" VerticalAlignment="Top" Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" />
<Grid x:Name="CanvasGrid" Grid.Row="1" Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" VerticalAlignment="Top"/>
</Grid>
It's all inside a Page.
My code behind looks like this:
My constructors:
public ImprovedCanvasManager(Grid boundingGrid, Grid overviewGrid, string filepath, double height)
{
drawCanvas = new Canvas();
overviewGrid.Loaded += OverviewGrid_Loaded;
overviewGrid.SizeChanged += OverviewGrid_SizeChanged;
RowDefinition rd = new RowDefinition();
rd.Height = new GridLength(height);
overviewGrid.RowDefinitions.Add(rd);
InitializeScrollViewer();
Grid.SetRow(scroll, overviewGrid.RowDefinitions.Count);
Grid.SetColumn(scroll, 0);
scroll.Content = drawCanvas;
overviewGrid.Children.Add(scroll);
LoadImage(filepath);
}
public ImprovedCanvasManager(Grid boundingGrid, Grid overviewGrid, Grid inkToolGrid, string filepath, double height = 1000) : this(boundingGrid, overviewGrid, filepath, height)
{
AddDrawingToolsToCanvas(inkToolGrid, overviewGrid);
EnableDrawingOnCanvas(drawCanvas);
}
I only got two contructors to make it simple for me to instantiate canvases with the ability to draw and without the ability to draw.
This is how i initialise my ScrollViewer:
private void InitializeScrollViewer()
{
scroll = new ScrollViewer();
scroll.VerticalAlignment = VerticalAlignment.Top;
scroll.VerticalScrollMode = ScrollMode.Auto;
scroll.HorizontalScrollMode = ScrollMode.Auto;
scroll.VerticalScrollBarVisibility = ScrollBarVisibility.Visible;
scroll.HorizontalScrollBarVisibility = ScrollBarVisibility.Visible;
scroll.ZoomMode = ZoomMode.Enabled;
scroll.ManipulationMode = ManipulationModes.All;
scroll.MinZoomFactor = 1;
scroll.MaxZoomFactor = 3;
}
Those are the only lines of code that affect any viewbuilding.
Edit 2:
My canvas doesn't fill the surrounding Grid on the left, but on the bottom.
The code in your PointerMoved handler should be relative to the canvas.
private void OnPointerMoved(object sender, PointerRoutedEventArgs e)
{
var point = e.GetCurrentPoint(canvas); // <-- relative to canvas.
var x = point.Position.X;
var y = point.Position.Y;
x = Math.Max(x, 0);
y = Math.Max(y, 0);
x = Math.Min(canvas.ActualWidth, x);
y = Math.Min(canvas.ActualHeight, y);
// add point to polyline...
}
If x/y are negative or greater than the size of the canvas, then they are out of bounds. You can either limit the point to the border of the canvas as the code above does, or you can discard the point completely.
How to prevent Window Boundary from over Screen Boundary when Window SizeChange in C# WPF?
(that's Restrict Window Boundary in Screen )
Use the Window's OnSizeChanged Event,
and do like this:
//get Screen's Width, Height
private double screenHeight = SystemParameters.FullPrimaryScreenHeight;
private double screenWidth = SystemParameters.FullPrimaryScreenWidth;
private void MultiToolWindow_OnSizeChanged(object sender, SizeChangedEventArgs e)
{
//when window RightBoundary over screen
if (this.Left + this.Width > screenWidth)
this.Width = screenWidth-this.Left; //shrink the width
//when window DownBoundary over screen
if (this.Top + this.Height > screenHeight)
this.Height = screenHeight-this.Top; //shrink the height
}
Note that when using this, Window's SizeToContent Property should be in Manual,
if not,
you can change it like this:
public void SomeMethod(){
//set to manual, this will invoke OnSizeChangedEvent at the same time but the shrink code won't work
this.SizeToContent = SizeToContent.Manual;
//this will invoke OnSizeChangedEvent and because now is manual the shrink code works
this.SizeToContent = SizeToContent.Manual;
}
do twice to ensure when window's original SizeToContent State is WidthAndHeight can take effect too,
first time will set it to Manual and the shrink code won't take effect,
and second time cause the state is manual so the shrink code will take effect.
I cannot get ActualWidth of a canvas to any nonzero value. I simplified the real scenario and devoted a new WPF Application in VS to getting a nonzero value (and understanding). Unfortunately, I'm only getting zero. I feel like I'm missing some basic WPF understanding: I'm not that familiar with WPF.
I copied the MDSN demo, modified it slightly and I now have
public partial class MainWindow : Window {
private Canvas myCanvas;
public MainWindow()
{
InitializeComponent();
CreateAndShowMainWindow();
}
private void CreateAndShowMainWindow()
{
// Create a canvas sized to fill the window
myCanvas = new Canvas();
myCanvas.Background = Brushes.LightSteelBlue;
// Add a "Hello World!" text element to the Canvas
TextBlock txt1 = new TextBlock();
txt1.FontSize = 14;
txt1.Text = "Hello World!";
Canvas.SetTop(txt1, 100);
Canvas.SetLeft(txt1, 10);
myCanvas.Children.Add(txt1);
// Add a second text element to show how absolute positioning works in a Canvas
TextBlock txt2 = new TextBlock();
txt2.FontSize = 22;
txt2.Text = "Isn't absolute positioning handy?";
Canvas.SetTop(txt2, 200);
Canvas.SetLeft(txt2, 75);
myCanvas.Children.Add(txt2);
Grid content = new Grid();
content.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
content.Children.Add(myCanvas);
this.Content = content;
this.Loaded += MainWindow_Loaded;
}
void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
var canvasRenderWidth = myCanvas.RenderSize.Width;
var canvasActualWidth = myCanvas.ActualWidth;
} //place breakpoint here
I expect the canvas to have the same ActualWidth as the textbox after loading, but at the specified breakpoint, it's zero. Notice that the textboxes are visible when running the code above.
Can someone tell me how to make myCanvas.ActualWidth to automatically become the textbox.ActualWidth or tell me why this shouldn't be done?
In my real usage scenario I've got a Canvas in a in column of a Grid, where the columndefinition's width is set to auto, so I expected it to increase as the canvas' width increases. However, this fails, and I suspect it's due to the canvas.ActualWidth being zero.
Remove this line, and it will work:
content.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
Note: The canvas is never resizing itself to fit it's content. The reason why the Text is visible in your example, is that the canvas is not clipping it's content.
when you set
myCanvas.ClipToBounds = true;
the text will also disappear.
Ok so complete rewrite of the question due to lack of replies. I want a window that is drag-able but as it's being dragged, alter the margin to extend as far as the old position of the window. I.e. Window moves right X, extend margin left X. Now I've hit a few snags such as the window having it's edges cut off for some reason. Here's my code, let me know if you can spot anything!
private void Window_LocationChanged(object sender, EventArgs e)
{
double TmpLeft = Math.Abs(this.Left - WinLeft);
double TmpTop = Math.Abs(this.Top - WinTop);
if (this.IsLoaded)
{//depending on whether the window is moved left, right
if (this.Left > WinLeft)
{//depending on whether the window is moved up, down
if (this.Top > WinTop)
bdr.Margin = new Thickness(TmpLeft, TmpTop, 0, 0);
else
bdr.Margin = new Thickness(TmpLeft, 0, 0, TmpTop);
}
else
{
if (this.Top > WinTop)
bdr.Margin = new Thickness(0, TmpTop, TmpLeft+ 40, 0);
else
bdr.Margin = new Thickness(0, 0, TmpLeft, TmpTop);
}
}
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
WinLeft = this.Left;
WinTop = this.Top;
bdr.Height = this.ActualHeight;//I set these because they are auto
bdr.Width = this.ActualWidth; //before the window opens
}
At the moment the whole window (set window background to yellow so I can see the margin) is moving, where as I want the top left corner of the margin to remain in place. I've also noticed that the area I click on to drag the window (a border) tends to move around as well so is no longer under my click as I move the window. Hope that's clear, comment any further questions.
OLD - ONLY READ TO UNDERSTAND WHAT I'M TRYING TO DO
So I'm trying to create an application that has a pop up window with a pointer/line coming from the child window to a particular place in the parent window. I achieved this like so;
<Border Name="brdr" Margin="40,0,0,0" >
//Content
</Border>
<Line
Name="Pointer"
X1="0"
X2="40"
Y1="55"
Y2="50"
StrokeThickness="2"
Stroke="Black"
></Line>
Notice the 40 left Margin on the border that makes the window larger than it appears so that the Polygon sticks out to the left and points to the parent window (If there's a better/cooler/more elegant way of doing this, I'm all ears).
So that worked fine but now I want the pointer to be dynamic. As in, if the child window gets dragged the pointer must scale relatively to the parent window's location. I.e. it must appear as if the two windows are attached via the line. Now my plan was to record the point the child window opens on (because it opens relative to the parent window, it's correct when it initialises) and then use the difference between this and the new location point (after dragging) to find the new points that the line should be going to. My code probably says it better than I ever could...
private void Window_LocationChanged(object sender, EventArgs e)
{
if (this.IsLoaded)
{
brdr.Margin = new Thickness(Math.Abs(this.Left - WinLeft) + 40, Math.Abs(this.Top - WinTop), 0, 0);
Pointer.X1 = Math.Abs(this.Left - WinLeft);
Pointer.Y1 = Math.Abs(this.Top - WinTop) + 55;
Pointer.X2 = Math.Abs(this.Left - WinLeft) + 40;
Pointer.Y2 = Math.Abs(this.Top - WinTop) + 50;
}
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
WinLeft = this.Left;
WinTop = this.Top;
}
As you can see I have to set the window margin so that it extends to the old position. Then I reset the Line coords to the new values. All these values are calculated, like I said, by comparing the opening window coords to the current coords.
My problem is, this isn't right. Would be very impressed to see someone able to figure this out.
i m surprised there's no code to switch from in-window coordinates to screen coordinates.
In a project of mine, i had to place a window right under a Control. I used this to get the screen coordinates of the point at the middle of the control:
Point point = MyControl.PointToScreen(new Point((MyControl.ActualWidth / 2)
, MyControl.ActualHeight));
PresentationSource source = PresentationSource.FromVisual(MyControl);
double dpiX = (96 * source.CompositionTarget.TransformToDevice.M11);
double dpiY = (96 * source.CompositionTarget.TransformToDevice.M22);
XInScreenCoordinates = ((point.X * 96 / dpiX));
YInScreenCoordinates = ((point.Y * 96 / dpiY));
If i didn't do this, there would be an error in placement, the more to the right the more error.
ok a small app i did :
there's MainWindow, the main window with very simple content, a line
and a Button :
<Grid>
<Line x:Name="MyLine" Stroke="Black" StrokeThickness="4" />
<Button Name="MyButton" Margin="248,101,0,0"
HorizontalAlignment="Left" VerticalAlignment="Top"
Content="Button" />
</Grid>
then i did another window, of small size (300*300) named TestWindow.
TestWindow has no content or code behind.
the code behind for MainWindow :
public partial class MainWindow : Window
{
public TestWindow MyTestWindow;
public MainWindow()
{
InitializeComponent();
MyTestWindow = new TestWindow();
MyTestWindow.Show();
MyTestWindow.LocationChanged += new EventHandler(WinLocationChanged);
MyTestWindow.SizeChanged += new SizeChangedEventHandler ( WinSizeChanged);
this.LocationChanged += new EventHandler(WinLocationChanged);
this.SizeChanged += new SizeChangedEventHandler(WinSizeChanged);
}
void WinSizeChanged(object sender, SizeChangedEventArgs e) {
UpdateLine(); }
void WinLocationChanged(object sender, EventArgs e) {
UpdateLine(); }
void UpdateLine()
{
// 1. get Window's center in in this window coordinates
double CX_sc =0.0, CY_sc = 0.0;
GetWindowCoordinatesInCurrentWindow(MyTestWindow, ref CX_sc, ref CY_sc);
// 2. Get Center of target Control coordinates in this window coordinates
Point CenterButtonPoint = MyButton.TransformToAncestor(this).Transform(new Point(MyButton.ActualWidth / 2.0, MyButton.ActualHeight / 2.0));
//3. Change line's coord.
MyLine.X1 = CX_sc;
MyLine.Y1 = CY_sc;
MyLine.X2 = CenterButtonPoint.X;
MyLine.Y2 = CenterButtonPoint.Y;
}
void GetWindowCoordinatesInCurrentWindow(Window ChildWindow, ref double X, ref double Y)
{
Point CenterOfChildWindow = ChildWindow.PointToScreen(new Point((ChildWindow.ActualWidth / 2)
, ChildWindow.ActualHeight/2));
Point UpperLeftOfCurrentWindow = this.PointToScreen(new Point(0, 0));
PresentationSource source = PresentationSource.FromVisual(this);
double dpiX = ( source.CompositionTarget.TransformToDevice.M11);
double dpiY = (source.CompositionTarget.TransformToDevice.M22);
X = (( CenterOfChildWindow.X -UpperLeftOfCurrentWindow.X ) /dpiX);
Y = ( (CenterOfChildWindow.Y-UpperLeftOfCurrentWindow.Y ) / dpiY );
}
}
What it does is that whenever one of the window is moved or resized, it will draw a
line into MainWindow that 'links' the Button to the middle of the TestWindow.
OK OK i know this is not what you want :=), since the line cannot go outside of MainWindow. But the idea is to have a transparent Popup which covers the whole screen in which you do the same thing.
Actually found a great way of doing this. What I did was I set the child window's WindowState="Maximized" then placed everything except the Line in a Canvas and by following http://denismorozov.blogspot.ie/2008/01/drag-controls-in-wpf-using.html (great article) I could make it so that I could move around my window. I had hard coded the line so that when the window opens it's in the correct position. Since one of the points of the Line shouldn't move, it was just a matter of updating the other point in the Canvas_MouseMove event like so;
Point p = border.TransformToAncestor(this).Transform(new Point(0, 0));
Line.X2 = p.X;
Line.Y2 = p.Y + 50;
Very simple way of getting your window to display relative to the parent window if you want a line joining the two. It gives the appearance of moving the window, but your actually moving a control around a maximised transparent window.