воскресенье, 2 декабря 2012 г.

Tk. Обязательная первая программа

Чтобы убедиться, что все сработало как надо, давайте попробуем запустить программу "Hello World" в Tk. Пока программы коротки, их можно набирать прямо в интерпретаторе, вместо использования твоего любимого текстового редактора для записи в файл.
package require Tk
grid [ttk::button .b -text "Hello World"] 
Сохраните это в файл "hello.tcl". Из оболочки wish наберите:
% source hello.tcl
Не смогли найти hellp.tcl? Вы возможно искали не в той папке. Вы либо можете передать полный путь к файлу hello.tcl, или использовать команды Tcl "pwd" и "cd"  для просмотра директории и ее смены на другую.
require 'tk'
require 'tkextlib/tile'
root = TkRoot.new() 
button = Tk::Tile::TButton.new(root) {text "Hello World"}.grid
Tk.mainloop()
Сохраните файл под именем 'hello.rb'. Запустите 'irb', и в командной строке наберите:
% source "hello.rb"
Не можете найти 'hello.rb'? Возможно вы искали не в той папке. Вы либо можете передать полный путь к hello.rb, либо использовать команды Ruby "Dir.pwd" и "Dir.chdir" для просмотра директории в которой находитесь и ее смены на другую.
use Tkx;
Tkx::grid( Tkx::ttk__button(".b", -text => "Hello, world" ) );
Tkx::MainLoop();
Заметьте, что здесь два символа подчеркивания между "ttk" и "button".
Сохраните это в файл под именем "hello.pl". Из командной строки наберите:
% perl hello.pl
Не смогли найти hello.pl? Возможно вы искали не в той папке. Попробуйте набрать полный путь к hello.pl
Не работает? Вы уверены, что используете версию Tcl/Tk 8.5.х? Смотрите главу "Установка Tk"
from tkinter import *
from tkinter import ttk
root = Tk()
ttk.Button(root, text="Hello World").grid()
root.mainloop()
Сохраните это в файл 'hello.py'. Из командной строки наберите:
% python hello.py
Не смогли найти hello.py? Может вы ищите не в той папке? Попробуйте  набрать полный путь к файлу hello.py

Our First Program. Some work left to do before the IPO.

Первый настоящий пример

Будет нелегко, но давайте попробуем немного более полезный пример, который даст нам первой ощущение того, какой код скрывается за видом Tk программы.

Проектирование

Пример, который мы используем является простой GUI утилитой, которая конвертирует число футов в эквивалент в метрах. Если бы мы сделали набросок, то он выглядел бы примерно так:

Набросок нашей программы конвертации футов в метры.
Что ж, похожу у нас есть небольшой виджет поля текстового ввода, который позволит вводить число футов, и кнопка "Вычислить", которая извлечет значение из поля, выполнит вычисления, и затем разместит значение в метрах на экране прямо под текстовым полем. Мы также имеем три статические лэйблы ("футы", "является эквивалентом" и "метрам"), которые помогут пользователям понять как использовать интерфейс.
С точки зрения расположения, все, кажется, естественно разделить на три столбца и три строки:

Разметка интерфейса, который соответствует сетке a 3 x 3.

Код

Вот Tcl/Tk код для создания программы.
package require Tk

wm title . "Feet to Meters"
grid [ttk::frame .c -padding "3 3 12 12"] -column 0 -row 0 -sticky nwes
grid columnconfigure . 0 -weight 1; grid rowconfigure . 0 -weight 1

grid [ttk::entry .c.feet -width 7 -textvariable feet] -column 2 -row 1 -sticky we
grid [ttk::label .c.meters -textvariable meters] -column 2 -row 2 -sticky we
grid [ttk::button .c.calc -text "Calculate" -command calculate] -column 3 -row 3 -sticky w

grid [ttk::label .c.flbl -text "feet"] -column 3 -row 1 -sticky w
grid [ttk::label .c.islbl -text "is equivalent to"] -column 1 -row 2 -sticky e
grid [ttk::label .c.mlbl -text "meters"] -column 3 -row 2 -sticky w

