Chương 19: Nghiên cứu cụ thể: Tkinter

Trở về Mục lục quyển sách

GUI

Phần lớn chương trình ta đã gặp đều hoạt động trên nền chữ, nhưng cũng có nhiều chương trình dùng giao diện đồ họa người dùng, cũng được gọi là GUI.

Python cung cấp một số cách để viết chương trình GUI: dùng wxPython, Tkinter, và Qt. Mỗi cách đều có ưu và nhược điểm, đó cũng là lí do mà Python chưa có quy chuẩn cụ thể về mặt này.

Công cụ mà tôi trình bày trong chương này là Tkinter vì tôi nghĩ rằng đó là thứ dễ nhất mà chúng ta bắt đầu học. Đa số các khái niệm trong chương này cũng dùng được với các module GUI khác.

Có một số cuốn sách và trang web về Tkinter. Một trong những nguồn trực tuyến tốt nhất là An Introduction to Tkinter viết bởi Fredrik Lundh.

Tôi đã viết một module có tên Gui.py đi kèm theo Swampy. Nó cung cáp một giao diện được đơn giản hóa với các hàm và lớp trong Tkinter. Những ví dụ trong chương này đều dựa theo module đó.

Sau đây là một ví dụ đơn giản nhằm tạo ra và hiển thị một Gui:

Để tạo ra một GUI, bạn cần nhập vào Gui rồi cá thể hóa một đối tượng Gui:

from Gui import * g = Gui() g.title('Gui') g.mainloop() 

Khi bạn chạy đoạn mã này, một cửa sổ sẽ xuất hiện với một hình vuông màu xám và tiêu đề chữ Gui. mainloop chạy vòng lặp sự kiện, để đợi người dùng thực hiện một thao tác và phản hồi một cách tương ứng. Đó là một vòng lặp vô hạn; nó tiếp tục chạy đến khi người dùng đóng cửa sổ, hoặc ấn Control-C, hoặc thực hiện thao tác khiến cho chương trình kết thúc.

Đối tượng Gui này chẳng có gì đáng kể vì nó không chứa một widget nào. Widget là thành phần tạo nên GUI; chúng bao gồm:

Nút (Button):
Một widget, có chứa chữ hoặc hình, để thực hiện một việc khi được nhấn.
Nền (Canvas):
Một vùng trên đó có thể hiển thị đường nét, hình chữ nhật, hình tròn và các hình khác.
Ô chữ (Entry):
Một vùng mà người dùng có thể gõ chữ vào đó.
Thanh trượt (Slider):
Một widget có khả năng điều chỉnh phần hiển thị của một widget khác.
Khung (Frame):
Một widget để chứa, thường luôn hiển thị, và đựng các widget khác ở trong.

Hình vuông màu xám mà bạn nhìn thấy khi tạo ra Gui là một Frame. Khi bạn tạo ra một widget mới, nó sẽ được thêm vào Frame này.

Nút và những điểm gọi lại

Phương thức bu sau đây tạo ra một widget kiểu Button (nút):

button = g.bu(text='Press me.') 

Giá trị trả về từ bu là một đối tượng Button. Hình nút hiện ra trong Frame là hình biểu diễn của đối tượng này; bạn có thể điều khiển nút bằng cách gọi các phương thức của nó.

bu nhận đến 32 tham biến có ảnh hưởng đến hình dáng và tính năng của nút. Các tham biến này được gọi là những tuỳ chọn. Thay vì cấp giá trị cho cả 32 tuỳ chọn này, bạn có thể dùng đối số từ khoá, như text='Press me.', để chỉ định những tuỳ chọn nào bạn cần và số còn lại thì dùng các giá trị mặc định.

Khi bạn thêm một widget vào trong Frame, nó sẽ bị “co hẹp;” nghĩa là, Frame sẽ co lại cho bằng kích thước của Button. Nếu bạn thêm nhiều widget nữa, Frame sẽ nở ra để có đủ chỗ.

Phương thức la sau đây sẽ tạo ra một widget kiểu Label:

label = g.la(text='Press the button.') 

Tkinter mặc nhiên chồng chất các widget theo chiều đứng và căn chúng vào giữa. Ta sẽ xét cách thay thế đè lên biểu hiện đó.

Nếu ấn nút, bạn sẽ thấy không ăn thua. Đó là vì bạn chưa “tiếp điện” cho nó; nghĩa là bạn chưa ra lệnh cho nó làm gì!

Tuỳ chọn để điều khiển động thái của nút là command. Giá trị của command là một hàm vốn sẽ được thực hiện khi nút được nhấn. Chẳng hạn, sau đây là một hàm để tạo mới một Label:

def make_label(): g.la(text='Thank you.') 

Bây giờ khi bạn tạo ra nút với hàm này đưa vào làm command của nó:

button2 = g.bu(text='No, press me!', command=make_label) 

Khi bạn ấn nút này, nó sẽ thực hiện make_label và một dòng chữ mới sẽ xuất hiện.

Giá trị của tuỳ chọn command là một đối tượng hàm, và cũng được gọi là một điểm gọi lại vì sau khi bạn gọi bu để tạo ra nút, luồng thực hiện sẽ “gọi lại” khi người dùng nhấn nút.

Kiểu luồng thực hiện này là đặc trưng cho lập trình hướng sự kiện. Các hành động của người dùng, như ấn nút và gõ phím, được gọi là {sự kiện}. Trong lập trình hướng sự kiện, luồng thực hiện được quyết định bởi người dùng thay vì người lập trình.

Thử thách đối với lập trình hướng sự kiện là việc thiết lập một tập hợp các widget và điểm gọi lại sao cho chúng hoạt động đúng (hoặc ít nhất phải phát sinh những thông báo lỗi hợp lý) cho bất kì chuỗi hành động nào từ phía người dùng.

Hãy viết một chương trình để tạo ra GUI chỉ có một nút. Khi nút được nhấn, một nút thứ hai sẽ được tạo ra. Khi nút thứ hai này được nhấn, cần tạo ra một dòng chữ viết, “Nice job!”.

Điều gì sẽ xảy ra khi bạn ấn các nút nhiều lần? Bạn có thể tham khảo lời giải của tôi tại thinkpython.com/code/button_demo.py

Các widget kiểu Canvas

Một trong những widget năng động nhất là Canvas, có tác dụng tạo ra một vùng để vẽ các đường thẳng, hình tròn và những hình khác. Nếu đã làm Bài tập canvas, bạn đã quen với kiểu widget nền này.

Phương thức ca sau đây sẽ tạo ra Canvas mới:

canvas = g.ca(width=500, height=500) 

widthheight là các kích thước của nền tính theo pixel.

Sau khi tạo ra một widget, bạn vẫn có thể thay đổi các giá trị của tuỳ chọn bằng phương thức config. Chẳng hạn, tuỳ chọn bg thay đổi màu nền:

canvas.config(bg='white') 

Giá trị của bg là một chuỗi chứa tên của một màu. Tập hợp những tên màu hợp lệ lại khác nhau với từng phiên bản Python dành cho các loại máy, nhưng tất cả phiên bản đều có ít nhất là các màu sau:

white black red green blue cyan yellow magenta 

Các hình trên một Canvas được gọi là item. Chẳng hạn, phương thức của Canvas có tên circle vẽ một hình tròn:

item = canvas.circle([0,0], 100, fill='red') 

Đối số thứ nhất là cặp toạ độ của tâm hình tròn; đối số thứ hai là bán kính.

Gui.py có một hệ toạ độ Đề-các tiêu chuẩn với điểm gốc tại tâm của Canvas và trục y hướng lên. Nó khác với một số hệ thống đồ thị khác trong đó điểm gốc lại ở góc bên trái phía trên và trục y hướng xuống.

Tuỳ chọn fill chỉ định rằng hình tròn cần được tô màu đỏ.

Giá trị trả về từ circle là một đối tượng Item nhằm cung cấp những phương thức để sửa đổi các item trên nền. Chẳng hạn, bạn có thể dùng config để thay đổi bất kì tuỳ chọn nào của hình tròn:

item.config(fill='yellow', outline='orange', width=10) 

width là độ dày của viền tính theo pixel; outline là màu của viền.

Hãy viết một chương trình để tạo ra một Canvas và một Button. Khi người dùng ấn Button, cần phải vẽ ra một hình tròn trên nền.

Dãy các toạ độ

Phương thức rectangle nhận vào một dãy hai toạ độ để định vị hai góc đối diện của hình chữ nhật. Ví dụ này vẽ một hình chữ nhật màu xanh lá cây với góc trái phía dưới ở gốc tạo độ và góc phải phía trên ở (200,100):

