| #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 指针调用,第二个直接调用,运行程序以观察二者区别。  |