foreach w [winfo children .c] {grid configure $w -padx 5 -pady 5}
focus .c.feet
bind . <Return> {calculate}

proc calculate {} {  
   if {[catch {
       set ::meters [expr {round($::feet*0.3048*10000.0)/10000.0}]
   }]!=0} {
       set ::meters ""
   }
}

Вот RubyTk код для создания программы.
require 'tk'
require 'tkextlib/tile'

root = TkRoot.new {title "Feet to Meters"}
content = Tk::Tile::Frame.new(root) {padding "3 3 12 12"}.grid( :sticky => 'nsew')
TkGrid.columnconfigure root, 0, :weight => 1; TkGrid.rowconfigure root, 0, :weight => 1

$feet = TkVariable.new; $meters = TkVariable.new
f = Tk::Tile::Entry.new(content) {width 7; textvariable $feet}.grid( :column => 2, :row => 1, :sticky => 'we' )
Tk::Tile::Label.new(content) {textvariable $meters}.grid( :column => 2, :row => 2, :sticky => 'we');
Tk::Tile::Button.new(content) {text 'Calculate'; command {calculate}}.grid( :column => 3, :row => 3, :sticky => 'w')

Tk::Tile::Label.new(content) {text 'feet'}.grid( :column => 3, :row => 1, :sticky => 'w')
Tk::Tile::Label.new(content) {text 'is equivalent to'}.grid( :column => 1, :row => 2, :sticky => 'e')
Tk::Tile::Label.new(content) {text 'meters'}.grid( :column => 3, :row => 2, :sticky => 'w')

TkWinfo.children(content).each {|w| TkGrid.configure w, :padx => 5, :pady => 5}
f.focus
root.bind("Return") {calculate}

def calculate
  begin
     $meters.value = (0.3048*$feet*10000.0).round()/10000.0
  rescue
     $meters.value = ''
  end
end

Tk.mainloop

Вот Perl код для создания программы
use Tkx;

Tkx::wm_title(".", "Feet to Meters");
Tkx::ttk__frame(".c",  -padding => "3 3 12 12");
Tkx::grid( ".c", -column => 0, -row => 0, -sticky => "nwes");
Tkx::grid_columnconfigure( ".", 0, -weight => 1); 
Tkx::grid_rowconfigure(".", 0, -weight => 1);

Tkx::ttk__entry(".c.feet", -width => 7, -textvariable => \$feet);
Tkx::grid(".c.feet", -column => 2, -row => 1, -sticky => "we");
Tkx::ttk__label(".c.meters", -textvariable => \$meters);
Tkx::grid(".c.meters", -column => 2, -row => 2, -sticky => "we");
Tkx::ttk__button(".c.calc", -text => "Calculate", -command => sub {calculate();});
Tkx::grid(".c.calc", -column => 3, -row => 3, -sticky => "w");

Tkx::grid( Tkx::ttk__label(".c.flbl", -text => "feet"), -column => 3, -row => 1, -sticky => "w");
Tkx::grid( Tkx::ttk__label(".c.islbl", -text => "is equivalent to"), -column => 1, -row => 2, -sticky => "e");
Tkx::grid( Tkx::ttk__label(".c.mlbl", -text => "meters"), -column => 3, -row => 2, -sticky => "w");

foreach (Tkx::SplitList(Tkx::winfo_children(".c"))) {
    Tkx::grid_configure($_, -padx => 5, -pady => 5);
}
Tkx::focus(".c.feet");
Tkx::bind(".", "<Return>", sub {calculate();});

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

Tkx::MainLoop();
Как мы увидим в следующей главе, есть другой, более объектно-ориентированный способ сделать это же. Вы удивлены?
Вот Python код для создания программы.
from tkinter import *
from tkinter import ttk

def calculate(*args):
    try:
        value = float(feet.get())
        meters.set((0.3048 * value * 10000.0 + 0.5)/10000.0)
    except ValueError:
        pass
    
root = Tk()
root.title("Feet to Meters")

mainframe = ttk.Frame(root, padding="3 3 12 12")
mainframe.grid(column=0, row=0, sticky=(N, W, E, S))
mainframe.columnconfigure(0, weight=1)
mainframe.rowconfigure(0, weight=1)

feet = StringVar()
meters = StringVar()

feet_entry = ttk.Entry(mainframe, width=7, textvariable=feet)
feet_entry.grid(column=2, row=1, sticky=(W, E))

ttk.Label(mainframe, textvariable=meters).grid(column=2, row=2, sticky=(W, E))
ttk.Button(mainframe, text="Calculate", command=calculate).grid(column=3, row=3, sticky=W)

ttk.Label(mainframe, text="feet").grid(column=3, row=1, sticky=W)
ttk.Label(mainframe, text="is equivalent to").grid(column=1, row=2, sticky=E)
ttk.Label(mainframe, text="meters").grid(column=3, row=2, sticky=W)

for child in mainframe.winfo_children(): child.grid_configure(padx=5, pady=5)

feet_entry.focus()
root.bind('<Return>', calculate)

root.mainloop()
Результат пользовательского интерфейса:



Скриншот готового интерфейса футов в метры (на Mac OS X, Windows и Linux).

Обратите внимание на стиль кода

Каждый язык, включенный в руководство имеет несколько стилей кода и соглашений доступных для выбора, которые помогают определить следующее: соглашения для именования переменных и функций, процедурного, функционального, или объектно-ориентированного стиля и т.д.
Поскольку в центре внимания в этом уроке Tk, этот урок будет преподносить все как можно более просто, как правило, используется очень прямой стиль, а не обертывание большей части нашего кода в процедуры, модули, объекты, классы и так далее. Насколько это возможно, вы также увидите, что на всех языках для каждого примера  используются одинаковые имена для объектов, переменных и т.д. 

Разбираем шаг за шагом

