메인 창은 WindowStyle=None + WindowChrome 으로 크롬을 직접 그린다. 그 조합에서 OS 는 최대화 사각형을 작업영역이 아니라 모니터 전체를 기준으로 잡고 리사이즈 프레임만큼 더 부풀린다. 실측으로 창이 (-7,-7)~(1927,1087) — 작업영역 (0,0)~(1920,1032) 를 55px 넘겨 작업표시줄이 통째로 가려졌다. 표준 크롬 창에는 없는 문제라 WindowStyle=None 창에만 건다. WM_GETMINMAXINFO 훅 하나로 답한다(Services/MaximizeToWorkArea.cs). 레이아웃·XAML·테마는 건드리지 않았다. 최대화 상태에서 콘텐츠가 밀려 보인다면 답은 Margin 보정이 아니라 이 훅의 계산이 틀린 것이다 — 창 사각형이 정확히 작업영역이면 잘릴 곳이 없다. 같이 처리한 것들: · 최소 크기가 최대화를 이기는 경우. OS 는 최대화 크기를 최소 트래킹 크기까지 되밀어 올린다. 그 값은 우리 뒤에 도는 WPF 가 MinWidth/MinHeight(940x640 DIP)로 채우므로, 작업영역이 그보다 작으면 창이 도로 부풀어 작업표시줄을 다시 덮는다 — 1366x768 을 125% 로 쓰면 최소 높이 800px 에 작업영역 720px 다. 그 경우에만 WPF 의 처리를 막고 트래킹 크기를 직접 채운다. 화면에 안 들어가는 최소 크기는 최소 크기 구실을 못 한다. 보통은 handled=false 로 두어 MinWidth/MinHeight 가 그대로 산다. · 최대화 중 상단 6px 리사이즈 띠. 전에는 창이 화면 밖으로 밀려 있어 그 띠가 안 보였다. 이제 타이틀바 위로 올라와 닫기 버튼 윗변이 먹혔다(실측 y=1·3·5 에서 HTTOP). 최대화된 창은 어차피 가장자리로 크기를 못 바꾸므로 그동안 띠를 걷는다. 여백 보정이 아니라 히트테스트 영역 보정이다 — 창 사각형은 그대로 작업영역이다. · 자동 숨김 작업표시줄. 자동 숨김이면 작업영역이 모니터 전체와 같아지고, 그대로 꽉 채우면 셸이 전체 화면 앱으로 보아 가장자리 호버에 반응하지 않는다 — 작업표시줄이 영영 안 나온다. 네 변을 각각 ABM_GETAUTOHIDEBAREX 로 보고 1px 씩 물러선다. 폴백은 '한 변도 못 찾았을 때'가 아니라 '그 변을 못 찾았을 때' 돈다(좌측 서드파티 바 하나 때문에 하단 작업표시줄이 구제받지 못하던 구조였다). 폴백이 쓰는 두 API 는 주 작업표시줄 전용이라 보조 모니터는 못 덮는다 — 주석을 그렇게 바로잡았다. · 재부착 가드. 최대화 중에 두 번 붙으면 그때의 0 을 '복원 시 두께'로 기억해 복원해도 가장자리를 끌 수 없는 창이 됐다. 진단 종료 코드가 죽어 있었다. 기본 ShutdownMode 는 OnLastWindowClose 라, 창을 띄웠다 전부 닫는 진단은 마지막 창이 닫히는 순간 WPF 가 먼저 Shutdown(0) 을 걸고 뒤이은 Shutdown(코드) 인자를 무시한다 — 리포트에 '실패 1건' 이 찍혀도 프로세스는 0 을 돌려줬다(--maxrect 첫 실행이 그랬다). --edit-smoke·--db-smoke 도 같은 구멍이었다. 진단 분기 진입부에서 OnExplicitShutdown 으로 바꿨다. 같은 자리에서 진단 모드 UI 예외도 모달 대신 로그+Shutdown(3) 으로 돌린다 — '진단 모드에서는 모달을 띄우지 않는다'는 규칙을 그보다 먼저 등록되는 예외 핸들러가 깨고 있었다. 신규 진단 --maxrect (10건). 기대값을 베껴 적지 않고 성질을 단언한다 — 모니터 안에 들어가는가, 자동 숨김 없는 변은 작업영역과 같은가, 있는 변은 물러섰는가, 작업표시줄과 겹치지 않는가, 최소 크기가 최대화 크기를 넘지 않는가. 전사(轉寫)하면 같은 착각을 양쪽이 공유하고 양보 폭을 1→2 로 바꾸는 순간 거짓 실패가 난다. 보내는 버퍼는 센티넬로 채운다 — 0 으로 두면 기대값이 (0,0) 인 모니터에서 훅이 아무것도 안 써도 통과한다(실제로 한 번 그렇게 통과했다). 창을 실제로 띄워 최대화·복원까지 해 OS 가 답을 지키는지와 StateChanged 배선까지 본다. 그 경로에서 화면이 한 번 번쩍인다. 실측(1920x1080·96DPI·하단 고정 작업표시줄): 최대화 (-7,-7)~(1927,1087) → (0,0)~(1920,1032), 침범 55px → 0px 복원 1440x920 유지, 최소 크기 940x640 유지 최대화 상단 히트테스트 HTTOP → HTCAPTION 자동 숨김·다중 모니터·125%/150% 배율은 이 개발기에서 재현할 수 없다. 그 환경에서 --maxrect 를 한 번 돌려 봐야 한다. 게이트: 테스트 230/230, --edit-smoke 0건, --query-popup 17/17, --db-smoke 3000 diff 0·예외 0, --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일, --dialog-shots 51개, --maxrect 10/10. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
406 lines
18 KiB
C#
406 lines
18 KiB
C#
using System.Runtime.InteropServices;
|
|
using System.Windows;
|
|
using System.Windows.Interop;
|
|
using System.Windows.Shell;
|
|
|
|
namespace SheetMe.Designer.Services;
|
|
|
|
/// <summary>
|
|
/// 최대화 크기를 <b>작업영역</b>으로 제한한다 — 작업표시줄을 덮지 않게.
|
|
///
|
|
/// <b>왜 필요한가.</b> 메인 창은 <c>WindowStyle="None"</c> + WindowChrome 으로 크롬을 직접 그린다.
|
|
/// 그 조합에서 OS 는 최대화 사각형을 작업영역(rcWork)이 아니라 <b>모니터 전체</b>(rcMonitor)를 기준으로
|
|
/// 잡고, 거기에 리사이즈 프레임 두께만큼 더 부풀린다. 실측으로 창이 <c>(-7,-7)~(1927,1087)</c> 이 되어
|
|
/// 작업영역 <c>(0,0)~(1920,1032)</c> 를 아래로 55px 넘겼다 — 작업표시줄이 통째로 가려졌다.
|
|
/// 표준 크롬 창에는 이 문제가 없다(OS 가 알아서 작업영역에 맞춘다). 그래서 이 보정은
|
|
/// <c>WindowStyle=None</c> 창에만 건다.
|
|
///
|
|
/// 고치는 방법은 <c>WM_GETMINMAXINFO</c> 한 통이다. OS 가 "최대화하면 어디에 얼마만큼"을 물어볼 때
|
|
/// 작업영역을 그대로 답한다. 레이아웃·XAML·테마는 건드리지 않는다.
|
|
///
|
|
/// <b>전제 — 이 프로세스는 System DPI aware 다.</b> 매니페스트에 DPI 선언이 없어 WPF 가 기동 중
|
|
/// <c>SetProcessDPIAware()</c> 로 올린 상태다. 그래서 MINMAXINFO·MONITORINFO·APPBARDATA 가 모두
|
|
/// 같은 좌표 공간(시스템 DPI 기준 물리 픽셀)에 있고, rcWork 를 <b>변환 없이</b> 넣을 수 있다.
|
|
/// 누군가 app.manifest 로 PerMonitorV2 를 선언하면 이 전제가 무너진다 — 좌표가 모니터별 실제 픽셀이 되고
|
|
/// WM_DPICHANGED 가 들어오기 시작하므로 이 파일을 다시 검토해야 한다.
|
|
/// PixelsPerDip 이나 TransformToDevice 를 곱하지 말 것. 지금 곱하면 125%/150% 에서 이중 스케일이 된다.
|
|
///
|
|
/// 조회에 실패하면 아무것도 쓰지 않는다 — 예전처럼 작업표시줄을 덮을 뿐 크래시는 없다.
|
|
/// </summary>
|
|
public static class MaximizeToWorkArea
|
|
{
|
|
#region Member Fields
|
|
private const int WmGetMinMaxInfo = 0x0024;
|
|
|
|
/// <summary>가장 가까운 모니터 — PRIMARY 를 쓰면 보조 모니터에서 최대화할 때 주 모니터 사각형을 쓴다</summary>
|
|
private const uint MonitorDefaultToNearest = 0x00000002;
|
|
|
|
private const uint AbmGetState = 0x00000004;
|
|
private const uint AbmGetTaskbarPos = 0x00000005;
|
|
private const uint AbmGetAutoHideBarEx = 0x0000000B;
|
|
private const int AbsAutoHide = 0x00000001;
|
|
|
|
private const uint AbeLeft = 0;
|
|
private const uint AbeTop = 1;
|
|
private const uint AbeRight = 2;
|
|
private const uint AbeBottom = 3;
|
|
|
|
/// <summary>
|
|
/// 자동 숨김 작업표시줄에게 양보하는 두께.
|
|
///
|
|
/// 자동 숨김이면 작업영역이 모니터 전체와 같아진다. 그대로 꽉 채우면 셸이 이 창을
|
|
/// '전체 화면 앱'으로 보고 가장자리 호버에 반응하지 않는다 — 작업표시줄이 영영 안 나온다.
|
|
/// 1px 로 안 나오는 셸 버전이 있으면 이 값만 2 로 올린다.
|
|
/// </summary>
|
|
private const int AutoHideReserve = 1;
|
|
|
|
/// <summary>이 창에 이미 붙였는가 — 중복 부착 표식(창과 함께 사라진다)</summary>
|
|
private static readonly DependencyProperty AttachedProperty = DependencyProperty.RegisterAttached(
|
|
"Attached", typeof(bool), typeof(MaximizeToWorkArea), new PropertyMetadata(false));
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
private struct RECT
|
|
{
|
|
public int Left;
|
|
public int Top;
|
|
public int Right;
|
|
public int Bottom;
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
private struct POINT
|
|
{
|
|
public int X;
|
|
public int Y;
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
private struct MONITORINFO
|
|
{
|
|
public int cbSize;
|
|
public RECT rcMonitor;
|
|
public RECT rcWork;
|
|
public uint dwFlags;
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
private struct APPBARDATA
|
|
{
|
|
public uint cbSize;
|
|
public IntPtr hWnd;
|
|
public uint uCallbackMessage;
|
|
public uint uEdge;
|
|
public RECT rc;
|
|
public IntPtr lParam;
|
|
}
|
|
|
|
/// <summary>
|
|
/// OS 가 최대화 크기를 물어보는 구조체.
|
|
/// <b>필드 순서를 바꾸면 조용히 오작동한다</b> — 네이티브 레이아웃 그대로여야 한다.
|
|
/// </summary>
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
private struct MINMAXINFO
|
|
{
|
|
public POINT ptReserved;
|
|
public POINT ptMaxSize;
|
|
public POINT ptMaxPosition;
|
|
public POINT ptMinTrackSize;
|
|
public POINT ptMaxTrackSize;
|
|
}
|
|
#endregion
|
|
|
|
#region Methods
|
|
[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = false)]
|
|
private static extern IntPtr MonitorFromWindow(IntPtr hwnd, uint flags);
|
|
|
|
[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = false)]
|
|
[return: MarshalAs(UnmanagedType.Bool)]
|
|
private static extern bool GetMonitorInfo(IntPtr monitor, ref MONITORINFO info);
|
|
|
|
[DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = false)]
|
|
private static extern IntPtr SHAppBarMessage(uint message, ref APPBARDATA data);
|
|
|
|
/// <summary>
|
|
/// 이 창의 최대화 크기를 작업영역으로 제한한다 — 창 핸들이 생긴 뒤(SourceInitialized)에 불러야 한다.
|
|
///
|
|
/// WindowChrome 은 <b>창마다 따로 만든 인스턴스</b>여야 한다. Style Setter 로 공유되면
|
|
/// 그 인스턴스가 봉인(Freezable)돼 최대화 시 리사이즈 띠를 끄는 쓰기가 예외를 던진다.
|
|
/// </summary>
|
|
public static void Attach(Window window)
|
|
{
|
|
// 표준 크롬 창은 이미 작업영역 기준으로 최대화된다. 거기에 창 사각형을 rcWork 로 고정하면
|
|
// 프레임 두께만큼 클라이언트가 줄고 테두리가 안쪽에 드러난다.
|
|
if (window.WindowStyle != WindowStyle.None)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// 두 번 붙으면 최대화 중에 캡처한 0 을 '복원 시 두께'로 기억해 버려,
|
|
// 복원해도 리사이즈 띠가 영영 0 인 창이 된다(가장자리를 끌어 크기를 못 바꾼다).
|
|
if ((bool)window.GetValue(AttachedProperty))
|
|
{
|
|
return;
|
|
}
|
|
window.SetValue(AttachedProperty, true);
|
|
|
|
// HWND 로 찾는다. PresentationSource.FromVisual 은 창이 아직 보이기 전에 null 을 돌려줘
|
|
// 조용히 아무것도 안 붙는다(진단 --maxrect 가 이 상태를 잡았다).
|
|
var handle = new WindowInteropHelper(window).Handle;
|
|
if (handle == IntPtr.Zero || HwndSource.FromHwnd(handle) is not { } source)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// AddHook 과 RemoveHook 은 같은 델리게이트 인스턴스여야 한다
|
|
HwndSourceHook hook = OnMessage;
|
|
source.AddHook(hook);
|
|
// Closing 이 아니라 Closed 다. 닫기 확인에서 취소되면(MainView.OnWindowClosing) 창이 살아남는데,
|
|
// 그때 훅을 이미 떼었으면 버그가 조용히 재발한다.
|
|
window.Closed += (_, _) => source.RemoveHook(hook);
|
|
|
|
TrackResizeBorder(window);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 최대화 중에는 리사이즈 띠를 걷는다.
|
|
///
|
|
/// 창을 작업영역에 정확히 맞추고 나서 드러난 증상이다. 전에는 창이 화면 밖으로 7px 밀려 있어
|
|
/// WindowChrome 의 상단 6px 리사이즈 띠가 화면 밖에 있었다. 이제 그 띠가 타이틀바 맨 위로 올라와
|
|
/// <b>최대화 상태에서 타이틀바 위쪽 6px 이 리사이즈 커서가 되고 그 부분 클릭이 먹지 않는다</b>
|
|
/// (실측: 최대화 시 y=1·3·5 에서 HTTOP, y=7 부터 HTCAPTION). 오른쪽 위 닫기 버튼의 윗변이
|
|
/// 그 띠에 걸리므로 "마우스를 구석으로 던져 닫기"가 안 된다.
|
|
///
|
|
/// 어차피 최대화된 창은 가장자리를 끌어 크기를 바꿀 수 없다 — 띠가 있을 이유가 없다.
|
|
/// 여백(Margin) 보정이 아니라 히트테스트 영역 보정이다. 창 사각형은 그대로 작업영역이다.
|
|
/// </summary>
|
|
private static void TrackResizeBorder(Window window)
|
|
{
|
|
if (WindowChrome.GetWindowChrome(window) is not { } chrome)
|
|
{
|
|
return;
|
|
}
|
|
var restored = chrome.ResizeBorderThickness;
|
|
void Sync() => chrome.ResizeBorderThickness =
|
|
window.WindowState == WindowState.Maximized ? default : restored;
|
|
window.StateChanged += (_, _) => Sync();
|
|
Sync();
|
|
}
|
|
|
|
private static IntPtr OnMessage(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
|
|
{
|
|
if (msg != WmGetMinMaxInfo || lParam == IntPtr.Zero)
|
|
{
|
|
return IntPtr.Zero;
|
|
}
|
|
|
|
try
|
|
{
|
|
// 보통은 handled 를 false 로 둔다. true 로 막으면 WPF 자신의 처리가 잘려
|
|
// MinWidth=940 / MinHeight=640 이 사라진다(창을 그 아래로 줄일 수 있게 된다).
|
|
// WPF 는 우리 뒤에 돌면서 트래킹 크기만 손대므로 우리 값은 그대로 살아남는다.
|
|
// 예외는 하나 — 최소 크기가 화면에 안 들어가는 경우다(Fill 안 설명 참조).
|
|
handled = Fill(hwnd, lParam);
|
|
}
|
|
catch (DllNotFoundException)
|
|
{
|
|
// user32/shell32 가 없는 환경은 사실상 없지만, 여기서 터지면 창이 안 뜬다.
|
|
// 아무것도 안 쓰면 예전 동작(작업표시줄을 덮음)으로 남을 뿐이다.
|
|
}
|
|
catch (EntryPointNotFoundException)
|
|
{
|
|
}
|
|
|
|
return IntPtr.Zero;
|
|
}
|
|
|
|
/// <returns>WPF 의 처리를 막아야 하면 true</returns>
|
|
private static bool Fill(IntPtr hwnd, IntPtr lParam)
|
|
{
|
|
var monitor = MonitorFromWindow(hwnd, MonitorDefaultToNearest);
|
|
if (monitor == IntPtr.Zero)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var info = new MONITORINFO { cbSize = Marshal.SizeOf<MONITORINFO>() };
|
|
if (!GetMonitorInfo(monitor, ref info))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var max = Reserve(info.rcWork, info.rcMonitor);
|
|
var width = max.Right - max.Left;
|
|
var height = max.Bottom - max.Top;
|
|
if (width <= 0 || height <= 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// 구조체 전체를 읽어 두 필드만 바꾸고 되쓴다. 새로 만들어 덮으면 OS 가 미리 채워 보낸
|
|
// 트래킹 크기 기본값이 지워진다 — lParam 은 기본값이 이미 들어찬 채로 온다.
|
|
var mmi = Marshal.PtrToStructure<MINMAXINFO>(lParam);
|
|
// ptMaxPosition 은 절대 좌표가 아니라 rcMonitor 좌상단 기준 상대 좌표다.
|
|
// rcWork 는 항상 rcMonitor 안쪽이라 이 차는 음수가 될 수 없다(Abs 를 씌우지 말 것).
|
|
mmi.ptMaxPosition = new POINT { X = max.Left - info.rcMonitor.Left, Y = max.Top - info.rcMonitor.Top };
|
|
mmi.ptMaxSize = new POINT { X = width, Y = height };
|
|
|
|
// 여기서 멈추면 좁은 화면에서 보정이 조용히 무효가 된다.
|
|
//
|
|
// OS 는 최대화 크기를 최소 트래킹 크기까지 <b>되밀어 올린다</b>. 그 값은 우리 뒤에 도는 WPF 가
|
|
// MinWidth/MinHeight(940x640 DIP)로 채운다. 작업영역이 그보다 작으면 최대화 창이 도로 부풀어
|
|
// 작업표시줄을 다시 덮는다 — 1366x768 을 125% 로 쓰면 최소 높이가 800px 인데 작업영역은 720px 다.
|
|
// 우리가 먼저 눌러 봐야 WPF 가 도로 덮어쓴다(진단 --maxrect 가 그 상태를 잡았다).
|
|
//
|
|
// 그래서 <b>그때만</b> 마지막 말을 한다: WPF 의 처리를 막고 트래킹 크기를 직접 채운다.
|
|
// 화면에 들어가지도 않는 최소 크기는 최소 크기 구실을 못 한다 — 그 화면에서는 최소 크기가 양보한다.
|
|
// 보통(작업영역이 최소 크기보다 큰 경우)에는 이 가지를 타지 않으므로
|
|
// MinWidth/MinHeight 는 WPF 가 계산한 그대로 살아 있다.
|
|
var window = SourceWindow(hwnd);
|
|
if (MinimumInPixels(window, hwnd) is not { } min || (min.X <= width && min.Y <= height))
|
|
{
|
|
Marshal.StructureToPtr(mmi, lParam, fDeleteOld: false);
|
|
return false;
|
|
}
|
|
|
|
mmi.ptMinTrackSize = new POINT { X = Math.Min(min.X, width), Y = Math.Min(min.Y, height) };
|
|
// 이 가지에서는 WPF 가 안 도므로 MaxWidth/MaxHeight 도 여기서 반영해 준다.
|
|
// (MainView 에는 없다 — 있으면 ptMaxTrackSize 가 ptMaxSize 를 잘라 최대화가 작아진다.)
|
|
if (window is not null)
|
|
{
|
|
if (ToPixels(window.MaxWidth, hwnd, horizontal: true) is { } maxWidth)
|
|
{
|
|
mmi.ptMaxTrackSize.X = maxWidth;
|
|
}
|
|
if (ToPixels(window.MaxHeight, hwnd, horizontal: false) is { } maxHeight)
|
|
{
|
|
mmi.ptMaxTrackSize.Y = maxHeight;
|
|
}
|
|
}
|
|
Marshal.StructureToPtr(mmi, lParam, fDeleteOld: false);
|
|
return true;
|
|
}
|
|
|
|
private static Window? SourceWindow(IntPtr hwnd)
|
|
=> HwndSource.FromHwnd(hwnd)?.RootVisual as Window;
|
|
|
|
/// <summary>MinWidth/MinHeight 를 픽셀로 — 창을 못 찾거나 제약이 없으면 null</summary>
|
|
private static POINT? MinimumInPixels(Window? window, IntPtr hwnd)
|
|
{
|
|
if (window is null)
|
|
{
|
|
return null;
|
|
}
|
|
var x = ToPixels(window.MinWidth, hwnd, horizontal: true);
|
|
var y = ToPixels(window.MinHeight, hwnd, horizontal: false);
|
|
return x is null && y is null ? null : new POINT { X = x ?? 0, Y = y ?? 0 };
|
|
}
|
|
|
|
/// <summary>
|
|
/// DIP → 픽셀. 변환 배율은 WPF 에게 묻는다 — 우리가 DPI 를 직접 읽어 곱하면
|
|
/// System DPI aware 전제가 깨진 날 이중 스케일이 된다.
|
|
/// </summary>
|
|
private static int? ToPixels(double dip, IntPtr hwnd, bool horizontal)
|
|
{
|
|
if (double.IsNaN(dip) || double.IsInfinity(dip) || dip <= 0)
|
|
{
|
|
return null;
|
|
}
|
|
var transform = HwndSource.FromHwnd(hwnd)?.CompositionTarget?.TransformToDevice;
|
|
var scale = horizontal ? transform?.M11 : transform?.M22;
|
|
return (int)Math.Ceiling(dip * (scale is > 0 ? scale.Value : 1.0));
|
|
}
|
|
|
|
/// <summary>
|
|
/// 자동 숨김 작업표시줄이 붙은 변에서 <see cref="AutoHideReserve"/> 만큼 물러선다.
|
|
///
|
|
/// 판정은 <b>변 단위</b>다. "rcWork == rcMonitor 일 때만"으로 묶으면
|
|
/// 하단 자동 숨김 + 우측 도킹 앱바 조합에서 감지에 실패한다.
|
|
/// </summary>
|
|
private static RECT Reserve(RECT work, RECT monitor)
|
|
{
|
|
var bars = new[]
|
|
{
|
|
HasAutoHideBar(monitor, AbeLeft),
|
|
HasAutoHideBar(monitor, AbeTop),
|
|
HasAutoHideBar(monitor, AbeRight),
|
|
HasAutoHideBar(monitor, AbeBottom),
|
|
};
|
|
|
|
// 폴백은 '한 변도 못 찾았을 때'가 아니라 '그 변을 못 찾았을 때' 돈다.
|
|
// 전자로 두면 좌측의 서드파티 자동 숨김 바 하나를 찾은 것 때문에
|
|
// EX 가 놓친 하단 작업표시줄이 구제받지 못한다.
|
|
if (FallbackEdge(monitor) is { } edge && !bars[edge])
|
|
{
|
|
bars[edge] = true;
|
|
}
|
|
|
|
// 변마다 '그 변이 모니터 가장자리에 닿아 있을 때'만 물러선다.
|
|
// 닿아 있지 않다면 다른 앱바가 이미 자리를 예약해 창이 가장자리에 닿지 않는다 — 물러서면 틈만 생긴다.
|
|
// 좌·상은 크기만 줄이면 안 되고 위치까지 밀어야 한다. Fill 이 이 사각형에서 ptMaxPosition 을
|
|
// 파생시키므로 여기서 left/top 을 옮기는 것으로 충분하다 — 순서를 바꾸지 말 것.
|
|
if (bars[AbeLeft] && work.Left == monitor.Left)
|
|
{
|
|
work.Left += AutoHideReserve;
|
|
}
|
|
if (bars[AbeTop] && work.Top == monitor.Top)
|
|
{
|
|
work.Top += AutoHideReserve;
|
|
}
|
|
if (bars[AbeRight] && work.Right == monitor.Right)
|
|
{
|
|
work.Right -= AutoHideReserve;
|
|
}
|
|
if (bars[AbeBottom] && work.Bottom == monitor.Bottom)
|
|
{
|
|
work.Bottom -= AutoHideReserve;
|
|
}
|
|
return work;
|
|
}
|
|
|
|
/// <summary>
|
|
/// ABM_GETAUTOHIDEBAREX 가 놓쳤을 때의 2차 판정 — 자동 숨김 바가 붙은 변, 없으면 null.
|
|
///
|
|
/// <b>주 작업표시줄만 본다.</b> ABM_GETSTATE 도 ABM_GETTASKBARPOS 도 주 작업표시줄 전용이라
|
|
/// 보조 모니터의 작업표시줄은 여기서도 안 잡힌다(교차 검사에서 걸러진다). 그러니 이 폴백이 실제로
|
|
/// 덮는 것은 "주 모니터에서 EX 조회가 실패한 경우"뿐이다 — 보조 모니터까지 구제하려면
|
|
/// 모니터별 앱바 열거라는 다른 수단이 필요하다.
|
|
///
|
|
/// 끝내 못 정하면 <b>양보하지 않는다</b>. "모르면 하단에 1px" 로 두면
|
|
/// 자동 숨김을 쓰지 않는 모든 사용자에게 최대화 창 아래로 바탕화면 한 줄이 상시 보인다.
|
|
/// </summary>
|
|
private static uint? FallbackEdge(RECT monitor)
|
|
{
|
|
var data = new APPBARDATA { cbSize = (uint)Marshal.SizeOf<APPBARDATA>() };
|
|
if ((SHAppBarMessage(AbmGetState, ref data).ToInt64() & AbsAutoHide) == 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var pos = new APPBARDATA { cbSize = (uint)Marshal.SizeOf<APPBARDATA>() };
|
|
if (SHAppBarMessage(AbmGetTaskbarPos, ref pos) == IntPtr.Zero || !Intersects(pos.rc, monitor))
|
|
{
|
|
return null;
|
|
}
|
|
return pos.uEdge <= AbeBottom ? pos.uEdge : null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 이 모니터의 그 변에 자동 숨김 바가 있는가.
|
|
/// rc 로 모니터를 지목할 수 있는 EX 판을 쓴다 — 구판(ABM_GETAUTOHIDEBAR)은 주 모니터만 본다.
|
|
/// </summary>
|
|
private static bool HasAutoHideBar(RECT monitor, uint edge)
|
|
{
|
|
var data = new APPBARDATA
|
|
{
|
|
cbSize = (uint)Marshal.SizeOf<APPBARDATA>(),
|
|
uEdge = edge,
|
|
rc = monitor,
|
|
};
|
|
return SHAppBarMessage(AbmGetAutoHideBarEx, ref data) != IntPtr.Zero;
|
|
}
|
|
|
|
private static bool Intersects(RECT a, RECT b)
|
|
=> a.Left < b.Right && a.Right > b.Left && a.Top < b.Bottom && a.Bottom > b.Top;
|
|
#endregion
|
|
}
|