FAQ

问题解答

常见问题

Q:关于lua的学习指南

lua是一种小巧、高效而且较为现代化的一种脚本语言,经常有人问到关于lua的学习的事情,一般来说我们推荐下面一些途径:


1)《Lua程序设计》,lua开发的必备手册,想必大家早就知道了

2)Lua的官方在线文档(http://www.lua.org/docs.html),包括一些lua api的使用,一些lua特性的介绍,虽然是英文的,但是有些lua开发基础的还是很容易看懂,遇到不清楚的lua api,建议首先查阅官方的文档

3)Lua的源代码 lua是一门完全开源的脚本语言,使用纯C编写,核心代码量不过几万行,各个模块分割较为明确,代码规范也较好,是深入学习lua的必经途径,也可以了解现代化的脚本语言是怎么设计的等

Q:关于协程lua_state在c++里面的获取和保存

因为lua里面lua虚拟机和协程都用lua_state标识,所以为了避免出错,lua并没有对协程提供lua_pushstate(lua_state, luastate)的接口,那么怎么在c++lua虚拟机里面操作协程? 比较典型的可以通过lua的注册表来保存,比如通过luaL_reflua_rawgeti,而不用去关心真正的lua数据类型。 

比如想保存当前栈上第二个参数,可以

lua_pushvalue(luaState, 2);

int ref = luaL_ref(luaState, LUA_REGISTRYINDEX);

在需要使用这个协程的时候,放入同一个虚拟机的某个luastatelua栈,可以使用

lua_rawgeti(luaState, LUA_REGISTRYINDEX, ref); 

这个方法对于所有的lua类型都有效,只不过协程更特殊一些

Q:关于lua的协程coroutine的使用

Lua协程并不是真正的线程,和windows系统下面的线程不同,不是并发执行的,同一个时刻,同一个虚拟机里面只可以有一个协程在运行,并且需要显示的调度和切换,和windows下面的纤程概念类型,协程有自己的lua栈和指令指针、局部变量等,可以保存自己的执行流,但是又和其它协程共享同一个虚拟机里面的很多东西,比如全局变量表,注册表等。

协程编程属于较为高级的lua应用,可以进行保存当前的执行流,显式调度执行流,这些特性对于一般的程序员来说,都是用的较少的东西,正如windows下面的纤程,我们基本不用。协程是一个lua里面一个重型的应用,开销相对其它数据类型较大,所以我们并不推荐使用协程来开发。

之前有人询问到使用协程来实现异步框架,其实lua的闭包特性的支持和函数作为第一类值的特性,可以支持类似函数式语言的编程方式,已经可以较好的支持异步的特性,并且实现的异步和c++里面的异步概念相似,以回调的形式,显式把一个执行序列分隔开;假如使用协程,相当于每个涉及到异步操作的执行流都需要一个协程,来保存异步调用时候的lua执行流和指令指针,看起来较为直观,但是异步操作的执行流很难界定界线,在同一个虚拟机里面,哪些部分属于一个执行流,多个异步操作怎么分割到lua协程里面,并不会让工作量减少,而且写出的代码并不一定有回调形式的异步框架简单明了。

总起来说,我们并不推荐使用协程,这一块属于较高级的特性,lua的协程本身是否完善,过度使用会有什么问题,现在还都不清楚(大家应该都知道,涉及到lua虚拟机的bug和崩溃修复是最复杂),如果你对lua有足够的了解,并且对于这种协同编程有经验,那么可以适当的使用协程。

Q:Lua里面关于64位整型数的表示

现在luaruntime内部,Lua的所有数值类型(TNUMBER)都是基于双精度浮点数double类型的,现在用的double实现一般都是IEEE 754标准,双精度浮点用4个字来表示,也即是8bytes,包括1bit的符号位,11bit的指数,和52bit的有效位,所以一般来说,如果你的int64整型数,有效位数为53位或者更少,那么完全可以转换成double,然后pushlua虚拟机里面,并且可以转换回来,不会造成精度的丢失;但是如果在lua代码涉及到一些数值运算,再转换过来,就可能会溢出截断,导致精度的丢失。如果你的int64只是用来表示文件大小、时间秒数等,一般来说53位足够了,但是如果是什么任务id、哈希值之类的,那么可能会使用int64里面任意一位,这种情况下不能直接当成double放到lua虚拟机里面了,因为该转换是不可逆的,会导致出各种莫名其妙的问题,一般来说在lua里面使用64位长整型有以下几种解决方案:

1)  使用字符串来表示int64,不方便的地方是无法在lua里面进行数值运算

2)  使用lightuserdata,同样无法在lua里面进行数值运算,甚至不能直接比较

3)  使用扩展的userdata,推荐使用XAF库里面定义的int64类,可以很方便的和lua numberstring相互转换,支持绝大部分数学运算等

4)  c/c++层避免使用64位的长整型,这样最彻底了,但是很多时候不太现实。

Q:关于lua_isnoneornil和lua_isnil的区别和使用

lua里面,nil是一种数据类型,但是none表示某个位置没有数据,下面两种情况下,lua_isnone会是true

1)  尝试取一个超过lua栈顶位置的值的类型,那么就会返回一个none

2)  取超出当前函数所在闭包的upvals个数的值的类型,那么也会返回一个none

一般来说,在C封装里面,假如一个函数期望3个值,而想知道lua调用者某个参数有没有传递,可以使用lua_isnoneornil来判断,注意此时用lua_isnil可能返回false;假如调用者只传递了1个参数,那么对于第二、第三个参数来说,lua_isnoneornil就为true,但是lua_isnilfalse

但是对于lua_pcallXLLRT_LuaCall发起的lua调用,一般需要指定期望的lua返回值的个数,那么判断用户到底返回了几个有效值,使用lua_isnillua_isnoneornil是完全一样的,因为lua会用nil来填充返回值个数不够的栈槽,比如lua_pcall(luaState2,3…),表示期望三个返回值,而用户只返回了1个,那么对于第二、第三个返回值,lua虚拟机会用nil来填充,所以lua_isnil也是返回true。

Q:怎么判断一个userdata上某个方法是否存在,或者枚举该类型的所有方法

一般来说userdata都需要指定一个元表,可以通过元表里面有没有这个字段,来判断存在不存在这个方法。获取一个userdata的元表方法如下

local mt = getmetatable(obj)

local mt = obj.__index

但是这里有一点是需要注意的是,对于使用luaruntime注册的自定义类型,并且使用XLLRT_PushObject放到lua虚拟机去的userdata,那么上面两种方法是等同的,因为我们内部的实现,元表的__index字段是指向了元表自身,而用户在该userdata上面自定义的方法,都是放到默认的元表里面的,这样可以节省一个table的开销。

但是假如是绕开了luaruntime,自定义了元表,那么取决于你的实现,如果你让元方法__index指向的是另外一个table甚至是方法,那么上面两种方法就不等同了,判断自定义方法是否存在,应该使用 local mt = obj.__index,此时你拿到的并不是真正的元表,但是可以判断你的自定义方法是否存在。

 

枚举指定类型的上的所有方法,可以如下:

for k, v in pairs(object.__index) do

    if type(v) == "function" then

         print(k, v)

    end

end

由于引擎本身会不断的增加新方法,一般来说使用者为了兼容性,需要根据某个类型上是否存在指定方法,来决定怎么操作,那么可以使用上述的方法来解决。

Q:怎么判断引擎内部的某个对象是否有效

引擎内部的核心对象,是基于句柄机制的,包括下面几种:

1)元对象,比如LayoutObject/FlashObject/自定义control

2)动画对象

3Hostwnd对象

4)模板对象

5)对象树对象

