iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 22
1
Software Development

從零開始學Python系列 第 22

[Day 22] 從零開始學Python - 圖形化使用者介面Tkinter:直到現在,我還默默的等待

  • 分享至 

  • twitterImage
  •  

註:本文同步刊載在Medium,若習慣Medium的話亦可去那邊看呦!

上次的練習我們就不驗收了,相信大家應該可以做得好的XD!
今天我們來聊聊Python的圖形化使用者介面。
如果讀者手上有一些Python的入門書,
相信在這個單元之前,常常會看到一個烏龜的範例(Turtle模組),
那就是一種會顯示圖形化的介面其中一種。
通常我們會將有圖形化的互動介面稱為GUI(Graphical User Interface)介面,
在Python當中如果算上其他的GUI介面的話,
其實相當多種,最常見的有Tkinter/PyQT/WxPython/Kivy等,
如果就一般人而言,沒有特殊需求的話,
使用內建的Tkinter模組作為開頭應該就很足夠囉!

每個tkinter的主體會先由tkinter.Tk()的物件生成,
通常讀者們應該會看到大部分的範例會將tkinter縮寫成tk
並將生成的物件命名為window或win(代表這個視窗介面),
後續的操作也都跟其息息相關。

>>> import tkinter as tk
>>> win = tk.Tk() # 如果使用直譯器的話,在這行Enter後就會先看到一個視窗了!
>>> win.title('從零開始學Python:第二件X折?') # 更改視窗的標題
''
>>> win.geometry('800x400') # 修改視窗大小(寬x高)
''
>>> win.resizable(False, False) # 如果不想讓使用者能調整視窗大小的話就均設為False
''
>>> win.iconbitmap('unicorn.ico') # 更改左上角的icon圖示
''
>>> win.mainloop() # 在一般python xxx.py的執行方式中,呼叫mainloop()才算正式啟動

(註:下面的操作我們都先在直譯器中嘗試,請先不要下mainloop()以免無法再輸入指令)
我們在上面的示範中使用了一個icon檔,是一隻打瞌睡的獨角獸:
https://www.flaticon.com/free-icon/unicorn_3468081
但下載下來的時候是png檔,而使用圖示只會接受icon bitmap的格式,
使用者可以利用上一篇教的PIL來進行轉換:

>>> from PIL import Image
>>> img = Image.open('unicorn.png')
>>> img.save('unicorn.ico')

或者,也可以用另一個iconphoto()方法
它可以接受一些其他類型的圖片(png可以,但jpg不行)

>>> win.iconphoto(True, tk.PhotoImage(file='./unicorn.png'))

接下來讓我們介紹Frame。Frame可以想成是一個區塊
用來切分視窗以方便進行排版,
舉例來說,如果我們想要在上面塞一個寬度800,高度為100的Frame:

# bg可以用顏色名稱,或者使用'#FF0000'的RGB形式來給定
>>> fm = tk.Frame(win, bg='violet', width=800, height=100)
>>> fm.pack() # 設定Frame的排版方法,這行完成後才會在視窗上顯現

https://ithelp.ithome.com.tw/upload/images/20201004/20119871NsybvK8yn5.jpg
(註:排版方法有pack(), grid(), place()三種,若讀者有興趣可再自行深入研究。)

再來是按鈕Button的部分,值得注意的是,
我們可以運用textvariable的參數來給定tk.StringVar變數
用來讓我們可以動態調整按鈕上的字的變化。

