DESKTOP-SAJ6RKV\Administrator 96213cba61 1
2025-06-11 18:01:35 +08:00

507 lines
20 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.

using System;
using System.Collections;
using System.Runtime.InteropServices;
using System.Text;
using TEngine;
using UnityEngine;
using UnityEngine.Events;
namespace DanMuHelper.Tools
{
/// <summary>
///强制设置Unity游戏窗口的长宽比。你可以调整窗口的大小他会强制保持一定比例
///通过拦截窗口大小调整事件(WindowProc回调)并相应地修改它们来实现的
///也可以用像素为窗口设置最小/最大宽度和高度
///长宽比和最小/最大分辨率都与窗口区域有关,标题栏和边框不包括在内
///该脚本还将在应用程序处于全屏状态时强制设置长宽比。当你切换到全屏,
///应用程序将自动设置为当前显示器上可能的最大分辨率,而仍然保持固定比。如果显示器没有相同的宽高比,则会在左/右或上/下添加黑条
///确保你在PlayerSetting中设置了“Resizable Window”否则无法调整大小
///如果取消不支持的长宽比在PlayerSetting中设置“Supported Aspect Rations”
///注意:因为使用了WinAPI所以只能在Windows上工作。在Windows 10上测试过
/// </summary>
[DisallowMultipleComponent]
public class AspectRatioController : MonoSingleton<AspectRatioController>
{
// /// <summary>
// /// 每当窗口分辨率改变或用户切换全屏时,都会触发此事件
// /// 参数是新的宽度、高度和全屏状态(true表示全屏)
// /// </summary>
public UnityEvent resolutionChangedEvent = new UnityEvent();
static int tempCount = 1;
static int lastTempCount = 1;
// 如果为false则阻止切换到全屏
[SerializeField]
private bool allowFullscreen = false;
// 长宽比的宽度和高度
[SerializeField]
private float aspectRatioWidth = 9;
[SerializeField]
private float aspectRatioHeight = 16;
// 最小值和最大值的窗口宽度/高度像素
[SerializeField]
private int minWidthPixel = (int)(900.0f * (9.0f / 16.0f));
[SerializeField]
private int minHeightPixel = 900;
[SerializeField]
private int maxWidthPixel = (int)(4000.0f * (9.0f / 16.0f));
[SerializeField]
private int maxHeightPixel = 4000;
private static int MinWidthPixel = 512;
private static int MinHeightPixel = 512;
private static int MaxWidthPixel = 2048;
private static int MaxHeightPixel = 2048;
// 当前锁定长宽比。
private static float aspect;
// 窗口的宽度和高度。不包括边框和窗口标题栏
// 当调整窗口大小时,就会设置这些值
private static int setWidth = -1;
private static int setHeight = -1;
// 最后一帧全屏状态。
private bool wasFullscreenLastFrame;
// 是否初始化了AspectRatioController
// 一旦注册了WindowProc回调函数就将其设置为true
private bool started;
// 显示器的宽度和高度。这是窗口当前打开的监视器
private int pixelHeightOfCurrentScreen;
private int pixelWidthOfCurrentScreen;
//一旦用户请求终止applaction则将其设置为true
private bool quitStarted;
// WinAPI相关定义
#region WINAPI
// 当窗口调整时,WM_SIZING消息通过WindowProc回调发送到窗口
private const int WM_SIZING = 0x214;
// WM大小调整消息的参数
private const int WMSZ_LEFT = 1;
private const int WMSZ_RIGHT = 2;
private const int WMSZ_TOP = 3;
private const int WMSZ_BOTTOM = 6;
// 获取指向WindowProc函数的指针
private const int GWLP_WNDPROC = -4;
// 委托设置为新的WindowProc回调函数
private delegate IntPtr WndProcDelegate(
IntPtr hWnd,
uint msg,
IntPtr wParam,
IntPtr lParam
);
//静态委托实例
private static WndProcDelegate wndProcDelegate = wndProc;
//如果用户不会乱点的话也可以用这个方法取代EnumThreadWindows
[DllImport("user32.dll")]
private static extern IntPtr GetActiveWindow();
// 检索调用线程的线程标识符
[DllImport("kernel32.dll")]
private static extern uint GetCurrentThreadId();
// 检索指定窗口所属类的名称
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern int GetClassName(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
// 通过将句柄传递给每个窗口,依次传递给应用程序定义的回调函数,枚举与线程关联的所有非子窗口
[DllImport("user32.dll")]
private static extern bool EnumThreadWindows(
uint dwThreadId,
EnumWindowsProc lpEnumFunc,
IntPtr lParam
);
private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
//静态委托实例
private static EnumWindowsProc enumProcDelegate = enumProc;
// 将消息信息传递给指定的窗口过程
[DllImport("user32.dll")]
private static extern IntPtr CallWindowProc(
IntPtr lpPrevWndFunc,
IntPtr hWnd,
uint Msg,
IntPtr wParam,
IntPtr lParam
);
// 检索指定窗口的边框的尺寸
// 尺寸是在屏幕坐标中给出的,它是相对于屏幕左上角的
[DllImport("user32.dll", SetLastError = true)]
private static extern bool GetWindowRect(IntPtr hwnd, ref RECT lpRect);
//检索窗口客户区域的坐标。客户端坐标指定左上角
//以及客户区的右下角。因为客户机坐标是相对于左上角的
//在窗口的客户区域的角落,左上角的坐标是(0,0)
[DllImport("user32.dll")]
private static extern bool GetClientRect(IntPtr hWnd, ref RECT lpRect);
// 更改指定窗口的属性。该函数还将指定偏移量的32位(长)值设置到额外的窗口内存中
[DllImport("user32.dll", EntryPoint = "SetWindowLong", CharSet = CharSet.Auto)]
private static extern IntPtr SetWindowLong32(IntPtr hWnd, int nIndex, IntPtr dwNewLong);
//更改指定窗口的属性。该函数还在额外的窗口内存中指定的偏移量处设置一个值
[DllImport("user32.dll", EntryPoint = "SetWindowLongPtr", CharSet = CharSet.Auto)]
private static extern IntPtr SetWindowLongPtr64(IntPtr hWnd, int nIndex, IntPtr dwNewLong);
//用于查找窗口句柄的Unity窗口类的名称
private const string UNITY_WND_CLASSNAME = "UnityWndClass";
// Unity窗口的窗口句柄
private static IntPtr unityHWnd;
// 指向旧WindowProc回调函数的指针
private static IntPtr oldWndProcPtr;
// 指向我们自己的窗口回调函数的指针
private IntPtr newWndProcPtr;
/// <summary>
/// WinAPI矩形定义。
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
public int Left;
public int Top;
public int Right;
public int Bottom;
}
#endregion
void Start()
{
GameObject.DontDestroyOnLoad(this);
//设为最小最大高宽度设为静态变量
setMinAndMaxPixel();
// 不要在Unity编辑器中注册WindowProc回调函数它会指向Unity编辑器窗口而不是Game视图
#if UNITY_STANDALONE_WIN&&!UNITY_EDITOR
//注册回调,然后应用程序想要退出
Application.wantsToQuit += ApplicationWantsToQuit;
// 找到主Unity窗口的窗口句柄
EnumThreadWindows(GetCurrentThreadId(), enumProcDelegate, IntPtr.Zero);
// 将长宽比应用于当前分辨率
SetAspectRatio(aspectRatioWidth, aspectRatioHeight, true);
// 保存当前的全屏状态
wasFullscreenLastFrame = Screen.fullScreen;
// Register (replace) WindowProc callback。每当一个窗口事件被触发时这个函数都会被调用
//例如调整大小或移动窗口
//保存旧的WindowProc回调函数因为必须从新回调函数中调用它
newWndProcPtr = Marshal.GetFunctionPointerForDelegate(wndProcDelegate);
oldWndProcPtr = SetWindowLong(unityHWnd, GWLP_WNDPROC, newWndProcPtr);
// 初始化完成
started = true;
resolutionChangedEvent?.Invoke();
#else
// Disable for good
this.enabled = false;
#endif
}
//由于变成静态方法,所以在最开始静态变量加载非静态变量的值
void setMinAndMaxPixel()
{
MinWidthPixel = minWidthPixel;
MaxWidthPixel = maxWidthPixel;
MinHeightPixel = minHeightPixel;
MaxHeightPixel = maxHeightPixel;
}
private static void resolutionChangedFunc()
{
tempCount++;
}
[AOT.MonoPInvokeCallback(typeof(EnumWindowsProc))]
private static bool enumProc(IntPtr hWnd, IntPtr lParam)
{
var classText = new StringBuilder(UNITY_WND_CLASSNAME.Length + 1);
GetClassName(hWnd, classText, classText.Capacity);
if (classText.ToString() == UNITY_WND_CLASSNAME)
{
unityHWnd = hWnd;
return false;
}
return true;
}
/// <summary>
///将目标长宽比设置为给定的长宽比。
/// </summary>
/// <param name="newAspectWidth">宽高比的新宽度</param>
/// <param name="newAspectHeight">纵横比的新高度</param>
/// <param name="apply">true当前窗口分辨率将立即调整以匹配新的纵横比 false则只在下次手动调整窗口大小时执行此操作</param>
public void SetAspectRatio(float newAspectWidth, float newAspectHeight, bool apply)
{
//计算新的纵横比
aspectRatioWidth = newAspectWidth;
aspectRatioHeight = newAspectHeight;
aspect = aspectRatioWidth / aspectRatioHeight;
// 调整分辨率以匹配长宽比(触发WindowProc回调)
if (apply)
{
Screen.SetResolution(
Screen.width,
Mathf.RoundToInt(Screen.width / aspect),
Screen.fullScreen
);
}
}
/// <summary>
/// WindowProc回调。应用程序定义的函数用来处理发送到窗口的消息
/// </summary>
/// <param name="msg">用于标识事件的消息</param>
/// <param name="wParam">额外的信息信息。该参数的内容取决于uMsg参数的值 </param>
/// <param name="lParam">其他消息的信息。该参数的内容取决于uMsg参数的值 </param>
/// <returns></returns>
[AOT.MonoPInvokeCallback(typeof(WndProcDelegate))]
private static IntPtr wndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam)
{
// 检查消息类型
// resize事件
if (msg == WM_SIZING)
{
// 获取窗口大小结构体
RECT rc = (RECT)Marshal.PtrToStructure(lParam, typeof(RECT));
// 计算窗口边框的宽度和高度
RECT windowRect = new RECT();
GetWindowRect(unityHWnd, ref windowRect);
RECT clientRect = new RECT();
GetClientRect(unityHWnd, ref clientRect);
int borderWidth =
windowRect.Right - windowRect.Left - (clientRect.Right - clientRect.Left);
int borderHeight =
windowRect.Bottom - windowRect.Top - (clientRect.Bottom - clientRect.Top);
// 在应用宽高比之前删除边框(包括窗口标题栏)
rc.Right -= borderWidth;
rc.Bottom -= borderHeight;
// 限制窗口大小
int newWidth = Mathf.Clamp(rc.Right - rc.Left, MinWidthPixel, MaxWidthPixel);
int newHeight = Mathf.Clamp(rc.Bottom - rc.Top, MinHeightPixel, MaxHeightPixel);
// 根据纵横比和方向调整大小
switch (wParam.ToInt32())
{
case WMSZ_LEFT:
rc.Left = rc.Right - newWidth;
rc.Bottom = rc.Top + Mathf.RoundToInt(newWidth / aspect);
break;
case WMSZ_RIGHT:
rc.Right = rc.Left + newWidth;
rc.Bottom = rc.Top + Mathf.RoundToInt(newWidth / aspect);
break;
case WMSZ_TOP:
rc.Top = rc.Bottom - newHeight;
rc.Right = rc.Left + Mathf.RoundToInt(newHeight * aspect);
break;
case WMSZ_BOTTOM:
rc.Bottom = rc.Top + newHeight;
rc.Right = rc.Left + Mathf.RoundToInt(newHeight * aspect);
break;
case WMSZ_RIGHT + WMSZ_BOTTOM:
rc.Right = rc.Left + newWidth;
rc.Bottom = rc.Top + Mathf.RoundToInt(newWidth / aspect);
break;
case WMSZ_RIGHT + WMSZ_TOP:
rc.Right = rc.Left + newWidth;
rc.Top = rc.Bottom - Mathf.RoundToInt(newWidth / aspect);
break;
case WMSZ_LEFT + WMSZ_BOTTOM:
rc.Left = rc.Right - newWidth;
rc.Bottom = rc.Top + Mathf.RoundToInt(newWidth / aspect);
break;
case WMSZ_LEFT + WMSZ_TOP:
rc.Left = rc.Right - newWidth;
rc.Top = rc.Bottom - Mathf.RoundToInt(newWidth / aspect);
break;
}
// 保存实际分辨率,不包括边界
setWidth = rc.Right - rc.Left;
setHeight = rc.Bottom - rc.Top;
// 添加边界
rc.Right += borderWidth;
rc.Bottom += borderHeight;
// 触发分辨率更改事件
resolutionChangedFunc();
// 回写更改的窗口参数
Marshal.StructureToPtr(rc, lParam, true);
}
// 调用原始的WindowProc函数
return CallWindowProc(oldWndProcPtr, hWnd, msg, wParam, lParam);
}
void Update()
{
// 如果不允许全屏,则阻止切换到全屏
if (!allowFullscreen && Screen.fullScreen)
{
Screen.fullScreen = false;
}
if (Screen.fullScreen && !wasFullscreenLastFrame)
{
//切换到全屏检测,设置为最大屏幕分辨率,同时保持长宽比
int height;
int width;
//根据当前长宽比和显示器的比例进行比较,上下或左右添加黑边
bool blackBarsLeftRight =
aspect < (float)pixelWidthOfCurrentScreen / pixelHeightOfCurrentScreen;
if (blackBarsLeftRight)
{
height = pixelHeightOfCurrentScreen;
width = Mathf.RoundToInt(pixelHeightOfCurrentScreen * aspect);
}
else
{
width = pixelWidthOfCurrentScreen;
height = Mathf.RoundToInt(pixelWidthOfCurrentScreen / aspect);
}
Log.Debug("宽高比 {0}{1}", width, height);
Screen.SetResolution(width, height, true);
resolutionChangedFunc();
}
else if (!Screen.fullScreen && wasFullscreenLastFrame)
{
Log.Debug("宽高比 {0}{1}", setWidth, setHeight);
// 从全屏切换到检测到的窗口。设置上一个窗口的分辨率。
Screen.SetResolution(setWidth, setHeight, false);
resolutionChangedFunc();
}
else if (
!Screen.fullScreen
&& setWidth != -1
&& setHeight != -1
&& (Screen.width != setWidth || Screen.height != setHeight)
)
{
//根据高度设置宽度因为Aero Snap不会触发WM_SIZING。
setHeight = Screen.height;
setWidth = Mathf.RoundToInt(Screen.height * aspect);
Log.Debug("宽高比 {0}{1}", setWidth, setHeight);
Screen.SetResolution(setWidth, setHeight, Screen.fullScreen);
resolutionChangedFunc();
}
else if (!Screen.fullScreen)
{
// 保存当前屏幕的分辨率
// 下次切换到全屏时,此分辨率将被设置为窗口分辨率
// 只有高度,如果需要,宽度将根据高度和长宽比设置,以确保长宽比保持在全屏模式
pixelHeightOfCurrentScreen = Screen.currentResolution.height;
pixelWidthOfCurrentScreen = Screen.currentResolution.width;
}
//保存下一帧的全屏状态
wasFullscreenLastFrame = Screen.fullScreen;
// 当游戏窗口调整大小时,在编辑器中触发分辨率改变事件。
#if UNITY_EDITOR
if (Screen.width != setWidth || Screen.height != setHeight)
{
setWidth = Screen.width;
setHeight = Screen.height;
Log.Debug("宽高比 {0}{1}", setWidth, setHeight);
resolutionChangedFunc();
}
#endif
if (lastTempCount != tempCount)
{
resolutionChangedEvent.Invoke();
lastTempCount = tempCount;
}
}
/// <summary>
/// 调用SetWindowLong32或SetWindowLongPtr64取决于可执行文件是32位还是64位。
/// 这样我们就可以同时构建32位和64位的可执行文件而不会遇到问题。
/// </summary>
/// <param name="hWnd">The window handle.</param>
/// <param name="nIndex">要设置的值的从零开始的偏移量</param>
/// <param name="dwNewLong">The replacement value.</param>
/// <returns>返回值是指定偏移量的前一个值。否则零.</returns>
private static IntPtr SetWindowLong(IntPtr hWnd, int nIndex, IntPtr dwNewLong)
{
//32位系统
if (IntPtr.Size == 4)
{
return SetWindowLong32(hWnd, nIndex, dwNewLong);
}
return SetWindowLongPtr64(hWnd, nIndex, dwNewLong);
}
/// <summary>
/// 退出时调用。 返回false将中止并使应用程序保持活动。True会让它退出。
/// </summary>
/// <returns></returns>
private bool ApplicationWantsToQuit()
{
//仅允许在应用程序初始化后退出。
if (!started)
return false;
//延迟退出clear up
if (!quitStarted)
{
StartCoroutine("DelayedQuit");
return false;
}
return true;
}
/// <summary>
/// 恢复旧的WindowProc回调然后退出。
/// </summary>
IEnumerator DelayedQuit()
{
// 重新设置旧的WindowProc回调,如果检测到WM_CLOSE,这将在新的回调本身中完成, 64位没问题32位可能会造成闪退
SetWindowLong(unityHWnd, GWLP_WNDPROC, oldWndProcPtr);
yield return new WaitForEndOfFrame();
quitStarted = true;
Application.Quit();
}
}
}