201801 DDD DSL Design War Layout in Python - xiaoxianfaye/Courses GitHub Wiki

1 Copyright Statement

本课程的主体内容均来自于孙鸣、邓辉发布在IBM论坛上的文章《基于Java的界面布局DSL的设计与实现》。
https://www.ibm.com/developerworks/cn/java/j-lo-uidsl/

2 Problem

需求一:请先编写一个迷你计算器的外观程序,如下图所示。注意仅需要编写外观程序,不需要编写控件事件响应和具体计算逻辑。

python problem layout 1

需求二:请将所有数字以及操作按钮按照横向和纵向各2%进行留白,如下图所示:

python problem layout 2

请具体分析一下,在需求一的基础上,为了满足需求二,修改了哪些代码?有没有减少代码修改量的方法?

3 Showcase & Discuss

请学员展示自己之前的设计思路和实现方式,大家可以互相点评、讨论。以学员为主,讲师为辅。

4 About Layout

  • Do you like UI design? Creative or Boring?
  • Why do you feel creative or boring?
  • What procedures are involved in UI design?
    • Creative Design: UI Layout Style & Interaction with Users
    • Boring Implementation

此时,你头脑中那些清晰、完整的设计概念开始变得琐碎,你不得不和那些低层次的坐标位置打交道。

更糟糕的是,当你好不容易做好了一个界面,但是发现其中某些元素的布局需要做一些调整时,这个你本应认为是一个很简单的改变却造成大量重复的低层次坐标位置更改时,你肯定会认为做界面非常机械和乏味。

  • What is the root cause of the problem?
    • Gap between Creative Design and Implementation Language

问题的根本原因在于:界面设计创意和实现这些创意概念的语言之间存在很大的断层。因为这个断层的存在,在具体实现时,你就必须得把这些清晰、完整的布局样式降级成一些琐碎的、没有什么意义的低层次的坐标值,使得实现语言能够理解。

这个过程不仅乏味,而且最终的实现也非常的脆弱。一个在布局样式层面非常简单的更改,就会造成实现层面的巨大变动。比如,现在要把一组元素同时按比例缩小10%,在座的各位肯定都知道这个更改意味着什么……。

  • What are the existing "solutions"?
    • Visual UI Design Tool
    • Layout Manager
      Do any of them fundamentally solve the problem?

可视化界面设计工具确实避免了不少繁琐的界面元素摆放工作,但是对于专业的界面设计来说,通过拖放设计出来的界面在准确度和规范性上都有待提高。事实上,在座的各位已经很少用可视化界面设计工具了吧?

此外还有更为重要一点,那就是存在于设计者头脑中的布局样式仍然没有被明确地描述出来,而是被降级成一个个摆放在一起的零散的组件,虽然这些组件本身是可视的。这个语义断层的存在同样会使得通过可视化界面设计工具设计出来的界面非常的脆弱。

布局管理器试图通过提供一些常用的布局样式来解决这个问题。但是,这种做法非常的僵化,也就是说你只能使用现有的布局管理器,如果它们无法满足你的要求,你也无法自己定制。此外,这些布局管理器仅仅适合于一些简单的情况。对于一些复杂的布局样式来说,它们的描述能力就显得非常得不足。在座的各位应该都和GridBagLayout斗争过吧?

  • Can we do better?

在这次课程中,我们会带着大家设计出一种更好的解决方案。

我们不去试图把界面设计者头脑中的设计概念和样式逐步降级、分解成所使用的实现语言能够理解的低层概念,也不是提供一些已经完成的、确定的、但难以扩充和更改的布局样式库供界面设计者使用。

我们会开发一种专门用于描述高层界面设计样式的语言。通过这种语言,界面设计者可以直接、明确地描述出他们头脑中的布局设计样式;通过这种语言,界面设计者可以自己方便、灵活地制定自己需要的布局样式。也就是说,本来仅存在于界面设计者头脑中的抽象布局样式现在也变得可描述、可编程了。

5 Analysis

分析:定义清楚问题是什么。

这个问题大家都很熟悉,而且问题本身也是比较清楚的,因此分析阶段就略过了。

6 Design

设计:问题分析清楚以后,提出解决问题的逻辑框架。

6.1 Core Concepts

design core concepts

Component(组件)和Position Relation(组件之间的位置关系),是这个领域的核心概念。

6.2 What does a Component mean?

一个Component是什么意思?什么是一个Component?Component的语义(Semantics)是什么?

Component是一种基本的布局元素,可以对Component进行平移和伸缩,使其和给定容器(Container)中的一个布局空间Rectangle匹配。

比如,对于Button这个Component来讲,它具有传统按钮的外观,但是它在布局上所占的实际空间则是由为其指定的Rectangle决定的。此外,Component最终要在界面上显示出来,就必须有一个物理上的Container。

也就是说,只要给定了一个Rectangle和一个Container,一个Component就可以在界面上指定的布局位置呈现出来。

design rectangle

例如,给定一个Rectangle(0, 0, 200, 60)和一个Container,将一个Button摆放到Rectangle中,描述如下:

button.at(0, 0, 200, 60).in(container)

布局展现如下:
design layout component

显然,仅仅提供Component这样一种基本的布局元素是不够的。

6.3 What does a Position Relation mean?

Component与Component之间还存在Position Relation(相对位置关系)。

6.3.1 What are the Core Position Relations?

有哪几种最基本、最核心的Position Relation呢?
beside和above。

design layout beside

design layout above

6.3.2 What does Beside or Above mean?

那么,beside和above到底是什么意思?什么是beside或above?beside和above的语义(Semantics)是什么?

beside和above用来定义Component之间的Position Relation。它们都由两个Component以及一个Ratio(比例)构造出来,构造出来还是一个Component。只要给定了一个Rectangle和一个Container,beside或above生成的Component就可以在界面上指定的布局位置呈现出来。

在beside中,按照给定的比例把第一个Component摆放在第二个Component的左边。在above中,按照给定的比例把第一个Component摆放在第二个Component的上边。beside和above被称为“组合子Component”(Combination Components: beside, above)。

例如,给定一个Rectangle(0, 0, 300, 60)和一个Container,将一个TextField和一个Button水平顺序摆放到Rectangle中,且TextField占据80%的比例,描述如下:

beside(textField, button, 0.8).at(0, 0, 300, 60).in(container)

布局展现如下:
design layout beside components

同样,给定一个Rectangle(0, 0, 300, 60)和一个Container,将一个TextField和一个Button垂直顺序摆放到Rectangle中,且TextField占据50%的比例,描述如下:

above(textField, button, 0.5).at(0, 0, 300, 60).in(container)

布局展现如下:
design layout above components

6.3.3 The components' world is Closed

Components在beside和above操作下是封闭的。也就是说,两个Component用 “beside”或者“above”组合以后还是一个Component,就可以和其它的Component再次进行“beside”或者“above”组合。这样,我们就可以使用这两个简单的操作生成更加复杂的Component,从而完成复杂的界面布局。

例如,下面这段描述:

above(beside(textField, button1, 0.8), button2, 0.5).at(0, 0, 300, 60).in(container)

布局展现如下:
design layout closed

组合子满足封闭性非常重要,因为只有满足封闭性,组合子才能和其它原子或者组合子再组合……。可以想象,如果一个语言提供的组合手段是能够封闭的,那么它就能够高效地帮助你构建出非常复杂的东西。

6.4 Special Component - Empty

如何描述界面布局中的空白?

