更好的实现方式,见这里。
下面这种方式可以废弃了。
基本功能:
通过监听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
}
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");
}
}
}
由于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<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
托盘图标对应的类是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();
// }
//}
}
}
}
使用了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();
}
}
}
}
包含Unity及Winform项目。
链接:https://pan.baidu.com/s/12zyoxck417dtRCobaybseg
提取码:ho48
博主个人博客本文链接。