Files
daluandou/.codely.packages/cn.tuanjie.codely.bridge@1.0.85-47f31100/Editor/Tauri/NativeWindowHelper.cs
T
huadexuan 13d0c80335 ```
chore(bridge): 删除桥接包的提交记录和Git属性配置

删除了 .codely-bridge-commit 文件中存储的提交哈希值
移除了 .gitattributes 文件中的行结束符配置规则
```
2026-09-24 21:41:14 +08:00

582 lines
25 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#if UNITY_EDITOR_WIN
using System;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using UnityEditor;
using UnityEngine;
using UnityTcp.Editor.Helpers;
namespace Cn.Tuanjie.Codely.Editor
{
/// <summary>
/// Locate the Win32 HWND that backs a Unity EditorWindow.
///
/// EditorWindow.position is in DPI-scaled "points" while Win32 reports
/// physical pixels, and Unity's point↔pixel mapping for monitors other
/// than the primary is not a single linear factor. A purely absolute
/// rect comparison therefore only works reliably on the primary monitor —
/// on secondary monitors it used to never match and the embedded panel
/// hung on "Loading" forever. The lookup below layers several strategies,
/// most reliable first:
///
/// 1. Reflection: the host GUIView's internal nativeHandle. If it is our
/// process's UnityGUIViewWndClass window, done — coordinate-free.
/// 2. If reflection yields a valid window of ours with a different class
/// (e.g. the ContainerWindow), restrict the search to that top-level
/// window; a sole GUIView child there is accepted outright (typical
/// for floating panels).
/// 3. Rect search over UnityGUIViewWndClass children, testing containment
/// under several coordinate hypotheses per candidate:
/// (a) legacy: window rect × EditorGUIUtility.pixelsPerPoint versus
/// physical pixels — exact on the primary monitor.
/// (b) pure scale: candidate physical rect ÷ its own GetDpiForWindow
/// factor versus the window rect in points.
/// (d) monitor-anchored: like (b) but preserving the monitor's
/// physical origin (points = monOrigin + (phys − monOrigin)/scale).
/// (c) container-relative: compare the candidate's offset within its
/// top-level window against the view's offset within its
/// ContainerWindow (both in points). Relative offsets never cross
/// monitors, so this holds under ANY per-monitor mapping.
/// Smallest matching candidate wins (the dock host's GUIView fully
/// contains its hosted EditorWindow's content rect; slack absorbs the
/// tab strip / scrollbar offset).
/// 4. When everything fails, a throttled diagnostic dump of every
/// candidate is logged so field reports pin down the actual mapping.
///
/// IMPORTANT: the EnumWindowsProc delegates are stored in static fields so
/// they're allocated once. Repeatedly creating fresh delegate instances on
/// every editor tick crashes Mono's delegate_hash_table when the marshal
/// cache fills up (mono-2.0-bdwgc:marshal.c delegate_hash_table_add assert).
/// </summary>
public static class NativeWindowHelper
{
// ── Win32 ──────────────────────────────────────────────────────────
[DllImport("user32.dll")]
static extern bool EnumWindows(EnumWindowsProc cb, IntPtr param);
[DllImport("user32.dll")]
static extern bool EnumChildWindows(IntPtr parent, EnumWindowsProc cb, IntPtr param);
[DllImport("user32.dll", CharSet = CharSet.Unicode, EntryPoint = "GetClassNameW")]
static extern int GetClassNameW(IntPtr hwnd, [Out] char[] buf, int max);
[DllImport("user32.dll")]
static extern uint GetWindowThreadProcessId(IntPtr hwnd, out uint processId);
[DllImport("user32.dll")]
static extern bool GetWindowRect(IntPtr hwnd, out RECT rect);
[DllImport("user32.dll")]
static extern bool GetClientRect(IntPtr hwnd, out RECT rect);
[DllImport("user32.dll")]
static extern bool ClientToScreen(IntPtr hwnd, ref POINT pt);
[DllImport("user32.dll")]
static extern bool IsWindow(IntPtr hwnd);
[DllImport("user32.dll")]
static extern IntPtr GetAncestor(IntPtr hwnd, uint flags);
[DllImport("user32.dll")]
static extern IntPtr MonitorFromWindow(IntPtr hwnd, uint flags);
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
static extern bool GetMonitorInfoW(IntPtr hMonitor, ref MONITORINFO mi);
// Windows 10 1607+; call sites guard with s_dpiApiAvailable.
[DllImport("user32.dll")]
static extern uint GetDpiForWindow(IntPtr hwnd);
const uint GA_ROOT = 2;
const uint MONITOR_DEFAULTTONEAREST = 2;
[StructLayout(LayoutKind.Sequential)]
struct RECT
{
public int Left, Top, Right, Bottom;
public int Width => Right - Left;
public int Height => Bottom - Top;
}
[StructLayout(LayoutKind.Sequential)]
struct POINT
{
public int X, Y;
}
[StructLayout(LayoutKind.Sequential)]
struct MONITORINFO
{
public int cbSize;
public RECT rcMonitor;
public RECT rcWork;
public uint dwFlags;
}
delegate bool EnumWindowsProc(IntPtr hwnd, IntPtr param);
const string kGUIViewClass = "UnityGUIViewWndClass";
// tab strip / scrollbar / window chrome can make the host GUIView several
// dozen pixels larger than the EditorWindow's inner content rect — give
// enough slack on each side to absorb that. Used as physical pixels in
// hypothesis (a) and as points elsewhere; both stay well below the size
// of a real dock area.
const int Slack = 64;
static readonly int s_currentPid = System.Diagnostics.Process.GetCurrentProcess().Id;
// Static delegate instances. Created once at type init, kept alive forever,
// so EnumWindows / EnumChildWindows always see the same managed callback.
static readonly EnumWindowsProc s_outerProc = OuterEnumProc;
static readonly EnumWindowsProc s_innerProc = InnerEnumProc;
// Per-call state. Safe to use static state because GetHWND is only invoked
// from the editor main thread and EnumWindows is synchronous.
static readonly char[] s_classBuf = new char[256];
static int s_targetLeft, s_targetTop, s_targetRight, s_targetBottom; // physical px, hypothesis (a)
static float s_targetPtLeft, s_targetPtTop, s_targetPtRight, s_targetPtBottom; // points, hypotheses (b)/(d)
static bool s_haveContainer; // hypothesis (c) inputs resolved
static bool s_relIsExact; // rel rect comes from the GUIView itself → near-equality match
static float s_containerW, s_containerH; // container size, points
static float s_relLeft, s_relTop, s_relRight, s_relBottom; // rect relative to container, points
static IntPtr s_best;
static long s_bestArea;
static int s_candidateCount;
static IntPtr s_firstCandidate;
// Flipped to false on pre-1607 Windows where GetDpiForWindow is missing;
// per-monitor hypotheses are then skipped and behavior matches the legacy search.
static bool s_dpiApiAvailable = true;
// Diagnostics: dump all candidates on the first failure and every 32nd
// one after that, so a stuck editor produces a conclusive log without
// flooding the console (retries run about twice per second).
static int s_failCount;
static StringBuilder s_diag;
static IntPtr s_diagReflected;
static string s_diagReflectedClass = "";
// Cached reflection lookups for hypothesis (c). Resolved once; any miss
// simply disables that hypothesis.
static bool s_reflectionResolved;
static FieldInfo s_fiParent; // EditorWindow.m_Parent
static PropertyInfo s_piWindow; // View.window → ContainerWindow
static PropertyInfo s_piScreenPosition; // View.screenPosition (points)
static PropertyInfo s_piContainerPosition; // ContainerWindow.position (points)
// ── Public API ─────────────────────────────────────────────────────
public static IntPtr GetHWND(EditorWindow win)
{
if (win == null) return IntPtr.Zero;
try
{
// Strategy 1/2: reflected native handle, exact or as search scope.
IntPtr searchRoot;
IntPtr reflected = TryReflectedHandle(win, out searchRoot);
if (reflected != IntPtr.Zero)
{
s_failCount = 0;
return reflected;
}
Rect pos = win.position;
if (pos.width < 1f || pos.height < 1f) return IntPtr.Zero;
PrepareTargets(win, pos);
bool collectDiag = s_failCount == 0 || (s_failCount % 32) == 0;
s_diag = collectDiag ? new StringBuilder() : null;
IntPtr found = IntPtr.Zero;
if (searchRoot != IntPtr.Zero)
{
if (s_diag != null) s_diag.AppendLine($"-- scoped pass under root=0x{searchRoot.ToInt64():X} --");
found = SearchScoped(searchRoot);
}
if (found == IntPtr.Zero)
{
if (s_diag != null) s_diag.AppendLine("-- global pass --");
found = SearchGlobal();
}
if (found != IntPtr.Zero)
{
s_failCount = 0;
s_diag = null;
return found;
}
s_failCount++;
if (s_diag != null)
{
LogDiagnostics(win, pos, searchRoot);
s_diag = null;
}
return IntPtr.Zero;
}
catch (Exception ex)
{
CodelyLogger.LogWarning($"[NativeWindowHelper] GetHWND exception: {ex.Message}");
return IntPtr.Zero;
}
}
// ── Strategy 1/2: reflection ───────────────────────────────────────
/// <summary>
/// Read GUIView.nativeHandle through the shared reflection helper. If it
/// is our process's UnityGUIViewWndClass window, return it. If it is a
/// valid window of ours with a different class (some editor builds hand
/// out the container window instead), expose its top-level ancestor via
/// <paramref name="searchRoot"/> so the rect search can be scoped to the
/// right container. Anything else (zero, foreign, not a window at all —
/// e.g. a native object pointer) is ignored.
/// </summary>
static IntPtr TryReflectedHandle(EditorWindow win, out IntPtr searchRoot)
{
searchRoot = IntPtr.Zero;
s_diagReflected = IntPtr.Zero;
s_diagReflectedClass = "";
try
{
IntPtr handle = EditorWindowNativeHandleHelper.GetGUIViewHandle(win);
s_diagReflected = handle;
if (handle == IntPtr.Zero || !IsWindow(handle)) return IntPtr.Zero;
GetWindowThreadProcessId(handle, out uint pid);
if ((int)pid != s_currentPid) return IntPtr.Zero;
string cls = ClassNameOf(handle);
s_diagReflectedClass = cls;
if (cls == kGUIViewClass) return handle;
searchRoot = GetAncestor(handle, GA_ROOT);
return IntPtr.Zero;
}
catch
{
// Reflection target changed shape or the native view is mid-teardown.
return IntPtr.Zero;
}
}
// ── Strategy 3: rect search ────────────────────────────────────────
static IntPtr SearchScoped(IntPtr root)
{
ResetSearchState();
EnumChildWindows(root, s_innerProc, IntPtr.Zero);
if (s_best != IntPtr.Zero) return s_best;
// The scope IS the window's own container; if it hosts exactly one
// GUIView there is nothing to disambiguate — take it even when no
// coordinate hypothesis matched (typical floating panel).
if (s_candidateCount == 1) return s_firstCandidate;
return IntPtr.Zero;
}
static IntPtr SearchGlobal()
{
ResetSearchState();
EnumWindows(s_outerProc, IntPtr.Zero);
return s_best;
}
static void ResetSearchState()
{
s_best = IntPtr.Zero;
s_bestArea = long.MaxValue;
s_candidateCount = 0;
s_firstCandidate = IntPtr.Zero;
}
/// <summary>Fill the per-call matching targets for all hypotheses.</summary>
static void PrepareTargets(EditorWindow win, Rect pos)
{
Rect target = ScaleToPixels(pos);
s_targetLeft = (int)target.x;
s_targetTop = (int)target.y;
s_targetRight = (int)(target.x + target.width);
s_targetBottom = (int)(target.y + target.height);
s_targetPtLeft = pos.x;
s_targetPtTop = pos.y;
s_targetPtRight = pos.x + pos.width;
s_targetPtBottom = pos.y + pos.height;
// Hypothesis (c): the window's rect relative to its ContainerWindow,
// in points. Prefer the host GUIView's own screenPosition — it should
// equal the candidate HWND's rect, allowing a near-equality match.
s_haveContainer = false;
s_relIsExact = false;
try
{
ResolveReflection();
object parent = s_fiParent?.GetValue(win);
if (parent == null) return;
object container = s_piWindow?.GetValue(parent, null);
if (container == null || s_piContainerPosition == null) return;
Rect containerPos = (Rect)s_piContainerPosition.GetValue(container, null);
if (containerPos.width < 1f || containerPos.height < 1f) return;
Rect basis = pos;
if (s_piScreenPosition != null)
{
Rect sp = (Rect)s_piScreenPosition.GetValue(parent, null);
if (sp.width >= 1f && sp.height >= 1f)
{
basis = sp;
s_relIsExact = true;
}
}
s_containerW = containerPos.width;
s_containerH = containerPos.height;
s_relLeft = basis.x - containerPos.x;
s_relTop = basis.y - containerPos.y;
s_relRight = s_relLeft + basis.width;
s_relBottom = s_relTop + basis.height;
s_haveContainer = true;
}
catch
{
s_haveContainer = false;
}
}
static void ResolveReflection()
{
if (s_reflectionResolved) return;
s_reflectionResolved = true;
try
{
const BindingFlags flags = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public;
s_fiParent = typeof(EditorWindow).GetField("m_Parent", flags);
Type viewType = s_fiParent?.FieldType; // HostView (derives from GUIView/View)
s_piWindow = viewType?.GetProperty("window", flags);
s_piScreenPosition = viewType?.GetProperty("screenPosition", flags);
Type containerType = s_piWindow?.PropertyType; // ContainerWindow
s_piContainerPosition = containerType?.GetProperty("position", flags);
}
catch
{
// Editor internals changed shape; hypothesis (c) stays disabled.
}
}
// ── Internals ──────────────────────────────────────────────────────
// EditorWindow.position is in DPI-scaled "points"; Win32 GetWindowRect
// returns physical pixels. Convert before comparing. Note this uses the
// CALLER's pixelsPerPoint, which only matches the window's monitor when
// both share a DPI scale — the other hypotheses cover the rest.
static Rect ScaleToPixels(Rect r)
{
float dpi;
try { dpi = EditorGUIUtility.pixelsPerPoint; }
catch { dpi = 1f; }
if (dpi <= 0f) dpi = 1f;
return new Rect(r.x * dpi, r.y * dpi, r.width * dpi, r.height * dpi);
}
static string ClassNameOf(IntPtr hwnd)
{
int clen = GetClassNameW(hwnd, s_classBuf, s_classBuf.Length);
return clen > 0 ? new string(s_classBuf, 0, clen) : "";
}
[AOT.MonoPInvokeCallback(typeof(EnumWindowsProc))]
static bool OuterEnumProc(IntPtr topHwnd, IntPtr param)
{
try
{
GetWindowThreadProcessId(topHwnd, out uint pid);
if ((int)pid != s_currentPid) return true;
EnumChildWindows(topHwnd, s_innerProc, IntPtr.Zero);
}
catch { /* ignore */ }
return true;
}
[AOT.MonoPInvokeCallback(typeof(EnumWindowsProc))]
static bool InnerEnumProc(IntPtr childHwnd, IntPtr param)
{
try
{
if (ClassNameOf(childHwnd) != kGUIViewClass) return true;
if (!GetWindowRect(childHwnd, out RECT wr)) return true;
s_candidateCount++;
if (s_candidateCount == 1) s_firstCandidate = childHwnd;
float scale = s_dpiApiAvailable ? GetWindowScale(childHwnd) : 0f;
// (a) physical-pixel comparison with the caller-context scale
bool a =
wr.Left - Slack <= s_targetLeft &&
wr.Top - Slack <= s_targetTop &&
wr.Right + Slack >= s_targetRight &&
wr.Bottom + Slack >= s_targetBottom;
// (b) point-space comparison with the candidate's own monitor scale
bool b = false;
if (!a && scale > 0f)
{
b =
wr.Left / scale - Slack <= s_targetPtLeft &&
wr.Top / scale - Slack <= s_targetPtTop &&
wr.Right / scale + Slack >= s_targetPtRight &&
wr.Bottom / scale + Slack >= s_targetPtBottom;
}
// (d) like (b) but anchored at the candidate's monitor origin
bool d = false;
if (!a && !b && scale > 0f)
{
var mi = new MONITORINFO { cbSize = Marshal.SizeOf(typeof(MONITORINFO)) };
IntPtr hmon = MonitorFromWindow(childHwnd, MONITOR_DEFAULTTONEAREST);
if (hmon != IntPtr.Zero && GetMonitorInfoW(hmon, ref mi))
{
float mx = mi.rcMonitor.Left, my = mi.rcMonitor.Top;
d =
mx + (wr.Left - mx) / scale - Slack <= s_targetPtLeft &&
my + (wr.Top - my) / scale - Slack <= s_targetPtTop &&
mx + (wr.Right - mx) / scale + Slack >= s_targetPtRight &&
my + (wr.Bottom - my) / scale + Slack >= s_targetPtBottom;
}
}
// (c) container-relative comparison — mapping-independent
bool c = false;
if (!a && !b && !d && scale > 0f && s_haveContainer)
{
c = MatchesContainerRelative(childHwnd, wr, scale);
}
if (s_diag != null)
{
// Extra geometry so a single failed dump reconstructs the mapping.
string monStr = "?", rootStr = "?";
var dmi = new MONITORINFO { cbSize = Marshal.SizeOf(typeof(MONITORINFO)) };
IntPtr dmon = MonitorFromWindow(childHwnd, MONITOR_DEFAULTTONEAREST);
if (dmon != IntPtr.Zero && GetMonitorInfoW(dmon, ref dmi))
monStr = $"({dmi.rcMonitor.Left},{dmi.rcMonitor.Top})";
IntPtr droot = GetAncestor(childHwnd, GA_ROOT);
if (droot != IntPtr.Zero && GetClientRect(droot, out RECT drc))
{
var dorigin = new POINT();
if (ClientToScreen(droot, ref dorigin))
rootStr = $"0x{droot.ToInt64():X}@({dorigin.X},{dorigin.Y}) client={drc.Right}x{drc.Bottom}";
}
s_diag.AppendLine(
$" cand=0x{childHwnd.ToInt64():X} phys=({wr.Left},{wr.Top})-({wr.Right},{wr.Bottom}) " +
$"scale={scale:F2} mon={monStr} root={rootStr} a={a} b={b} d={d} c={c}");
}
if (!(a || b || d || c)) return true;
long area = (long)wr.Width * wr.Height;
if (area < s_bestArea)
{
s_bestArea = area;
s_best = childHwnd;
}
}
catch { /* window may have closed mid-enumeration */ }
return true;
}
static bool MatchesContainerRelative(IntPtr childHwnd, RECT wr, float scale)
{
IntPtr root = GetAncestor(childHwnd, GA_ROOT);
if (root == IntPtr.Zero) return false;
if (!GetClientRect(root, out RECT rc)) return false;
var origin = new POINT();
if (!ClientToScreen(root, ref origin)) return false;
// The right top-level window has (about) the ContainerWindow's size.
// Unity's ContainerWindow.position may describe the client area or the
// full frame depending on chrome — accept either within slack.
float clientW = rc.Right / scale, clientH = rc.Bottom / scale;
bool sizeOk =
Math.Abs(clientW - s_containerW) <= Slack &&
Math.Abs(clientH - s_containerH) <= Slack;
if (!sizeOk && GetWindowRect(root, out RECT rwr))
{
sizeOk =
Math.Abs(rwr.Width / scale - s_containerW) <= Slack &&
Math.Abs(rwr.Height / scale - s_containerH) <= Slack;
}
if (!sizeOk) return false;
float relLeft = (wr.Left - origin.X) / scale;
float relTop = (wr.Top - origin.Y) / scale;
float relRight = (wr.Right - origin.X) / scale;
float relBottom = (wr.Bottom - origin.Y) / scale;
if (s_relIsExact)
{
// Comparing the GUIView's own rect — edges should line up.
return
Math.Abs(relLeft - s_relLeft) <= Slack &&
Math.Abs(relTop - s_relTop) <= Slack &&
Math.Abs(relRight - s_relRight) <= Slack &&
Math.Abs(relBottom - s_relBottom) <= Slack;
}
// Comparing the hosted EditorWindow content rect — the GUIView contains it.
return
relLeft - Slack <= s_relLeft &&
relTop - Slack <= s_relTop &&
relRight + Slack >= s_relRight &&
relBottom + Slack >= s_relBottom;
}
static float GetWindowScale(IntPtr hwnd)
{
try
{
uint dpi = GetDpiForWindow(hwnd);
return dpi > 0 ? dpi / 96f : 0f;
}
catch (EntryPointNotFoundException)
{
s_dpiApiAvailable = false;
return 0f;
}
}
static void LogDiagnostics(EditorWindow win, Rect pos, IntPtr searchRoot)
{
try
{
float ppp;
try { ppp = EditorGUIUtility.pixelsPerPoint; } catch { ppp = -1f; }
var header = new StringBuilder();
header.AppendLine($"[NativeWindowHelper] diagnose (fail #{s_failCount}): no GUIView matched {win.GetType().Name}");
header.AppendLine($" win.position=({pos.x:F1},{pos.y:F1},{pos.width:F1},{pos.height:F1}) pixelsPerPoint={ppp:F2} dpiApi={s_dpiApiAvailable}");
header.AppendLine($" reflected=0x{s_diagReflected.ToInt64():X} class='{s_diagReflectedClass}' searchRoot=0x{searchRoot.ToInt64():X}");
header.AppendLine(s_haveContainer
? $" container={s_containerW:F1}x{s_containerH:F1} rel=({s_relLeft:F1},{s_relTop:F1})-({s_relRight:F1},{s_relBottom:F1}) exact={s_relIsExact}"
: " container=<unavailable>");
header.Append(s_diag);
CodelyLogger.LogWarning(header.ToString());
}
catch { /* diagnostics must never break the lookup */ }
}
}
}
#endif