前言:本节介绍Tk的基本设计思想

本节参考官方介绍文档,所以英文夹杂,这个大家谅解,主要是方便参考:

此外,我们主要以Python语言为基础


1 Widgets(组件)

Widgets are all the things that you see onscreen. In our example, we had a button, an entry, a few labels, and a frame. Others are things like checkboxes, tree views, scrollbars, text areas, and so on. Widgets are often referred to as "controls." You'll also sometimes see them referred to as "windows," particularly in Tk's documentation. This is a holdover from its X11 roots (under that terminology, both your toplevel application window and things like a button would be called windows).

Here is an example showing some of Tk's widgets, which we'll cover individually shortly.

python 3d 框架 python 框架设计_sed

组件是GUI设计通常运用的一个元素。


1.1 Widget Hierarchy(组件的继承关系)

 组件的概念,首先在软件里面理解为一个类。

这个类继承于一个窗口的Frame,而Frame又来源于root一个包含container的父类。

python 3d 框架 python 框架设计_sed_02

 对于继承的深度级别,似乎没有限制:

python 3d 框架 python 框架设计_ide_03

root = Tk() # 创建一个窗口元素
content = ttk.Frame(root)
button = ttk.Button(content)

 Tcl表述

ttk::button .b
ttk::frame .f
ttk::entry .f.entry

Perl

Tkx::ttk__button(".b", -text => "hello");
Tkx::ttk__frame(".f");
Tkx::ttk__entry(".f.entry");

1.2  Widget Objects 对象:

Many widgets have operations that you can call on them, such as to disable a widget, invoke a button's command, and so on. 

组件对象具备基本的面向对象的设计思想,这比较好理解。例如disable,invoke等。不过,在官方的例子里,没给出python的实现方法。

只给出了tcl,perl的实现例子,这个大家有兴趣可以参考我的参考链接:

Tcl的表述方式

ttk::button .b
ttk::frame .f
ttk::entry .f.entry

 对应的Perl,这个表述有点复杂,而且移植性不好,所以,后面改写了这个表达式:

Tkx::i::call(".b", "invoke");
Tkx::i::call(".f.entry", "state", "disabled");

#构建一方法
Tkx::ttk__entry(".c.feet", -width => 7, -textvariable => \$feet);
Tkx::grid(".c.feet", -column => 2, -row => 1, -sticky => "we");

 Perl的表述方式的改写如下:

1 先把之前的写死的定义给到一个表达变量

my $b = Tkx::widget->new(".b");
my $e = Tkx::widget->new(".f.entry");

$b->invoke;  #然后用这个表达变量的参考去控制对象的方法
$e->state("disabled");

2 再进一步优化为子对象从父对象的继承关系

my $mw = Tkx::widget->new(".");
my $b = $mw->new_ttk__button(-text => "hello");
my $f = $mw->new_ttk__frame;
my $e = $f->new_ttk__entry;

然后,我们实现一个方法:

my $mw = Tkx::widget->new(".");
my $ft = $mw->new_ttk__entry(-width => 7, -textvariable => \$feet);
$ft->g_grid(-column => 2, -row => 1, -sticky => "we");#前缀g_,表述这是一个方法

1.3 Translation Rules(不同语言表达式的规则)

What's somewhat scary about the Tkx module is that it implements all of this on a purely syntactic level. That is, it has no clue about buttons and entries and grid. It just knows that if it sees a method "new_something" it's creating a widget using the Tcl command "something", or if you call a method "g_otherthing", that it's going to invoke a Tcl command "otherthing", and pass the widget pathname associated with the object as a first parameter (followed by any other parameters passed to the method).

上面一节,我们提到了TK在不同的语言下,不用的表达方式和逻辑。

这对TK的解释器来说是一个挑战,我们看看有哪些可能的语句

  • a single toplevel command, like grid
  • a multi-word command (called an ensemble in Tcl), e.g. wm title
  • a command in a namespace, e.g. ttk::button
  • a single toplevel command with underscores, e.g. tk_messageBox

这些不同的语言表述,在TK这里用下划线进行了改编,这样可以实现兼容:

  • a single underscore is replaced with a space, e.g. wm_title in Perl becomes wm title in Tcl
  • two underscores is replaced with the namespace qualifier "::", e.g. ttk__button becomes ttk::button
  • three underscores is replaced with a single underscore, e.g. tk___messageBox becomes tk_messageBox

