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

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

2021年2月28日 0 作者 老王

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-showwindow
        public 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=MSDN
        public 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-taskbar
            IntPtr 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-buttons
            IntPtr 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_Method
    private void Start()
    {
        Init();
    }

    private void OnDestroy()
    {
        TermWndProc();
    }

    private void OnGUI()
    {
        // TODO:有bug
        if (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);
        }
    }

    #endregion

    private 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/IL2cppStartProcess

        var 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_WIN
using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
// ReSharper disable FieldCanBeMadeReadOnly.Local
// ReSharper disable InconsistentNaming
// ReSharper disable UnusedMember.Local
// ReSharper disable MemberCanBePrivate.Local

namespace 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()
            };
            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 main
        static Mutex mutex = new Mutex(false, "6e40dbb7-9cc3-440e-9a75-5525dc3b1bfe");
        // Guid guid = Guid.NewGuid(); // 创建Guid

        /// 
        /// 应用程序的主入口点。
        /// 
        [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 参考文章: