Interaction between Unity desktop two screen programs

Posted by thedon on Thu, 03 Mar 2022 07:13:32 +0100

There are three development requirements, as follows:

  • The Unity desktop program is opened externally, and the program is started only once
  • The evocation program can be displayed immediately without delay, and looks like an app
  • Pass parameters between two exe programs
  • When exe evokes double screen

We can't rely on unity alone to realize these functions. We need to understand some API s and other tricks on the windows PC side

The Unity desktop program is opened externally, and the program is started only once

This is the demand for programs on the windows platform. When searching for programs on the desktop, you should search exe instead of windows

Only one instance is found
Unity exe start pass parameters

How does Unity restrict one instance from starting

After testing, it is found that this option is only started once. For process The static method start () is useless. You need to use the object method,
Fortunately, I can think that one of them is class method and the other is object method. Otherwise, I will miss the opportunity to look for it for a long time.
It's important to think of different uses

Arousal end

public class CallJTShow
{
    private ExternalShowExe _externalShowExe = new ExternalShowExe();
    
    // private const string exePath = @"D:\Unity Projects\Experiment\ExternalCall\TestExternalCallerA\Pack\TestExternalCallerA.exe";
    //
    // private const string exeName = "TestExternalCallerA.exe";
    
    //The format of the exe path to be invoked is as follows
    private const string exePath = @"C:\Users\admin\Desktop\FengHuoXiange9\JTShow3D\Fenghuoxiange.exe";

    
    /// <summary>
    ///Name used to compare traversal in the process
    ///Do not carry exe suffix
    /// </summary>
    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);
    }
    
}


/// <summary>
///Jump to another exe
///When the exe program is already running, it will not be restarted
///It's a direct jump
/// </summary>
 class ExternalShowExe 
{
    private Process myprocess;
    
    /// <summary>
    ///Transmit data
    /// </summary>
    [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("Error reason:" + 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;
                }
            }
        }
    }
}

The Start method here needs to use object instead of class.

It should be noted that if the name of the evocation end is different, changing the name directly does not work. It should be repackaged. When packaging, change the name here to be the same as the name marked during evocation.


When the invoked exe is called by the FindOpenEXE function during startup, an error prompt box will appear. In other cases, the EXE to be invoked will be directly popped up to the screen

Because the specific requirement is that there is an ExternExe folder under the same level directory of the EXE folder packaged by A. under this folder is another Unity exe program to be opened. Therefore, when writing the path, in order to ensure that the EXE under it should be read correctly no matter where the program is, the path should be written like this

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; Indicates the directory where the current exe is located, excluding exe
reference resources Unity (C#) method to get the path of currently running exe

Aroused end

Pay attention to the item setting of the called end
By checking the force Single Instance option under resolution and presence in Edit - > project setting - > player, you can limit this exe to open only one instance.

Receive parameter code

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("No parameters");
            Application.Quit();
        }
        else
        {
            if (CommandLineArgs[1] =="")
            {
                
            }
            else
            {
            //CommandLineArgs[1] is the passed parameter
            }
        }
    }
}

The evoke program can be displayed immediately without delay and looks like an exe

The exe aroused here is called A, and the exe aroused here is called B

Think of three ways:

  1. Open B first and then A in the form of bat
  2. Use Unity to export only the windbos project. Find the starting function in the windows project of A and call up B at the beginning
  3. Write an exe, first evoke B, and then evoke A after the B interface comes out

According to the characteristics aroused before, it can only be shown immediately that the B program is already running, not A restart. Then only at A certain moment when A evokes, B also evokes, but when B evokes, it still keeps A completely occupying the screen

After the trial, it is found that arousal and appearance are not corresponding. It may be aroused first and then appear. In this way, B will cover in front of A.

So there are several ways to think of at present
1. Use C's exe to evoke both AB programs at the same time. At the beginning of A period of time, check whether both are turned on and B is in front. If so, call the corresponding API to make A in front.
2. First call up program B, and then call up program A when it is detected that program B appears on the screen.
3. There is a method in a program. If it is detected that it is not in the front of the screen for a period of time, it will call the function to make it in the front.

