在 Lua 脚本中完全运行客户端
通过实现一个 UI 库并将其必要的组件导出到 Lua 脚本,我们可以创建美观的 UI 并在 Lua 脚本中完全实现相关逻辑。
引言
要构建客户端程序,常见的选项可能包括 mfc、wtl、qt 等。最近,许多公司也选择使用 CEF3 来构建其客户端程序。由于 CEF3 是一个浏览器核心,大部分工作可以通过 JavaScript 完成,这使得它具有很大的灵活性,尽管它会占用大量资源。尽管如此,一些新的 UI 框架也提供了脚本模块,但它们无法像 CEF3 那样提供一个程序员仅基于脚本就能完成项目的能力。我认为,一个具有完整脚本能力、体积小巧且效率高的客户端框架,应该是一个不错的客户端程序选择。
代码模块
在源代码包中,我们提供了 soui4.08.sln 和 soui4.22.sln。选择一个喜欢的 sln 打开并构建所有模块,然后运行 SLuaDemo.ex。您将看到它是如何工作的。
整个解决方案包含八个模块
utilities4
,该模块提供了一些基本工具类,如SStringA
、SStringW
、SXml
(pugixml
的封装)。soui-system-resource
,该模块是一个简单的资源容器。soui4
,该模块是核心模块,提供了直接的 UI 框架。imgdecoder-gdip
,该模块使用gdiplus
进行图像解码。在整个 git 仓库中还有其他替代方案,如 wic、stb、png。render-gdi
,渲染模块,基于 GDI API 将绘图渲染到位图。在整个 git 仓库中提供了render-skia
,它比 gdi 具有更高的效率。lua-54
,Lua 引擎。这里我们使用的是 Lua 5.4.4。ScriptModule-lua
,该模块将使用的 COM 接口导出到 Lua。我们使用lua_tinker
(https://github.com/zupet/LuaTinker)将接口导出到 Lua。我们更新了lua_tinker
以支持 x64,并支持 Visual Studio 2008 中的调用模式,如 WINAPI(似乎 2015+ 没有这个问题)。SLuaDemo
,可执行模块,演示了如何集成上述模块来构建一个客户端程序并在 Lua 脚本中完成所有逻辑。
下面的 _tWinMain
显示了如何启动演示。这些代码仅由必要组件组成,以构成演示,而不执行任何实际逻辑。
//
int WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE /*hPrevInstance*/,
LPTSTR lpstrCmdLine, int /*nCmdShow*/)
{
HRESULT hRes = OleInitialize(NULL);
SASSERT(SUCCEEDED(hRes));
SetDefaultDir();
int nRet = 0;
{
SouiFactory souiFac;
SComMgr comMgr;
SApplication *theApp = InitApp(comMgr,hInstance);
LoadSystemRes(theApp,souiFac); //load system resource
LoadLUAModule(theApp,comMgr); //load lua script module.
{
SAutoRefPtr<IScriptModule> script;
theApp->CreateScriptModule(&script); //create a lua instance
script->executeScriptFile("main.lua");//load lua script
TCHAR szDir[MAX_PATH];
GetCurrentDirectory(MAX_PATH,szDir);
SStringA strDir = S_CT2A(szDir);
nRet = script->executeMain
(hInstance,strDir.c_str(),NULL); //execute the main function
//defined in lua script
}
theApp->Release();
}
OleUninitialize();
return nRet;
}
所有实际逻辑都位于 main.lua
中。在解释 main.lua
的代码之前,我将首先简单解释一下资源包和布局 XML。在 soui
中,所有可绘制对象都定义为 ISkinObj
。
#undef INTERFACE
#define INTERFACE ISkinObj
DECLARE_INTERFACE_(ISkinObj, IObject)
{
#include <interface/SobjectApi.h> //define IObject methods
STDMETHOD_(void, DrawByState2)(CTHIS_ IRenderTarget * pRT,
LPCRECT rcDraw, DWORD dwState, BYTE byAlpha) SCONST PURE;
STDMETHOD_(void, DrawByState)(CTHIS_ IRenderTarget * pRT,
LPCRECT rcDraw, DWORD dwState) SCONST PURE;
STDMETHOD_(void, DrawByIndex2)(CTHIS_ IRenderTarget * pRT,
LPCRECT rcDraw, int iState, BYTE byAlpha) SCONST PURE;
STDMETHOD_(void, DrawByIndex)(CTHIS_ IRenderTarget * pRT,
LPCRECT rcDraw, int iState) SCONST PURE;
STDMETHOD_(SIZE, GetSkinSize)(CTHIS) SCONST PURE;
STDMETHOD_(int, GetStates)(CTHIS) SCONST PURE;
STDMETHOD_(BYTE, GetAlpha)(CTHIS) SCONST PURE;
STDMETHOD_(void, SetAlpha)(THIS_ BYTE byAlpha) PURE;
STDMETHOD_(void, OnColorize)(THIS_ COLORREF cr) PURE;
STDMETHOD_(int, GetScale)(CTHIS) SCONST PURE;
STDMETHOD_(ISkinObj *, Scale)(THIS_ int nScale) PURE;
};
资源包的主要用途之一是定义由 XML 和图像文件描述的 ISkinObj
。
由于我们已经定义了必要的 drawable(ISkinObj)
,我们可以在布局 XML 中通过名称引用这些 ISkinObj
。soui 布局的语法与 Android 布局非常相似。Soui 提供三种主要的布局类型,包括线性布局、gridlayout
和锚点布局。锚点布局类似于 Android 中的相对布局,但我认为它更灵活。例如,一个空白布局可能看起来像
<soui>
<!-- define a root window that includes 2 children layout from left to right -->
<root width="400" height="300" layout="hbox" >
<window name="child1" size="100,-2" colorBkgnd="@color/red"/>
<window name="child2" size="100,-2" colorBkgnd="@color/blue"/>
</root>
</soui>
所有资源文件都已在 uires.idx 文件中索引。
定义 UI 布局后,我们可以在 main.lua
中编写相应的逻辑代码。
function onBtnDialog(e) --show about dialog
slog("onBtnDialog");
local btnSender = toSWindow(e:Sender());
local hParent = btnSender:GetHostHwnd();
local souiFac = CreateSouiFactory();
local dialog = souiFac:CreateHostDialog(T"layout:dlg_test");
souiFac:Release()
local ret = dialog:DoModal(hParent);
dialog:Release();
end
function on_toggle(e) --animtion slide the left pane
slog("on toggle");
local hostWnd = GetHostWndFromObject(e:Sender());
local leftPane = hostWnd:FindIChildByName(L"pane_left",-1);
local toggle = toSWindow(e:Sender());
local isChecked = toggle:IsChecked();
local theApp = GetApp();
local anim;
if(isChecked == 1) then
slog("on toggle true".. isChecked);
anim = theApp:LoadAnimation(T"anim:slide_show");
else
slog("on toggle false".. isChecked);
anim = theApp:LoadAnimation(T"anim:slide_hide");
end
leftPane:SetAnimation(anim);
anim:Release();
end
function main(hinst,strWorkDir,strArgs)
slog("main start");
local souiFac = CreateSouiFactory();
local resProvider = souiFac:CreateResProvider(1);-- create a resource
-- package from a file path
InitFileResProvider(resProvider,strWorkDir .."\\uires");--init resource
-- package from file
local theApp = GetApp();
local resMgr = theApp:GetResProviderMgr();
resMgr:AddResProvider(resProvider,T"uidef:xml_init"); -- add the resource
-- package to program
resProvider:Release();
local hostWnd = souiFac:CreateHostWnd(T"LAYOUT:dlg_main"); --define a host
-- window using layout file
--defined in dlg_main. host window is a win32 native hwnd, which contains
-- all of direct ui widgets.
local hwnd = GetActiveWindow();
hostWnd:Create(hwnd,0,0,0,0); --create the host window
hostWnd:ShowWindow(1); --1==SW_SHOWNORMAL
initListview(hostWnd);
--...
local btnDialog = hostWnd:FindIChildByNameA("btn_dialog",-1); -- find command
-- button named "btn_dialog"
LuaConnect(btnDialog,10000,"onBtnDialog"); -- connect event of commend
-- to responding function
-- "onBtnDialog"
local btnSwitch = hostWnd:FindIChildByNameA("tgl_left",-1); -- find switch
-- button named "tgl_left"
LuaConnect(btnSwitch,10000,"on_toggle"); -- connect event of commend
-- to responding function "on_toggle"
souiFac:Release(); -- release souiFac object
slog("main done");
return theApp:Run(hostWnd:GetHwnd()); --run the app until event loop broken.
end
从上面的 Lua 脚本可以看出,基本步骤包括
- 创建一个
resprovider
对象,并根据创建的resprovider
类型,使用文件路径或其他源对其进行初始化。资源类型可以是文件路径、DLL 或 zip 文件等。然后将其添加到应用程序中。 - 创建一个主机窗口对象,并使用前面资源包中定义的资源名称对其进行初始化。然后调用
hostWnd:create
来创建hwnd
,并调用hostWnd:ShowWindow
来显示。 - 按名称查找子控件,并将其事件连接到响应函数。
好的,就是这样!
如何将 C++ 对象导出到 Lua
我们使用 lua_tinker
来完成这项工作。类似的实现可能包括 luabind、tolua++ 等。我对其他库不太熟悉。在使用 lua_tinker
时,我发现如果尝试导出复杂的类,例如具有多个基类的类,它可能会出错。幸运的是,我们已经使用 COM 实现的所有接口,并且 lua_tinker
对 COM 的支持足以将其导出到 Lua。如果您想知道在 Lua 脚本中可以使用哪些类或 API,请查看 scriptmodule-lua
模块的源代码。您将看到类似的代码
BOOL ExpLua_IObjRef(lua_State *L)
{
try{
lua_tinker::class_add<IObjRef>(L,"IObjRef");
lua_tinker::class_def<IObjRef>(L,"AddRef",&IObjRef::AddRef);
lua_tinker::class_def<IObjRef>(L,"Release",&IObjRef::Release);
lua_tinker::class_def<IObjRef>(L,"OnFinalRelease",&IObjRef::OnFinalRelease);
return TRUE;
}catch(...)
{
return FALSE;
}
}
使用这段代码,我们导出了一个名为 IObjRef
的 COM 接口,该接口包含三个方法。因此,如果我们获取一个 IObjRef
对象并将其存储在变量 obj
中,我们可以调用 obj:AddRef
来增加该对象的引用计数。
完整的 soui4 仓库
在源代码包中,这里只展示了使用的模块。soui 的重要思想之一是提供可替换的模块,以实现外部模块的插件化。在完整的 git 仓库中,您可以找到许多功能相似的模块。我们将 soui4 存储在 GitHub 上,https://github.com/soui4/soui。
结论和关注点
在这篇文章中,我无意推销 soui4。只要它们能提供类似的功能,您就可以将 soui4 替换为任何其他相应的模块。我试图推销的是可以在脚本中运行所有逻辑并使其看起来像一个迷你浏览器的想法。尽管 soui4 对于商业用途不是免费的(需要支付少量费用),但我认为如果您仔细查看代码,您会发现许多有用的地方。非常感谢!欢迎任何建议!
请注意,我多次提到了 COM 这个术语。我应该为此道歉,因为我所使用的并非严格意义上的 COM,而是 COM 类的技术。
历史
- 2022 年 10 月 31 日:初始版本