上面几种对象的生命周期,都是基于句柄机制的,而非基于引用计数,这些对象在pushlua虚拟机后,lua对象持有的只是该对象的句柄,那么会导致的一个问题就是:在某个时刻,该对象已经被销毁,也就是句柄无效了,但是对应的某个或者某些lua对象还存在,那么在使用这些lua对象操作该真正对象的时候,可能会出现非预期的结果,比如操作失败,或者脚本错误等,那么怎么判断该对象时候被销毁了呢? 下面每种对象给出相应的判断方法:

1)元对象,GetClass方法

2)动画对象, GetClass方法

3Hostwnd对象,GetClassName方法

4)模板对象,新版本(276)会增加GetClass方法,现在可以通过GetID来判断,只要返回值不为nil即有效,但有可能为空串

5)对象树对象,可以通过GetID来判断,只要返回值不为nil即有效

只要上面几个方法,返回值不为nil,那么说明该对象有效;否则说明该对象已经被销毁了,不可以再调用对象上的任何方法了

Q:Hostwnd的居中函数Center

只有FrameHostWndModalHostWnd两种hostwnd提供该函数,接收的参数是该窗口需要居中的窗口,参数取值可以是:

1HWND,也就是窗口的GDI句柄

2Xlue内部的hostwnd的句柄,就是hostwndlua对象

如果没有指定窗口,或者上面两种方式指定的窗口无效,那么会采用默认居中窗口,策略如下

1) 如果是popup窗口,那么相对于owner居中

2) 如果是child窗口,那么相对于parent居中

3) 对于popup窗口,如果owner不存在,那么取当前窗口所在屏幕的工作区进行居中,如果并无所在屏幕,那么取主屏幕进行居中

大家可以根据xlue内部的居中策略,来传递相应的参数,尤其是popup的窗口,如果创建时候并没有指定owner,或者想对非owner窗口进行居中,那么需要传入合适的窗口句柄。

Q:关于元对象的OnMouseEnter和OnMouseLeave

由于之前引擎并没有提供OnMouseEnter消息,所以大家都是使用OnMouseMove来模拟该该事件,一般来说还需要设置一个标志位。从今年四月份的版本起,xlue已经支持了OnMouseEnter事件,该事件和OnMouseLeave是严格对称的,事实上引擎内部实现,正是根据该对象上收到的第一次mousemove消息生成了对应的mouseenter事件,事件参数类型和mousemove完全一致,和稍后收到的第一次mousemove的参数也完全相同。

现在大家在开发一些新控件或者功能时候,可以使用该事件,这样可以使得逻辑更清楚明了,更容易控制,减少不必要的开销。

Q:关于control对象的OnControlMouseEnter和OnControlMouseLeave

鉴于大家在开发中,经常碰到下面的需求:一个复杂控件,里面有多个对象,需要判断鼠标是否移动该控件的任意一个object,或者已经离开所有的object,这种需求看似简单,但是对于之间xlue,实现该功能的工作量却很大,涉及到的细节也很多。所以引擎增加了OnControlMouseEnterOnControlMouseLeave,来方便基于上面需求的开发工作。 

OnControlMouseEnterOnControlMouseLeave只在自定义控件的object上触发,触发条件说明如下:

假如之前鼠标在object A上面,现在移动到了object B上面,其中AB都可以接收鼠标和键盘消息,那么

1)对于某个control,如果A属于该control内部对象,而B不属于该control内部对象,那么该control会触发OnControlMouseLeave

2)对于某个control,如果A不属于该control内部对象,而B属于该control内部对象,那么该control会触发OnControlMouseEnter

3)对于某个control,如果AB都属于该control内部对象,那么不会触发两个事件中的任意一个

4OnControlMouseEnterOnControlMouseLeave事件的触发均是递归向上的,从A或者B到所在对象树的根对象,该路径上所有满足12条件的control,都会触发对应的事件。

Q:关于元对象的焦点问题

一个对象是否可以获取焦点,有下面几个关键要素:

 1)对象是否可见,只有可见对象可以获得焦点

2)对象是否位enable,被disable的对象不可获得焦点

3)对象是否可以处理鼠标和键盘消息,如果该对象上并没有挂接任意的鼠标和键盘消息,那么不可以获得焦点