Давайте взглянем ближе на код, кусочек за кусочком. Сейчас мы попытаемся дать базовое представление о типах вещей, которые нужны для создания интерфейса в Tk, и более грубо посмотрим на то, как эти вещи выглядят. Детали обсудим позже.
package require Tk
First thing we do is tell Tcl that our program needs Tk. Though not strictly necessary, it's considered good form to include this line. It can also be used to specify exactly what version of Tk is needed.
require 'tk'
require 'tkextlib/tile' 
These two lines tell Ruby that our program needs two packages. The first, "tk", is the Ruby binding to Tk, which when loaded also causes the existing Tk library on your system to be loaded. The second,"tkextlib/tile", is Ruby Tk's binding to the newer "themed widgets" that were added to Tk in 8.5.
The themed widget set evolved out of an earlier Tk add-on called Tile, hence the nomenclature. Despite that, the Tk::Tile::* calls you'll see in the programs are actually using the proper ttk versions in 8.5. Expect this to get better rationalized in the future.
use Tkx;
The first thing that we need to do is tell Perl to load the "Tkx" module, which provides the Perl interface to Tk that we are using.
As mentioned here, there are other Perl bindings to Tk. However, we very strongly recommend using Tkx for development, and that will be the only binding we will be describing here. Tkx has the advantage of being a very thin layer above Tk's native Tcl API, which means that in almost all cases it automatically tracks the latest changes to Tk (something which became a considerable issue with Perl/Tk, which was extremely popular in earlier years, but has not been recently updated). As well, by avoiding introducing another layer of code, API errors are reduced, and we can also take advantage of available reference documentation for Tk (which is usually Tcl oriented).
from tkinter import *
from tkinter import ttk
Эти две строчки говорят Python, что наша программа нуждается в двух модулях. Первый, "tkinter", является стандартной связкой к Tk, которая, когда загрузится, также повлечет за собой существующую в вашей системе библиотеку Tk. Второй, "ttk", является питоновой связкой к новым виджетам с поддержкой тем ("themed widgets"), которые были включены в Tk 8.5.
Заметьте, что мы импортировали все из модуля tkinter, так что мы можем вызывать еще и функции tkinter'а без префиксов, что является стандартной практикой Tkinter. Однако по причине импорта только самого "ttk", нам придется использовать префиксы везде внутри модуля. Ну к примеру вызов  "Entry(...)",  выполнил бы функцию из модуля tkinter, в то время как "ttk.Entry(...)", привела бы к выполнению функции из ttk. Как вы увидите, некоторые функции определены в обоих модулях, и иногда вам будут нужны обе, в зависимости от контекста. Для упрощения, мы будем делать вызовы ttk явно, и это будет стилем этого руководства.
Одной из первых вещей, которую вы обнаружили мигрируя на новый код, является написание в нижнем регистре имени модуля Tkinter, т.е. "tkinter", чаще чем "Tkinter". Это было изменено с выходом Python 3.0
wm title . "Feet to Meters"
grid [ttk::frame .c -padding "3 3 12 12"] -column 0 -row 0 -sticky nwes
grid columnconfigure . 0 -weight 1; grid rowconfigure . 0 -weight 1
root = TkRoot.new {title "Feet to Meters"}
content = Tk::Tile::Frame.new(root) {padding "3 3 12 12"}.grid(:sticky => 'nsew')
TkGrid.columnconfigure root, 0, :weight => 1; TkGrid.rowconfigure root, 0, :weight => 1
Tkx::wm_title(".", "Feet to Meters");
Tkx::ttk__frame(".c",  -padding => "3 3 12 12");
Tkx::grid( ".c", -column => 0, -row => 0, -sticky => "nwes");
Tkx::grid_columnconfigure( ".", 0, -weight => 1); 
Tkx::grid_rowconfigure(".", 0, -weight => 1);
root = Tk()
root.title("Feet to Meters")
mainframe = ttk.Frame(root, padding="3 3 12 12")
mainframe.grid(column=0, row=0, sticky=(N, W, E, S))
mainframe.columnconfigure(0, weight=1)
mainframe.rowconfigure(0, weight=1) 
Да, вы нас раскусили, функция "calculate" была раньше этого. Мы опишем ее ниже, но мы должны были включить ее вначале из-за ссылок на нее в других частях программы.
Далее, верхние строчки настраивают главное окно, задавая его заголовок "Feet to meters". Далее, мы создаем виджет "frame", который будет содержать контент нашего интерфейса, и разместит это в нашем главном окне. Биты "columnconfigure"/"rowconfigure"  просто сообщают Tk, что если размеры главного окна изменились, фрейм должен увеличиться, чтобы заполнить лишнее пространство.
Говоря прямо, мы могли просто поместить другие части нашего интерфейса прямо в главное
окно root без промежуточного фрейма контента. Однако главное окно не является само частью виджетов с темами ("themed" widgets), так что его фоновой цвет не совпадал бы с виджетами, которые мы поместили бы внутрь его. Используя фрейм с темами для содержания контента, убедитесь, что фон корректный. (Using a "themed" frame widget to hold the content ensures that the background is correct.)
grid [ttk::entry .c.feet -width 7 -textvariable feet] -column 2 -row 1 -sticky we
grid [ttk::label .c.meters -textvariable meters] -column 2 -row 2 -sticky we
grid [ttk::button .c.calc -text "Calculate" -command calculate] -column 3 -row 3 -sticky w
$feet = TkVariable.new; $meters = TkVariable.new
f = Tk::Tile::Entry.new(content) {width 7; textvariable $feet}.grid( :column => 2, :row => 1, :sticky => 'we' )
Tk::Tile::Label.new(content) {textvariable $meters}.grid( :column => 2, :row => 2, :sticky => 'we');
Tk::Tile::Button.new(content) {text 'Calculate'; command {calculate}}.grid( :column => 3, :row => 3, :sticky => 'w')
Tkx::ttk__entry(".c.feet", -width => 7, -textvariable => \$feet);
Tkx::grid(".c.feet", -column => 2, -row => 1, -sticky => "we");
Tkx::ttk__label(".c.meters", -textvariable => \$meters);
Tkx::grid(".c.meters", -column => 2, -row => 2, -sticky => "we");
Tkx::ttk__button(".c.calc", -text => "Calculate", -command => sub {calculate();});
Tkx::grid(".c.calc", -column => 3, -row => 3, -sticky => "w");
feet = StringVar()
meters = StringVar()
feet_entry = ttk.Entry(mainframe, width=7, textvariable=feet)
feet_entry.grid(column=2, row=1, sticky=(W, E))
ttk.Label(mainframe, textvariable=meters).grid(column=2, row=2, sticky=(W, E))
ttk.Button(mainframe, text="Calculate", command=calculate).grid(column=3, row=3, sticky=W)
Предыдущие строчки создают три главных виджета нашей программы: entry (поле ввода), где мы будем печатать число футов, label, где мы поместим результат в метрах и кнопку caculate, которую мы будем нажимать для выполнения вычислений.
ForДля каждого из трех виджетов нам нужны 2 вещи: создать сам виджет, и разместить его на экране. Все три виджета, которые являются "потомками" нашего окна контента созданы как экземпляры одного из классов виджетов с темами Tk. В то же время, как мы создаем их, мы передаем им определенные опции, такие как насколько широким будет поле ввода, текст внутри кнопки и т.д. Полю ввода и лабелу назначена местическая "textvariable"; мы скоро увидим, что она делает.
Если виджеты просто созданы, они автоматически не покажутся на экране, поскольку Tk не знает как вы хотите из разместить с другими виджетами. Вот это делает часть с "grid". Вспоминая сетчатое расположение для нашего приложения, мы размещаем каждый виджет в соответствующей колонке (1, 2 или 3), и строке (тоже 1, 2 или 3). Опция "sticky", говорит как виджет должен выстраиваться в клетках сетки , используя направления компаса. Ну, "w" (west/запад) подразумевает закрепление виджета на левой стороне клетки "we" (west-east) подразумевает закрепление сразу на левой и правой сторонах, и т. д.
grid [ttk::label .c.flbl -text "feet"] -column 3 -row 1 -sticky w
grid [ttk::label .c.islbl -text "is equivalent to"] -column 1 -row 2 -sticky e
grid [ttk::label .c.mlbl -text "meters"] -column 3 -row 2 -sticky w
Tk::Tile::Label.new(content) {text 'feet'}.grid( :column => 3, :row => 1, :sticky => 'w')
Tk::Tile::Label.new(content) {text 'is equivalent to'}.grid( :column => 1, :row => 2, :sticky => 'e')
Tk::Tile::Label.new(content) {text 'meters'}.grid( :column => 3, :row => 2, :sticky => 'w')
Tkx::grid( Tkx::ttk__label(".c.flbl", -text => "feet"), -column => 3, -row => 1, -sticky => "w");
Tkx::grid( Tkx::ttk__label(".c.islbl", -text => "is equivalent to"), -column => 1, -row => 2, -sticky => "e");
Tkx::grid( Tkx::ttk__label(".c.mlbl", -text => "meters"), -column => 3, -row => 2, -sticky => "w");
ttk.Label(mainframe, text="feet").grid(column=3, row=1, sticky=W)
ttk.Label(mainframe, text="is equivalent to").grid(column=1, row=2, sticky=E)
ttk.Label(mainframe, text="meters").grid(column=3, row=2, sticky=W)
Три верхние строчки делают одно и то же для трех статических label'ов нашего пользовательского интерфейса; создают каждую и размещают ее на экране в соответствующей клетке сетки.
foreach w [winfo children .c] {grid configure $w -padx 5 -pady 5}
focus .c.feet
bind . <Return> {calculate}
TkWinfo.children(content).each {|w| TkGrid.configure w, :padx => 5, :pady => 5}
f.focus
root.bind("Return") {calculate}
foreach (Tkx::SplitList(Tkx::winfo_children(".c"))) { Tkx::grid_configure($_, -padx => 5, -pady => 5); }
Tkx::focus(".c.feet");
Tkx::bind(".", "<Return>", sub {calculate();});
for child in mainframe.winfo_children(): child.grid_configure(padx=5, pady=5)
feet_entry.focus()
root.bind('<Return>', calculate)
Предыдущие три строки помогают внести некоторые красивые штрихи для нашего интерфейса.
The Первая строчка пробегает по всем виджетам, которые являются потомками нашего фрейма контента, и добавляет небольшой отступ вокруг каждого, так что они не будут сжаты вместе. Мы могли добавить эти опции для каждого вызова "grid", когда мы впервые поместили виджеты на экран, но это способ получше.
Вторая строчка говорит говорит Tk перевести фокус на наш виджет ввода. При запуску курсор будет находится сразу в текстовом поле, так что пользователю не придется кликать на него перед вводом.
The Третья строчка Tk говорит, что если пользователь нажмет кнопку возврата (Enter в Windows) где-нибудь в окне root, оно должно вызвать функцию calculate, что также произойдет при нажатии кнопки Calculate.
proc calculate {} {  
   if {[catch {
       set ::meters [expr {round($::feet*0.3048*10000.0)/10000.0}]
   }]!=0} {
       set ::meters ""
   }
}
def calculate
  begin
     $meters.value = (0.3048*$feet*10000.0).round()/10000.0
  rescue
     $meters.value = ''
  end