canvas.rectangle([[0, 0], [200, 100]], fill='blue', outline='orange', width=10) 

Cách làm chỉ định các góc nói trên cũng được gọi là chỉ định hình bao vì hai điểm góc đó giới hạn hình chữ nhật.

oval nhận vào một hình bao và vẽ một hình trái xoan bên trong hình chữ nhật bao đó:

canvas.oval([[0, 0], [200, 100]], outline='orange', width=10) 

line nhận vào một dãy các toạ độ và vẽ đoạn thẳng nối các điểm đó. Ví dụ sau vẽ hai cạnh bên của một hình tam giác:

canvas.line([[0, 100], [100, 200], [200, 100]], width=10) 

polygon nhận vào cũng các đối số như trên, nhưng nó còn vẽ thêm một đoạn thẳng nối hai điểm đầu cuối của đa giác (nếu cần) và tô màu nó:

canvas.polygon([[0, 100], [100, 200], [200, 100]], fill='red', outline='orange', width=10) 

Các widget khác

Tkinter có hai loại widget cho phép người dùng gõ chữ vào: Entry, chứa một dòng chữ, và Text, chứa nhều dòng.

en tạo ra một Entry mới:

entry = g.en(text='Default text.') 

Tuỳ chọn text cho phép bạn đưa chữ vào Entry một khi nó được tạo ra. Phương thức get trả về nội dung của Entry (mà người dùng thay đổi được):

>>> entry.get() 'Default text.' 

te tạo ra một widget kiểu Text:

text = g.te(width=100, height=5) 

widthheight là các kích thước của tính theo số kí tự bề rộng và số dòng.

insert đặt đoạn chữ vào trong widget kiểu Text:

text.insert(END, 'A line of text.') 

END là một chỉ số đặc biệt để biểu thị kí tự cuối trong widget kiểu Text.

Bạn cũng có thể chỉ định một kí tự bằng cách dùng chỉ số dấu chấm, như 1.1, trong đó số dòng đi trước dấu chấm còn số cột đi sau. Ví dụ sau thêm chữ vào sau kí tự thứ nhất của dòng đầu tiên.

>>> text.insert(1.1, 'nother') 

Phương thức get đọc những chữ có trong widget; nó nhận các đối số gồm có chỉ số đầu và cuối. Ví dụ sau trả về toàn bộ chữ trong widget, kể cả kí tự xuống dòng:

>>> text.get(0.0, END) 'Another line of text.\n' 

Phương thức delete xoá chữ trong widget; ví dụ sau xoá toàn bộ, chỉ để lại hai kí tự đầu tiên:

>>> text.delete(1.2, END) >>> text.get(0.0, END) 'An\n' 

Hãy sửa lại lời giải của bạn cho Bài tập circle bằng cách thêm vào một widget kiểu Entry và một nút thứ hai. Khi người dùng nhấn nút thứ hai, chương trình sẽ đọc một tên màu từ Entry và dùng nó để đổi cho màu tô hình tròn. Hãy dùng config để sửa đổi hình tròn đã có; đừng tạo ra hình mới.

Chương trình của bạn cần giải quyết được các trường hợp khi người dùng thử thay đổi màu của một hình tròn khi nó chưa được tạo ra, và trường hợp khi tên màu không hợp lệ.

Bạn có thể xem lời giải của tôi tại thinkpython.com/code/circle_demo.py.

Xếp các widget

Đến đây ta đã xếp các widget theo một cột thẳng đứng, nhưng trong hầu hết các GUI, sự sắp xếp còn phức tạp hơn nhiều. Chẳng hạn, sau đây là một phiên bản được rút gọn chút ít từ TurtleWorld (xem Chương 4).

TurtleWorld
Mục này trình bày đoạn mã dùng để tạo ra GUI này, chia nhỏ thành một loạt các bước. Bạn có thể tải về ví dụ trọn vẹn từ thinkpython.com/code/SimpleTurtleWorld.py.

Ở cấp cao nhất, GUI này chứa hai widget—một Canvas và một Frame —được xếp theo hàng ngang. Vì vậy bước thứ nhất là tạo ra hàng đó.

class SimpleTurtleWorld(TurtleWorld): """This class is identical to TurtleWorld, but the code that lays out the GUI is simplified for explanatory purposes.""" def setup(self): self.row() ... 

