Давайте реализуем сохранение aspect ratio в виде attached behavior, чтобы не вмешиваться в код окна, а просто навесить его «со стороны».
Код окна при этом будет выглядеть примерно так:
<Window x:Class="WindowAspectRatio.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:local="clr-namespace:WindowAspectRatio"
Title="MainWindow" Height="350" Width="525">
<i:Interaction.Behaviors>
<!-- параметр AspectRatio можно и не указывать -->
<local:WindowAspectRatioBehavior AspectRatio="3"/>
</i:Interaction.Behaviors>
<Grid Background="Azure"/>
</Window>
(для поддержки behaviours нужно подключить сборку System.Windows.Interactivity).
Реализацию поведения (behaviour) отделим от логики. (Логику можно повторно использовать где-то ещё.)
Итак, вот WindowAspectRatioBehavior
:
class WindowAspectRatioBehavior : Behavior<Window> // только для целого окна
{
protected override void OnAttached()
{
base.OnAttached();
// пробуем навеситься сразу...
var hwnd = TryGetHwndSource(AssociatedObject);
if (hwnd != null)
keeper = new AspectRatioKeeper(hwnd, double.IsNaN(AspectRatio) ?
default(double?) : AspectRatio);
else
AssociatedObject.SourceInitialized += OnSourceInitialized;
// если у окна ещё нету HWND, подождём, пока появится
}
void OnSourceInitialized(object sender, EventArgs e)
{
// опять пробуем навеситься
var hwnd = TryGetHwndSource(AssociatedObject);
if (hwnd != null)
keeper = new AspectRatioKeeper(hwnd, double.IsNaN(AspectRatio) ?
default(double?) : AspectRatio);
AssociatedObject.SourceInitialized -= OnSourceInitialized;
}
static HwndSource TryGetHwndSource(Window w)
{
return (HwndSource)HwndSource.FromVisual(w);
}
protected override void OnDetaching()
{
base.OnDetaching();
AssociatedObject.SourceInitialized -= OnSourceInitialized;
if (keeper != null)
keeper.Dispose();
keeper = null;
}
public double AspectRatio
{
get { return (double)GetValue(AspectRatioProperty); }
set { SetValue(AspectRatioProperty, value); }
}
// обыкновенное dependency property. при изменении значения вызываем
// функцию OnAspectChanged. По умолчанию NaN, что означает
// пользоваться aspect ratio, как у окна на момент навешивания
public static readonly DependencyProperty AspectRatioProperty =
DependencyProperty.Register(
"AspectRatio", typeof(double), typeof(WindowAspectRatioBehavior),
new PropertyMetadata(double.NaN, (o, args) =>
((WindowAspectRatioBehavior)o).OnAspectChanged()));
void OnAspectChanged()
{
var ratio = AspectRatio;
// если мы уже поддерживаем размер, нам нужно его переустановить
if (keeper != null && !double.IsNaN(ratio))
keeper.SetAspectRatio(ratio);
}
AspectRatioKeeper keeper;
}
Вся логика пересчёта лежит в AspectRatioKeeper
'е. Здесь нам придётся спуститься на нижний уровень, к оконной процедуре, и серьёзно поиспользовать P/Invoke.
class AspectRatioKeeper : IDisposable
{
// хэндл окна, за которым мы следим
HwndSource hwnd;
// если forceRatio == null, мы запоминаем начальное состояние и поддерживаем его
public AspectRatioKeeper(HwndSource hwnd, double? forceRatio)
{
this.hwnd = hwnd;
RECT rect;
GetWindowRect(hwnd.Handle, out rect);
_aspectRatio = forceRatio ?? ((double)rect.Height / rect.Width);
if (forceRatio.HasValue)
EnsureAspectRatio(rect.Width, rect.Height);
hwnd.AddHook(DragHook);
}
public void SetAspectRatio(double ratio)
{
_aspectRatio = ratio;
RECT rect;
GetWindowRect(hwnd.Handle, out rect);
EnsureAspectRatio(rect.Width, rect.Height);
}
void EnsureAspectRatio(int w, int h)
{
EnsureAspectRatioValue(ref w, ref h, true);
SetWindowPos(hwnd.Handle, IntPtr.Zero, -1, -1, w, h,
SWP.NOACTIVATE | SWP.NOMOVE | SWP.NOSENDCHANGING);
}
public void Dispose()
{
hwnd.RemoveHook(DragHook);
}
// это наш хук, следим за изменениями размеров окна
IntPtr DragHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
switch ((WM)msg)
{
case WM.WINDOWPOSCHANGING:
{
// новый размер и позиция окна
WINDOWPOS pos =
(WINDOWPOS)Marshal.PtrToStructure(lParam, typeof(WINDOWPOS));
if ((pos.flags & (int)SWP.NOMOVE) != 0)
return IntPtr.Zero;
// Здесь была проверка, является ли окно с данным хэндлом
// действительно WPF-окном. Но она по идее не нужна.
// если мы ещё не знаем, меняем мы высоту или ширину, определим это
if (!_adjustingHeight.HasValue)
{
Point p = GetMousePosition();
// считаем расстояния позиции мыши от краёв окна
double mouseFromLeft = Math.Abs(p.X - pos.x),
mouseFromRight = Math.Abs(p.X - pos.x - pos.cx),
mouseFromTop = Math.Abs(p.Y - pos.y),
mouseFromBottom = Math.Abs(p.Y - pos.y - pos.cy);
double diffWidth = Math.Min(mouseFromLeft, mouseFromRight),
diffHeight = Math.Min(mouseFromTop, mouseFromBottom);
// похоже, мы меняем высоту. или ширину. запомним это.
_adjustingHeight = diffHeight < diffWidth;
}
EnsureAspectRatioValue(ref pos.cx, ref pos.cy, _adjustingHeight.Value);
Marshal.StructureToPtr(pos, lParam, true);
handled = true;
}
break;
case WM.EXITSIZEMOVE:
// мы закончили перетаскивание, в следующий раз направление придётся
// определять заново
_adjustingHeight = null;
break;
}
return IntPtr.Zero;
}
// просто пересчёт численного значения высоты и ширины, с учётом их отношения
// и максимального/минимального системного значений
void EnsureAspectRatioValue(ref int w, ref int h, bool tryKeepWidth)
{
if (tryKeepWidth)
w = (int)(h / _aspectRatio); // adjusting height to width change
else
h = (int)(w * _aspectRatio); // adjusting width to heigth change
if (w > maxw)
{
w = (int)maxw;
h = (int)(w * _aspectRatio);
}
else if (w < minw)
{
w = (int)minw;
h = (int)(w * _aspectRatio);
}
if (h > maxh)
{
h = (int)maxh;
w = (int)(h / _aspectRatio);
}
else if (h < minh)
{
h = (int)minh;
w = (int)(h / _aspectRatio);
}
}
double _aspectRatio;
bool? _adjustingHeight = null;
double minh = SystemParameters.MinimumWindowHeight,
minw = SystemParameters.MinimumWindowWidth,
maxh = SystemParameters.MaximumWindowTrackHeight,
maxw = SystemParameters.MaximumWindowTrackWidth;
/////////////////////////////////////////////////////////
// дальше будет просто импорт функция WinAPI и определение структур данных
// большая часть взята с сайта pinvoke.net
enum WM
{
WINDOWPOSCHANGING = 0x0046,
EXITSIZEMOVE = 0x0232,
}
[Flags]
enum SWP : uint
{
ASYNCWINDOWPOS = 0x4000,
DEFERERASE = 0x2000,
DRAWFRAME = 0x0020,
FRAMECHANGED = 0x0020,
HIDEWINDOW = 0x0080,
NOACTIVATE = 0x0010,
NOCOPYBITS = 0x0100,
NOMOVE = 0x0002,
NOOWNERZORDER = 0x0200,
NOREDRAW = 0x0008,
NOREPOSITION = 0x0200,
NOSENDCHANGING = 0x0400,
NOSIZE = 0x0001,
NOZORDER = 0x0004,
SHOWWINDOW = 0x0040
}
[StructLayout(LayoutKind.Sequential)]
internal struct WINDOWPOS
{
public IntPtr hwnd;
public IntPtr hwndInsertAfter;
public int x;
public int y;
public int cx;
public int cy;
public int flags;
}
[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
public int Left, Top, Right, Bottom;
public int Height { get { return Bottom - Top; } }
public int Width { get { return Right - Left; } }
}
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool GetCursorPos(ref Win32Point pt);
[DllImport("user32.dll", SetLastError = true)]
static extern bool GetWindowRect(IntPtr hwnd, out RECT rect);
[DllImport("user32.dll", SetLastError = true)]
static extern bool SetWindowPos(
IntPtr hWnd, IntPtr hWndInsertAfter,
int X, int Y, int cx, int cy, SWP uFlags);
[StructLayout(LayoutKind.Sequential)]
internal struct Win32Point
{
public Int32 X;
public Int32 Y;
};
static Point GetMousePosition() // mouse position relative to screen
{
Win32Point w32Mouse = new Win32Point();
GetCursorPos(ref w32Mouse);
return new Point(w32Mouse.X, w32Mouse.Y);
}
}
Код во многом основан на этом ответе.