我们增加一种特殊的原子元素:Empty。它的作用只是占据一定的布局空间。

例如,给定一个Rectangle(0, 0, 300, 60)和一个Container,将一个Button放在右半边并占据50%的比例,左半边空置,描述如下:

beside(empty, button, 0.5).at(0, 0, 300, 60).in(container)

布局展现如下:
design layout empty

后面会看到,正是这个Empty以及beside和above操作的闭包性质为我们描述任意复杂的布局样式提供了可能。

6.5 Language of Geometric Positions

界面布局位置描述语言:

  • Primitive Components: Empty, ButtonCmp, EntryCmp …
  • Combinator Components: beside, above
  • Rectangle:AT
  • Container:IN

7 Implementation - Language of Geometric Positions

实现:选择实现技术把逻辑框架的软件模型实现出来。

7.1 Overview

7.1.1 OO && Functional

可以用OO或Functional实现。课堂上用OO实现,Functional实现留作作业。

代码路径:original.oo

7.1.2 Based on Tkinter

我们基于Python的Tkinter来实现界面布局位置描述语言。以后大家可以根据自己的需要在其它的语言和界面开发工具包上去实现该界面布局语言。

7.1.3 About Test

关于测试,一部分代码只能通过肉眼观察布局展现来验证,另一部分代码可以通过TDD测试用例验证。

因此,我们要准备一个用于测试布局效果的测试框架。代码详见layout_test(framework_oo).py。拷贝到开发环境中时,注意修改文件名为layout_test.py。

layout_test.py

from Tkinter import Tk, Canvas

class LayoutTest(object):
    def __init__(self):
        root = Tk()
        root.title('Layout Test')

        self.container = Canvas(root, width=600, height=400)
        self.container.create_rectangle(0, 0, 600, 400, fill='light grey')
        self.container.pack()

        self.tests()

        root.mainloop()

    # Definiting Test cases here
    
    def tests(self):
        # Invoking test cases here
        pass

if __name__ == '__main__':
    LayoutTest()

运行结果如下:
python impl layout test framework

7.1.4 Fluent Interface

API的写法采用“Fluent Interface”风格。

“Fluent Interface”是由《Domain Driven Design》的作者Eric Evans提出的概念。

在这种风格中,为了能够将调用形成一个句子,每个调用在结束时都返回了self。另外,在给方法起名时也有不同的考虑,不只是关注该方法的职责和功能,而且更关注该方法名在整个句子的上下文中是否通顺、是否更富表达力。

7.1.5 Steps

实现步骤:

  • Component
  • beside
  • above
  • Empty

7.2 Coding

7.2.1 Component

在前面的小节中,举过下面这个例子,现在我们来实现它。

给定一个Rectangle(0, 0, 200, 60)和一个Container,将一个Button摆放到Rectangle中,描述如下:

button.at(0, 0, 200, 60).in(container)

布局展现如下:
design layout component

layout_test.py

from layout import *

    # Definiting Test cases here
    def test_component(self):
        button('Button').AT(0, 0, 200, 60).IN(self.container)

    def tests(self):
        # Invoking test cases here
        self.test_component()

由于“in”是Python的关键字,所以IN和AT都全大写了。

新增Rectangle类封装了Rectangle的左上角坐标、长度和宽度。新增Component类包含AT()和IN()两个空方法。新增ButtonCmp类继承自Component类,包含一个Tkinter的Button成员变量,实现了AT()和IN()方法。为了实现Fluent Interface风格,ButtonCmp类的所有方法都返回self,类型都是Component。

layout.py

from Tkinter import Button

class Rectangle(object):
    def __init__(self, left, top, width, height):
        self.left = left
        self.top = top
        self.width = width
        self.height = height

class Component(object):
    def AT(self, left, top, width, height):
        pass

    def IN(self, container):
        pass

class ButtonCmp(Component):
    def __init__(self, text):
        self.cmp = Button(text=text)

    def AT(self, left, top, width, height):
        self.rectangle = Rectangle(left, top, width, height)
        return self

    def IN(self, container):
        self.cmp.master = container
        container.create_window(int(self.rectangle.left + self.rectangle.width * 0.5),
                                int(self.rectangle.top + self.rectangle.height * 0.5),
                                width=self.rectangle.width,
                                height=self.rectangle.height,
                                window=self.cmp)
        return self

def button(text):
    return ButtonCmp(text)

运行结果如下:
python impl layout component

7.2.2 beside

在前面的小节中,举过下面这个例子,现在我们来实现它。

给定一个Rectangle(0, 0, 300, 60)和一个Container,将一个TextField和一个Button水平顺序摆放到Rectangle中,且TextField占据80%的比例,描述如下:

beside(textField, button, 0.8).at(0, 0, 300, 60).in(container)

布局展现如下:
design layout beside components

先实现entry。

layout_test.py

    def test_beside(self):
        beside(entry(), button('Btn'), 0.8).AT(0, 0, 300, 60).IN(self.container)

    def tests(self):
        # Invoking test cases here
        # self.test_component()
        self.test_beside()

Python的Tkinter的单行输入框叫“Entry”。新增EntryCmp类继承自Component类,包含一个Tkinter的Entry成员变量,实现了AT()和IN()方法。为了实现Fluent Interface风格,EntryCmp类的所有方法都返回self,类型都是Component。

layout.py

from Tkinter import Button, Entry

class EntryCmp(Component):
    def __init__(self):
        self.cmp = Entry()

    def AT(self, left, top, width, height):
        self.rectangle = Rectangle(left, top, width, height)
        return self

    def IN(self, container):
        self.cmp.master = container
        container.create_window(int(self.rectangle.left + self.rectangle.width * 0.5),
                                int(self.rectangle.top + self.rectangle.height * 0.5),
                                width=self.rectangle.width,
                                height=self.rectangle.height,
                                window=self.cmp)
        return self

def entry():
    return EntryCmp()

再来实现beside。

我们用TDD实现beside的业务逻辑,即左右两个Component各自对应的布局空间Rectangle是否正确。既然是测试环境,肯定不用真实的控件,新增ComponentStub类为Component空实现类打桩。打桩的方法是简单保存一下Component的rectangle参数和container引用,用于验证beside的at()和in()是否以正确的参数被调用过。另外,为了更好地验证逻辑,rectangle左上角的坐标没有从(0, 0)开始。

test_all.py

import unittest

from tests.test_layout import TestBeside

if '__name__' == '__main__':
    unittest.main()

tests.test_layout.py

import unittest

from layout import Component, Beside

from Tkinter import Tk, Canvas

TOLERANCE = 1

class ComponentStub(Component):
    def AT(self, left, top, width, height):
        self.rectangle = [left, top, width, height]

    def IN(self, container):
        self.container = container

class TestBeside(unittest.TestCase):
    def setUp(self):
        self.leftcmp = ComponentStub()
        self.rightcmp = ComponentStub()
        self.beside = Beside(self.leftcmp, self.rightcmp, 0.8)

    def test_at(self):
        self.beside.AT(20, 10, 300, 60)

        self.check_rectangle([20, 10, 240, 60], self.leftcmp.rectangle)
        self.check_rectangle([260, 10, 60, 60], self.rightcmp.rectangle)

    def test_in(self):
        container = Canvas(Tk())

        self.beside.IN(container)

        self.assertTrue(container == self.leftcmp.container)
        self.assertTrue(container == self.rightcmp.container)

    def check_rectangle(self, expected, actual):
        self.assertEquals(len(expected), len(actual))
        for i in range(0, len(expected)):
            self.assertAlmostEquals(expected[i], actual[i], delta=TOLERANCE)

