SpaceVIL-用于在.Net Core,.Net Standard和JVM上开发的跨平台GUI框架

在本文中,我将尝试讨论SpaceVIL框架(Visual Items Layout的空间),该框架用于在.Net / .Net Core和JVM平台上构建用户图形界面。


SpaceVIL是一个跨平台的多语言框架,它基于OpenGL图形技术,而GLFW库负责创建窗口。 使用此框架,您可以在Linux,Mac OS X和Windows操作系统中工作和创建图形客户端应用程序。 对于C#程序员来说,目前尤其如此,因为Microsoft不会将WPF移植到其他操作系统,而Avalonia是唯一可能的类似产品。 在这种情况下,SpaceVIL的独特之处在于多语言,也就是说,.Net Core框架目前可以与以下编程语言结合使用:C#,VisualBasic。 JVM框架可以与Java和Scala语言结合使用。 也就是说,SpaceVIL可以与这些语言中的任何一种一起使用,并且所产生的代码将看起来相同,因此,当您切换到另一种语言时,无需再次学习它。


SpaceVIL仍处于Alpha阶段,但是尽管如此,该框架现在可以充分使用,因为该框架具有构建复杂UI和创建全新的可视用户元素所需的一切。 本文的目的恰恰是使您相信这一点。


SpaceVIL是从零开始开发的,这就是为什么该框架有自己的原理将其与类似物区分开的原因。


  • SpaceVIL用户可以完全控制正在发生的事情。
  • 在所有平台上,用SpaceVIL编写的任何应用程序看上去都将完全相同。 没有陷阱。 您可以使用任何版本的SpaceVIL(.Net / JVM,Mac OS X,Linux,Windows),结果和外观将始终相同。
  • JVM的SpaceVIL版本与.Net的SpaceVIL版本的使用相同
  • SpaceVIL提供了对元素进行深度自定义的机会,因为所有交互对象都是其他交互对象的容器。
  • 该框架非常灵活且易于使用,因为其中几乎没有基本的严格规则,在使用该框架之前,您唯一需要了解的就是Padding,Margin,Alignment的设置含义(并且创建简单接口的任何人都知道它们) ,在WPF,Android Studio中或在CSS中编写样式)。
  • SpaceVIL不需要您对它的内部进行任何深入研究,它将完全按照您的编写来做。 所有元素都遵循通用规则,一种方法将适用于所有元素。 记住了基础之后,就可以预测元素的组成和样式。
  • 该框架非常轻巧,不到一兆字节,并且全部集中在一个文件中。

可能性


现在,让我们看看当前版本的框架能做什么。


  • 有54个元素可供使用,其中10个是专用容器,6个原语(非交互式元素)和38个用于各种目的的交互式元素。
  • 基于所有这些元素,再加上特殊接口的实现,您可以创建自己的任何复杂性的元素。
  • 元素具有样式,可以选择创建整个样式主题或更改/替换框架中任何标准元素的样式。 到目前为止,SpaceVIL中仅存在一个主题,并且默认情况下已安装该主题。
  • 有一个国家体系。 可以为每种元素分配一种可视化状态,以使用外部影响方法之一:将其悬停在元素上,按下鼠标键,释放鼠标键,切换,聚焦和关闭元素。
  • 有事件过滤。 交互中的每个元素都可以过滤掉通过它的事件,这允许一个事件通过该元素并被另一个事件丢弃。 在一个示例中,我将更详细地介绍它。
  • 实现了浮动独立元素的系统。
  • 对话框和对话框元素的系统已实现。
  • 实现了独立渲染。 每个窗口都有两个流-一个控制渲染,另一个控制来自传入事件的任务,也就是说,该窗口现在始终继续渲染(并且不“挂起”),而与按下按钮后启动的任务无关。

结构形式


现在让我们看一下元素的结构。 框架中存在以下类型的元素:活动窗口和对话框窗口,容器,用于方便放置元素,交互元素和图元。 让我们简要地介绍一下以上所有内容。 我希望无需解释此类窗口,我只注意到对话框会阻止导致该窗口的窗口关闭。


