跳到主要内容

自定义界面系统

功能概述:允许通过服务端脚本(QM/NPC)动态控制客户端 UI 布局、数据展示和交互逻辑。无需修改客户端代码即可创建复杂的自定义窗口界面。

源文件位置引擎包内\自定义界面说明\


一、核心架构

1.1 架构与交互流程

┌──────────────┐ JSON(打开/更新) ┌──────────┐
│ 服务端 NPC │ ─────────────── │ 客户端 │
│ (脚本+CSV) │ │ (XML+Lua) │
│ │ SendMsg(JSON回传) ─ │ │
└──────────────┘ └──────────┘
│ │
▼ ▼
QCustomUI-0.txt LuaMsgToLuaWnd()
[@CustomUI] 标签触发 解析 → 渲染/刷新

1.2 文件部署结构

客户端目录(覆盖到 F:\传奇世界官方\传奇世界\Data\ui):

文件说明
GlobalTool.lua全局工具库(客户端自动加载)
CustomUIWnd.lua你的自定义窗口脚本(文件名=窗口名)

服务端目录

文件/目录说明
<$CONFIG>\xuanwulu.csv业务数据文件(如需要)
Envir\market_def\QCustomUI-0.txt自定义界面通讯入口脚本

NPC/QM 触发脚本(放在任意 NPC 或 QM 脚本中):

[@打开玄武炉] CustomUI <-- 注意末尾的 CUSTOMUI 关键字

部署步骤(来自 玄武炉示范\使用说明.txt):

步骤位置操作
客户端 Data\ui\放入 GlobalTool.luaCustomUIWnd.lua
服务端 <$CONFIG>\放入 CSV 数据文件(如 xuanwulu.csv
服务端 NPC/QM 脚本编写触发打开界面的脚本
服务端 Envir\market_def\放入/覆盖 QCustomUI-0.txt

二、XML 控件类型详解

以下 12 种控件及其全部属性均来自官方文档 传世自定义控件.html

2.1 Window — 窗口根容器

用途:所有自定义界面的最外层必须且只能有一个 <Window> 节点。它是整个界面的容器,定义窗口的基本属性(背景、关闭按钮、坐标等)。

<Window PosX="0" PosY="0" Mask="6600"
CloseBtnID="23018" CloseBtnX="560" CloseBtnY="6"
BackTextureIndex="6601" bSearchChildGood="1">
<!-- 子控件放在这里 -->
</Window>
属性名类型说明
PosX整数窗口左上角的 X 坐标
PosY整数窗口左上角的 Y 坐标
BackTextureIndex整数窗口背景纹理/图片的资源索引 ID
Mask整数窗口掩码值,控制窗口的显示属性和行为
CloseBtnID整数关闭按钮的资源 ID(点击此 ID 的按钮可关闭窗口)
CloseBtnX整数关闭按钮在窗口内的 X 坐标
CloseBtnY整数关闭按钮在窗口内的 Y 坐标
bSearchChildGood字符串 "0"/"1"是否在子控件中查找物品。设为 "1" 时窗口支持物品拖放操作

来自实战示例CustomUIWnd.lua 第 10 行):

<Window CloseBtnID="33018" CloseBtnX="823" CloseBtnY="9"
BackTextureIndex="34719" bSearchChildGood="1">

2.2 Button — 按钮

用途:用户交互的主要方式。支持四种视觉状态(正常/悬停/按下/禁用),可通过 OnClick 绑定 Lua 回调函数。

<Button Name="Button1" PosX="342" PosY="372"
NormalTexture="1210" HighlightTexture="1211"
PushedTexture="1212" DisableTexture="1213"
Text="确定" OnClick="OnClickExt"
Show="1" Status="0"/>
属性名类型说明
Name字符串按钮名称标识符,用于在 Lua 中引用
PosX整数按钮左上角 X 坐标
PosY整数按钮左上角 Y 坐标
NormalTexture整数正常状态下的纹理/图片资源 ID
HighlightTexture整数鼠标悬停(高亮)状态的纹理 ID
PushedTexture整数鼠标按下状态的纹理 ID
DisableTexture整数禁用状态的纹理 ID
Text字符串按钮上显示的文字(支持 $变量名 引用 DATA)
OnClick字符串点击时调用的 Lua 函数名。支持传参格式 "函数名&参数"
Show字符串 "0"/"1"是否显示(也支持 $变量 动态控制显隐)
Status整数按钮初始状态:0=正常、1=选中/高亮、2=按下

Status 状态实战用法(来自 CustomUIWnd.lua 分类菜单):

-- 在 Lua 中动态切换按钮选中状态
DATA['Menu'][i].BtnStatus = (i == idx and 1 or 0)
-- 选中项 Status=1(高亮),其余 Status=0(正常)
-- 物品选中时 Status=2(按下效果)

OnClick 参数传递机制

<!-- XML 中用 & 分隔函数名和参数 -->
<button OnClick="JumpToPage&3"/>
<button OnClick="OnClickDetail&15"/>
<button OnClick="$eachVal.ClickMenu"/> <!-- foreach 中绑定动态函数名 -->
-- Lua 中接收参数为字符串,需手动转换
function CustomUIWnd.JumpToPage(param)
local pageIdx = tonumber(param) - 1 -- param = "3"
end
function CustomUIWnd.OnClickDetail(param)
local idx = tonumber(param) -- param = "15"
end
function CustomUIWnd.ClickMenu(param)
local idx = tonumber(param) -- param 对应 foreach 当前索引
end

2.3 Text — 文本标签

用途:显示静态或动态文本信息。支持字体、颜色、大小等样式设置。

<Text Name="Text1" PosX="430" PosY="500"
Font="0" FontFlag="14" FontColor="green"
FontSize="12" Text="$label" Show="1"/>
属性名类型说明
Name字符串控件名称标识符
PosX整数左上角 X 坐标
PosY整数左上角 Y 坐标
Font整数字体类型索引(0=默认字体, 1=其他字体)
FontFlag整数字体样式标志(位标志,可叠加:如 24=粗体+阴影)
FontColor字符串/整数字体颜色。支持颜色名("green")或 ARGB 十六进制("0xFFEA9E54"
FontSize整数字体大小(像素)
Text字符串显示内容(支持 $变量名 引用 DATA 中的值)
Show字符串 "0"/"1"是否显示(支持 $变量 动态控制)

实战中的字体颜色写法(来自 CustomUIWnd.lua):

<!-- 标题文字:金黄色 -->
<Text FontSize="24" Font="1" FontColor="0xFFEA9E54" FontFlag="24"/>

<!-- 物品名字:橙色 -->
<Text FontSize="12" FontColor="0xFFC2934C" FontFlag="12"/>

<!-- 材料名:浅黄色 -->
<Text FontSize="12" FontColor="0xFFE6B260" FontFlag="0"/>

<!-- 材料数量:金色 -->
<Text FontSize="12" FontColor="0xFFE6C800" FontFlag="0"/>

2.4 Image — 图片

用途:显示静态图片/图标资源。支持纹理包索引、渲染模式、悬停提示等。

<Image Name="Image1" PosX="430" PosY="500"
Enble="1" TextID="930" TextPack="16"
RenderMode="2" EffTextID="0"
Show="1" MouseOnImage="0" Tips="鼠标悬停提示"/>
属性名类型说明
Name字符串控件名称标识符
PosX整数左上角 X 坐标
PosY整数左上角 Y 坐标
Enble字符串 "0"/"1"是否启用控件
TextID整数图片纹理/图标的资源 ID
TextPack整数纹理包/图包 ID(指定从哪个资源包加载)
RenderMode整数渲染模式(影响图片的绘制方式)
EffTextID整数特效纹理 ID(可在图片上叠加特效)
Show字符串 "0"/"1"是否显示
MouseOnImage整数鼠标悬停时显示的替代图片 ID
Tips字符串鼠标悬停时显示的提示文本

实战用法(材料列表中的图标):

<Image PosX="110" PosY="2" TextID="33242" Show="$TextShow"/>

2.5 Good — 物品图标

用途:显示游戏中的物品图标(装备、药品、材料等)。通过 DbName+Look 从数据库获取物品外观,支持数量显示和鼠标提示。

<Good Name="good1" PosX="25" PosY="25"
Width="44" Height="44" BackTex="0"
Bind="0" Look="100" Num="1"
DbName="金疮药" Tips="回复生命" Show="1"/>
属性名类型说明
Name字符串控件名称标识符
PosX整数左上角 X 坐标
PosY整数左上角 Y 坐标
Width整数物品图标宽度(像素)
Height整数物品图标高度(像素)
BackTex整数物品背景纹理 ID(0=无背景)
Bind整数绑定状态:0=未绑定、1=绑定
Look整数物品外观 ID(对应数据库 Looks 字段)。支持 $变量
Num整数显示的物品数量。支持 $变量
DbName字符串物品数据库名称(用于获取物品信息和 Tips)。支持 $变量
Tips字符串鼠标悬停时的提示文字
Show字符串 "0"/"1"是否显示

实战用法——foreach 中的动态物品CustomUIWnd.lua 第 22 行):

