65.9K
CodeProject 正在变化。 阅读更多。
Home

Unity 5.6 带标签和回调的 WebView

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.20/5 (3投票s)

2017年12月26日

CPOL

5分钟阅读

viewsIcon

13853

downloadIcon

275

在Unity编辑器中实现带有选项卡窗口和从HTML回调的webview的简单方法

引言

最近有人要求我在 Unity 编辑器中实现一个 WebView。我的客户要求以下功能:

  1. 一个编辑器窗口 - 及其所有实用功能
  2. 基本的 Chromium 功能(加载 URL、加载文件、执行 JS 等)
  3. 接收来自 JS 的回调
  4. 并使其简单:0)

这项任务非常艰巨,因为它需要大量的反射以及对 Unity 回调机制的深入理解。我花了一个多月的时间才弄清楚如何正确地完成它。所以,让我帮你花不到 5 分钟搞定。

背景

Unity 有自己的 Chromium 包装器,所以你不需要使用操作系统 API 或自己实现它,尽管它可能在其他项目中很有用。此外,自己实现 Chromium 在大小方面成本非常高。如果你要分发代码,最好考虑一下。

另一个问题是,Unity 5.4 及更新版本在所有 Web 实现方面与早期版本有很大不同。我强烈建议反编译 Unity 编辑器并检查文件。

使用代码

正如我所说的,Unity 5.4 及更新版本在他们实现整个 Web-view 设计的方式方面有很大不同。

直到 5.4 版本,我才能够使用 web-view DLL(通过反射)调用其初始化并接收一个功能齐全的 Web-view 窗口,该窗口可以停靠或浮动,并且可以调用 web-view 函数,如“onLoadError”来毫无问题地接收回调……但这已不再是当前情况。

目前,仅使用“web-view”DLL 会得到一个部分功能的窗口,该窗口无法停靠或在停靠后重新浮动,这仅仅是因为 Unity 保存渲染在窗口上的对象的方式。

这一段是为那些不太了解 Chromium WebView 是什么的人准备的。让我们一步一步来(我花了几天才弄明白,所以别担心)。

总的来说,想想 Asset Store。Asset Store 窗口神奇地使用了 Unity 的 Web 功能。嗯……一旦你反编译了 Unity,你会发现根本没有魔法,只有一堆混乱的代码和大量的内部 hack,而且它没有被提供为 API 是有充分理由的。

从 Asset Store 反编译

   private void InitWebView(Rect webViewRect)
    {
      this.m_CurrentSkin = EditorGUIUtility.skinIndex;
      this.m_IsDocked = this.docked;
      this.m_IsOffline = false;
      if (!(bool) this.webView)
      {
        int x = (int) webViewRect.x;
        int y = (int) webViewRect.y;
        int width = (int) webViewRect.width;
        int height = (int) webViewRect.height;
        this.webView = ScriptableObject.CreateInstance<WebView>();
        this.webView.InitWebView((GUIView) this.m_Parent, x, y, width, height, false);
        this.webView.hideFlags = HideFlags.HideAndDontSave;
        this.webView.AllowRightClickMenu(true);
        if (this.hasFocus)
          this.SetFocus(true);
      }
      this.webView.SetDelegateObject((ScriptableObject) this);
      this.webView.LoadFile(AssetStoreUtils.GetLoaderPath());
    }

所以,要调用 Asset Store 窗口,你需要“rectangle”(矩形)、“this”参数——即实际的窗口,以及 (GUIVIEW).m_Parent——这是一个存在于 EditorWindow 中的私有成员,它保存着停靠区域。这实际上是最棘手的部分。通过反射,你并不总是能获得 m_parent。但这个参数在停靠或取消停靠窗口时是必需的。

Asset Store 通过调用 JS 进行更新来处理这个问题。

        AssetStoreContext.GetInstance().docked = this.docked;
        this.InvokeJSMethod("document.AssetStore", "updateDockStatus");

所以,长话短说,这不是处理它的正确方法。经过一些调查,我意识到我正在寻找的是 WebView 和标签式窗口之间的连接。

我发现了这个 DLL "UnityEditor.Web.WebViewEditorWindowTabs",它几乎完美。在窗口实用功能(如停靠和取消停靠)方面,它的表现符合预期,但为了接收回调或调用额外的 Chromium 功能,我需要窗口的实例、WebView 对象以及实际的窗口。遗憾的是,没有简单的方法可以获取它们。