setup là hàm dùng để tạo ra và sắp xếp các widget. Việc sắp đặt các widget trong GUI còn được gọi là xếp.

row tạo ra một Frame hàng và làm nó trở nên “Frame hiện hành”. Cho đến tận khi Frame này được đóng lại hoặc một Frame khác được tạo ra, tất cả các widget tiếp theo đều được xếp trên một hàng.

Sau đây là đoạn mã dùng để tạo Canvas và Frame cột để chứa những widget khác:

 self.canvas = self.ca(width=400, height=400, bg='white') self.col() 

Widget thứ nhất trong cột là một Frame lưới, trong đó lại chứa bốn nút được xếp theo hai hàng và hai cột:

 self.gr(cols=2) self.bu(text='Print canvas', command=self.canvas.dump) self.bu(text='Quit', command=self.quit) self.bu(text='Make Turtle', command=self.make_turtle) self.bu(text='Clear', command=self.clear) self.endgr() 

gr tạo ra lưới; đối số ở đây là số cột. Các widget trong lưới được đặt lần lượt từ trái sang phải, từ trên xuống dưới.

Nút thứ nhất sử dụng self.canvas.dump làm điểm gọi lại; còn nút thứ hai dùng self.quit. Đó là những phương thức hạn hẹp, theo nghĩa là chúng được gắn với những đối tượng cụ thể. Khi được kích hoạt, chúng được gọi với các đối tượng đó.

Widget tiếp theo trong cột là một Frame hàng có chứa một Button và một Entry:

 self.row([0,1], pady=30) self.bu(text='Run file', command=self.run_file) self.en_file = self.en(text='snowflake.py', width=5) self.endrow() 

Đối số thứ nhất cho row là một danh sách các trọng số dùng để xác định xem có bao nhiêu khoảng trống phụ thêm được đặt giữa các widget. Danh sách [0,1] nghĩa là toàn bộ khoảng trống thêm vào được dành cho widget thứ hai, tức là Entry. Nếu chạy chương trình và thay đổi kích cỡ cửa sổ, bạn sẽ thấy rằng Entry sẽ được phóng to hoặc thu nhỏ, còn Button thì không.

Tùy chọn pady “chèn” hàng này theo phương y, bằng cách thêm vào 30 pixel khoảng trống cả phía trên lẫn dưới.

endrow kết thúc hàng widget này, vì vậy các widget tiếp theo được xếp trong Frame cột. Gui.py lưu giữ một chồng các Frame:

  • Khi bạn dùng row, col hoặc gr để tạo ra Frame, nó sẽ được xếp lên đỉnh của chồng và trở nên Frame hiện hành.
  • Khi bạn dùng endrow, endcol hoặc endgr để đóng một Frame, nó được đẩy khỏi chồng xếp và Frame liền trước đó trên chồng trở thành Frame hiện hành.

Phương thức run_file đọc vào nội dung của Entry, dùng nó như một tên file, đọc vào nội dung và chuyển nó đến cho run_code. Còn self.inter là một đối tượng Interpreter (trình thông dịch) để nhận vào một chuỗi và thực hiện nó như đoạn mã lệnh Python.

 def run_file(self): filename = self.en_file.get() fp = open(filename) source = fp.read() self.inter.run_code(source, filename) 

Hai widget sau cùng gồm có một Text và một Button:

 self.te_code = self.te(width=25, height=10) self.te_code.insert(END, 'world.clear()\n') self.te_code.insert(END, 'bob = Turtle(world)\n') self.bu(text='Run code', command=self.run_text) 

run_text cũng tương tự như run_file, chỉ khác ở chỗ nó lấy đoạn mã từ widget Text thay vì lấy từ file:

 def run_text(self): source = self.te_code.get(1.0, END) self.inter.run_code(source, '<user-provided code>') 

Thật không may là chi tiết về cách sắp đặt widget rất khác nhau giữa các ngôn ngữ, và giữa các module trong Python. Riêng Tkinter cho phép ba cơ chế sắp đặt widget. Các cơ chế này được gọi là quản lí không gian. Cơ chế mà tôi giới thiệu trong mục này được gọi là quản lí không gian dạng “lưới”; các dạng khác là “xếp” và “đặt”.

May mắn là phần nhiều các khái niệm trong mục này cũng áp dụng được cho các module lập GUI và các ngôn ngữ khác.

Trình đơn và các Callable

Menubutton là một widget trông giống như một nút, nhưng khi bạn nhấn, nó sẽ bật ra một trình đơn. Sau khi người dùng chọn một mục, trình đơn sẽ biến mất.

Sau đây là đoạn mã nhằm tạo ra một Menubutton để chọn màu (bạn có thể tải nó về từ thinkpython.com/code/menubutton_demo.py):

g = Gui() g.la('Select a color:') colors = ['red', 'green', 'blue'] mb = g.mb(text=colors[0]) 

mb tạo ra một Menubutton. Ban đầu, dòng chữ trên nút là tên gọi của màu mặc định. Vòng lặp sau đây tạo ra mỗi mục trên trình đơn ứng với một màu:

for color in colors: g.mi(mb, text=color, command=Callable(set_color, color)) 

Đối số thứ nhất của mi là Menubutton mà các mục này gắn liền với.

Tùy chọn command là một đối tượng Callable; đây là một khái niệm mới. Đến giờ chúng ta đã thấy các hàm và phương thức hạn hẹp được dùng như những điểm gọi lại, vốn sẽ hoạt động tốt nếu bạn không cần chuyển đối số nào vào trong hàm. Với trường hợp ngược lại bạn sẽ phải lập một đối tượng Callable có chứa một hàm, như set_color, cùng các đối số của nó, như color.

Đối tượng Callable lưu trữ một tham chiếu đến hàm và các đối số, tất cả như những thuộc tính. Sau này, khi người dùng kích chuột vào một mục trên trình đơn, điểm gọi lại sẽ gọi hàm và truyền vào các đối số được lưu trữ.

Có thể viết set_color như sau:

def set_color(color): mb.config(text=color) print color 

Khi người dùng chọn một mục trên trình đơn và set_color được gọi, nó sẽ đặt cấu hình cho Menubutton để hiển thị màu mới được chọn. Nó cũng in ra tên màu; nếu bạn thử ví dụ này, bạn sẽ thấy đúng là set_color được gọi khi bạn chọn một mục (và không được gọi khi bạn tạo ra đối tượng Callable).

Bó buộc

Bó buộc là việc gắn một widget, một sự kiện và một điểm gọi lại với nhau: khi một sự kiện (như việc nhấn nút) xảy ra đối với widget, điểm gọi lại sẽ được kích hoạt.

Nhiều widget có những bó buộc mặc định. Chẳng hạn, khi bạn nhấn một nút, sự bó buộc mặc định sẽ thay đổi nền của nút bấm khiến nó trông như bị lõm xuống. Khi bạn nhả tay ra, sự bó buộc sẽ làm phục hồi vẻ bề ngoài của nút và kích hoạt điểm gọi lại được chỉ định ở tùy chọn command.

Bạn có thể dùng phương thức bind để thay thế đè lên những bó buộc mặc định này, hoặc tạo ra những bó buộc mới. Chẳng hạn, đoạn mã này tạo ra một bó buộc cho nền (bạn có thể tải về các đoạn mã ở mục này từ địa chỉ thinkpython.com/code/draggable_demo.py):

ca.bind('<ButtonPress-1>', make_circle) 

Đối số thứ nhất là một chuỗi chứa sự kiện; sự kiện này được bật lên khi người dùng ấn nút chuột bên trái. Các sự kiện khác liên quan đến thao tác chuột gồm có ButtonMotion, ButtonReleaseDouble-Button.

Đối số thứ hai là chuôi nắm sự kiện. Một chuôi nắm sự kiện là một hàm hoặc phương thức hạn hẹp, giống như một điểm gọi lại, nhưng có một khác biệt quan trọng là chuôi nắm sự kiện nhận đối tượng Event làm tham biến. Sau đây là một ví dụ:

def make_circle(event): pos = ca.canvas_coords([event.x, event.y]) item = ca.circle(pos, 5, fill='red') 

Đối tượng Event bao gồm thông tin về kiểu sự kiện và các chi tiết như tọa độ của con trỏ chuột. Ở ví dụ này thông tin mà ta cần là vị trí của con trỏ khi kích chuột. Các giá trị này được đo bằng “tọa độ pixel”, vốn được định nghĩa bởi hệ thống đồ họa bên trong. Phương thức canvas_coords viết tắt từ “Canvas coordinates”, cũng tương thích với các phương thức của Canvas như circle.

Với các widget Entry, cách thông thường là buộc chúng với sự kiện <Return>, vốn được bật ra khi người dùng gõ phím {Return} hay {Enter}. Chẳng hạn, đoạn mã sau tạo ra một Button và một Entry.

bu = g.bu('Make text item:', make_text) en = g.en() en.bind('<Return>', make_text) 

make_text được gọi khi Button được ấn hoặc khi người dùng gõ phím {Return} khi đang gõ trong Entry. Để làm được điều này, ta cần một hàm có thể gọi được như một lệnh (không có đối số) hoặc một chuôi nắm sự kiện (với một Event làm đối số):

def make_text(event=None): text = en.get() item = ca.text([0,0], text) 

make_text nhận vào nội dung của Entry và hiển thị nó dưới dạng một item kiểu Text bên trong Canvas.

Cũng có thể tạo ra những bó buộc cho các item trong Canvas. Sau đây là một lời định nghĩa lớp cho Draggable, vốn là một lớp con của Item để cung cấp những bó buộc giúp thực hiện thao tác kéo-và-thả.

class Draggable(Item): def __init__(self, item): self.canvas = item.canvas self.tag = item.tag self.bind('<Button-3>', self.select) self.bind('<B3-Motion>', self.drag) self.bind('<Release-3>', self.drop) 

Phương thức init nhận một Item làm tham biến. Nó sao chép lại các thuộc tính của Item rồi tạo những bó buộc cho ba sự kiện: nhấn nút, nút được nhấn khi di chuyển, và nhả nút.

Một chuôi nắm sự kiện, select, lưu trữ các tọa độ của sự kiện hiện hành và màu ban đầu của đối tượng, sau đó đổi màu sang vàng:

 def select(self, event): self.dragx = event.x self.dragy = event.y self.fill = self.cget('fill') self.config(fill='yellow') 

cget viết tắt cho “get configuration;” nó nhận vào tên của một tùy chọn dưới dạng chuỗi và trả lại màu hiện thời của tùy chọn đó.

drag tính xem đối tượng phải di chuyển bao xa so với điểm khởi đầu, cập nhật các tọa độ được lưu lại, và sau đó di chuyển đối tượng.

 def drag(self, event): dx = event.x - self.dragx dy = event.y - self.dragy self.dragx = event.x self.dragy = event.y self.move(dx, dy) 

Việc tính toán này được thực hiện trên các tọa độ pixel; ta không cần phải chuyển về tọa độ của Canvas.

Sau cùng, drop khôi phục lại màu ban đầu của đối tượng:

 def drop(self, event): self.config(fill=self.fill) 

Bạn có thể dùng lớp Draggable để thêm vào tính năng kéo-và-thả cho một đối tượng có sẵn. Chẳng hạn, sau đây là một phiên bản được sửa lại của make_circle trong đó dùng circle để tạo ra một Item và Draggable để khiến nó kéo được:

def make_circle(event): pos = ca.canvas_coords([event.x, event.y]) item = ca.circle(pos, 5, fill='red') item = Draggable(item) 

Ví dụ này cho thấy một trong những lợi ích của việc thừa kế: bạn có thể sửa lại tính năng của một lớp cha mẹ mà không cần sửa đổi định nghĩa của nó. Điều này đặc biệt có ích nếu bạn muốn thay đổi biểu hiện được định nghĩa trong một module mà bạn không viết ra.

Gỡ lỗi

Một trong những khó khăn khi lập trình GUI là phải theo dõi những việc nào xảy ra trong khi xây dựng GUI và những việc nào sẽ xảy ra để phản hồi lại sự kiện từ phía người dùng.

Chẳng hạn, khi bạn thiết lập một điểm gọi lại, một lỗi thông thường là gọi hàm thay vì chuyển một tham chiếu chỉ đến nó:

def the_callback(): print 'Called.' g.bu(text='This is wrong!', command=the_callback()) 

Nếu chạy đoạn mã này, bạn sẽ thấy rằng nó gọi the_callback lập tức, và sau đó mới tạo ra nút. Khi bạn ấn nút, chương trình sẽ không làm gì vì bạn giá trị được trả về từ the_callbackNone. Thường thì bạn sẽ không muốn kích hoạt một điểm gọi lại khi đang xây dựng GUI; nó cần được kích hoạt sau này, để phản hồi lại sự kiện từ phía người dùng.

