简体   繁体   中英

How to implement drag move snapping to screen edges and corners for WPF Window?

I am trying to implement a basic window snapping to screen edges and corners using the first answer here: How to automatically snap a WPF window to an edge of the screen while retaining its size?

But the window is increasingly sliding as you move the window using mouse down events. Also the snapping has considerable gap on all sides except the top part. I tried subtracting this but it didn't fix it:

SystemParameters.WindowNonClientFrameThickness.Left

Here is the full code I am using:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace snapwindow
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow ( )
        {
            InitializeComponent ( );
        }

        private Point offset = new Point ( );

        private void OnMouseLeftButtonUp ( object sender, MouseButtonEventArgs e )
        {
            Mouse.Capture ( null );
        }

        private void OnMouseMove ( object sender, MouseEventArgs e )
        {
            if ( Mouse.Captured == this && Mouse.LeftButton == MouseButtonState.Pressed )
            {
                Point cursorPos = PointToScreen ( Mouse.GetPosition ( this ) );
                double newLeft = cursorPos.X - offset.X;
                double newTop = cursorPos.Y - offset.Y;

                int snappingMargin = 100;

                if ( Math.Abs ( SystemParameters.WorkArea.Left - newLeft ) < snappingMargin )
                    newLeft = SystemParameters.WorkArea.Left;
                else if ( Math.Abs ( newLeft + this.ActualWidth - SystemParameters.WorkArea.Left - SystemParameters.WorkArea.Width ) < snappingMargin )
                    newLeft = SystemParameters.WorkArea.Left + SystemParameters.WorkArea.Width - this.ActualWidth;

                if ( Math.Abs ( SystemParameters.WorkArea.Top - newTop ) < snappingMargin )
                    newTop = SystemParameters.WorkArea.Top;
                else if ( Math.Abs ( newTop + this.ActualHeight - SystemParameters.WorkArea.Top - SystemParameters.WorkArea.Height ) < snappingMargin )
                    newTop = SystemParameters.WorkArea.Top + SystemParameters.WorkArea.Height - this.ActualHeight;

                // here you can change the window position and implement
                // the snapping behaviour that you need

                this.Left = newLeft;
                this.Top = newTop;
            }
        }

        private void OnMouseLeftButtonDown ( object sender, MouseButtonEventArgs e )
        {
            Point cursorPos = PointToScreen ( Mouse.GetPosition ( this ) );
            Point windowPos = new Point ( this.Left, this.Top );
            offset = ( Point ) ( cursorPos - windowPos );

            // capturing the mouse here will redirect all events to this window, even if
            // the mouse cursor should leave the window area
            Mouse.Capture ( this, CaptureMode.Element );
        }
    }
}

EDIT: here is how it looks with the new code:

在此处输入图像描述

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace snapwindow
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow ( )
        {
            InitializeComponent ( );
        }

        [DllImport ( "user32.dll" )]
        static extern int GetSystemMetrics ( int smIndex );

        /// <summary>
        /// The default width, in pixels, of a maximized top-level window on the primary display monitor.
        /// </summary>
        const int SM_CXMAXIMIZED = 61;

        /// <summary>
        /// The default maximum width of a window that has a caption and sizing borders, in pixels.
        /// This metric refers to the entire desktop. The user cannot drag the window frame to a size larger than these dimensions.
        /// A window can override this value by processing the WM_GETMINMAXINFO message.
        /// </summary>
        private const int SM_CXMAXTRACK = 59;

        /// <summary>
        /// The default height, in pixels, of a maximized top-level window on the primary display monitor.
        /// </summary>
        private const int SM_CYMAXIMIZED = 62;

        /// <summary>
        /// The default maximum height of a window that has a caption and sizing borders, in pixels. This metric refers to the entire desktop.
        /// The user cannot drag the window frame to a size larger than these dimensions. A window can override this value by processing
        /// the WM_GETMINMAXINFO message.
        /// </summary>
        private const int SM_CYMAXTRACK = 60;

        private const int SnapThreshold = 100;

        protected override void OnLocationChanged ( EventArgs e )
        {
            base.OnLocationChanged ( e );

            var currentWindowBounds = new Rect ( new Point ( this.Left, this.Top ), this.RenderSize );
            currentWindowBounds.Inflate ( SnapThreshold, 0 );

            Rect screenBounds = SystemParameters.WorkArea;
            int totalDesktopWidth = GetSystemMetrics ( SM_CXMAXIMIZED );
            double systemScreenGutterWidth = ( totalDesktopWidth - screenBounds.Width ) / 2;
            screenBounds.Inflate ( systemScreenGutterWidth, 0 );

            if ( screenBounds.Contains ( currentWindowBounds ) )
            {
                return;
            }

            this.Left = currentWindowBounds switch
            {
                { Left: var left } when left < screenBounds.Left => screenBounds.Left,
                { Right: var right } when right > screenBounds.Right => screenBounds.Right - this.ActualWidth,
                _ => this.Left,
            };
        }
    }
}