新增Beside类继承自Component类,包含一个leftcmp和rightcmp两个成员变量,实现了AT()和IN()方法。为了实现Fluent Interface风格,Beside类的所有方法都返回self,类型都是Component。同时,新增beside()函数。

layout.py

class Beside(Component):
    def __init__(self, leftcmp, rightcmp, ratio):
        self.leftcmp = leftcmp
        self.rightcmp = rightcmp
        self.ratio = ratio

    def AT(self, left, top, width, height):
        self.leftcmp.AT(left, top, int(width * self.ratio), height)
        self.rightcmp.AT(left + int(width * self.ratio), top, int(width * (1 - self.ratio)), height)
        return self

    def IN(self, container):
        self.leftcmp.IN(container)
        self.rightcmp.IN(container)
        return self

def beside(leftcmp, rightcmp, ratio):
    return Beside(leftcmp, rightcmp, ratio)

layout_test.py

    def test_beside(self):
        beside(entry(), button('Btn'), 0.8).AT(0, 0, 300, 60).IN(self.container)
    
    def tests(self):
        # Invoking test cases here
        # self.test_component()
        self.test_beside()

运行结果如下:
python impl layout beside

7.2.3 above

在前面的小节中,举过下面这个例子,现在我们来实现它。

给定一个Rectangle(0, 0, 300, 60)和一个Container,将一个TextField和一个Button垂直顺序摆放到Rectangle中,且TextField占据50%的比例,描述如下:

above(textField, button, 0.5).at(0, 0, 300, 60).in(container)

布局展现如下:
design layout above components

我们用TDD实现above的业务逻辑,即上下两个Component各自对应的布局空间Rectangle是否正确。同beside,使用ComponentStub。另外,为了更好地验证逻辑,rectangle左上角的坐标没有从(0, 0)开始。

test_all.py

import unittest

from tests.test_layout import TestBeside, TestAbove

if '__name__' == '__main__':
    unittest.main()

tests.test_layout.py

from layout import Component, Beside, Above

class TestAbove(unittest.TestCase):
    def setUp(self):
        self.upcmp = ComponentStub()
        self.downcmp = ComponentStub()
        self.above = Above(self.upcmp, self.downcmp, 0.5)

    def test_at(self):
        self.above.AT(20, 10, 300, 60)

        self.check_rectangle([20, 10, 300, 30], self.upcmp.rectangle)
        self.check_rectangle([20, 40, 300, 30], self.downcmp.rectangle)

    def test_in(self):
        container = Canvas(Tk())

        self.above.IN(container)

        self.assertTrue(container == self.upcmp.container)
        self.assertTrue(container == self.upcmp.container)

    def check_rectangle(self, expected, actual):
        self.assertEquals(len(expected), len(actual))
        for i in range(0, len(expected)):
            self.assertAlmostEquals(expected[i], actual[i], delta=TOLERANCE)

新增Above类继承自Component类,包含一个upcmp和downcmp两个成员变量,实现了AT()和IN()方法。为了实现Fluent Interface风格,Above类的所有方法都返回self,类型都是Component。同时,新增above()函数。

layout.py

class Above(Component):
    def __init__(self, upcmp, downcmp, ratio):
        self.upcmp = upcmp
        self.downcmp = downcmp
        self.ratio = ratio

    def AT(self, left, top, width, height):
        self.upcmp.AT(left, top, width, int(height * self.ratio))
        self.downcmp.AT(left, top + int(height * self.ratio), width, int(height * (1 - self.ratio)))
        return self

    def IN(self, container):
        self.upcmp.IN(container)
        self.downcmp.IN(container)
        return self

def above(upcmp, downcmp, ratio):
    return Above(upcmp, downcmp, ratio)

layout_test.py

    def test_above(self):
        above(entry(), button('Button'), 0.5).AT(0, 0, 300, 60).IN(self.container)
    
    def tests(self):
        # Invoking test cases here
        # self.test_component()
        # self.test_beside()
        self.test_above()

运行结果如下:
python impl layout above

7.2.4 Refactoring

7.2.4.1 Product Code

观察ButtonCmp和EntryCmp这两个原子组件类,会发现IN和AT方法有重复代码,因此将重复代码提取到一个父类BaseCmp中。

layout.py

class BaseCmp(Component):
    def AT(self, left, top, width, height):
        self.rectangle = Rectangle(left, top, width, height)
        return self

    def IN(self, container):
        self.cmp.master = container
        container.create_window(int(self.rectangle.left + self.rectangle.width * 0.5),
                                int(self.rectangle.top + self.rectangle.height * 0.5),
                                width=self.rectangle.width,
                                height=self.rectangle.height,
                                window=self.cmp)
        return self

class ButtonCmp(BaseCmp):
    def __init__(self, text):
        self.cmp = Button(text=text)

class EntryCmp(BaseCmp):
    def __init__(self):
        self.cmp = Entry()

7.2.4.2 Test Code

观察TestBeside和TestAbove这两个测试类,也有一些重复代码,比如构造container和check_rectangle()方法等,因此将重复代码提取到函数中。

test_layout.py

def _check_rectangle(testcase, expected, actual):
    testcase.assertEquals(len(expected), len(actual))
    for i in range(0, len(expected)):
        testcase.assertAlmostEquals(expected[i], actual[i], delta=TOLERANCE)

def _container():
    return Canvas(Tk())

class TestBeside(unittest.TestCase):
    def setUp(self):
        self.leftcmp = ComponentStub()
        self.rightcmp = ComponentStub()
        self.beside = Beside(self.leftcmp, self.rightcmp, 0.8)

    def test_at(self):
        self.beside.AT(20, 10, 300, 60)

        _check_rectangle(self, [20, 10, 240, 60], self.leftcmp.rectangle)
        _check_rectangle(self, [260, 10, 60, 60], self.rightcmp.rectangle)

    def test_in(self):
        container = _container()

        self.beside.IN(container)

        self.assertTrue(container == self.leftcmp.container)
        self.assertTrue(container == self.rightcmp.container)

class TestAbove(unittest.TestCase):
    def setUp(self):
        self.upcmp = ComponentStub()
        self.downcmp = ComponentStub()
        self.above = Above(self.upcmp, self.downcmp, 0.5)

    def test_at(self):
        self.above.AT(20, 10, 300, 60)

        _check_rectangle(self, [20, 10, 300, 30], self.upcmp.rectangle)
        _check_rectangle(self, [20, 40, 300, 30], self.downcmp.rectangle)

    def test_in(self):
        container = _container()

        self.above.IN(container)

        self.assertTrue(container == self.upcmp.container)
        self.assertTrue(container == self.upcmp.container)    

7.2.5 The components' world is Closed

在前面的小节中,我们提到:Components在beside和above操作下是封闭的。也就是说,两个Component用 “beside”或者“above”组合以后还是一个Component,就可以和其它的Component再次进行“beside”或者“above”组合。这样,我们就可以使用这两个简单的操作生成更加复杂的Component,从而完成复杂的界面布局。

在前面的小节中,举过下面这个例子,现在我们来实现它。

above(beside(textField, button1, 0.8), button2, 0.5).at(0, 0, 300, 60).in(container)

布局展现如下:
design layout closed

