Initial commit

This commit is contained in:
2023-04-07 17:20:52 -04:00
commit 622aefa7fc
10 changed files with 1152 additions and 0 deletions

350
.gitignore vendored Normal file
View File

@@ -0,0 +1,350 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/

10
AssemblyInfo.cs Normal file
View File

@@ -0,0 +1,10 @@
using System.Windows;
[assembly: ThemeInfo(
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
//(used if a resource is not found in the page,
// or application resource dictionaries)
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
//(used if a resource is not found in the page,
// app, or any theme specific resource dictionaries)
)]

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Chris Kaczor
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

182
PInvoke.cs Normal file
View File

@@ -0,0 +1,182 @@
using System;
using System.Runtime.InteropServices;
using JetBrains.Annotations;
namespace ChrisKaczor.Wpf.Windows;
internal partial class PInvoke
{
[StructLayout(LayoutKind.Sequential)]
public struct Point
{
public int X;
public int Y;
public Point(int x, int y)
{
X = x;
Y = y;
}
public Point(System.Drawing.Point pt) : this(pt.X, pt.Y) { }
public static implicit operator System.Drawing.Point(Point p)
{
return new System.Drawing.Point(p.X, p.Y);
}
public static implicit operator Point(System.Drawing.Point p)
{
return new Point(p.X, p.Y);
}
}
[StructLayout(LayoutKind.Sequential)]
public struct Rect
{
public int Left, Top, Right, Bottom;
public Rect(int left, int top, int right, int bottom)
{
Left = left;
Top = top;
Right = right;
Bottom = bottom;
}
public Rect(System.Drawing.Rectangle r) : this(r.Left, r.Top, r.Right, r.Bottom) { }
public int X
{
get => Left;
set { Right -= (Left - value); Left = value; }
}
public int Y
{
get => Top;
set { Bottom -= (Top - value); Top = value; }
}
public int Height
{
get => Bottom - Top;
set => Bottom = value + Top;
}
public int Width
{
get => Right - Left;
set => Right = value + Left;
}
public static implicit operator System.Drawing.Rectangle(Rect r)
{
return new System.Drawing.Rectangle(r.Left, r.Top, r.Width, r.Height);
}
public static implicit operator Rect(System.Drawing.Rectangle r)
{
return new Rect(r);
}
public static bool operator ==(Rect r1, Rect r2)
{
return r1.Equals(r2);
}
public static bool operator !=(Rect r1, Rect r2)
{
return !r1.Equals(r2);
}
public bool Equals(Rect r)
{
return r.Left == Left && r.Top == Top && r.Right == Right && r.Bottom == Bottom;
}
public override bool Equals(object? obj)
{
return obj switch
{
Rect rect => Equals(rect),
System.Drawing.Rectangle rectangle => Equals(new Rect(rectangle)),
_ => false
};
}
public override int GetHashCode()
{
return ((System.Drawing.Rectangle) this).GetHashCode();
}
public override string ToString()
{
return string.Format(System.Globalization.CultureInfo.CurrentCulture, "{{Left={0},Top={1},Right={2},Bottom={3}}}", Left, Top, Right, Bottom);
}
}
public struct WindowPlacement
{
[UsedImplicitly]
public int Length;
[UsedImplicitly]
public int Flags;
[UsedImplicitly]
public uint ShowCommand;
[UsedImplicitly]
public Point MinPosition;
[UsedImplicitly]
public Point MaxPosition;
[UsedImplicitly]
public Rect NormalPosition;
}
[Flags]
public enum WindowPositionFlags
{
NoMove = 0x0002
}
[StructLayout(LayoutKind.Sequential)]
public struct WindowPosition
{
public nint Handle;
public nint HandleInsertAfter;
public int Left;
public int Top;
public int Width;
public int Height;
public WindowPositionFlags Flags;
public int Right => Left + Width;
public int Bottom => Top + Height;
public bool IsSameLocationAndSize(WindowPosition compare)
{
return compare.Left == Left && compare.Top == Top && compare.Width == Width && compare.Height == Height;
}
public bool IsSameSize(WindowPosition compare)
{
return compare.Width == Width && compare.Height == Height;
}
}
public enum WindowMessage
{
WindowPositionChanging = 0x0046,
EnterSizeMove = 0x0231,
ExitSizeMove = 0x0232
}
[LibraryImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static partial bool GetWindowPlacement(nint hWnd, ref WindowPlacement lpWindowPlacement);
}

15
README.md Normal file
View File

@@ -0,0 +1,15 @@
# ChrisKaczor.Wpf.Windows.SnappingWindow
Extends the base WPF window to allow snapping to screen edges and/or other specified windows.
## Authors
* **Chris Kaczor** - *Initial work* - https://github.com/ckaczor - https://chriskaczor.com
## License
This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details.
## Disclaimer
This code is used by my various personal projects and may not be useful or work in all use cases.

450
SnappingWindow.cs Normal file
View File

@@ -0,0 +1,450 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
using JetBrains.Annotations;
using WpfScreenHelper;
using Rectangle = System.Drawing.Rectangle;
namespace ChrisKaczor.Wpf.Windows
{
[PublicAPI]
public class SnappingWindow : Window
{
#region Member variables
private HwndSource? _hWndSource;
private bool _inSizeMove;
private PInvoke.WindowPosition _lastWindowPosition;
private List<WindowInformation>? _otherWindows;
#endregion
private class WindowBorderRectangles
{
public WindowBorderRectangles(Rectangle windowPosition, int width, bool inside)
{
var offset = inside ? width : 0;
var actualWidth = inside ? width * 2 : width;
Top = new Rectangle(windowPosition.Left, windowPosition.Top - offset, windowPosition.Width, actualWidth);
Bottom = new Rectangle(windowPosition.Left, windowPosition.Bottom - offset, windowPosition.Width, actualWidth);
Left = new Rectangle(windowPosition.Left - offset, windowPosition.Top, actualWidth, windowPosition.Height);
Right = new Rectangle(windowPosition.Right - offset, windowPosition.Top, actualWidth, windowPosition.Height);
}
public WindowBorderRectangles(PInvoke.WindowPosition windowPosition, int width, bool inside)
{
var offset = inside ? width : 0;
var actualWidth = inside ? width * 2 : width;
Top = new Rectangle(windowPosition.Left, windowPosition.Top - offset, windowPosition.Width, actualWidth);
Bottom = new Rectangle(windowPosition.Left, windowPosition.Bottom - offset, windowPosition.Width, actualWidth);
Left = new Rectangle(windowPosition.Left - offset, windowPosition.Top, actualWidth, windowPosition.Height);
Right = new Rectangle(windowPosition.Right - offset, windowPosition.Top, actualWidth, windowPosition.Height);
}
public Rectangle Top { get; }
public Rectangle Bottom { get; }
public Rectangle Left { get; }
public Rectangle Right { get; }
}
private class ResizeSide
{
public ResizeSide(PInvoke.WindowPosition position1, PInvoke.WindowPosition position2)
{
if (position1.IsSameSize(position2))
return;
IsTop = position1.Top != position2.Top;
IsBottom = position1.Bottom != position2.Bottom;
IsLeft = position1.Left != position2.Left;
IsRight = position1.Right != position2.Right;
}
public bool IsTop { get; }
public bool IsBottom { get; }
public bool IsLeft { get; }
public bool IsRight { get; }
}
#region Enumerations
private enum SnapMode
{
Move,
Resize
}
#endregion
#region Properties
protected virtual int SnapDistance => 20;
protected virtual List<WindowInformation>? OtherWindows => null;
#endregion
#region Window overrides
protected override void OnSourceInitialized(EventArgs e)
{
base.OnSourceInitialized(e);
// Store the window handle
_hWndSource = PresentationSource.FromVisual(this) as HwndSource;
// Add a hook
_hWndSource?.AddHook(WndProc);
}
protected override void OnClosed(EventArgs e)
{
base.OnClosed(e);
// Unhook the window procedure
_hWndSource?.RemoveHook(WndProc);
_hWndSource?.Dispose();
}
#endregion
#region Window procedure
protected virtual nint WndProc(nint hWnd, int msg, nint wParam, nint lParam, ref bool handled)
{
switch (msg)
{
case (int) PInvoke.WindowMessage.WindowPositionChanging:
return OnWindowPositionChanging(lParam, ref handled);
case (int) PInvoke.WindowMessage.EnterSizeMove:
// Initialize the last window position
_lastWindowPosition = new PInvoke.WindowPosition
{
Left = (int) Left,
Width = (int) Width,
Height = (int) Height,
Top = (int) Top
};
// Store the current other windows
_otherWindows = OtherWindows;
_inSizeMove = true;
break;
case (int) PInvoke.WindowMessage.ExitSizeMove:
_inSizeMove = false;
break;
}
return nint.Zero;
}
#endregion
#region Snapping
private nint OnWindowPositionChanging(nint lParam, ref bool handled)
{
if (!_inSizeMove)
return nint.Zero;
var snapDistance = SnapDistance;
// Initialize whether we've updated the position
var updated = false;
// Convert the lParam into the current structure
var windowPosition = (PInvoke.WindowPosition) Marshal.PtrToStructure(lParam, typeof(PInvoke.WindowPosition))!;
// If the window flags indicate no movement then do nothing
if ((windowPosition.Flags & PInvoke.WindowPositionFlags.NoMove) != 0)
return nint.Zero;
// If nothing changed then do nothing
if (_lastWindowPosition.IsSameLocationAndSize(windowPosition))
return nint.Zero;
// Figure out if the window is being moved or resized
var snapMode = _lastWindowPosition.IsSameSize(windowPosition) ? SnapMode.Move : SnapMode.Resize;
// Figure out what side is resizing
var resizeSide = new ResizeSide(windowPosition, _lastWindowPosition);
// Get the screen the cursor is currently on
var screen = Screen.FromPoint(MouseHelper.MousePosition);
// Create a rectangle based on the current working area of the screen
var snapToBorder = screen.WorkingArea;
// Deflate the rectangle based on the snap distance
snapToBorder.Inflate(-snapDistance, -snapDistance);
if (snapMode == SnapMode.Resize)
{
// See if we need to snap on the left
if (windowPosition.Left < snapToBorder.Left)
{
windowPosition.Width += windowPosition.Left - (int) screen.WorkingArea.Left;
windowPosition.Left = (int) screen.WorkingArea.Left;
updated = true;
}
// See if we need to snap on the right
if (windowPosition.Right > snapToBorder.Right)
{
windowPosition.Width += (int) screen.WorkingArea.Right - windowPosition.Right;
updated = true;
}
// See if we need to snap to the top
if (windowPosition.Top < snapToBorder.Top)
{
windowPosition.Height += windowPosition.Top - (int) screen.WorkingArea.Top;
windowPosition.Top = (int) screen.WorkingArea.Top;
updated = true;
}
// See if we need to snap to the bottom
if (windowPosition.Bottom > snapToBorder.Bottom)
{
windowPosition.Height += (int) screen.WorkingArea.Bottom - windowPosition.Bottom;
updated = true;
}
}
else
{
// See if we need to snap on the left
if (windowPosition.Left < snapToBorder.Left)
{
windowPosition.Left = (int) screen.WorkingArea.Left;
updated = true;
}
// See if we need to snap on the top
if (windowPosition.Top < snapToBorder.Top)
{
windowPosition.Top = (int) screen.WorkingArea.Top;
updated = true;
}
// See if we need to snap on the right
if (windowPosition.Right > snapToBorder.Right)
{
windowPosition.Left = (int) screen.WorkingArea.Right - windowPosition.Width;
updated = true;
}
// See if we need to snap on the bottom
if (windowPosition.Bottom > snapToBorder.Bottom)
{
windowPosition.Top = (int) screen.WorkingArea.Bottom - windowPosition.Height;
updated = true;
}
}
var otherWindows = _otherWindows;
if (otherWindows is { Count: > 0 })
{
// Get the snap source rectangles for the window being changed
var sourceRectangles = new WindowBorderRectangles(windowPosition, 1, false);
// Loop over all other windows looking to see if we should stick
foreach (var otherWindow in otherWindows)
{
// Get a rectangle with the bounds of the other window
var otherWindowRect = otherWindow.Location;
// Get the snap target rectangles for the current window
var targetRectangles = new WindowBorderRectangles(otherWindow.Location, snapDistance, true);
if (snapMode == SnapMode.Move)
{
if (sourceRectangles.Left.IntersectsWith(targetRectangles.Right))
{
windowPosition.Left = otherWindowRect.Right;
updated = true;
}
if (sourceRectangles.Right.IntersectsWith(targetRectangles.Left))
{
windowPosition.Left = otherWindowRect.Left - windowPosition.Width;
updated = true;
}
if (sourceRectangles.Top.IntersectsWith(targetRectangles.Bottom))
{
windowPosition.Top = otherWindowRect.Bottom;
updated = true;
}
if (sourceRectangles.Bottom.IntersectsWith(targetRectangles.Top))
{
windowPosition.Top = otherWindowRect.Top - windowPosition.Height;
updated = true;
}
if (windowPosition.Top == otherWindowRect.Bottom || windowPosition.Bottom == otherWindowRect.Top)
{
if (Math.Abs(windowPosition.Left - otherWindowRect.Left) <= snapDistance)
{
windowPosition.Left = otherWindowRect.Left;
updated = true;
}
if (Math.Abs(windowPosition.Right - otherWindowRect.Right) <= snapDistance)
{
windowPosition.Left = otherWindowRect.Right - windowPosition.Width;
updated = true;
}
}
if (windowPosition.Left == otherWindowRect.Right || windowPosition.Right == otherWindowRect.Left)
{
if (Math.Abs(otherWindowRect.Bottom - windowPosition.Bottom) <= snapDistance)
{
windowPosition.Top = otherWindowRect.Bottom - windowPosition.Height;
updated = true;
}
if (Math.Abs(otherWindowRect.Top - windowPosition.Top) <= snapDistance)
{
windowPosition.Top = otherWindowRect.Top;
updated = true;
}
}
}
else
{
if (resizeSide.IsLeft)
{
// Check the current window left against the other window right
if (sourceRectangles.Left.IntersectsWith(targetRectangles.Right))
{
var sign = windowPosition.Left < otherWindowRect.Right ? -1 : 1;
windowPosition.Width += Math.Abs(otherWindowRect.Right - windowPosition.Left) * sign;
windowPosition.Left = otherWindowRect.Right;
updated = true;
}
else if (windowPosition.Top == otherWindowRect.Bottom || windowPosition.Bottom == otherWindowRect.Top)
{
if (Math.Abs(windowPosition.Left - otherWindowRect.Left) <= snapDistance)
{
var sign = windowPosition.Left < otherWindowRect.Left ? -1 : 1;
windowPosition.Width += Math.Abs(otherWindowRect.Left - windowPosition.Left) * sign;
windowPosition.Left = otherWindowRect.Left;
updated = true;
}
}
}
else if (resizeSide.IsRight)
{
// Check the current window right against the other window left
if (sourceRectangles.Right.IntersectsWith(targetRectangles.Left))
{
var sign = windowPosition.Right < otherWindowRect.Left ? 1 : -1;
windowPosition.Width += Math.Abs(otherWindowRect.Left - windowPosition.Right) * sign;
updated = true;
}
else if (windowPosition.Top == otherWindowRect.Bottom || windowPosition.Bottom == otherWindowRect.Top)
{
if (Math.Abs(windowPosition.Right - otherWindowRect.Right) <= snapDistance)
{
var sign = windowPosition.Right < otherWindowRect.Right ? 1 : -1;
windowPosition.Width += Math.Abs(otherWindowRect.Right - windowPosition.Right) * sign;
updated = true;
}
}
}
if (resizeSide.IsBottom)
{
// Check the current window bottom against the other window top
if (sourceRectangles.Bottom.IntersectsWith(targetRectangles.Top))
{
var sign = windowPosition.Bottom < otherWindowRect.Top ? 1 : -1;
windowPosition.Height += Math.Abs(otherWindowRect.Top - windowPosition.Bottom) * sign;
updated = true;
}
else if (windowPosition.Left == otherWindowRect.Right || windowPosition.Right == otherWindowRect.Left)
{
if (Math.Abs(otherWindowRect.Bottom - windowPosition.Bottom) <= snapDistance)
{
var sign = windowPosition.Bottom < otherWindowRect.Bottom ? 1 : -1;
windowPosition.Height += Math.Abs(otherWindowRect.Bottom - windowPosition.Bottom) * sign;
updated = true;
}
}
}
else if (resizeSide.IsTop)
{
// Check the current window top against the other window bottom
if (sourceRectangles.Top.IntersectsWith(targetRectangles.Bottom))
{
var sign = windowPosition.Top < otherWindowRect.Bottom ? 1 : -1;
windowPosition.Height -= Math.Abs(otherWindowRect.Bottom - windowPosition.Top) * sign;
windowPosition.Top = otherWindowRect.Bottom;
updated = true;
}
else if (windowPosition.Left == otherWindowRect.Right || windowPosition.Right == otherWindowRect.Left)
{
if (Math.Abs(otherWindowRect.Top - windowPosition.Top) <= snapDistance)
{
var sign = windowPosition.Top < otherWindowRect.Top ? -1 : 1;
windowPosition.Height += Math.Abs(otherWindowRect.Top - windowPosition.Top) * sign;
windowPosition.Top = otherWindowRect.Top;
updated = true;
}
}
}
}
}
}
// Update the last window position
_lastWindowPosition = windowPosition;
if (!updated) return nint.Zero;
Marshal.StructureToPtr(windowPosition, lParam, true);
handled = true;
return nint.Zero;
}
#endregion
}
}

27
WindowInformation.cs Normal file
View File

@@ -0,0 +1,27 @@
using System.Drawing;
namespace ChrisKaczor.Wpf.Windows;
public class WindowInformation
{
private nint Handle { get; }
public Rectangle Location { get; }
public WindowInformation(nint handle)
{
Handle = handle;
var windowPlacement = new PInvoke.WindowPlacement();
PInvoke.GetWindowPlacement(Handle, ref windowPlacement);
var normalPosition = windowPlacement.NormalPosition;
Location = new Rectangle(normalPosition.X, normalPosition.Y, normalPosition.Width, normalPosition.Height);
}
public WindowInformation(nint handle, Rectangle location)
{
Handle = handle;
Location = location;
}
}

View File

@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWPF>true</UseWPF>
<RootNamespace>ChrisKaczor.Wpf.Windows</RootNamespace>
<Title>ChrisKaczor.Wpf.Windows.SnappingWindow</Title>
<Authors>Chris Kaczor</Authors>
<Product>ChrisKaczor.Wpf.Windows.SnappingWindow</Product>
<RepositoryUrl>https://github.com/ckaczor/ChrisKaczor.Wpf.Windows.SnappingWindow</RepositoryUrl>
<PackageReadmeFile>README.md</PackageReadmeFile>
<Description>Extends the base WPF window to allow snapping to screen edges and/or other specified windows.</Description>
<PackageId>ChrisKaczor.Wpf.Windows.SnappingWindow</PackageId>
<PackageReadmeFile>README.md</PackageReadmeFile>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2022.3.1" />
<PackageReference Include="WpfScreenHelper" Version="2.1.0" />
</ItemGroup>
<ItemGroup>
<None Update="README.md">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,25 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.33424.131
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wpf.Windows.SnappingWindow", "Wpf.Windows.SnappingWindow.csproj", "{E617775F-9345-4532-B883-3B227A5CAC9D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{E617775F-9345-4532-B883-3B227A5CAC9D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E617775F-9345-4532-B883-3B227A5CAC9D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E617775F-9345-4532-B883-3B227A5CAC9D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E617775F-9345-4532-B883-3B227A5CAC9D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {80F2171E-35CA-4020-812E-DBA70B71E902}
EndGlobalSection
EndGlobal

45
azure-pipelines.yml Normal file
View File

@@ -0,0 +1,45 @@
name: 1.0.$(Rev:r)
pr: none
trigger:
batch: 'true'
branches:
include:
- main
pool:
vmImage: 'windows-latest'
variables:
buildConfiguration: 'Release'
steps:
- task: DotNetCoreCLI@2
displayName: 'dotnet build'
inputs:
command: 'build'
arguments: '--configuration $(buildConfiguration)'
projects: 'Wpf.Windows.SnappingWindow.csproj'
- task: DotNetCoreCLI@2
displayName: "dotnet pack"
inputs:
command: 'pack'
arguments: '--configuration $(buildConfiguration)'
packagesToPack: 'Wpf.Windows.SnappingWindow.csproj'
nobuild: true
versioningScheme: 'byBuildNumber'
- task: NuGetCommand@2
displayName: 'nuget push'
inputs:
command: 'push'
feedsToUse: 'select'
packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg;!$(Build.ArtifactStagingDirectory)/**/*.symbols.nupkg'
nuGetFeedType: external
publishFeedCredentials: 'NuGet'
publishVstsFeed: 'Packages'
versioningScheme: 'byBuildNumber'
allowPackageConflicts: true