IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 游戏开发 -> Unity桌面二屏程序之间的交互 -> 正文阅读

[游戏开发]Unity桌面二屏程序之间的交互

遇到了三个开发需求,如下

  • 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 = @"D:\Unity Projects\Experiment\ExternalCall\TestExternalCallerA\Pack\TestExternalCallerA.exe";
    //
    // private const string exeName = "TestExternalCallerA.exe";
    
    //要唤起的exe路径格式形如下
    private const string exePath = @"C:\Users\admin\Desktop\FengHuoXiange9\JTShow3D\Fenghuoxiange.exe";

    
    /// <summary>
    /// 用于比较进程中遍历的名字
    /// 不要带有.exe后缀
    /// </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>
/// 跳转到另一个exe
/// 当当前已经有在运行的该exe程序时,则不重新开启
/// 而是直接跳转
/// </summary>
 class ExternalShowExe 
{
    private Process myprocess;
    
    /// <summary>
    /// 传输数据
    /// </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("出错原因:" + 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
            {
            //CommandLineArgs[1]是所传参数
            }
        }
    }
}

唤起程序能立刻表现出来,无延迟,看起来像是在一个exe

这里唤起的exe叫A,被唤起的exe叫B

想了三种方法:

  1. 用bat的方式先打开B再打开A
  2. 用Unity只导出windwos工程,在A的windows工程里面找到开端的函数在最开始的时候进行B的唤起
  3. 写一个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()
    {
		
        //枚举SelfService进程到一个进程数组
        foreach(Process myproc in Process.GetProcessesByName("TestWindowOpen"))
        {
            if(myproc.MainWindowHandle.ToInt32() == 
               GetForegroundWindow().ToInt32())
            {
                print(" 111111111 ");
                
            }
            else
            {
                //SelfService进程的主窗口不是活动窗口
                //do what you want here...
            }
        }

这段代码保证了能检测到某Unity的exe程序是否展现在了平面上

参考

如何判断某个窗口已经成为活动窗口?

最小化代码

因为启动的顺序和出现的顺序不敢保证是一致的,所以需要在B中进行最小化。
以及B程序在退出的时候相对于发消息给A程序让A显示,直接最小化是更简便的做法,所以最小化是需要的

一开始百度和谷歌搜索的"exe不显示" “exe不弹出 ” “windows 不弹出” “exe程序不出现” 都搜不出来
QQ群友突然提出最小化这个名词

换个名字搜 叫做最小化 然后 Unity最小化

搜到了是可以
在这unity 窗口最小化

Unity打出来的exe如果要获取一些窗口相关的信息 user32.dll是少不了的

Unity3D中调用Windows窗口句柄[DlImport(&#34user32.dll&#34)]实现去Windows边框、窗口最大/最小化、获取状态栏高度等

public class Minimize 
{
    
    //通过非托管方式导入dll。这里是导入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; //{最小化, 激活}  
    const int SW_SHOWMAXIMIZED = 3;//最大化  
    const int SW_SHOWRESTORE = 1;//还原  

    private void Awake()
    {
        OnClickMinimize();
    }


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


    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
{
    //通过非托管方式导入dll。这里是导入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);
    }
    
}

唤起exe的时候的双屏唤起

因为两个交互的exe都是双屏的,用普通的process类对象.Start方法进行已存在进程的即刻唤起以及minimize等方法的话,
双屏的exe的第二屏可能会随机出现,所以需要能保证在第二个exe出现的时候,它的第二屏一定能出现

那么有两种方法:

  1. 在exe被唤起的时候,对被唤起的exe的第二屏强制显示
  2. 在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;
    
    // 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);
                //宽高设置为0是不会起作用的
                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;
            }
        }
        
        
    }
}

工程配置如下
在这里插入图片描述
工程链接

但是到了实际项目中的两个工程中的时候都出现了问题。即使相关的分辨率和显示设置和代码都一模一样。
一个是最小化的时候不能显示桌面,而是虽然不显示二屏相机了但却变成了显示一片灰色
一个是不能最小化,只对最大化刷新二屏有反应。
最后测试发现在这样的设置中,
只有让第二屏全屏的代码在两个实际项目工程有效,如下

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

这个方法在调用之后要配合用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);
 
    // 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);
        });
    }
}
 

知道二屏的句柄之后想做什么就都可以了。

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]

  游戏开发 最新文章
6、英飞凌-AURIX-TC3XX: PWM实验之使用 GT
泛型自动装箱
CubeMax添加Rtthread操作系统 组件STM32F10
python多线程编程:如何优雅地关闭线程
数据类型隐式转换导致的阻塞
WebAPi实现多文件上传,并附带参数
from origin ‘null‘ has been blocked by
UE4 蓝图调用C++函数(附带项目工程)
Unity学习笔记(一)结构体的简单理解与应用
【Memory As a Programming Concept in C a
上一篇文章      下一篇文章      查看所有文章
加:2022-03-06 13:29:43  更:2022-03-06 13:30:38 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/27 16:38:02-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码