Most programs of any length will probably be a bit simpler if they predominately use the object oriented form, but this is almost entirely a stylistic issue.

然后,我们在perl用面向对象的风格改写之前的例子:

use Tkx;

my $mw = Tkx::widget->new(".");
$mw->g_wm_title("Feet to Meters");
my $frm = $mw->new_ttk__frame(-padding => "3 3 12 12");
$frm->g_grid(-column => 0, -row => 0, -sticky => "nwes");
$mw->g_grid_columnconfigure(0, -weight => 1);
$mw->g_grid_rowconfigure(0, -weight => 1);

my $ef = $frm->new_ttk__entry(-width => 7, -textvariable => \$feet);
$ef->g_grid(-column => 2, -row => 1, -sticky => "we");
my $em = $frm->new_ttk__label(-textvariable => \$meters);
$em->g_grid(-column => 2, -row => 2, -sticky => "we");
my $cb = $frm->new_ttk__button(-text => "Calculate", -command => sub {calculate();});
$cb->g_grid(-column => 3, -row => 3, -sticky => "w");

$frm->new_ttk__label(-text => "feet")->g_grid(-column => 3, -row => 1, -sticky => "w");
$frm->new_ttk__label(-text => "is equivalent to")->g_grid(-column => 1, -row => 2, -sticky => "e");
$frm->new_ttk__label(-text => "meters")->g_grid(-column => 3, -row => 2, -sticky => "w");

foreach (Tkx::SplitList($frm->g_winfo_children)) {
    Tkx::grid_configure($_, -padx => 5, -pady => 5);
}
$ef->g_focus;
$mw->g_bind("<Return>", sub {calculate();});

sub calculate {
   $meters = int(0.3048*$feet*10000.0+.5)/10000.0 || '';
}

Tkx::MainLoop();

1.4 Configuration Options(窗口组件的配置属性)

理解为配置属性 

All widgets have several configuration options. These control how the widget is displayed or how it behaves.
>>> from tkinter import *
>>> from tkinter import ttk
>>> root = Tk()
create a button, passing two options:
>>> button = ttk.Button(root, text="Hello", command="buttonpressed")
>>> button.grid()
check the current value of the text option:
>>> button['text']
'Hello'
change the value of the text option:
>>> button['text'] = 'goodbye'
another way to do the same thing:
>>> button.configure(text='goodbye')
check the current value of the text option:
>>> button['text']
'goodbye'
get all information about the text option:
>>> button.configure('text')
('text', 'text', 'Text', '', 'goodbye')
get information on all options for this widget:
>>> button.configure()
{'cursor': ('cursor', 'cursor', 'Cursor', '', ''), 'style': ('style', 'style', 'Style', '', ''), 
'default': ('default', 'default', 'Default', <index object at 0x00DFFD10>, <index object at 0x00DFFD10>), 
'text': ('text', 'text', 'Text', '', 'goodbye'), 'image': ('image', 'image', 'Image', '', ''), 
'class': ('class', '', '', '', ''), 'padding': ('padding', 'padding', 'Pad', '', ''), 
'width': ('width', 'width', 'Width', '', ''), 
'state': ('state', 'state', 'State', <index object at 0x0167FA20>, <index object at 0x0167FA20>), 
'command': ('command', 'command' , 'Command', '', 'buttonpressed'), 
'textvariable': ('textvariable', 'textVariable', 'Variable', '', ''), 
'compound': ('compound', 'compound', 'Compound', <index object at 0x0167FA08>, <index object at 0x0167FA08>), 
'underline': ('underline', 'underline', 'Underline', -1, -1), 
'takefocus': ('takefocus', 'takeFocus', 'TakeFocus', '', 'ttk::takefocus')}

1.5 Widget Introspection(组件信息)

Tk 提供了获取所有窗口组件信息的方法:

def print_hierarchy(w, depth=0):
    print('  '*depth + w.winfo_class() + ' w=' + str(w.winfo_width()) + ' h=' + str(w.winfo_height()) + ' x=' + str(w.winfo_x()) + ' y=' + str(w.winfo_y()))
    for i in w.winfo_children():
        print_hierarchy(i, depth+1)
print_hierarchy(root)

winfo_class:

a class identifying the type of widget, e.g. TButton for a themed button

winfo_children:

