遇到了三个开发需求,如下
- Unity桌面程序外部开启并且这个程序只启动一次
- 唤起程序能立刻表现出来,无延迟,看起来像是在一个app
- 两个exe程序之间传递参数
- 唤起exe的时候的双屏唤起
实现这些功能不能单靠unity了,要了解一些windows PC端的API和其他奇技淫巧了
Unity桌面程序外部开启并且这个程序只启动一次
这是针对windows平台的程序的需求,桌面端的程序搜索的时候应该搜索exe 不要搜windows
只开启一个实例是搜出来的 Unity exe启动传递参数
Unity如何限制启动一次实例
经过测试发现,只启动一次这个选项对于是用Process.Start()这个静态方法是没有用的,需要使用对象方法才行, 幸好能想到他们一个是类方法一个是对象方法,否则就会错失良机再去找很久了。 能想到不同的使用情况是很重要的
唤起端
public class CallJTShow
{
private ExternalShowExe _externalShowExe = new ExternalShowExe();
private const string exePath = @"C:\Users\admin\Desktop\FengHuoXiange9\JTShow3D\Fenghuoxiange.exe";
private const string exeName = "Fenghuoxiange";
private static CallJTShow instance;
public static CallJTShow Instance
{
get
{
if (instance == null)
{
instance = new CallJTShow();
}
return instance;
}
}
public void Call(string transportMessage)
{
_externalShowExe.FindOpenEXE(exePath, transportMessage, exeName);
}
}
class ExternalShowExe
{
private Process myprocess;
[SerializeField]
string ExeArgus;
public void FindOpenEXE(string exePathName, string transportMessage, string exeName)
{
try
{
FindExistProcess(exeName);
if (myprocess == null)
{
myprocess = new Process();
}
ProcessStartInfo startInfo = new ProcessStartInfo(exePathName, transportMessage);
myprocess.StartInfo = startInfo;
myprocess.StartInfo.UseShellExecute = false;
myprocess.Start();
}
catch (Exception ex)
{
UnityEngine.Debug.Log("出错原因:" + ex.Message);
}
}
private void FindExistProcess(string exeName)
{
if (myprocess == null)
{
Process[] allProcesses = Process.GetProcesses();
foreach (var pro in allProcesses)
{
UnityEngine.Debug.Log(" Id " + pro.Id + " ProcessName " + pro.ProcessName +
" exeName " + exeName + " equal " + (pro.ProcessName == exeName));
if (pro.ProcessName == exeName)
{
myprocess = pro;
break;
}
}
}
}
}
这里的Start方法要用的是对象的而不是类的。
要注意唤起端如果名字不同的话,直接改名字不起作用要重新打包,打包的时候在这里改名字改成和唤起时所标记的名字一样。
在被唤起的exe在启动过程中又被FindOpenEXE函数调用,则会出现报错提示框,其他情况则是直接将需要唤起的exe弹出到屏幕
因为具体的需求是在A打包的exe文件夹同级目录下有个ExternExe文件夹,这个文件夹下面才是要打开的另一个Unity的exe程序,所以在写路径的时候为了保证无论程序在哪里都要正确读取到其下的exe,路径应该这样写
private const string exeName = "Fenghuoxiange";
private const string args = "666";
private const string exeExtension = ".exe";
private const string fixSlant = "\\";
private const string outsideFolderName = "ExternExe";
string toOpenExeDir = String.Empty;
string GetToOpenDirectoryStr()
{
if (toOpenExeDir == String.Empty)
{
string currentExeDirectory = System.Environment.CurrentDirectory;
toOpenExeDir = currentExeDirectory + fixSlant + outsideFolderName + fixSlant + exeName + exeExtension;
print(toOpenExeDir);
}
return toOpenExeDir;
}
System.Environment.CurrentDirectory;表示的是当前exe所在的目录,不包括exe 参考 Unity(C#)获取当前运行exe路径的方法
被唤起端
注意被唤起端的项目设置 通过勾选edit->project setting->player中resution and presention下面的force Single Instance选项,可以限制这个exe只能打开一个。
接收参数代码
using System;
using UnityEngine;
using UnityEngine.UI;
public class StartGame : MonoBehaviour
{
public Text text1;
void Start()
{
string[] CommandLineArgs = Environment.GetCommandLineArgs();
if (CommandLineArgs.Length < 2)
{
Debug.Log("没有参数");
Application.Quit();
}
else
{
if (CommandLineArgs[1] =="")
{
}
else
{
}
}
}
}
唤起程序能立刻表现出来,无延迟,看起来像是在一个exe
这里唤起的exe叫A,被唤起的exe叫B
想了三种方法:
- 用bat的方式先打开B再打开A
- 用Unity只导出windwos工程,在A的windows工程里面找到开端的函数在最开始的时候进行B的唤起
- 写一个exe,先唤起B,等B界面出来以后, 再会唤起A
按照之前唤起的特性,要立刻表现出来只能是B程序已经在运行中,而不能是重新开始。那么只能是在A唤起的某一刻,B也唤起,但是唤起B时,依然保持A完全占据屏幕
试过后发现唤起和出现不是对应的,可能先唤起的后出现,这样就会出现B覆盖在A的前面了。
那么目前想到的有几种方法 1. 用C的exe 同时唤起AB两个程序,在开始的一段时间检测是否两者都开启了并且B在前面,如果是就调用对应的API让A在前面。 2. 先唤起B程序,在检测到B程序出现在屏幕时候 ,再唤起A程序。 3. A程序中有个方法在持续一段时间内如果检测到不是在屏幕最前面,就自己调用函数让其在最前面。
在熟悉了相关的API之后,这些应该都是可以轻易做到的。
用bat的方式先打开B再打开A
bat的方式对于普通的Unity空工程打出的 exe程序能保证执行顺序,有点复杂的工程就不行了。
写一个exe,先唤起B,等B界面出来以后, 再会唤起A
除了MFC,后来听说有Winform和WPF这些名词
Unity的exe来做
代码
[DllImport("user32.dll")]
private static extern IntPtr GetForegroundWindow();
void Update()
{
foreach(Process myproc in Process.GetProcessesByName("TestWindowOpen"))
{
if(myproc.MainWindowHandle.ToInt32() ==
GetForegroundWindow().ToInt32())
{
print(" 111111111 ");
}
else
{
}
}
这段代码保证了能检测到某Unity的exe程序是否展现在了平面上
参考
如何判断某个窗口已经成为活动窗口?
最小化代码
因为启动的顺序和出现的顺序不敢保证是一致的,所以需要在B中进行最小化。 以及B程序在退出的时候相对于发消息给A程序让A显示,直接最小化是更简便的做法,所以最小化是需要的
一开始百度和谷歌搜索的"exe不显示" “exe不弹出 ” “windows 不弹出” “exe程序不出现” 都搜不出来 QQ群友突然提出最小化这个名词
换个名字搜 叫做最小化 然后 Unity最小化
搜到了是可以 在这unity 窗口最小化
Unity打出来的exe如果要获取一些窗口相关的信息 user32.dll是少不了的 如 Unity3D中调用Windows窗口句柄[DlImport("user32.dll")]实现去Windows边框、窗口最大/最小化、获取状态栏高度等
public class Minimize
{
[DllImport("user32.dll")]
public static extern bool ShowWindow(IntPtr hwnd, int nCmdShow);
[DllImport("user32.dll")]
static extern IntPtr GetForegroundWindow();
const int SW_SHOWMINIMIZED = 2;
const int SW_SHOWMAXIMIZED = 3;
const int SW_SHOWRESTORE = 1;
private void Awake()
{
OnClickMinimize();
}
void Start ()
{
}
public void OnClickMinimize()
{
ShowWindow(GetForegroundWindow(), SW_SHOWMINIMIZED);
}
public void OnClickMaximize()
{
ShowWindow(GetForegroundWindow(), SW_SHOWMAXIMIZED);
}
public void OnClickRestore()
{
ShowWindow(GetForegroundWindow(), SW_SHOWRESTORE);
}
}
测试是可以的
但是启动时候即使最小化的API是写到了Awake下面 但还是会先加载启动画面(个人版没办法,不是pro版本) 想了想在Unity启动画面刚开始的时候能不能有回调方法,但是找了好久找不到
所以
- 方案一: 想了想两个启动画面一起运行好了 批处理里面去启动 先启动要最小化的,再启动要显示的 于是最小化的会被要显示的覆盖住
然后最小化的程序最小化之后 就不显示出来 现在问题在于想规避批处理启动时的黑窗口 以及想把批处理的开启图标改成程序启动的图标,那么有了搜索 “批处理不显示黑框”和“批处理更换图标” 于是有了 如何修改.bat文件的图标? 运行批处理bat文件不出现黑框
问题解决
方案二:找到Unity程序刚启动一瞬间时候的回调方法,在这里进行最小化操作 于是搜“Unity刚启动时候调用”,“Unity启动画面回调” 于是有了 我可以在Unity启动画面中运行自己的加载脚本吗? unity生命周期 静态函数的测试是不行的,加的标签因为是UnityEditor下面的所以打包之后带不出来 后来想是干脆用命令行的方式在刚开启的时候就将其最小化吧 目前还不知道命令行最小化某个窗口的可行性,所以先把方案一做出来是最妥的
start/min JTShow.exe
这句话对于一些程序有效 对于一些程序无效 而且对于同一个程序可能刚开始有效 后面就无效了 也不知道是为什么
两个程序之间传递参数
上面的方法只能在打开的时候传递参数
而且process里面的方法找了也没有其他方式能够传递参数了
所以现在就只能用其他方式传递参数了
百度搜索“Unity exe 传递参数”,“Unity exe交互”“Unity 程序交互”
然后突然想起user32.dll 可能里面有一些函数能够根据第一次开启的句柄找到窗口然后进行传参,对现有的功能的了解可能会推进后面出现的问题的解决
这个dll 使用到的API的功能以及声明写法在
User32.dll 函数的相关方法整理
在这之前找到个
Unity3D使用UDP协议实现两个程序传递信息
感觉是行的,没试,有点复杂想作为后备方案, 在搜User32时候,有两个方法一开始眼前一亮后来有点失望,再到后来又有点峰回路转的, SendMessage和PostMessage 后来搜索 “exe 程序发送消息” 找到这个
C#实现在应用程序间发送消息的方法示例 搜 “unity exe 通信”再找到这个 使用C#进行应用程序间通信(WPF与Unity通信)
好像不太对,WPF我不会,要的是两个unity的exe的通信
然后项目组大佬提示了这个组内已经使用的技术 一个项目组里面用同一种技术是对维护友好的,但我就是对这些字眼看不顺眼 所以还是自己搜 于是 unity3d进程通信利用WM_COPYDARE和HOOK
这个试了两小时还是不行,
最后还是 Unity3D使用UDP协议实现两个程序传递信息 这个好使
于是有了整合 Unity在PC端的exe程序之间的通信
其他相关链接:
Unity 定时开启/关闭外部应用
Standalone Player settings
一个小插曲是:我开发中莫名其妙rider和unity会自动最小化,刚开始十多分钟一直以为是病毒,后来想想,原来是测试时写了这个函数
[InitializeOnLoad]
public static class StaticClass
{
[DllImport("user32.dll")]
public static extern bool ShowWindow(IntPtr hwnd, int nCmdShow);
[DllImport("user32.dll")]
static extern IntPtr GetForegroundWindow();
static StaticClass()
{
ShowWindow(GetForegroundWindow(), 2);
}
}
唤起exe的时候的双屏唤起
因为两个交互的exe都是双屏的,用普通的process类对象.Start方法进行已存在进程的即刻唤起以及minimize等方法的话, 双屏的exe的第二屏可能会随机出现,所以需要能保证在第二个exe出现的时候,它的第二屏一定能出现
那么有两种方法:
- 在exe被唤起的时候,对被唤起的exe的第二屏强制显示
- 在exe被唤起的时候,对唤起其他exe的第二屏强制最小化
按照这两个逻辑我在Unity2022中试了很多user32.dll里面的方法 但是都不行,这些方法包括
[DllImport("user32.dll")]
public static extern bool ShowWindow(IntPtr hwnd, int nCmdShow);
[DllImport("user32.dll")]
static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")]
public static extern int BringWindowToTop(IntPtr hWnd);
const int SW_SHOWMAXIMIZED = 3;
const int SW_SHOWMINIMIZED = 2;
const int SW_SHOWRESTORE = 1;
public void BringWindowToTop()
{
BringWindowToTop(GetForegroundWindow() );
}
public void MaximizeThisWindow()
{
ShowWindow(GetForegroundWindow(), SW_SHOWMAXIMIZED);
}
public void ResumeThisWindow()
{
ShowWindow(GetForegroundWindow(), SW_SHOWRESTORE);
}
这种双屏的exe,实际的进程也是只有一个
可能是我传入的参数不对GetForegroundWindow()可能只代表Unity第一屏的窗口的句柄,第二屏的应该要用其他的API来获取,但感觉这个API比较难找,所以就没试了。 并且当时测试了Unity的API也无效,代码如下, 无效的原因可能是和当时的工程环境有关系
public void OnSecondDisplayShow()
{
Screen.fullScreen = true;
foreach (Display display in Display.displays)
{
if (display != null)
{
display.Activate();
display.SetRenderingResolution(display.systemWidth, display.systemHeight);
display.SetParams(display.systemWidth, display.systemHeight,0 ,0);
}
}
}
然后后面试了一下Unity里面的方法,这个方法是二屏最小化和最大化切换的方法, 其中有个最小化的方法需要配合一些PlayerSettings的设置,这个方法在空工程里面是百试百灵的。代码如下
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SecondaryDisplay : MonoBehaviour
{
private int originSecondDisplayWidth;
private int originSecondDisplayHeight;
private bool isMinimize;
void Start()
{
Screen.fullScreen = true;
if (Display.displays.Length > 1)
{
Display.displays[1].Activate();
Display.displays[1].SetRenderingResolution(Display.displays[1].systemWidth, Display.displays[1].systemHeight);
originSecondDisplayWidth = Display.displays[1].renderingWidth;
originSecondDisplayHeight = Display.displays[1].renderingHeight;
}
}
public void OnSecondDisplayResize()
{
if (!isMinimize)
{
if (Display.displays.Length > 1)
{
Display.displays[1].Activate();
Display.displays[1].SetParams(1, 1,0 ,0);
isMinimize = true;
}
}
else
{
if (Display.displays.Length > 1)
{
Display.displays[1].Activate();
Display.displays[1].SetParams(originSecondDisplayWidth,
originSecondDisplayHeight,0 ,0);
isMinimize = false;
}
}
}
}
工程配置如下 工程链接
但是到了实际项目中的两个工程中的时候都出现了问题。即使相关的分辨率和显示设置和代码都一模一样。 一个是最小化的时候不能显示桌面,而是虽然不显示二屏相机了但却变成了显示一片灰色 一个是不能最小化,只对最大化刷新二屏有反应。 最后测试发现在这样的设置中, 只有让第二屏全屏的代码在两个实际项目工程有效,如下
void ForceShowSecondDisplay()
{
if (Display.displays.Length > 1)
{
Display.displays[1].Activate();
Display.displays[1].SetParams(originSecondDisplayWidth,
originSecondDisplayHeight, 0, 0);
}
}
这个方法在调用之后要配合用Process对象的Start方法才有效,。
项目配置 然后决定就是现在变成两个exe互相发消息, 当其中任意一个exe被唤起的时候收到消息就自个将自个的二屏强制显示。
后来经过测试这个方法也不是百试百灵的方法,测试情况是对于其中一端来说,只有接触到了用户输入才能让二屏显示出来,这个是不能容忍的。 对于进程来说这两个窗口属于一个进程,并且进程的名字是打包时候的exe的名字,所以通过
Process.GetProcessesByName("Unity Secondary Display");
这种方法来找是找不到的。
之前有测试过一系列的关于窗口弹出的方法
但是都不奏效,后来想想应该是窗口的句柄不对。 之前只了解过Process.GetProcessesByName找到窗口句柄以及user32.dll的GetForeGroundWindow找到窗口句柄,因为给的名字都是exe本身的名字, 这两个方法都不是针对于二屏的,二屏实际上有个独立的句柄。只有通过名字来找才找得到。二屏独立的名字叫做Unity Secondary Screen 经过测试可行
using UnityEngine;
using System.Collections;
using UnityEngine.VR;
using System.Runtime.InteropServices;
using System;
using System.Diagnostics;
using System.Threading;
using System.Collections.Generic;
using System.Text;
public class CMDFilesToPlay : MonoBehaviour {
void Awake()
{
var videosSelectors = FindWindowsWithText("VideosSelector");
foreach (IntPtr hwnd in videosSelectors)
{
Thread.Sleep(100);
SetForegroundWindow(hwnd);
break;
}
}
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern bool SetForegroundWindow(IntPtr hWnd);
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern int GetWindowText(IntPtr hWnd, StringBuilder strText, int maxCount);
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern int GetWindowTextLength(IntPtr hWnd);
[DllImport("user32.dll")]
private static extern bool EnumWindows(EnumWindowsProc enumProc, IntPtr lParam);
public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
public static string GetWindowText(IntPtr hWnd)
{
int size = GetWindowTextLength(hWnd);
if (size > 0)
{
var builder = new StringBuilder(size + 1);
GetWindowText(hWnd, builder, builder.Capacity);
return builder.ToString();
}
return String.Empty;
}
public static IEnumerable<IntPtr> FindWindows(EnumWindowsProc filter)
{
IntPtr found = IntPtr.Zero;
List<IntPtr> windows = new List<IntPtr>();
EnumWindows(delegate(IntPtr wnd, IntPtr param)
{
if (filter(wnd, param))
{
windows.Add(wnd);
}
return true;
}, IntPtr.Zero);
return windows;
}
public static IEnumerable<IntPtr> FindWindowsWithText(string titleText)
{
return FindWindows(delegate(IntPtr wnd, IntPtr param)
{
return GetWindowText(wnd).Contains(titleText);
});
}
}
知道二屏的句柄之后想做什么就都可以了。
Unity自己启动的二屏窗口名字都叫做Unity Secondary Window, 因为查了感觉改不了二屏窗口的名字, 思路是 在完全没有其他Unity二屏exe的情况下, 先开启A程序找到名字为Unity Secondary Window的窗口句柄,保存起来,这个是A的二屏窗口的句柄。然后开启B窗口,将A的句柄信息传递给B,B在查找的时候排除这个,然后找到的就是B的二屏窗口句柄,这样就可以AB两个程序都自由控制自己的二屏了。
或者使用改窗口标题的方法
[DllImport("user32.dll")]
static extern int SetWindowText(IntPtr hWnd, string text)
参考文章
Run another application in backround without lose focus and minimize (silent run in background)
https://www.pinvoke.net/search.aspx?search=BringWindowToTop&namespace=[All]
|