<!-- 物品网格中的单个物品格子 -->
<Good PosX="25" PosY="25"
DbName="$eachVal.DBName" Look="$eachVal.Look"
Bind="0" BackTex="0" Num="1" Show="1"/>

<!-- 右侧详情区的大图预览 -->
<Good PosX="685" PosY="235"
DbName="$DBName" Look="$Look"
BackTex="0" Tips="$DBName" Num="1" Show="$TextShow"/>

2.6 MarkView — 多行文本框

用途:显示多行文本内容(如物品描述、任务说明等)。支持指定行数、行高、字体颜色。

<MarkView Name="markview1" PosX="585" PosY="60"
Width="216" Height="124"
Row="12" RowHeight="16"
Font="0" FontSize="0" Color="0xFFEA9E54"
TagText="$ItemDesc" Show="1"/>
属性名类型说明
Name字符串控件名称标识符
PosX整数左上角 X 坐标
PosY整数左上角 Y 坐标
Width整数控件宽度(像素)
Height整数控件高度(像素)
Row整数最大显示行数
RowHeight整数每行高度(像素)
Font整数字体索引
FontSize整数字体大小
Color整数字体颜色(ARGB 十六进制格式)
TagText字符串要显示的文本内容(支持 $变量
Show字符串 "0"/"1"是否显示

实战用法——右侧物品描述区域CustomUIWnd.lua 第 12 行):

<MarkView PosX="585" PosY="60" Row="12" RowHeight="16"
Font="0" Color="0xFFEA9E54"
TagText="$ItemDesc" Width="216" Height="124"/>

对应的 Lua 数据:

DATA['ItemDesc'] = "这是一段很长的物品描述文字\n可以包含多行\n自动换行显示"

2.7 Effect — 特效播放

用途:在指定位置播放动画/粒子特效。通过 Lua 的 PlayEffect()/StopEffect() 动态控制启停。

<effect Name="suceft1" PosX="585" PosY="35"
TextID="34722" PlaySpeed="0"
PackageID="0" PlayMode="2"
RenderMode="0" TotalTims="0"
OnceTime="0" Func="0"/>
属性名类型说明
Name字符串特效名称(关键!)—— Lua 中通过此名称控制播放/停止
PosX整数特效左上角 X 坐标
PosY整数特效左上角 Y 坐标
TextID整数特效资源 ID(指定播放哪个特效)
PlaySpeed整数播放速度(数值越大越快)
PackageID整数特效包 ID
PlayMode整数播放模式:0=循环播放、2=单次播放后停止
RenderMode整数渲染模式
TotalTims整数总播放次数(0=无限/由 PlayMode 决定)
OnceTime整数单次播放时间(毫秒)
Func字符串播放完成后的回调函数名

实战用法CustomUIWnd.lua 第 13、36 行):

<!-- 页面切换时的标题闪光特效(单次播放) -->
<effect Name="suceft1" PosX="585" PosY="35" TextID="34722" PlayMode="2"/>

<!-- 合成按钮点击时的爆炸特效(循环播放) -->
<effect Name="suceft2" PosX="275" PosY="-30" TextID="27741" PlayMode="0"/>

Lua 中控制特效播放/停止:

-- 播放特效(来自 Lua接口说明.txt 第 100~108 行)
function CustomUIWnd.PlayEffect(wndName, effectName)
MsgToWnd({ ['MsgType']=3, ['WndName']=wndName,
['ControlName']=effectName, ['VecInt']={1} })
end

-- 停止特效(来自 Lua接口说明.txt 第 111~119 行)
function CustomUIWnd.StopEffect(wndName, effectName)
MsgToWnd({ ['MsgType']=3, ['WndName']=wndName,
['ControlName']=effectName, ['VecInt']={0} })
end

-- 使用示例
CustomUIWnd.PlayEffect(LocalWndName, "suceft1") -- 播放 suceft1
CustomUIWnd.PlayEffect(LocalWndName, "suceft2") -- 播放 suceft2
CustomUIWnd.StopEffect(LocalWndName, "suceft1") -- 停止 suceft1

注意PlayEffect/StopEffectLua接口说明.txt 中推荐放入 GlobalTool 的工具函数。实际项目中可以在窗口脚本内部定义(如 CustomUIWnd.PlayEffect),也可以放到 GlobalTool.lua 中全局共享。


2.8 Unit — 动态列表单元(foreach 循环)

用途:这是自定义界面系统最核心的功能。根据 DATA 表的数据动态生成重复的子控件,实现列表、表格、网格等布局。