Một khó khăn khác của lập trình GUI là bạn không có quyền kiểm soát luồng thực hiện của chương trình. Phần nào của chương trình được chạy và thứ tự thực hiện đều được quyết định bởi những thao tác của người dùng. Điều đó có nghĩa là bạn phải thiết kế chương trình để hoạt động đúng với một dãy sự kiện bất kì có thể xảy ra.

Chẳng hạn, GUI trong Bài tập circle2 có hai widget: một cái để tạo ra một Circle và cái kia để đổi màu Circle. Nếu người dùng tạo ra hình tròn rồi mới đổi màu của nó thì không sao. Nhưng điều gì sẽ xảy ra nếu người dùng đổi màu của một hình tròn thậm chí chưa tồn tại? Hay tạo ra nhiều hình tròn?

Càng có nhiều widget, sẽ càng khó hình dung tất cả những dãy sự kiện có thể xảy ra. Một cách kiểm soát sự phức tạp này là bằng cách gói trạng thái của hệ thống vào một đối tượng rồi xem xét:

  • Các trạng thái có thể là gì? Ở ví dụ với Circle, ta có thể xét hai trạng thái: trước và sau khi người dùng tạo ra hình tròn thứ nhất.
  • Với mỗi trạng thai, những sự kiện nào có thể xảy ra? Ở ví dụ trên, người dùng có thể ấn một trong hai nút, hoặt kết thúc chương trình.
  • Với mỗi cặp trạng thái-sự kiện, đâu là kết quả mong muốn? Vì có hai trạng thái và hai nút, nên sẽ có bốn cặp trạng thái-sự kiện cần xét đến.
  • Điều gì có thể gây ra một sự chuyển đổi từ trạng thái này sang trạng thái khác? Trong trường hợp này, có một sự chuyển đổi khi người dùng tạo ra hình tròn thứ nhất.

Bạn cũng có thể thấy cần định nghĩa, và kiểm tra, những bất biến cần thỏa mãn bất kể dãy các sự kiện như thế nào.

Phương pháp lập trình GUI này có thể giúp bạn viết mã lệnh đúng mà không mất thời gian thử từng dãy sự kiện có thể từ phía người dùng!

Thuật ngữ

GUI:
Giao diện đồ họa người dùng.
widget:
Một trong các yếu tố để tạo nên GUI, bao gồm nút, trình đơn, ô chữ, v.v.
tùy chọn:
Giá trị chi phối bề ngoài hoặc tính năng của một widget.
đối số từ khóa:
Đối số để chỉ định tên của tham biến như một phần của lời gọi hàm.
điểm gọi lại:
Hàm gắn liền với một widget và sẽ được gọi khi người dùng thực hiện thao tác.
phương thức hạn hẹp:
Phương thức gắn với một cá thể riêng.
lập trình hướng sự kiện:
Phong cách lập trình trong đó luồng thực hiện được quyết định bởi thao tác từ phía người dùng.
sự kiện:
Thao tác từ phía người dùng, như kích chuột, gõ phím để GUI phản hồi lại.
vòng lặp sự kiện:
Vòng lặp vô hạn để đợi thao tác từ phía người dùng và phản hồi.
item:
Yếu tố đồ họa trên widget kiểu Canvas.
hình bao:
Hình chữ nhật bao chứa một nhóm các item, thường được chỉ định bởi hai góc đối diện.
xếp:
Sắp xếp và hiển thị các yếu tố của GUI.
quản lí không gian:
Hệ thống giúp xếp các widget.
bó buộc:
Hình thức gắn kết giữa một widget, một sự kiện, và một chuôi nắm sự kiện. Chuôi nắm sẽ được gọi khi sự kiện xảy ra đối với widget.

Bài tập

Trong bài tập này, bạn sẽ viết một trình xem ảnh. Sau đây là một ví dụ đơn giản:

g = Gui() canvas = g.ca(width=300) photo = PhotoImage(file='danger.gif') canvas.image([0,0], image=photo) g.mainloop() 

PhotoImage đọc vào một file và trả lại một đối tượng PhotoImage mà Tkinter có thể hiển thị. Canvas.image đặt hình ảnh lên nền, căn giữa theo các tọa độ cho trước. Bạn cũng có thể đặt các hình lên nhãn, nút, và một số widget khác:

g.la(image=photo) g.bu(image=photo) 

PhotoImage chỉ có thể xử lý một số ít định dạng ảnh, như GIF and PPM, nhưng ta có thể dùng Python Imaging Library (PIL) để đọc các dạng file khác.