Your computations are too complicated and wrong. Because they are wrong, the dragged Window appears to skit under the cursor.

You must know that the screen size that WPF returns is the pure client area:
client_area = desktop_size - system_areas , where system_areas are restricted OS areas like taskbar or docking gutters (for the split screen feature).
To get the full desktop size you would have to use the low-level Win32 API.

If you also want to handle the scenario when the user drags the Window by using the non-client area of the Window (title bar), you should handle Window.LocationChanged event by overriding the protected Window.OnLocationChanged method.
Handling WPF mouse input events like MouseMove won't work properly as they are only raised inside the WPF client area.

The Rect type exposes a very useful API. Whenever you are working with screen areas or graphic areas in general, you should abstract them as positionable rectangles. It will simplify your computations significantly.

An improved version could look as follows:

MainWindow.xaml.cs
This example implements horizontal snapping. It uses a snap threshold to add the visual snapping effect.

[DllImport("user32.dll")]
static extern int GetSystemMetrics(int smIndex);

/// <summary>
/// The default width, in pixels, of a maximized top-level window on the primary display monitor.
/// </summary>
const int SM_CXMAXIMIZED = 61;

/// <summary>
/// The default height, in pixels, of a maximized top-level window on the primary display monitor.
/// </summary>
private const int SM_CYMAXIMIZED = 62;

/// <summary>
/// The default maximum width of a window that has a caption and sizing borders, in pixels.
/// This metric refers to the entire desktop. The user cannot drag the window frame to a size larger than these dimensions.
/// A window can override this value by processing the WM_GETMINMAXINFO message.
/// </summary>
private const int SM_CXMAXTRACK = 59;

/// <summary>
/// The default maximum height of a window that has a caption and sizing borders, in pixels. This metric refers to the entire desktop.
/// The user cannot drag the window frame to a size larger than these dimensions. A window can override this value by processing
/// the WM_GETMINMAXINFO message.
/// </summary>
private const int SM_CYMAXTRACK = 60;

/// <summary>
/// The width of the client area for a full-screen window on the primary display monitor, in pixels.
/// To get the coordinates of the portion of the screen that is not obscured by the system taskbar or by application desktop toolbars,
/// call the SystemParametersInfofunction with the SPI_GETWORKAREA value.
/// </summary>
private const int SM_CXFULLSCREEN = 16;

/// <summary>
/// The height of the client area for a full-screen window on the primary display monitor, in pixels.
/// To get the coordinates of the portion of the screen not obscured by the system taskbar or by application desktop toolbars,
/// call the SystemParametersInfo function with the SPI_GETWORKAREA value.
/// </summary>
private const int SM_CYFULLSCREEN = 17;


/// <summary>
/// The width of the screen of the primary display monitor, in pixels. This is the same value obtained by calling 
/// GetDeviceCaps as follows: GetDeviceCaps( hdcPrimaryMonitor, HORZRES).
/// </summary>
private const int SM_CXSCREEN = 0;

/// <summary>
/// The height of the screen of the primary display monitor, in pixels. This is the same value obtained by calling 
/// GetDeviceCaps as follows: GetDeviceCaps( hdcPrimaryMonitor, VERTRES).
/// </summary>
private const int SM_CYSCREEN = 1;

private const int SnapThreshold = 100;

protected override void OnLocationChanged(EventArgs e)
{
  base.OnLocationChanged(e);

  var currentWindowBounds = new Rect(new Point(this.Left, this.Top), this.RenderSize);
  currentWindowBounds.Inflate(SnapThreshold, 0);

  // int clientDesktopWidth = GetSystemMetrics(SM_CXFULLSCREEN);
  // int clientDesktopHeight = GetSystemMetrics(SM_CYFULLSCREEN);
  // var screenBounds = new Rect(new Size(clientDesktopWidth, clientDesktopHeight));
  var screenBounds = SystemParameters.WokArea;
  int totalDesktopWidth = GetSystemMetrics(SM_CXMAXIMIZED);
  double systemScreenGutterWidth = (totalDesktopWidth - screenBounds.Width) / 2;
  screenBounds.Inflate(systemScreenGutterWidth, 0);

  if (screenBounds.Contains(currentWindowBounds))
  {
    return;
  }

  this.Left = currentWindowBounds switch
  {
    { Left: var left } when left < screenBounds.Left => screenBounds.Left,
    { Right: var right } when right > screenBounds.Right => screenBounds.Right - this.ActualWidth,
    _ => this.Left,
  };
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM