This is what you will build. A window, buttons, and a file that remembers which words are hard for you. นี่คือสิ่งที่คุณจะสร้าง หน้าต่าง ปุ่มกด และไฟล์ที่จำได้ว่าคำไหนยากสำหรับคุณ 这就是你要做的东西。一个窗口、几个按钮,还有一个记得哪些词对你来说难的文件。
🇬🇧 English
In Space Invaders you drove the program. Your while loop went round and round, and it decided when things happened.
An app is the other way round. The program sits still and waits. The person clicks, and only then does your code run.
That is the big idea on this page. We use tkinter, which comes inside Python. There is nothing to install.
🇹🇭 ไทย
ในเกม Space Invaders คุณเป็นคนขับโปรแกรม ลูป while วนไปเรื่อย ๆ และเป็นตัวตัดสินว่าอะไรจะเกิดขึ้นเมื่อไหร่
แต่แอปกลับกัน โปรแกรมจะอยู่นิ่ง ๆ แล้วรอ เมื่อมีคนคลิก โค้ดของคุณถึงจะทำงาน
นั่นคือแนวคิดใหญ่ของหน้านี้ เราใช้ tkinter ซึ่งมีอยู่ใน Python อยู่แล้ว ไม่ต้องติดตั้งอะไร
🇨🇳 中文
在 Space Invaders 里,是你在驱动程序。你的 while 循环一圈一圈转,由它决定什么时候发生什么。
程序则反过来。它安安静静地等着。有人点了,你的代码才跑。
这就是这一页最重要的想法。我们用 tkinter,它在 Python 里本来就有,不用装。
0 Get ready · เตรียมตัว · 做好准备
🇬🇧 English
Nothing to install. Check it works with one command — a small grey test window should open.
If you use Linux and it says no module named tkinter, install it with sudo apt install python3-tk. On Mac and Windows it is already there.
🇹🇭 ไทย
ไม่ต้องติดตั้งอะไร ลองตรวจด้วยคำสั่งเดียว จะมีหน้าต่างทดสอบสีเทาเล็ก ๆ เปิดขึ้นมา
ถ้าใช้ Linux แล้วขึ้นว่า no module named tkinter ให้ติดตั้งด้วย sudo apt install python3-tk ส่วน Mac กับ Windows มีมาให้อยู่แล้ว
🇨🇳 中文
什么都不用装。用一条命令检查一下,会弹出一个灰色的小测试窗口。
如果你用 Linux,提示 no module named tkinter,就用 sudo apt install python3-tk 装上。Mac 和 Windows 本来就有。
python3 -m tkinter
mkdir ~/Documents/Code/flashcards
cd ~/Documents/Code/flashcards
vim flashcards.py
1 A window that waits · หน้าต่างที่รออยู่ · 一个在等待的窗口
🇬🇧 English
Four lines make a real window.
mainloop() is the important one. It is a loop, like your game loop — but this one is not yours. It belongs to tkinter, and it waits for the person.
Nothing after mainloop() runs until the window closes. So put it last, always.
🇹🇭 ไทย
สี่บรรทัดก็ได้หน้าต่างจริงแล้ว
mainloop() คือตัวสำคัญ มันเป็นลูปเหมือนลูปเกมของคุณ แต่ลูปนี้ไม่ใช่ของคุณ มันเป็นของ tkinter และมันคอยรอผู้ใช้
โค้ดที่อยู่หลัง mainloop() จะไม่ทำงานจนกว่าหน้าต่างจะปิด ฉะนั้นให้วางไว้บรรทัดสุดท้ายเสมอ
🇨🇳 中文
四行就能做出一个真的窗口。
mainloop() 是最重要的一行。它是个循环,像你的游戏循环 —— 但这个不是你的,是 tkinter 的,它在等人来操作。
mainloop() 后面的代码,要等窗口关掉才会跑。所以永远把它放在最后。
import tkinter as tk
root = tk.Tk()
root.title("คำศัพท์ Flash")
root.mainloop()
2 A word on the card, big enough to read · คำบนบัตร ที่ใหญ่พอจะอ่านออก · 卡片上的字,要大到看得清
🇬🇧 English
A Label shows text. pack() puts it in the window.
Now a real problem. The normal size is 9. That is fine for a menu and far too small for a flashcard across a classroom.
Font names are also different on every computer. Leelawadee UI is on Windows. Thonburi is on Mac. Asking for a font that is not there does not crash — you just quietly get a different one.
So we ask the computer what it really has, and take the first one that fits.
🇹🇭 ไทย
Label ใช้แสดงข้อความ ส่วน pack() ใช้วางลงในหน้าต่าง
ทีนี้มาถึงปัญหาจริง ขนาดปกติคือ 9 ซึ่งพอดีสำหรับเมนู แต่เล็กเกินไปมากสำหรับบัตรคำที่ต้องอ่านข้ามห้องเรียน
ชื่อฟอนต์ก็ต่างกันในแต่ละเครื่อง Leelawadee UI อยู่บน Windows ส่วน Thonburi อยู่บน Mac ถ้าขอฟอนต์ที่ไม่มี โปรแกรมจะไม่พัง แต่คุณจะได้ฟอนต์อื่นมาเงียบ ๆ
เราจึงถามเครื่องว่ามีฟอนต์อะไรจริง ๆ บ้าง แล้วเลือกตัวแรกที่ใช้ได้
🇨🇳 中文
Label 用来显示文字,pack() 把它放进窗口。
现在遇到一个真问题。默认字号是 9。做菜单还行,但作为要在教室里隔着看的单词卡,太小了。
字体名字在每台电脑上也不一样。Leelawadee UI 在 Windows 上,Thonburi 在 Mac 上。要一个不存在的字体不会崩 —— 你只是悄悄拿到了别的字体。
所以我们问问这台电脑到底有什么,挑第一个能用的。
from tkinter import font as tkfont
def pick_font():
here = set(tkfont.families())
for name in ("Leelawadee UI", "Noto Sans Thai", "Thonburi", "Tahoma"):
if name in here:
return name
return tkfont.nametofont("TkDefaultFont").actual("family")
family = pick_font()
word = tk.Label(root, text="temple", font=(family, 34, "bold"))
word.pack(padx=40, pady=20)
print(family) and run it. Which font did your computer choose?
ลองเพิ่ม print(family) แล้วรันดู เครื่องของคุณเลือกฟอนต์ไหน
加一行 print(family) 跑一下。你的电脑选了哪个字体?
3 A button, and the mistake everyone makes · ปุ่ม กับความผิดพลาดที่ทุกคนเจอ · 按钮,和每个人都会犯的错
🇬🇧 English
A Button needs to know what to do when it is clicked. You tell it with command=.
Here is the mistake. Write command=flip() with brackets, and Python runs flip once, immediately, and hands the button the answer. The button then does nothing, for ever.
Write command=flip, with no brackets. You are giving the button the function itself, not the result of running it.
Brackets mean "run this now". You do not want it now. You want it later, when someone clicks.
🇹🇭 ไทย
Button ต้องรู้ว่าเมื่อถูกคลิกแล้วต้องทำอะไร คุณบอกมันด้วย command=
นี่คือจุดที่ผิดกันบ่อย ถ้าเขียน command=flip() มีวงเล็บ Python จะรัน flip ทันทีหนึ่งครั้ง แล้วส่งผลลัพธ์ให้ปุ่ม หลังจากนั้นปุ่มจะไม่ทำอะไรอีกเลยตลอดไป
ให้เขียน command=flip ไม่มีวงเล็บ คุณกำลังส่งตัวฟังก์ชันให้ปุ่ม ไม่ใช่ส่งผลลัพธ์จากการรันมัน
วงเล็บแปลว่า "รันเดี๋ยวนี้" แต่คุณไม่ได้อยากให้รันตอนนี้ คุณอยากให้รันทีหลัง ตอนที่มีคนคลิก
🇨🇳 中文
Button 需要知道被点的时候要做什么。你用 command= 告诉它。
错就错在这里。写 command=flip() 带括号,Python 会立刻把 flip 跑一次,然后把返回值交给按钮。之后这个按钮就永远什么都不做了。
要写 command=flip,不带括号。你是把函数本身交给按钮,不是把运行结果交给它。
括号的意思是"现在就跑"。但你不想现在跑,你想等有人点了再跑。
def flip():
answer.config(text="วัด") # change what a Label says
answer = tk.Label(root, text="?", font=(family, 26))
answer.pack(pady=10)
tk.Button(root, text="Show", width=10, command=flip).pack(pady=8)
# ^^^^ no () here
command=flip() on purpose. The answer shows before you click anything, and the button is dead. Now take the brackets off.
ลองเขียน command=flip() ดูสักครั้ง คำตอบจะโผล่มาก่อนที่คุณจะคลิก และปุ่มจะกดไม่ติด แล้วค่อยเอาวงเล็บออก
故意写成 command=flip()。答案在你点之前就出来了,按钮也死了。然后再把括号去掉。
4 Many words, and a score · หลายคำ และคะแนน · 很多词,还有分数
🇬🇧 English
One card is not a trainer. Keep a list of words, and remember which one you are on.
The buttons only change numbers and text. The list is the real app; the window just shows it.
We put it all in a class, so the list, the number and the labels live together instead of floating loose.
🇹🇭 ไทย
บัตรใบเดียวยังไม่ใช่เครื่องฝึก ต้องเก็บลิสต์ของคำไว้ และจำว่าตอนนี้อยู่ใบไหน
ปุ่มแค่เปลี่ยนตัวเลขกับข้อความ ลิสต์ต่างหากคือตัวแอปจริง ๆ ส่วนหน้าต่างแค่แสดงผลออกมา
เราเอาทุกอย่างใส่ไว้ในคลาส เพื่อให้ลิสต์ ตัวเลข และ label อยู่ด้วยกัน แทนที่จะกระจัดกระจาย
🇨🇳 中文
一张卡片还算不上训练器。要存一个词的列表,还要记住现在到第几张了。
按钮只是改改数字和文字。列表才是真正的程序,窗口只负责显示。
我们把这些都放进一个类里,让列表、数字和标签待在一起,而不是到处乱飘。
STARTER_WORDS = [
{"en": "temple", "th": "วัด", "misses": 0},
{"en": "mountain", "th": "ภูเขา", "misses": 0},
{"en": "market", "th": "ตลาด", "misses": 0},
]
def show_card(self):
card = self.words[self.index]
self.word.config(text=card["en"])
self.answer.config(text="?")
self.score.config(text="Right %d Missed %d Card %d of %d"
% (self.right, self.wrong, self.index + 1, len(self.words)))
5 Lay it out with grid · จัดวางด้วย grid · 用 grid 排版
🇬🇧 English
pack() stacks things top to bottom. That is fine until you want three buttons in a row.
grid() works like a table: you say which row and which column. columnspan makes something stretch across several columns.
Pick one and stay with it. Mixing pack() and grid() in the same window makes tkinter freeze with no error at all. That one costs people an hour.
🇹🇭 ไทย
pack() จะวางของเรียงจากบนลงล่าง ซึ่งก็ดี จนกว่าคุณจะอยากได้ปุ่มสามปุ่มเรียงกันในแถวเดียว
grid() ทำงานเหมือนตาราง คุณบอกว่าอยู่ row ไหน column ไหน ส่วน columnspan ทำให้ของกินพื้นที่หลายคอลัมน์ได้
เลือกอย่างใดอย่างหนึ่งแล้วใช้ให้ตลอด การผสม pack() กับ grid() ในหน้าต่างเดียวกันจะทำให้ tkinter ค้างไปเฉย ๆ โดยไม่มี error เรื่องนี้กินเวลาคนไปเป็นชั่วโมง
🇨🇳 中文
pack() 会把东西从上往下堆。这挺好,直到你想让三个按钮排成一行。
grid() 像表格:你说清楚是第几 row、第几 column。columnspan 让一个东西横跨好几列。
选一个,然后一直用它。在同一个窗口里混用 pack() 和 grid(),tkinter 会直接卡死,而且一点错误信息都没有。这个坑能让人搭进去一小时。
self.word.grid(row=0, column=0, columnspan=3, padx=30, pady=(26, 6))
self.answer.grid(row=1, column=0, columnspan=3, pady=(0, 14))
self.show_btn.grid(row=2, column=0, padx=6, pady=6)
self.knew_btn.grid(row=2, column=1, padx=6, pady=6)
self.miss_btn.grid(row=2, column=2, padx=6, pady=6)
6 Remember it tomorrow · จำไว้ถึงพรุ่งนี้ · 明天还记得
🇬🇧 English
Close the app and everything is gone. Save the list to a file instead. JSON is a text file that looks like Python.
One thing matters a lot here: ensure_ascii=False. Without it, Python writes Thai as วัด. It still works, but nobody can read or fix the file by hand.
Save every time an answer is given. It costs nothing and it never loses work.
🇹🇭 ไทย
พอปิดแอป ทุกอย่างก็หายไป ให้บันทึกลิสต์ลงไฟล์แทน JSON คือไฟล์ข้อความที่หน้าตาคล้าย Python
มีจุดหนึ่งสำคัญมาก คือ ensure_ascii=False ถ้าไม่ใส่ Python จะเขียนภาษาไทยเป็น วัด ซึ่งยังใช้งานได้ แต่ไม่มีใครอ่านหรือแก้ไฟล์ด้วยมือได้เลย
บันทึกทุกครั้งที่มีการตอบ ไม่เปลืองอะไร และไม่มีวันทำงานหาย
🇨🇳 中文
一关程序,东西全没了。改成把列表存到文件里。JSON 是一种长得很像 Python 的文本文件。
这里有一点很重要:ensure_ascii=False。不加的话,Python 会把泰文写成 วัด。虽然还能用,但没人能用手读或改这个文件了。
每答一次就存一次。不花什么代价,而且永远不会丢东西。
import json
from pathlib import Path
WORDS_PATH = Path(__file__).with_name("words.json")
def save_words(words):
WORDS_PATH.write_text(json.dumps(words, ensure_ascii=False, indent=2),
encoding="utf-8")
def load_words():
if WORDS_PATH.exists():
return json.loads(WORDS_PATH.read_text(encoding="utf-8"))
return [dict(w) for w in STARTER_WORDS]
words.json in your editor. You can read your own Thai in there.
ลองตอบสักสองสามใบ ปิดแอป แล้วเปิด words.json ใน editor คุณจะอ่านภาษาไทยของตัวเองในนั้นได้
答几张卡,关掉程序,用编辑器打开 words.json。你能在里面读到自己写的泰文。
7 Hard words come back sooner · คำที่ยากจะกลับมาบ่อยกว่า · 难的词更快再出现
🇬🇧 English
Now the app becomes better than paper cards. Count every miss. Then show the worst words first.
One line does it. The minus sign in -w["misses"] sorts from big to small.
This is the whole idea behind every serious vocabulary app. You just wrote it.
🇹🇭 ไทย
ตอนนี้แอปเริ่มดีกว่าบัตรกระดาษแล้ว ให้นับทุกครั้งที่ตอบผิด แล้วเอาคำที่แย่ที่สุดขึ้นมาก่อน
ใช้บรรทัดเดียว เครื่องหมายลบใน -w["misses"] ทำให้เรียงจากมากไปน้อย
นี่คือแนวคิดเบื้องหลังแอปท่องศัพท์จริงจังทุกตัว และคุณเพิ่งเขียนมันเอง
🇨🇳 中文
现在这个程序开始比纸卡片强了。把每次答错都记下来,然后先出最难的词。
一行就够。-w["misses"] 里的负号让它从大到小排。
所有正经背单词软件背后就是这个想法。你刚刚把它写出来了。
def missed(self):
self.wrong += 1
self.words[self.index]["misses"] += 1
save_words(self.words)
self.words.sort(key=lambda w: -w["misses"]) # worst first
self.index = (self.index + 1) % len(self.words)
self.show_card()
8 The whole app · ทั้งหมดรวมกัน · 完整的程序
🇬🇧 English
Here is the finished file. It also has a box where you can add your own words, typed as cat=แมว.
Give it to a friend. They only need Python. Nothing else at all.
🇹🇭 ไทย
นี่คือไฟล์ที่เสร็จแล้ว มีช่องให้เพิ่มคำของคุณเองด้วย พิมพ์แบบ cat=แมว
ส่งให้เพื่อนได้เลย เขาต้องมีแค่ Python ไม่ต้องมีอะไรอย่างอื่นเลย
🇨🇳 中文
这是做完的文件。里面还有一个输入框,可以加自己的词,格式是 cat=แมว。
发给朋友吧。他们只需要有 Python,别的什么都不用。
cd ~/Documents/Code/flashcards
python3 flashcards.py
9 Now change it · ลองแก้ดู · 现在动手改它
🇬🇧 English
The app is yours. Most of these are one line, or one new small function.
🇹🇭 ไทย
แอปนี้เป็นของคุณแล้ว ส่วนใหญ่แก้แค่บรรทัดเดียว หรือเพิ่มฟังก์ชันเล็ก ๆ หนึ่งตัว
🇨🇳 中文
这个程序是你的了。下面大部分只要改一行,或者加一个小函数。
| Try this | How | Level |
|---|---|---|
| Show Thai first instead | Swap card["en"] and card["th"] in show_card. | easy |
| Bigger letters | One number: (family, 34, "bold"). | easy |
| Mix the cards up | import random, then random.shuffle(self.words). | easy |
| A Delete button | Remove self.words[self.index], then save. | harder |
| Use the keyboard | root.bind("<space>", lambda e: self.flip()). | harder |
| Share one word list | Keep the words in SQLite instead of JSON. See the homework app. | harder |