我最终实现了自己的“Create”函数,该函数同时创建窗口并获取 WebView。这个 DLL 的好处是m_parent不是必需的,我也不需要使用“set host”反射函数。

我所做的就是

public static T CreateWebViewEditorWindow<T>(string title, string sourcesPath, int minWidth, int minHeight, int maxWidth, int maxHeight) where T : CustomWebViewEditorWindow, new(){

 var createMethod = webViewEditorWindowType.GetMethod("Create", BindingFlags.Static | BindingFlags.Public | BindingFlags.FlattenHierarchy).MakeGenericMethod(webViewEditorWindowType);

        //For a static method (such as create in UnityEditor.Web.WebViewEditorWindow),
        //pass null as the first argument to Invoke. That's nothing to do with generic methods - it's just normal reflection.
        var window = createMethod.Invoke(null, new object[] {
            title,
            sourcesPath,
            minWidth,
            minHeight,
            maxWidth,
            maxHeight
        });

        //saving the window object in webViewEditorWindow
        var customWebEditorWindow = new T { webViewEditorWindow = window };

        // attach a delegate which will execute after the editor is done updating inspectors
        EditorApplication.delayCall += () =>
        {
            EditorApplication.delayCall += () =>
            {
                //after all was updated I also take the inst of the window.
                webView = webViewEditorWindowType.GetField("m_WebView", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(customWebEditorWindow.webViewEditorWindow);
                //addgloabals is necessary for the js communication with the window
                AddGlobalObject(Type.GetType("webView"));
            };
        };

我从这个 GitHub 获得了这个绝妙的主意
https://github.com/kimsama/Unity-WebViewEditorWindow/blob/master/Assets/Editor/CustomWebViewEditorWindow.cs

是的,我复制了 WebViewEditor 反编译的“create window”模板,并获取了 WebView 对象、窗口及其实例。但我还希望这个脚本能够与 JS 对话,所以我不得不创建一个 jsproxymgr 的实例并赋予它我所有的全局变量。请参阅附件代码以获取说明。

从这一点开始,事情就容易多了,因为我拥有了处理 Chromium 所需的一切。现在我有窗口、窗口实例,以及我从 DLL 反射出的 WebView 对象。我仍然想要所有的 Chromium 功能,为此我使用了反射。例如:

    public void LoadURL(string path)
    {
        try
        {
            if (webViewEditorWindow != null && webView != null)
            {
                MethodInfo invokeLoadURLMethod = webView.GetType().GetMethod("LoadURL", BindingFlags.Static | BindingFlags.Public | BindingFlags.FlattenHierarchy | BindingFlags.NonPublic | BindingFlags.Instance);
                if (invokeLoadURLMethod != null)
                {
                    object[] param = new object[] { path };
                    invokeLoadURLMethod.Invoke(webView, param);
                }
            }
        }
        catch (TargetInvocationException ex)
        {
            // should be set as null to open the editor window again.
            webView = null;
            // force stop calling the delegate even after closing the window.
            EditorApplication.update = null;
            Debug.LogFormat("{0}", ex.Message);
            return;
        }
    }

当我实例化窗口时,使用我获取的 WebView。

所以现在我有一个可以停靠和取消停靠的 Web View 窗口的设置方法,有一个可以调用 Unity Chromium 包装器的 WebView 对象,剩下要做的就是看看如何从 HTML 获取回调到我的窗口。

为此,我使用了 unityAsync 这个机制,它类似于协程。这是一种通过 JSON 格式发送和接收来自 JS 到 Unity 的消息的良好文档化的方式。我首先调用 init 函数,并链接到 html 文件。

    [MenuItem("Window/webViewImp")]
    private static void Open()
    {
        string path = "file:///{0}/Assets/StreamingAssets/test/index.html";
        var w = CreateWebViewEditorWindow<webViewImp>("webViewImp", path, 200, 530, 800, 600);
    }

第一个参数是窗口的名称,第二个参数是完整的文件名,另外四个参数是矩形(x, y, 宽度, 高度)。请注意,我这里获取的是实际的窗口,而不是仅仅 scriptableObject 实例,使用了“var w”。当执行 JS 或加载 URL 时,这会很有用。你必须确保你的脚本中有与 HTML 中相同的函数名称,并且不要忘记将它们设置为 public。

请参阅附带的示例。

© . All rights reserved.