Login

如何创建 GUI 配置界面

原文:HOWTO:_Create_GUI_Config_Options,发表于 WoWWiki

译者:流润之风

为插件编写 GUI 的配置/选项对话框并不容易,希望这一篇 "如何" 可以有所帮助。通常,我们都是添加配置对话框到某个已完成的插件,所以本文在讲解的时候也会采用一个已存在的插件作为例子,但我们鼓励大家在学习本文的时候能够使用自己编写的插件。

参考材料(非必需)

Scheid 的 myClock

本文的目的并不是以任何形式重写 myClock,而且你也可以注意到这里的代码做过多处改动。不过,本文是基于 myClock 之上的,而且在填补完代码间空白之后,你可以得到一个类似于 myClock 的基本插件。

TOC 文件

参考材料(可选阅读)

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

OptionalDeps

  • 表示可选依赖。myAddons不是必需的,但是推荐采用。

SavedVariables

  • 让我们可以使用以帐号为单位、跨会话保存的变量。

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

参考材料(可选阅读)

如何获取Player和Realm名字

我们需要:

  • 获取 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 (也许是某天另一篇 "如何" 的内容)。


插件开发资料类, HOWTOs类, 流润之风

如何创建GUI配置界面 (last edited 2007-12-29 07:53:58 by localhost)