{read_more}
本文档打印出来容易阅读。点击 浏览器菜单→打印 或者使用 `Ctrl+P` 快捷键。
欢迎使用 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()
或者 Modal()
回调中返回 LA_RUNNING
或者 LA_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 示例程序以及“好得涂”软件的源代码更详细地了解向 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 制作而成的应用程序,例如“好得涂”。您也可以在代码仓库提出工单以确认更多未在教程中明确解释的操作。
2022/10/22 16:35:47 - 2023/02/22 20:42:33