温故知新,遇见WPF/WinForms,自动记忆和还原WinFroms窗体大小和位置及状态

前言

有时候我们可能需要将用户上一次弹窗的大小和位置自动记忆和还原,让用户有一种宾至如归的感觉。

image

窗体起始位置控制

对WinFroms窗体而言,默认起始位置是在左上角的。但是可以通过System.Windows.Forms.Form.StartPosition来指定,存在如下几个选项:

属性值 含义
Manual 手动指定位置
CenterScreen 使其屏幕居中
WindowsDefaultLocation 默认位置,尺寸在窗体中设置
WindowsDefaultBounds 默认位置、默认尺寸
CenterParent 在父窗体中居中

指定窗体位置的本质

虽然窗体默认有LeftTopRightBottom四个属性,但是实际只有前两个是可以设置的,后面两个是只读的。

public int Left
{
    get
    {
        return x;
    }
    set
    {
        SetBounds(value, y, width, height, BoundsSpecified.X);
    }
}

我们常说的Location值实际上就是指的LeftTop的值。

Location = new Point(100, 200);
Left = 100;
Top = 200;

指定窗体的宽度和高度

完成了对窗体位置的指定之后,接下来就是设置窗体的宽度和高度了。

Width = 600;
Height = 300;

如果有需要,还可以设置MaximumSizeMinimumSize

获取不含任务栏的桌面工作区

var WorkingArea = Screen.PrimaryScreen?.WorkingArea ?? SystemInformation.WorkingArea;
var width = WorkingArea.Width;
var height = WorkingArea.Height;

置顶窗体状态

可以通过System.Windows.Forms.Form.WindowState来指定,存在如下几个选项:

属性值 含义
Normal 正常大小
Minimized 最小化
Maximized 最大化

窗体置顶

TopMost = true;

实现思路

存储窗体信息

public class WindowInfo
{
    public FormWindowState WindowState;

    public Point Location = new Point(int.MinValue, int.MinValue);

    public Size ClientSize;

    public Size Size;

    public SizeF DPI = new SizeF(96F, 96F);
}

这里也准备一个本地配置文件来保存和读取。

const string WindowInfoConfigFile = "windowinfo.json";

在关闭时记住位置

protected override void OnFormClosed(FormClosedEventArgs e)
{
    base.OnFormClosed(e);
    MemoryWindowInfo();
}

private void MemoryWindowInfo()
{
    var bounds = WindowState == FormWindowState.Normal ? DesktopBounds : CurrentBounds;
    var clientSize = WindowState == FormWindowState.Normal ? ClientSize : CurrentClientSize;
    var windowInfo = new WindowInfo();
    windowInfo.Location = bounds.Location;
    windowInfo.Size = bounds.Size;
    windowInfo.ClientSize = clientSize;
    windowInfo.DPI = GetDpi();
    windowInfo.WindowState = WindowState;
    File.WriteAllText(WindowInfoConfigFile, JsonConvert.SerializeObject(windowInfo));
}

这里引入了CurrentBoundsCurrentClientSize上下文变量,原因是,当窗体处于最小化时,这时候获取的位置和大小就是有问题的,这里我们需要使用这个上下文变量来托管下正常模式下的值。

Rectangle CurrentBounds;
Size CurrentClientSize;

protected override void OnLocationChanged(EventArgs e)
{
    base.OnLocationChanged(e);
    if (WindowState == FormWindowState.Normal)
    {
        CurrentBounds.Location = this.Location;
        CurrentClientSize = this.ClientSize;
    }
}

protected override void OnSizeChanged(EventArgs e)
{
    base.OnSizeChanged(e);
    if (WindowState == FormWindowState.Normal)
    {
        CurrentBounds.Size = this.Size;
        CurrentClientSize = this.ClientSize;
    }
}

在加载时还原位置和大小

public XXXXXXXXForm()
{
    InitializeComponent();
    RestoreWindowInfo();
}

private void RestoreWindowInfo()
{
    WindowInfo windowInfo;
    if (File.Exists(WindowInfoConfigFile))
    {
        windowInfo = JsonConvert.DeserializeObject<WindowInfo>(File.ReadAllText(WindowInfoConfigFile));
    }
    else
    {
        windowInfo = new WindowInfo();
    }

    var memoryDPI = windowInfo.DPI;
    if (memoryDPI.Width == 0F)
        memoryDPI.Width = 96F;
    if (memoryDPI.Height == 0F)
        memoryDPI.Height = 96F;
    var currentDPI = GetDpi();
    var factor = new SizeF(currentDPI.Width / memoryDPI.Width, currentDPI.Height / memoryDPI.Height);
    var loc = ScaleUtils.Scale(windowInfo.Location, factor);
    var size = ScaleUtils.Scale(windowInfo.Size, factor);
    var clientSize = ScaleUtils.Scale(windowInfo.ClientSize, factor);
    if (loc.X > int.MinValue && loc.Y > int.MinValue)
    {
        StartPosition = FormStartPosition.Manual;
    }
    else
    {
        loc = this.DesktopLocation;
    }
    if (!clientSize.IsEmpty)
        size = SizeFromClientSize(clientSize);
    if (size.IsEmpty)
        size = this.Size;
    var bounds = new Rectangle(loc, size);
    var workArea = Screen.GetWorkingArea(bounds);
    var intersectRect = Rectangle.Intersect(workArea, bounds);
    var minWinSize = SystemInformation.MinimumWindowSize;
    if (intersectRect.Width <= minWinSize.Width || intersectRect.Height < minWinSize.Height)
    {
        // 如果保存的位置不在工作区内,由默认放到主屏显示
        var mainWorkArea = Screen.PrimaryScreen?.WorkingArea ?? SystemInformation.WorkingArea; // Screen.PrimaryScreen 有可能为空
        bounds.Location = mainWorkArea.Location;
    }
    this.DesktopBounds = bounds;
    this.WindowState = windowInfo.WindowState;
}

关键的帮助类

获取当前DPI

private SizeF GetDpi()
{
    HandleRef hDC = new HandleRef(null, Win32NativeMethods.GetDC(default(HandleRef)));
    int deviceCaps = Win32NativeMethods.GetDeviceCaps(hDC, 88);
    int deviceCaps2 = Win32NativeMethods.GetDeviceCaps(hDC, 90);
    Size result = new Size(deviceCaps, deviceCaps2);
    Win32NativeMethods.ReleaseDC(default(HandleRef), hDC);
    return result;
}

根据缩放值计算

public static class ScaleUtils
{
    public static Point Scale(Point point, SizeF factor)
    {
        return new Point((int)Math.Round((float)point.X * factor.Width, MidpointRounding.AwayFromZero), (int)Math.Round((float)point.Y * factor.Height, MidpointRounding.AwayFromZero));
    }

    public static Size Scale(Size size, SizeF factor)
    {
        return new Size((int)Math.Round((float)size.Width * factor.Width, MidpointRounding.AwayFromZero), (int)Math.Round((float)size.Height * factor.Height, MidpointRounding.AwayFromZero));
    }
}

获取系统信息的API

public static class Win32NativeMethods
{
    [DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
    public static extern IntPtr GetDC(HandleRef hWnd);

    [DllImport("gdi32.dll", CharSet = CharSet.Auto, ExactSpelling = true, SetLastError = true)]
    public static extern int GetDeviceCaps(HandleRef hDC, int nIndex);

    [DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
    public static extern int ReleaseDC(HandleRef hWnd, HandleRef hDC);
}

参考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章