I'm the author of a direct UI library named SOUI. I'm going to introduce the recent finished work, with which one can build a client program using Lua script completely. Since soui 4.0, I try to export all of classes to C by building it based on COM technology, and expect it can be called by other languages besides C++. As COM based interfaces had been done, it becomes easy to export those interfaces to lua script as well. Using lua script, you can load program resources, create windows, connect events of UI widgets to event slots wroten in Lua, and so on. You can treat it as a simple browser yet only take up less than 2M space.
Introduction
To build a client program, common options may include mfc, wtl, qt, etc. Recently, many companies also choose to use CEF3 to build their client programs. As CEF3 is a browser core exactly, most of the work can be done by JavaScript, which grants it with great flexibility despite its huge resource occupation. Although, some of the new UI frameworks provide script module as well, they can't provide the capability with which a programmer can finish a project based on script only as CEF3 does. I thought, a client framework with complete script capability and slim volume and high efficiency should be a good choice for a client program.
Code Modules
In the source package, we provided soui4.08.sln and soui4.22.sln. Select a favored sln to open and build all of modules, then run SLuaDemo.ex. You will see how it works.
The whole solution includes eight modules:
utilities4
, the module provides some basic tool classes such as SStringA
, SStringW
, SXml
(a wrapper of pugixml
). soui-system-resource
, the module is a simple resource container. soui4
, the module is the core module, which provides the direct UI framework. imgdecoder-gdip
, the module using gdiplus
to do image decode. There are other alternatives such wic, stb, png in the whole git repo. render-gdi
, the render module, render drawing to bitmap based on GDI api. render-skia
is available in the whole git repo, which provides much higher efficiency than gdi. lua-54
, the lua engine. Here, we are using lua 5.4.4. ScriptModule-lua
, the module that exports the used COM interface to lua. We use lua_tinker
(https://github.com/zupet/LuaTinker) to export our interface to lua. We update lua_tinker
to support x64 and can support call mode such as WINAPI in Visual Studio 2008 (It seems 2015+ doesn't have such a problem). SLuaDemo
, the executable module, which demos how to integrate the above mentioned modules to build a client program and do all of logic in lua script.
The following _tWinMain
shows how to start a demo. Those code only composite necessary components to make up the demo, without doing any actual logics.
int WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE ,
LPTSTR lpstrCmdLine, int )
{
HRESULT hRes = OleInitialize(NULL);
SASSERT(SUCCEEDED(hRes));
SetDefaultDir();
int nRet = 0;
{
SouiFactory souiFac;
SComMgr comMgr;
SApplication *theApp = InitApp(comMgr,hInstance);
LoadSystemRes(theApp,souiFac); LoadLUAModule(theApp,comMgr); {
SAutoRefPtr<IScriptModule> script;
theApp->CreateScriptModule(&script); script->executeScriptFile("main.lua"); TCHAR szDir[MAX_PATH];
GetCurrentDirectory(MAX_PATH,szDir);
SStringA strDir = S_CT2A(szDir);
nRet = script->executeMain
(hInstance,strDir.c_str(),NULL); }
theApp->Release();
}
OleUninitialize();
return nRet;
}
All of the actual logic is located in main.lua
. Before explaining the code of main.lua
, I shall simply explain the resource package and layout XML at first. In soui
, all of drawable is defined as 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;
};
One of the main usages of resource package is to define ISkinObj
described by XML and image files.
As we have defined necessary drawable(ISkinObj)
, we can reference those ISkinObj
by name in layout XML. Syntax of soui layout is very similar to Android layout. Soui provides three main layout types including linear layout, gridlayout
and anchor layout. Anchor layout is similar to relative layout in Android but is more flexible in my opinion. For example, a blank layout maybe looks like:
<soui>
<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>
All resource files are indexed in uires.idx file.
After defined UI layout, we can write corresponding logic code in main.lua
.
function onBtnDialog(e)
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)
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);
InitFileResProvider(resProvider,strWorkDir .."\\uires");
local theApp = GetApp();
local resMgr = theApp:GetResProviderMgr();
resMgr:AddResProvider(resProvider,T"uidef:xml_init");
resProvider:Release();
local hostWnd = souiFac:CreateHostWnd(T"LAYOUT:dlg_main");
local hwnd = GetActiveWindow();
hostWnd:Create(hwnd,0,0,0,0);
hostWnd:ShowWindow(1);
initListview(hostWnd);
local btnDialog = hostWnd:FindIChildByNameA("btn_dialog",-1);
LuaConnect(btnDialog,10000,"onBtnDialog");
local btnSwitch = hostWnd:FindIChildByNameA("tgl_left",-1);
LuaConnect(btnSwitch,10000,"on_toggle");
souiFac:Release();
slog("main done");
return theApp:Run(hostWnd:GetHwnd());
end
From the above lua script, we can see the basic steps including:
- Create a
resprovider
object and initialize it with file path or other source depending on which resprovider
type was created. Resource type can be a file path, a DLL, or a zip file, etc. Then add it to the application. - Create a host window object, and initialize it with a resource name defined in the previous resource package. Then call
hostWnd:create
to create a hwnd
and call hostWnd:ShowWindow
to show. - Find child by name and connect its event to responding functions.
OK, that's all!
How to Export a C++ Object to Lua
We using lua_tinker
to do the work. Similar code may include luabind, tolua++, etc. I'm not familiar with the other packages. In using lua_tinker
, I found it may do wrong things if I try to export complicated classes such as a class has multiple base classes. Fortunately, we had implemented all interface using COM, and lua_tinker
support COM is good enough to export them to lua. If you want to know which class or API can be used in lua script, look into the source code of module scriptmodule-lua
. You will see code like:
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;
}
}
Using the code, we exported a COM interface named IObjRef
that includes three methods. Thus, if we got a IObjRef
object and stored it to variable obj
, we can call obj:AddRef
to add reference to this object.
Full soui4 Repository
In the source package, only used modules were presented here. One of the important ideas of soui is to provide replaceable modules for external module look like plugins. There are many functional similar modules you can find in the full git repo. We store soui4 in github, https://github.com/soui4/soui.
Conclusion and Points of Interest
In this article, I'm not going to sell soui4 to you. You can replace soui4 in any other corresponding modules, only if they can provide similar functions. What I'm try to sell is the idea that you can run all logic in script and make it look like a mini browser. Although soui4 is not free for commercial usage (pay a bit of coin), I think you will find many useful points if you look into the code carefully. Thank you very much! Any suggestions are welcome!
Note that I had mentioned the term of COM many times. I should apologize for it because what I had used is not exactly COM but COM kind of technology.
History
- 31st October, 2022: Initial version