溫故知新,遇見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);
}

參考

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