WPF SkiaSharp Canvas Click Mouse position - c#

Note: This is a C# SkiaSharp WPF application.
When I repaint my SKCanvas (which is the full width of my app), I record the width and height. Ignore height for now. On a 4K monitor the width is recorded correctly 3840 pixels (when maximised).
When I click on the canvas far left, the mouse X position recorded is almost zero as expected.
However, when I click on the far right of the canvas 2552 is given as the X position, not something near 3840 as I expected?
This is the code I use to get the mouse position when it is clicked.
private void MyCanvas_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
var pixelPosition = e.GetPosition(sender as SKElement);
// ...
}
and my XAML:
<skia:SKElement
Name="MyCanvas"
PaintSurface="MyCanvas_PaintSurface"
MouseLeftButtonDown="MyCanvas_MouseLeftButtonDown"
/>
I also checked the width/height in the SizeChanged event of MyCanvas and that does NOT report 3840 either, it reports 2552 as the width?
Any ideas why the reported width is 2552 whereas the actual width is 3840?

As #Leandro pointed out your DPI scaling is 150%. You can get the DPI scale in .NET Framework >= 4.6.2 and .NET Core >= 3.0 with the GetDpi method of VisualTreeHelper. It returns a DpiScale struct that contains the scale horizontal and vertical DPI scale factors.
private void MyCanvas_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
var mainWindow = Application.Current.MainWindow;
var dpiScale = VisualTreeHelper.GetDpi(mainWindow);
var dpiScaleX = dpiScale.DpiScaleX;
var dpiScaleY = dpiScale.DpiScaleY;
var pixelPosition = e.GetPosition(sender as Canvas);
var scaledPixelPosition = new System.Windows.Point(pixelPosition.X * dpiScaleX, pixelPosition.Y * dpiScaleY);
// ...your code.
}
If you are working with a lower version of a framework without GetDpi, you can use this instead.
var mainWindow = Application.Current.MainWindow;
Matrix m = PresentationSource.FromVisual(mainWindow).CompositionTarget.TransformToDevice;
var dpiScaleX = m.M11;
var dpiScaleY = m.M22;

Related

Resizing drawlines on a paint event