layout_test.py

    def test_beside_above(self):
        above(beside(entry(), button('Btn1'), 0.8), button('Btn2'), 0.5).AT(0, 0, 300, 60).IN(self.container)
    
    def tests(self):
        # Invoking test cases here
        # self.test_component()
        # self.test_beside()
        # self.test_above()
        self.test_beside_above()

运行结果如下:
python impl layout beside above

7.2.6 Empty

在前面的小节中,举过下面这个例子,现在我们来实现它。

给定一个Rectangle(0, 0, 300, 60)和一个Container,将一个Button放在右半边并占据50%的比例,左半边空置,描述如下:

beside(empty, button, 0.5).at(0, 0, 300, 60).in(container)

布局展现如下:
design layout empty

layout_test.py

    def test_empty(self):
        beside(empty(), button('Button'), 0.5).AT(0, 0, 300, 60).IN(self.container)
    
    def tests(self):
        # Invoking test cases here
        # self.test_component()
        # self.test_beside()
        # self.test_above()
        # self.test_beside_above()
        self.test_empty()

layout.py

class Empty(Component):
    def AT(self, left, top, width, height):
        return self

    def IN(self, container):
        return self

def empty():
    return Empty()

运行结果如下:
python impl layout empty

8 Implementation - Language of Layout Style

有了前面的那些基础的布局元素和组合手段后,我们就可以通过组合手段来把一些典型的布局样式抽象出来。

8.1 Horizontal Center

给定一个布局空间和一个布局组件,我们期望该组件能够按照指定的留白比例位于该布局空间的横向中心地带。我们可以把该布局样式抽象出来,并命名为hcenter。

design layout style hcenter

我们还可以在更复杂的布局样式中把hcenter当作一个基本语素使用。

layout_test.py

    def test_hcenter(self):
        hcenter(button('Button'), 0.1).AT(0, 0, 600, 400).IN(self.container)
    
    def tests(self):
        # Invoking test cases here
        # self.test_component()
        # self.test_beside()
        # self.test_above()
        # self.test_beside_above()
        # self.test_empty()
        self.test_hcenter()

layout.py

def hcenter(cmp, ratio):
    return beside(empty(), beside(cmp, empty(), (1.0 - 2.0 * ratio) / (1.0 - ratio)), ratio)

运行结果如下:
python impl layout style hcenter

8.2 Vertical Center

给定一个布局空间和一个布局组件,我们期望该组件能够按照指定的留白比例位于该布局空间的纵向中心地带。我们可以把该布局样式抽象出来,并命名为vcenter。

design layout style vcenter

我们还可以在更复杂的布局样式中把vcenter当作一个基本语素使用。

layout_test.py

    def test_vcenter(self):
        vcenter(button('Button'), 0.1).AT(0, 0, 600, 400).IN(self.container)
    
    def tests(self):
        # Invoking test cases here
        # self.test_component()
        # self.test_beside()
        # self.test_above()
        # self.test_beside_above()
        # self.test_empty()
        # self.test_hcenter()
        self.test_vcenter()

layout.py

def vcenter(cmp, ratio):
    return above(empty(), above(cmp, empty(), (1.0 - 2.0 * ratio) / (1.0 - ratio)), ratio)

运行结果如下:
python impl layout style vcenter

可以看到hcenter()和vcenter()之间除了beside、above不一样,其他代码都一样,用函数式编程范式重构去重。

layout.py

def hcenter(cmp, ratio):
    return _center(beside, cmp, ratio)

def vcenter(cmp, ratio):
    return _center(above, cmp, ratio)

def _center(poslayout, cmp, ratio):
    return poslayout(empty(), poslayout(cmp, empty(), (1.0 - 2.0 * ratio) / (1.0 - ratio)), ratio)

8.3 Center

给定一个布局空间和一个布局组件,我们期望该组件能够按照指定的纵、横留白比例位于该布局空间的中心地带。我们可以把该布局样式抽象出来,并命名为center。

design layout style center

我们可以通过hcenter和vcenter组合实现center。我们也可以在更复杂的布局样式中把center当作一个基本语素使用。

layout_test.py

    def test_center(self):
        center(button('Center'), 0.2, 0.1).AT(0, 0, 600, 400).IN(self.container)
    
    def tests(self):
        # Invoking test cases here
        # self.test_component()
        # self.test_beside()
        # self.test_above()
        # self.test_beside_above()
        # self.test_empty()
        # self.test_hcenter()
        # self.test_vcenter()
        self.test_center()

layout.py

def center(cmp, hratio, vratio):
    return vcenter(hcenter(cmp, hratio), vratio)

运行结果如下:
python impl layout style center

8.4 Horizontal Sequence

给定一个布局空间和一组布局元素,把这组给定的布局元素横向顺序排列。我们可以把该布局样式抽象出来,并命名为hseq。

design layout style hseq

同理,我们也可以在更复杂的布局样式中把hseq当作一个基本语素使用。

layout_test.py

    def test_hseq(self):
        hseq([button('1'), button('2'), button('3')]).AT(0, 0, 300, 60).IN(self.container)
    
    def tests(self):
        # Invoking test cases here
        # self.test_component()
        # self.test_beside()
        # self.test_above()
        # self.test_beside_above()
        # self.test_empty()
        # self.test_hcenter()
        # self.test_vcenter()
        # self.test_center()
        self.test_hseq()

layout.py

def hseq(cmps):
    if len(cmps) == 1:
        return cmps[0]

    return beside(cmps[0], hseq(cmps[1:]), 1.0 / len(cmps))

运行结果如下:
python impl layout style hseq

8.5 Vertical Sequence

我们希望抽象出这样一种布局样式:给定一个布局空间和一组布局元素,把这组给定的布局元素纵向顺序排列。我们可以把该布局样式抽象出来,并命名为vseq。

design layout style vseq

同理,我们也可以在更复杂的布局样式中把vseq当作一个基本语素使用。

layout_test.py

    def test_vseq(self):
        vseq([button('1'), button('2'), button('3')]).AT(0, 0, 150, 200).IN(self.container)
    
    def tests(self):
        # Invoking test cases here
        # self.test_component()
        # self.test_beside()
        # self.test_above()
        # self.test_beside_above()
        # self.test_empty()
        # self.test_hcenter()
        # self.test_vcenter()
        # self.test_center()
        # self.test_hseq()
        self.test_vseq()

layout.py

def vseq(cmps):
    if len(cmps) == 1:
        return cmps[0]

    return above(cmps[0], vseq(cmps[1:]), 1.0 / len(cmps))

运行结果如下:
python impl layout style vseq

可以看到hseq()和vseq()之间除了beside、above不一样,其他代码都一样,用函数式编程范式重构去重。

layout.py

def hseq(cmps):
    return _seq(beside, cmps)

def vseq(cmps):
    return _seq(above, cmps)

def _seq(poslayout, cmps):
    if len(cmps) == 1:
        return cmps[0]

    return poslayout(cmps[0], _seq(poslayout, cmps[1:]), 1.0 / len(cmps))

8.6 Block

在center、hseq、vseq这些布局样式的基础上,我们还可以定义出更加高阶的样式。比如,给定一个布局元素序列,我们希望它们在给定的布局空间中按照N行、M列排列。我们可以把该布局样式抽象出来,并命名为block。

design layout style block