4)对象的zorder、位置信息和limitrect等影响该对象输入的,也都对获取焦点有影响

在一个对象上发生了左键、右键或者中键的down事件,会自动设置该对象焦点状态,同时触发以下事件:

1)获取焦点对象上触发OnFocusChange(true)事件

2)失去焦点对象上触发OnFocusChange(false)事件

3)外围控件上会根据具体情况,触发OnControlFocusChange事件,逻辑OnControlMouseEnterOnControlMouseLeave

判断一个元对象是否获取焦点,可以通过调用GetFocus()来判断判断一个对象树上当前获取焦点的元对象,可以通过调用GetFocusObject()来获取

Q:关于元对象的RouteToFather对焦点的影响

如果一个对象,把左键、中键或者右键的down事件,向上路由到了直接或者间接parent,那么按照11里面描述的规则,对于某个parent来说,就好比收到了相应的鼠标事件。这里需要注意的是对于这个parent来说,并不知道该事件是由用户触发的,还是某个孩子调用了routetofather而来的,所以导致的结果就是该parent也会设置焦点。这样处理导致的一个现象就是:在一个对象里面点击了左键、中键或者右键,该对象首先获取了焦点,但是同时由于RouteToFather调用导致的副作用,焦点又转移到了相应的parent上面,从而和用户所预期的可能不一致

如果你的确是想让某个parent在处理相应的鼠标事件的同时,也获得焦点,那么可以安心的调用RouteToFather;但是这并不是你所预期的,那么请不要直接调用RouteToFather,而是调用更上层的、约定好的接口,来知会parent做相应的处理

Q:元对象状态在变为invisible或者disable时候的副作用

一个对象变为invisible,可能是对象本身调用了SetVisible(false),也可能是某个直接或者间接parent调用了SetChildrenVisible(false);类似的,一个对象变为disable的时候,可能是对象本身调用了SetEnable(false),也可能是某个直接或者间接parent调用了SetChildrenEnable(false)

在一个对象从visible切换到invisible,或者从enable切换到disable时候,可能会按照先后顺序,触发以下事件:

1)如果该对象在执行拖放操作中,那么会触发OnDragLeave事件

2)如果该对象正在捕获鼠标,那么会终止捕获,触发OnCaptureChange(false)事件如果该对象在MouseIn状态(也就是收到了OnMouseEnter但是没有leave),那么会触发OnMouseLeaveOnControlMouseLeave事件

 3)如果该对象获取了焦点,那么会失去焦点,触发OnFocusChange(false)OnControlFocusChange(false)事件

4)最后触发该对象自身的OnVisibleChange(false)事件或者OnEnableChange(false)事件

 大家可能在开发控件过程会碰到这些问题,需要注意一下

Q:关于hostwnd的parent和owner,子窗口等的说明

Xlue内部的窗口有下面五种:

 1)FrameHostWnd

 2)ModalHostWnd

 3)TipsHostWnd

4)MenuHostWnd

5)DragDropHostWnd

这五种窗口各司其职,分别在不同的场合有不同的作用,默认这些窗口都是top levelwindows窗口,其中除了FrameHostWnd,其余的窗口也只可以作为top level的窗口,而FrameHostWnd可以作为子窗口存在,但是一般很少使用(关于child windowtop level window这些windows下面窗口的基础知识,这里不再累述,大家可以去参考msdn里面关于窗口类别的描述)

创建一个作为子窗口的FrameHostWnd,需要先对FrameHostWnd调用SetParent,设置有效的父窗口进去,然后调用Create函数创建即可,如果SetParent指定的父窗口有效,那么Create函数会忽略Create传入的owner参数,创建一个子窗口。之所以我们不推荐使用子窗口,因为子窗口有很多限制,尤其对层窗口(layeredwindow)的限制,子窗口不可以作为每个像素自带alpha通道的窗口,其所在父窗口也不可以作为每个像素自带alpha通道的窗口,只可以是普通的窗口,视觉效果上大打折扣。

