(按:
web领域,GUI编程有着学习曲线,
琐碎,平凡的GUI,为什么需要复杂的学习呢,
我希望可以设计实现一个简单,能立即上手的GUI框架)
我曾经在预研工作中实现过一版IMGUI,作为用户扩展的GUI,
模拟器 API文档
IMGUI明显的好处是,用户不需要熟悉GUI的概念,比如html / xml / css或其它的 GUI 领域特定语言(DSL),只需要有一般脚本编程技能就可以了,
const button = Button.create({text: 'click me'});
if (button.click) {
console.log(’i am clicked, dont stop‘)
}
一目了然,是吧
IMGUI的问题
问题来了,那为什么大多数GUI框架,未选择IMGUI?
首先,这问题不成立,在早期C编程时代,大家写的GUI都是IMGUI,因为那时,大家写UI本来就是即时绘制,
// 每祯循环,所以叫做“即时”模式
void onUpdate() {
// 绘制 button
drawButton("click me", "thisButtonID");
Event clickEvent;
// 过滤是否有点击消息
filterEvent(&clickEvent);
if(clickEvent.valid) {
// 是否点击在button区域中
if( getRec("thisButtonID").include(clickEvent.point) ) {
printf("i am clicked")
}
}
}
然后,这问题很有价值,
GUI思想一直在发展,从早期绘制,到GUI编程成为一个相对独立的编程,出现了面向对象编程,将
GUI关注于 对象和事件,Button作为控件
对象
const btClickMe = new Button(...)
btClickMe.on('click', ()=>{
console.log('i am clicked')
})
更近一步地,因为GUI相当关心排版问题,即需要组织一颗GUI树,出现了GUI排版描述语言, 即一般的XML/HTML/JSON表示
const GUIDesc = {
type: "layout", // 是排版容器节点
direction: "vertical", // 竖直排版
align : {
longtitude: "begin", // 径度对齐,begin即向上对齐
latitude: "begin", // 纬度对齐,begin即向左对齐
},
children : [
{
type: 'button', // 一个button组件
text: 'click me',
onClick: ()=>{ // 点击回调
console.log('i am clicked');
},
children: [...]
}
]
}
const gui = createGUI(GUIDesc);
这看起来有点像HTML了,是不?
这种方式对于良好组织复杂GUI有好处,但对于制作,比如说一个面板,简单的工具界面,反而显得复杂。
另外,
IMGUI一般被人认为影响效率, 它的实现经常每次事件都会有两次执行,一遍逻辑,再一遍layout输出,但也许有巧妙的设计可以避免效率上的开销;
IMGUI 混合逻辑和渲染,—— 有时是缺点,它影响大型GUI的整体设计,但在用户扩展里,经常是优点,而且,运用组件化实践,它可以做到通用化的GUI编程;
IMGUI的需求
需求来自两方面,
GUI编程的学习成本开始变高(你需要学习一个描述型语言,学习框架用法才能产生一个简单GUI),不能满足立即上手的需求(很多人不需要成为专家),这块先不表,
另一方面,现代的GUI越来越有动态性,传统上,XML/HTML擅长表达静态页面,如果兼容做动态页面,则需要和代码结合的一种表示方法,比如说 react 的 jsx文件,vue的 嵌入表达式。
如果有这样一个需求,如果上有一个按钮(打开/收起),下面是一张图,随着按钮点击,图会展开和收起,
IMGUI是这样做的
const button = Button.create({text: 'click me'});
if(button.click){
isPicShown = !isPicShown; // toggle state
}
let pic = null;
if(isPicShown) {
pic = Image.create('xxx.jpg')
}
可以看到, 做高交互性的GUI,是 排版和逻辑的混合,很适合直接用代码表达,这里IMGUI是相当有价值的
IMGUI的改进
之前说过,GUI的思想在发展,而我想的是,大大加强IMGUI,让之能适应大多数情况的GUI设计,
关键的几个设计点,
- 每个GUI对象有构造,内部状态,外部属性
- 在渲染更新函数里,渲染,事件,生命周期都写在一起
GUIDefine("Button") {
arguments:{ // 这里构造属性,比如 Button.create(...arguments) 这里的表示
...
},
states: { // 这里是内部持久状态
...
},
export: { // 这里是外部属性, 比如说 button.isClick == true
...
}
onUpdate: (event) => {
if(event.afterAttach) { // 生命周期事件
} else if (event.beforeDetach) {
}
rect = Rect.create(...)
clickEvt = event.system.click
if(rect.include(clickEvt.position) ) { // 比如这里处理系统点击事件,但真实并不会这样做,请思考为什么?
triggerEvent(this.isClick, true) // 这样就可以if(button.isClick) 了
}
}
}
- 用样式模板+属性覆盖,做为theme方案
// theme 是UI模板
GUI.setTheme('defaultTheme');
// ...
Button.create({Theme="xxx", text:"click me", color:0x000}); // color 已经包含在
- 如何定义UI模板(theme)
// 对所有组件,给予一个默认的属性
UITemplate("defaultTheme") {
Button = {
color:0xbbb;
...
};
panel = {
background:...,
}
}
// 第二种方式可以在组件里,加入几种theme的本组件属性
GUIDefine("Button") {
// ...
ThemeOf("defaultTheme", {
color:"xxx";
},
ThemeOf("deepinTheme", {
color:"xxx";
}
}
// 第一种适合实现在UI库,整体的theme定义
// 第二种特别适合用在 用户自定义组件里