layout_test.py

    def test_block(self):
        cmps = [button(str(i)) for i in range(1, 12)]
        block(cmps, 4, 3).AT(0, 0, 600, 400).IN(self.container)
    
    def tests(self):
        # Invoking test cases here
        # self.test_component()
        # self.test_beside()
        # self.test_above()
        # self.test_beside_above()
        # self.test_empty()
        # self.test_hcenter()
        # self.test_vcenter()
        # self.test_center()
        # self.test_hseq()
        # self.test_vseq()
        self.test_block()

layout.py

import copy

def block(cmps, rownum, colnum):
    paddedcmps = _padding(cmps, rownum * colnum)
    return vseq([hseq(paddedcmps[idx:idx + colnum]) for idx in range(0, rownum * colnum, colnum)])

def _padding(cmps, total):
    copiedcmps = copy.copy(cmps)
    return copiedcmps + [empty() for idx in range(0, total - len(cmps))]

运行结果如下:
python impl layout style block

8.7 Block With Margin

在block布局样式的基础上,希望每个元素都可以指定一些横向和纵向的留白,我们可以把该布局样式抽象出来,并命名为blockm。

design layout style blockm

layout_test.py

    def test_blockm(self):
        cmps = [button(str(i)) for i in range(1, 12)]
        blockm(cmps, 4, 3, 0.1, 0.1).AT(0, 0, 600, 400).IN(self.container)
    
    def tests(self):
        # Invoking test cases here
        # self.test_component()
        # self.test_beside()
        # self.test_above()
        # self.test_beside_above()
        # self.test_empty()
        # self.test_hcenter()
        # self.test_vcenter()
        # self.test_center()
        # self.test_hseq()
        # self.test_vseq()
        # self.test_block()
        self.test_blockm()

layout.py

def blockm(cmps, rownum, colnum, hratio, vratio):
    return block([center(cmp, hratio, vratio) for cmp in cmps], rownum, colnum)

运行结果如下:
python impl layout style blockm

9 Implementation - Mini-calculator

我们来看一个稍微复杂一些的例子,我们将使用前面制作的一些布局样式构建一个迷你计算器的外观,如下图所示:

python impl minicalc

layout_test.py

    def test_minicalc(self):
        texts = ['0', '1', '2', '+',
                 '3', '4', '5', '-',
                 '6', '7', '8', '*',
                 '9', '=', '%', '/']
        btns = [button(text) for text in texts]

        above(above(entry(),
                    beside(button('Backspace'), button('C'), 0.5), 0.5),
              block(btns, 4, 4), 0.3).AT(0, 0, 600, 400).IN(self.container)
    
    def tests(self):
        # Invoking test cases here
        # self.test_component()
        # self.test_beside()
        # self.test_above()
        # self.test_beside_above()
        # self.test_empty()
        # self.test_hcenter()
        # self.test_vcenter()
        # self.test_center()
        # self.test_hseq()
        # self.test_vseq()
        # self.test_block()
        # self.test_blockm()
        self.test_minicalc()

如果我们现在希望将所有数字以及操作按钮按照横向和纵向各2%进行留白,如下图所示:

python impl minicalc margin

我们所要做的仅仅是一行的改动,就是把block()换成blockm()。

layout_test.py

    def test_minicalc_margin(self):
        texts = ['0', '1', '2', '+',
                 '3', '4', '5', '-',
                 '6', '7', '8', '*',
                 '9', '=', '%', '/']
        btns = [button(text) for text in texts]

        above(above(entry(),
                    beside(button('Backspace'), button('C'), 0.5), 0.5),
              blockm(btns, 4, 4, 0.02, 0.02), 0.3).AT(0, 0, 600, 400).IN(self.container)
    
    def tests(self):
        # Invoking test cases here
        # self.test_component()
        # self.test_beside()
        # self.test_above()
        # self.test_beside_above()
        # self.test_empty()
        # self.test_hcenter()
        # self.test_vcenter()
        # self.test_center()
        # self.test_hseq()
        # self.test_vseq()
        # self.test_block()
        # self.test_blockm()
        # self.test_minicalc()
        self.test_minicalc_margin()

10 Summary

10.1 UI Layout Language

Language-Oriented Programming vs. Object-Oriented Programming
在设计中,我们没有采用对象技术中常用的一些设计手段,我们没有对界面布局本身进行抽象,也不是设计出一些特定的界面布局管理器。相反,我们把对象技术当成一种低层的抽象工具,并基于它来构建更高层次的抽象,创建出更加接近我们所工作的问题领域的界面布局语言,从而获得更高的生产力、表达力以及可重用性(还有什么比语言更加易于重用),这就是Language-Oriented Programming。

APIs for Describing UI Layout Specification vs. Object Interfaces of Java/Python Language
界面布局语言所提供的接口不是Java/Python语言层面上的对象接口,也不是使用基于Java/Python的语法来使用这些接口构建复杂的界面。相反,我们提供了一个面向界面设计规格描述的接口,接口的语义、规则以及命名完全和界面设计中的规则、概念相符,这样就可以直接使用代码来清晰、直接地表达出界面设计中的布局概念。

Layout styles defined by UI layout language can be combined to higher-order layout styles.
使用界面布局语言可以非常方便地定义出一些常见的布局样式,还可以把这些样式组合成更为复杂的一些高阶布局样式,并且这种组合是没有任何限制的。这些布局样式的定义描述方式和界面设计者头脑中的所使用的一些布局词汇和规则是贴近的。

UI designers can write programs to design UI in UI layout language. No boring, enjoy it!
界面设计者可以直接使用界面布局语言进行界面制作,可以直接针对布局进行编程,所写出来的界面代码就是布局规格说明。界面设计者完全可以摆脱那些呆板、机械又难以定制和扩展的布局管理器,可以轻松的把头脑中的布局创意直接描述出来,逐步形成自己的布局样式库,充分享受这种创造性的工作所带来的乐趣。

10.2 A Sequence of Layers of Language

python summary layers of language

  • Python界面开发语言:

    • 位于最底层。
    • Button, Entry …
    • create_window, cmp.master
  • 界面布局位置描述语言

    • 基于Python界面开发语言,构建出界面布局位置描述语言。
    • Primitive Components: Empty, ButtonCmp, EntryCmp …
    • Combinator Components: beside, above
    • Rectangle:AT
    • Container:IN
  • 界面布局样式描述语言

    • 基于界面布局位置描述语言,构建出用来定义和表达各种布局样式的界面布局样式描述语言。
    • hcenter, vcenter, center, hseq, vseq, block, blockm …

在界面布局语言的设计上,我们没有采用定制的面向对象的设计,而是由一组处于不同层次的语言组成,每个层次都是通过对该层的基本原子进行组合构造而来,每个层次所构造出来的实体,则可以作为上一层语言的基本原子使用。这样,我们就在通用的Java/Python语言之上,逐步构建出了一种专用于表达界面布局的语言。比起传统的对象设计,这种方法具有更高的抽象层次和通用性。

  • 程序的健壮性
    界面布局语言是分层的,这种设计非常有助于构建健壮的程序。这里健壮的含意是指:问题领域中的一个小的更改,所导致的程序更改也应当是相应地小的,呈线性是最好的。

比如,我们在构建迷你计算器时,希望所有数字以及运算符按钮都在横向和纵向留一些空白,这个问题领域中的一个小的更改,所对应的程序更改就是把block更改为blockm而已。

  • 程序的隔离性
    此外,由于分层的存在,我们可以自由地修改不同层次的表达细节而对其他层次不会造成任何影响。也就是说,每一层提供了用于表达系统特征的不同词汇以及不同的更改方式和能力。