货柜


SpaceVIL中提供了以下容器类型:


  • 通用容器(由于对齐,填充,边距,尺寸和尺寸策略参数而定位此类容器内的元素)。
  • 垂直和水平堆栈(添加到此类容器中的元素将按顺序排列,而无需微调使用先前类型的容器时所需的参数)。
  • 网格-元素被添加到网格单元中并位于其单元内部。
  • 列表(ListBox,TreeView),一个基于垂直堆栈的容器,但具有滚动显示不适合该容器的项目的能力。
  • 分隔符(SplitArea),容器可以是两种类型-垂直和水平,分隔两个区域并交互控制这些区域的大小。
  • 选项卡式容器(TabView),控制页面可见性。
  • WrapGrid将元素定位在一定大小的单元内,并根据方向填充所有可用空间,并具有滚动功能(最醒目的示例是图标显示模式下的Windows资源管理器)。
  • 最后,免费容器(可能是其中最稀有的容器)是一个无限区域,可以在其中添加任何固定大小的元素,但最好将它与ResizableItem类型的元素结合使用。

互动元素


这种类型的元素接受许多状态并具有各种事件。 如果更简单,则可以与之交互的所有内容以及交互元素,例如:按钮,复选框,用于输入文本的元素等。


原语


与交互元素相反,有原始的,完全非交互的元素,无法联系它们,它们的存在只是为了展示自己。 基本类型:三角形,矩形,椭圆形和稍微复杂一些的图元-可以采用任何复杂度形式的任意图形。


也有静态服务类。 这些类中的某些类允许程序员根据自己的喜好获取和自定义元素。 例如,有一个控制元素样式的类,以及一个可以为交互式元素设置任意显示形式的类,一个默认设置等。


使用SpaceVIL框架的简单应用程序的示例


我对示例有点热衷。 您总是可以拿出一个抽象的且能准确反映功能的示例,但是如果从侧面阅读它,则看起来总是令人难以置信,因此,作为示例,我尝试制作一些小型但现成的应用程序,以实现某种可理解的目的。


让我们继续对应用程序的描述。 该程序是《地牢与龙》等游戏的英雄卡编辑器,名称为CharacterEditor。 该程序随机生成指定数量的不同字符,这些字符具有名称,年龄,种族,性别,阶级和特征。 为用户提供了写传记和赋予角色专门技能的机会。 结果,您可以将英雄卡另存为文本文件。 让我们开始进行代码解析。 该程序用C#编写。 使用Java时,代码本质上是相同的。


结果,我们得到了这样一个应用程序:



创建应用程序窗口


至此,我们将创建一个窗口。 让我提醒您,SpaceVIL使用GLFW,因此,如果您正在为.Net平台编写应用程序,则必须将已编译的GLFW库复制到可执行文件旁边。 JVM使用GLFW库包装器(LWJGL),该包装器的组成中已包含已编译的GLFW。


接下来,在渲染区域中填充必要的元素,并为它们提供漂亮的外观。 实现此目标的主要步骤如下:


  • 在使用之前初始化SpaceVIL。 在Main函数中,只需编写:

if (!SpaceVIL.Common.CommonService.InitSpaceVILComponents()) return; 

  • 现在创建一个窗口。 为此,您需要编写一个窗口类,从SpaceVIL.ActiveWindow类继承它,描述InitWindow()方法并设置几个基本参数,例如窗口名称,标题栏文本和尺寸。 结果,我们得到如下代码:

 using System; using SpaceVIL; namespace CharacterEditor { internal class MainWindow : ActiveWindow { public override void InitWindow() { SetParameters("CharacterEditor", "CharacterEditor", 1000, 600); } } } 

  • 仅保留创建此类的实例并对其进行调用。 为此,我们用以下几行代码补充Main方法:

 MainWindow mw = new MainWindow(); mw.Show(); 

就是这样,在这个阶段,您可以启动应用程序并检查是否一切正常。


充满元素


为了实现CharacterEditor,我决定在窗口上放置标题栏,工具栏和垂直分隔符。 工具栏将包含:用于刷新新生成的字符列表的按钮,用于保存字符的按钮以及具有生成的字符数的元素。 垂直分隔符的左侧是生成的字符的列表,右侧是用于编辑从列表中选择的字符的文本区域。 为了不使窗口类的元素设置混乱,您可以编写一个静态类,为我们提供外观和设置就绪的元素。 添加时,请务必记住,每个交互元素(无论是按钮还是列表)都是容器,也就是说,您可以将按钮中的任何内容,从基元到其他容器的任何内容放入其中,任何复杂的元素都只是一组简单的元素,它们总可以一起使用一个目标。 知道了这一点,您必须记住第一个严格的规则-在将其他元素添加到元素之前,您需要将其自己添加到某个窗口类本身(在我们的情况下为MainWindow ),容器或任何其他交互式元素中。 让我们用一个例子来解释:


 public override void InitWindow() { SetParameters("CharacterEditor", "CharacterEditor", 1000, 600); //   Frame frame = new Frame(); // ,      frame ButtonCore btn = new ButtonCore("Button"); //    , //      . // frame     , //,   . frame.AddItem(btn); //  frame    AddItem(frame); } 

可以先在窗口中添加frame ,然后在frame添加一个按钮。 规则似乎很不方便,在创建复杂的窗口时您将不得不大汗淋漓,这就是为什么SpaceVIL鼓励您创建自己的元素的原因,这大大简化了添加元素的过程。 稍后,我将展示和解释我自己的元素的创建。 让我们回到该应用程序。 这是结果窗口:



现在让我们来分析代码:


MainWindow标记摘要代码
 internal ListBox ItemList = new ListBox(); //     internal TextArea ItemText = new TextArea(); //     internal ButtonCore BtnGenerate; //   internal ButtonCore BtnSave; //    internal SpinItem NumberCount; //    public override void InitWindow() { SetParameters("CharacterEditor", "CharacterEditor", 1000, 600); IsBorderHidden = true; //      IsCentered = true; //       //  TitleBar title = new TitleBar(nameof(CharacterEditor)); //     title.SetIcon( DefaultsService.GetDefaultImage(EmbeddedImage.User, EmbeddedImageSize.Size32x32), 20, 20); // ,       VerticalStack layout = ItemFactory.GetStandardLayout(title.GetHeight()); //  HorizontalStack toolbar = ItemFactory.GetToolbar(); //  VerticalSplitArea splitArea = ItemFactory.GetSplitArea(); //  BtnGenerate = ItemFactory.GetToolbarButton(); //  BtnSave = ItemFactory.GetToolbarButton(); //    NumberCount = ItemFactory.GetSpinItem(); //    ItemText.SetStyle(StyleFactory.GetTextAreaStyle()); //     AddItems(title, layout); layout.AddItems(toolbar, splitArea); toolbar.AddItems(BtnGenerate, BtnSave, ItemFactory.GetVerticalDivider(), NumberCount); splitArea.AssignLeftItem(ItemList); splitArea.AssignRightItem(ItemText); //    BtnGenerate.AddItem(ItemFactory.GetToolbarIcon( DefaultsService.GetDefaultImage(EmbeddedImage.Refresh, EmbeddedImageSize.Size32x32))); BtnSave.AddItem(ItemFactory.GetToolbarIcon( DefaultsService.GetDefaultImage(EmbeddedImage.Diskette, EmbeddedImageSize.Size32x32))); } 

ItemFactory类中ItemFactory我描述了项目的外观和布局。 例如, ItemFactory.GetToolbarButton()方法如下所示:


 internal static ButtonCore GetToolbarButton() { ButtonCore btn = new ButtonCore(); //     btn.SetBackground(55, 55, 55); // btn.SetHeightPolicy(SizePolicy.Expand); //       btn.SetWidth(30); //  btn.SetPadding(5, 5, 5, 5); //       // ,           btn.AddItemState(ItemStateType.Hovered, new ItemState(Color.FromArgb(30, 255, 255, 255))); //  ItemState   return btn; } 

其余元件以类似方式描述。


创建和应用样式


您可能已经注意到,我将样式应用于ItemText元素。 因此,让我们看一下如何创建样式,首先看一下样式代码:


 Style style = Style.GetTextAreaStyle(); //       style.Background = Color.Transparent; //   Style textedit = style.GetInnerStyle("textedit"); //     textedit.Foreground = Color.LightGray; //   Style cursor = textedit.GetInnerStyle("cursor"); //     cursor.Background = Color.FromArgb(0, 162, 232);//   

如您所见,我从SpaceVIL.Style类中为该元素SpaceVIL.Style了一种现成的样式,并SpaceVIL.Style进行了少许更改,以校正颜色。 每种样式可以包含几种内部样式,用于对每个组成复杂元素进行样式设置。 例如,一个CheckBox元素由一个容器,一个指示器和一个文本组成,因此其样式具有指示器(“指示器”)和文本(“文本行”)的内部样式。


Style类涵盖元素的所有视觉属性,除此之外,使用样式可以交互地更改元素的形状,例如,从椭圆变为矩形,反之亦然。 要应用样式,您需要在元素上调用SetStyle(Style style)方法,如上所示:


 ItemText.SetStyle(StyleFactory.GetTextAreaStyle()); 

创建自己的物品


现在让我们继续创建项目。 元素本身不必一定是特定的,它可以是一个常规堆栈,您可以在其中添加其他几个元素。 例如,在上面的示例中,我有一个工具栏,其中包含三个元素。 工具栏本身只是一个水平堆栈。 一切都可以安排为单独的元素,并称为ToolBar。 它本身不执行任何操作,但是在MainWindow类中,行数将减少,并且对标记的理解将更加容易,此外,这也是削弱第一个严格规则的一种方法,尽管最终所有事情都遵循它。 好的,我们不再触摸工具栏了。 我们需要一个用于列表的元素,以显示生成的字符。


为了使其更有趣,我们将元素的组成定义为更复杂:


  • 一个字符图标,其颜色表示属于幻想种族。
  • 文字形式的人物的姓名,姓氏和种族。
  • 字符快速帮助按钮(需要演示过滤事件)。
  • 如果不适合我们,请删除该字符的按钮。

要创建您自己的元素的类,您需要从SpaceVIL的任何交互式元素中继承它,如果其组成中至少有一些适合我们的类,但是对于当前示例,我们将从头开始组装该元素,因此,我们将从交互式元素的基本抽象类SpaceVIL.Prototype继承它。 。 我们还需要实现InitElements()方法,该方法描述元素的外观,嵌套元素的位置和类型以及添加嵌套元素的顺序。 元素本身将称为CharacterCard


让我们继续分析完成元素的代码:


字符卡物品代码
 using System; using System.Drawing; using SpaceVIL; using SpaceVIL.Core; using SpaceVIL.Decorations; using SpaceVIL.Common; namespace CharacterEditor { //   SpaceVIL.Prototype internal class CharacterCard : Prototype { private Label _name; private CharacterInfo _characterInfo = null; //      CharacterInfo, //       internal CharacterCard(CharacterInfo info) { //      //        SetSizePolicy(SizePolicy.Expand, SizePolicy.Fixed); SetHeight(30); //   SetBackground(60, 60, 60); //  SetPadding(10, 0, 5, 0); //    SetMargin(2, 1, 2, 1); //   AddItemState(ItemStateType.Hovered, new ItemState(Color.FromArgb(30, 255, 255, 255))); _characterInfo = info; //      //      Label _name = new Label(info.Name + " the " + info.Race); } public override void InitElements() { //  ImageItem _race = new ImageItem(DefaultsService.GetDefaultImage( EmbeddedImage.User, EmbeddedImageSize.Size32x32), false); _race.KeepAspectRatio(true); //   // ImageItem   _race.SetWidthPolicy(SizePolicy.Fixed); _race.SetWidth(20); // ImageItem // ImageItem       _race.SetAlignment(ItemAlignment.Left, ItemAlignment.VCenter); //  ()     switch (_characterInfo.Race) { case CharacterRace.Human: //   _race.SetColorOverlay(Color.FromArgb(0, 162, 232)); break; case CharacterRace.Elf: //   _race.SetColorOverlay(Color.FromArgb(35, 201, 109)); break; case CharacterRace.Dwarf: //   _race.SetColorOverlay(Color.FromArgb(255, 127, 39)); break; } // Label _name _name.SetMargin(30, 0, 30, 0); //    //    ButtonCore infoBtn = new ButtonCore("?"); infoBtn.SetBackground(Color.FromArgb(255, 40, 40, 40)); infoBtn.SetWidth(20); infoBtn.SetSizePolicy(SizePolicy.Fixed, SizePolicy.Expand); infoBtn.SetFontStyle(FontStyle.Bold); infoBtn.SetForeground(210, 210, 210); infoBtn.SetAlignment(ItemAlignment.VCenter, ItemAlignment.Right); infoBtn.SetMargin(0, 0, 20, 0); infoBtn.AddItemState(ItemStateType.Hovered, new ItemState(Color.FromArgb(0, 140, 210))); //   // info        infoBtn.SetPassEvents(false); //   //     info //  hover    infoBtn.EventMouseHover += (sender, args) => { SetMouseHover(true); }; //     info   //      infoBtn.EventMouseClick += (sender, args) => { //  ImageItem ImageItem race = new ImageItem(DefaultsService.GetDefaultImage( EmbeddedImage.User, EmbeddedImageSize.Size32x32), false); race.SetSizePolicy(SizePolicy.Fixed, SizePolicy.Fixed); race.SetSize(32, 32); race.SetAlignment(ItemAlignment.Left, ItemAlignment.Top); race.SetColorOverlay(_race.GetColorOverlay()); //  PopUpMessage popUpInfo = new PopUpMessage( _characterInfo.Name + "\n" + "Age: " + _characterInfo.Age + "\n" + "Sex: " + _characterInfo.Sex + "\n" + "Race: " + _characterInfo.Race + "\n" + "Class: " + _characterInfo.Class); //    3  popUpInfo.SetTimeOut(3000); popUpInfo.SetHeight(200); //   //  ,    //    popUpInfo.Show(GetHandler()); //      popUpInfo.AddItem(race); }; //   ButtonCore removeBtn = new ButtonCore(); removeBtn.SetBackground(Color.FromArgb(255, 40, 40, 40)); removeBtn.SetSizePolicy(SizePolicy.Fixed, SizePolicy.Fixed); removeBtn.SetSize(10, 10); removeBtn.SetAlignment(ItemAlignment.VCenter, ItemAlignment.Right); removeBtn.SetCustomFigure(new CustomFigure(false, GraphicsMathService.GetCross(10, 10, 2, 45))); removeBtn.AddItemState(ItemStateType.Hovered, new ItemState(Color.FromArgb(200, 95, 97))); //       removeBtn removeBtn.EventMouseClick += (sender, args) => { RemoveSelf(); //  }; //      CharacterCard AddItems(_race, _name, infoBtn, removeBtn); } internal void RemoveSelf() { //         GetParent().RemoveItem(this); } public override String ToString() { return _characterInfo.ToString(); } } } 

在示例中,我使用了一个辅助类,其中包含角色的所有基本特征,例如姓名,姓氏,种族,性别,年龄,专业等级,特征,能力和传记。 在此类中,将生成除技能和传记(假定用户将独立发明它们)之外的所有参数。


事件处理和过滤


在前面的示例中,描述了两种类型的事件:MouseHover和MouseClick。 目前只有11个基本事件,这是列表:


  • EventMouseHover
  • EventMouseLeave
  • EventMouseClick
  • EventMouseDoubleClick
  • EventMousePress
  • EventMouseDrag
  • EventScrollUp
  • EventScrollDown
  • EventKeyPress
  • EventKeyRelease
  • EventTextInput

复杂元素具有自己独特的事件,但是上述事件对所有人都可用(有保留)。


事件处理的语法很简单,看起来像这样:


 // C# item.EventMouseClick += (sender, args) => { // - }; 

 // Java item.eventMouseClick.add((sender, args) -> { // - }); 

现在让我们继续过滤事件。 默认情况下,事件通过元素金字塔。 在我们的示例中,按钮infoBtn收到在infoBtn按钮上单击按钮的事件,然后此事件将接收CharacterCard元素,然后将其放置在ListBox中,然后是SplitAreaVerticalStack ,最后它将到达基本Wontainer元素。


在每个元素上,您都可以处理EventMouseClick事件,并且EventMouseClick指定顺序执行所有这些操作,但是如果单击任何元素时我们不希望该事件在整个过程中走得更远,该怎么办? 为此,只有事件过滤。 让我们更清楚地显示,例如在CharacterCard元素上。 想象一下, CharacterCard描述了EventMouseClick事件,该事件将来自链接的CharacterInfo信息插入到用于编辑字符的文本字段中。 这种行为是合乎逻辑的-我们单击该元素,然后查看字符的所有参数。 接下来,我们编辑角色,发明传记和技能或更改特征。 在某个时候,我们希望从列表中看到有关另一个生成的字符的简要信息,然后单击infoBtn按钮。 如果我们不过滤事件,则在调用工具提示后,将在CharacterCard元素本身上执行EventMouseClick,正如我们记得的,它将在字符编辑字段中插入文本,如果不保存结果,将导致更改丢失,并且应用程序本身的行为将是看起来不合逻辑。 因此,为了仅在按钮上执行事件,我们可以使用infoBtn.SetPassEvents(false)方法设置过滤器。


如果您以这种方式调用此方法,则按钮本身将停止跳过任何事件。 假设我们不想只跳过鼠标单击事件,那么我们可以调用带有其他参数的方法,例如infoBtn.SetPassEvents(false, InputEventType.MousePress, MouseRelease)


因此,有可能在每个步骤中过滤事件,以获得所需的结果。


您可以再次查看该应用程序,最终结果是。 当然,这里省略了业务逻辑实现的细节,特别是字符的生成,其技能等等,而这些不再与SpaceVIL直接相关。 可以在指向GitHub的链接上查看完整的应用程序代码,在GitHub上已经有使用C#和Java的其他几个使用SpaceVIL的示例。


完成的CharacterEditor应用程序的屏幕截图


结论


最后,我想提醒您,该框架正在积极开发中,因此可能发生崩溃和崩溃,并且可以修改某些功能,并且最终使用结果可能与当前版本大不相同,因此,如果当前版本的框架适合您,那么请不要忘记此版本的备份,因为我不能保证新版本将向后兼容。 可以重做一些要点,以提高使用SpaceVIL的舒适性和速度,到目前为止,您还不想拖延旧的和废弃的想法。 此外,由于缺少适当的设备,因此未在AMD的视频卡上测试SpaceVIL的工作。 测试是在Intel和NVidia的视频卡上进行的。 SpaceVIL的进一步开发将集中于添加新功能(例如,当前不支持渐变)和优化。


我还想提到一个限制,在使用此技术编写跨平台应用程序时要记住一个限制-不建议使用对话框(由于渲染错误,通常在Linux中创建多窗口应用程序),对话框可以轻松地替换为对话框元素。 Mac OS X通常禁止创建多窗口应用程序,因为它要求仅在主应用程序线程中启动GUI。


您可以在以下链接中下载所需版本的框架以及所有提供的测试程序示例。 该文档的第一个版本也可在此处获得。


最后,再进行一些视觉演示。 以下是使用SpaceVIL技术编写的应用程序,这些应用程序可以使您对使用SpaceVIL可以实现的目标有所了解。


使用SpaceVIL编写的应用程序的屏幕截图






参考文献


Source: https://habr.com/ru/post/zh-CN448790/


All Articles