你会如何设计一个非常“Pythonic”的 UI 框架?
-
09-06-2019 - |
题
我一直在玩Ruby库“鞋子”。基本上,您可以通过以下方式编写 GUI 应用程序:
Shoes.app do
t = para "Not clicked!"
button "The Label" do
alert "You clicked the button!" # when clicked, make an alert
t.replace "Clicked!" # ..and replace the label's text
end
end
这让我思考——如何在 Python 中设计一个同样好用的 GUI 框架?一种基本上不具有 C* 库包装器的通常联系(对于 GTK、Tk、wx、QT 等)
鞋子从网络开发中获取了东西(比如 #f0c2f0
样式颜色表示法、CSS 布局技术等 :margin => 10
),以及来自 ruby(以合理的方式广泛使用块)
Python 缺乏“rubyish 块”使得(隐喻性的)直接移植变得不可能:
def Shoeless(Shoes.app):
self.t = para("Not clicked!")
def on_click_func(self):
alert("You clicked the button!")
self.t.replace("clicked!")
b = button("The label", click=self.on_click_func)
没有那么干净,也不会 几乎 很灵活,我什至不确定它是否可以实施。
使用装饰器似乎是一种将代码块映射到特定操作的有趣方法:
class BaseControl:
def __init__(self):
self.func = None
def clicked(self, func):
self.func = func
def __call__(self):
if self.func is not None:
self.func()
class Button(BaseControl):
pass
class Label(BaseControl):
pass
# The actual applications code (that the end-user would write)
class MyApp:
ok = Button()
la = Label()
@ok.clicked
def clickeryHappened():
print "OK Clicked!"
if __name__ == '__main__':
a = MyApp()
a.ok() # trigger the clicked action
基本上,装饰器函数存储函数,然后当发生操作(例如单击)时,将执行适当的函数。
各种东西的范围(例如 la
上面示例中的标签)可能相当复杂,但它似乎可以以相当简洁的方式实现。
解决方案
你实际上可以做到这一点,但它需要使用元类,它们是 深的 魔法(有龙)。如果您想了解元类,这里有一系列 来自 IBM 的文章 它设法在不融化你的大脑的情况下介绍这些想法。
来自像 SQLObject 这样的 ORM 的源代码也可能有所帮助,因为它使用相同类型的声明性语法。
其他提示
## All you need is this class:
class MainWindow(Window):
my_button = Button('Click Me')
my_paragraph = Text('This is the text you wish to place')
my_alert = AlertBox('What what what!!!')
@my_button.clicked
def my_button_clicked(self, button, event):
self.my_paragraph.text.append('And now you clicked on it, the button that is.')
@my_paragraph.text.changed
def my_paragraph_text_changed(self, text, event):
self.button.text = 'No more clicks!'
@my_button.text.changed
def my_button_text_changed(self, text, event):
self.my_alert.show()
## The Style class is automatically gnerated by the framework
## but you can override it by defining it in the class:
##
## class MainWindow(Window):
## class Style:
## my_blah = {'style-info': 'value'}
##
## or like you see below:
class Style:
my_button = {
'background-color': '#ccc',
'font-size': '14px'}
my_paragraph = {
'background-color': '#fff',
'color': '#000',
'font-size': '14px',
'border': '1px solid black',
'border-radius': '3px'}
MainWindow.Style = Style
## The layout class is automatically generated
## by the framework but you can override it by defining it
## in the class, same as the Style class above, or by
## defining it like this:
class MainLayout(Layout):
def __init__(self, style):
# It takes the custom or automatically generated style class upon instantiation
style.window.pack(HBox().pack(style.my_paragraph, style.my_button))
MainWindow.Layout = MainLayout
if __name__ == '__main__':
run(App(main=MainWindow))
只要掌握一些元类Python魔法知识,在Python中就可以相对容易地做到这一点。我有。以及 PyGTK 的知识。我也有。有想法吗?
我对 IBM 的 David Mertz 关于元类的文章一直不满意,所以我最近写了自己的文章 元类文章. 。享受。
这是非常人为的,根本不是Python式的,但这是我使用新的“with”语句进行半字面翻译的尝试。
with Shoes():
t = Para("Not clicked!")
with Button("The Label"):
Alert("You clicked the button!")
t.replace("Clicked!")
最难的部分是处理这样一个事实:Python 不会为我们提供包含多个语句的匿名函数。为了解决这个问题,我们可以创建一个命令列表并运行这些命令......
无论如何,这是我运行它的后端代码:
context = None
class Nestable(object):
def __init__(self,caption=None):
self.caption = caption
self.things = []
global context
if context:
context.add(self)
def __enter__(self):
global context
self.parent = context
context = self
def __exit__(self, type, value, traceback):
global context
context = self.parent
def add(self,thing):
self.things.append(thing)
print "Adding a %s to %s" % (thing,self)
def __str__(self):
return "%s(%s)" % (self.__class__.__name__, self.caption)
class Shoes(Nestable):
pass
class Button(Nestable):
pass
class Alert(Nestable):
pass
class Para(Nestable):
def replace(self,caption):
Command(self,"replace",caption)
class Command(Nestable):
def __init__(self, target, command, caption):
self.command = command
self.target = target
Nestable.__init__(self,caption)
def __str__(self):
return "Command(%s text of %s with \"%s\")" % (self.command, self.target, self.caption)
def execute(self):
self.target.caption = self.caption
通过一些元类魔法来保持顺序,我可以进行以下工作。我不确定它有多Pythonic,但创建简单的东西很有趣。
class w(Wndw):
title='Hello World'
class txt(Txt): # either a new class
text='Insert name here'
lbl=Lbl(text='Hello') # or an instance
class greet(Bbt):
text='Greet'
def click(self): #on_click method
self.frame.lbl.text='Hello %s.'%self.frame.txt.text
app=w()
据我所知,这样做的唯一尝试是 汉斯·诺瓦克的蜡 (不幸的是已经死了)。
最接近 rubyish 块的是 pep343 的 with 语句:
如果你使用 PyGTK 和 林间空地 和 这个林间空地包装纸, ,那么 PyGTK 实际上就变得有点 Pythonic 了。至少有一点。
基本上,您在 Glade 中创建 GUI 布局。您还可以在空地中指定事件回调。然后你为你的窗口编写一个类,如下所示:
class MyWindow(GladeWrapper):
GladeWrapper.__init__(self, "my_glade_file.xml", "mainWindow")
self.GtkWindow.show()
def button_click_event (self, *args):
self.button1.set_label("CLICKED")
在这里,我假设我有一个 GTK 按钮,名为 按钮1 我指定了 按钮点击事件 作为 点击了 打回来。林间空地包装器在事件映射方面花费了大量精力。
如果我要设计一个 Pythonic GUI 库,我会支持类似的东西,以帮助快速开发。唯一的区别是我会确保小部件也有一个更Pythonic 的界面。当前的 PyGTK 类对我来说似乎非常 C,除了我使用 foo.bar(...) 而不是 bar(foo, ...) 尽管我不确定我到底会做什么不同。可能允许 Django 模型风格的声明方式在代码中指定小部件和事件,并允许您通过迭代器访问数据(在有意义的地方,例如小部件列表也许),尽管我还没有真正考虑过它。
也许不像 Ruby 版本那么流畅,但是像这样的怎么样:
from Boots import App, Para, Button, alert
def Shoeless(App):
t = Para(text = 'Not Clicked')
b = Button(label = 'The label')
def on_b_clicked(self):
alert('You clicked the button!')
self.t.text = 'Clicked!'
就像贾斯汀说的, ,要实现这一点,您需要在类上使用自定义元类 App
, ,以及一系列属性 Para
和 Button
. 。这实际上并不会太难。
接下来你遇到的问题是:你如何跟踪 命令 那些东西出现在类定义中?在 Python 2.x 中,无法知道是否 t
应该在上面 b
或相反,因为您收到的类定义的内容为 python dict
.
然而,在Python 3.0中 元类正在改变 以几个(次要的)方式。其中之一是 __prepare__
方法,它允许您提供自己的自定义类似字典的对象来代替使用 - 这意味着您将能够跟踪定义项目的顺序,并将它们相应地放置在窗口中。
这可能过于简单化了,我认为尝试以这种方式创建通用 ui 库不是一个好主意。另一方面,您可以使用这种方法(元类和朋友)来简化现有 ui 库的某些用户界面类的定义,并取决于应用程序,这实际上可以节省您大量的时间和代码行。
我有着同样的问题。我想为 Python 的任何 GUI 工具包创建一个包装器,该工具包易于使用,并且受到 Shoes 的启发,但需要采用 OOP 方法(针对 ruby 块)。
更多信息请参见: http://wiki.alcidesfonseca.com/blog/python-universal-gui-revisited
欢迎任何人加入该项目。
如果你真的想编写 UI 代码,你可以尝试使用类似于 django 的 ORM 的东西;像这样获得一个简单的帮助浏览器:
class MyWindow(Window):
class VBox:
entry = Entry()
bigtext = TextView()
def on_entry_accepted(text):
bigtext.value = eval(text).__doc__
这个想法是将一些容器(如窗口)解释为简单的类,将一些容器(如表、v/hbox)解释为对象名称,并将简单的小部件解释为对象。
我认为不必命名窗口内的所有容器,因此一些快捷方式(例如通过名称将旧式类识别为小部件)将是可取的。
关于元素的顺序:在上面的 MyWindow 中,您不必跟踪这一点(窗口在概念上是一个单槽容器)。在其他容器中,您可以尝试跟踪顺序,假设每个小部件构造函数都可以访问某些全局小部件列表。这就是 django 中的实现方式(据我所知)。
这里很少有黑客攻击,那里很少有调整......还有一些事情需要考虑,但我相信这是可能的......并且可用,只要您不构建复杂的 UI。
不过我对 PyGTK+Glade 非常满意。UI 对我来说只是一种数据,它应该被视为数据。有太多的参数需要调整(例如不同位置的间距),最好使用 GUI 工具来管理。因此,我在 Glade 中构建 UI,另存为 xml 并使用 gtk.glade.XML() 进行解析。
就我个人而言,我会尝试实施 jQuery 就像 GUI 框架中的 API 一样。
class MyWindow(Window):
contents = (
para('Hello World!'),
button('Click Me', id='ok'),
para('Epilog'),
)
def __init__(self):
self['#ok'].click(self.message)
self['para'].hover(self.blend_in, self.blend_out)
def message(self):
print 'You clicked!'
def blend_in(self, object):
object.background = '#333333'
def blend_out(self, object):
object.background = 'WindowBackground'
这是一种使用基于类的元编程而不是继承来处理 GUI 定义的方法,该方法略有不同。
这是受到 Django/SQLAlchemy 的启发,因为它很大程度上基于元编程,并将 GUI 代码与“代码”分开。我还认为它应该像 Java 一样大量使用布局管理器,因为当你删除代码时,没有人愿意不断调整像素对齐。我还认为,如果我们能够拥有类似 CSS 的属性,那就太酷了。
这是一个粗略的头脑风暴示例,它将显示一个顶部有标签的列,然后是一个文本框,然后是一个单击底部显示消息的按钮。
from happygui.controls import * MAIN_WINDOW = Window(width="500px", height="350px", my_layout=ColumnLayout(padding="10px", my_label=Label(text="What's your name kiddo?", bold=True, align="center"), my_edit=EditBox(placeholder=""), my_btn=Button(text="CLICK ME!", on_click=Handler('module.file.btn_clicked')), ), ) MAIN_WINDOW.show() def btn_clicked(sender): # could easily be in a handlers.py file name = MAIN_WINDOW.my_layout.my_edit.text # same thing: name = sender.parent.my_edit.text # best practice, immune to structure change: MAIN_WINDOW.find('my_edit').text MessageBox("Your name is '%s'" % ()).show(modal=True)
值得注意的一件很酷的事情是,您可以通过以下方式引用 my_edit 的输入: MAIN_WINDOW.my_layout.my_edit.text
. 。在窗口的声明中,我认为能够在函数 kwargs 中任意命名控件非常重要。
这是仅使用绝对定位的同一个应用程序(控件将出现在不同的位置,因为我们没有使用花哨的布局管理器):
from happygui.controls import * MAIN_WINDOW = Window(width="500px", height="350px", my_label=Label(text="What's your name kiddo?", bold=True, align="center", x="10px", y="10px", width="300px", height="100px"), my_edit=EditBox(placeholder="", x="10px", y="110px", width="300px", height="100px"), my_btn=Button(text="CLICK ME!", on_click=Handler('module.file.btn_clicked'), x="10px", y="210px", width="300px", height="100px"), ) MAIN_WINDOW.show() def btn_clicked(sender): # could easily be in a handlers.py file name = MAIN_WINDOW.my_edit.text # same thing: name = sender.parent.my_edit.text # best practice, immune to structure change: MAIN_WINDOW.find('my_edit').text MessageBox("Your name is '%s'" % ()).show(modal=True)
我还不完全确定这是否是一个超级好的方法,但我绝对认为它走在正确的道路上。我没有时间进一步探索这个想法,但如果有人将此作为一个项目,我会喜欢他们。