I've seen few questions about this problem, I tried every solution but none of them worked for my case.
My code is working; this image shows what happens when I click on Draw button.
I need to zoom on that drawing.Is it possible to code something like autocad feature "zoom/extent"?
Pen myPen = new Pen(Color.Black);
int centerpointx, centerpointy;
private void pictureBoxDraw_Paint(object sender, PaintEventArgs e)
{
centerpointx = pictureBoxDraw.Size.Width/2;
centerpointy = pictureBoxDraw.Size.Height/2;
myPen.Width = 2;
if (binary > 0)
{
var sizecrestgeo = 40;
var distancearraycrestgeo = new float[sizecrestgeo];
var elevationarraycrestgeo = new float[sizecrestgeo];
for (int i = 0; i < sizecrestgeo; i++)
{
distancearraycrestgeo[i] = float.Parse(dataGridViewCrestGeo.Rows[i].Cells[0].Value.ToString());
elevationarraycrestgeo[i] = float.Parse(dataGridViewCrestGeo.Rows[i].Cells[1].Value.ToString())*-1;
}
for (int i=0; i < sizecrestgeo-1; i++)
{
e.Graphics.DrawLine(myPen, distancearraycrestgeo[i]+centerpointx, elevationarraycrestgeo[i]+centerpointy, distancearraycrestgeo[i + 1]+centerpointx, elevationarraycrestgeo[i + 1]+centerpointy);
}
}
else
{
}
}
private void buttonDraw_Click_1(object sender, EventArgs e)
{
if (Hd > 0.0001)
{
binary = 1;
pictureBoxDraw.Invalidate();
}
else
{
MessageBox.Show("No data to draw, perform analysis first.");
}
}
private void buttoncleardraw_Click(object sender, EventArgs e)
{
binary = 0;
pictureBoxDraw.Invalidate();
}
}
This is not so hard, provided you know all the puzzle pieces.
Let's start with the obvious one:
You can scale the Graphics object to create zoomed graphics with ScaleTransform.
As I mentioned, this will include the widths of pens, font sizes and also any images you draw (though not the hatches of a HatchBrush).
You also asked about keeping the drawing 'centered'. This is a non-obvious concept: Just what is the center of your drawing surface??
When zooming (just like rotating) you always need to know the center point of the zoom (or the rotation.) By default this is the origin (0,0). I chose the center of the Panel. You may want to pick some other point..
Once you do you can move the origin of the graphics viewport to this point with TranslateTransform.
Once you have achieved all this you almost certainly will want to allow scrolling.
To do so you have two options:
You can keep AutoScroll = false and nest the canvas control inside another control, usually a Panel, which has AutoScroll = true; next make the canvas control big enough to always hold your drawing and you're done.
Or you can turn on AutoScroll for the canvas control and also set a large enough AutoScrollMinSize. If you then add the current scrolling position to the translation you are also done. Let's see this solution in action:
This is the code in the Paint event:
Size sz = panel3.ClientSize;
Point center = new Point(sz.Width / 2, sz.Height / 2);
Graphics g = e.Graphics;
// center point for testing only!
g.DrawEllipse(Pens.Orange, center.X - 3, center.Y - 3, 6, 6);
// you determine the value of the zooming!
float zoom = (trackBar1.Value+1) / 3f;
// move the scrolled center to the origon
g.TranslateTransform(center.X + panel3.AutoScrollPosition.X,
center.Y + panel3.AutoScrollPosition.Y);
// scale the graphics
g.ScaleTransform(zoom, zoom);
// draw some stuff..
using(Pen pen = new Pen(Color.Yellow, 0.1f))
for (int i = -100; i < 100; i+= 10)
g.DrawEllipse(Pens.Yellow, i-22,i-22,44,44);
A few notes:
I draw an orange circle in the center to show this point is invariant.
My coordinates go from the negative to the positive so you can see that this works nicely.
I draw with a tiny pen width; so the width of the drawing only changes once the resulting pen goes over 1 pixel. Anything draw will always be draw with 1 pxiel width, though.
I first translate and then scale so I don't have to calculate scaled poitions.
The only line in the TrackBar's Scroll event is to trigger the Paint event: panel3.Invalidate();
The only settings needed for the Panel are
panel3.AutoScroll = true;
panel3.AutoScrollMinSize = new Size(500, 500); // use the size you want to allow!
However to avoid flicker it is highly recommended to use a DoubleBuffered control, maybe a Panel subclass like this:
class DrawPanel : Panel
{
public DrawPanel() { DoubleBuffered = true; }
}
Update: Instead of a Panel, which is a Container control and not really meant to draw onto you can use a Picturebox or a Label (with Autosize=false); both have the DoubleBuffered property turned on out of the box and support drawing better than Panels do.
Graphics.ScaleTransform() is how you can zoom. Try using something like this inside your paint event handler:
e.Graphics.ScaleTransform(2.0F, 2.0F);

WPF C# multiple monitors restoring position scaling at 125%

