你的位置:首页 > ASP.net教程

[ASP.net教程]NanUI for Winform 使用示例【第一集】——山寨个代码编辑器


NanUI for Winform从昨天写博客发布到现在获得了和多朋友的关注,首先感谢大家的关注和支持!请看昨天本人的博文《NanUI for Winform发布,让Winform界面设计拥有无限可能》。

有朋友问到我是否考虑开源NanUI,我想回答是肯定的。但是目前来看NanUI内部还有一些问题并没有得到解决,因此暂时不会开放源代码。待这些问题顺利解决之后我会第一时间把NanUI的源码放到GitHub,请稍等片刻。

那么,从今天起我会陆续放出一些NanUI使用的一些小示例和源代码供感兴趣的朋友参考把玩。任何关于NanUI的问题欢迎大家进群(群号:241088256)或留言与我交流。

下面,开始今天的示例

NanUI for Winform 使用示例【第一集】

山寨个代码编辑器

去年微软破天荒的发布了个吊炸天的开源代码编辑器VS Code,观看VS Code的目录结构可知,其实它也是基于CEF来进行开发的,使用的是名为electron的框架。那么,下面我将用NanUI山寨一个简单的Code编辑器实现对代码文件的新建、打开和保存操作,取名为NanUI代码编辑器。

NanUI代码编辑器使用的核心技术有:

  • Bootstrap
  • CodeMirror
  • NanUI for Winform(我晓得,这句话是废话)
  • 从分离的资源程序集加载资源

Bootstrap做响应式的页面吊炸天,虽然我们今天要进行的小示例用不到响应式布局,但是引用进来就当CSS Clear用吧。另外一个CodeMirror作为网页端最强大的代码编辑器,这次通过NanUI,我们的Winform也将享受它带来的强大功能。下面,我将分步讲解如何来山寨这个代码编辑器。

在VS中新建Windows Application项目(后面称为主项目),然后在项目->属性->调试中关闭“启动VS承载进程”选项,因为经过实践,开启该选项后无法加载嵌入的网页资源。同时,开启“启用本机代码调试选项”,因为ChromiumFX使用了PInvoke的方式调用,会有很多莫名其妙的非托管错误,例如,我之前就遇到个只要启动项目就报错的问题,开启了本机代码调试后发现是QQ拼音输入法钩子的问题,点个忽略继续就可以正常调试了。设置好后引用NanUI的库NetDimenison.NanUI.dll

 

再新建一个类库项目(后面称为资源项目),在里面建立文件夹www,文件夹名字没有要求,随意就好,但要强调一点,html文件不能在类库项目的根目录下,必须建立个文件夹来放置网页文档。将bootstrap和codemirror的html、css和js文件等拷贝进www目录,当然你也可以直接从nuget上下载它们,只是需要把nuget拿到的文件都拖到www里面,形成下面的文件结构。

剔除掉用不着的文件,从项目中排除或直接删除都行,剩下的需要用的项目都在属性窗口中把生成操作改成“嵌入的资源”。然后新建个静态类,名字随便取,里面新建个方法来暴露资源项目的Assembly。

namespace NanUI.Demo.CodeEditor.Resources{  public static class SchemeHelper  {    public static System.Reflection.Assembly GetSchemeAssembley()    {      return System.Reflection.Assembly.GetExecutingAssembly();    }  }}

新建这个类的作用是方便主项目注册资源项目里面的程序集,如果用这种方法来注册资源文件需要在主项目中引用资源项目。另外一个方法,可以直接在主项目中直接使用Assembly.LoadFile加载资源项目,如果项目需要经常更新的话用这个方法可以做到随时更新资源文件而不用重新安装整个软件,具体的用法会在将来的示例中介绍,在此就不多说了。

如此这般,资源项目就弄好了。接下来的工作都将在主项目上进行了。在主项目的main函数里初始化NanUI。