<unit Name="menuUnit" PosX="16" PosY="52"
Width="80" Height="22"
MaxRow="7" IsVertical="0" Show="1"
foreachKey="$eachKey" foreachVal="$eachVal"
foreachData="$Menu">
<button PosX="0" PosY="0" Width="80" Height="22"
Text="$eachVal.Text"
NormalTexture="33435" HighlightTexture="33435"
PushedTexture="33435"
OnClick="$eachVal.ClickMenu"
Status="$eachVal.BtnStatus"/>
</unit>
属性名类型说明
Name字符串单元名称标识符
PosX整数左上角 X 坐标
PosY整数左上角 Y 坐标
Width整数单元区域宽度(像素)
Height整数单元区域高度(像素)
MaxRow整数最大显示行/列数(超出部分不渲染)
IsVertical整数排列方向:0=水平排列、1=垂直排列
Show字符串 "0"/"1"整个单元是否显示
foreachKey字符串当前迭代的变量名(通常固定为 "$eachKey"
foreachVal字符串当前迭代的变量名(通常固定为 "$eachVal"
foreachData字符串要遍历的 DATA 键名(必须是 DATA 表中的键,写法:"$Menu" 表示遍历 DATA['Menu']

foreach 工作原理

客户端会自动遍历 DATA[foreachData] 中的每个元素,对 <unit> 内部的子控件进行复制:

  1. 客户端读取 foreachData 的值(如 "$Menu"),去 DATA 表中找到 DATA['Menu']
  2. 遍历 DATA['Menu'] 的每个元素(假设是 {[1]={...}, [2]={...}, [3]={...}}
  3. 对每次迭代,将 unit 内部子控件中的 $eachVal 替换为当前元素,$eachKey 替换为当前键
  4. 根据 MaxRowIsVertical 决定最多渲染多少个、如何排列

嵌套 foreach 示例(二维网格——来自 CustomUIWnd.lua 第 19~26 行):

<!-- 外层 unit:垂直排列 3 行(每行是一组物品) -->
<unit PosX="15" PosY="102" Width="558" Height="116"
MaxRow="3" IsVertical="1"
foreachKey="$eachKey" foreachVal="$eachVal"
foreachData="$Item">
<!-- 内层 unit:每行内水平排列 6 个物品 -->
<unit PosX="0" PosY="0" Width="93" Height="116"
MaxRow="6" IsVertical="0"
foreachKey="$eachKey" foreachVal="$eachVal"
foreachData="$eachVal"> <!-- [注意] 内层遍历外层的 $eachVal -->
<button OnClick="$eachVal.OnClickDetail"
Status="$eachVal.BtnStatus"
NormalTexture="34721" HighlightTexture="34720"
PushedTexture="34720"/>
<Good DbName="$eachVal.DBName"
Look="$eachVal.Look" Bind="0"
BackTex="0" Num="1" Show="1"/>
<Text PosX="50" PosY="95" Width="93"
Text="$eachVal.DBName"
FontSize="12" FontColor="0xFFC2934C"
FontFlag="12"/>
<Trigger PosX="0" PosY="0" Width="93" Height="116"
OnClick="$eachVal.OnClickDetail" Show="1"/>
</unit>
</unit>

对应的 DATA 结构(三维 table):

DATA['Item'] = {
[1] = { -- 第 1 行(外层 foreach 第 1 次)
[1] = { -- 第 1 列(内层 foreach 第 1 次)
DBName = "倚天剑",
Look = 1201,
BtnStatus = 0,
OnClickDetail = "OnClickDetail&1",
ItemDesc = "传说中的神兵...",
material1 = "玄铁", material1_num = 5,
-- ...
},
[2] = { DBName="屠龙刀", Look=1202, ... }, -- 第 2 列
[3] = { DBName="乾坤扇", Look=1203, ... }, -- 第 3 列
-- ... 共 6 列(MaxRow=6)
},
[2] = { ... }, -- 第 2 行
[3] = { ... } -- 第 3 行(外层 MaxRow=3)
}

[重要] 嵌套 foreach 的关键规则:内层 foreachData="$eachVal" —— 这里的 $eachVal 不加引号也不加 $ 前缀的外层变量名,表示"遍历外层当前迭代的值"。这是最容易出错的地方。


2.9 Trigger — 透明触发区域

用途:在已有控件上方叠加一个不可见的点击热区,扩大可点击范围或添加额外的点击响应。

<Trigger Name="trigger1" PosX="0" PosY="0"
Width="93" Height="116"
OnClick="$cellData.OnClickDetail" Show="1"/>
属性名类型说明
Name字符串触发器名称标识符
PosX整数左上角 X 坐标
PosY整数左上角 Y 坐标
Width整数触发区域宽度(像素)
Height整数触发区域高度(像素)
OnClick字符串点击事件绑定的 Lua 函数名(支持 "函数名&参数" 格式)
Show字符串 "0"/"1"是否激活("1" 可点击、"0" 不可点击)

实战用法CustomUIWnd.lua 第 24 行)—— 让整个物品格子(93×116px)都可点击:

<!-- Good 控件只占 44x44,Trigger 扩大到整个格子 -->
<Good PosX="25" PosY="25" DbName="$eachVal.DBName" Look="$eachVal.Look" .../>
<Trigger PosX="0" PosY="0" Width="93" Height="116"
OnClick="$eachVal.OnClickDetail" Show="1"/>

2.10 Progress — 进度条

用途:显示任务进度、经验条、HP/MP 等数值型进度信息。

<Progress Name="Progress1" PosX="0" PosY="0"
Enble="1" BackTex="0" TextID="0"
ProgVal="50" ProgType="0"
RenderMode="0" Show="1"/>
属性名类型说明
Name字符串进度条名称标识符
PosX整数左上角 X 坐标
PosY整数左上角 Y 坐标
Enble字符串 "0"/"1"是否启用控件
BackTex整数进度条背景纹理 ID
TextID整数进度条填充图标/纹理 ID
ProgVal整数当前进度值(支持 $变量 动态更新)
ProgType整数进度条类型(决定外观样式)
RenderMode整数渲染模式
Show字符串 "0"/"1"是否显示

2.11 Timer — 定时器

用途:在指定时间间隔后触发 Lua 函数。常用于延迟刷新、倒计时、周期性检查等场景。

<Timer Name="refreshTimer"
TotalTims="1" OnceTime="100"
Func="RefreshProperty" Show="1"/>
属性名类型说明
Name字符串定时器名称(Lua 中通过此名称控制启停
TotalTims整数执行总次数:1=执行一次后自动停止、0=无限循环
OnceTime整数触发间隔时间,单位毫秒1000 = 1 秒)
Func字符串时间到达时调用的 Lua 函数名字符串
Show字符串 "0"/"1"是否运行:"1" 启动/运行、"0" 停止

Lua 中动态控制定时器启停(来自 Lua接口说明.txt 第 122~141 行):

-- 启动定时器
function GlobalTool.StartTimer(wndName, timerName)
local msg = {
['MsgType'] = 4,
['WndName'] = wndName, -- 窗口名称
['ControlName'] = timerName, -- Timer 控件的 Name 属性
['VecInt'] = {1} -- 1=启动
}
MsgToWnd(msg)
end

-- 停止定时器
function GlobalTool.StopTimer(wndName, timerName)
local msg = {
['MsgType'] = 4,
['WndName'] = wndName,
['ControlName'] = timerName,
['VecInt'] = {0} -- 0=停止
}
MsgToWnd(msg)
end

-- 使用示例
GlobalTool.StartTimer("CustomUIWnd", "refreshTimer") -- 启动
GlobalTool.StopTimer("CustomUIWnd", "refreshTimer") -- 停止

2.12 Grid — 物品网格容器

用途:放置可拖拽物品的网格区域(如背包格子、合成槽位等)。支持拖放检查和物品变化事件。

<Grid Name="Grid1" PosX="0" PosY="0"
BackTex="0" iBackOffX="0" iBackOffY="0"
TipsNoGood="0" bDrawBackColor="0"
bCheckDropGoodCanContain="0"
Show="1"
OnGridDGCheck="0" OnGridGoodChg="0"/>
属性名类型说明
Name字符串网格容器名称标识符
PosX整数左上角 X 坐标
PosY整数左上角 Y 坐标
BackTex整数网格背景纹理 ID
iBackOffX整数背景纹理 X 轴偏移量
iBackOffY整数背景纹理 Y 轴偏移量
TipsNoGood整数/字符串网格为空时的提示信息
bDrawBackColor字符串 "0"/"1"是否绘制背景颜色
bCheckDropGoodCanContain字符串 "0"/"1"是否检查拖入的物品是否可被此网格接受
Show字符串 "0"/"1"是否显示
OnGridDGCheck字符串/整数网格拖放检查事件的回调
OnGridGoodChg字符串/整数网格内物品发生变化时的回调

控件速查表

控件主要用途支持 $变量支持 foreach支持 OnClick
Window窗口根容器---
Button交互按钮✔ Text/Show/Status作为子控件
Text文本标签✔ Text/Show作为子控件-
Image静态图片✔ Show作为子控件-
Good物品图标✔ Look/Num/DbName/Show作为子控件-
MarkView多行文本✔ TagText/Show作为子控件-
Effect特效动画---
Unit动态列表✔ foreachData✔ 本身就是 foreach-
Trigger透明热区✔ OnClick/Show作为子控件
Progress进度条✔ ProgVal/Show作为子控件-
Timer定时器---
Grid物品网格✔ Show-✔ 事件回调

三、Lua 脚本接口详解

以下所有函数签名、参数说明、使用示例均来自 Lua接口说明.txtGlobalTool.luaCustomUIWnd.lua 源文件。

3.1 窗口脚本标准结构

每个自定义窗口 Lua 文件必须遵循以下结构:

-- ============================================================
-- 模块定义
-- ============================================================
ModuleName = {} -- 模块名(建议与文件名一致)

-- [[ XML 布局字符串 ]]
local XML = [[
<Window CloseBtnID="33018" CloseBtnX="823" CloseBtnY="9"
BackTextureIndex="34719" bSearchChildGood="1">
<!-- 控件树 -->
</Window>
]]

-- [[ 数据表 ]]
local DATA = {
['字段1'] = '值1',
['字段2'] = 0,
}

-- [[ 消息接收处理 ]]
function ModuleName.Msg(msg)
-- 处理服务端推送的消息
end

-- [[ 必须导出的两个接口 ]]
function ModuleName.GetRenderXml() return XML end
function ModuleName.GetRenderData() return DATA end

return ModuleName -- 文件末尾必须有 return

三个必须要素

要素说明
GetRenderXml()返回 XML 布局字符串。客户端调用此函数获取界面长什么样
GetRenderData()返回 DATA 数据表。客户端调用此函数获取界面显示什么数据
return ModuleName文件末尾返回模块表。缺少此项会导致模块无法被加载

3.2 内置全局函数(客户端提供,共 14 个)

这些函数由客户端内置,无需定义,直接在任意 CustomUI Lua 脚本中调用。

3.2.1 UpdateWnd — 刷新窗口

来源Lua接口说明.txt 第 1~6 行

UpdateWnd(参数1)
参数类型说明
参数1字符串窗口名称(即 OperWindow() 中使用的名称)

作用:通知客户端重新读取 GetRenderXml()GetRenderData() 并重新渲染窗口。每次修改 DATA 后都必须调用此函数才能看到界面变化

使用示例(来自 CustomUIWnd.lua,全文多处调用):

local LocalWndName = 'CustomUIWnd'

-- 修改数据后刷新
DATA['label'] = '新标题'
UpdateWnd(LocalWndName) -- 界面立即更新显示"新标题"

-- 在 Msg() 中收到消息后刷新
function CustomUIWnd.Msg(msg)
if msg['MsgType'] == 0 then
-- 解析消息、修改 DATA...
UpdateWnd(LocalWndName) -- 使修改生效
end
end

3.2.2 MsgBox_PopSimpleMsgBox — 弹出提示框

来源Lua接口说明.txt 第 7~14 行

MsgBox_PopSimpleMsgBox(参数1, 参数2, 参数3, 参数4)
参数类型说明
参数1整数弹框类型,固定填 1
参数2字符串提示框中显示的文字内容
参数3整数0=不关闭窗口、1=关闭窗口
参数4字符串所属窗口名称

使用示例(来自 CustomUIWnd.lua 第 157 行):

local str = "背包中 " .. itemName .. " 的数量为:" .. totalCount
MsgBox_PopSimpleMsgBox(1, str, 1, LocalWndName)
-- 弹出提示框显示数量,点击确定后关闭窗口

3.2.3 SELF_GetSelfAttrInt — 获取玩家属性

来源Lua接口说明.txt 第 16~21 行

local value = SELF_GetSelfAttrInt(参数1)
参数类型说明
参数1字符串属性名称

返回值:整数

已知的可用属性名

属性名返回值含义
"packagesize2"背包中物品的总数量
"packagesize"背包最大格子数(来源提及但标注"暂时只支持 packagesize2")

使用示例(来自 CustomUIWnd.lua 第 144 行):

local packagesize = SELF_GetSelfAttrInt("packagesize2")
-- 返回值例如:40(背包中有40个物品格子被占用)

for i = 0, packagesize - 1 do
local good = Good_GetPackageGood(i, false)
-- 遍历背包中每个物品...
end

3.2.4 Good_GetPackageGood — 获取背包物品

来源Lua接口说明.txt 第 23~28 行

local good = Good_GetPackageGood(参数1, 参数2)
参数类型说明
参数1整数背包位置索引(从 0 开始)
参数2布尔值falsetrue(具体差异待确认,示例中使用 false

返回值:物品结构体(table),后续传入 Good_GetGoodAttrStr / Good_GetGoodAttrInt 使用

使用示例(来自 CustomUIWnd.lua 第 149 行):

local packagesize = SELF_GetSelfAttrInt("packagesize2")
for i = 0, packagesize - 1 do
local good = Good_GetPackageGood(i, false) -- 获取第i个位置的物品
local strName = Good_GetGoodAttrStr(good, "name", "", "")
-- strName = "金疮药" 等
end

3.2.5 Good_GetGoodAttrStr — 获取物品字符串属性

来源Lua接口说明.txt 第 29~37 行

local strValue = Good_GetGoodAttrStr(参数1, 参数2, 参数3, 参数4)
参数类型说明
参数1table物品结构体(由 Good_GetPackageGoodGood_GetSingleGoodGG 返回)
参数2字符串要获取的属性名
参数3字符串窗口名称(可为空字符串 ""
参数4字符串CGoodGridNL 控件名称(可为空字符串 ""

返回值:字符串

已知支持的属性名

属性名返回值示例
"name"物品名称,如 "金疮药"

使用示例

local good = Good_GetPackageGood(0, false)
local strName = Good_GetGoodAttrStr(good, "name", "", "")
-- strName = "超级金疮药"

3.2.6 Good_SingleGoodBackToPack — 物品放回背包

来源Lua接口说明.txt 第 38~44 行

Good_SingleGoodBackToPack(参数1, 参数2)
参数类型说明
参数1字符串窗口名称
参数2字符串CGoodGridNL 控件的 Name 属性值

作用:将指定 CGoodGridNL 控件中的物品放回玩家背包。

使用示例

Good_SingleGoodBackToPack(LocalWndName, "SNDGG1")
-- 将窗口中名为 "SNDGG1" 的物品控件里的物品放回背包

3.2.7 Good_SetSingleGoodGG — 放置物品到控件

来源Lua接口说明.txt 第 45~51 行

Good_SetSingleGoodGG(参数1, 参数2, 参数3)
参数类型说明
参数1table物品结构体
参数2字符串窗口名称
参数3字符串CGoodGridNL 控件的 Name 属性值

作用:将一个物品放入指定的 CGoodGridNL 物品控件中显示。

使用示例

local good = Good_GetPackageGood(0, false)
Good_SetSingleGoodGG(good, LocalWndName, "SNDGG1")
-- 把背包第0号位置的物品放到 "SNDGG1" 控件中

3.2.8 Good_GetSingleGoodGG — 获取控件中的物品

来源Lua接口说明.txt 第 53~58 行

local good = Good_GetSingleGoodGG(参数1, 参数2)
参数类型说明
参数1字符串窗口名称
参数2字符串CGoodGridNL 控件的 Name 属性值

返回值:物品结构体

使用示例

local good1 = Good_GetSingleGoodGG(LocalWndName, "SNDGG1")
local id1 = Good_GetGoodAttrInt(good1, "id", LocalWndName, "SNDGG1")

3.2.9 Good_GetGoodAttrInt — 获取物品整数属性

来源Lua接口说明.txt 第 60~68 行

local intValue = Good_GetGoodAttrInt(参数1, 参数2, 参数3, 参数4)
参数类型说明
参数1table物品结构体
参数2字符串属性名
参数3字符串窗口名称
参数4字符串CGoodGridNL 控件名称

返回值:整数

已知支持的属性名

属性名返回值含义
"id"物品唯一 ID
"stdmode"物品标准类型
"shape"物品外形

使用示例

local good1 = Good_GetSingleGoodGG(LocalWndName, "SNDGG1")
local id1 = Good_GetGoodAttrInt(good1, "id", LocalWndName, "SNDGG1")

3.2.10 Good_GetDropGood — 获取拖放区物品

来源Lua接口说明.txt 第 69~71 行

local good = Good_GetDropGood()

参数:无

返回值:物品结构体(拖放区/放取框中的物品)

使用示例

local good = Good_GetDropGood()
if good then
local name = Good_GetGoodAttrStr(good, "name", "", "")
end

3.2.11 GetServerType — 获取服务器版本类型

来源Lua接口说明.txt 第 73~75 行

local servertype = GetServerType()

参数:无

返回值:整数,表示服务器版本类型

已知返回值(来自 GlobalTool.lua 第 41~46 行):

返回值含义
12商城版本
15商城版本(另一变体)
其他非商城版本

使用示例(来自 GlobalTool.lua):

function GlobalTool.GFnUseShiChangUi()
local servertype = GetServerType()
if servertype == 12 or servertype == 15 then
return true
end
return false
end

3.2.12 OperWindow — 打开/操作窗口

来源Lua接口说明.txt 第 77~79 行

OperWindow(参数1, 参数2, 参数3, 参数4)
参数类型说明
参数1字符串窗口名称(必须与 Lua 文件名一致,如 "CustomUIWnd"
参数2字符串操作类型,"create" = 创建并打开窗口
参数3整数固定填 0
参数4布尔值固定填 true

作用:创建并显示指定的自定义界面窗口。首次收到 MsgType=0 消息时必须先调用此函数

使用示例(来自 GlobalTool.lua 多处):

-- 创建并打开窗口
OperWindow("CustomUIWnd", "create", 0, true)
OperWindow("HuaXingShenLuWnd", "create", 0, true)
OperWindow("ShenNongDingWnd", "create", 0, true)
OperWindow("XingheYinqingWnd", "create", 0, true)

3.2.13 LuaMsgToLuaWnd — 转发消息到窗口

来源Lua接口说明.txt 第 81~85 行

LuaMsgToLuaWnd(参数1, 参数2)
参数类型说明
参数1字符串目标窗口名称
参数2table消息数据(即服务端发送的原始 msg table)

作用:将消息转发给指定窗口脚本的 Msg(msg) 函数。在 OperWindow() 之后调用。

使用示例(来自 GlobalTool.lua):

-- 先创建窗口,再转发消息
OperWindow("CustomUIWnd", "create", 0, true)
LuaMsgToLuaWnd("CustomUIWnd", msg)

-- 后续消息直接转发(不需要再次 OperWindow)
LuaMsgToLuaWnd("CustomUIWnd", msg)

3.2.14 SendMsg — 发送消息到服务端

来源Lua接口说明.txt 第 86~95 行

SendMsg(参数1)
参数类型说明
参数1tableJSON 格式的消息封包

参数结构

字段类型说明
ActName字符串动作名称(服务端通过 $CUSTOMPARAM(0) 匹配此值)
ActType整数协议包序号(服务端通过 $CUSTOMPARAM(1) 匹配此值)
ListStrtable字符串参数列表(按索引存放,如 {"字符串1", "字符串2"}
ListInttable整数型参数列表(按索引存放,如 {100, 200}

完整使用示例(来自 Lua接口说明.txt 第 90~95 行):

SendMsg({
['ActName'] = "CustomUI2026", -- 窗口/动作名称
['ActType'] = 2, -- 协议包序号
['ListStr'] = {tostring(DATA['DBName']), "你好"}, -- 字符串列表
['ListInt'] = {tonumber(DATA['CurIndex']), 10121212} -- 整数型列表
})

实战示例——用户点击分类菜单(来自 CustomUIWnd.lua 第 119 行):

function CustomUIWnd.ClickMenu(index)
local idx = tonumber(idx)
-- 更新菜单按钮状态...
SendMsg({
['ActName'] = "CustomUI2026",
['ActType'] = 1, -- ActType=1 表示请求分类下的物品列表
['ListInt'] = {idx} -- 告诉服务端选择了第几个分类
})
end

实战示例——用户点击合成按钮(来自 CustomUIWnd.lua 第 132~137 行):

function CustomUIWnd.OnClickExt()
if DATA['TextShow'] == 0 then return end

-- 组装材料字符串:"材料A:数量#材料B:数量#"
local matStr = ""
for _, m in ipairs(DATA['material']) do
matStr = matStr .. m.name .. ":" .. m.Num .. "#"
end

SendMsg({
['ActName'] = "CustomUI2026",
['ActType'] = 2, -- ActType=2 表示执行合成
['ListStr'] = {tostring(DATA['DBName']), matStr}, -- 物品名 + 材料清单
['ListInt'] = {tonumber(DATA['CurIndex'])} -- 物品在全量数据中的索引
})
end

3.3 GlobalTool.lua 工具函数(共 10 个)

以下函数来自 GlobalTool.lua 源文件(113 行)和 Lua接口说明.txt 第 97~141 行。 GlobalTool.lua 放在客户端 Data\ui\ 目录下会被客户端自动加载。

3.3.1 PrintTable — 打印 Table 内容(调试用)

来源GlobalTool.lua 第 6~17 行

function GlobalTool.PrintTable(table1)
参数类型说明
table1table要打印的 table

行为:递归遍历 table,对 string 和 number 类型的键值对输出到日志。嵌套 table 会递归打印。

源码

function GlobalTool.PrintTable(table1)
for ii, v in pairs(table1) do
if type(v) == 'table' then
PrintTable(v) -- 递归打印嵌套 table
elseif type(v) == 'string' then
Log(ii .. " :" .. v)
elseif type(v) == 'number' then
Log(ii .. " :" .. v)
end
end
end

3.3.2 split — 字符串分割

来源GlobalTool.lua 第 20~29 行

local result = GlobalTool.split(str, reps)
参数类型说明
str字符串要分割的源字符串
reps字符串分隔符(如 "#", ","

返回值:数字索引的 table(数组)

源码

function GlobalTool.split(str, reps)
local resultStr = {}
local i = 1
string.gsub(str, '[^'..reps..']+', function(w)
table.insert(resultStr, i, w)
i = i + 1
end)
return resultStr
end

使用示例(来自 CustomUIWnd.lua 第 72~84 行——注意:这里是在窗口脚本内重新实现了一份):

-- GlobalTool.split 版本
local parts = GlobalTool.split("境界类#融合元神#法宝类", "#")
-- parts = {"境界类", "融合元神", "法宝类"}

-- CustomUIWnd.Split 版本(功能相同,实现在窗口脚本内部)
function CustomUIWnd.Split(str, sep)
local result = {}
if not str or str == "" then return result end
local start = 1
local s_start, s_end = string.find(str, sep, start, true)
while s_start do
table.insert(result, string.sub(str, start, s_start - 1))
start = s_end + 1
s_start, s_end = string.find(str, sep, start, true)
end
table.insert(result, string.sub(str, start))
return result end

3.3.3 TableSize — 计算 Table 元素数量

来源GlobalTool.lua 第 31~38 行

local count = GlobalTool.TableSize(table1)
参数类型说明
table1table要计数的 table

返回值:整数(元素个数)

源码

function GlobalTool.TableSize(table1)
local iCount = 0;
for ii, v in pairs(table1) do
iCount = iCount + 1;
end
print(123) -- 调试残留代码
return iCount;
end

3.3.4 GFnUseShiChangUi — 判断是否商城版服务器

来源GlobalTool.lua 第 40~46 行

local isMarket = GlobalTool.GFnUseShiChangUi()

参数:无

返回值:布尔值

源码

function GlobalTool.GFnUseShiChangUi()
local servertype = GetServerType()
if servertype == 12 or servertype == 15 then
return true
end
return false
end

3.3.5 Msg_UtilsActResultLua — 消息分发器(核心路由函数)

来源GlobalTool.lua 第 49~72 行

local handled = GlobalTool.Msg_UtilsActResultLua(msg)
参数类型说明
msgtable客户端收到的消息(包含 ActName, MsgType 等字段)

返回值:布尔值,true=消息已被处理,false=未匹配任何窗口

行为:读取 msg['ActName'],根据值分发到对应的处理函数。新增自定义窗口时必须在此函数中添加分支

源码(完整的路由表):

function GlobalTool.Msg_UtilsActResultLua(msg)
local strActName = msg['ActName']

if strActName == "pet_huaxing" then
GlobalTool.pet_huaxing(msg)
return true
end

if strActName == "ShiYao2024" then
GlobalTool.ShiYao2024(msg)
return true
end

if strActName == "CustomUI2026" then
GlobalTool.CustomUI2026(msg)
return true
end

if strActName == "XingheYinqing" then
GlobalTool.XingheYinqing(msg)
return true
end

return false -- 未匹配任何已知窗口
end

当前已注册的路由表

ActName 值处理函数创建的窗口说明
"pet_huaxing"pet_huaxing(msg)HuaXingShenLuWnd化形神炉
"ShiYao2024"ShiYao2024(msg)ShenNongDingWnd神农鼎(炼药)
"CustomUI2026"CustomUI2026(msg)CustomUIWnd自定义通用窗口
"XingheYinqing"XingheYinqing(msg)XingheYinqingWnd星河引擎

3.3.6 ~ 3.3.9 — 四个路由处理函数

这四个函数的结构完全相同,只是目标窗口不同。以 CustomUI2026 为例:

来源GlobalTool.lua 第 74~81 行

function GlobalTool.CustomUI2026(msg)
if msg['MsgType'] == 0 then
-- 首次消息:创建窗口 + 转发
OperWindow("CustomUIWnd", "create", 0, true)
LuaMsgToLuaWnd("CustomUIWnd", msg)
elseif msg['MsgType'] == 1 or msg['MsgType'] == 2 then
-- 后续消息:仅转发
LuaMsgToLuaWnd("CustomUIWnd", msg)
end
end

四个路由函数对照

函数名MsgType=0 时创建的窗口MsgType=1/2 时转发到
CustomUI2026(msg)CustomUIWndCustomUIWnd
XingheYinqing(msg)XingheYinqingWndXingheYinqingWnd
ShiYao2024(msg)ShenNongDingWndShenNongDingWnd
pet_huaxing(msg)HuaXingShenLuWndHuaXingShenLuWnd

MsgType 含义(从实战推断):

MsgType含义处理方式
0首次/初始化消息OperWindow("create") + LuaMsgToLuaWnd()
1数据更新消息LuaMsgToLuaWnd()
2操作结果/其他消息LuaMsgToLuaWnd()

3.3.10 PlayEffect — 播放特效

来源Lua接口说明.txt 第 100~108 行(推荐放入 GlobalTool 的工具函数)

GlobalTool.PlayEffect(wndName, effectName)
参数类型说明
wndName字符串窗口名称
effectName字符串XML 中 <effect> 控件的 Name 属性值

实现:构造 MsgType=3 的消息,VecInt={1} 表示播放,然后调用 MsgToWnd()

function GlobalTool.PlayEffect(wndName, effectName)
local msg = {
['MsgType'] = 3,
['WndName'] = wndName,
['ControlName'] = effectName, -- 对应 XML 中 <effect Name="xxx">
['VecInt'] = {1} -- 1=播放
}
MsgToWnd(msg)
end

3.3.11 StopEffect — 停止特效

来源Lua接口说明.txt 第 111~119 行

GlobalTool.StopEffect(wndName, effectName)

参数同 PlayEffect,区别在于 VecInt={0} 表示停止。

function GlobalTool.StopEffect(wndName, effectName)
local msg = {
['MsgType'] = 3,
['WndName'] = wndName,
['ControlName'] = effectName,
['VecInt'] = {0} -- 0=停止
}
MsgToWnd(msg)
end

3.3.12 StartTimer — 启动定时器

来源Lua接口说明.txt 第 122~130 行

GlobalTool.StartTimer(wndName, timerName)
参数类型说明
wndName字符串窗口名称
timerName字符串XML 中 <Timer> 控件的 Name 属性值

实现:构造 MsgType=4 的消息,VecInt={1} 表示启动。

function GlobalTool.StartTimer(wndName, timerName)
local msg = {
['MsgType'] = 4,
['WndName'] = wndName,
['ControlName'] = timerName, -- 对应 XML 中 <Timer Name="xxx">
['VecInt'] = {1} -- 1=启动
}
MsgToWnd(msg)
end

3.3.13 StopTimer — 停止定时器

来源Lua接口说明.txt 第 133~141 行

GlobalTool.StopTimer(wndName, timerName)

参数同 StartTimer,区别在于 VecInt={0} 表示停止。

function GlobalTool.StopTimer(wndName, timerName)
local msg = {
['MsgType'] = 4,
['WndName'] = wndName,
['ControlName'] = timerName,
['VecInt'] = {0} -- 0=停止
}
MsgToWnd(msg)
end

3.4 GlobalTool 函数完整清单

#函数名来源文件类型用途
1PrintTable(table1)GlobalTool.lua L6工具递归打印 table 到日志
2split(str, reps)GlobalTool.lua L20工具字符串分割为数组
3TableSize(table1)GlobalTool.lua L31工具计算 table 元素数量
4GFnUseShiChangUi()GlobalTool.lua L40判断是否商城版服务器
5Msg_UtilsActResultLua(msg)GlobalTool.lua L49路由核心消息分发器
6CustomUI2026(msg)GlobalTool.lua L74路由CustomUI2026 窗口路由
7XingheYinqing(msg)GlobalTool.lua L83路由XingheYinqing 窗口路由
8ShiYao2024(msg)GlobalTool.lua L93路由ShiYao2024 窗口路由
9pet_huaxing(msg)GlobalTool.lua L103路由pet_huaxing 窗口路由
10PlayEffect(wndName, effectName)Lua接口说明.txt L100控制播放指定特效
11StopEffect(wndName, effectName)Lua接口说明.txt L111控制停止指定特效
12StartTimer(wndName, timerName)Lua接口说明.txt L122控制启动指定定时器
13StopTimer(wndName, timerName)Lua接口说明.txt L133控制停止指定定时器

:其中 69 是路由处理函数(每个对应一个已注册的 ActName),1013 是 Lua接口说明.txt 中推荐放到 GlobalTool 中的工具函数。


四、服务端与客户端通信协议详解

以下协议规范来自 引擎自定义UI说明.txt(引擎官方协议文档)和 QCustomUI-0.txt(实战通讯脚本)。

4.1 服务端 -> 客户端:#SAY 发送消息

触发条件

NPC/QM 脚本的标签后必须加上 CUSTOMUI 关键字:

[@打开玄武炉] CustomUI ; <- 末尾的 CUSTOMUI 触发自定义界面通讯
#IF
TRUE
#ACT
ReadCSVInfo <$CONFIG>\xuanwulu.csv
#SAY
{
"MsgType": "0",
"WndName": "CustomUI2026",
"StrList":
{
"S1": "境界类#融合元神#法宝类#器魂类#装扮类#面具类#灵兽类"
}
}

来源打开玄武炉界面.txt(完整实战触发脚本)

JSON 消息格式

{
"MsgType": "0",
"WndName": "CustomUI2026",
"StrList":
{
"S1": "字符串参数1",
"S2": "字符串参数2"
},
"NumList":
{
"N1": 100,
"N2": 200
}
}
字段是否必填类型说明
MsgType[必填]字符串消息类型:"0"=初始化/打开、"1"=数据推送、"2"=其他
WndName[必填]字符串目标窗口/动作名称(用于 GlobalTool 路由匹配 ActName
StrList[可选]对象字符串参数列表,键名格式 "S1", "S2", ...
NumList[可选]对象整数参数列表,键名格式 "N1", "N2", ... (实战中较少使用)

StrList 与客户端 msg.StrList 的映射

服务端发送:

"StrList": { "S1": "AAA", "S2": "BBB", "S3": "CCC" }

客户端 Lua 收到的 msg.StrList

msg.StrList[1] = "AAA" -- S1 -> 索引 1
msg.StrList[2] = "BBB" -- S2 -> 索引 2
msg.StrList[3] = "CCC" -- S3 -> 索引 3

4.2 客户端 -> 服务端:SendMsg 回传

调用方式

在 Lua 脚本的事件回调函数中调用 SendMsg()

SendMsg({
['ActName'] = "CustomUI2026", -- 动作名称
['ActType'] = 2, -- 包序号
['ListStr'] = {"字符串1", "字符串2"}, -- 字符串参数
['ListInt'] = {100, 200} -- 整数参数
})

参数说明

字段类型说明
ActName字符串动作名称(服务端通过 $CUSTOMPARAM(0) 读取)
ActType整数包序号(服务端通过 $CUSTOMPARAM(1) 读取)
ListStrtable字符串数组,最多 20 个
ListInttable整数数组,最多 20 个

4.3 服务端接收回传:QCustomUI-0.txt

入口机制

引擎接收到客户端的 SendMsg 后,会自动触发 Envir\market_def\QCustomUI-0.txt 脚本中的 [@CustomUI] 标签。

来源引擎自定义UI说明.txt 第 5~12 行(引擎官方协议说明)

$CUSTOMPARAM 变量映射表

来源引擎自定义UI说明.txt 第 7~12 行(引擎官方定义)

索引变量来源说明
(0)$CUSTOMPARAM(0)ActName自定义界面/动作名称
(1)$CUSTOMPARAM(1)ActType返回包序号
(2)$CUSTOMPARAM(2)自动计算字符串参数的数量
(3)$CUSTOMPARAM(3)自动计算整数型参数的数量
(10) ~ (29)$CUSTOMPARAM(10~29)ListStr[1~20]返回的字符串变量(最多 20 个)
(30) ~ (49)$CUSTOMPARAM(30~49)ListInt[1~20]返回的整数型变量(最多 20 个)

映射关系

客户端 SendMsg 参数服务端 QM 变量
ActName = "CustomUI2026"-> $CUSTOMPARAM(0) = "CustomUI2026"
ActType = 2-> $CUSTOMPARAM(1) = 2
$CUSTOMPARAM(2) = 2 (ListStr 有 2 个元素)
$CUSTOMPARAM(3) = 1 (ListInt 有 1 个元素)
ListStr[1] = "倚天剑"-> $CUSTOMPARAM(10) = "倚天剑"
ListStr[2] = "玄铁:5#精钢:3#"-> $CUSTOMPARAM(11) = "玄铁:5#精钢:3#"
ListInt[1] = 15-> $CUSTOMPARAM(30) = 15

完整 QCustomUI 模板(来自实战源码)

来源QCustomUI-0.txt(玄武炉示范项目的完整通讯脚本)

; ============================================================
; 自定义界面通讯入口
; 触发条件:客户端调用 SendMsg() 后引擎自动执行此脚本
; ============================================================

[@CustomUI]
#IF
EQUAL $CUSTOMPARAM(0) CustomUI2026 ; 匹配 ActName
#ACT
Goto @CustomUI2026 ; 跳转到具体处理

; ==================== CustomUI2026 处理 ====================
[@CustomUI2026]
; 根据 ActType 分发到不同处理逻辑
#IF
EQUAL $CUSTOMPARAM(1) 1 ; ActType=1:请求某分类下的物品列表
#ACT
Goto @CustomUI2026_1

#IF
EQUAL $CUSTOMPARAM(1) 2 ; ActType=2:执行合成操作
#ACT
Goto @CustomUI2026_2

; -------------------- ActType=1:请求物品列表 --------------------
[@CustomUI2026_1]
#IF
TRUE
#ACT
SENDMSG 5 用户选择了分类-<$CUSTOMPARAM(10)> ; $CUSTOMPARAM(10)=分类索引

; 从 CSV 文件中读取该分类的所有物品数据
MOV S$物品列表 ""
MOV D$计数 $CSVINFO[duanzao.csv].Field(yeqian0).Value($CUSTOMPARAM(10))
FORLOOP D$计数 <= $CSVINFO[duanzao.csv].Field(yeqian0).LastValue($CUSTOMPARAM(10))
INC S$物品列表 $CSVINFO[duanzao.csv].Index($STR(D$计数))
INC S$物品列表 #
INC D$计数 1
ENDFORLOOP

; 通过 #SAY 将物品列表发送回客户端
#IF
TRUE
#SAY
{
"MsgType": "1",
"WndName": "CustomUI2026",
"StrList":
{
"S1": "<$STR(S$物品列表)>"
}
}

; -------------------- ActType=2:执行合成 --------------------
[@CustomUI2026_2]
#IF
TRUE
#ACT
; 读取客户端传来的合成参数
SENDMSG 5 正在合成物品-<$CUSTOMPARAM(30)> ; $CUSTOMPARAM(30)=物品全量索引
SENDMSG 5 合成目标物品名-<$CUSTOMPARAM(10)> ; $CUSTOMPARAM(10)=DBName
SENDMSG 5 材料清单-<$CUSTOMPARAM(11)> ; $CUSTOMPARAM(11)=材料字符串
; 这里编写实际的合成逻辑(扣除材料、给予成品等)

4.4 完整通信实战追踪(以玄武炉为例)

下面以用户从打开界面到完成一次合成的完整过程为例,追踪每一轮通信的详细数据:

第一轮:打开界面(服务端 -> 客户端)

触发:玩家与 NPC 对话,触发 [@打开玄武炉] CustomUI

服务端发送打开玄武炉界面.txt):

[@打开玄武炉] CustomUI
#IF
TRUE
#ACT
ReadCSVInfo <$CONFIG>\xuanwulu.csv
#SAY
{
"MsgType": "0",
"WndName": "CustomUI2026",
"StrList":
{
"S1": "境界类#融合元神#法宝类#器魂类#装扮类#面具类#灵兽类"
}
}

客户端处理流程

1. 引擎接收 #SAY JSON
2. GlobalTool.Msg_UtilsActResultLua(msg) 被调用
3. msg.ActName = "CustomUI2026" -> 匹配 -> CustomUI2026(msg)
4. msg.MsgType = "0" -> 首次消息
5. -> OperWindow("CustomUIWnd", "create", 0, true) // 创建窗口
6. -> LuaMsgToLuaWnd("CustomUIWnd", msg) // 转发消息
7. CustomUIWnd.Msg(msg) 被调用
8. 解析 msg.StrList[1] = "境界类#融合元神#法宝类#..."
9. 用 "#" 分割成菜单项 -> 写入 DATA['Menu']
10. UpdateWnd("CustomUIWnd") -> 界面渲染显示
11. 自动发送 SendMsg({ActName:"CustomUI2026", ActType:1, ListInt:{1}})
-> 请求第一个分类的物品列表

客户端 Lua 处理代码CustomUIWnd.lua 第 246~262 行):

function CustomUIWnd.Msg(msg)
if not msg then return end

if msg['MsgType'] == 0 then
-- 解析菜单列表
DATA['Menu'] = {}
if msg.StrList and msg.StrList[1] then
local mItems = CustomUIWnd.Split(msg.StrList[1], "#")
for i, txt in ipairs(mItems) do
if txt ~= "" then
table.insert(DATA['Menu'], {
Text = txt,
BtnStatus = (i == 1 and 1 or 0), -- 默认选中第一个
ClickMenu = "ClickMenu&" .. i
})
end
end
end
UpdateWnd(LocalWndName)
-- 自动请求第一页物品数据
SendMsg({ ['ActName'] = "CustomUI2026", ['ActType'] = 1, ['ListInt'] = {1} })
end
end

第二轮:服务端返回物品列表(服务端 -> 客户端)

触发:上一轮客户端发送了 ActType=1 的请求,QM 脚本处理后返回

服务端发送QCustomUI-0.txt@CustomUI2026_1#SAY):

{
"MsgType": "1",
"WndName": "CustomUI2026",
"StrList": {
"S1": "倚天剑,1201,玄铁,5,精钢,3,,0,,传说中的神兵#屠龙刀,1202,玄铁,8,寒铁,4,,0,,天下无双#..."
}
}

数据编码约定(来自 CustomUIWnd.lua 第 269~286 行的解析逻辑):

物品之间用 "#" 分隔
每个物品的字段之间用 "," 分隔
字段顺序:DBName,Look,材料1名,材料1数量,材料2名,材料2数量,材料3名,材料3数量,描述

示例解析:
"倚天剑,1201,玄铁,5,精钢,3,,0,,传说中的神兵"
[down] split by ","
f[1]="倚天剑" f[2]="1201" f[3]="玄铁" f[4]="5"
f[5]="精钢" f[6]="3" f[7]="" f[8]="0"
f[9]="传说中的神兵"

-> DBName="倚天剑" Look=1201 material1="玄铁" material1_num=5
material2="精钢" material2_num=3 ItemDesc="传说中的神兵"

客户端 Lua 处理代码CustomUIWnd.lua 第 265~296 行):

elseif msg['MsgType'] == 1 then
DATA['RawItems'] = {}
local total = 0
if msg.StrList then
for _, itemStr in pairs(msg.StrList) do
if itemStr ~= "" then
-- 按 "#" 分割出各个物品
local subItems = CustomUIWnd.Split(itemStr, "#")
for _, oneItem in ipairs(subItems) do
if oneItem ~= "" then
-- 按 "," 分割出各字段
local f = CustomUIWnd.Split(oneItem, ",")
if f[2] and f[3] then
total = total + 1
DATA['RawItems'][total] = {
DBName = f[2],
Look = tonumber(f[3]) or 0,
material1 = f[4] or "",
material1_num = tonumber(f[5]) or 0,
material2 = f[6] or "",
material2_num = tonumber(f[7]) or 0,
material3 = f[8] or "",
material3_num = tonumber(f[9]) or 0,
ItemDesc = f[10] or ""
}
end
end
end
end
end
end
DATA.total = total
DATA.CurPage = 0
DATA.MaxPage = math.max(1, math.ceil(total / PAGE_SIZE))
CustomUIWnd.RefreshPageData() -- 截取当前页数据并刷新界面
end

第三轮:用户点击合成(客户端 -> 服务端 -> 服务端处理)

触发:用户选中一个物品后点击"合成"按钮

客户端发送CustomUIWnd.lua 第 132~137 行):

SendMsg({
['ActName'] = "CustomUI2026", -> $CUSTOMPARAM(0)
['ActType'] = 2, -> $CUSTOMPARAM(1)
['ListStr'] = {tostring(DATA['DBName']), matStr}, -> $CUSTOMPARAM(10)~(11)
['ListInt'] = {tonumber(DATA['CurIndex'])} -> $CUSTOMPARAM(30)
})

假设当前选中DBName="倚天剑", CurIndex=15, 材料="玄铁:5#精钢:3#"

则实际发送的数据

ActName = "CustomUI2026"
ActType = 2
ListStr[1] = "倚天剑" -> $CUSTOMPARAM(10)
ListStr[2] = "玄铁:5#精钢:3#" -> $CUSTOMPARAM(11)
ListInt[1] = 15 -> $CUSTOMPARAM(30)

服务端接收处理QCustomUI-0.txt@CustomUI2026_2):

[@CustomUI2026_2]
#IF
TRUE
#ACT
SENDMSG 5 正在合成物品-<$CUSTOMPARAM(30)> ; 输出: 正在合成物品-15
SENDMSG 5 合成目标-<$CUSTOMPARAM(10)> ; 输出: 合成目标-倚天剑
SENDMSG 5 材料-<$CUSTOMPARAM(11)> ; 输出: 材料-玄铁:5#精钢:3#
; 在此处编写实际合成逻辑:
; 1. 检查玩家背包是否有足够材料
; 2. 扣除材料
; 3. 给予成品
; 4. 可选:#SAY 返回合成结果给客户端

五、实战案例:玄武炉合成界面

完整源码位于:自定义界面说明\玄武炉示范\ 目录

5.1 功能概览

功能模块说明
分类菜单左侧 7 个分类标签(境界类/融合元神/法宝类/器魂类/装扮类/面具类/灵兽类),水平排列,点击切换
物品网格中间 3 行 × 6 列 = 每页 18 个物品,支持分页翻页
物品详情右侧大图预览 + 描述文字 + 材料清单
分页导航底部页码按钮,动态生成
合成操作选中物品后显示"合成"按钮,点击发送合成请求

5.2 完整 XML 布局

<Window CloseBtnID="33018" CloseBtnX="823" CloseBtnY="9"
BackTextureIndex="34719" bSearchChildGood="1">

<!-- 标题 -->
<Text PosX="430" PosY="20" Text="$label"
FontSize="24" Font="1" FontColor="0xFFEA9E54" FontFlag="24"/>

<!-- 右侧:物品描述(多行文本框) -->
<MarkView PosX="585" PosY="60" Row="12" RowHeight="16"
Font="0" Color="0xFFEA9E54" TagText="$ItemDesc"
Width="216" Height="124"/>

<!-- 右侧标题区特效 -->
<effect Name="suceft1" PosX="585" PosY="35" TextID="34722" PlayMode="2"/>

<!-- ===== 左侧:分类菜单(水平排列,最多7个) ===== -->
<unit PosX="16" PosY="52" Width="60" Height="22"
foreachKey="$eachKey" foreachVal="$eachVal"
foreachData="$Menu" MaxRow="7" IsVertical="0" Show="1">
<button PosX="0" PosY="0" Width="60" Height="22"
Text="$eachVal.Text"
NormalTexture="33435" HighlightTexture="33435"
PushedTexture="33435"
OnClick="$eachVal.ClickMenu"
Status="$eachVal.BtnStatus"/>
</unit>

<!-- ===== 中间:物品网格(外层3行垂直 × 内层6列水平) ===== -->
<unit PosX="15" PosY="102" Width="558" Height="116"
foreachKey="$eachKey" foreachVal="$eachVal"
foreachData="$Item" MaxRow="3" IsVertical="1" Show="1">
<unit PosX="0" PosY="0" Width="93" Height="116"
foreachKey="$eachKey" foreachVal="$eachVal"
foreachData="$eachVal" MaxRow="6" IsVertical="0" Show="1">
<button PosX="0" PosY="0"
NormalTexture="34721" HighlightTexture="34720"
PushedTexture="34720"
OnClick="$eachVal.OnClickDetail"
Status="$eachVal.BtnStatus"/>
<Good PosX="25" PosY="25"
DbName="$eachVal.DBName" Look="$eachVal.Look"
Bind="0" BackTex="0" Num="1" Show="1"/>
<Text PosX="50" PosY="95" Width="93"
Text="$eachVal.DBName"
FontSize="12" FontColor="0xFFC2934C" FontFlag="12"/>
<Trigger PosX="0" PosY="0" Width="93" Height="116"
OnClick="$eachVal.OnClickDetail" Show="1"/>
</unit>
</unit>

<!-- 右侧详情区标题(仅选中物品时显示) -->
<Text PosX="680" PosY="373" Text="- 详情 -"
FontSize="12" Font="0" FontColor="0xFFEA9E54"
FontFlag="4" Show="$TextShow"/>

<!-- 右侧:材料列表(垂直排列,最多3行) -->
<unit PosX="585" PosY="395" Width="200" Height="20"
foreachKey="$eachKey" foreachVal="$eachVal"
foreachData="$material" MaxRow="3" IsVertical="1"
Show="$TextShow">
<Text PosX="0" PosY="5" Text="$eachVal.name"
FontSize="12" Font="0" FontColor="0xFFE6B260"
FontFlag="0" Show="$TextShow"/>
<Image PosX="110" PosY="2" TextID="33242" Show="$TextShow"/>
<Text PosX="145" PosY="6" Text="$eachVal.Num"
FontSize="12" Font="0" FontColor="0xFFE6C800"
FontFlag="0" Show="$TextShow"/>
</unit>

<!-- 右侧:选中物品的大图预览 -->
<Good PosX="685" PosY="235" DbName="$DBName" Look="$Look"
BackTex="0" Tips="$DBName" Num="1" Show="$TextShow"/>

<!-- 合成按钮旁的特效 -->
<effect Name="suceft2" PosX="275" PosY="-30" TextID="27741" PlayMode="0"/>

<!-- 合成按钮 -->
<button PosX="667" PosY="463" Text="合成"
NormalTexture="33024" HighlightTexture="33025"
PushedTexture="33025"
OnClick="OnClickExt" Show="$TextShow"/>

<!-- 底部:分页按钮(水平排列,最多6个) -->
<unit PosX="230" PosY="458" Width="50" Height="30"
foreachKey="$eachKey" foreachVal="$eachVal"
foreachData="$PageList" MaxRow="6" IsVertical="0" Show="1">
<button PosX="0" PosY="0" Width="65" Height="25"
Text="$eachVal.Text"
NormalTexture="0" HighlightTexture="0"
PushedTexture="0"
OnClick="$eachVal.ClickCall"
Status="$eachVal.BtnStatus"/>
</unit>
</Window>

5.3 关键 Lua 逻辑

分页刷新逻辑

local PAGE_SIZE = 18 -- 每页物品数
local ROW_SIZE = 6 -- 每行物品数

function CustomUIWnd.RefreshPageData()
-- 1. 生成分页按钮数据
DATA['PageList'] = {}
for i = 1, DATA.MaxPage do
table.insert(DATA['PageList'], {
Text = "第" .. i .. "页",
BtnStatus = (i == (DATA.CurPage + 1)) and 1 or 0,
ClickCall = "JumpToPage&" .. i
})
end

-- 2. 从 RawItems 全量数据截取当前页 -> 填充到 DATA['Item'][行][列]
DATA['Item'] = { [1] = {}, [2] = {}, [3] = {} }
local startOffset = DATA.CurPage * PAGE_SIZE
for i = 1, PAGE_SIZE do
local raw = DATA['RawItems'][startOffset + i]
if raw then
local r = math.floor((i - 1) / ROW_SIZE) + 1
local c = i - (r - 1) * ROW_SIZE
if r <= 3 then
DATA['Item'][r][c] = {
DBName = raw.DBName,
Look = raw.Look,
material1 = raw.material1,
material1_num = raw.material1_num,
material2 = raw.material2,
material2_num = raw.material2_num,
material3 = raw.material3,
material3_num = raw.material3_num,
ItemDesc = raw.ItemDesc,
BtnStatus = 0,
OnClickDetail = "OnClickDetail&" .. i
}
end
else break end
end
UpdateWnd(LocalWndName)
end

物品选中逻辑

function CustomUIWnd.OnClickDetail(param)
local idx = tonumber(param)
if not idx then return end

-- 计算在 3×6 网格中的行列位置
local targetRow = math.floor((idx - 1) / ROW_SIZE) + 1
local targetCol = idx - (targetRow - 1) * ROW_SIZE

-- 遍历所有格子:目标格选中(2),其余取消(0)
for i = 1, 3 do
for j = 1, ROW_SIZE do
local item = DATA['Item'][i][j]
if item then
if i == targetRow and j == targetCol then
item.BtnStatus = 2 -- 选中状态
DATA['DBName'] = item.DBName -- 记录选中物品
DATA['Look'] = item.Look
DATA['ItemDesc'] = item.ItemDesc
-- 构建材料列表
DATA['material'] = {}
if item.material1 ~= "" then
table.insert(DATA['material'],
{name=item.material1, Num=item.material1_num})
end
if item.material2 ~= "" then
table.insert(DATA['material'],
{name=item.material2, Num=item.material2_num})
end
if item.material3 ~= "" then
table.insert(DATA['material'],
{name=item.material3, Num=item.material3_num})
end
DATA['TextShow'] = 1 -- 显示详情区
DATA['CurIndex'] = (DATA.CurPage * PAGE_SIZE) + idx
else
item.BtnStatus = 0 -- 未选中
end
end
end
end
UpdateWnd(LocalWndName)
CustomUIWnd.PlayEffect(LocalWndName, "suceft1") -- 播放选中特效
end

六、开发注意事项

6.1 必须遵守的规则

规则详细说明违反后果
文件命名Lua 文件名必须与 OperWindow() 中使用的窗口名完全一致窗口无法创建,报错
两个导出函数必须实现 GetRenderXml()GetRenderData()引擎无法获取布局和数据,界面空白
return 语句文件末尾必须有 return ModuleName模块无法被加载
$变量引用XML 中用 $变量名 引用 DATA 中的字段(如 "$label" -> DATA['label']数据不显示
foreachData 格式foreachData 的值为 DATA 的键名字符串,写作 "$Menu" 而非 $Menuforeach 不展开
嵌套 foreach内层 foreachData 引用外层 $eachVal不加 $ 符号也不加引号,写作 foreachData="$eachVal"内层数据为空
OnClick 参数& 连接("FuncName&参数"),Lua 中用 tonumber(param) 解析参数传递失败
路由注册新增窗口必须在 GlobalTool.Msg_UtilsActResultLua() 中添加 if 分支消息无法到达窗口
CUSTOMUI 标记NPC 脚本 [@标签] 后必须加 CUSTOMUI 关键字不触发 QCustomUI-0.txt
修改后 Refresh每次 DATA 修改后必须调用 UpdateWnd()界面不更新

6.2 常见问题排查

问题现象可能原因排查方法
界面完全不显示OperWindow() 未被调用;窗口名与文件名不一致检查 GlobalTool 路由是否有对应分支
界面白屏/无内容XML 语法错误;DATA 为空;未 return 模块检查 XML 标签闭合、DATA 初始化
数据不显示$变量名 与 DATA 键名不匹配确保 XML 中 $Menu 对应 DATA['Menu'](完全一致)
点击按钮无反应OnClick 函数名拼写错误;函数未定义检查函数是否存在,注意大小写敏感
foreach 列表为空foreachData 写错(多了/少了 $ 或引号)对照规则表检查
只显示第一项MaxRow 设置过小增大 MaxRow 值
分页数据错乱PAGE_SIZE/ROW_SIZE 与 XML 中 MaxRow 不一致统一数值
SendMsg 服务端没收到脚本标签缺 CUSTOMUI[@标签] 后加上 CUSTOMUI
$CUSTOMPARAM 取值不对索引范围错误(字符串 1029,整数 3049)检查索引是否在合法范围内

6.3 数据编码约定(服务端组装 StrList 时)

当需要在一条字符串中传递多个物品的多个字段时,玄武炉项目采用了两级分隔符编码方案:

一级分隔 "#" -> 分隔不同物品
二级分隔 "," -> 分隔同一物品的不同字段

编码(服务端 QM 脚本组装):

物品1名,物品1Look,材料1名,材料1数量,材料2名,材料2数量,...,描述#物品2名,物品2Look,...

解码(客户端 Lua 脚本解析):

-- 第一级:按 "#" 分割出各个物品
local subItems = CustomUIWnd.Split(itemStr, "#")
-- 第二级:按 "," 分割出各字段
local f = CustomUIWnd.Split(oneItem, ",")
-- f[1]=DBName, f[2]=Look, f[3]=material1, f[4]=material1_num, ...

这种编码方式不受 JSON 格式限制,可以在单个字符串中传递大量结构化数据。