关于GUI库适应性布局设计的思考与总结

最近几天又开始把注意力放到搁置很久的坑——KatUI上。最初是一个目标为跨Linux Windows的桌面开发库,不过每次才做完窗口管理,开始考量如何利用已有的基础搞一个适应性的布局——我都会感到原来的代码实在太烂根本没法实现接下来的构想,只好又推倒重来。反反复复让人耐心全无。不过这次我打算把整个工程的工作划分成几个模块抽离出来,挨个实现、通过git submodule管理。所以我创建了个KatLayout仓库专门做适应性布局实验。

啊?不就是一个控件x y Width Height四个属性,复杂点就控件嵌套吗?有什么好研究的?

发出这种疑问的人的眼中的桌面开发可能长这样:QQ截图20200422131524.jpg

可那是过去式了,我的目标是这样:
QQ截图20200422131700.jpg

甚至是这样:
QQ截图20200422131647.jpg

简单介绍

整个布局库的类大体如下(当然还在调整中)
QQ截图20200422133730.jpg

其中主要算法在Layout内;Dynamic、Panel、Grid、Stack负责以不同的方式组织多个子组件;Widget留作以后添加消息传递和界面渲染代码;其余都是Layout的包装。

布局的核心:class Layout

Layout通过这样一个结构储存一个组件的布局信息:

    static const float empty=std::numeric_limits<float>::lowest();
    struct Axis{
        float head=empty;
        float body=empty;
        float tail=empty;
        bool scaleHead=false,scaleBody=false,scaleTail=false;
        bool extended=false;
        struct Limit{
            float max=empty;
            float min=empty;
        }limit;
    }x,y;

以x轴为例:

  1. headbodytail分别代表本组件(图中蓝色)与父组件的左边距、宽度、右边距。通常地,三个变量有一个留空(empty),Layout根据父组件和用户设置的两个变量计算出留空变量;特别地,当headtail均为空,Layout将计算head=tail=(parent.body-body)/2,表现为组件水平居中。
  2. 当scaleXXX为false时,XXX所代表的坐标单位为像素(px);反之代表的坐标单位为parent.body,也就是按照父组件的大小缩放(scale)。
  3. 当extended==true,body的大小由其子组件决定,表现为随子组件的变大而变大。
  4. Limit.max Limit.min将限制组件宽度大于等于min、小于等于max;留空则不限制。

tail.jpg

y轴同理,具体算法见代码仓库。

class Layout的包装

因为如上的使用规则,直接让用户接触struct Axis可能会造成许多意料外的行为。为了使错误尽可能在编译期间暴露出来,我增加了几个包装类。

Fixed:传统的固定布局

QQ截图20200422144220.jpg

为了调用者可以自由的决定对应的字段使用px还是scale,此处使用函数模板和static_assert限定X,Y,W,H的类型只能为int或者float,后面几个类也使用了这样的手法

template<typename X,typename Y,typename W,typename H>
Fixed(Horizontal horizontalDock,Vertical verticalDock,X x,Y y,W width,H height){
//static_assert(std::is_same<X,int>::value||std::is_same<X,float>::value,"The template parameter must be int or float.")
//...
}

Horizontal Vertical定义如下

enum class Horizontal{Left,Center,Right};
enum class Vertical{Top,Center,Bottom};

horizontalDock决定了组件是左停靠、水平居中还是右停靠;verticalDock决定组件是上停靠、垂直居中还是下停靠。
horizontalDock为左停靠时,x就是组件与父组件的左边距,右停靠则为右边距,verticalDock同理;width height就是x.bodyy.body

Margin:受前端启发的布局

相对于Fixed的布局状态只能head\bodybody\tailbody\居中三选一,Margin则只允许填入head和tail,不接受body设定。
QQ截图20200422150019.jpg

new Margin(10,10,10,10);   //上下左右都距离父组件10px!
new Margin(10);            //缩略的写法

new Margin(0.2f,0.2f,0,0); //左右在距离父组件边缘的五分之一处,上下紧贴父组件边缘

