从最大化恢复时的 WPF window 状态卡在奇数状态
WPF window state when restored from maximized gets stuck in odd state
我发现 WPF 有一些奇怪的行为。我有一个带有三个按钮的表格。一个按钮应该使 window 全屏,一个按钮应该使它在当前打开的显示器上居中,第三个按钮应该将 window 恢复到它的正常位置。
XAML是
<Window x:Class="TestRestore.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:TestRestore"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525" ResizeMode="CanResizeWithGrip" WindowStartupLocation="CenterScreen">
<Grid>
<Button Content="Max" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Width="94" Click="max_click" Name="max_button"/>
<Button Content="Center" HorizontalAlignment="Left" Margin="10,35,0,0" VerticalAlignment="Top" Width="94" Click="center_click" Name="center_button"/>
<Button Content="Restore" HorizontalAlignment="Left" Margin="227,143,0,0" VerticalAlignment="Top" Width="75" Click="restore_click" Name="restore_button" IsEnabled="False"/>
</Grid>
</Window>
代码如下。奇怪的是,当我最大化,然后恢复 window 时,位置被正确恢复,但 window 仍然认为它最大化(最大化按钮看起来像一个恢复按钮,你不能调整大小window 即使 ResizeMode 已设置为 CanResizeWithGrip)。
当最大化的window恢复后,即使window位置没有最大化,它认为它仍然是最大化的,只需拖动标题手动移动window bar 足以使其自行更正为 non-maximized 模式。
此外,如果我最大化然后恢复 window 然后再次最大化它而不移动它,最大化的 window 位置不正确(不在左上角)。
谜团加深了。如果我最大化然后恢复 window,然后按 alt,然后按下(获得 window 菜单)和 select 'Move' 然后移动 window在键盘周围,即使 window 正在移动,它仍然卡在 'bogus not-mazimized mode' 中,所以似乎唯一的方法就是用鼠标移动它。
using System;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
namespace TestRestore
{
public partial class MainWindow : Window
{
WindowStyle old_window_style;
WindowState old_window_state;
double old_left;
double old_top;
double old_width;
double old_height;
public MainWindow()
{
InitializeComponent();
}
// remember position, style and state
private void SaveWindowPos()
{
old_window_style = WindowStyle;
old_window_state = WindowState;
old_left = Left;
old_top = Top;
old_width = Width;
old_height = Height;
max_button.IsEnabled = false;
center_button.IsEnabled = false;
restore_button.IsEnabled = true;
}
// put position, style and state back
private void RestoreWindowPos()
{
WindowStyle = old_window_style;
WindowState = old_window_state;
ResizeMode = ResizeMode.CanResizeWithGrip;
Left = old_left;
Top = old_top;
Width = old_width;
Height = old_height;
max_button.IsEnabled = true;
center_button.IsEnabled = true;
restore_button.IsEnabled = false;
}
// make it centered or fullscreen
private void SetActivePos(bool full_screen)
{
SaveWindowPos();
Hide();
if (full_screen)
{
ResizeMode = ResizeMode.NoResize;
WindowStyle = WindowStyle.None;
WindowState = WindowState.Maximized;
}
else
{
Size s = new Size(800, 600);
Point p = CenterRectInMonitor(this, s);
Left = p.X;
Top = p.Y;
Width = s.Width;
Height = s.Height;
ResizeMode = ResizeMode.NoResize;
WindowState = WindowState.Normal;
}
Show();
}
private void restore_click(object sender, RoutedEventArgs e)
{
Hide();
RestoreWindowPos();
Show();
}
private void max_click(object sender, RoutedEventArgs e)
{
SetActivePos(true);
}
private void center_click(object sender, RoutedEventArgs e)
{
SetActivePos(false);
}
// interop
public const Int32 MONITOR_DEFAULTTOPRIMARY = 0x00000001;
public const Int32 MONITOR_DEFAULTTONEAREST = 0x00000002;
[DllImport("user32.dll")]
public static extern IntPtr MonitorFromWindow(IntPtr handle, Int32 flags);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
public static extern bool GetMonitorInfo(IntPtr hMonitor, ref MonitorInfoEx lpmi);
// size of a device name string
private const int CCHDEVICENAME = 32;
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public struct MonitorInfoEx
{
public int Size;
public RectStruct Monitor;
public RectStruct WorkArea;
public uint Flags;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCHDEVICENAME)]
public string DeviceName;
public void Init()
{
this.Size = 40 + 2 * CCHDEVICENAME;
this.DeviceName = string.Empty;
}
}
[StructLayout(LayoutKind.Sequential)]
public struct RectStruct
{
public int Left;
public int Top;
public int Right;
public int Bottom;
public int Width
{
get
{
return Right - Left;
}
}
public int Height
{
get
{
return Bottom - Top;
}
}
}
public static MonitorInfoEx GetMonitorFromWindow(Window w)
{
var hwnd = new WindowInteropHelper(w).EnsureHandle();
var monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
MonitorInfoEx monitor_info = new MonitorInfoEx();
monitor_info.Init();
GetMonitorInfo(monitor, ref monitor_info);
return monitor_info;
}
// work out how a rect of 'Size size' should be centered on the monitor containing 'Window w'
public static Point CenterRectInMonitor(Window w, Size size)
{
var source = PresentationSource.FromVisual(w);
double x_scale = source.CompositionTarget.TransformToDevice.M11;
double y_scale = source.CompositionTarget.TransformToDevice.M22;
var width = size.Width * x_scale;
var height = size.Height * y_scale;
var monitor_info = GetMonitorFromWindow(w);
Size s = new Size(monitor_info.Monitor.Width, monitor_info.Monitor.Height);
Point p = new Point(monitor_info.Monitor.Left, monitor_info.Monitor.Top);
Point c = new Point(p.X + s.Width / 2, p.Y + s.Height / 2);
return new Point((c.X - width / 2) / x_scale, (c.Y - height / 2) / y_scale);
}
}
}
我没有完整的答案给你。但是,您会发现一旦删除 Hide() 和 Show() 调用,您的代码就会开始工作得更好。
private void restore_click(object sender, RoutedEventArgs e)
{
// Hide();
RestoreWindowPos();
// Show();
}
我确定你放入这个是为了减少闪烁,但我认为正在发生的是 Hide() 和 Show() 调用正在翻转 window 中的 WS_VISIBLE 位底层 OS window 的样式词与包含 WS_MAXIMIZE 和 WS_BORDER 以及您正在操纵的其他一些东西的词相同。参见 https://msdn.microsoft.com/en-us/library/windows/desktop/ms632600(v=vs.85).aspx
需要更多的研究才能弄清楚到底发生了什么,但我认为根本问题是 "leaky abstraction"。您的代码设置顶部、左侧、样式和状态,就好像它们是独立的非耦合变量一样。但他们不是!要设置左侧,必须调用 OS SetWindowPos() 函数,它不需要左上角坐标、window 大小、Z 顺序以及可见性标志以及 windows 是否是最大化!参见 https://msdn.microsoft.com/en-us/library/windows/desktop/ms633545(v=vs.85).aspx。因此,每次您设置其中一个 "independent" 变量时,您都会调用 SetWindowPos()。这个 API 调用让人回想起过去糟糕的日子,当时 CPU 周期非常宝贵,您需要在每个 API 调用中包含尽可能多的功能。
具有讽刺意味的是,这会使您的代码变得非常低效。我认为解决这个问题的方法是绕过 System.Windows.Window 的泄漏抽象并直接从 user32.dll 调用 SetWindowPos 和可能的其他 API 函数。那么事情就会变得更加可预测。
我发现 WPF 有一些奇怪的行为。我有一个带有三个按钮的表格。一个按钮应该使 window 全屏,一个按钮应该使它在当前打开的显示器上居中,第三个按钮应该将 window 恢复到它的正常位置。
XAML是
<Window x:Class="TestRestore.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:TestRestore"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525" ResizeMode="CanResizeWithGrip" WindowStartupLocation="CenterScreen">
<Grid>
<Button Content="Max" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Width="94" Click="max_click" Name="max_button"/>
<Button Content="Center" HorizontalAlignment="Left" Margin="10,35,0,0" VerticalAlignment="Top" Width="94" Click="center_click" Name="center_button"/>
<Button Content="Restore" HorizontalAlignment="Left" Margin="227,143,0,0" VerticalAlignment="Top" Width="75" Click="restore_click" Name="restore_button" IsEnabled="False"/>
</Grid>
</Window>
代码如下。奇怪的是,当我最大化,然后恢复 window 时,位置被正确恢复,但 window 仍然认为它最大化(最大化按钮看起来像一个恢复按钮,你不能调整大小window 即使 ResizeMode 已设置为 CanResizeWithGrip)。
当最大化的window恢复后,即使window位置没有最大化,它认为它仍然是最大化的,只需拖动标题手动移动window bar 足以使其自行更正为 non-maximized 模式。
此外,如果我最大化然后恢复 window 然后再次最大化它而不移动它,最大化的 window 位置不正确(不在左上角)。
谜团加深了。如果我最大化然后恢复 window,然后按 alt,然后按下(获得 window 菜单)和 select 'Move' 然后移动 window在键盘周围,即使 window 正在移动,它仍然卡在 'bogus not-mazimized mode' 中,所以似乎唯一的方法就是用鼠标移动它。
using System;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
namespace TestRestore
{
public partial class MainWindow : Window
{
WindowStyle old_window_style;
WindowState old_window_state;
double old_left;
double old_top;
double old_width;
double old_height;
public MainWindow()
{
InitializeComponent();
}
// remember position, style and state
private void SaveWindowPos()
{
old_window_style = WindowStyle;
old_window_state = WindowState;
old_left = Left;
old_top = Top;
old_width = Width;
old_height = Height;
max_button.IsEnabled = false;
center_button.IsEnabled = false;
restore_button.IsEnabled = true;
}
// put position, style and state back
private void RestoreWindowPos()
{
WindowStyle = old_window_style;
WindowState = old_window_state;
ResizeMode = ResizeMode.CanResizeWithGrip;
Left = old_left;
Top = old_top;
Width = old_width;
Height = old_height;
max_button.IsEnabled = true;
center_button.IsEnabled = true;
restore_button.IsEnabled = false;
}
// make it centered or fullscreen
private void SetActivePos(bool full_screen)
{
SaveWindowPos();
Hide();
if (full_screen)
{
ResizeMode = ResizeMode.NoResize;
WindowStyle = WindowStyle.None;
WindowState = WindowState.Maximized;
}
else
{
Size s = new Size(800, 600);
Point p = CenterRectInMonitor(this, s);
Left = p.X;
Top = p.Y;
Width = s.Width;
Height = s.Height;
ResizeMode = ResizeMode.NoResize;
WindowState = WindowState.Normal;
}
Show();
}
private void restore_click(object sender, RoutedEventArgs e)
{
Hide();
RestoreWindowPos();
Show();
}
private void max_click(object sender, RoutedEventArgs e)
{
SetActivePos(true);
}
private void center_click(object sender, RoutedEventArgs e)
{
SetActivePos(false);
}
// interop
public const Int32 MONITOR_DEFAULTTOPRIMARY = 0x00000001;
public const Int32 MONITOR_DEFAULTTONEAREST = 0x00000002;
[DllImport("user32.dll")]
public static extern IntPtr MonitorFromWindow(IntPtr handle, Int32 flags);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
public static extern bool GetMonitorInfo(IntPtr hMonitor, ref MonitorInfoEx lpmi);
// size of a device name string
private const int CCHDEVICENAME = 32;
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public struct MonitorInfoEx
{
public int Size;
public RectStruct Monitor;
public RectStruct WorkArea;
public uint Flags;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCHDEVICENAME)]
public string DeviceName;
public void Init()
{
this.Size = 40 + 2 * CCHDEVICENAME;
this.DeviceName = string.Empty;
}
}
[StructLayout(LayoutKind.Sequential)]
public struct RectStruct
{
public int Left;
public int Top;
public int Right;
public int Bottom;
public int Width
{
get
{
return Right - Left;
}
}
public int Height
{
get
{
return Bottom - Top;
}
}
}
public static MonitorInfoEx GetMonitorFromWindow(Window w)
{
var hwnd = new WindowInteropHelper(w).EnsureHandle();
var monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
MonitorInfoEx monitor_info = new MonitorInfoEx();
monitor_info.Init();
GetMonitorInfo(monitor, ref monitor_info);
return monitor_info;
}
// work out how a rect of 'Size size' should be centered on the monitor containing 'Window w'
public static Point CenterRectInMonitor(Window w, Size size)
{
var source = PresentationSource.FromVisual(w);
double x_scale = source.CompositionTarget.TransformToDevice.M11;
double y_scale = source.CompositionTarget.TransformToDevice.M22;
var width = size.Width * x_scale;
var height = size.Height * y_scale;
var monitor_info = GetMonitorFromWindow(w);
Size s = new Size(monitor_info.Monitor.Width, monitor_info.Monitor.Height);
Point p = new Point(monitor_info.Monitor.Left, monitor_info.Monitor.Top);
Point c = new Point(p.X + s.Width / 2, p.Y + s.Height / 2);
return new Point((c.X - width / 2) / x_scale, (c.Y - height / 2) / y_scale);
}
}
}
我没有完整的答案给你。但是,您会发现一旦删除 Hide() 和 Show() 调用,您的代码就会开始工作得更好。
private void restore_click(object sender, RoutedEventArgs e)
{
// Hide();
RestoreWindowPos();
// Show();
}
我确定你放入这个是为了减少闪烁,但我认为正在发生的是 Hide() 和 Show() 调用正在翻转 window 中的 WS_VISIBLE 位底层 OS window 的样式词与包含 WS_MAXIMIZE 和 WS_BORDER 以及您正在操纵的其他一些东西的词相同。参见 https://msdn.microsoft.com/en-us/library/windows/desktop/ms632600(v=vs.85).aspx
需要更多的研究才能弄清楚到底发生了什么,但我认为根本问题是 "leaky abstraction"。您的代码设置顶部、左侧、样式和状态,就好像它们是独立的非耦合变量一样。但他们不是!要设置左侧,必须调用 OS SetWindowPos() 函数,它不需要左上角坐标、window 大小、Z 顺序以及可见性标志以及 windows 是否是最大化!参见 https://msdn.microsoft.com/en-us/library/windows/desktop/ms633545(v=vs.85).aspx。因此,每次您设置其中一个 "independent" 变量时,您都会调用 SetWindowPos()。这个 API 调用让人回想起过去糟糕的日子,当时 CPU 周期非常宝贵,您需要在每个 API 调用中包含尽可能多的功能。
具有讽刺意味的是,这会使您的代码变得非常低效。我认为解决这个问题的方法是绕过 System.Windows.Window 的泄漏抽象并直接从 user32.dll 调用 SetWindowPos 和可能的其他 API 函数。那么事情就会变得更加可预测。