I am having a trouble restoring floating tools in Avalondock.
The app Im developing uses avalondock for document managements with a few tools.
I usually use the tools on the 2nd monitor.
I use multiple monitors at work with 125% scaling on one and 100% scaling on the other. The main monitor is 4k monitor, the other is 2k monitor.
When i remote desktop the work pc with a single monitor (3440x1440) and ran the app, i noticed that the tools in 2nd monitors are not visible and i have no way to bring them back to main screen.
Avalondock's floating LayoutAnchorables are not treated as separate views.
if any one knows how to make LayoutAnchorable as a separate windows view, that would be the best solution. But i could not find how to do it. I tried the following
if (args.Model.IsFloating)
{
var left = (int)args.Model.FloatingLeft;
var top = (int)args.Model.FloatingTop;
var width = (int)args.Model.FloatingWidth;
var height = (int)args.Model.FloatingHeight;
var rect = new System.Drawing.Rectangle(left, top, width, height);
var intersected = Screen.AllScreens.Any(p => p.WorkingArea.IntersectsWith(rect));
if (!intersected)
{
//need to reposition
args.Model.FloatingLeft = 0;
args.Model.FloatingTop = 0;
}
//args.Model.FloatingTop;
}
System.Windows.SystemParameters.PrimaryScreenHeight 1440 double
System.Windows.SystemParameters.PrimaryScreenWidth 3440 double
System.Windows.SystemParameters.VirtualScreenHeight 1440 double
System.Windows.SystemParameters.VirtualScreenWidth 3440 double
args.Model.FloatingLeft 4133.6 double
args.Model.FloatingTop 909.6 double
WorkingArea {X = 0 Y = 0 Width = 4300 Height = 1750} System.Drawing.Rectangle
The problem is that the working area is scaled at 125%.
This makes args.Model within the bounds of the main windows.
So i guess i can't use System.Windows.Forms.Screen info because i do not know which scaling the user will be using.
How do i get the real resolutions of the multiple monitors and positions and scaling?
i found the answer myself.
I was able to make the floating windows owner to null.
Apparently the dock's floatingwindow inherit from System.Windows.Window
This is what i wanted from the beginning.
private void DockManager_LayoutUpdated(object sender, EventArgs e)
{
foreach (var floatingWindow in dockManager.FloatingWindows)
{
if (floatingWindow.Owner != null)
{
floatingWindow.Owner = null;
}
floatingWindow.ShowInTaskbar = true;
}
}

Prevent Window Boundary from Over Screen Boundary when SizeChange

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.

Transform Screen.PrimaryScreen.WorkingArea to WPF dimensions at higher DPI settings

I have the following function in my WPF application that I use to resize a window to the primary screen's working area (the whole screen minus the taskbar):
private void Window_Loaded(object sender, RoutedEventArgs e)
{
int theHeight = System.Windows.Forms.Screen.PrimaryScreen.WorkingArea.Height;
int theWidth = System.Windows.Forms.Screen.PrimaryScreen.WorkingArea.Width;
this.MaxHeight = theHeight;
this.MinHeight = theHeight;
this.MaxWidth = theWidth;
this.MinWidth = theWidth;
this.Height = theHeight;
this.Width = theWidth;
this.Top = 0;
this.Left = 0;
}
This works very well, as long as the machine's DPI is set at 100%. However, if they have the DPI set higher, then this doesn't work, and the window spills off the screen. I realize that this is because WPF pixels aren't the same as "real" screen pixels, and because I'm using a WinForms property to get the screen dimensions.
I don't know of an WPF equivalent to Screen.PrimaryScreen.WorkingArea. Is there something I can use for this that would work regardless of DPI setting?
If not, then I guess I need to some sort of scaling, but I'm not sure how to determine how much to scale by.
How can I modify my function to account for different DPI settings?
By the way, in case you're wondering why I need to use this function instead of just maximizing the window, it's because it's a borderless window (WindowStyle="None"), and if you maximize this type of window, it covers the taskbar.
You get the transformed work area size from the SystemParameters.WorkArea property:
Top = 0;
Left = 0;
Width = System.Windows.SystemParameters.WorkArea.Width;
Height = System.Windows.SystemParameters.WorkArea.Height;
In WPF, you can use the SystemParameters.PrimaryScreenWidth and SystemParameters.PrimaryScreenHeight properties to find out the primary screen dimensions:
double width = SystemParameters.PrimaryScreenWidth;
double height = SystemParameters.PrimaryScreenHeight;
If you wanna get the dimensions of both Screens you simply can use:
var primaryScreen =
System.Windows.Forms
.Screen
.AllScreens
.Where(s => s.Primary)
.FirstOrDefault();
var secondaryScreen =
System.Windows.Forms
.Screen
.AllScreens
.Where(s => !s.Primary)
.FirstOrDefault();
After this you can reach Width, Height etc. by using
primaryScreen.Bounds.Width
So Long ;)

Dynamic Margin on Window Drag

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.

Categories