Extended:扩展的布局

QQ截图20200422151816.jpg

看起来复杂了点,其实就是前几个类的变换。
且看最长的构造函数:

template<typename X,typename Y>
Extended(Horizontal horizontalDock, Vertical verticalDock, X x, Y y){

Fixed对比,缺少了Width和Height,此处表明长宽都由子布局决定,horizontalDockverticalDockxy的作用与前面介绍过的一致。
类似的:
上或者下停靠、高度扩展、宽度固定

Extended(Vertical verticalDock, Y y, W width)

上或者下停靠、高度扩展、左右边距

Extended(Vertical verticalDock, Y y, L left, R right)

至此布局设计基础类已经介绍完,但只凭这些还远远不够。

多子组件布局

上面介绍的几个基础类都只允许保留一个子组件,而多子组件布局负责组织这些布局类;同时多子组件布局还可以互相组合来实现更复杂的布局效果。
这是多子组件的目标(动图示例取自别的应用):
GIF.gif

GIF2.gif

GIF3.gif

GIF4.gif

也许看到vs和vscode你会质疑富文本渲染要怎么做——渲染是另一个难题(从最开始的展示图可以看到,class Widget留作今后做渲染的接口),这里只考虑布局计算

Grid 表格组件

QQ截图20200422171029.jpg
该组件可以把一个区域按照像素序列(指定每行每列的像素宽度)、比例序列(指定每行每列占比)或者指定数量(平分成任意行任意列)划分成任意块,添加子组件的同时需要设置子组件在表格中的行列、子组件跨过的行数列数:

void setChild(int col,int row,int spanCol,int spanRow,Widget* widget)

当使用像素序列构造时,表格长宽为序列总和;当使用比例序列和指定数量构造时,表格占满整个父组件。

Panel

最简单的一个多子组件布局。不干涉子组件的位置,效果类似一个开了多个窗口的屏幕。

Stack

QQ截图20200422172035.jpg

Stack将添加的子组件套在Extended容器里(因此可以放入任意大小的子组件),从指定的角落开始,按照指定方向排列子组件,并自动换行的布局类。
Stack子组件排列的方向的body将铺满父组件。
其原理是覆写了Layout的计算布局函数,在其中挨个计算子布局,超过就换行,且每行的高度为此行最高的子组件。
QQ图片20200422172400.gif

Dynamic

用来实现布局切换,例如UWP设置中的列表被挤压收起的效果
GIF4.gif

这个布局将displayConditionWidget*成对保存,每次计算时选择一个符合displayCondition的组件作为其子组件展示,所有子组件的展示条件都不符合时使用默认子组件。displayCondition实质上是一个lambda表达式:

struct Size
{
    int height, width;
    float scale_height, scale_width;
};
using displayCondition = std::function<bool(Size)>;

这几个负责管理组件的组件还在修改测试中。

一些别的想法

碍于c++的特性,像以上那些布局类使用起来实在太过麻烦。如果套用XML,运行时根据XML实时加载、或者编译时根据XML生成对应的布局c++代码,或许会好一些。不过这都是布局系统写完了以后顺便加上去的事,并不是一个难点。至于尝试html+js+WebAssembly生成到前端,我对前端只是略知一二而对他们的API并不熟悉,况且写一个c++ parser又是另一个吐血工程(偷懒用llvm?)以后再慢慢考虑吧。

本文链接:

http://yorkin.cool/index.php/archives/23/
1 + 9 =
5 评论
    404Chrome 84Windows 10
    4月22日 回复

    tql

    InkHinChrome 84OSX
    4月22日 回复

    tql。

    阳光加冰Chrome 70Android
    4月23日 回复

    看起来对布局有过研究啊,接近我认识的使用层面的qt的布局了,如果便于使用的话,linux版本值得期待喔

      YorkinChrome 81Windows 10
      4月23日 回复

      @阳光加冰 有 生 之 年 了(悲观)

    阳光减冰QQ Browser 6Android
    4月23日 回复

    tql。顶一个