namespace NanUI.Demo.CodeEditor{  using NetDimension.NanUI;  static class Program  {    /// <summary>    /// 应用程序的主入口点。    /// </summary>    [STAThread]    static void Main()    {      Application.EnableVisualStyles();      Application.SetCompatibleTextRenderingDefault(false);      UIStartupManager.UseSharedFramework = true;      if (UIStartupManager.InitializeChromium())      {        //初始化成功,加载程序集内嵌的资源到运行时中        UIStartupManager.RegisterEmbeddedScheme(Resources.SchemeHelper.GetSchemeAssembley());                //启动主窗体        Application.Run(new EditorForm());      }          }  }}

其中,Resources.SchemeHelper.GetSchemeAssembley()方法就是从资源项目里面把Assembly加载过来。之后新建编辑器的主窗体EditorForm。EditorForm除了简单的设置外,需另外添加几个方法来控制文件的新建、打开、保存等操作。

namespace NanUI.Demo.CodeEditor{  using NetDimension.NanUI;  public partial class EditorForm : HtmlUIForm  {    internal bool isClean = true;    internal bool isNew = true;    internal string currentFilePath = string.Empty;    public EditorForm()      : base("embedded://www/main.html")    {      InitializeComponent();      UI.GlobalObject.Add("hostEditor", new JsCodeEditorObject(this));    }    void SetEditorMode()    {      if (!string.IsNullOrEmpty(currentFilePath))      {        var fileInfo = new System.IO.FileInfo(currentFilePath);        var ext = fileInfo.Extension;        if (ext.IndexOf('.') == 0)        {          ext = ext.Substring(1);          UI.ExecuteJavascript($"CodeEditor.changeCodeScheme('{ext}');");        }      }    }    //设置编辑器标题逻辑    void SetEditorTitle()    {      if (isNew || string.IsNullOrEmpty(currentFilePath))      {        UI.ExecuteJavascript($"CodeEditor.setTitle('新建');");      }      else      {        var fileInfo = new System.IO.FileInfo(currentFilePath);        UI.ExecuteJavascript($"CodeEditor.setTitle('{fileInfo.Name}');");      }    }    //保存文件逻辑    internal bool SaveFile()    {      var result = false;      UI.EvaluateJavascript(@"CodeEditor.getContent()", (value, ex) =>      {        if (ex == null)        {          var content = value.IsString ? value.StringValue : null;          if (content != null)          {            if (isNew)            {              var saveFileDialog = new SaveFileDialog()              {                AddExtension = true,                Filter = "支持的文件|*.txt;*.js;*.cs;*.html;*.htm;*.css;*.h;*.cpp;*.php;*.",                OverwritePrompt = true              };              if (saveFileDialog.ShowDialog(this) == DialogResult.OK)              {                currentFilePath = saveFileDialog.FileName;                result = true;              }            }            if (result)            {              System.IO.File.WriteAllText(currentFilePath, content, Encoding.UTF8);              isClean = true;              SetEditorMode();              SetEditorTitle();            }                      }        }      });      return result;    }    //新建文件逻辑    internal void NewFile()    {      var continueFlag = true;      if (!isClean)      {        var ret = MessageBox.Show(this, "文件已经更改,是否保存下先?", "提示", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question);        if (ret == DialogResult.Yes)        {          if (!SaveFile())          {            continueFlag = false;          }        }        else if (ret == DialogResult.Cancel)        {          return;        }      }      if (!continueFlag)      {        return;      }      isNew = true;      isClean = true;      UI.ExecuteJavascript(@"CodeEditor.setNew()");      SetEditorTitle();    }    //打开文件逻辑    internal string OpenFile()    {      var continueFlag = true;      if (!isClean)      {        var ret = MessageBox.Show(this, "文件已经更改,是否保存下先?", "提示", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question);        if (ret == DialogResult.Yes)        {          if (!SaveFile())          {            continueFlag = false;          }        }        else if (ret == DialogResult.Cancel)        {          return null;        }      }      if (!continueFlag)      {        return null;      }      var content = string.Empty;      var openDialog = new OpenFileDialog()      {        AddExtension = true,        Filter = "支持的文件|*.txt;*.js;*.cs;*.html;*.htm;*.css;*.h;*.cpp;*.php;*."      };      if (openDialog.ShowDialog() == DialogResult.OK)      {        currentFilePath = openDialog.FileName;        var fileInfo = new System.IO.FileInfo(currentFilePath);        content = System.IO.File.ReadAllText(fileInfo.FullName);        isNew = false;        SetEditorMode();        SetEditorTitle();      }      else      {        content = null;      }      return content;    }  }}

 

下面重点来了,以下内容将涉及C#操作网页内JS代码以及JS调用C#的方法等等,核心就是中间对象JsCodeEditorObject。该对象继承自ChromiumFX的JSObject对象,对JS有了解的朋友不会对JS的Object对象感到陌生,这里的JSObject对象和JS的Object对象其实很相似,也是包含了数据和方法的一个集合。如下所示,在其中注册了多个Function,这些Function都是能够被网页端js调用的。

namespace NanUI.Demo.CodeEditor{  using Chromium.Remote;  using NetDimension.NanUI;  class JsCodeEditorObject : JSObject  {    EditorForm parentForm;        internal JsCodeEditorObject(EditorForm parentForm)    {      this.parentForm = parentForm;            AddFunction("newFile").Execute += JsCodeEditorObject_ExecuteNew;      AddFunction("openFile").Execute += JsCodeEditorObject_ExecuteOpen;      AddFunction("saveFile").Execute += JsCodeEditorObject_ExecuteSave;      AddFunction("setClean").Execute += JsCodeEditorObject_ExecuteSetClean;    }    private void JsCodeEditorObject_ExecuteSetClean(object sender, Chromium.Remote.Event.CfrV8HandlerExecuteEventArgs e)    {      if (e.Arguments.Length > 0)      {        parentForm.isClean = e.Arguments.First(p => p.IsBool).BoolValue;      }    }    private void JsCodeEditorObject_ExecuteSave(object sender, Chromium.Remote.Event.CfrV8HandlerExecuteEventArgs e)    {      var result = parentForm.SaveFile();      e.SetReturnValue(CfrV8Value.CreateBool(result));    }    private void JsCodeEditorObject_ExecuteOpen(object sender, Chromium.Remote.Event.CfrV8HandlerExecuteEventArgs e)    {      var result = parentForm.OpenFile();      if (result != null)      {        e.SetReturnValue(CfrV8Value.CreateString(result));      }      else      {        e.SetReturnValue(CfrV8Value.CreateNull());      }    }    private void JsCodeEditorObject_ExecuteNew(object sender, Chromium.Remote.Event.CfrV8HandlerExecuteEventArgs e)    {      parentForm.NewFile();    }  }}

如上面代码中所展示的,这个类注册了newFile, openFile, saveFile和setClean几个方法供网页端js调用。再折回到上面EditorForm的代码,有这么一行:

UI.GlobalObject.Add("hostEditor", new JsCodeEditorObject(this));

这行代码的左右就是将我们的中间对象JsCodeEditorObject实例化后传到浏览器的JS环境中,并且取了个新名字叫做“hostEditor”,这样,我们就能够在网页环境中用js执行下面的几个方法了。

