# 脚本自定义书本

# 1.概述

书本界面本质上也是个ScreenNode控件,用户可以通过获取UI节点的接口来获取它:

import mod.client.extraClientApi as clientApi
# 其中参数 nameSpace = "BookMod",uiKey = "book_screen.main"
node = clientApi.GetUI("BookMod", "book_screen.main")
print node

在自定义书本框架中,一个章节由每一个特定类型的页面组成,而组成一个页面的UI元素我们又称之为组件,下图简单的展示了何为页面,何为组件。

pageAndComps

按照功能定义,书本的三个层级“书本”,“目录”,“章节”决定了每一个页的排版(比如双页结构的书本,第1页就会放置在书本的左侧,第2页会放置在书本的右侧)以及每一个页是否能被显示。

页面则相当于组件的容器,对内在的组件按需排版,而组件是实际存放数据和处理显示数据的单位,所谓的数据实际就是开发者定义的书本json文件,经由页面获取后分配给其内置的组件。

dataFlowInBook
以上图的图片页为例子显示数据流向。

# 2.自定义页面

# 1.创建页面

在预设的页面中(比如图片页,文本页等)普遍含有标题的处理,这里我们先编写个没有标题处理的页面,学习通用的页面是如何编写,其次再做类似预设页面的标题处理,下面以CustomBookMod中的页面类型"MyNoTitlePage"为例说明。

noTitleTestPage
自定义的一个页面

在编写脚本类之前,我们先将数据定义在”behavior_pack/customBooks/customBook/entry/noTitlePage.json“这一文件中,对于一个自定义页面,除了"type"属性外,其他属性均可以自定义(由后续编写的脚本类来解析),"type"值为要编写的页面的名称(后面注册页面的时候会详述),这里自定义了"testTitle","image","content1","content2"四个属性,后面编写类的时候我们将分别用文本组件,图片组件,文本组件,文本组件去存储显示。

myNoTitlePage.json

{
    "parent": "pages",
    "icon": "textures/items/bed_lime",
    "title": "测试页No Title",
    "lockMsg": "需解锁固定成就",
    "lockIcon": "textures/ui/book_gui/icon02",
    "isLocked": false,
    "pages":[
        {
            "type": "CustomMod:MyNoTitlePage",
            "testTitle": "标题文本",
            "image": "textures/ui/myCustomBook/testImage",
            "content1": "第一段文本",
            "content2": "第二段文本"
        }
    ]    
}

CustomBookMod中,与myNoTitlePage.json相对应的脚本类是“behavior_pack/tutorialScripts/pages/noTitlePage.py”中的MyNoTitlePage,一个页面需要编写3个基本的函数接口:

  • 类初始化函数 "__init__"

  • 获取json传过来的数据的函数"SetData"

  • 显示页面的函数"Show"

    这些接口需符合链式调用的格式,最终返回selfMyNoTitlePage实现的是最基本的页面,所以这里继承是BasePageBasePage不包含对标题的预处理,如果需要对标题进行预处理,则需要继承TitlePage,后续会再提到该页面。BasePage的接口详见 “页面API”。另外,下面的代码中含有很多预设组件的用法,详细请参考“组件API”

myNoTitlePage.py

#-*- coding: UTF-8 -*- 

import mod.client.extraClientApi as clientApi
# 获取书本管理对象,详细用法见“05-常见脚本对象”
bookManager = clientApi.GetBookManager()
# 获取书本配置常量,详细API见“05-常见脚本对象”
bcf = bookManager.GetBookConfig()
# 获取页面基类 BasePage
BasePage = bookManager.GetBasePageCls()
# 获取预设组件类 TextComp
TextComp = bookManager.GetTextCompCls()
# 获取预设组件类 ImageComp
ImageComp = bookManager.GetImageCompCls()