Tên của module trong PIL là Image, nhưng Tkinter đã định nghĩa một đối tượng cùng tên. Để tránh sự xung khắc này, bạn có thể dùng import...as như sau:

import Image as PIL import ImageTk 

Dòng đầu tiên nhập vào Image và đặt cho nó tên địa phương là PIL. Dòng thứ hai nhập vào ImageTk, vốn có thể chuyển một hình PIL thành dạng PhotoImage của Tkinter. Sau đây là một ví dụ:

image = PIL.open('allen.png') photo2 = ImageTk.PhotoImage(image) g.la(image=photo2) 
  1. Hãy tải về image_demo.py, danger.gifallen.png từ thinkpython.com/code. Chạy image_demo.py. Bạn có thể phải cài đặt PILImageTk. Chúng có thể đã sẵn có trong kho phần mềm của máy bạn, nhưng nếu chưa thì có thể tải được về từ pythonware.com/products/pil/.
  2. Trong image_demo.py, hãy đổi tên của PhotoImage thứ hai từ photo2 thành photo và chạy lại chương trình. Bạn cần thấy được PhotoImage thứ hai chứ không phải tấm thứ nhất.Vấn đề là khi bạn gán lại photo nó ghi đè lên tham chiếu đến PhotoImage thứ nhất, mà bản thân sau đó sẽ mất đi. Điều tương tự cũng xảy ra khi bạn gán một PhotoImage cho một biến địa phương; nó biến mất khi hàm kết thúc.Để tránh điều này, bạn phải lưu một tham chiếu cho mỗi PhotoImage mà bạn muốn giữ lại. Bạn có thể dùng một biến toàn cục, hoặc lưu lại các PhotoImage trong một cấu trúc dữ liệu hoặc dưới dạng thuộc tính của một đối tượng.

    Biểu hiện này có thể rất khó chịu, chính vì vậy mà tôi báo cho bạn biết (và hình dùng cho thí nghiệm này có chữ “Danger!”).

  3. Bắt đầu từ ví dụ này, hãy viết một chương trình để nhận vào tên của một thư mục và lặp qua tất cả các file, hiển thị những file mà PIL coi là hình ảnh. Bạn có thể dùng lệnh try để bắt những file mà PIL không nhận là ảnh.Khi người dùng kích chuột vào ảnh, chương trình cần phải hiển thị tấm tiếp theo.
  4. PIL có nhiều phương thức xử lý ảnh. Bạn có thể tham khảo toàn bộ tại pythonware.com/library/pil/handbook. Hãy chọn ra một số phương thức, đố bạn lập một GUI để áp dụng những phương thức đó lên tấm ảnh?

Bạn có thể tải về một lời giải đơn giản từ thinkpython.com/code/ImageBrowser.py.

Một trình đồ họa véc-tơ là một chương trình cho phép người dùng vẽ và chỉnh sửa những hình trên màn hình và tạo ra các file kết quả dưới dạng đồ họa véc-tơ như Postscript và SVG1.

Hãy viết một chương trình xử lý đồ họa véc-tơ bằng Tkinter. Ít nhất nó phải cho phép người dùng vẽ đường thẳng, hình tròn, hình chữ nhật, và phải dùng Canvas.dump để phát sinh bản mô tả Postscript cho nội dung trên Canvas.

Đố bạn lập trình cho phép người dùng chọn và thay đổi kích cỡ của các item trên Canvas.

Dùng Tkinter để viết một trình duyệt web đơn giản. Nó phải có một widget kiểu Text cho phép người dùng nhập vào một URL và một Canvas để hiển thị nội dung của trang web.

Bạn có thể dùng module urllib để tải về các file (xem Bài tập urllib) và module HTMLParser để tách các thẻ HTML (xem docs.python.org/lib/module-HTMLParser.html).

Ít nhất là trình duyệt mà bạn viết phải xử lý được văn bản chữ thuần túy và các liên kết (đường link). Đố bạn lập trình xử lý màu nền, các thẻ định dạng và hình ảnh.

2 bình luận

Filed under Think Python

2 responses to “Chương 19: Nghiên cứu cụ thể: Tkinter

  1. Pingback: Think Python: Cách tư duy như nhà khoa học máy tính | Blog của Chiến

  2. Pingback: Phụ lục: Lumpy | Blog của Chiến

Bình luận về bài viết này