  • hostEditor.newFile
  • hostEditor.openFile
  • hostEditor.saveFile
  • hostEditor.setClean

当网页端js调用上面这些方法的时候实际上会执行到EditorForm中相应的操作逻辑,如此就实现了js与c#环境的交互。C#与js环境交互与之相比要简单得多,例如EdiorForm中的SetEditorTitle方法中,通过UI.ExecuteJavascript方法就可以执行web环境中的js代码或方法。

UI.ExecuteJavascript($"CodeEditor.setTitle('新建');");

如上这行代码,调用了js的方法CodeEditor.setTitle来设置编辑器的标题。那么,如果需要执行js代码并返回相应代码,就得使用UI.EvaluateJavascript方法。该方法第一个参数为js代码,第二个参数为执行js代码后的回调,它是一个有两个参数的action对象,第一个参数为回调的返回值,第二个参数是exception对象,如果js代码有问题,那么第二个对象exception就包含了错误信息,正确执行时,excepiton对象返回null。

UI.EvaluateJavascript(@"CodeEditor.getContent()", (value, ex) =>{  if (ex == null)  {    var content = value.IsString ? value.StringValue : null; //value对象是CfrV8Value对象,内置了各种数据转换的方法。            //其他逻辑  }});

 

有了上面的代码要点,大家应该已经清楚C#和JS之间的交互方法和JS于C#之间的交互的方式了。简而言之,C#与NanUI的js交互,无返回值的用UI.ExecuteJavascript方法,有返回值的用UI.EvaluateJavascript。除了这两个方法外,可以用UI.AddFunction方法来直接在js环境中注册C#的方法,方法大家自行研究在此不再阐述。如果js需要与C#之间的交互,通过编写继承JSObject的中间对象注册方法或数据对象,即可实现。

那么,NanUI的示例第一集就这么讲完了,如果不明白请留言给我或进群(群号:241088256)交流,感谢大家关注。

 

附件

Release - 编译后的示例,不带CEF框架,首次启动会自动下载。

Source - 源代码