After being familiar with the relevant API s, these should be easy to do.

Open B first and then A in the form of bat

bat for ordinary Unity empty projects, the exe program can ensure the execution order, but not for a little complex projects.

Write an exe, first evoke B, and then evoke A after the B interface comes out

In addition to MFC, I heard later that there are nouns such as Winform and WPF

Unity exe to do

code

[DllImport("user32.dll")]
    private static extern IntPtr GetForegroundWindow();


    void Update()
    {
		
        //Enumerate SelfService processes into a process array
        foreach(Process myproc in Process.GetProcessesByName("TestWindowOpen"))
        {
            if(myproc.MainWindowHandle.ToInt32() == 
               GetForegroundWindow().ToInt32())
            {
                print(" 111111111 ");
                
            }
            else
            {
                //The main window of the SelfService process is not an active window
                //do what you want here...
            }
        }

This code ensures that it can detect whether a Unity exe program is displayed on the plane

reference resources

How to judge whether a window has become an active window?

Minimize code

Since the sequence of startup and appearance cannot be guaranteed to be consistent, it needs to be minimized in B.
And when program B exits, it is easier to minimize directly than to send A message to program A to let program A display, so minimization is needed

At first, Baidu and Google couldn't find "exe doesn't display", "exe doesn't pop up", "windows doesn't pop up" and "exe program doesn't appear"
QQ group friends suddenly put forward the term minimization

Change the name to search for minimization, and then Unity minimization

Yes
Here Minimize the unit window

If you want to get some window related information from the exe printed by Unity, user32 DLL is indispensable
as
The Windows window handle [DlImport (&#34user32.dll&#34) is called in Unity3D to realize the Windows border, the maximum / minimum window, and the height of the status bar.

public class Minimize 
{
    
    //Import DLLs unmanaged. Here is to import user32 dll. 
    [DllImport("user32.dll")]
    public static extern bool ShowWindow(IntPtr hwnd, int nCmdShow);


    [DllImport("user32.dll")]
    static extern IntPtr GetForegroundWindow();

    const int SW_SHOWMINIMIZED = 2; //{minimize, activate}  
    const int SW_SHOWMAXIMIZED = 3;//Maximize  
    const int SW_SHOWRESTORE = 1;//reduction  

    private void Awake()
    {
        OnClickMinimize();
    }


    void Start ()
    {
        // transform.Find("Button").GetComponent<Button>().onClick.AddListener(delegate() 
        // {
        // });
    }


    public void OnClickMinimize()
    { 
        //minimize   
        ShowWindow(GetForegroundWindow(), SW_SHOWMINIMIZED);
    }


    public void OnClickMaximize()
    {
        //Maximize  
        ShowWindow(GetForegroundWindow(), SW_SHOWMAXIMIZED);
    }


    public void OnClickRestore()
    {
        //reduction  
        ShowWindow(GetForegroundWindow(), SW_SHOWRESTORE);
    }
}

The test is OK

But at startup, even the minimized API is written under wake
However, the startup screen will be loaded first (the personal version can't help, not the pro version)
I wondered if there could be a callback method at the beginning of the Unity startup screen, but I couldn't find it for a long time

therefore

  • Scheme 1: after thinking about it, the two start-up screens run together and start the batch process. First start the one to be minimized, and then start the one to be displayed, so the minimized one will be covered by the one to be displayed
    Then the minimized program will not be displayed after minimization. Now the problem is to avoid the black window during batch startup and change the batch startup icon to the program startup icon. Then search for "batch does not display black box" and "batch replacement icon"
    So there was
    How to modify bat file icon?
    No black box appears when running batch bat file

Problem solving

Scheme 2: find the callback method at the moment when the Unity program is just started, and minimize it here
So search "call when Unity starts", "Unity startup screen callback"
So there was
Can I run my own loading script in the Unity startup screen?
unity life cycle
The test of static functions is not good. The label added is under unitydeditor, so it can't be brought out after packaging
Later, I thought I could minimize it by using the command line when it was just started
At present, we don't know the feasibility of minimizing a window on the command line, so it's best to make scheme 1 first

start/min JTShow.exe

This sentence is valid for some programs, invalid for some programs, and invalid for the same program at the beginning. I don't know why

Pass parameters between two programs

The above method can only pass parameters when it is opened

Moreover, there is no other way to pass parameters when the method in process is found

So now we can only pass parameters in other ways

Baidu search "Unity exe pass parameters", "Unity exe interaction" and "Unity program interaction"

Then suddenly remembered user32 There may be some functions in DLL that can find the window according to the handle opened for the first time and then transfer parameters. Understanding the existing functions may promote the solution of the following problems

The functions and declaration of the API used by this dll are in

User32. Relevant methods of DLL functions

Find one before that

Unity3D uses UDP protocol to transfer information between two programs

I think it's OK. I didn't try it. It's a little complicated. I want to use it as a backup plan,
When searching User32, there are two methods. At first, the eyes are bright, then a little disappointed, and then a little twists and turns,
SendMessage and PostMessage
Later, I found this by searching "exe program sends message"

Example of C# implementing the method of sending messages between applications
Search "unity exe communication" and find this Use C# for inter application communication (WPF and Unity communication)

It doesn't seem quite right. I won't use WPF. What I want is the communication between two unity exe s

Then the leader of the project team prompted the technology already used in the group

Using the same technology in a project team is maintenance friendly, but I just don't like these words
So search it yourself unity3d process communication uses WM_COPYDARE and HOOK

I tried this for two hours, but it still didn't work,

Finally Unity3D uses UDP protocol to transfer information between two programs This works well

So there is integration Unity communication between exe programs on PC side

Other related links:

Unity regularly turns on / off external applications

Standalone Player settings

A small episode is: I have no idea that rider and unity will be automatically minimized in my development. I thought it was a virus for more than ten minutes at the beginning. Later, I thought it was written during the test

[InitializeOnLoad]
public static class StaticClass
{
    //Import DLLs unmanaged. Here is to import user32 dll. 
    [DllImport("user32.dll")]
    public static extern bool ShowWindow(IntPtr hwnd, int nCmdShow);

    [DllImport("user32.dll")]
    static extern IntPtr GetForegroundWindow();

    static StaticClass()
    {
        ShowWindow(GetForegroundWindow(), 2);
    }
    
}

When exe evokes double screen

Because the two interactive exe s are dual screen, they use ordinary process class objects If the Start method is used to immediately evoke existing processes and minimize other methods,
The second screen of double screen exe may appear randomly, so we need to ensure that when the second exe appears, its second screen will appear

Then there are two ways:

  1. When the EXE is invoked, the second screen of the invoked exe is forcibly displayed
  2. When an exe is invoked, the second screen that evokes other exes is forcibly minimized

According to these two logics, I tried a lot of user32 in Unity2022 Methods in DLL
But none of them. These methods include

    [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;//Window maximization (full screen with taskbar)
    const int SW_SHOWMINIMIZED = 2;//window minimizing
    const int SW_SHOWRESTORE = 1;//Window restore
    
 	public void BringWindowToTop()
    {
        BringWindowToTop(GetForegroundWindow() );
    }
    
   public void MaximizeThisWindow()
    {
        ShowWindow(GetForegroundWindow(), SW_SHOWMAXIMIZED);
    }
    
    public void ResumeThisWindow()
    {
        ShowWindow(GetForegroundWindow(), SW_SHOWRESTORE);
    }

This double screen exe has only one actual process

Maybe the parameter I passed in is wrong. GetForegroundWindow() may only represent the handle of the window in the first screen of Unity. The window in the second screen should be obtained by other APIs, but I feel that this API is difficult to find, so I didn't try it.
Moreover, the API of Unity tested at that time was also invalid. The code is as follows. The reason for the invalidity may be related to the engineering environment at that time

   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);

            }
        }
    }

Then I interviewed the method in Unity. This method is the method of two screen minimization and maximization switching. One of the minimization methods needs to be matched with the settings of some PlayerSettings. This method is a try and try method in air engineering. The code is as follows

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SecondaryDisplay : MonoBehaviour
{
    private int originSecondDisplayWidth;
    private int originSecondDisplayHeight;

    private bool isMinimize;
    
    // Start is called before the first frame update
    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].SetRenderingResolution(1, 1);
                //Setting the width height to 0 does not work
                Display.displays[1].SetParams(1, 1,0 ,0);
                isMinimize = true;
            }
        }
        else
        {
            if (Display.displays.Length > 1)
            {
                Display.displays[1].Activate();
                // Display.displays[1].SetRenderingResolution(1, 1);
                Display.displays[1].SetParams(originSecondDisplayWidth,
                    originSecondDisplayHeight,0 ,0);
                isMinimize = false;
            }
        }
        
        
    }
}

The project configuration is as follows:

Project link

But when it comes to the two projects in the actual project, there are problems. Even the relevant resolutions as like as two peas and the same display.
One is that the desktop cannot be displayed when minimized, but although the two screen camera is not displayed, it becomes gray
One is that it can't be minimized. It only reacts to the maximum refresh of the second screen.
Finally, the test found that in such a setting,
Only the code of the second full screen is valid in two actual projects, as follows

 void ForceShowSecondDisplay()
    {
        if (Display.displays.Length > 1)
        {
            Display.displays[1].Activate();
            // Display.displays[1].SetRenderingResolution(1, 1);
            Display.displays[1].SetParams(originSecondDisplayWidth,
                originSecondDisplayHeight, 0, 0);
        }
    }

This method is only effective after being called in conjunction with the Start method of the Process object,.

Project configuration

Then it is decided that now two exe send messages to each other. When any exe is aroused, the message will be forcibly displayed on the second screen.

Later, after testing, this method is not the method of bailing. The test situation is that for one end, the second screen can be displayed only after contacting the user input. This is intolerable. For processes, these two windows belong to one process, and the name of the process is the name of exe when packaging, so through

Process.GetProcessesByName("Unity Secondary Display");

You can't find it this way.

A series of methods about window pop-up have been tested before

