| #1467 | 自定义面板并添加界面元素
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();
}
请注意,由于读取用户设置后将创建窗口,因此各种界面资源的注册(比如这里的面板)需要在读取用户设置之前进行。 |
| #1642 | 添加按钮
按钮在 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() Or Modal() 回调中返回 LA_RUNNING Or 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();
}
|
| #1643 | 显示数值控件
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"
|
| #1664 | 在执行工具时获得数据块引用
许多时候,我们想要针对某个数据块执行操作,就像类的成员函数一样操作其自身。在 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 指针调用,第二个直接调用,运行程序以观察二者区别。 |