10.3 Model-View-Presenter

在前面讲述的界面布局语言中仅仅涉及了界面布局元素的显示样式方面的内容,但是一个完整的界面是需要和后端的应用逻辑交互的,因此还需要一个粘合界面显示和应用模型的层次。

之所以在这里没有提这项内容主要是为了避免陷入其实现的琐碎细节中,从而可以集中介绍界面布局语言本身。

为了能够对界面布局元素进行编程控制,我们让每个布局元素都有一个“拥有者”。和布局元素在物理上的包含关系不同,“拥有者”是编程语意上的。也就是说,对布局元素在编程意义上的所有控制操作都在其“拥有者”中完成,这种思路完全隔离了显示和控制,其实就是MVP模式的一种实现。

比如,我们可以这样描述一个Button:

button('button1').ownby(btn_controller)

关于Button的所有事件处理和操控都在btn_controller中完成。

关于这项内容,就不做具体介绍了,感兴趣的同学可以自己试试。

10.5 What is Design?

设计就是把问题变成可计算的。

design computable

10.6 What is Good Design?

设计出的计算模型所提供的语义和问题领域的根本需求是否匹配,匹配就是好的设计,不匹配就是不好的设计。

good design

10.7 How to do Design?

how to do design

  • 对问题领域进行深入分析,发现问题领域的核心需求;
  • 通过核心需求驱动出计算模型和语义;
  • 再围绕这个计算模型提供一套语言,给外面的人使用这个计算模型提供一个接口,这个接口可以是API、可以是数据表达、也可以是语言;
  • 最终要实现这个计算模型,实现的方法有解释器和编译器两种。

这就是DDD!这才是DDD!
这就是DSL!这才是DSL!

10.8 What is Programming?

“编程”不过是在某个计算模型上用某种语言去表达计算。 用DSL编程不过是在问题领域的计算模型上用DSL来表达计算。
计算模型是相应领域中的“通用机器”。 问题领域的计算模型是问题领域的通用机器。
编程语言不过是描述计算机器的一种方法。 DSL描述的是DSL语言的计算机器。
程序是对特定机器的描述,这个特定机器可以被通用机器仿真。 用DSL程序实现了问题领域中的某个功能,这个DSL程序就是对这个功能(特定机器)的描述。

10.9 Design & Programming

design and programming

  • 从问题领域导出核心需求,得到领域的计算模型(通用计算机器),在上面可以包装一个DSL语言或者数据或者API,基于这些开发程序和应用。
  • 领域的计算模型和通用语言的计算模型之间存在鸿沟,可以用解释器或者编译器来填补。
  • 解释器和编译器听起来很复杂,其实思想很简单,而且我们没有必要实现一个工业级别的、非常全面的解释器和编译器,只要借鉴这个思想实现我们的计算模型就够了。

Common Languages(Java/C) UML对应Java/C/Python通用语言。
Common Languages(Java/C) UM对应JavaC/Python通用语言提供的计算模型(通用计算机器)。

10.10 How to improve Design Capability?

提升设计能力的根本在于提升计算模型构造和语义定义能力。

Thinking, thinking, thinking …
Practice, practice and practice …
No shortcuts.

11 Homework

  1. 【必选】实现“全局参数配置”功能的外观,如下图所示:

python impl layout global param

  1. 【可选】用函数式编程方式实现Layout。

12 My Homework

12.1 Global Parameters

代码路径:globalparam.oo

layout_test.py

    def test_globalparam(self):
        params = vseq([self._param('Parameter 1', entry()),
                       self._param('Parameter 2', entry()),
                       self._param('Parameter 3', entry())])
        btns = beside(empty(), center(beside(button('Set'), beside(empty(), button('Close'), 0.1), 0.5), 0.06, 0.2), 0.2)
        above(params, btns, 0.8).AT(0, 0, 600, 400).IN(self.container)

    def _param(self, labeltext, cmp):
        return center(beside(LabelCmp(labeltext), beside(empty(), cmp, 0.1), 0.3), 0.05, 0.3)

    def tests(self):
        # Invoking test cases here
        # self.test_component()
        # self.test_beside()
        # self.test_above()
        # self.test_beside_above()
        # self.test_empty()
        # self.test_hcenter()
        # self.test_vcenter()
        # self.test_center()
        # self.test_hseq()
        # self.test_vseq()
        # self.test_block()
        # self.test_blockm()
        # self.test_minicalc()
        # self.test_minicalc_margin()
        self.test_globalparam()

layout.py

from Tkinter import Button, Entry, Label

class LabelCmp(BaseCmp):
    def __init__(self, text):
        self.cmp = Label(text=text)

运行结果如下:
python impl global param

12.2 Functional

12.2.1 About Test

用于测试布局效果的测试框架代码详见layout_test(framework_functional).py。拷贝到开发环境中时,注意修改文件名为layout_test.py。

代码路径:original.functional

layout_test.py

from Tkinter import Tk, Canvas

# Definiting Test cases here


def tests(container):
    # Invoking test cases here
    pass

def main():
    root = Tk()
    root.title('Layout Test')

    container = Canvas(root, width=600, height=400)
    container.create_rectangle(0, 0, 600, 400, fill='light grey')
    container.pack()

    tests(container)

    root.mainloop()

if __name__ == '__main__':
    main()

12.2.2 Language of Geometric Positions

12.2.2.1 Component

layout_test.py

from layout import *

# Definiting Test cases here
def test_component(container):
    button('Button')(rectangle(0, 0, 200, 60))(container)

def tests(container):
    # Invoking test cases here
    test_component(container)

layout.py

from Tkinter import Button

def rectangle(left, top, width, height):
    return {'left':left, 'top':top, 'width':width, 'height':height}

def button(text):
    def AT(_rectangle):
        def IN(_container):
            cmp = Button(_container, text=text)
            _container.create_window(int(_rectangle['left'] + _rectangle['width'] * 0.5),
                                     int(_rectangle['top'] + _rectangle['height'] * 0.5),
                                     width=_rectangle['width'],
                                     height=_rectangle['height'],
                                     window=cmp)
        return IN
    return AT

12.2.2.2 beside

先实现entry。

layout_test.py

def test_beside(container):
    beside(entry(), button('Btn'), 0.8)(rectangle(0, 0, 300, 60))(container)

def tests(container):
    # Invoking test cases here
    # test_component(container)
    test_beside(container)

layout.py

from Tkinter import Button, Entry

def entry():
    def AT(_rectangle):
        def IN(_container):
            cmp = Entry(_container)
            _container.create_window(int(_rectangle['left'] + _rectangle['width'] * 0.5),
                                     int(_rectangle['top'] + _rectangle['height'] * 0.5),
                                     width=_rectangle['width'],
                                     height=_rectangle['height'],
                                     window=cmp)
        return IN
    return AT

再来实现beside。

我们用TDD实现beside的业务逻辑,即左右两个Component各自对应的布局空间Rectangle是否正确。既然是测试环境,肯定不用真实的控件,componentstub函数用来打桩,简单保存两个Component的rectangle参数和container引用。另外,为了更好地验证逻辑,rectangle左上角的坐标没有从(0, 0)开始。

test_all.py

import unittest

from tests.test_layout import TestLayout

if '__name__' == '__main__':
    unittest.main()

tests/test_layout.py

import unittest

from layout import *

from Tkinter import Tk, Canvas

TOLERANCE = 1