默认情况下,由于历史上的一些原因,有下面的规则:

 1)FrameHostWndCreate传入的参数为该窗口的owner(注意不是parent),可以通过GetOwner来获取popup窗口的owner;如果事先有调用SetParent,那么会创建一个子窗口,可以通过GetParent来获取父窗口

2)ModalHostWndDoModal传入的第一个参数是该窗口的owner,如果没有指定owner,并不会取SetParent设置进来的窗口,而是取当前的活动窗口来作为owner,可以通过GetOwner来获取该窗口的owner

3)MenuHostWndTrackPopupMenu传入的第一个参数,是menu所在窗口的owner,如果没有指定,该窗口会置顶,并按照系统默认设置owner,可以通过GetOwner来获取该窗口的owner

4)TipsHostWnd,在自动模式下,使用SetParent设置进来的窗口作为owner,可以通过GetOwner或者GetParent来获取该窗口的owner;在手动模式下,使用Create的第一个参数作为窗口的owner,如果没有指定,则按照系统默认,可以通过GetOwner来获取该窗口的owner

5)DragDropHostWnd不接受指定owner,均采用系统默认

Q:窗口的显示如何不抢焦点

对于FrameHostWnd,可以设置visible属性位false,先创建窗口,然后调用Show(4)来显示

ModalHostWnd内部是基于系统模态对话框的,所以必抢焦点;想设计不抢焦点的窗口,请使用FrameHostWnd

MenuHostWndTipsHostWnd内部自动控制显示,不抢焦点

Q:元对象的SetXXXColor的使用

目前涉及到颜色设置的对象有

TextObject

FillObject

EditObject

RichEditObject

FlashObject

LineObject

RectangleObject

一般来说,元对象的每个颜色属性都有对应的四个方法,比如TextObjectSetTextColor

SetTextColor,设置颜色值,可以接收的类型包括

  .字符串,和资源xml里面配置的一致,可以是RGB(0,0,255),RGBA(0,0,255,255)或者000000FF

  .整型,内部会直接当做xl_color来处理

  .XL_Colorlua封装类,一般是由别处得来

SetTextColorResID 设置颜色的资源id

GetTextColor 获取颜色值,一般是一个lua_integer类型

GetTextColorResID 获取颜色的资源id,如果一个该颜色值由SetTextColorResID或者xml里面配置id而来,那么返回对应的id,如果已经调用过SetTextColor,那么返回一个空串

Q:XGP库的初始化和反初始化

在使用XGP库之前,一定要对其进行初始化操作,否则可能出现崩溃、操作失败等各种不可预期的问题

关于XGP得得初始化和反初始化,说明如下:

1)初始化

XGP库依赖于XLFSIOXLGraphicXLLuaruntime(lua封装的版本)三个库,所以XGP库在初始化之前,首先需要初始化XLGraphic库,而XLGraphic库内部又会初始化XLFSIO库。

关于XLLuaruntime库的初始化,主要是创建全局默认的环境和默认的runtime,如果使用XGP的程序也同时使用XLUE,那么需要先初始化XLUE(XLUE_InitLoader),内部会初始化luaruntime的;假如不使用XLUE,只是使用XLGraphic,并且是带lua封装的XGP版本,那么需要自己初始化luaruntime,创建默认的env或者runtime;如果不希望XGP注册到默认的env,可以在初始化时候修改参数为不注册luahost,而稍后自己调用接口来注册到指定的env

初始化代码如下:

XLGraphicPlusParam plusParam;

XLGP_PrepareGraphicPlusParam(&plusParam);

//这里可以修改一些初始化选项

XLGP_InitGraphicPlus(&plusParam);

2)反初始化

只要保证晚于XLGraphic库的初始化即可,反初始化代码很简单:XLGP_UnInitGraphicPlus();

Q:关于元对象的位置表达式和SetObjPos只可以接收整数的说明