class MyNoTitlePage(BasePage):
    """
        自定义的页类
    """
    def __init__(self, size = None, position = None):
        """"
            编写该接口需按照如下编写。
            1. 先调用父类的同名方法。
            2. 定义需要用到的组件。
            3. 调用AddComps接口添加所有组件。
        """
        # 调用父类__init__方法
        BasePage.__init__(self, size, position)
        
        # 实例化需要用到的组件
        self.testTitle = TextComp(bcf.TextAlign.Fit_Center)  # 使用文本组件存储显示自定义属性"testTitle""
        self.content1 = TextComp(bcf.TextAlign.Left)  # 使用文本组件存储显示自定义属性"content1""  
        self.image = ImageComp()  # 使用图片组件存储显示自定义属性"image""    
        self.content2 = TextComp(bcf.TextAlign.Left)  # 使用文本组件存储显示自定义属性"content2""  

        # 调用AddComps接口添加所有定义了的组件
        self.AddComps(self.testTitle, self.content1, self.image, self.content2)

        # 照应SetData方法
        self.data = None


    def SetData(self, data):
        """
            书本在调用Show之前会调用该方法将json中的数据打包成Dict变量"data"作为参数传进来。
            建议是将数据存储下来然后在Show方法中进行读取。
        """
        # 存储各个组件的数据
        self.data = data
        return self

    def Show(self):
        """
            书本向页面传递数据后调用该接口,这里负责为每个组件填充数据并且排版它们的位置。
            编写该接口需按照如下编写。
            1. 向所有组件注入数据。
            2. 调用父类的同名方法。
            3. 重置所有组件相对于页面的位置。
            4. 对组件进行排版(通过调用组件的排版API,每个API的解释见“03-组件API”)。           
        """
        if self.data:
            # 为每个组件注入数据,data中的键值对应json文件中的页面属性值
            self.testTitle.SetDataBeforeShow(self.data["testTitle"], bcf.TextSize.title)
            self.content1.SetDataBeforeShow(self.data["content1"], bcf.TextSize.content)
            self.content2.SetDataBeforeShow(self.data["content2"], bcf.TextSize.content)
            self.image.SetDataBeforeShow(self.data["image"])

            # 执行父类的Show方法,该方法会依次调用会所有组件的Show方法
            BasePage.Show(self)
            # 重置所有组件相对页面的位置,保证每次调用Show的时候组件的排版都是相对于页面的原点
            self.ResetCompsPosition()   
            # 获取该页面的中心坐标和大小,方便后面使用
            pageCenter = self.Center()    
            pageSize = self.GetSize() 
                   
            # 布局自己的组件,组件在调用Align以及Move方法前需要先调用SetSize方法,因为这些方法都是基于GetSize来计算的。
            # testTitle 是Fit类型文本,会根据文本内容来调整自己的大小,无需调用SetSize。将其中心对齐到页面的中心,将其上边界与 页面 的上边界对齐,然后往下移动4px
            self.testTitle.AlignCenterToX(pageCenter[0]).AlignTopToY(self.Top()).MoveY(4)
            # 对 content1 设置大小后,将其左边界对齐到页面的左边界,将其上边界与 testTitle 的下边界对齐,然后往下移动5px
            self.content1.SetSize((pageSize[0], 20)).AlignLeftToX(self.Left()).AlignTopToY(self.testTitle.Bottom()).MoveY(5)
            # 对 image 设置大小后,将其左中心对齐到页面的中心,将其上边界与 content1 的下边界对齐,然后往下移动5px
            self.image.SetSize((80, 80)).AlignCenterToX(pageCenter[0]).AlignTopToY(self.content1.Bottom()).MoveY(5)
            # 对 content2 设置大小后,将其左边界对齐到页面的左边界,将其上边界与 image 的下边界对齐,然后往下移动5px
            self.content2.SetSize((pageSize[0], 40)).AlignLeftToX(self.Left()).AlignTopToY(self.image.Bottom()).MoveY(5)
            
            return self
        else:
            print "in MyNoTitlePage Show: no data present"
            return self

MyNoTitlePage这个页中,其实是包含了简单的文本标题处理(self.testTitle这个文本组件),而本书提供了TitlePage这个类为开发者省去对标题的处理,详述见 “页面API”,以及Demo中的“behavior_pack/customBooks/customBook/entry/myTitlePage.json”以及“behavior_pack/tutorialScripts/pages/myTitlePage.py”。

# 2.注册页面

创建好自定义的页面以及对应的json文件后,需要系统中注册你的自定义页面,使系统在解析json文件的时候能读取到类信息。仅需要在客户端System初始化的时候调用注册接口即可,BookManager包含了自定义书本的各种接口,详见BookManager API。因为自定义书本系统中内置了一些常用的页面类型,所以开发者尽量使用规范的名称来命名自己的页面类型,这里建议使用固定格式"Mod名称:页面类型名称",其中页面类型名称建议就和定义的类名是一致的(类名后置如果带上“page”会更加有辨识度),比如上面的则MyNoTitlePage可以命名为"CustomMod:MyNoTitlePage",这个自定义的页面类型名称,是用于json数据中对应的type字段属性值。

# -*- coding: utf-8 -*-

# 获取客户端引擎API模块
import mod.client.extraClientApi as clientApi
# 获取客户端system的基类ClientSystem
ClientSystem = clientApi.GetClientSystemCls()

# 在modMain中注册的Client System类
class TutorialClientSystem(ClientSystem):

    # 客户端System的初始化函数
    def __init__(self, namespace, systemName):
        # 首先初始化TutorialClientSystem的基类ClientSystem
        super(TutorialClientSystem, self).__init__(namespace, systemName)
        print "==== TutorialClientSystem Init ===="
        # 注册自定义的page
        # 先import定义好的页面类
        from tutorialScripts.pages.myNoTitlePage import MyNoTitlePage
        from tutorialScripts.pages.myTitlePage import MyTitlePage
        from tutorialScripts.pages.customCompPage import MyCustomCompPage
        from tutorialScripts.pages.buttonPage import MyButtonPage
        from tutorialScripts.pages.addrPage import MyAddrPage
        from tutorialScripts.pages.recyclePage import MyRecyclePage
        # 获取书本管理对象,详细用法见“05-常见脚本对象”
        bookManager = clientApi.GetBookManager()
        # 注册自定义的页面类,同时为它们命名,这些名称是在json中使用的
        bookManager.AddPageType("CustomMod:MyNoTitlePage", MyNoTitlePage)
        bookManager.AddPageType("CustomMod:MyTitlePage", MyTitlePage)
        bookManager.AddPageType("CustomMod:MyCustomCompPage", MyCustomCompPage)
        bookManager.AddPageType("CustomMod:MyButtonPage", MyButtonPage)
        bookManager.AddPageType("CustomMod:MyAddrPage", MyAddrPage)
        bookManager.AddPageType("CustomMod:MyRecyclePage", MyRecyclePage)
        print "============== add MyTestPage success =============="
 

    # 函数名为Destroy才会被调用,在这个System被引擎回收的时候会调这个函数来销毁一些内容
    def Destroy(self):   
        pass

# 3.如何供其他人使用

其他开发者若想使用你的自定义页面,只需要加载你的MOD,然后在配置自定义书本的json的时候使用你的页面类型名称即可。

# 3.自定义组件(进阶)

该内容为进阶向话题,如果开发者不满足于系统提供的预设组件,则可以通过这里学习如何自定义组件。需注意以下两点:

  • 在阅读此处前请先掌握界面与交互MC Studio的使用以及对各类UI控件BaseUIControl)的熟悉,UI json文件的编写。

  • 目前书本仅支持使用这些控件来封装组件,关于它们的介绍具体见UI说明文档

    • Label
    • Image
    • Button
    • Panel
    • StackPanel
    • NeteasePaperDoll
    • ItemRenderer
    • ProgressBar

在自定义书系统中,是通过加载UI json文件的方式来获取UI控件对象,而获取到的UI控件对象又被封装成组件使用,我们在CustomBookMod中定义了一个自定义组件MyCustomComp,它的脚本文件为”behavior_pack/tutorialScripts/comps/customComp.py“,与该组件相关的UI json文件为”resource_pack/ui/CustomComp.json“,并且又自定义了一个页面来使用组件MyCustomComp,它的脚本文件为“behavior_pack/tutorialScripts/pages/customCompPage.py”,然后我们用一个章节来调用该页面,数据定义在”behavior_pack/customBooks/customBook/entry/customCompPage.json“文件中,最终该章节的页面展示结果如下。

自定义组件效果图
该自定义组件可以填充图片,文本,设置文本字体大小,而该页面有两个这样的组件,分别位于左上和右下。

# 1.定义UI json文件

界面与交互一样, 我们可以通过MC Studio来创建一个组件的UI模板,也可以按照UIjson文件规范来编写UIjson文件,在本例中我们使用了MC Studio创建一个名为testComp的UI控件,它将作为自定义组件MyCustomComp的封装对象,同时,该UI json文件需要满足以下的节点结构才能供组件使用,我们将comps下的所有UI控件节点的集合称为组件的UI模板库

需注意!请不要将该json文件起名为“book_preset.json”,该名字已被预设模板使用。

main (名字固定,必须为"main")
	| comps (panel控件,名字固定,必须为"comps")
		| testComp (组件要使用的UI的根节点,本例中为 testComp,并且该节点的控件类型无规定)
		| ...

你可以在comps层级下创建多个UI根节点,但是一个根节点只会和一个组件进行绑定。

并且为了让自定义组件坐标系与预设组件坐标系一致,comps节点的的大小必须设置如下

comps节点属性

对应到UI json文件中的属性为 "size" : [ "100.0%+0.0px", "100.0%+0.0px" ]

   "comps" : {
      "alpha" : 1.0,
      "anchor_from" : "center",
      "anchor_to" : "center",
      "clip_offset" : [ 0, 0 ],
      "clips_children" : false,
      "controls" : [
         {
            "testComp@CustomComp.testComp" : {}
         }
      ],
      "enabled" : true,
      "layer" : 0,
      "max_size" : [ 0, 0 ],
      "min_size" : [ 0, 0 ],
      "offset" : [ 0, 0 ],
      "priority" : 0,
      "propagate_alpha" : false,
      "size" : [ "100.0%+0.0px", "100.0%+0.0px" ],
      "type" : "panel",
      "visible" : true
   }

并且Comps下的子节点(比如testComp)的锚点坐标系必须设置如下图:

testComp节点属性

对应到UI json文件中的属性为如下:

"anchor_from" : "top_left",
"anchor_to" : "top_left",

# 2.创建组件类

类似页面,一个组件类需要编写3个基本的函数接口:

  • 类初始化函数 "__init__"

  • 将从页面传过来的数据存储下来的方法 "SetDataBeforeShow"

  • 显示组件的函数 "Show"

    这些接口需符合链式调用的格式,最终返回selfMyCustomComp实现的是一个图文控件,可以根据传入的数据更改文本大小,颜色,内容,图片。

    这里继承的父类是BaseComp,详细请参考“组件API”

customComp.py

#-*- coding: UTF-8 -*- 

import mod.client.extraClientApi as clientApi
# 获取书本管理对象,详细用法见“05-常见脚本对象”
bookManager = clientApi.GetBookManager()
# 获取书本配置常量,详细API见“05-常见脚本对象”
bcf = bookManager.GetBookConfig()
# 获取组件基类 BaseComp
BaseComp = bookManager.GetBaseCompCls()

class MyCustomComp(BaseComp):
    """
        自定义的书本组件类
    """    
    def __init__(self):
        """
            编写该接口需按照如下编写。
            1. 先调用父类的同名方法。
            2. 定义自定义的属性           
        """
        # 调用父类__init__方法,该方法会注册书本自定义组件名称,因此该方法只能调用一次(一个组件绑定一个控件节点)。
        # 第1个参数为组件的注册名称,为了防止重名,建议名字格式为 'mod名称:任意名称',最好就是带“Comp”后缀。
        # 第2个参数为组件所封装的UI控件所在的json文件名+'.main'
        # 第3个参数为组件所封装的UI控件节点名称
        # 通过第2,3个参数可以让组件定位到所要封装的UI控件节点,更详细的内容可见 "组件API" 章节。
        BaseComp.__init__(self, "CustomMod:MyCustomComp", "CustomComp.main", "testComp")

        # 定义自定义的属性,这些都是向页面暴露的属性
        self.text = None                # 组件的文本字符串,类型为str
        self.textSize = None            # 组件的文本大小,类型为int
        self.textColor = None           # 组件的文本颜色,类型为tuple(4)
        self.image = None               # 组件的图片路径,类型为str


    def SetDataBeforeShow(self, image, text, textColor, textSize):
        """
            页面在调用组件的Show之前默认会先调用该方法进行数据的存储
            建议是将数据存储下来然后在Show方法中进行读取。
        """       
        # 存储页面传过来的数据
        self.text = text
        self.textSize = textSize
        self.textColor = textColor
        self.image = image   
 
        return self


    def Show(self):
        """
            页面向组件传递数据后调用该接口,组件在这里对UIControl节点进行操作。
            编写该接口需按照如下编写。
            1. 调用父类的同名方法。
            2. 按照特定逻辑处理你的UIControl节点         
        """        
        # 执行父类的Show方法,执行该方法后组件才能得到对应的UI控件节点并用_node_属性指向它。
        BaseComp.Show(self)

        # 往UI控件节点填充数据,_node_为组件所对应的UI控件节点(本demo中为 testComp,即__init__方法中的第3个参数)。
        textNode = self.GetRootUINode().GetChildByPath("/text").asLabel()
        # 如果是LabelUIControl节点,设置文本和字体大小时需要分别调用 SetNodeText 和 SetTextFontSize,具体可见 "组件API" 章节。
        self.SetNodeText(textNode, self.text).SetNodeTextFontSize(textNode, 10, self.textSize)
        textNode.SetTextColor(self.textColor)
        imageNode = self.GetRootUINode().GetChildByPath("/image").asImage()         
        imageNode.SetSprite(self.image)                                    
        return self

本质上组件就是一个带数据的UI控件节点,当组件调用Show的时候,自定义书系统将UI模板中对应的节点拷贝到组件中,所以如果组件没调用Show而调用了它里面的UI控件或者该UI控件下的子节点都是会报错的。

尽管大部分编写的节点都存在拷贝操作,但是可以在初始化节点的时候设置为可回收,防止UI控件节点的过度拷贝和删除,具体可见“组件API” 章节

入门

分钟

1.概述

2.自定义页面

1.创建页面

2.注册页面

3.如何供其他人使用

3.自定义组件(进阶)

1.定义UI json文件

2.创建组件类