a list of widgets that are the direct children of a widget in the hierarchy

winfo_parent:

parent of the widget in the hierarchy

winfo_toplevel:

the toplevel window containing this widget

winfo_width, winfo_height:

current width and height of the widget; not accurate until appears onscreen

winfo_reqwidth, winfo_reqheight:

the width and height the widget requests of the geometry manager (more on this shortly)

winfo_x, winfo_y:

the position of the top-left corner of the widget relative to its parent

winfo_rootx, winfo_rooty:

the position of the top-left corner of the widget relative to the entire screen

winfo_vieweable:

whether the widget is displayed or hidden (all its ancestors in the hierarchy must be viewable for it to be viewable)


 2 Geometry Management位置控制

A geometry manager's job is to figure out exactly where those widgets are going to be put. This turns out to be a complex optimization problem, and a good geometry manager relies on quite sophisticated algorithms. A good geometry manager provides the flexibility, power, and ease of use that makes programmers happy. It also makes it easy to create good looking user interface layouts without needing to jump through hoops. Tk's grid is, without a doubt, one of the absolute best. A poor geometry manager... well, all the Java programmers who have suffered through "GridBagLayout" please raise their hands.

 TK的作者解释了GM的主要作用,顺带骂了一下JAVA,比较过瘾啊

In our example, positioning each widget was accomplished by the grid command. We specified the column and row we wanted each widget to go in, how things were to be aligned within the grid, etc. Grid is an example of a geometry manager (of which there are several in Tk, grid being the most useful).

grid命令是Tk窗口组件位置的基本控制命令,这个会在未来一节里面做详细介绍。

2.1 位置控制的主要问题:

  • The widgets may have a natural size, e.g., the natural width of a label would depend on the text it displays and the font used to display it. What if the application window containing all these different widgets isn't big enough to accommodate them? The geometry manager must decide which widgets to shrink to fit, by how much, etc.
  • If the application window is bigger than the natural size of all the widgets, how is the extra space used? Is extra space placed between each widget, and if so, how is that space distributed? Is it used to make certain widgets larger than they normally want to be, such as a text entry growing to fill a wider window? Which widgets should grow?
  • If the application window is resized, how does the size and position of each widgets inside it change? Will certain areas (e.g., a text entry area) expand or shrink while other parts stay the same size, or is the area distributed differently? Do certain widgets have a minimum size that you want to avoid going below? A maximum size? Does the window itself have a minimum or maximum size?
  • How can widgets in different parts of the user interface be aligned with each other? How much space should be left between them? This is needed to present a clean layout and comply with platform-specific user interface guidelines.
  • For a complex user interface, which may have many frames nested in other frames nested in the window (etc.), how can all the above be accomplished, trading off the conflicting demands of different parts of the entire user interface?

2.2 工作原理

Tk采用的主从模式来进行位置控制。然后,通过递归的方法进行适配。

A master is a widget, typically a toplevel application window or a frame, which contains other widgets, called slaves.

详细的解释在这里:

Your program tells the geometry manager what slaves to manage within the master, i.e., via calling grid. Your program also provides hints as to how it would like each slave to be displayed, e.g., via the column and row options. You can also provide other things to the geometry manager. For example, we used columnconfigure and rowconfigure to indicate the columns and rows we'd like to expand if there is extra space available in the window. It's worth noting that all these parameters and hints are specific to grid; other geometry managers would use different ones.

The geometry manager takes all the information about the slaves in the master, as well as information about how large the master is. It then asks each slave widget for its natural size, i.e., how large it would ideally like to be displayed. The geometry manager's internal algorithm calculates the area each slave will be allocated (if any!). The slave is then responsible for rendering itself within that particular rectangle. And of course, any time the size of the master changes (e.g., because the toplevel window was resized), the natural size of a slave changes (e.g., because we've changed the text in a label), or any of the geometry manager parameters change (e.g., like rowcolumn, or sticky) we repeat the whole thing.

前面我们的例子包括了两个主组件,windows和content。其中content是windows的从组件,然而,然后其他的组件都是从组件在content Frame里面。

这两个主组件分别负责整个GUI的外部位置和内部位置


3 事件控制

TK 和其他的GUI一样,接受从操作系统来的事件。并且通过构建event loop来进行事件分发。原文讲的比较细节,包括了操作系统的基本原理,我这里就略过了。