>>> def calculate():
...     btnstr.set('計算中...')
...
>>> btnstr = tk.StringVar() # 初始化tk的字串變數
>>> btnstr.set('按我計算')
>>> btn = tk.Button(win, bg='violet', fg='white', textvariable=btnstr, font=('微
軟正黑體', 20), command=calculate)
>>> btn.pack()

在上面的例子中,我們初始化了一個btnstr的變數,
並且將其在開一個Button變數時,作為文字變數(StringVar)給定進去;
同時,當按鈕被按下時,calculate這個函式將被呼叫,
從而將按鈕上的字進行改變。
tk的變數有StringVar, IntVar, DoubleVar等三種,
其使用方式是set()用以設定變數,get()用以取得變數內容

如果我們單純想要顯示文字,不想被使用者修改的話,
應該要使用文字標籤(Label)
如果是要被輸入的文字方塊的話,
則可以使用文字輸入欄(Entry)
想要使用單選的選項的話,
則可以使用單選按鈕(Radiobutton)
想要跳出提示方塊的話,
就要使用對話框(Messagebox)

由於這當中嘗試一些排版和顏色太過複雜,
我們直接上最後的程式碼給讀者,
請一段一段沿著往下讀,
每一段可以先貼到直譯器上跑,
可以看到對應的元件生成的樣貌,
這樣會更為清楚每一段在作什麼呦!

import tkinter as tk
import tkinter.messagebox as msg # messagebox要另行匯入,否則會出錯。

### calculate函式用來接受按鈕事件,取得商品價格及折扣方式,計算後輸出
def calculate():
    # 每次要計算時,都讓按鈕顯示計算哪一個折扣法
    btnstr.set('計算' + radiostr.get() + '中')
    # 用try...except...的方式來避免掉輸入的不是數字
    try:
        choice = radiostr.get() # radiobutton的選擇項
        p1 = int(price1.get())
        p2 = int(price2.get())
        if p1 <= 0 or p2 <= 0: # 如果輸入<=0的數字也當成例外處理
            raise Exception()
        if choice == '一': # 買二件88折
            p = 0.88 * (p1 + p2)
        elif choice == '二': # 第二件6折
            p = p1 + p2 * 0.6
        else: # 第二件半價
            p = p1 + p2 * 0.5
        total.set(str(int(p))) # 顯示總額
    except:
        # 例外處理:先全部歸0,再跳提示視窗
        price1.set('0')
        price2.set('0')
        total.set('0')
        msg.showerror('輸入錯誤!', '請輸入正確的數字!')
    btnstr.set('算選項' + radiostr.get())


### select會在選中了某個折扣的時候被呼叫,此時會再呼叫calculate快速計算比較方便
def select():
    btnstr.set('算選項' + radiostr.get())
    calculate()

#### 主視窗生成
win = tk.Tk()
win.title('從零開始學Python:第二件X折?')
win.geometry('800x220')
win.resizable(False, False)
win.iconbitmap('unicorn.ico')

### Frame fm_cal: 放計算按鈕、"總額"文字label、總額金額顯示label
fm_cal = tk.Frame(win, bg='skyblue', width=800, height=100)
# fill表示沒填滿的部分是否填滿,BOTH表示xy方向都填滿,讀者可以試試看去掉的差別
fm_cal.pack(fill=tk.BOTH) 

btnstr = tk.StringVar() # 初始化tk的字串變數
btnstr.set('按我計算')
btn = tk.Button(fm_cal, bg='#71C973', fg='white', textvariable=btnstr, font=('微軟正黑體', 20), command=calculate, pady=10) # pad是指兩個元件之間空出多少距離
# side代表排版對齊時跟上個元件從哪個方向開始對齊
btn.pack(side=tk.LEFT, padx=10, pady=10) # padx/pady分別就是x方向跟y方向

lbl_text = tk.Label(fm_cal, bg='#F95E62', fg='white', 
               text='總額:', font=('微軟正黑體', 20), 
               padx=10, pady=10)
lbl_text.pack(side=tk.LEFT, padx=108)

total = tk.StringVar() # 初始化tk的字串變數
total.set('0')
lbl_total = tk.Label(fm_cal, bg='#F95E62', fg='white', 
               text='0', textvariable=total, font=('微軟正黑體', 20), 
               padx=10, pady=10)             
lbl_total.pack(side=tk.LEFT, padx=57, pady=10)

### Frame fm_lbl: 放標籤及Radiobutton(折數)
fm_lbl = tk.Frame(win, bg='#FF9955', width=800, height=150)
fm_lbl.pack(side=tk.TOP, fill=tk.BOTH)

lbl1 = tk.Label(fm_lbl, bg='#F95E62', fg='white', 
               text='第一件價格', font=('微軟正黑體', 20), 
               padx=10, pady=10)
lbl1.pack(side=tk.LEFT, padx=10, pady=10)
lbl_plus = tk.Label(fm_lbl, bg='#FF9955', fg='black', 
               text='及', font=('微軟正黑體', 20), 
               padx=10, pady=10)
lbl_plus.pack(side=tk.LEFT, padx=10)
lbl2 = tk.Label(fm_lbl, bg='#F95E62', fg='white', 
               text='第二件價格', font=('微軟正黑體', 20), 
               padx=10, pady=10)               
lbl2.pack(side=tk.LEFT, padx=10)
lbl_plus2 = tk.Label(fm_lbl, bg='#FF9955', fg='black', 
               text='及', font=('微軟正黑體', 20), 
               padx=10, pady=10)
lbl_plus2.pack(side=tk.LEFT, padx=10)
lbl_coupon = tk.Label(fm_lbl, bg='#F95E62', fg='white', 
               text='折扣', font=('微軟正黑體', 20), 
               padx=10, pady=10)
lbl_coupon.pack(side=tk.LEFT, padx=10)

### Frame fm_rad: 放Radiobutton(框一組自己對齊)
fm_rad = tk.Frame(fm_lbl, bg='#FF9955', width=150, height=150, padx=30)
fm_rad.pack(side=tk.LEFT, fill=tk.BOTH)

# StringVar的初始化值在第二個參數,第一個要填None
radiostr = tk.StringVar(None, '一')
# command會對應到選取時呼叫的函式,同時當選擇到它,value的值會放入variable的變數
r1 = tk.Radiobutton(fm_rad, bg='#FF9955', text='買兩件88折', variable=radiostr, value='一', command=select)
r1.pack(anchor=tk.W) # 另一個對齊方式,由上而下,但上下之間是靠左對齊
r2 = tk.Radiobutton(fm_rad, bg='#FF9955', text='第二件6折', variable=radiostr, value='二', command=select)
r2.pack(anchor=tk.W)
r3 = tk.Radiobutton(fm_rad, bg='#FF9955', text='第二件半價', variable=radiostr, value='三', command=select)
r3.pack(anchor=tk.W)

### Frame fm_ent: 放entry(輸入兩件商品分別的價格)
fm_ent = tk.Frame(win, width=800, height=200)
fm_ent.pack(side=tk.TOP, fill=tk.BOTH)

# ent1對應到price1
price1 = tk.StringVar(None, '0')
ent1 = tk.Entry(fm_ent, width=20, justify='center', textvariable=price1)
ent1.pack(side=tk.LEFT, padx=17, pady=7, fill=tk.Y)

# 為了排版增加的空白label
lbl_empty = tk.Label(fm_ent, 
               text=' ', font=('微軟正黑體', 20), 
               padx=20)
lbl_empty.pack(side=tk.LEFT)

# ent1對應到price2
price2 = tk.StringVar(None, '0')
ent2 = tk.Entry(fm_ent, width=20, justify='center', textvariable=price2)
ent2.pack(side=tk.LEFT, padx=17, pady=7, fill=tk.Y)

# 開始整個主程式
win.mainloop()

最後的成果大概像這樣子:
https://ithelp.ithome.com.tw/upload/images/20201004/20119871jj2Viu3V4m.jpg

輸入錯誤時的對話框:
https://ithelp.ithome.com.tw/upload/images/20201004/20119871OGaappLjL9.jpg

這當中其實還有很多沒有講到的東西和元件,
像是Listbox/Scrollbar等。
但實在太多可以用的了,所以我們就先不管啦!
有興趣的讀者可以再行深入玩玩看其他的元件呦XD!

那麼今天也來一點簡單的練習吧!

  1. 我們知道其實這個程式可能存在一些漏洞,
    最顯眼的應該就是在屈X氏的第二件O折這種事情,
    第二件一般都應該要取價低者計算才對。
    請想辦法修改上面的程式,使得當使用者有正常輸入數字,
    碰到上述狀況時,主動將價格排列正確,
    並同樣計算出正確的結果。

  2. (Optional,可以不做)事實上,
    tkinter也有人撰寫可以在GUI界面拉出整個排版的程式,
    請有興趣的讀者在GitHub(https://github.com/)上進行搜尋
    (例如"tkinter gui")
    並找到一個適合你使用的介面,嘗試操作看看。

那麼,我們就明天見囉!


上一篇
[Day 21] 從零開始學Python - 基本圖形處理Pillow:花下是誰對影成雙
下一篇
[Day 23] 從零開始學Python - 資料結構模組deque:旁人來來去去像行雲流水
系列文
從零開始學Python30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言