Last active
January 9, 2026 07:02
-
-
Save fangzhangmnm/f11c89e0922ff34bbb8f6912a6d9dbaa to your computer and use it in GitHub Desktop.
Toggle fullscreen in Unity3D using F11 hotkey
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /********* | |
| This script enables F11 to toggle fullscreen mode in the Unity Editor. | |
| Warning: Code is blind-generated by AI without human inspection, use at your own risk. | |
| Author: chatgpt.com | |
| Supervisor: fangzhangmnm, Jan.9 2026 | |
| License: MIT | |
| *********/ | |
| #if UNITY_EDITOR | |
| using System; | |
| using System.Reflection; | |
| using UnityEditor; | |
| using UnityEditor.ShortcutManagement; | |
| using UnityEngine; | |
| #if UNITY_EDITOR_WIN | |
| using System.Runtime.InteropServices; | |
| #endif | |
| /// <summary> | |
| /// F11 fullscreen toggle: | |
| /// - Edit Mode (Windows): toggles the main Unity Editor window borderless fullscreen. | |
| /// - Play Mode: toggles a borderless fullscreen GameView popup. | |
| /// | |
| /// Notes: | |
| /// - In some Unity versions, shortcuts won't fire when the GameView popup is focused or right after entering Play Mode. | |
| /// On Windows, a Win32 hotkey poller (GetAsyncKeyState) keeps F11 working. | |
| /// - Some GameView settings (eg "Warn if No Cameras Rendering") are stored in Unity internals with version-specific names. | |
| /// We mirror them by heuristic reflection. | |
| /// </summary> | |
| [InitializeOnLoad] | |
| public static class EditorFullscreenF11 | |
| { | |
| private const string GamePopupTitle = "Game (F11 Fullscreen)"; | |
| private static readonly Type GameViewType = Type.GetType("UnityEditor.GameView,UnityEditor"); | |
| private static readonly PropertyInfo ShowToolbarProp = | |
| GameViewType?.GetProperty("showToolbar", BindingFlags.Instance | BindingFlags.NonPublic); | |
| private static EditorWindow s_PrePopupFocused; | |
| #if UNITY_EDITOR_WIN | |
| private static bool s_PollF11; // Poll while popup exists | |
| private static double s_SuppressPollUntil; // Debounce to avoid immediate re-toggles | |
| #endif | |
| static EditorFullscreenF11() | |
| { | |
| #if UNITY_EDITOR_WIN | |
| EditorApplication.update -= PollF11; | |
| EditorApplication.update += PollF11; | |
| EditorApplication.playModeStateChanged -= OnPlayModeStateChanged; | |
| EditorApplication.playModeStateChanged += OnPlayModeStateChanged; | |
| EditorApplication.quitting -= OnQuitting; | |
| EditorApplication.quitting += OnQuitting; | |
| #endif | |
| // Close popup when leaving play mode (works on all platforms) | |
| EditorApplication.playModeStateChanged -= ClosePopupOnPlayMode; | |
| EditorApplication.playModeStateChanged += ClosePopupOnPlayMode; | |
| } | |
| // ============================================================ | |
| // F11 entry point (Unity Shortcut) | |
| // ============================================================ | |
| [Shortcut("Custom/F11 Fullscreen Toggle", KeyCode.F11)] | |
| private static void OnF11() | |
| { | |
| if (EditorApplication.isPlaying) | |
| { | |
| #if UNITY_EDITOR_WIN | |
| // If the poller is active, suppress it for this same keypress. | |
| SuppressPoll(0.25); | |
| ConsumeF11Transition(); | |
| #endif | |
| ToggleGamePopup(); | |
| return; | |
| } | |
| #if UNITY_EDITOR_WIN | |
| ToggleMainEditorBorderless(); | |
| #else | |
| ToggleFocusedDockedMaximizeFallback(); | |
| #endif | |
| } | |
| // ============================================================ | |
| // Play Mode fullscreen GameView popup | |
| // ============================================================ | |
| private static void ToggleGamePopup() | |
| { | |
| if (CloseGamePopupWindows()) | |
| return; | |
| OpenGamePopupWindow(); | |
| } | |
| private static void OpenGamePopupWindow() | |
| { | |
| if (GameViewType == null) | |
| { | |
| Debug.LogError("UnityEditor.GameView type not found (Unity internals changed?)."); | |
| return; | |
| } | |
| s_PrePopupFocused = EditorWindow.focusedWindow; | |
| var popup = (EditorWindow)ScriptableObject.CreateInstance(GameViewType); | |
| popup.titleContent = new GUIContent(GamePopupTitle); | |
| // Hide toolbar if Unity exposes it (internal; not guaranteed) | |
| try { ShowToolbarProp?.SetValue(popup, false); } catch { /* ignore */ } | |
| popup.ShowPopup(); | |
| // Initial best-effort fullscreen sizing | |
| var res = Screen.currentResolution; | |
| popup.position = new Rect(0, 0, res.width, res.height); | |
| popup.Focus(); | |
| // Copy settings after internal init | |
| EditorApplication.delayCall += () => ApplyPopupGameViewSettings(popup); | |
| #if UNITY_EDITOR_WIN | |
| // Enable Win32 polling so F11 works even when popup is focused. | |
| s_PollF11 = true; | |
| SuppressPoll(0.35); | |
| ConsumeF11Transition(); | |
| // Taskbar-cover + topmost after native HWND exists. | |
| EditorApplication.delayCall += () => EditorApplication.delayCall += ForcePopupTopmostAndCoverTaskbar; | |
| #endif | |
| } | |
| private static void ClosePopupOnPlayMode(PlayModeStateChange state) | |
| { | |
| if (state == PlayModeStateChange.ExitingPlayMode || | |
| state == PlayModeStateChange.EnteredEditMode || | |
| state == PlayModeStateChange.ExitingEditMode) | |
| { | |
| CloseGamePopupWindows(); | |
| } | |
| } | |
| private static bool CloseGamePopupWindows() | |
| { | |
| if (GameViewType == null) return false; | |
| bool closed = false; | |
| var all = Resources.FindObjectsOfTypeAll(GameViewType); | |
| foreach (var obj in all) | |
| { | |
| if (obj is not EditorWindow ew) continue; | |
| if (ew.titleContent == null) continue; | |
| if (ew.titleContent.text == GamePopupTitle) | |
| { | |
| ew.Close(); | |
| closed = true; | |
| } | |
| } | |
| if (closed) | |
| { | |
| #if UNITY_EDITOR_WIN | |
| s_PollF11 = false; | |
| SuppressPoll(0.20); | |
| ConsumeF11Transition(); | |
| #endif | |
| AfterPopupClosed(); | |
| } | |
| return closed; | |
| } | |
| private static void AfterPopupClosed() | |
| { | |
| // GameView can remain stretched until it receives focus. Mimic the click. | |
| EditorApplication.delayCall += () => | |
| { | |
| try | |
| { | |
| if (GameViewType != null) | |
| EditorWindow.FocusWindowIfItsOpen(GameViewType); | |
| } | |
| catch { /* ignore */ } | |
| EditorApplication.QueuePlayerLoopUpdate(); | |
| try { UnityEditorInternal.InternalEditorUtility.RepaintAllViews(); } catch { /* ignore */ } | |
| // Optional: restore focus back to whatever window you were using. | |
| try { s_PrePopupFocused?.Focus(); } catch { /* ignore */ } | |
| }; | |
| } | |
| #if UNITY_EDITOR_WIN | |
| private static void ForcePopupTopmostAndCoverTaskbar() | |
| { | |
| var hwnd = Win32.FindWindowByExactOrContains(GamePopupTitle); | |
| if (hwnd == IntPtr.Zero) return; | |
| var bounds = Win32.GetMonitorRect(hwnd); // rcMonitor (covers taskbar) | |
| Win32.MakeBorderless(hwnd); | |
| Win32.SetTopmostFullscreen(hwnd, bounds); | |
| } | |
| #endif | |
| // ============================================================ | |
| // Copy GameView settings (heuristic reflection) | |
| // ============================================================ | |
| private static void ApplyPopupGameViewSettings(EditorWindow popup) | |
| { | |
| if (popup == null || GameViewType == null) return; | |
| var source = FindPrimaryGameViewForSettings(); | |
| // Warn if No Cameras Rendering | |
| bool warn = false; | |
| if (source != null && TryGetNoCameraWarningHeuristic(source, out var fromSource)) | |
| warn = fromSource; | |
| // Some Unity versions store this globally/static; apply to both. | |
| ApplyNoCameraWarningHeuristic(popup, warn); | |
| ApplyNoCameraWarningHeuristic(GameViewType, warn, includeStatic: true); | |
| // Target display (best-effort) | |
| if (source != null && TryGetIntByPredicate(source, out var display, mi => NameContains(mi, "display"))) | |
| TrySetIntByPredicate(popup, display, mi => NameContains(mi, "display"), includeStatic: false); | |
| try { popup.Repaint(); } catch { /* ignore */ } | |
| } | |
| private static EditorWindow FindPrimaryGameViewForSettings() | |
| { | |
| if (GameViewType == null) return null; | |
| var all = Resources.FindObjectsOfTypeAll(GameViewType); | |
| EditorWindow firstNonPopup = null; | |
| foreach (var obj in all) | |
| { | |
| if (obj is not EditorWindow ew) continue; | |
| if (ew.titleContent == null) continue; | |
| // Skip our popup(s) | |
| if (ew.titleContent.text == GamePopupTitle) | |
| continue; | |
| // Prefer Unity's default title | |
| if (ew.titleContent.text == "Game") | |
| return ew; | |
| firstNonPopup ??= ew; | |
| } | |
| return firstNonPopup; | |
| } | |
| private static bool TryGetNoCameraWarningHeuristic(object obj, out bool value) | |
| => TryGetBoolByPredicate(obj, out value, mi => NameContains(mi, "warn") && NameContains(mi, "camera")); | |
| private static void ApplyNoCameraWarningHeuristic(object target, bool warn, bool includeStatic = false) | |
| { | |
| TrySetBoolByPredicate(target, warn, mi => NameContains(mi, "warn") && NameContains(mi, "camera"), includeStatic); | |
| // Historic/common names (backup) | |
| TrySetBoolMember(target, warn, | |
| "warnIfNoCameraRendering", | |
| "warnIfNoCamerasRendering", | |
| "m_WarnIfNoCameraRendering", | |
| "m_WarnIfNoCamerasRendering", | |
| "showNoCameraWarning", | |
| "m_ShowNoCameraWarning", | |
| "m_ShowNoCamerasWarning"); | |
| } | |
| // --- Reflection helpers (compact, heuristic) --- | |
| private static bool NameContains(MemberInfo mi, string token) | |
| => mi != null && mi.Name.IndexOf(token, StringComparison.OrdinalIgnoreCase) >= 0; | |
| private static int ScoreMemberName(string name) | |
| { | |
| if (string.IsNullOrEmpty(name)) return 0; | |
| var n = name.ToLowerInvariant(); | |
| int s = 0; | |
| if (n.Contains("nocamera")) s += 5; | |
| if (n.Contains("nocameras")) s += 5; | |
| if (n.Contains("render")) s += 2; | |
| if (n.Contains("warn")) s += 2; | |
| return s; | |
| } | |
| private static bool TryGetBoolByPredicate(object obj, out bool value, Func<MemberInfo, bool> predicate) | |
| { | |
| value = default; | |
| if (obj == null) return false; | |
| var t = obj as Type ?? obj.GetType(); | |
| const BindingFlags Inst = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; | |
| const BindingFlags Stat = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; | |
| bool found = false; | |
| bool best = default; | |
| int bestScore = int.MinValue; | |
| foreach (var p in t.GetProperties(Inst | Stat)) | |
| { | |
| if (p.PropertyType != typeof(bool) || !p.CanRead) continue; | |
| if (!predicate(p)) continue; | |
| try | |
| { | |
| bool v = (bool)p.GetValue(obj is Type ? null : obj); | |
| int score = ScoreMemberName(p.Name); | |
| if (score > bestScore) { bestScore = score; best = v; found = true; } | |
| } | |
| catch { } | |
| } | |
| foreach (var f in t.GetFields(Inst | Stat)) | |
| { | |
| if (f.FieldType != typeof(bool)) continue; | |
| if (!predicate(f)) continue; | |
| try | |
| { | |
| bool v = (bool)f.GetValue(obj is Type ? null : obj); | |
| int score = ScoreMemberName(f.Name); | |
| if (score > bestScore) { bestScore = score; best = v; found = true; } | |
| } | |
| catch { } | |
| } | |
| if (!found) return false; | |
| value = best; | |
| return true; | |
| } | |
| private static void TrySetBoolByPredicate(object obj, bool value, Func<MemberInfo, bool> predicate, bool includeStatic) | |
| { | |
| if (obj == null) return; | |
| var t = obj as Type ?? obj.GetType(); | |
| const BindingFlags Inst = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; | |
| const BindingFlags Stat = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; | |
| var flags = Inst | (includeStatic ? Stat : 0); | |
| foreach (var p in t.GetProperties(flags)) | |
| { | |
| if (p.PropertyType != typeof(bool) || !p.CanWrite) continue; | |
| if (!predicate(p)) continue; | |
| try { p.SetValue(obj is Type ? null : obj, value); } catch { } | |
| } | |
| foreach (var f in t.GetFields(flags)) | |
| { | |
| if (f.FieldType != typeof(bool) || f.IsInitOnly) continue; | |
| if (!predicate(f)) continue; | |
| try { f.SetValue(obj is Type ? null : obj, value); } catch { } | |
| } | |
| } | |
| private static bool TryGetIntByPredicate(object obj, out int value, Func<MemberInfo, bool> predicate) | |
| { | |
| value = default; | |
| if (obj == null) return false; | |
| var t = obj as Type ?? obj.GetType(); | |
| const BindingFlags Inst = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; | |
| const BindingFlags Stat = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; | |
| foreach (var p in t.GetProperties(Inst | Stat)) | |
| { | |
| if (p.PropertyType != typeof(int) || !p.CanRead) continue; | |
| if (!predicate(p)) continue; | |
| try { value = (int)p.GetValue(obj is Type ? null : obj); return true; } catch { } | |
| } | |
| foreach (var f in t.GetFields(Inst | Stat)) | |
| { | |
| if (f.FieldType != typeof(int)) continue; | |
| if (!predicate(f)) continue; | |
| try { value = (int)f.GetValue(obj is Type ? null : obj); return true; } catch { } | |
| } | |
| return false; | |
| } | |
| private static void TrySetIntByPredicate(object obj, int value, Func<MemberInfo, bool> predicate, bool includeStatic) | |
| { | |
| if (obj == null) return; | |
| var t = obj as Type ?? obj.GetType(); | |
| const BindingFlags Inst = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; | |
| const BindingFlags Stat = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; | |
| var flags = Inst | (includeStatic ? Stat : 0); | |
| foreach (var p in t.GetProperties(flags)) | |
| { | |
| if (p.PropertyType != typeof(int) || !p.CanWrite) continue; | |
| if (!predicate(p)) continue; | |
| try { p.SetValue(obj is Type ? null : obj, value); } catch { } | |
| } | |
| foreach (var f in t.GetFields(flags)) | |
| { | |
| if (f.FieldType != typeof(int) || f.IsInitOnly) continue; | |
| if (!predicate(f)) continue; | |
| try { f.SetValue(obj is Type ? null : obj, value); } catch { } | |
| } | |
| } | |
| private static void TrySetBoolMember(object obj, bool value, params string[] names) | |
| { | |
| if (obj == null) return; | |
| var t = obj as Type ?? obj.GetType(); | |
| const BindingFlags F = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; | |
| foreach (var n in names) | |
| { | |
| var p = t.GetProperty(n, F); | |
| if (p != null && p.PropertyType == typeof(bool) && p.CanWrite) | |
| { | |
| try { p.SetValue(obj is Type ? null : obj, value); return; } catch { } | |
| } | |
| var f = t.GetField(n, F); | |
| if (f != null && f.FieldType == typeof(bool) && !f.IsInitOnly) | |
| { | |
| try { f.SetValue(obj is Type ? null : obj, value); return; } catch { } | |
| } | |
| } | |
| } | |
| // ============================================================ | |
| // Non-Windows fallback (docked maximize) | |
| // ============================================================ | |
| private static void ToggleFocusedDockedMaximizeFallback() | |
| { | |
| var w = EditorWindow.focusedWindow; | |
| if (w == null) return; | |
| w.maximized = !w.maximized; | |
| w.Focus(); | |
| } | |
| #if UNITY_EDITOR_WIN | |
| // ============================================================ | |
| // Windows: F11 poller for Play Mode and popup focus quirks | |
| // ============================================================ | |
| private const int VK_F11 = 0x7A; | |
| private static void PollF11() | |
| { | |
| // Poll in two cases: | |
| // - While popup exists (shortcuts often stop firing when popup is focused) | |
| // - While in Play Mode (sometimes you need to click editor before shortcuts work) | |
| if (!EditorApplication.isPlaying && !s_PollF11) | |
| return; | |
| // In Play Mode, allow even if Unity isn't foreground yet (matches controller-input observation). | |
| // Out of play mode, require Unity foreground to avoid toggling while typing elsewhere. | |
| if (!EditorApplication.isPlaying && !Win32.IsUnityForegroundProcess()) | |
| return; | |
| // If not playing, polling only makes sense while the popup exists. | |
| if (!EditorApplication.isPlaying && !Win32.WindowExists(GamePopupTitle)) | |
| { | |
| s_PollF11 = false; | |
| return; | |
| } | |
| if (EditorApplication.timeSinceStartup < s_SuppressPollUntil) | |
| return; | |
| if ((Win32.GetAsyncKeyState(VK_F11) & 1) != 0) | |
| { | |
| SuppressPoll(0.20); | |
| ToggleGamePopup(); | |
| } | |
| } | |
| private static void SuppressPoll(double seconds) | |
| => s_SuppressPollUntil = Math.Max(s_SuppressPollUntil, EditorApplication.timeSinceStartup + seconds); | |
| private static void ConsumeF11Transition() | |
| => _ = Win32.GetAsyncKeyState(VK_F11); | |
| // ============================================================ | |
| // Windows: Borderless fullscreen main Unity Editor window | |
| // ============================================================ | |
| private const string K_IsBorderless = "F11Borderless.IsBorderless"; | |
| private const string K_WasBorderlessBeforePlay = "F11Borderless.WasBeforePlay"; | |
| private const string K_OrigStyle = "F11Borderless.OrigStyle"; | |
| private const string K_OrigExStyle = "F11Borderless.OrigExStyle"; | |
| private const string K_OrigRectL = "F11Borderless.OrigRectL"; | |
| private const string K_OrigRectT = "F11Borderless.OrigRectT"; | |
| private const string K_OrigRectR = "F11Borderless.OrigRectR"; | |
| private const string K_OrigRectB = "F11Borderless.OrigRectB"; | |
| private static void ToggleMainEditorBorderless() | |
| { | |
| if (!Win32.TryGetUnityMainWindow(out var hwnd)) | |
| { | |
| Debug.LogWarning("Could not find Unity main window handle. Make sure the Unity Editor is the foreground window."); | |
| return; | |
| } | |
| if (!SessionState.GetBool(K_IsBorderless, false)) | |
| EnterBorderless(hwnd); | |
| else | |
| ExitBorderless(hwnd); | |
| } | |
| private static void OnPlayModeStateChanged(PlayModeStateChange state) | |
| { | |
| // Before entering play mode: restore normal window to avoid getting stuck after domain reload. | |
| if (state == PlayModeStateChange.ExitingEditMode) | |
| { | |
| if (SessionState.GetBool(K_IsBorderless, false) && Win32.TryGetUnityMainWindow(out var hwnd)) | |
| { | |
| SessionState.SetBool(K_WasBorderlessBeforePlay, true); | |
| ExitBorderless(hwnd); | |
| } | |
| else | |
| { | |
| SessionState.SetBool(K_WasBorderlessBeforePlay, false); | |
| } | |
| } | |
| // After returning to edit mode: re-apply borderless if it was enabled before. | |
| if (state == PlayModeStateChange.EnteredEditMode) | |
| { | |
| if (SessionState.GetBool(K_WasBorderlessBeforePlay, false) && Win32.TryGetUnityMainWindow(out var hwnd)) | |
| { | |
| EnterBorderless(hwnd, preserveOriginals: true); | |
| SessionState.SetBool(K_WasBorderlessBeforePlay, false); | |
| } | |
| } | |
| } | |
| private static void OnQuitting() | |
| { | |
| if (SessionState.GetBool(K_IsBorderless, false) && Win32.TryGetUnityMainWindow(out var hwnd)) | |
| ExitBorderless(hwnd); | |
| } | |
| private static bool HasStoredOriginals() => SessionState.GetInt(K_OrigStyle, 0) != 0; | |
| private static void EnterBorderless(IntPtr hwnd, bool preserveOriginals = false) | |
| { | |
| if (!preserveOriginals || !HasStoredOriginals()) | |
| { | |
| SessionState.SetInt(K_OrigStyle, Win32.GetWindowLongSafe(hwnd, Win32.GWL_STYLE)); | |
| SessionState.SetInt(K_OrigExStyle, Win32.GetWindowLongSafe(hwnd, Win32.GWL_EXSTYLE)); | |
| Win32.GetWindowRect(hwnd, out var r); | |
| SessionState.SetInt(K_OrigRectL, r.left); | |
| SessionState.SetInt(K_OrigRectT, r.top); | |
| SessionState.SetInt(K_OrigRectR, r.right); | |
| SessionState.SetInt(K_OrigRectB, r.bottom); | |
| } | |
| int originalStyle = SessionState.GetInt(K_OrigStyle, Win32.GetWindowLongSafe(hwnd, Win32.GWL_STYLE)); | |
| Win32.SetWindowLongSafe(hwnd, Win32.GWL_STYLE, Win32.BorderlessStyleFrom(originalStyle)); | |
| Win32.SetFullscreenRect(hwnd, Win32.GetMonitorRect(hwnd)); | |
| SessionState.SetBool(K_IsBorderless, true); | |
| } | |
| private static void ExitBorderless(IntPtr hwnd) | |
| { | |
| if (!HasStoredOriginals()) | |
| { | |
| Win32.SetWindowLongSafe(hwnd, Win32.GWL_STYLE, Win32.RestoreableStyleFrom(Win32.GetWindowLongSafe(hwnd, Win32.GWL_STYLE))); | |
| SessionState.SetBool(K_IsBorderless, false); | |
| return; | |
| } | |
| Win32.SetWindowLongSafe(hwnd, Win32.GWL_STYLE, SessionState.GetInt(K_OrigStyle, Win32.GetWindowLongSafe(hwnd, Win32.GWL_STYLE))); | |
| Win32.SetWindowLongSafe(hwnd, Win32.GWL_EXSTYLE, SessionState.GetInt(K_OrigExStyle, Win32.GetWindowLongSafe(hwnd, Win32.GWL_EXSTYLE))); | |
| var r = new Win32.RECT | |
| { | |
| left = SessionState.GetInt(K_OrigRectL, 0), | |
| top = SessionState.GetInt(K_OrigRectT, 0), | |
| right = SessionState.GetInt(K_OrigRectR, 0), | |
| bottom = SessionState.GetInt(K_OrigRectB, 0), | |
| }; | |
| Win32.SetFullscreenRect(hwnd, r); | |
| SessionState.SetBool(K_IsBorderless, false); | |
| } | |
| // ============================================================ | |
| // Win32 helpers (contained) | |
| // ============================================================ | |
| private static class Win32 | |
| { | |
| // Window styles | |
| public const int GWL_STYLE = -16; | |
| public const int GWL_EXSTYLE = -20; | |
| private const int WS_CAPTION = 0x00C00000; | |
| private const int WS_THICKFRAME = 0x00040000; | |
| private const int WS_MINIMIZE = 0x20000000; | |
| private const int WS_MAXIMIZEBOX = 0x00010000; | |
| private const int WS_SYSMENU = 0x00080000; | |
| private const uint SWP_NOSENDCHANGING = 0x0400; | |
| private const uint SWP_FRAMECHANGED = 0x0020; | |
| private const uint SWP_NOZORDER = 0x0004; | |
| private const uint SWP_NOACTIVATE = 0x0010; | |
| private const uint SWP_NOMOVE = 0x0002; | |
| private const uint SWP_NOSIZE = 0x0001; | |
| private static readonly IntPtr HWND_TOPMOST = new IntPtr(-1); | |
| private const uint MONITOR_DEFAULTTONEAREST = 2; | |
| public static bool IsUnityForegroundProcess() | |
| { | |
| IntPtr hwnd = GetForegroundWindow(); | |
| if (hwnd == IntPtr.Zero) return false; | |
| GetWindowThreadProcessId(hwnd, out uint pid); | |
| return pid == (uint)System.Diagnostics.Process.GetCurrentProcess().Id; | |
| } | |
| public static bool TryGetUnityMainWindow(out IntPtr hwnd) | |
| { | |
| hwnd = GetForegroundWindow(); | |
| if (hwnd == IntPtr.Zero) return false; | |
| GetWindowThreadProcessId(hwnd, out uint pid); | |
| if (pid != (uint)System.Diagnostics.Process.GetCurrentProcess().Id) | |
| return false; | |
| // Ensure it's a top-level window | |
| if (GetParent(hwnd) != IntPtr.Zero) | |
| return false; | |
| return true; | |
| } | |
| public static bool WindowExists(string exactTitle) | |
| => FindWindowW(null, exactTitle) != IntPtr.Zero || FindTopLevelWindowContains(exactTitle) != IntPtr.Zero; | |
| public static IntPtr FindWindowByExactOrContains(string title) | |
| { | |
| var hwnd = FindWindowW(null, title); | |
| return hwnd != IntPtr.Zero ? hwnd : FindTopLevelWindowContains(title); | |
| } | |
| public static RECT GetMonitorRect(IntPtr hwnd) | |
| { | |
| IntPtr hMon = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); | |
| var mi = new MONITORINFO { cbSize = Marshal.SizeOf<MONITORINFO>() }; | |
| GetMonitorInfo(hMon, ref mi); | |
| return mi.rcMonitor; // covers taskbar | |
| } | |
| public static void MakeBorderless(IntPtr hwnd) | |
| { | |
| int style = GetWindowLongSafe(hwnd, GWL_STYLE); | |
| SetWindowLongSafe(hwnd, GWL_STYLE, BorderlessStyleFrom(style)); | |
| } | |
| public static void SetTopmostFullscreen(IntPtr hwnd, RECT bounds) | |
| { | |
| SetWindowPos( | |
| hwnd, | |
| HWND_TOPMOST, | |
| bounds.left, | |
| bounds.top, | |
| bounds.right - bounds.left, | |
| bounds.bottom - bounds.top, | |
| SWP_NOACTIVATE | SWP_FRAMECHANGED | SWP_NOSENDCHANGING | |
| ); | |
| SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE | SWP_NOSENDCHANGING); | |
| } | |
| public static void SetFullscreenRect(IntPtr hwnd, RECT r) | |
| { | |
| SetWindowPos( | |
| hwnd, | |
| IntPtr.Zero, | |
| r.left, | |
| r.top, | |
| r.right - r.left, | |
| r.bottom - r.top, | |
| SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED | SWP_NOSENDCHANGING | |
| ); | |
| } | |
| public static int BorderlessStyleFrom(int style) | |
| { | |
| int s = style; | |
| s &= ~WS_CAPTION; | |
| s &= ~WS_THICKFRAME; | |
| s &= ~WS_SYSMENU; | |
| s &= ~WS_MINIMIZE; | |
| s &= ~WS_MAXIMIZEBOX; | |
| return s; | |
| } | |
| public static int RestoreableStyleFrom(int style) | |
| => style | (WS_CAPTION | WS_THICKFRAME | WS_SYSMENU | WS_MINIMIZE | WS_MAXIMIZEBOX); | |
| public static int GetWindowLongSafe(IntPtr hWnd, int nIndex) | |
| { | |
| IntPtr result = IntPtr.Size == 8 ? GetWindowLongPtr(hWnd, nIndex) : new IntPtr(GetWindowLong(hWnd, nIndex)); | |
| return result.ToInt32(); | |
| } | |
| public static void SetWindowLongSafe(IntPtr hWnd, int nIndex, int dwNewLong) | |
| { | |
| if (IntPtr.Size == 8) SetWindowLongPtr(hWnd, nIndex, new IntPtr(dwNewLong)); | |
| else SetWindowLong(hWnd, nIndex, dwNewLong); | |
| } | |
| private static IntPtr FindTopLevelWindowContains(string contains) | |
| { | |
| IntPtr found = IntPtr.Zero; | |
| EnumWindows((h, _) => | |
| { | |
| if (!IsWindowVisible(h)) return true; | |
| int len = GetWindowTextLengthW(h); | |
| if (len <= 0) return true; | |
| var sb = new System.Text.StringBuilder(len + 1); | |
| GetWindowTextW(h, sb, sb.Capacity); | |
| var title = sb.ToString(); | |
| if (!string.IsNullOrEmpty(title) && title.Contains(contains)) | |
| { | |
| found = h; | |
| return false; | |
| } | |
| return true; | |
| }, IntPtr.Zero); | |
| return found; | |
| } | |
| // --- Win32 P/Invoke --- | |
| public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); | |
| [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow(); | |
| [DllImport("user32.dll")] public static extern IntPtr GetParent(IntPtr hWnd); | |
| [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); | |
| [DllImport("user32.dll", SetLastError = true)] | |
| public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect); | |
| [DllImport("user32.dll", SetLastError = true)] | |
| public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags); | |
| [DllImport("user32.dll")] | |
| public static extern IntPtr MonitorFromWindow(IntPtr hwnd, uint dwFlags); | |
| [DllImport("user32.dll", CharSet = CharSet.Auto)] | |
| public static extern bool GetMonitorInfo(IntPtr hMonitor, ref MONITORINFO lpmi); | |
| [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)] | |
| public static extern IntPtr FindWindowW(string lpClassName, string lpWindowName); | |
| [DllImport("user32.dll")] public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam); | |
| [DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr hWnd); | |
| [DllImport("user32.dll", CharSet = CharSet.Unicode)] | |
| public static extern int GetWindowTextLengthW(IntPtr hWnd); | |
| [DllImport("user32.dll", CharSet = CharSet.Unicode)] | |
| public static extern int GetWindowTextW(IntPtr hWnd, System.Text.StringBuilder lpString, int nMaxCount); | |
| [DllImport("user32.dll")] | |
| public static extern short GetAsyncKeyState(int vKey); | |
| [DllImport("user32.dll", EntryPoint = "GetWindowLong")] | |
| private static extern int GetWindowLong(IntPtr hWnd, int nIndex); | |
| [DllImport("user32.dll", EntryPoint = "SetWindowLong")] | |
| private static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong); | |
| [DllImport("user32.dll", EntryPoint = "GetWindowLongPtr")] | |
| private static extern IntPtr GetWindowLongPtr(IntPtr hWnd, int nIndex); | |
| [DllImport("user32.dll", EntryPoint = "SetWindowLongPtr")] | |
| private static extern IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr dwNewLong); | |
| [StructLayout(LayoutKind.Sequential)] | |
| public struct RECT { public int left, top, right, bottom; } | |
| [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] | |
| public struct MONITORINFO | |
| { | |
| public int cbSize; | |
| public RECT rcMonitor; | |
| public RECT rcWork; | |
| public uint dwFlags; | |
| } | |
| } | |
| #endif | |
| } | |
| #endif |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment