Unity3D之Windows端隐藏任务栏图标并添加至托盘

发布时间:2025-12-10 11:29:31 浏览次数:2

目录

  • 1 基本效果
  • 2 代码实现
    • 2.1 思路
    • 2.2 实现
      • 2.2.1 Unity程序监听最小化和关闭事件
      • 2.2.2 方便打包的菜单栏
      • 2.2.3 IL2CPP启动外部程序
      • 2.2.4 winform程序的托盘图标
      • 2.2.5 winform程序单例运行
  • 3 完整项目
  • 4 参考文章

更好的实现方式,见这里
下面这种方式可以废弃了。


1 基本效果

基本功能

  • 点击关闭,不直接关闭,缩小到托盘
  • 托盘图标上可打开、隐藏和关闭程序

效果展示:

2 代码实现

2.1 思路

  • 两个程序,Unity一个程序,winform一个程序
  • winform程序用来生成托盘图标,并且控制Unity程序的最大、最小化及关闭
  • Unity程序需要监听到鼠标点击标题栏右上角最小化和关闭事件
  • winform程序需要单例运行(同一时间只允许一个程序运行)
  • Unity程序启动时,同时启动winform程序

2.2 实现

2.2.1 Unity程序监听最小化和关闭事件

通过监听windows系统的api来实现的,就废话少说了,具体代码如下。
用到的Win32 Api引入。
这里需要注意一下的是,引入FindWindow这个方法时,最好把 CharSet设置为Unicode。如果Untiy打包的程序名是中文,又没设置CharSet为Unicode,调用此函数很可能查找不到窗体。我之前就遇到死活找不到窗体,坑惨了。

[DllImport("user32.dll", CharSet = CharSet.Unicode)]public static extern IntPtr FindWindow(string lpszClass, string lpszTitle);

WinUser32.cs

/***┌──────────────────────────────────────────────────────────────┐*│ 描 述: *│ 作 者:wangying *│ 创建时间:2021/2/28 10:33:02 *│ 作者blog: http://www.laowangomg.com *└──────────────────────────────────────────────────────────────┘*/using System;using System.Runtime.InteropServices;namespace UnityWin{public delegate IntPtr WndProcDelegate(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);class WinUser32{// Ref:// https://docs.microsoft.com/zh-cn/windows/win32/winmsg/about-windows#desktop-window// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-showwindowpublic const int SW_HIDE = 0;public const int SW_MAXIMIZE = 3;public const int SW_SHOW = 0;public const int SW_MINIMIZE = 6;public const int SW_RESTORE = 9;// https://docs.microsoft.com/zh-cn/windows/win32/api/winuser/nf-winuser-showwindow?redirectedfrom=MSDNpublic const int WM_SYSCOMMAND = 0x0112;public const int SC_CLOSE = 0xF060;public const int SC_MAXIMIZE = 0xF030;public const int SC_MINIMIZE = 0xF020;public const int GWL_EXSTYLE = -0x14;public const int WS_EX_TOOLWINDOW = 0x0080;public const int WS_EX_APPWINDOW = 0x00040000;[DllImport("user32.dll")]public static extern bool ShowWindow(IntPtr hwnd, int nCmdShow);[DllImport("user32.dll", CharSet = CharSet.Unicode)]public static extern IntPtr FindWindow(string lpszClass, string lpszTitle);[DllImport("user32.dll")]public static extern IntPtr GetActiveWindow();[DllImport("User32.dll")]public static extern IntPtr GetWindowLong(IntPtr hWnd, int nIndex);[DllImport("user32.dll", EntryPoint = "SetWindowLong")]private static extern int SetWindowLong32(HandleRef hWnd, int nIndex, int dwNewLong);[DllImport("user32.dll", EntryPoint = "SetWindowLongPtr")]private static extern IntPtr SetWindowLongPtr64(HandleRef hWnd, int nIndex, IntPtr dwNewLong);[DllImport("user32.dll", EntryPoint = "DefWindowProcA")]public static extern IntPtr DefWindowProc(IntPtr hWnd, uint wMsg, IntPtr wParam, IntPtr lParam);// 将消息信息传递给指定的窗口过程[DllImport("user32.dll")]public static extern IntPtr CallWindowProc(IntPtr lpPrevWndFunc, IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);[DllImport("user32.dll")]public static extern bool IsIconic(IntPtr hWnd);[DllImport("user32.dll")]public static extern bool IsZoomed(IntPtr hWnd);public static IntPtr GetWindow(string titleOrClassname){IntPtr hWnd = FindWindow(null, titleOrClassname); ;if (hWnd == IntPtr.Zero){hWnd = FindWindow(titleOrClassname, null); }return hWnd;}public static IntPtr SetWindowLongPtr(HandleRef hWnd, int nIndex, IntPtr dwNewLong){if (IntPtr.Size == 8)return SetWindowLongPtr64(hWnd, nIndex, dwNewLong);else{return new IntPtr(SetWindowLong32(hWnd, nIndex, dwNewLong.ToInt32()));}}// 展示任务栏public static void ShowTaskWnd(){ShowWindow(FindWindow("Shell_TrayWnd", null), SW_RESTORE);}// 隐藏任务栏public static void HideTaskWnd(){ShowWindow(FindWindow("Shell_TrayWnd", null), SW_HIDE);}// 展示任务栏上的图标 TODO:有问题public static void ShowTaskIcon(string titleOrClassname){// https://stackoverflow.com/questions/1462504/how-to-make-window-appear-in-taskbarIntPtr mainWindIntPtr = GetWindow(titleOrClassname);if (mainWindIntPtr != IntPtr.Zero){HandleRef pMainWindow = new HandleRef(null, mainWindIntPtr);SetWindowLongPtr(pMainWindow, GWL_EXSTYLE, (IntPtr)(GetWindowLong(mainWindIntPtr, GWL_EXSTYLE).ToInt32() | WS_EX_APPWINDOW));ShowWindow(mainWindIntPtr, SW_HIDE);ShowWindow(mainWindIntPtr, SW_SHOW);}}// 隐藏任务栏上的图标 TODO:有问题public static void HideTaskIcon(string titleOrClassname){// https://forum.unity.com/threads/can-the-taskbar-icon-of-a-unity-game-be-hidden.888625/?_ga=2.191055082.1747733629.1614429624-1257832814.1586182347#post-5838658// https://docs.microsoft.com/en-us/windows/win32/shell/taskbar#managing-taskbar-buttonsIntPtr mainWindIntPtr = GetWindow(titleOrClassname);if (mainWindIntPtr != IntPtr.Zero){HandleRef pMainWindow = new HandleRef(null, mainWindIntPtr);SetWindowLongPtr(pMainWindow, GWL_EXSTYLE, (IntPtr)(GetWindowLong(mainWindIntPtr, GWL_EXSTYLE).ToInt32() | WS_EX_TOOLWINDOW));ShowWindow(mainWindIntPtr, SW_HIDE);ShowWindow(mainWindIntPtr, SW_SHOW);}}}}

监听最小化和关闭事件。
AppStart.cs

using Lavender.Systems;using System;using System.IO;using System.Runtime.InteropServices;using UnityEngine;using UnityWin;public class AppStart : MonoBehaviour{#region Unity_Methodprivate void Start(){Init();}private void OnDestroy(){TermWndProc();}private void OnGUI(){// TODO:有bugif (GUI.Button(new Rect(20, 20, 100, 30), "显示任务栏图标")){WinUser32.ShowTaskIcon(AppConst.AppName);}if (GUI.Button(new Rect(20, 60, 100, 30), "隐藏任务栏图标")){WinUser32.HideTaskIcon(AppConst.AppName);}}#endregionprivate void Init(){Screen.SetResolution(AppConst.DefaultWidth, AppConst.DefaultHeight, false);InitWndProc();WinUser32.ShowWindow(WinUser32.GetWindow(AppConst.AppName), WinUser32.SW_HIDE);#if UNITY_STANDALONE// https://github.com/josh4364/IL2cppStartProcessvar processPath = Directory.GetCurrentDirectory() + "\\UnityWinNotify\\UnityWinNotify.exe";if (File.Exists(processPath)){uint ptr = StartExternalProcess.Start(processPath, Directory.GetCurrentDirectory());}#endif}#region 监听窗体事件private HandleRef hMainWindow;private static IntPtr oldWndProcPtr;private IntPtr newWndProcPtr;private WndProcDelegate newWndProc;public void InitWndProc(){hMainWindow = new HandleRef(null, WinUser32.GetWindow(AppConst.AppName));newWndProc = new WndProcDelegate(WndProc);newWndProcPtr = Marshal.GetFunctionPointerForDelegate(newWndProc);oldWndProcPtr = WinUser32.SetWindowLongPtr(hMainWindow, -4, newWndProcPtr);}public void TermWndProc(){WinUser32.SetWindowLongPtr(hMainWindow, -4, oldWndProcPtr);hMainWindow = new HandleRef(null, IntPtr.Zero);oldWndProcPtr = IntPtr.Zero;newWndProcPtr = IntPtr.Zero;newWndProc = null;}[MonoPInvokeCallback]private static IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam){if (msg == WinUser32.WM_SYSCOMMAND){if ((int)wParam == WinUser32.SC_CLOSE){// 关闭WinUser32.ShowWindow(hWnd, WinUser32.SW_HIDE);return IntPtr.Zero;}else if ((int)wParam == WinUser32.SC_MAXIMIZE){// 最大化}else if ((int)wParam == WinUser32.SC_MINIMIZE){// 最小化WinUser32.ShowWindow(hWnd, WinUser32.SW_HIDE);return IntPtr.Zero;}}//Debug.Log("WndProc msg:0x" + msg.ToString("x4") + " wParam:0x" + wParam.ToString("x4") + " lParam:0x" + lParam.ToString("x4"));return WinUser32.CallWindowProc(oldWndProcPtr, hWnd, msg, wParam, lParam);}#endregion}

2.2.2 方便打包的菜单栏


BuildTool.cs

using System.Diagnostics;using System.IO;using UnityEditor;using UnityEditor.Build.Reporting;using UnityEngine;using UnityWin;using Debug = UnityEngine.Debug;public class BuildTool : Editor{[MenuItem("Build/Build WindowsStandalone x864")]private static void Build(){PlayerSettings.productName = AppConst.AppName;PlayerSettings.runInBackground = true;PlayerSettings.fullScreenMode = FullScreenMode.Windowed;PlayerSettings.defaultIsNativeResolution = true;PlayerSettings.defaultScreenWidth = AppConst.DefaultWidth;PlayerSettings.defaultScreenWidth = AppConst.DefaultHeight;PlayerSettings.resizableWindow = true;PlayerSettings.forceSingleInstance = true;PlayerSettings.SetScriptingBackend(BuildTargetGroup.Standalone, ScriptingImplementation.IL2CPP);PlayerSettings.SetScriptingDefineSymbolsForGroup(BuildTargetGroup.Standalone, "");BuildPlayerOptions buildPlayerOptions = new BuildPlayerOptions();buildPlayerOptions.scenes = new[] { "Assets/Scenes/Start.unity"};buildPlayerOptions.locationPathName = $"Build/WindowsStandalone/{AppConst.AppName}.exe";buildPlayerOptions.target = BuildTarget.StandaloneWindows;buildPlayerOptions.options = BuildOptions.None;string exePath = System.Environment.CurrentDirectory + "/Build/WindowsStandalone";Directory.Delete(exePath, true);BuildReport report = BuildPipeline.BuildPlayer(buildPlayerOptions);BuildSummary summary = report.summary;if (summary.result == BuildResult.Succeeded){FileUtil.CopyFileOrDirectory($"{System.Environment.CurrentDirectory}/UnityWinNotify", $"{exePath}/UnityWinNotify");Directory.Delete($"{exePath}/UnityWin_BackUpThisFolder_ButDontShipItWithYourGame", true);Process.Start(exePath);Process.Start($"{exePath}/{AppConst.AppName}.exe");}if (summary.result == BuildResult.Failed){Debug.Log("Build failed");}}}

2.2.3 IL2CPP启动外部程序

由于Unity的IL2CPP还未实现c#的Process类,所以不能使用Process.Start启动其他程序。
具体可见IL2CPP and Process.Start。
github上有其他人写好的工具可解决这个问题。
代码如下:
StartExternalProcess.cs

#if UNITY_STANDALONE_WIN || UNITY_EDITOR_WINusing System;using System.ComponentModel;using System.Runtime.InteropServices;// ReSharper disable FieldCanBeMadeReadOnly.Local// ReSharper disable InconsistentNaming// ReSharper disable UnusedMember.Local// ReSharper disable MemberCanBePrivate.Localnamespace Lavender.Systems{public static class StartExternalProcess{[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)][return: MarshalAs(UnmanagedType.Bool)]private static extern bool CreateProcessW(string lpApplicationName,[In] string lpCommandLine,IntPtr procSecAttrs,IntPtr threadSecAttrs,bool bInheritHandles,ProcessCreationFlags dwCreationFlags,IntPtr lpEnvironment,string lpCurrentDirectory,ref STARTUPINFO lpStartupInfo,ref PROCESS_INFORMATION lpProcessInformation);[DllImport("kernel32.dll", SetLastError = true)][return: MarshalAs(UnmanagedType.Bool)]private static extern bool CloseHandle(IntPtr hObject);[DllImport("kernel32.dll", SetLastError = true)][return: MarshalAs(UnmanagedType.Bool)]private static extern bool TerminateProcess(IntPtr processHandle, uint exitCode);[DllImport("kernel32.dll", SetLastError = true)]private static extern IntPtr OpenProcess(ProcessAccessRights access, bool inherit, uint processId);[Flags]private enum ProcessAccessRights : uint{PROCESS_CREATE_PROCESS = 0x0080, // Required to create a process.PROCESS_CREATE_THREAD = 0x0002, // Required to create a thread.PROCESS_DUP_HANDLE = 0x0040, // Required to duplicate a handle using DuplicateHandle.PROCESS_QUERY_INFORMATION = 0x0400, // Required to retrieve certain information about a process, such as its token, exit code, and priority class (see OpenProcessToken, GetExitCodeProcess, GetPriorityClass, and IsProcessInJob).PROCESS_QUERY_LIMITED_INFORMATION = 0x1000, // Required to retrieve certain information about a process (see QueryFullProcessImageName). A handle that has the PROCESS_QUERY_INFORMATION access right is automatically granted PROCESS_QUERY_LIMITED_INFORMATION. Windows Server 2003 and Windows XP/2000: This access right is not supported.PROCESS_SET_INFORMATION = 0x0200, // Required to set certain information about a process, such as its priority class (see SetPriorityClass).PROCESS_SET_QUOTA = 0x0100, // Required to set memory limits using SetProcessWorkingSetSize.PROCESS_SUSPEND_RESUME = 0x0800, // Required to suspend or resume a process.PROCESS_TERMINATE = 0x0001, // Required to terminate a process using TerminateProcess.PROCESS_VM_OPERATION = 0x0008, // Required to perform an operation on the address space of a process (see VirtualProtectEx and WriteProcessMemory).PROCESS_VM_READ = 0x0010, // Required to read memory in a process using ReadProcessMemory.PROCESS_VM_WRITE = 0x0020, // Required to write to memory in a process using WriteProcessMemory.DELETE = 0x00010000, // Required to delete the object.READ_CONTROL = 0x00020000, // Required to read information in the security descriptor for the object, not including the information in the SACL. To read or write the SACL, you must request the ACCESS_SYSTEM_SECURITY access right. For more information, see SACL Access Right.SYNCHRONIZE = 0x00100000, // The right to use the object for synchronization. This enables a thread to wait until the object is in the signaled state.WRITE_DAC = 0x00040000, // Required to modify the DACL in the security descriptor for the object.WRITE_OWNER = 0x00080000, // Required to change the owner in the security descriptor for the object.STANDARD_RIGHTS_REQUIRED = 0x000f0000,PROCESS_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0xFFF // All possible access rights for a process object.}[StructLayout(LayoutKind.Sequential)]private struct PROCESS_INFORMATION{internal IntPtr hProcess;internal IntPtr hThread;internal uint dwProcessId;internal uint dwThreadId;}[StructLayout(LayoutKind.Sequential)]private struct STARTUPINFO{internal uint cb;internal IntPtr lpReserved;internal IntPtr lpDesktop;internal IntPtr lpTitle;internal uint dwX;internal uint dwY;internal uint dwXSize;internal uint dwYSize;internal uint dwXCountChars;internal uint dwYCountChars;internal uint dwFillAttribute;internal uint dwFlags;internal ushort wShowWindow;internal ushort cbReserved2;internal IntPtr lpReserved2;internal IntPtr hStdInput;internal IntPtr hStdOutput;internal IntPtr hStdError;}[Flags]private enum ProcessCreationFlags : uint{NONE = 0,CREATE_BREAKAWAY_FROM_JOB = 0x01000000,CREATE_DEFAULT_ERROR_MODE = 0x04000000,CREATE_NEW_CONSOLE = 0x00000010,CREATE_NEW_PROCESS_GROUP = 0x00000200,CREATE_NO_WINDOW = 0x08000000,CREATE_PROTECTED_PROCESS = 0x00040000,CREATE_PRESERVE_CODE_AUTHZ_LEVEL = 0x02000000,CREATE_SECURE_PROCESS = 0x00400000,CREATE_SEPARATE_WOW_VDM = 0x00000800,CREATE_SHARED_WOW_VDM = 0x00001000,CREATE_SUSPENDED = 0x00000004,CREATE_UNICODE_ENVIRONMENT = 0x00000400,DEBUG_ONLY_THIS_PROCESS = 0x00000002,DEBUG_PROCESS = 0x00000001,DETACHED_PROCESS = 0x00000008,EXTENDED_STARTUPINFO_PRESENT = 0x00080000,INHERIT_PARENT_AFFINITY = 0x00010000}public static uint Start(string path, string dir, bool hidden = false){ProcessCreationFlags flags = hidden ? ProcessCreationFlags.CREATE_NO_WINDOW : ProcessCreationFlags.NONE;STARTUPINFO startupinfo = new STARTUPINFO{cb = (uint)Marshal.SizeOf<STARTUPINFO>()};PROCESS_INFORMATION processinfo = new PROCESS_INFORMATION();if (!CreateProcessW(null, path, IntPtr.Zero, IntPtr.Zero, false, flags, IntPtr.Zero, dir, ref startupinfo, ref processinfo)){throw new Win32Exception();}return processinfo.dwProcessId;}public static int KillProcess(uint pid){IntPtr handle = OpenProcess(ProcessAccessRights.PROCESS_ALL_ACCESS, false, pid);if (handle == IntPtr.Zero){return -1;}if (!TerminateProcess(handle, 0)){throw new Win32Exception();}if (!CloseHandle(handle)){throw new Win32Exception();}return 0;}}}#endif

2.2.4 winform程序的托盘图标

托盘图标对应的类是NotifyIcon,使用比较简单,相信一看代码就明白了。

using System;using System.Diagnostics;using System.Windows.Forms;using UnityWin;namespace UnityWinNotify{public partial class MainForm : Form{private const string UnityWinApp = "UnityWin";private NotifyIcon notifyIcon;public MainForm(){InitializeComponent();}private void MainForm_Load(object sender, EventArgs e){InitNotifyIcon();this.Closed += MainForm_Closed;// 隐藏窗体this.ShowInTaskbar = false;this.WindowState = FormWindowState.Minimized;}private void MainForm_Closed(object sender, EventArgs e){}private void InitNotifyIcon(){notifyIcon = new NotifyIcon();notifyIcon.BalloonTipText = "Unity程序正在后台运行"; // 首次运行时的提示notifyIcon.Text = "控制Unity程序";notifyIcon.Icon = Properties.Resources.GithubIco;notifyIcon.Visible = true;notifyIcon.ShowBalloonTip(2000); // 气泡显示的时间 毫秒notifyIcon.MouseClick += notifyIcon_MouseClick;MenuItem maximumMenuItem = new MenuItem("最大化");MenuItem minimumMenuItem = new MenuItem("最小化");MenuItem spiltLineMenuItem = new MenuItem("-");MenuItem exitMenuItem = new MenuItem("退出");MenuItem[] childen = new MenuItem[] { maximumMenuItem, minimumMenuItem, spiltLineMenuItem, exitMenuItem };notifyIcon.ContextMenu = new ContextMenu(childen);maximumMenuItem.Click += MaximumMenuItem_Click;minimumMenuItem.Click += MinimumMenuItem_Click;exitMenuItem.Click += ExitMenuItem_Click;}// 最大化private void MaximumMenuItem_Click(object sender, EventArgs e){IntPtr hWnd = WinUser32.GetWindow(UnityWinApp);if (hWnd != IntPtr.Zero){WinUser32.ShowWindow(hWnd, WinUser32.SW_MAXIMIZE);}}// 最小化private void MinimumMenuItem_Click(object sender, EventArgs e){IntPtr hWnd = WinUser32.GetWindow(UnityWinApp);if (hWnd != IntPtr.Zero){//WinUser32.ShowWindow(hWnd, WinUser32.SW_MINIMIZE);// 这里直接隐藏WinUser32.ShowWindow(hWnd, WinUser32.SW_HIDE);}}private void ExitMenuItem_Click(object sender, EventArgs e){try {Process[] processes = Process.GetProcesses();foreach (Process p in processes){if (p.ProcessName == UnityWinApp){p.Kill();}}Environment.Exit(0);}catch (Exception){}}// 点击托盘图标private void notifyIcon_MouseClick(object sender, MouseEventArgs e){//if (e.Button == MouseButtons.Left)//{// if (this.Visible == true)// {// this.Visible = false;// }// else// {// this.Visible = true;// this.Activate();// }//}}}}

2.2.5 winform程序单例运行

使用了Mutex。

using System;using System.Runtime.InteropServices;using System.Threading;using System.Windows.Forms;namespace UnityWinNotify{[Guid("6e40dbb7-9cc3-440e-9a75-5525dc3b1bfe")]static class Program{// Mutex can be made static so that GC doesn't recycle// same effect with GC.KeepAlive(mutex) at the end of mainstatic Mutex mutex = new Mutex(false, "6e40dbb7-9cc3-440e-9a75-5525dc3b1bfe");// Guid guid = Guid.NewGuid(); // 创建Guid/// <summary>/// 应用程序的主入口点。/// </summary>[STAThread]static void Main(){if (!mutex.WaitOne(TimeSpan.FromSeconds(2), false)){//MessageBox.Show("Application already started!", "", MessageBoxButtons.OK);return;}try{Application.EnableVisualStyles();Application.SetCompatibleTextRenderingDefault(false);Application.Run(new MainForm());}finally{mutex.ReleaseMutex();}}}}

3 完整项目

包含Unity及Winform项目。
链接:https://pan.baidu.com/s/12zyoxck417dtRCobaybseg
提取码:ho48

博主个人博客本文链接。

4 参考文章

  • https://forum.unity.com/threads/can-the-taskbar-icon-of-a-unity-game-be-hidden.888625/?_ga=2.191055082.1747733629.1614429624-1257832814.1586182347#post-5838658
  • https://stackoverflow.com/questions/8243588/c-sharp-hiding-an-application-from-the-taskbar
  • https://www.vishalon.net/blog/run-single-instance-of-winform-application
需要做网站?需要网络推广?欢迎咨询客户经理 13272073477