class TestLayout(unittest.TestCase):
    def test_beside(self):
        rectangles = []
        containers = []

        def componentstub():
            def AT(_rectangle):
                def IN(_container):
                    rectangles.append(_rectangle)
                    containers.append(_container)
                return IN
            return AT

        leftcmp = componentstub()
        rightcmp = componentstub()
        container = Canvas(Tk())

        beside(leftcmp, rightcmp, 0.8)(rectangle(20, 10, 300, 60))(container)

        self._check_rectangle(rectangle(20, 10, 240, 60), rectangles[0])
        self._check_rectangle(rectangle(260, 10, 60, 60), rectangles[1])

        self.assertTrue(container == containers[0])
        self.assertTrue(container == containers[1])

    def _check_rectangle(self, expected, actual):
        self.assertEquals(len(expected), len(actual))
        for (k, v) in actual.items():
            self.assertTrue(k in expected.keys())
            self.assertAlmostEquals(expected[k], v, delta=TOLERANCE)

layout.py

def beside(leftcmp, rightcmp, ratio):
    def AT(_rectangle):
        def IN(_container):
            leftcmp(rectangle(_rectangle['left'], _rectangle['top'],
                              int(_rectangle['width'] * ratio), _rectangle['height']))(_container)
            rightcmp(rectangle(_rectangle['left'] + int(_rectangle['width'] * ratio), _rectangle['top'],
                               int(_rectangle['width'] * (1 - ratio)), _rectangle['height']))(_container)
        return IN
    return AT

button()和entry()这两个函数存在重复代码,将重复代码抽取到函数中。

layout.py

def _AT_IN(cmp):
    def AT(_rectangle):
        def IN(_container):
            cmp.master = _container
            _container.create_window(int(_rectangle['left'] + _rectangle['width'] * 0.5),
                                     int(_rectangle['top'] + _rectangle['height'] * 0.5),
                                     width=_rectangle['width'],
                                     height=_rectangle['height'],
                                     window=cmp)
        return IN
    return AT

def button(text):
    return _AT_IN(Button(text=text))

def entry():
    return _AT_IN(Entry())

12.2.2.3 above

我们用TDD实现above的业务逻辑,即上下两个Component各自对应的布局空间Rectangle是否正确。既然是测试环境,肯定不用真实的控件,componentstub函数用来打桩,简单保存两个Component的rectangle参数和container引用。另外,为了更好地验证逻辑,rectangle左上角的坐标没有从(0, 0)开始。

tests/test_layout.py

    def test_above(self):
        rectangles = []
        containers = []

        def componentstub():
            def AT(_rectangle):
                def IN(_container):
                    rectangles.append(_rectangle)
                    containers.append(_container)
                return IN
            return AT

        upcmp = componentstub()
        downcmp = componentstub()
        container = Canvas(Tk())

        above(upcmp, downcmp, 0.5)(rectangle(20, 10, 300, 60))(container)

        self._check_rectangle(rectangle(20, 10, 300, 30), rectangles[0])
        self._check_rectangle(rectangle(20, 40, 300, 30), rectangles[1])

        self.assertTrue(container == containers[0])
        self.assertTrue(container == containers[1])

layout.py

def above(upcmp, downcmp, ratio):
    def AT(_rectangle):
        def IN(_container):
            upcmp(rectangle(_rectangle['left'], _rectangle['top'],
                            _rectangle['width'], int(_rectangle['height'] * ratio)))(_container)
            downcmp(rectangle(_rectangle['left'], _rectangle['top'] + int(_rectangle['height'] * ratio),
                              _rectangle['width'], int(_rectangle['height'] * (1 - ratio))))(_container)
        return IN
    return AT

layout_test.py

def test_above(container):
    above(entry(), button('Button'), 0.5)(rectangle(0, 0, 300, 60))(container)

def tests(container):
    # Invoking test cases here
    # test_component(container)
    # test_beside(container)
    test_above(container)

12.2.2.4 Refactoring

观察test_layout.py,test_beside()和test_above()两个测试方法有重复代码,比如componentstub()方法和构造container的代码。

test_layout.py

    def setUp(self):
        self.rectangles = []
        self.containers = []
        self.container = Canvas(Tk())

    def test_beside(self):
        leftcmp = self._componentstub()
        rightcmp = self._componentstub()

        beside(leftcmp, rightcmp, 0.8)(rectangle(20, 10, 300, 60))(self.container)

        self._check_rectangle(rectangle(20, 10, 240, 60), self.rectangles[0])
        self._check_rectangle(rectangle(260, 10, 60, 60), self.rectangles[1])

        self.assertTrue(self.container == self.containers[0])
        self.assertTrue(self.container == self.containers[1])

    def test_above(self):
        upcmp = self._componentstub()
        downcmp = self._componentstub()

        above(upcmp, downcmp, 0.5)(rectangle(20, 10, 300, 60))(self.container)

        self._check_rectangle(rectangle(20, 10, 300, 30), self.rectangles[0])
        self._check_rectangle(rectangle(20, 40, 300, 30), self.rectangles[1])

        self.assertTrue(self.container == self.containers[0])
        self.assertTrue(self.container == self.containers[1])

    def _componentstub(self):
        def AT(_rectangle):
            def IN(_container):
                self.rectangles.append(_rectangle)
                self.containers.append(_container)
            return IN
        return AT

12.2.2.5 The components' world is Closed

layout_test.py

def test_beside_above(container):
    above(beside(entry(), button('Btn1'), 0.8), button('Btn2'), 0.5)(rectangle(0, 0, 300, 60))(container)

def tests(container):
    # Invoking test cases here
    # test_component(container)
    # test_beside(container)
    # test_above(container)
    test_beside_above(container)

12.2.2.6 Empty

layout_test.py

def test_empty(container):
    beside(empty(), button('Button'), 0.5)(rectangle(0, 0, 300, 60))(container)

def tests(container):
    # Invoking test cases here
    # test_component(container)
    # test_beside(container)
    # test_above(container)
    # test_beside_above(container)
    test_empty(container)

layout.py

def empty():
    def AT(_rectangle):
        def IN(_container):
            pass
        return IN
    return AT

12.2.3 Language of Layout Style

12.2.3.1 Horizontal Center

layout_test.py

def test_hcenter(container):
    hcenter(button('Button'), 0.1)(rectangle(0, 0, 600, 400))(container)

def tests(container):
    # Invoking test cases here
    # test_component(container)
    # test_beside(container)
    # test_above(container)
    # test_beside_above(container)
    # test_empty(container)
    test_hcenter(container)

layout.py

def hcenter(cmp, ratio):
    return beside(empty(), beside(cmp, empty(), (1.0 - 2.0 * ratio) / (1.0 - ratio)), ratio)

12.2.3.2 Vertical Center

layout_test.py

def test_vcenter(container):
    vcenter(button('Button'), 0.1)(rectangle(0, 0, 600, 400))(container)

def tests(container):
    # Invoking test cases here
    # test_component(container)
    # test_beside(container)
    # test_above(container)
    # test_beside_above(container)
    # test_empty(container)
    # test_hcenter(container)
    test_vcenter(container)

layout.py

def vcenter(cmp, ratio):
    return above(empty(), above(cmp, empty(), (1.0 - 2.0 * ratio) / (1.0 - ratio)), ratio)

可以看到hcenter()和vcenter()之间除了beside、above不一样,其他代码都一样,用函数式编程范式重构去重。

layout.py

def hcenter(cmp, ratio):
    return _center(beside, cmp, ratio)

