Thread
LaGUI 编程教程
-
-
Use LaGUI
Demonstration programs
The best way to learn LaGUI is to look at code from demonstration programs. The demonstration program package for LaGUI is also provided for download. To compile these programs, go into the demo source code directory, use roughly the same commands as when compiling LaGUI itself. Use example_viewer to see detailed explanation of each examples.
mkdir build cd build cmake ../ make ./example_viewerClick here to see programming tutorials.
-
LaGUI
Programming Tutorials (unfinished)>>> Support via Patreon <<<↗ >>> Support via Alipay <<<
LaGUI is an OpenGL based data driven graphical application framework, it has following main features:
Widgets driven by properties. Fully automatic undo/redo for the entire data structure. Multiple working file and file management. Any kind of GL canvas. Flexible tiling and multi-window interface. Game controller/joystick and Wacom pen input support. Interface translation and theme support. Simple 3D mesh modelling support. Very few external dependencies.Due to the inherit complexity of data structure descriptor for the property driven interface and undo system, LaGUI is not suitable for quick simple programs. It's more suitable to be used as a infrastructure for medium scale tool-styled programs.
Download
Latest version: LaGUI 5.1
It's recommended to use git to acquire the source code for LaGUI (and demonstration programs). Please go to my code repository.
View development logs
-
-
这个教程将展示使用 LaGUI 创建应用程序的一般原理。
-
制作第一个 LaGUI 应用程序
制作最简单的 LaGUI 应用程序只需创建一个窗口。菜单栏、内部按钮、以及默认的数据系统会自动生成以支持程序运行。
#include "la_5.h" int main(int argc, char *argv[]){ laGetReady(); laWindow* w = laDesignWindow(-1,-1,600,600); laStartWindow(w); laMainLoop(); }
在使用其他 LaGUI 调用之前应始终使用
laGetReady();
。之后通过laDesignWindow()
创建窗口。将位置设置为负数则交由操作系统决定窗口放置的位置。可通过菜单栏访问 LaGUI 程序的面板和布局。点击
🞆
按钮可以打开新的面板,点击🗖
则可以将面板固定在布局中间。多个面板可以停靠在同一个布局,在停靠时,拖动面板标题可以重新布局或者将面板从停靠的位置撕下来。保存和加载用户设置
LaGUI 应用程序的设置和用户界面布局可以自动保存和加载以在程序再次启动时保持期望的状态。在
laGetReady()
之后使用laEnsureUserPreferences();
即可。LaGUI 会在程序运行目录下保存preferences.udf
,此后启动时该函数会尝试读取该文件。由于 LaGUI 会恢复窗口,因此在用户设置成功读取之后就不需要再手动创建窗口了,因此可将程序修改为如下的样式:#include "la_5.h" extern LA MAIN; int main(int argc, char *argv[]){ laGetReady(); laEnsureUserPreferences(); if(!MAIN.Windows.pFirst){ laWindow* w = laDesignWindow(-1,-1,600,600); laStartWindow(w); } laMainLoop(); }
您还可以在该用户设置文件中保存您应用程序的其他设置,之后会解释如何实现。
-
自定义面板并添加界面元素
LaGUI 界面对象结构是这样的:
窗口 [laWindow] -> 浮动面板 [laPanel] -> 布局 [laLayout] -> 面板组 [laBlock] -> 面板 [laPanel] -> 控件 [laUiList->laUiItem]
要显示面板并在面板中添加界面元素,需要将自定义的界面列表函数注册为一个面板类。界面列表函数的形式是这样的:
void MyPanel(laUiList *uil, laPropPack *This, laPropPack *DetachedProps, laColumn *EXTRA_UNUSED, int context){ laColumn* c=laFirstColumn(uil); laShowLabel(uil,c,"Hello world!",0,0); }
在这个最简单例子中,我们只需要用到第一个参数
laUiList *uil
,别的暂时不需要。利用laFirstColumn(uil)
获得挂件列表的第一列,然后在其中添加一个叫做“Hello world!”的标签。在启动任何窗口之前,我们调用
laRegisterUiTemplate()
将上述函数注册为一个面板类:laRegisterUiTemplate("my_panel","My Panel", MyPanel,0,0,"Demonstration", 0,0,0);
这个面板类将显示在程序左上角的“🞆”菜单中供随时调用。要想在程序启动时默认显示这个面板,我们要将它固定在窗口中,此时需要创建一个布局,面板将固定在布局中,像下面这样:
laWindow* w = laDesignWindow(-1,-1,600,600); laLayout* l = laDesignLayout(w,"My Layout"); laCreatePanel(l->FirstBlock,"my_panel");
这样我们就创建了一个占满默认布局的自定义面板。总结起来,这个程序的完整代码如下所示:
#include "la_5.h" extern LA MAIN; void MyPanel(laUiList *uil, laPropPack *This, laPropPack *DetachedProps, laColumn *UNUSED, int context){ laColumn* c=laFirstColumn(uil); laShowLabel(uil,c,"Hello world!",0,0); } int main(int argc, char *argv[]){ laGetReady(); laRegisterUiTemplate("my_panel","My Panel", MyPanel,0,0,"Demonstration", 0,0,0); laEnsureUserPreferences(); if(!MAIN.Windows.pFirst){ laWindow* w = laDesignWindow(-1,-1,600,600); laLayout* l = laDesignLayout(w,"My Layout"); laCreatePanel(l->FirstBlock,"my_panel"); laStartWindow(w); } laMainLoop(); }
请注意,由于读取用户设置后将创建窗口,因此各种界面资源的注册(比如这里的面板)需要在读取用户设置之前进行。
-
添加按钮
按钮在 LaGUI 中用来调用其他工具,要通过 LaGUI 运行业务程序,那么您需要将这些业务程序注册为工具。
一个最简单的工具只包含
Invoke()
回调,只在调用时触发一次,它的格式是这样的:int INV_MyOperator(laOperator* a, laEvent* e){ printf("Something happened in stdout!\n"); logPrint("Something happened in LaGUI terminal!\n"); return LA_FINISHED; }
该回调需要返回
LA_FINISHED
示意工具执行完成。在程序初始化时,通过laCreateOperatorType
注册该工具:laCreateOperatorType("MY_invoke_test", "Something!", "Print some strings.",0,0,0,INV_MyOperator,0,L'🗨',0);
最后,在面板中显示该按钮:
void MyPanel(laUiList *uil, laPropPack *This, laPropPack *DetachedProps, laColumn *UNUSED, int context){ laColumn* c=laFirstColumn(uil); laShowLabel(uil,c,"Hello world!",0,0); laShowItem(uil,c,0,"MY_invoke_test"); }
运行程序,一个叫做“Something!”的按钮会出现在之前添加的标签下方,点击它就会执行该工具,并且您在标准输出和 LaGUI 终端中都能看见打印的字符串。
工具调用方式
LaGUI 中的所有输入事件均经过工具处理,窗口和控件自身的事件处理也是通过工具实现的。工具可以独占输入或者将输入传递到其他正在运行的工具。特别地,对于按钮控件,它可以调用用户自定义的工具以实现业务功能。每个窗口下都有一个工具栈,典型情况下呈现这样的结构:
Event/事件 | [__CUSTOM__] <--其他被调用的工具 | [__WIDGET__] <--控件工具 | [LA_panel_operator] <--面板工具(服务于鼠标下方的面板) | [LA_window_operator] <--窗口总工具(仅在程序退出时结束) V
工具可以包含几个不同的回调函数,它们调用流程可以描述成这样:
调用工具 "My Operation": --> Check(); --+-- false ---------> Exit(); +-- true ----------> Init(); --> Init(); ------------------------> Invoke(); --> Invoke(); --+-- FINISHED ------> Exit(); +-- RUNNING -------> Modal(); +-- RUNNING_PASS --> Modal(); PASS --> Modal(); --+-- FINISHED ------> Exit(); +-- RUNNING -------> Modal(); +-- RUNNING_PASS --> Modal(); PASS --> Exit();
标有 PASS 的路径意味着,输入事件将被传递到到窗口工具栈的下一个工具,否则当前工具将独占该输入事件。因此,我们可以注册一种工具,使得它在运行时暂时控制全部输入,直到工具退出,这样的好处是,如果你的工具需要点击或者许多其他操作,或者你通过独占工具再次调用了另一个独占工具,你的输入只会对最后调用的工具起作用而不会影响用户界面,前几个工具必须等独占的工具退出才能继续接收事件。
独占式工具例子
只需要在
Invoke()
OrModal()
回调中返回LA_RUNNING
OrLA_RUNNING_PASS
即可启动工具独占。在前述例子上进行改进,工具的两个回调类似这样:int INV_MyModalOperator(laOperator* a, laEvent* e){ printf("Modal opeator!\n"); logPrint("Modal opeator!\n"); return LA_RUNNING; } int MOD_MyModalOperator(laOperator* a, laEvent* e){ printf("%d,%d\n",e->x,e->y); logPrint("%d,%d\n",e->x,e->y); if(e->Type==LA_R_MOUSE_DOWN){ printf("Modal opeator finished!\n"); logPrint("Modal opeator finished!\n"); return LA_FINISHED; } return LA_RUNNING; }
然后注册:
laCreateOperatorType("MY_modal_test", "Modal!", "Print mouse positions.",0,0,0,INV_MyModalOperator,MOD_MyModalOperator,L'🏃',0);
将
MY_modal_test
也加入面板,之后运行程序,点击“Modal!”按钮,之后移动鼠标,标准输出和 LaGUI 终端将输出鼠标位置,此时在界面上点击鼠标左键不会触发任何操作,点击鼠标右键后该工具停止。若将
MOD_MyModalOperator()
中的LA_RUNNING
改为LA_RUNNING_PASS
,则工具运行时您仍然可以操作界面(当然,如果你再点击“Modal!”按钮,就会有两个工具同时运行)。整个程序的代码应当类似于下面这样:
#include "la_5.h" extern LA MAIN; int INV_MyOperator(laOperator* a, laEvent* e){ printf("Something happened in stdout!\n"); logPrint("Something happened in LaGUI terminal!\n"); return LA_FINISHED; } int INV_MyModalOperator(laOperator* a, laEvent* e){ printf("Modal opeator!\n"); logPrint("Modal opeator!\n"); return LA_RUNNING; } int MOD_MyModalOperator(laOperator* a, laEvent* e){ printf("%d,%d\n",e->x,e->y); logPrint("%d,%d\n",e->x,e->y); if(e->Type==LA_R_MOUSE_DOWN){ printf("Modal opeator finished!\n"); logPrint("Modal opeator finished!\n"); return LA_FINISHED; } return LA_RUNNING; } void MyPanel(laUiList *uil, laPropPack *This, laPropPack *DetachedProps, laColumn *UNUSED, int context){ laColumn* c=laFirstColumn(uil); laShowLabel(uil,c,"Hello world!",0,0); laShowItem(uil,c,0,"MY_invoke_test"); laShowItem(uil,c,0,"MY_modal_test"); } int main(int argc, char *argv[]){ laGetReady(); laCreateOperatorType("MY_invoke_test", "Something!", "Print some strings.",0,0,0,INV_MyOperator,0,L'🗨',0); laCreateOperatorType("MY_modal_test", "Modal!", "Print mouse positions.",0,0,0,INV_MyModalOperator,MOD_MyModalOperator,L'🏃',0); laRegisterUiTemplate("my_panel","My Panel", MyPanel,0,0,"Demonstration", 0,0,0); // Uncomment this to load preferences. // laEnsureUserPreferences(); if(!MAIN.Windows.pFirst){ laWindow* w = laDesignWindow(-1,-1,600,600); laLayout* l = laDesignLayout(w,"My Layout"); laCreatePanel(l->FirstBlock,"my_panel"); laStartWindow(w); } laMainLoop(); }
-
显示数值控件
LaGUI 的所有数值控件均需要数据源才能显示。因此我们需要为您应用程序的业务数据注册数据的访问方式,这个“访问方式”在 LaGUI 中定义成一条条的属性。
通过您定义的属性,LaGUI 还自动管理数据的撤销和重做,也包括资源文件的读写。交由 LaGUI 撤销系统、修改记录系统和共享资源系统来管理的数据必须使用 LaGUI 的内存调用来分配。在接下来的例子中,我们只需要用到界面显示,因此涉及不到这些复杂用法。
最简单的属性定义例子
例如我们有这样一个全局的 C 定义:
typedef struct My{ int _pad; laSafeString* Name; int Age; int Gender; real Height; } My; My Stats;
在 LaGUI 中,C Struct 相当于
laPropContainer
。我们首先创建一个适用于My
类型的laPropContainer
:laPropContainer* my=laAddPropertyContainer("my", "My", "Struct My",0,0,0,0,0,LA_PROP_OTHER_ALLOC);
由于所有
My
实例(在这里只有一个My Stats;
)的内存都不由 LaGUI 分配,因此在最后一个参数必须设置LA_PROP_OTHER_ALLOC
以告知 LaGUI ,同时由于在这个例子中我们不需要 LaGUI 创建或者删除My
实例,也不需要赋值NodeSize
参数。接下来我们就可以向
my
这个laPropContainer
中添加各个属性,使用对应的laAddxxxxProperty()
函数。这个例子足够简单,我们不需要get/set
回调,因此只需提供成员相对于My
的首地址偏移量。laAddStringProperty(my, "name", "Name", "My name",0,0,0,0,1,offsetof(My,Name),0,0,0,0,0); laAddIntProperty(my, "age", "Age", "My age",0,0,"years old",100,0,1,25,0,offsetof(My,Age),0,0,0,0,0,0,0,0,0,0,0); laAddFloatProperty(my, "height", "Height", "My height",0,0,"cm",2,0.3,0.01,1.76,0,offsetof(My,Height),0,0,0,0,0,0,0,0,0,0,0); laProp* ep=laAddEnumProperty(my, "gender","Gender","My gender",0,0,0,0,0,offsetof(My,Gender),0,0,0,0,0,0,0,0,0,0); laAddEnumItemAs(ep,"MALE","Male","Gender being male",0,L'♂'); laAddEnumItemAs(ep,"FEMALE","female","Gender being female",1,L'♀');
注意到
laSafeString* Name;
不是char[]
,LaGUI 提供了laSafeString
的便利功能,只需在注册属性时将IsSafeString
参数置为非0。此外,我们要告诉 LaGUI 我们业务数据的根,这样 LaGUI 才能找到第一个
my
的实例(在这个例子中只有一个)。laPropContainer* root=laDefineRoot(); laAddSubGroup(root,"me","Me","Me root", "my", 0,0,0,0,myget_Stats,0,0,0,0,0,0,0);
属性到这里就注册完成了,现在可以在界面上显示刚才注册的这些属性:
void MyProperties(laUiList *uil, laPropPack *This, laPropPack *DetachedProps, laColumn *UNUSED, int context){ laColumn* c=laFirstColumn(uil); laShowLabel(uil,c,"Hello world!",0,0); laShowItem(uil,c,0,"me.name"); laShowItem(uil,c,0,"me.age"); laShowItem(uil,c,0,"me.height"); laShowItem(uil,c,0,"me.gender"); }
属性注册与面板注册的先后顺序无所谓。之后,对
My Stats;
的值初始化之后,就能够运行程序了。你可以通过控件修改这些属性的值,如果通过“🞆”菜单调出一个新的“Properties”面板,你可以观察到两个面板上的属性同步刷新。属性简易示例程序的代码应该类似于这样:
#include "la_5.h" extern LA MAIN; typedef struct My{ int _pad; laSafeString* Name; int Age; int Gender; real Height; } My; My Stats; void* myget_Stats(void* unused, void* unused1){ return &Stats; } void MyProperties(laUiList *uil, laPropPack *This, laPropPack *DetachedProps, laColumn *UNUSED, int context){ laColumn* c=laFirstColumn(uil); laShowLabel(uil,c,"Hello world!",0,0); laShowItem(uil,c,0,"me.name"); laShowItem(uil,c,0,"me.age"); laShowItem(uil,c,0,"me.height"); laShowItem(uil,c,0,"me.gender"); } int main(int argc, char *argv[]){ laGetReady(); Stats.Age=25; Stats.Gender=0; Stats.Height=1.76; strSafeSet(&Stats.Name,"ChengduLittleA"); laPropContainer* root=laDefineRoot(); laAddSubGroup(root,"me","Me","Me root", "my", 0,0,0,0,myget_Stats,0,0,0,0,0,0,0); laPropContainer* my=laAddPropertyContainer("my", "My", "Struct My",0,0,0,0,0,LA_PROP_OTHER_ALLOC); laAddStringProperty(my, "name", "Name", "My name",0,0,0,0,1,offsetof(My,Name),0,0,0,0,0); laAddIntProperty(my, "age", "Age", "My age",0,0,"years old",100,0,1,25,0,offsetof(My,Age),0,0,0,0,0,0,0,0,0,0,0); laAddFloatProperty(my, "height", "Height", "My height",0,0,"cm",2,0.3,0.01,1.76,0,offsetof(My,Height),0,0,0,0,0,0,0,0,0,0,0); laProp* ep=laAddEnumProperty(my, "gender","Gender","My gender",0,0,0,0,0,offsetof(My,Gender),0,0,0,0,0,0,0,0,0,0); laAddEnumItemAs(ep,"MALE","Male","Gender being male",0,L'♂'); laAddEnumItemAs(ep,"FEMALE","female","Gender being female",1,L'♀'); laRegisterUiTemplate("my_properties","Properties", MyProperties,0,0,"Demonstration", 0,0,0); // Uncomment this to load preferences. // laEnsureUserPreferences(); if(!MAIN.Windows.pFirst){ laWindow* w = laDesignWindow(-1,-1,600,600); laLayout* l = laDesignLayout(w,"My Layout"); laCreatePanel(l->FirstBlock,"my_properties"); laStartWindow(w); } laMainLoop(); }
属性定义参考
LaGUI 支持的属性类型如下表所示:
属性类型 对应 C 类型 LaGUI 控制的操作 LA_PROP_INT
32位整数 读、写、数组、显示 LA_PROP_FLOAT
64位浮点数 读、写、数组、显示 LA_PROP_ENUM
8/16/32位整数 读、写、数组、显示 LA_PROP_STRING
8位整数数组或 laSafeString*
读、写、显示 LA_PROP_SUB
64位地址或 laListHandle
写指针、读指针和偏移、读写列表、显示列表和成员 LA_PROP_RAW
64位地址 (仅通过回调在文件读写时访问) LA_PROP_OPERATOR
- 通过 This 的工具调用1 1: 目前属性路径必须仅包含工具属性标识符,否则不工作。
LA_PROP_SUB
属性可递归包含,因此可以以树状方式描述整个应用程序的数据结构。下面这个对照示意解释了一种简易文件树结构的可能注册方式。建议通过各个 LaGUI 示例程序以及“Our Paint”软件的源代码更详细地了解向 LaGUI 描述您业务数据结构的方法。数据结构 | 属性注册样式 | struct Folder{ | laListItem Item; | SUB "folder" char Name[128]; | STRING "name" int Privileges; | INT "privileges" laListHandle SubFolders; | SUB_PROP LIST "folders" of "folder" laListHandle Files; | SUB_PROP LIST "files" of "file" }; | | struct File{ | laListItem Item; | SUB "file" char Name[128]; | STRING "name" int Size; | INT "size" void* Data; | RAW "data" }; | | struct FileBrowser{ | SUB "application" int SomeOtherStuff; | SUB_PROP LIST "folders" of "folder" laListHandle SubFolders; | }; | | SUB "(__LAGUI_ROOT__)" | SUB_PROP "my_application" of "application"
-
在执行工具时获得数据块引用
许多时候,我们想要针对某个数据块执行操作,就像类的成员函数一样操作其自身。在 LaGUI 中,这通过将工具注册为属性容器下的“工具属性”来实现,就像下表中类比的一样:
C C++ LaGUI func(object); object→func(); object_prop_container::"tool_func" 如果要操作上述程序的
My Stats
,就需要将工具注册为laPropContainer "my"
下的一个工具属性,这时 LaGUI 在以属性方式调用该工具时就能传递正确的This
指针。从工具回调中取This->EndInstance
,即可获得原始的&Stats
地址。这里我们继续在刚才的程序上改进:int INV_ShowMyStats(laOperator* a, laEvent* e){ My* stats=a->This?a->This->EndInstance:0; if(!stats){ printf("Operator not invoked from property.\n"); return LA_FINISHED; } char* name=(stats->Name&&stats->Name->Ptr)?stats->Name->Ptr:""; printf("Hi! My name is %s and I'm %d years old :D\n",name,stats->Age); logPrint("Hi! My name is %s and I'm %d years old :D\n",name,stats->Age); return LA_FINISHED; }
这个工具执行时先通过
a->This->EndInstance
获得实例指针,如果发生任何问题(例如未通过属性调用)则不工作。你也可以设计为在两种情况下都工作(例如在fruits
示例程序),具体实现取决于程序的业务逻辑。注册工具时,要指定该工具在
laPropContainer "my"
容器中的标识符以及替代界面名称、描述等:laCreateOperatorType("MY_show_my_stats", "Show Stats!", "Shoy my stats!",0,0,0,INV_ShowMyStats,0,0,0); laPropContainer* my=laAddPropertyContainer("my", "My", "Struct My",0,0,0,0,0,LA_PROP_OTHER_ALLOC); // ... laAddOperatorProperty(my, "show", "Show Stats with *This","Show stats called from \"my\" container","MY_show_my_stats",0,0);
在显示按钮时,原则上只需输入
"my.show"
作为属性路径,但由于 LaGUI 的限制,工具属性只能作为属性路径字符串的起点,也就是说只能输入"show"
。此时需要设置"my"
作为其This
父级,由于我们没有单独的"my"
作为控件(或者从模板中访问),所以暂时只能设置为一个我们额外创建的"my"
控件(它将以 LaGUI 的默认模板显示为一个属性的集合)。laShowItem(uil,c,0,"me.name"); // ... laUiItem* collection=laShowItem(uil,c,0,"me"); laShowItem(uil,c,&collection->PP,"show"); laShowItem(uil,c,0,"MY_show_my_stats");
在这个例子中显示了两个按钮,第一个按钮通过
This
指针调用,第二个直接调用,运行程序以观察二者区别。 -
更进一步
初步入门教程到这里就结束了,要了解 LaGUI 的更进一步用法,请参阅附带的 LaGUI 示例程序包,以及其他的用 LaGUI 制作而成的应用程序,例如“Our Paint”。您也可以在代码仓库提出工单以确认更多未在教程中明确解释的操作。
2023/02/22 20:42:33