如何创建 GUI 配置界面
原文:HOWTO:_Create_GUI_Config_Options,发表于 WoWWiki
译者:流润之风
为插件编写 GUI 的配置/选项对话框并不容易,希望这一篇 "如何" 可以有所帮助。通常,我们都是添加配置对话框到某个已完成的插件,所以本文在讲解的时候也会采用一个已存在的插件作为例子,但我们鼓励大家在学习本文的时候能够使用自己编写的插件。
参考材料(非必需)
Scheid 的 myClock
本文的目的并不是以任何形式重写 myClock,而且你也可以注意到这里的代码做过多处改动。不过,本文是基于 myClock 之上的,而且在填补完代码间空白之后,你可以得到一个类似于 myClock 的基本插件。
Contents
TOC 文件
参考材料(可选阅读)
我们需要在 TOC 文件中添加对话框的 .xml 文件名。另外,为了让对话框能够记忆设置,还要使用 SavedVariables 指令。
myClock.toc: 修改前
## Interface: 1600 ## Title: myClock ## Notes: Clock replacing day-night indicator myClockIndicatorFrame.xml
我们需要:
- 添加配置对话框的 frame .xml
添加 SavedVariables 指令
- 添加可选依赖 myAddOns
本文用 myAddons 来显示配置对话框,这样比创建斜杠命令要简单得多。如何使用斜杠命令的内容不在文本的讨论范围之内,建议阅读如何创建SlashCommand。
myClock.toc: 修改后
## Interface: 1600 ## Title: myClock ## Notes: Clock replacing day-night indicator ## OptionalDeps: myAddOns ## SavedVariables: myClockConfig myClockIndicatorFrame.xml myClockConfigFrame.xml
- 表示可选依赖。myAddons不是必需的,但是推荐采用。
- 让我们可以使用以帐号为单位、跨会话保存的变量。
myClockIndicatorFrame.xml
- 主插件框体
myClockConfigFrame.xml
- 配置插件框体
从 SavedVariables 读取配置
参考材料(可选阅读)
通过 .toc 文件中声明的变量 'myClockConfig',现在我们可以载入和保存插件的配置参数。需要记住的是,SavedVariable 是以帐号为单位的,而不是以 realm 和 角色为单位的,所以需要用表格来为每个角色保存设置。
主框体的 .xml
我们将修改主插件的框体,在本例中,即用于显示当前时间的 'myClockIndicatorFrame.xml'。
我们需要:
添加一个 OnLoad 脚本行为 添加一个 OnEvent 脚本行为 OnLoad 用于监视 'VARIABLES_LOADED' 事件(此时 myClockConfig 将获取保存的变量值)。然后,OnEvent 将在事件触发时真正地做某些事情。
myClockIndicatorFrame.xml
<Ui ...> <Frame name="myClockIndicatorFrame" ...> ... <Scripts> <OnLoad> myClockIndicatorFrame_OnLoad(); </OnLoad> <OnEvent> myClockIndicatorFrame_OnEvent(); </OnEvent> ... </Scripts> </Frame> </Ui>
主框体的 .lua
Globals
参考材料(可选阅读)
我们需要:
- 获取 realm 和角色名
- 确保变量 'myClockConfig' 存在
- 为 myAddons 创建细节
- 创建默认配置值
myClockIndicatorFrame.lua
...
-- Globals
-- so we know when our configuration is loaded
myClock_variablesLoaded = false;
-- for configuration saving
myClockRealm = GetCVar("realmName");
myClockChar = UnitName("player");
-- details used by myAddOns
myClock_details = {
name = "myClock",
frame = "myClockIndicatorFrame",
optionsframe = "myClockConfigFrame"
};
-- default config settings
local myClockConfig_defaultOn = true; -- addon enabled?
local myClockConfig_defaultTime24 = true; -- 24 hour format?
local myClockConfig_defaultOffset = 0; -- time offset?
...myClock_variablesLoaded, myClock_realm, myClock_char 用于创建一个基于每个 realm、每个角色的设置表。因为 SavedVariable 只能是以帐号为单位的。通过这个表,就能在不影响所有角色的情况下修改单个角色的配置。
myClock_details myAddons 将会显示插件的信息,因而需要主框体和配置框体的名字,从而正确显示配置对话框。
myClockConfig_default 这些变量是配置项的默认设置值,用户可以在配置对话框中进行修改。同时它们也是角色第一次使用插件时所采用的设置,因为此时还没有进行过保存。
注册事件
由于我们使用 VARIABLES_LOADED 事件来判断什么时候才能安全地使用变量,
我们需要:
- 注册 VARIABLES_LOADED 事件
- 当事件触发时调用一个函数
myClockIndicatorFrame.lua: 内部任何地方
...
-- OnLoad
function myClockIndicatorFrame_OnLoad()
-- register events
this:RegisterEvent("VARIABLES_LOADED"); -- eventually will call OnEvent
...
end
...
myClockIndicatorFrame.lua: anywhere inside
...
-- OnEvent
function myClockIndicatorFrame_OnEvent()
-- VARIABLES_LOADED event
if ( event == "VARIABLES_LOADED" ) then
-- execute event code in this function
myClockIndicatorFrame_VARIABLES_LOADED();
end
end
...
自定义更新函数
现在我们需要一个在载入变量时可供调用的函数。
它需要:
载入默认的 SavedVariable 值
- 将插件注册到 myAddons
- 将配置已经载入这个状态记录下来
myClockIndicatorFrame.lua
...
function myClockIndicatorFrame_VARIABLES_LOADED()
-- initialize our SavedVariable
if ( not myClockConfig ) then
myClockConfig = {};
end
if ( not myClockConfig[myClockRealm] ) then
myClockConfig[myClockRealm] = {};
end
if ( not myClockConfig[myClockRealm][myClockChar] ) then
myClockConfig[myClockRealm][myClockChar] = {};
end
-- load each option, set default if not there
if ( not myClockConfig[myClockRealm][myClockChar].on ) then
myClockConfig[myClockRealm][myClockChar].on = myClockConfig_defaultOn;
end
if ( not myClockConfig[myClockRealm][myClockChar].time24 ) then
myClockConfig[myClockRealm][myClockChar].time24 = myClockConfig_defaultTime24;
end
if ( not myClockConfig[myClockRealm].offset ) then
myClockConfig[myClockRealm].offset = myClockConfig_defaultOffset;
end
-- record that we have been loaded
myClock_variablesLoaded = true;
-- we know other addons have been "loaded" now
-- optional dependance on myAddOns, leads to our config panel
if( myAddOnsFrame_Register ) then
myAddOnsFrame_Register( myClock_details );
end
-- configuration might have changed
myClock_ConfigChange();
end
...这个函数非常简单,它首先创建一个新表,在插件、realm范围内,以角色为单位存放配置项,然后检查每个配置项,确保它们已被载入,如果没有载入,那么把配置项设为默认值。我们还加入了 myAddons 支持,通过 globals 中声明的表将插件名、框体名、配置框体名告知 myAddons。
最后一行很有趣,我们调用了 myClock_ConfigChange()。这个函数将会真正地用设置值更改插件。在钟的例子中,大多数选项('time24', 'offset')实际是在 OnUpdate 中,而选项 'on' 则是在这里处理。
我们的配置变更会:
- 开启/关闭插件
myClockIndicatorFrame.lua 中
...
function myClock_ConfigChange()
-- make sure that our profile has been loaded before allowing this function to be called
if ( not myClock_variablesLoaded ) then -- config not loaded
myClockIndicatorFrame:Hide(); -- turn our mod off
return;
end
-- make sure to use the frame's name here, cannot rely on 'this' to mean the main frame
if ( myClockConfig[myClockRealm][myClockChar].on ) then
myClockIndicatorFrame:Show(); -- show our mod frame
else
myClockIndicatorFrame:Hide(); -- hide our mod frame
end
end
...另外,添加一个将所有值设为默认值的函数也会很有用……这儿有一个
myClockIndicatorFrame.lua 中
...
-- reset to defaults
function grokClock_ConfigToDefault()
-- make sure that our profile has been loaded before allowing this function to be called
if ( not myClock_variablesLoaded ) then -- config not loaded
myClockIndicatorFrame:Hide(); -- turn our mod off
return;
end
-- set our profile to defaults
myClockConfig[myClockRealm][myClockChar].on = myClockConfig_defaultOn;
myClockConfig[myClockRealm][myClockChar].time24 = myClockConfig_defaultTime24;
myClockConfig[myClockRealm][myClockChar].offset = myClockConfig_defaultOffset;
-- frame name used since called from various places
-- set the location of the frame... in case they dragged it away
myClockIndicatorFrame:SetPoint("TOPLEFT", "MinimapCluster", "TOPLEFT", 122, -28);
-- wow automatically saves the location of frames on the screen!
-- make sure the defaults are loaded onto our mod frame
myClock_ConfigChange();
end
...
编写配置框体
既然你的插件使用了 SavedVariable,并从中读取保存值(或使用默认值),那么非常需要一个配置框体来让用户来进行修改!流行的实现方法很多,比如创建斜杠命令(请参考如何创建SlashCommand),或创建右键菜单,不过我们选择了一种简单的方法--弹出一个框体。在游戏中,你可以按下 'ESC' 键、选择 'Addons' 来打开 myAddons。然后,在 myAddons 上点击你的插件名,然后再点击右下角的 'Options' 按钮。不过,现在还不能打开什么,因为你还没编写这个框体呢!
配置框体 .xml
具有背景的基本框体
让我们来填充一个基本的配置框体:
myClockConfigFrame.xml
<Ui ...>
<Script file="myClockConfigFrame.lua"/>
<Frame name="myClockConfigFrame"
toplevel="true" parent="UIParent" frameStrata="DIALOG"
hidden="true" enableMouse="true">
<Size><AbsDimension x="260" y="280"/></Size>
<Anchors><Anchor point="CENTER"/></Anchors>
<Backdrop bgFile="Interface\DialogFrame\UI-DialogBox-Background"
edgeFile="Interface\DialogFrame\UI-DialogBox-Border" tile="true">
<BackgroundInsets>
<AbsInset left="11" right="12" top="12" bottom="11"/>
</BackgroundInsets>
<TileSize><AbsValue val="32"/></TileSize>
<EdgeSize><AbsValue val="32"/></EdgeSize>
</Backdrop>
</Frame>
</Ui>现在,我们有了一个配置框体,如果进入游戏,你就能看到这个杰作了。你可以随意调节它的大小,而 ANCHOR point="CENTER" 会确保它始终位于屏幕的中间,而不论分辨率设为多少。
也许你会想要为它加上某个标题,标明这是属于什么插件的选项框,那么请看下面的 Layer:
myClockConfigFrame.xml 中
...
<Frame ...>
...
<Backdrop ...>
...
</Backdrop>
<Layers>
<Layer level="ARTWORK">
<Texture file="Interface\DialogFrame\UI-DialogBox-Header">
<Size><AbsDimension x="256" y="64"/></Size>
<Anchors>
<Anchor point="TOP">
<Offset><AbsDimension x="0" y="12"/></Offset>
</Anchor>
</Anchors>
</Texture>
</Layer>
<Layer level="OVERLAY">
<FontString inherits="GameFontNormal" text="Clock Config">
<Anchors>
<Anchor point="TOP" relativeTo="$parent"></Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
</Frame>
...
添加关闭和默认按钮
上面的代码会显示一个文本框,你可以注意到 FontString 的文本属性设置为 "Clock Config"--你可以将它改为其他任何内容。如果你在做本地化,甚至可以是一个常量!你还会注意到,这个框体一旦显示之后就无法关掉了!好吧,让我们添加一个 'Close' 按钮来修正这个问题:
myClockConfigFrame.xml 内
...
<Frame ...>
...
<Frames>
...
<Button name="$parentButtonClose" inherits="OptionsButtonTemplate" text="Close">
<Anchors>
<Anchor point="BOTTOMRIGHT">
<Offset><AbsDimension x="-12" y="16"/></Offset>
</Anchor>
</Anchors>
<Scripts>
<OnClick> myClockConfigFrame:Hide(); </OnClick>
</Scripts>
</Button>
...
</Frames>
...
</Frame ...>
...这个锚固在配置框体右下角的按钮简单地告诉框体,隐藏起来吧。
既然到了这里,那么再让我们添加一个默认按钮到框体的左下角吧。
myClockConfigFrame.xml 内
...
<Frame ...>
...
<Frames>
...
<Button name="$parentButtonToDefault" inherits="OptionsButtonTemplate" text="Defaults">
<Anchors>
<Anchor point="BOTTOMLEFT">
<Offset><AbsDimension x="13" y="16"/></Offset>
</Anchor>
</Anchors>
<Scripts>
<OnClick> grokClock_ConfigToDefault(); </OnClick>
</Scripts>
</Button>
...
</Frames>
...
</Frame ...>
...你可以注意到,这里我们要做的只是调用 'myClockIndicatorFrame.lua' 中编写的 ConfigToDefault 函数--它会替我们更新插件!现在我们就可以在搞乱配置后撤销了。
Checkbox 和滑动条
框体已经没什么大问题了,现在是时候加上用于偏移的 checkbox 和滑动条了(下拉菜单过于复杂,不在本文的介绍之内)。
你的 checkbox 配置类似于:
myClockConfigFrame.xml 内
...
<Frame ...>
...
<Frames>
...
<CheckButton name="$parentCheckButtonOn" inherits="OptionsCheckButtonTemplate">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parent">
<Offset><AbsDimension x="20" y="-30"/></Offset>
</Anchor>
</Anchors>
<Scripts>
<OnLoad> getglobal(this:GetName().."Text"):SetText("On"); </OnLoad>
<OnClick> myClockConfigFrameOption_OnClick(); </OnClick>
</Scripts>
</CheckButton>
...
</Frames>
...
</Frame ...>
...你可以注意到,我使用了 '$parent' 和 'this' 变量来提高通用性,这样代码就能用在任何地方了。如果这是第二个 checkbox,那么我的 Anchor relativeTo 就会是 '$parentCheckButtonOn',依此类推,这样一来,对话框中的每一项都排列在其他一项稍微下面的地方。当然,也可以去掉所有的 'relativeTo',采用绝对定位的方式。
你还可以看到,CheckButton 用一个 OnLoad 来设置文本,因为这是一个自动创建的 FontString。同样,我也用了 'this:GetName().."Text"' 来提高通用性。这里我将文字设为 "On",你也可以将它设为其他任何内容,甚至是一个本地化变量!
OnClick 简单地调用了一个 lua 函数,但有一点很重要,我们所有的 CheckButton 都将使用这个函数。是的,我们所有的 CheckButton 都可以用同一个函数!
在这个例子中,我们需要重复 CheckButton 的 xml 代码两次,一次用于 'on',一次用于 'time24'。所以,你再编写另一个 checkbox 吧。要留意偏移!请看下面的滑动条示例中是如何处理与上一项的偏移的。
我们的滑动条代码应该类似于:
myClockConfigFrame.xml 内
...
<Frame ...>
...
<Frames>
...
<Slider name="$parentSliderOffset" inherits="OptionsSliderTemplate">
<Size>
<AbsDimension x="220" y="16"/>
</Size>
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parentCheckButtonTime24">
<Offset><AbsDimension x="0" y="-40"/></Offset>
</Anchor>
</Anchors>
<Scripts>
<OnLoad>
getglobal(this:GetName().."Text"):SetText("Offset");
getglobal(this:GetName().."High"):SetText("12");
getglobal(this:GetName().."Low"):SetText("-12");
this:SetMinMaxValues(-12,12);
this:SetValueStep(1);
</OnLoad>
<OnValueChanged> grokClockConfigFrameOption_OnClick(); </OnValueChanged>
</Scripts>
</Slider>
...
</Frames>
...
</Frame>
...你可以再次注意到,我们使用了 Anchor relativeTo,指向上一个 checkbox,即 $parentCheckButtonTime24。我们还再一次动态获取了 WoW 为我们创建的 FontString,如设置为 "Offset" 的 'Text'。另外,由于这是一个滑动条,我们还有 FontString "High" 和 "Low",我们还要设置滑动条的最小和最大值,以及每次移动滑动条时增加 / 减少的数值。最让你吃惊的……是的……我们的滑动条也使用那个函数!一个用于所有基础配置选项的函数!
框体脚本
将当前设置载入到框体也很重要,由于我们可能会通过斜杠命令或其他什么方式改变设置,所以最好使用 OnShow 事件。
myClockConfigFrame.xml 内
...
<Frame ...>
...
<Scripts>
<OnShow> myClockConfigFrame_OnShow(); </OnShow>
</Scripts>
...
</Frame>
...
配置框体的 .lua
配置框体的 lua 文件划分为两个函数,一个用于显示框体时调用,另一个用于框体中的选项(如 checkbox、或滑动条)改变时调用。
myClockConfigFrame.lua 内
-- OnShow
function myClockConfigFrame_OnShow()
-- make sure our profile has been loaded
if ( not myClock_variablesLoaded ) then -- config not loaded
this:Hide(); -- hide our config pane
return;
end
-- read settings from profile, and change our checkbuttons and slider to represent them
getglobal(this:GetName().."CheckButtonOn"):SetChecked( myClockConfig[myClockRealm][myClockChar].on );
getglobal(this:GetName().."CheckButtonTime24"):SetChecked( myClockConfig[myClockRealm][myClockChar].time24 );
getglobal(this:GetName().."SliderOffset"):SetValue( myClockConfig[myClockRealm].offset );
end你可以再次注意到,我们需要确保配置已经存在(或者说变量已经载入)。代码中也使用了 getglobal() 函数来构建控件的名称,所以无论框体的名字是什么都没有关系。然后是用 SetChecked() 和 SetValue() 之类的方法让对话框显示正确的值。
最后一步,确保在修改对话框的同时修改配置。类似于这样的代码:
myClockConfigFrame.lua 内
-- OnClick
function grokClockConfigFrameOption_OnClick()
-- make sure our profile has been loaded
if ( not myClock_variablesLoaded ) then -- config not loaded
this:GetParent():Hide(); -- hide our config pane (this is now a checkbox)
return;
end
-- read setting out of checkbox (or slider) and put into profile
-- use this:GetName() to know which checkbox was hit.
if ( this:GetName() == (this:GetParent():GetName().."CheckButtonOn" ) ) then
myClockConfig[myClockRealm][myClockChar].on = this:GetChecked(); -- set profile
elseif ( this:GetName() == (this:GetParent():GetName().."CheckButtonTime24" ) ) then
myClockConfig[myClockRealm][myClockChar].time24 = this:GetChecked();
elseif ( this:GetName() == (this:GetParent():GetName().."SliderOffset" ) ) then
myClockConfig[myClockRealm].offset = this:GetValue();
end
-- configuration was changed, make sure our addon changes too!
-- notice our addon is changed right away, not when we hit 'done'.
myClock_ConfigChange();
end我们再一次检查配置是否已经载入,然后用一个模仿的 switch/case 语句来判断按下的是哪一个 CheckButton。一旦我们知道是哪一个,用 this:GetChecked() 或 this:GetValue() 来更改我们的配置就是小菜一碟了!最后,通知主插件我们已经做出了某些更改。
你也可以选择在这里删去 'myClock_ConfigChange();',把它添加到 Done 按钮的 OnClick 事件处理中,编写一个保存/取消对话框。因为一般来说,人们更希望在调整设置的时候能够实时地看到效果。
大功告成!但愿本文能够为你提供一些关于如何创建配置对话框的具体示例,感谢 Scheid 充当小白鼠!你可以以此为起点,创造出更加复杂的配置面板,包括tab、多页,甚至是 Cosmos 的 Khaos (也许是某天另一篇 "如何" 的内容)。


