feat: setup window using the api

This commit is contained in:
Gerard Gascón 2024-04-15 01:24:13 +02:00
parent 61c18429a2
commit 19cd8e5d03
27 changed files with 1364 additions and 10 deletions

View file

@ -0,0 +1,114 @@
using UnityEngine;
namespace SatorImaging.AppWindowUtility
{
public static class AppWindowUtility
{
public static IPlatformDependent platform;
public static bool AlwaysOnTopSupported { get => platform?.AlwaysOnTopSupported ?? false; }
public static bool AlwaysOnTop
{
get => platform?.GetAlwaysOnTop() ?? false;
set => platform?.SetAlwaysOnTop(value);
}
public static bool TransparentSupported { get => platform?.TransparentSupported ?? false; }
public static bool Transparent
{
get => platform?.GetTransparent() ?? false;
set => platform?.SetTransparent(value);
}
public static bool FrameVisiblitySupported { get => platform?.FrameVisibilitySupported ?? false; }
public static bool FrameVisibility
{
get => platform?.GetFrameVisibility() ?? true;
set => platform?.SetFrameVisibility(value);
}
public static bool ClickThroughSupported { get => platform?.ClickThroughSupported ?? false; }
public static bool ClickThrough
{
get => platform?.GetClickThrough() ?? false;
set => platform?.SetClickThrough(value);
}
public static bool AsWallpaperSupported { get => platform?.AsWallpaperSupported ?? false; }
public static bool AsWallpaper
{
get => platform?.GetAsWallpaper() ?? false;
set => platform?.SetAsWallpaper(value);
}
public static bool KeyingColorSupported { get => platform?.KeyingColorSupported ?? false; }
public static void SetKeyingColor(byte red, byte green, byte blue)
=> platform?.SetKeyingColor(red, green, blue);
public static bool WindowOpacitySupported { get => platform?.WindowOpacitySupported ?? false; }
public static void SetWindowOpacity(byte opacity)
=> platform?.SetWindowOpacity(opacity);
public static bool WindowPlacementSupported { get => platform?.WindowPlacementSupported ?? false; }
public static void MoveWindowRelative(int pixelX, int pixelY)
=> platform?.MoveWindowRelative(pixelX, pixelY);
public static void MoveWindow(int pixelX, int pixelY)
=> platform?.MoveWindow(pixelX, pixelY);
public static bool WindowResizeSupported { get => platform?.WindowResizeSupported ?? false; }
public static void ResizeWindowRelative(int pixelX, int pixelY)
=> platform?.ResizeWindowRelative(pixelX, pixelY);
// fullscreen
private static int[] lastScreenSize = new int[] { 640, 480 };
private static bool isFullScreen = (Screen.width == Screen.currentResolution.width && Screen.height == Screen.currentResolution.height);
public static bool FullScreen
{
get { return isFullScreen; }
set
{
if (isFullScreen == value) return;
if (value)
{
// unity turns window frame visible when returned from fullscreen.
// so match the status BEFORE going to full screen.
FrameVisibility = true;
lastScreenSize = new int[] { Screen.width, Screen.height };
Screen.SetResolution(Screen.currentResolution.width, Screen.currentResolution.height, true);// FullScreenMode.FullScreenWindow);
}
else
{
Screen.SetResolution(lastScreenSize[0], lastScreenSize[1], false);// FullScreenMode.Windowed);
}
isFullScreen = value;
// no way to control SetResolution update timing. it's done at next frame.
// so FrameVisibility done BEFORE full screen disabled no matter when it invoked in setter.
// to make it better, simply, reset transparent state.
if (!isFullScreen) Transparent = false;
}//set
}//
}//class
}//namespace

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1f147022b54faec44aa2bf93bc5c5737
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,43 @@
namespace SatorImaging.AppWindowUtility
{
public interface IPlatformDependent
{
bool AlwaysOnTopSupported { get; }
bool GetAlwaysOnTop();
void SetAlwaysOnTop(bool enable);
bool TransparentSupported { get; }
bool GetTransparent();
void SetTransparent(bool enable);
bool FrameVisibilitySupported { get; }
bool GetFrameVisibility();
void SetFrameVisibility(bool visible);
bool ClickThroughSupported { get; }
bool GetClickThrough();
void SetClickThrough(bool enable);
bool AsWallpaperSupported { get; }
bool GetAsWallpaper();
void SetAsWallpaper(bool enable);
bool KeyingColorSupported { get; }
void SetKeyingColor(byte red, byte green, byte blue);
bool WindowOpacitySupported { get; }
void SetWindowOpacity(byte opacity);
bool WindowPlacementSupported { get; }
void MoveWindowRelative(int pixelX, int pixelY);
void MoveWindow(int x, int y);
bool WindowResizeSupported { get; }
public void ResizeWindowRelative(int relativeWidth, int relativeHeight);
}//
}//namespace

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c91b36bb61130ec4aa52b609135aa56e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,24 @@
using UnityEngine;
namespace SatorImaging.AppWindowUtility
{
static class InitializeOnLoad
{
#if UNITY_EDITOR
//[UnityEditor.InitializeOnLoadMethod]
#endif
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterAssembliesLoaded)]
static void Install()
{
#if UNITY_STANDALONE_WIN
AppWindowUtility.platform = new Windows();
#endif
}
}//class
}//namespace

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 127a7c2393583d743bd2bb10e100a3cf
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,66 @@
using UnityEngine;
using UnityEngine.EventSystems;
namespace SatorImaging.AppWindowUtility
{
public class WindowGrabber : MonoBehaviour
{
public enum MouseButton
{
Left = 0,
Right = 1,
Middle = 2,
}
public MouseButton mouseButton;
public KeyCode modifierKey = KeyCode.None;
public KeyCode[] temporarilyDisableIfKeyPressed;
private bool isDragging = false;
Vector2 targetPosition = Vector2.zero;
void Update()
{
#if UNITY_EDITOR
if(isDragging.Equals(isDragging)) return; // to avoid CS0162 warning
#endif
// do nothing if any uGUI is in use.
if (EventSystem.current?.currentSelectedGameObject) return;
// initialize dragging state. don't check modifier key.
if (Input.GetMouseButtonUp((int)mouseButton)) isDragging = false;
// key check.
foreach (var k in temporarilyDisableIfKeyPressed) if (Input.GetKey(k)) return;
if (modifierKey != KeyCode.None && !Input.GetKey(modifierKey)) return;
if (Input.GetMouseButtonDown((int)mouseButton))
{
targetPosition = Event.current.mousePosition;
isDragging = true;
}
if (isDragging && Input.GetMouseButton((int)mouseButton))
{
// do NOT use Event.current.delta. it's sampled in local window coordinate.
// and moving window while mouse dragging changes coordinate sample by sample.
// just remove the gap between current mouse position and drag starting position.
AppWindowUtility.MoveWindowRelative(
(int)(Event.current.mousePosition.x - targetPosition.x),
(int)(Event.current.mousePosition.y - targetPosition.y)
);
}
}//
}//class
}//namespace

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a1866eebb0b62a44da3196d0f2fb1b0c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: cfd42f4c85ef7c248b021f4f37c70a4c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,201 @@
#if UNITY_STANDALONE_WIN
using System;
using System.Runtime.InteropServices;
using System.Text;
using UnityEngine;
namespace SatorImaging.AppWindowUtility
{
public static class WinApi
{
[StructLayout(LayoutKind.Sequential)]
public struct DwmMargin
{
public int cxLeftWidth;
public int cxRightWidth;
public int cyTopHeight;
public int cyBottomHeight;
}
[StructLayout(LayoutKind.Sequential)]
public struct POINT
{
public int X;
public int Y;
}
[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
public int left;
public int top;
public int right;
public int bottom;
}
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GetCursorPos(out POINT lpPoint);
public static Vector2 GetWindowsMousePosition()
{
POINT pos;
if (GetCursorPos(out pos)) return new Vector2(pos.X, pos.Y);
return Vector2.zero;
}
[DllImport("user32.dll")]
public static extern IntPtr GetActiveWindow();
[DllImport("user32.dll")]
public static extern IntPtr GetDesktopWindow();
[DllImport("user32.dll")]
public static extern IntPtr GetWindow(IntPtr hWnd, uint uCmd);
[DllImport("user32.dll")]
public static extern uint SendMessageTimeout(IntPtr hWnd, uint Msg, uint wParam, uint lParam, uint fuFlags, uint uTimeout, out uint lpdwResult);
[DllImport("user32.dll")]
public static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent);
[DllImport("user32.dll")]
public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
[DllImport("user32.dll")]
public static extern IntPtr FindWindowEx(IntPtr hWndParent, IntPtr hWndChild, string lpszClass, string lpszWindow);
[DllImport("user32.dll")]
public static extern IntPtr FindWindow(string className, string windowName);
public static IntPtr CurrentWindowHandle = IntPtr.Zero;
public static IntPtr GetUnityWindowHandle()
{
if (CurrentWindowHandle == IntPtr.Zero)
{
CurrentWindowHandle = FindWindow(null, Application.productName);
}
return CurrentWindowHandle;
}
[DllImport("user32.dll")]
public static extern IntPtr GetForegroundWindow();
public static bool IsWindowActive()
{
return GetUnityWindowHandle() == GetForegroundWindow();
}
[DllImport("user32.dll")]
public static extern int SetWindowLong(IntPtr hWnd, int nIndex, uint dwNewLong); /*x uint o int unchecked*/
[DllImport("user32.dll")]
public static extern uint GetWindowLong(IntPtr hWnd, int nIndex);
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
public static extern bool SetWindowText(IntPtr hwnd, String lpString);
[DllImport("user32.dll", SetLastError = true)]
public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, SetWindowPosFlags uFlags);
[DllImport("user32.dll")]
public static extern bool GetWindowRect(IntPtr hWnd, out RECT rect);
[DllImport("user32.dll")]
public static extern bool GetClientRect(IntPtr hWnd, out RECT rect);
public static readonly IntPtr HWND_TOPMOST = new IntPtr(-1);
public static readonly IntPtr HWND_NOTOPMOST = new IntPtr(-2);
public static readonly IntPtr HWND_TOP = new IntPtr(0);
public static readonly IntPtr HWND_BOTTOM = new IntPtr(1);
[Flags()]
public enum SetWindowPosFlags : uint
{
AsynchronousWindowPosition = 0x4000,
DeferErase = 0x2000,
DrawFrame = 0x0020,
FrameChanged = 0x0020,
HideWindow = 0x0080,
DoNotActivate = 0x0010,
DoNotCopyBits = 0x0100,
IgnoreMove = 0x0002,
DoNotChangeOwnerZOrder = 0x0200,
DoNotRedraw = 0x0008,
DoNotReposition = 0x0200,
DoNotSendChangingEvent = 0x0400,
IgnoreResize = 0x0001,
IgnoreZOrder = 0x0004,
ShowWindow = 0x0040,
NoFlag = 0x0000,
}
[DllImport("Dwmapi.dll")]
public static extern uint DwmExtendFrameIntoClientArea(IntPtr hWnd, ref DwmMargin margins);
public static void SetDwmTransparent(bool enable)
{
var margins = new DwmMargin()
{
cxLeftWidth = enable ? -1 : 0,
};
DwmExtendFrameIntoClientArea(GetUnityWindowHandle(), ref margins);
}
public const int GWL_STYLE = -16;
public const uint WS_POPUP = 0x80000000;
public const uint WS_BORDER = 0x00800000;
public const uint WS_VISIBLE = 0x10000000;
public const uint WS_CAPTION = 0x00C00000;
public const uint WS_THICKFRAME = 0x00040000;
public const int GWL_EXSTYLE = -20;
public const uint WS_EX_LAYERED = 0x00080000;
public const uint WS_EX_TRANSPARENT = 0x00000020;
public const uint WS_EX_DLGMODALFRAME = 0x00000001;
public const uint WS_EX_WINDOWEDGE = 0x00000100;
public const uint WS_EX_CLIENTEDGE = 0x00000200;
public const uint WS_EX_STATICEDGE = 0x00020000;
public const int LWA_COLORKEY = 0x00000001;
public const int LWA_ALPHA = 0x00000002;
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern int GetWindowTextLength(IntPtr hWnd);
[DllImport("user32.dll", SetLastError = true)]
public static extern bool SetLayeredWindowAttributes(IntPtr hwnd, uint crKey, byte bAlpha, uint dwFlags);
[DllImport("user32.dll", SetLastError = true)]
public static extern bool GetLayeredWindowAttributes(IntPtr hwnd, out uint crKey, out byte bAlpha, out uint dwFlags);
}//class
}//namespace
#endif

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d8438e9a2634454429f47962402d6be3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,298 @@
#if UNITY_STANDALONE_WIN
using System;
using UnityEngine;
namespace SatorImaging.AppWindowUtility
{
public class Windows : IPlatformDependent
{
static IntPtr hWnd;
static uint defaultWindowStyle;
static uint defaultExWindowStyle;
// unity's shader error color.
static uint defaultKeyingColor = 0xFF00FF;
static int borderWidth;
static int titleBarHeight;
private static bool isAlwaysOnTop = false;
private static bool isTransparent = false;
private static bool isFrameVisible = true;
private static bool isClickThrough = false;
private static bool isWallpaper = false;
private static bool isInitialized = false;
private static WinApi.RECT lastClientRect = new WinApi.RECT { left = 32, top = 64, right = 640, bottom = 480 };
// features supported
public bool AlwaysOnTopSupported { get; } = true;
public bool TransparentSupported { get; } = true;
public bool FrameVisibilitySupported { get; } = true;
public bool ClickThroughSupported { get; } = true;
public bool AsWallpaperSupported { get; } = true;
public bool KeyingColorSupported { get; } = true;
public bool WindowOpacitySupported { get; } = true;
public bool WindowPlacementSupported { get; } = true;
public bool WindowResizeSupported { get; } = true;
public Windows()
{
if (isInitialized) return;
hWnd = WinApi.GetUnityWindowHandle();
defaultWindowStyle = WinApi.GetWindowLong(WinApi.GetUnityWindowHandle(), WinApi.GWL_STYLE);
defaultExWindowStyle = WinApi.GetWindowLong(WinApi.GetUnityWindowHandle(), WinApi.GWL_EXSTYLE);
// initialize client rect.
WinApi.GetClientRect(hWnd, out lastClientRect);
// calculate title bar and frame size
WinApi.RECT windowRect;
WinApi.GetWindowRect(hWnd, out windowRect);
WinApi.RECT clientRect;
WinApi.GetClientRect(hWnd, out clientRect);
borderWidth = (windowRect.right - windowRect.left) - clientRect.right;
borderWidth = (int)(borderWidth * 0.5f);
titleBarHeight = (windowRect.bottom - windowRect.top) - clientRect.bottom - borderWidth;
//Debug.Log($"{typeof(AppWindowUtility).FullName}.Initialize: Title Bar {titleBarHeight}px / Border {borderWidth}px");
isInitialized = true;
}//
public void ResetStyle()
{
WinApi.SetWindowLong(hWnd, WinApi.GWL_STYLE, defaultWindowStyle);
WinApi.SetWindowLong(hWnd, WinApi.GWL_EXSTYLE, defaultExWindowStyle);
}//
public bool GetAlwaysOnTop() => isAlwaysOnTop;
public void SetAlwaysOnTop(bool enable)
{
WinApi.SetWindowPos(
hWnd,
enable ? WinApi.HWND_TOPMOST : WinApi.HWND_NOTOPMOST,
0, 0, 0, 0,
WinApi.SetWindowPosFlags.IgnoreMove | WinApi.SetWindowPosFlags.IgnoreResize
);
isAlwaysOnTop = enable;
}//
public bool GetTransparent() => isTransparent;
public void SetTransparent(bool enable)
{
if (enable)
{
SetFrameVisibility(false);
var currExStyle = WinApi.GetWindowLong(hWnd, WinApi.GWL_EXSTYLE);
WinApi.SetWindowLong(hWnd, WinApi.GWL_EXSTYLE, currExStyle & ~WinApi.WS_EX_LAYERED);
WinApi.SetDwmTransparent(true);
}
else
{
SetFrameVisibility(true);
WinApi.SetDwmTransparent(false);
}
isTransparent = enable;
}//
public void SetKeyingColor(byte red, byte green, byte blue)
{
var currExStyle = WinApi.GetWindowLong(hWnd, WinApi.GWL_EXSTYLE);
WinApi.SetWindowLong(hWnd, WinApi.GWL_EXSTYLE, currExStyle | WinApi.WS_EX_LAYERED);// | WindowsApi.WS_EX_TRANSPARENT);
var color = (uint)(blue + (green << 8) + (red << 16));
WinApi.SetLayeredWindowAttributes(hWnd, color, 0xFF, WinApi.LWA_COLORKEY);
}//
public bool GetFrameVisibility() => isFrameVisible;
public void SetFrameVisibility(bool visible)
{
if (AppWindowUtility.FullScreen) return;
if (visible == isFrameVisible) return;
if (visible)
{
// must be done BEFORE SetWindowLong
MoveWindowRelative(-borderWidth, -titleBarHeight);
//ResizeWindowRelative(borderWidth * 2, titleBarHeight + borderWidth);
WinApi.SetWindowLong(hWnd, WinApi.GWL_STYLE, defaultWindowStyle);
// to make uGUI correct.
WinApi.SetWindowPos(hWnd, IntPtr.Zero,
lastClientRect.left - borderWidth,
lastClientRect.top - titleBarHeight,
lastClientRect.right - lastClientRect.left + borderWidth * 2,
lastClientRect.bottom - lastClientRect.top + titleBarHeight + borderWidth,
WinApi.SetWindowPosFlags.FrameChanged | WinApi.SetWindowPosFlags.IgnoreMove
);
}
else
{
// store last client size for showing frame again
if (!AppWindowUtility.FullScreen)
{
WinApi.GetClientRect(hWnd, out lastClientRect);
//Debug.Log($"SetFrameVisibility: Client Rect stored.");
}
var currStyle = WinApi.GetWindowLong(hWnd, WinApi.GWL_STYLE);
WinApi.SetWindowLong(hWnd, WinApi.GWL_STYLE, currStyle & ~WinApi.WS_BORDER & ~WinApi.WS_THICKFRAME & ~WinApi.WS_CAPTION);
//// must be done AFTER SetWindowLong
MoveWindowRelative(borderWidth, titleBarHeight);
// must be change window size 2 times to work correctly, idk why.
WinApi.SetWindowPos(hWnd, IntPtr.Zero,
lastClientRect.left - borderWidth,
lastClientRect.top - titleBarHeight,
lastClientRect.right,
lastClientRect.bottom + 1,
WinApi.SetWindowPosFlags.FrameChanged | WinApi.SetWindowPosFlags.IgnoreMove
);
WinApi.SetWindowPos(hWnd, IntPtr.Zero,
lastClientRect.left - borderWidth,
lastClientRect.top - titleBarHeight,
lastClientRect.right,
lastClientRect.bottom - 1,
WinApi.SetWindowPosFlags.FrameChanged | WinApi.SetWindowPosFlags.IgnoreMove
);
}
isFrameVisible = visible;
}//
public bool GetClickThrough() => isClickThrough;
public void SetClickThrough(bool enable)
{
if (enable)
{
var currExStyle = WinApi.GetWindowLong(hWnd, WinApi.GWL_EXSTYLE);
WinApi.SetWindowLong(hWnd, WinApi.GWL_EXSTYLE, currExStyle | WinApi.WS_EX_LAYERED | WinApi.WS_EX_TRANSPARENT);
}
else
{
WinApi.SetWindowLong(hWnd, WinApi.GWL_EXSTYLE, defaultExWindowStyle);
}
isClickThrough = enable;
}//
public void SetWindowOpacity(byte opacity)
{
////////if(0xFF == opacity)
{
var currExStyle = WinApi.GetWindowLong(hWnd, WinApi.GWL_EXSTYLE);
WinApi.SetWindowLong(hWnd, WinApi.GWL_EXSTYLE, currExStyle | WinApi.WS_EX_LAYERED);
WinApi.SetLayeredWindowAttributes(hWnd, defaultKeyingColor, opacity, WinApi.LWA_ALPHA);
}
/* do not reset for combination with Transparent
else
{
ResetStyle();
}
*/
}//
public bool GetAsWallpaper() => isWallpaper;
public void SetAsWallpaper(bool enable)
{
}//
public void MoveWindowRelative(int relativeX, int relativeY)
{
if (AppWindowUtility.FullScreen) return;
WinApi.RECT rect;
WinApi.GetWindowRect(hWnd, out rect);
WinApi.SetWindowPos(hWnd, IntPtr.Zero,
rect.left + relativeX,
rect.top + relativeY,
rect.right - rect.left,
rect.bottom - rect.top,
WinApi.SetWindowPosFlags.NoFlag //ApiWindows.SetWindowPosFlags.IgnoreResize
);
}//
public void MoveWindow(int x, int y)
{
if (AppWindowUtility.FullScreen) return;
WinApi.RECT rect;
WinApi.GetWindowRect(hWnd, out rect);
WinApi.SetWindowPos(hWnd, IntPtr.Zero,
x,
y,
rect.right - rect.left,
rect.bottom - rect.top,
WinApi.SetWindowPosFlags.NoFlag //ApiWindows.SetWindowPosFlags.IgnoreResize
);
}//
public void ResizeWindowRelative(int relativeWidth, int relativeHeight)
{
if (AppWindowUtility.FullScreen) return;
WinApi.RECT rect;
WinApi.GetWindowRect(hWnd, out rect);
WinApi.SetWindowPos(hWnd, IntPtr.Zero,
rect.left,
rect.top,
relativeWidth,
relativeHeight,
WinApi.SetWindowPosFlags.NoFlag //ApiWindows.SetWindowPosFlags.IgnoreMove
);
}//
}//class
}//namespace
#endif

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 00a74ea748d2df149864a0f9d4455e24
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: