---
front:
hard: 入门
time: 分钟
sidebarDepth: 4
---
# 脚本自定义书本
## 1.概述
在自定义书本框架中,一个章节由每一个特定类型的页面组成,而组成一个页面的UI元素我们又称之为组件,下图简单的展示了何为页面,何为组件。
按照功能定义,书本的三个层级“书本”,“目录”,“章节”决定了每一个页的排版(比如双页结构的书本,第1页就会放置在书本的左侧,第2页会放置在书本的右侧)以及每一个页是否能被显示。
页面则相当于组件的容器,对内在的组件按需排版,而组件是实际存放数据和处理显示数据的单位,所谓的数据实际就是开发者定义的书本json文件,经由页面获取后分配给其内置的组件。
以上图的图片页为例子显示数据流向。
## 2.自定义页面
### 1.创建页面
在预设的页面中(比如图片页,文本页等)普遍含有标题的处理,这里我们先编写个没有标题处理的页面,学习通用的页面是如何编写,其次再做类似预设页面的标题处理,下面以[CustomBookMod](../../13-模组SDK编程/60-Demo示例.md#CustomBookMod)中的页面类型"**MyNoTitlePage**"为例说明。
自定义的一个页面
在编写脚本类之前,我们先将数据定义在”**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](../../13-模组SDK编程/60-Demo示例.md#CustomBookMod)中,与**myNoTitlePage.json**相对应的脚本类是“**behavior_pack/tutorialScripts/pages/noTitlePage.py**”中的**MyNoTitlePage**,一个页面需要编写3个基本的函数接口:
- 类初始化函数 "\_\_init\_\_"
- 获取json传过来的数据的函数"SetData"
- 显示页面的函数"Show"
这些接口需符合链式调用的格式,最终返回**self**。**MyNoTitlePage**实现的是最基本的页面,所以这里继承是**BasePage**,**BasePage**不包含对标题的预处理,如果需要对标题进行预处理,则需要继承**TitlePage**,后续会再提到该页面。**BasePage**的接口详见 [“页面API”](04-页面API.md)。另外,下面的代码中含有很多预设组件的用法,详细请参考[“组件API”](03-组件API.md)。
myNoTitlePage.py
```python
#-*- 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”](04-页面API.md),以及Demo中的“**behavior_pack/customBooks/customBook/entry/myTitlePage.json**”以及“**behavior_pack/tutorialScripts/pages/myTitlePage.py**”。
### 2.注册页面
创建好自定义的页面以及对应的json文件后,需要系统中注册你的自定义页面,使系统在解析json文件的时候能读取到类信息。仅需要在**客户端System**初始化的时候调用注册接口即可,**BookManager**包含了自定义书本的各种接口,详见[BookManager API](05-常用脚本对象.md#BookManager)。因为自定义书本系统中内置了一些常用的页面类型,所以开发者尽量使用规范的名称来命名自己的页面类型,这里建议使用固定格式"**Mod名称:页面类型名称**",其中页面类型名称建议就和定义的类名是一致的(类名后置如果带上“**page**”会更加有辨识度),比如上面的则**MyNoTitlePage**可以命名为"**CustomMod:MyNoTitlePage**",这个自定义的页面类型名称,是用于**json**数据中对应的**type**字段属性值。
```python
# -*- 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.自定义组件(进阶)
该内容为**进阶向**话题,如果开发者不满足于系统提供的**预设组件**,则可以通过这里学习如何自定义组件。需注意以下两点:
- 在阅读此处前请先掌握[界面与交互](../../../18-界面与交互/2-从零开始创建UI.md)中**MC Studio**的使用以及对各类**UI控件**(**BaseUIControl**)的熟悉,**UI json**文件的编写。
- 目前书本仅支持使用这些控件来封装组件,关于它们的介绍具体见[UI说明文档](../../../18-界面与交互/30-UI说明文档.md):
- Label
- NineSliceImage
- Image Button
- Panel
- StackPanel
- NeteasePaperDoll
- ItemRenderer
- ProgressBar
在自定义书系统中,是通过加载**UI json**文件的方式来获取UI控件对象,而获取到的UI控件对象又被封装成组件使用,我们在[CustomBookMod](../../13-模组SDK编程/60-Demo示例.md#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文件
和[界面与交互](../../../18-界面与交互/2-从零开始创建UI.md)一样, 我们可以通过MC Studio来创建一个组件的UI模板,也可以按照**UI**的**json**文件规范来编写**UI**的**json**文件,在本例中我们使用了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**节点的的大小必须设置如下
对应到**UI json**文件中的属性为 **"size" : [ "100.0%+0.0px", "100.0%+0.0px" ]**
```json
"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**)的锚点坐标系必须设置如下图:
对应到**UI json**文件中的属性为如下:
```json
"anchor_from" : "top_left",
"anchor_to" : "top_left",
```
### 2.创建组件类
类似页面,一个组件类需要编写3个基本的函数接口:
- 类初始化函数 "\_\_init\_\_"
- 将从页面传过来的数据存储下来的方法 "SetDataBeforeShow"
- 显示组件的函数 "Show"
这些接口需符合链式调用的格式,最终返回**self**。**MyCustomComp**实现的是一个图文控件,可以根据传入的数据更改文本大小,颜色,内容,图片。
这里继承的父类是**BaseComp**,详细请参考[“组件API”](03-组件API.md)。
customComp.py
```python
#-*- 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”](03-组件API.md) 章节