目前整个xlue整个系统的设计都是基于像素的,并没有设计基于英寸等的坐标系,因为像素都是整数,所以对象的位置也就只能是整数,所以在设置对象位置时候需要注意以下:

1) 如果是直接设置数值进去,而数值带小数,比如SetObjPos(10.3, 10.5, 10.7, 10.9),那么内部不会进行四舍五入,都会统一截断为10,所以在尽量不要设置带小数位的浮点数进去,假如需要四舍五入,那么请在调用函数之前设置,xml里面静态配置也是如此。

2)如果是设置表达式进去,那么表达式里面不能出现带小数点的数值,会导致表达式解析失败!比如SetObjPos(“10.5 + father.wdith”….)会导致解析失败,对应值会一直是0,出现不可预期的结果;如果是一个核合法的表达式,但是里面使用除法操作,可能会出现浮点的运算结果,那么引擎在内部会自动舍去小数点部分,同样不会进行四舍五入,这点也需要特殊注意一下。

Q:为什么控件的method定义,有些没写func和file?

这种写法确实是可以……只是不为人所知,在method定义中,如果不指定funcfile,那么默认取file为当前的xml文件名加上.lua作为filefunc便是method的名字,

比如在control.xml里面,定义了如下的方法:

<method_def>

       <CallMethod/>

</method_def>

相当于如下

<method_def>

       <CallMethod file=”control.xml.lua” func=”CallMethod”/>

</method_def>

当初这么设计,是因为发现很多时候,filefunc都是冗余的,可以通过method的名字和所在的xml推断出来,所以为了方便,便有这种写法。不过话又说回来,这种写法虽然用的不多,但是带来的问题确不少,尤其是涉及到模板合并和继承的地方,让我颇为头疼。不过出于兼容性的考虑,这个特性还是保留了下来,大家如果有需要可以使用这种写法

Q:关于图形库在alpha混合中的预乘

XlGraphic中对带alpha通道的图片,在加载完毕之后,都是经过预乘(PreMultiply)的,有做过alpha混合的同学应该对预乘有所了解,就是分别对位图的RGB三个通道利用对应的Alpha通道,做以下运算:Src = (Src * Alpha)/256。这样在做source alpha混合运算的时候,可以提高运算速度,尤其是在原图会被反复使用做alpha混合的情况下。

只有带alpha通道的图片格式被加载以后,才需要做预乘处理,主要是XLgraphic支持的png格式,和XGP新增支持的32bitmapicon这三种格式,而对于jpggif这种本身不带alpha通道的位图,就没有必要做预乘运算。

这里需要的注意的是

1)通过XlGraphic加载的png图片,都已经是经过预乘运算的,所以如果你们想得到没有经过预乘运算的XL_BITMAP_HANDLE,可以使用gdi+自己加载,或者使用我们提供的不经过预乘的加载函数(XLGraphic/XGP)

2)进入xlue渲染链的图片,如果该图片附带alpha通道,那么一定要是经过预乘运算的,现在XLGraphic已经导出一个函数XL_PreMultiplyBitmap,可以用作对图片的预处理

3)XLGraphic内部在做alpha混合时候,不会对未经过预乘的bitmap做自动预乘运算

4)带apha通道的图片而又未经过预乘处理,进入渲染体系后,会导致渲染有误差,但可能不明显

5)预乘运算是不可逆的,假如原图的alpha通道值比较小,那么对应的RGB通道经过预乘以后,可能会成为0,导致该运算不可逆

Q:关于引擎里面的光标策略

每个元对象可以配置cursor id,关于cursor有几点需要说明一下:

1)Hostwndcursor id优先级最高,如果hostwnd配置或者设置了cursor id,那么会忽略对象上树上所有对象对cursor id的配置

2)对象树上的元对象对cursor的命中策略是基于zorder的,从高到低,并且满足以下条件:

  .对象可见

  .对象没有被disable

  .对象配置了cursor id

  .命中测试点落在对象区域(包括被limit的区域)

3)Cursor和光标caret不相同,和焦点没有关系