But it didn't work. Later, I thought it should be the wrong handle of the window.
I've only known process before Getprocessesbyname found window handle and user32 DLL GetForeGroundWindow finds the window handle. Neither of these two methods is aimed at the second screen. The second screen actually has an independent handle. You can only find it by name. The code is as follows:

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);
 
    // Delegate to filter which windows to include
    public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
 
    /// <summary> Get the text for the window pointed to by hWnd </summary>
    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;
    }
 
    /// <summary> Find all windows that match the given filter </summary>
    /// <param name="filter"> A delegate that returns true for windows
    ///    that should be returned and false for windows that should
    ///    not be returned </param>
    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))
            {
                // only add the windows that pass the filter
                windows.Add(wnd);
            }
 
            // but return true here so that we iterate all windows
            return true;
        }, IntPtr.Zero);
 
        return windows;
    }
 
    /// <summary> Find all windows that contain the given title text </summary>
    /// <param name="titleText"> The text that the window title must contain. </param>
    public static IEnumerable<IntPtr> FindWindowsWithText(string titleText)
    {
        return FindWindows(delegate(IntPtr wnd, IntPtr param)
        {
            return GetWindowText(wnd).Contains(titleText);
        });
    }
}
 

After knowing the handle of the second screen, you can do whatever you want.

The name of the two screen window started by Unity itself is called Unity Secondary Window, because it feels that the name of the two screen window cannot be changed after checking,
The idea is to open program a, find the window handle named Unity Secondary Window and save it. This is the handle of the two screen window of A. Then open window B and pass the handle information of a to B. B excludes this when looking for it, and then finds the handle of the second screen window of B, so that both AB programs can freely control their second screen.

Or use the method of changing the window title

[DllImport("user32.dll")]
static extern int SetWindowText(IntPtr hWnd, string text)

Reference articles

Run another application in backround without lose focus and minimize (silent run in background)

https://www.pinvoke.net/search.aspx?search=BringWindowToTop&namespace=[All]

Topics: Unity