3.1 Command Callbacks 命令回调

event 触发的处理,通过绑定回调函数来实现,

def calculate(*args):
    ...

ttk.Button(mainframe, text="Calculate", command=calculate)

不解释了,就是calculate这个command

3.2 Binding to Events(事件绑定)

对于没有回调处理的组件,可以通过事件绑定处理来执行相关的事件反馈。

from tkinter import *
from tkinter import ttk
root = Tk()
l =ttk.Label(root, text="Starting...")
l.grid()
l.bind('<Enter>', lambda e: l.configure(text='Moved mouse inside'))
l.bind('<Leave>', lambda e: l.configure(text='Moved mouse outside'))
l.bind('<ButtonPress-1>', lambda e: l.configure(text='Clicked left mouse button'))
l.bind('<3>', lambda e: l.configure(text='Clicked right mouse button'))
l.bind('<Double-1>', lambda e: l.configure(text='Double clicked'))
l.bind('<B3-Motion>', lambda e: l.configure(text='right button drag to %d,%d' % (e.x, e.y)))
root.mainloop()

The first two bindings are pretty straightforward, just watching for simple events. An <Enter> event means the mouse has moved over top the widget, while the <Leave> event is generated when the mouse moves outside the widget to a different one.

The next binding looks for a mouse click, specifically a <ButtonPress-1> event. Here, the <ButtonPress> is the actual event, but the -1 is an event detail specifying the left (main) mouse button on the mouse. The binding will only trigger when a <ButtonPress> event is generated involving the main mouse button. If another mouse button was clicked, this binding would ignore it.

This next binding looks for a <3> event. This is actually a shorthand for <ButtonPress-3>. It will respond to events generated when the right mouse button is clicked. The next binding, <Double-1> (shorthand for <Double-ButtonPress-1>) adds another modifier, Double, and so will respond to the left mouse button being double clicked.

The last binding also uses a modifier: capture mouse movement (Motion), but only when the right mouse button (B3) is held down. This binding also shows an example of how to use event parameters. Many events, such as mouse clicks or movement carry additional information like the current position of the mouse. Tk provides access to these parameters in Tcl callback scripts through the use of percent substitutions. These percent substitutions let you capture them so they can be used in your script.

Tkinter abstracts away these percent substitutions and instead encapsulates all the event parameters in an event object. Above, we used the x and y fields to retrieve the mouse position. We'll see percent substitutions used later in another context, entry widget validation.

3.3 Multiple Bindings for an Event(多重事件绑定)

Tk支持多事件序列支持。

TK也支持,在toplevel window上进行事件绑定,这样可以对window里面的组件进行一组事件的控制。

3.4 事件列表

<Activate>:

Window has become active.

<Deactivate>:

Window has been deactivated.

<MouseWheel>:

Scroll wheel on mouse has been moved.

<KeyPress>:

Key on keyboard has been pressed down.

<KeyRelease>:

Key has been released.

<ButtonPress>:

A mouse button has been pressed.

<ButtonRelease>:

A mouse button has been released.

<Motion>:

Mouse has been moved.

<Configure>:

Widget has changed size or position.

<Destroy>:

Widget is being destroyed.

<FocusIn>:

Widget has been given keyboard focus.

<FocusOut>:

Widget has lost keyboard focus.

<Enter>:

Mouse pointer enters widget.

<Leave>:

Mouse pointer leaves widget.

TK 的事件后缀指定具体的窗口组件

Event detail for mouse events are the button that was pressed, e.g. 12, or 3. For keyboard events, it's the specific key, e.g. A9spacepluscommaequal. A complete list can be found in the keysyms command reference.

Event modifiers for include, e.g. B1 or Button1 to signify the main mouse button being held down, Double or Triple for sequences of the same event. Key modifiers for when keys on the keyboard are held down inline ControlShiftAltOption, and Command.

3.4 虚拟事件

Many widgets also generate higher level or semantic events called virtual events

 理解为实际触发事件外的事件,例如:

 listbox widget will generate a <<ListboxSelect>> virtual event whenever its selection changes

<<Cut>><<Copy>> and <<Paste>>.

同样可以定义自己的虚拟事件

root.event_generate("<<MyOwnEvent>>")

参考:

TkDocs Tutorial - Tk Concepts


参考:

TkDocs Tutorial - Tk Concepts