def vcenter(cmp, ratio):
    return _center(above, cmp, ratio)

def _center(poslayout, cmp, ratio):
    return poslayout(empty(), poslayout(cmp, empty(), (1.0 - 2.0 * ratio) / (1.0 - ratio)), ratio)

12.2.3.3 Center

layout_test.py

def test_center(container):
    center(button('Center'), 0.2, 0.1)(rectangle(0, 0, 600, 400))(container)

def tests(container):
    # Invoking test cases here
    # test_component(container)
    # test_beside(container)
    # test_above(container)
    # test_beside_above(container)
    # test_empty(container)
    # test_hcenter(container)
    # test_vcenter(container)
    test_center(container)

layout.py

def center(cmp, hratio, vratio):
    return vcenter(hcenter(cmp, hratio), vratio)

12.2.3.4 Horizontal Sequence

layout_test.py

def test_hseq(container):
    hseq([button('1'), button('2'), button('3')])(rectangle(0, 0, 300, 60))(container)

def tests(container):
    # Invoking test cases here
    # test_component(container)
    # test_beside(container)
    # test_above(container)
    # test_beside_above(container)
    # test_empty(container)
    # test_hcenter(container)
    # test_vcenter(container)
    # test_center(container)
    test_hseq(container)

layout.py

def hseq(cmps):
    if len(cmps) == 1:
        return cmps[0]

    return beside(cmps[0], hseq(cmps[1:]), 1.0 / len(cmps))

12.2.3.5 Vertical Sequence

layout_test.py

def test_vseq(container):
    vseq([button('1'), button('2'), button('3')])(rectangle(0, 0, 150, 200))(container)

def tests(container):
    # Invoking test cases here
    # test_component(container)
    # test_beside(container)
    # test_above(container)
    # test_beside_above(container)
    # test_empty(container)
    # test_hcenter(container)
    # test_vcenter(container)
    # test_center(container)
    # test_hseq(container)
    test_vseq(container)

layout.py

def vseq(cmps):
    if len(cmps) == 1:
        return cmps[0]

    return above(cmps[0], vseq(cmps[1:]), 1.0 / len(cmps))

可以看到hseq()和vseq()之间除了beside、above不一样,其他代码都一样,用函数式编程范式重构去重。

layout.py

def hseq(cmps):
    return _seq(beside, cmps)

def vseq(cmps):
    return _seq(above, cmps)

def _seq(poslayout, cmps):
    if len(cmps) == 1:
        return cmps[0]

    return poslayout(cmps[0], _seq(poslayout, cmps[1:]), 1.0 / len(cmps))

12.2.3.6 Block

layout_test.py

def test_block(container):
    cmps = [button(str(i)) for i in range(1, 12)]
    block(cmps, 4, 3)(rectangle(0, 0, 600, 400))(container)

def tests(container):
    # Invoking test cases here
    # test_component(container)
    # test_beside(container)
    # test_above(container)
    # test_beside_above(container)
    # test_empty(container)
    # test_hcenter(container)
    # test_vcenter(container)
    # test_center(container)
    # test_hseq(container)
    # test_vseq(container)
    test_block(container)

layout.py

import copy

def block(cmps, rownum, colnum):
    paddedcmps = _padding(cmps, rownum * colnum)
    return vseq([hseq(paddedcmps[idx:idx + colnum]) for idx in range(0, rownum * colnum, colnum)])

def _padding(cmps, total):
    copiedcmps = copy.copy(cmps)
    return copiedcmps + [empty() for idx in range(0, total - len(cmps))]

12.2.3.7 Block With Margin

layout_test.py

def test_blockm(container):
    cmps = [button(str(i)) for i in range(1, 12)]
    blockm(cmps, 4, 3, 0.1, 0.1)(rectangle(0, 0, 600, 400))(container)

def tests(container):
    # Invoking test cases here
    # test_component(container)
    # test_beside(container)
    # test_above(container)
    # test_beside_above(container)
    # test_empty(container)
    # test_hcenter(container)
    # test_vcenter(container)
    # test_center(container)
    # test_hseq(container)
    # test_vseq(container)
    # test_block(container)
    test_blockm(container)

layout.py

def blockm(cmps, rownum, colnum, hratio, vratio):
    return block([center(cmp, hratio, vratio) for cmp in cmps], rownum, colnum)

12.2.4 Mini-calculator

layout_test.py

def test_minicalc(container):
    texts = ['0', '1', '2', '+',
             '3', '4', '5', '-',
             '6', '7', '8', '*',
             '9', '=', '%', '/']
    btns = [button(text) for text in texts]

    above(above(entry(),
                beside(button('Backspace'), button('C'), 0.5), 0.5),
          block(btns, 4, 4), 0.3)(rectangle(0, 0, 600, 400))(container)

def tests(container):
    # Invoking test cases here
    # test_component(container)
    # test_beside(container)
    # test_above(container)
    # test_beside_above(container)
    # test_empty(container)
    # test_hcenter(container)
    # test_vcenter(container)
    # test_center(container)
    # test_hseq(container)
    # test_vseq(container)
    # test_block(container)
    # test_blockm(container)
    test_minicalc(container)

如果我们现在希望将所有数字以及操作按钮按照横向和纵向各2%进行留白,我们所要做的仅仅是一行的改动,就是把block()换成blockm()。

layout_test.py

def test_minicalc_margin(container):
    texts = ['0', '1', '2', '+',
             '3', '4', '5', '-',
             '6', '7', '8', '*',
             '9', '=', '%', '/']
    btns = [button(text) for text in texts]

    above(above(entry(),
                beside(button('Backspace'), button('C'), 0.5), 0.5),
          blockm(btns, 4, 4, 0.02, 0.02), 0.3)(rectangle(0, 0, 600, 400))(container)

def tests(container):
    # Invoking test cases here
    # test_component(container)
    # test_beside(container)
    # test_above(container)
    # test_beside_above(container)
    # test_empty(container)
    # test_hcenter(container)
    # test_vcenter(container)
    # test_center(container)
    # test_hseq(container)
    # test_vseq(container)
    # test_block(container)
    # test_blockm(container)
    # test_minicalc(container)
    test_minicalc_margin(container)

12.2.5 Global Parameters

代码路径:globalparam.functional

layout_test.py

def test_globalparam(container):
    params = vseq([_param('Parameter 1', entry()),
                   _param('Parameter 2', entry()),
                   _param('Parameter 3', entry())])
    btns = beside(empty(), center(beside(button('Set'), beside(empty(), button('Close'), 0.1), 0.5), 0.06, 0.2), 0.2)
    above(params, btns, 0.8)(rectangle(0, 0, 600, 400))(container)

def _param(labeltext, cmp):
    return center(beside(label(labeltext), beside(empty(), cmp, 0.1), 0.3), 0.05, 0.3)

def tests(container):
    # Invoking test cases here
    # test_component(container)
    # test_beside(container)
    # test_above(container)
    # test_beside_above(container)
    # test_empty(container)
    # test_hcenter(container)
    # test_vcenter(container)
    # test_center(container)
    # test_hseq(container)
    # test_vseq(container)
    # test_block(container)
    # test_blockm(container)
    # test_minicalc(container)
    # test_minicalc_margin(container)
    test_globalparam(container)

layout.py

from Tkinter import Button, Entry, Label

def label(text):
    return _AT_IN(Label(text=text))
⚠️ **GitHub.com Fallback** ⚠️