end
sub calculate {
   $meters = int(0.3048*$feet*10000.0+.5)/10000.0 || '';
}
def calculate(*args):
    try:
        value = float(feet.get())
        meters.set((0.3048 * value * 10000.0 + 0.5)/10000.0)
    except ValueError:
        pass
Здесь мы объявили нашу процедуру calculate, которая вызывается когда пользователь нажимает кнопку Calculate, или жмякает на кнопку "Возврат". Она выполняет преобразование футов в метры, принимая число футов из нашего виджета ввода, и размещая результат в нашем виджете label. 
Скажите что? Не похоже, чтобы мы делали что-либо с этими виджетами! Здесь, где магические  опции "textvariable", которые мы определили, когда создавали виджеты, вступают в игру. Мы определили глобальную переменную "feet" как текстовую переменную для ввода, которая подразумевает, что в любое время поле ввода изменяется, Tk будет автоматически обновлять глобальную переменную feet. Кроме того, если мы явно изменили значение текстовой переменной, ассоциированной с виджетом (как мы делали для "meters", которая прикреплена к нашей label), виджет будет автоматичесйи обновлять содержание переменной. Ловко.
Tk.mainloop
Последняя строчка говорит Tk войти в событийный цикл, который нужен для того, чтобы все заработало.
Tkx::MainLoop();
Последняя строчка говорит Tk войти в событийный цикл, который нужен для того, чтобы все заработало.
root.mainloop();
Последняя строчка говорит Tk войти в событийный цикл, который нужен для того, чтобы все заработало.


Чего не хватает

Также стоит рассмотреть то, что мы нам не нужно включать в нашу программу Tk чтобы заставить ее работать. Например:
  • нам не нужно волноваться о перерисовке экрана, когда что-либо меняется.
  • нам не нужно волноваться о парсинге и управлении событиями, обнаружения кликов, или обработки событий для каждого виджета.
  • нам не нужно предоставлять кучу опций, когда мы создаем виджеты. стандартные параметры будут заботиться о большинстве вещей, таким образом, нам придется изменять вещи, такие как отображаемый текст на кнопке.
  • нам не нужно писать комплексный код для получения и установки значений простых виджетов. мы просто прикрепляем их к переменным.
  • нам не нужно волноваться о том, что случиться, когда пользователь закроет окно или решит изменить его размер
  • нам не нужно дополнительного кода, чтобы заставить это все работать кросс-платформенно