2016年5月20日 星期五

Python 內建 GUI 模組 tkinter 測試 (一) : 建立視窗

最近因為玩樹苺派的關係, 接觸到 Python 內建的 GUI 開發模組 Tkinter (意思是 Tk Interface), 初步覺得比用 Java 的 Swing 還要來得容易, 因此就來學看看唄!

Tk 原先是為 Tcl 語言所開發的 GUI 套件, 因為是 Tcl 的第一個擴充, 所以現在都合起來稱呼為 Tcl/Tk. Tcl 是一種以 string-based 的跨平台工具命令式直譯語言 (Tool command language), 繼承了 LISP/C/Shell 等語言的優點, 並具有語法簡單, 容易擴展與可靈活嵌入其他語言的特點, 而且全面支持 unicode.

而 Tkinter 是 Python 內建的標準模組, 內部嵌入了 Tcl/Tk GUI 套件, 用來在 Python 中建構 GUI 圖形介面程式, 它具有如下優點 :
  1. 簡單易學 :
    比 Python 其他 GUI 要容易, 甚至於我覺得比學 Java Swing 還容易.
  2. 程式碼精簡 :
    以很短的程式碼便能產生強大功能的 GUI 程式.
  3. 跨平台 :
    同樣的程式可以在 Linux/Windows/Mac 等系統上執行.
不過在 Python 2 中的模組名稱 Tkinter 到 Python 3 版後已被改為小寫的 tkinter, 使用時要注意所用之 Python 版本, 匯入時注意該用首字大寫與否. 不過在 Python 2 下以 Tkinter 所寫的 GUI 程式可以利用 2to3 程式轉成 Python 3 版的程式, 詳見 :

https://docs.python.org/2/library/tkinter.html

在 Python 2 中使用 Tkinter 需先匯入模組 : 

import Tkinter



from Tkinter import *

但書裡說不建議使用後面這種方式, 因為此方法是匯入 Tkinter 所有函數與屬性, 這樣會占用較多記憶體, 而且容易讓命名空間混淆, 並使得 debug 難度增加. 不過, 這種方式使用起來較方便 (凡事皆有代價).

比較常見的匯入方式是幫 Tkinter 取個別名 :

import Tkinter as tk

注意, 在 Python 3 要用小寫的 tkinter :

import tkinter as tk

這樣就能使用 tk 這個別名來呼叫此模組內的函數 :

tk.函數名稱()

匯入模組就可以呼叫 Tk() 函數建立一個根視窗實體 :

import Tkinter as tk
root=tk.Tk()

如果沒有取別名, 就要用 Tkinter.Tk() :

import Tkinter
root=Tkinter.Tk()

如果是 Python3 模組名稱要用小寫, 但 Tk() 函數一樣是首字大寫 :

import tkinter
root=tkinter.Tk()

然後呼叫此視窗實體之 mainloop() 函數將此視窗加入事件監視迴圈, 這樣就會產生 GUI 視窗了. 注意, mainloop() 除了監視事件外, 也是維持視窗持續存在之方法 :

root.mainloop()

真是太簡單了, 建立一個空視窗只要三行程式碼!

如果用 Java Swing 來產生一個空視窗, 至少需要 10 行以上的程式碼 :

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;

public class JFrame1 {
  JFrame f;
  public static void main(String argv[]) {
    new JFrame1();
    }
  public JFrame1() {
    f=new JFrame("JFrame 1");
    f.setBounds(0,0,400,300);
    f.setVisible(true);
    }
  }

這就是為什麼 Tkinter 非常適合用來快速開發 GUI 程式的原因.

在 IDLE 介面輸入此三行程式, 馬上就建立一個視窗了 :

Python 2.7.6 (default, Nov 10 2013, 19:24:18) [MSC v.1500 32 bit (Intel)] on win32
Type "copyright", "credits" or "license()" for more information.
>>> import Tkinter as tk
>>> root=tk.Tk()
>>> root.mainloop()




注意, 視窗描繪是在加入事件監視函數 mainloop() 後才發生的唷!

此空視窗右上角有標準視窗的縮小, 放大, 以及關閉按鈕, 還可以拖曳調整視窗大小. 預設標題 (title) 是 "tk", 我們可以呼叫 title 函數來加以設定 :

>>> import Tkinter as tk
>>> root=tk.Tk()
>>> root.title("My First Tk GUI")
>>> root.mainloop()




如果不想讓使用者調整視窗大小, 可以呼叫 resizable(0, 0) 來禁止, 以下在 Win10 下使用 Python 3 測試 :

import tkinter as tk
root=tk.Tk()
root.title("My First Tk GUI")
root.resizable(0,0)
root.mainloop()




這樣不僅無法拖曳縮放視窗, 右上角的放大按鈕也被禁能了, 只能縮小與關閉. 此 resizable() 函數事實上是用來設定視窗左上角座標, 設為 0,0 表示無法縮放.

tkinter 提供了下列 21 種 GUI 元件, 稱為 widget 或 control, 所謂 widget 即 Window gadget 之意  :
  1. Label
  2. Button
  3. Radiobutton
  4. Checkbutton
  5. Entry
  6. Frame
  7. LabelFrame
  8. Listbox
  9. Text
  10. Message
  11. PanedWindow
  12. Scrollbar
  13. Scale
  14. Spinbox
  15. Menu
  16. OptionMenu
  17. Menubutton
  18. Canvas
  19. Image
  20. Bitmap
  21. Toplevel

下面來測試其中的 Button 與 Label 元件, 把下列程式用記事本存成 test.py :

import tkinter as tk
root=tk.Tk()     #建立視窗容器物件
root.title("Tk GUI")
label=tk.Label(root, text="Hello World!")   #建立標籤物件
label.pack()       #將元件放入容器
button=tk.Button(root, text="OK")
button.pack()     #將元件放入容器
root.mainloop()

然後在命令提示字元視窗執行 :

D:\Python\test>python test.py




可見在呼叫 pack() 函數做版面管理的話, tkinter 會將元件由上而下水平置中順序排版.

注意, 在建立 GUI 元件時, 第一個參數必須為其容器物件 (此處為 root 變數所代表之視窗), 格式如下 :

元件變數=元件名稱(容器物件變數, [元件選項]) 

其他為元件之選項參數, 例如標籤文字 (text), 大小(size), 邊框 (border), 前景顏色 (foreground), 或背景顏色 (background), 每一種元件可能有不同之選項, 可以在建立元件時直接設定, 也可以在建立之後呼叫 configure() 函數或其別名 config() 來設定.

其次, 所建立的元件必須利用版面管理員 (geometry manager) 於視窗容器中定位, 這樣它才會在視窗中顯現, 而此 pack() 函數就是一個版面管理員, 它會由上而下擺放元件.

如果是使用 star import 匯入方式, 上面程式的寫法要改為 :

from tkinter import *
root=Tk()
root.title("Tk GUI")
label=Label(root, text="Hello World!")
button=Button(root, text="OK")
label.pack()
button.pack()
root.mainloop()

ㄟ, 這個寫法好像比較簡單哩! 不用寫 tk. 如果只是寫個簡單的視窗程式或做測試這樣寫很簡便, 但如果寫比較複雜的系統就不宜用 star import 方式, 因為可能會使命名空間衝突導致 debug 變得很麻煩.

除了由上而下的 pack() 版面管理員外, 也可以呼叫 grid() 函數使用網格版面來管理元件 :

from tkinter import *
root=Tk()
root.title("Tk GUI")
label=Label(root, text="Hello World!")
button=Button(root, text="OK")
label.grid(column=0,row=0)
button.grid(column=1,row=0)
root.mainloop()




在呼叫 grid() 時, 我們將 label 放在 0 行 0 列, button 放在 1 行 0 列, 亦即這是一個 1x2 (1 列 2 行) 的網格, 因此元件是左右各排一個.

目前這個按鈕按下去不會有反應, 因為我們還沒有幫它設定事件處理函數. 在下面範例中, 我定義了一個按鈕事件處理函數 clickOK() 來處理 OK 鍵被按事件, 並設定了一個全域變數 count 來記錄 OK 鍵被按了幾次, 當 OK 被按時, count 會增量, 並且透過呼叫元件的 configure() 或 config() 函數來更改標籤元件的文字內容 (text 屬性) :

from tkinter import *
root=Tk()
root.title("Tk GUI")
label=Label(root, text="Hello World!")
count=0
def clickOK():
    global count
    count=count + 1
    label.configure(text="Click OK " + str(count) + " times")
button=Button(root, text="OK", command=clickOK)
label.pack()
button.pack()
root.mainloop()




注意, 事件處理函數與 GUI 元件之間是透過 command 參數來綁定的. 另外上面也用到了 Python 內建函數 str() 來將數值類型的 count 轉成字串類型.

上面範例中的按鈕看起來似乎不是那麼好看, 所以 Tkinter 後來又推出了加強版的 ttk 模組來美化元件的外觀, 這 ttk 意思是 Themed Tkinter, 亦即主題化版本, 參考 :

https://docs.python.org/2/library/ttk.html

此 ttk 模組包含了 17 種元件, 其中的 11 種是 Tkinter 原本已經有的 :
  1. Label
  2. Button
  3. Radiobutton
  4. Checkbutton
  5. Entry
  6. Frame
  7. Labelframe
  8. Menubutton
  9. Scale
  10. Scrollbar
  11. Panedwindow
另外 6 個是 ttk 推出的新元件 :
  1. Combobox
  2. Notebook
  3. Progressbar
  4. Separator
  5. Sizegrip
  6. Treeview
注意, ttk 須在匯入 tkinter 後才能匯入, 這樣才會蓋掉原來的 tkinter 元件 :

from tkinter import ttk     (建議)



from tkinter.ttk import *    (不建議)

將上面範例用 ttk 改寫如下 :

from tkinter import *
from tkinter.ttk import *
root=Tk()
root.title("ttk GUI")
label=Label(root, text="Hello World!")
count=0
def clickOK():
    global count
    count=count + 1
    label.config(text="Click OK " + str(count) + " times")
button=Button(root, text="OK", command=clickOK)
label.pack()
button.pack()
root.mainloop()




可見按鈕真的變漂亮了!

上面範例使用 star import 共用命名空間不容易 debug, 建議用下面寫法 :

import tkinter as tk
from tkinter import ttk

root=tk.Tk()
root.title("ttk GUI")
label=ttk.Label(root, text="Hello World!")
count=0
def clickOK():
    global count
    count=count + 1
    label.config(text="Click OK " + str(count) + " times")
button=ttk.Button(root, text="OK", command=clickOK)
label.pack()
button.pack()
root.mainloop()


Tk 模組有一個測試函數 _test(), 會顯示一個內建的測試視窗 :

Python 2.7.8 (default, Jun 30 2014, 16:03:49) [MSC v.1500 32 bit (Intel)] on win32
Type "copyright", "credits" or "license()" for more information.
>>> from Tkinter import *
>>> Tkinter._test()




呼叫 Tcl().eval() 函數可以查詢 Tk 版本 :

>>> Tkinter.Tcl().eval('info patchlevel')
'8.5.15'

我這是在 Python 2.7.6 上測試的, Tk 的版本是 8.5, 如果在 Python 3 上測試, Tk 版本應該是 8.6 版以上. 8.6 版在功能上比起 8.5 有大幅改進. Python 3 的測試函數寫法如下 :

Python 3.6.1 (v3.6.1:69c0db5, Mar 21 2017, 18:41:36) [MSC v.1900 64 bit (AMD64)] on win32
Type "copyright", "credits" or "license()" for more information.
>>> import tkinter as tk 
>>> tk._test() 




>>> tk.Tcl().eval('info patchlevel') 
'8.6.6'

可見 Python 3 使用的是 Tk 8.6 版.

用 Tkinter 模組寫好的 Python 程式可以用 py2exe 模組轉成 Windows 執行檔, 參考 :

將python轉成執行檔(py2exe)
http://www.py2exe.org/index.cgi/Tutorial
# Tk : Dialog window

建立 tkinter 元件時需傳入參數, 這些參數依元件而異, 但有一些與尺寸, 顏色, 字型或樣式有關的參數是共通的, 如下表所示 :

 長度參數 說明
 width 元件寬度
 height 元件高度
 wraplength 跳行長度
 padX 與鄰近元件之水平間隔
 padY 與鄰近元件之垂直間隔
 borderwidth 邊框寬度

 顏色參數 說明
 background (bg) 背景顏色
 foreground(fg) 前景顏色
 hightcolor 高亮度時顏色
 activebackground 致能時背景顏色 
 activeforeground 致能時前景顏色
 disablebackground 禁能時背景顏色
 disableforeground 禁能時前景顏色
 selectbackground 被選擇時背景顏色
 selectforeground 被選擇時前景顏色


其他參考 :

# 傻貓布落格 : Tkinter
# TutorialsPoint
# An Introduction to Tkinter
# Thinking in Tkinter
https://docs.python.org/3.1/library/tkinter.ttk.html

10 則留言 :

匿名 提到...

Hi 您好,
拜讀您的blogger真的受益良多,我目前在python上緩慢學習,
您的blogger解答我不少疑惑,目前有個問題想請教.
請問tkinter的button能夠直接直接執行.sh的指令嗎?
我目前有編輯一個.sh指令,希望藉由tkinter的button去執行它?
請問這可行嗎?

任何回答都非常的感謝: )

Tony Huang 提到...

這很有意思, 只是我現在還沒用到. 不過這應該是沒問題的 (很多 Python 高手早就想到了), 呼叫 subprocess() 函數即可辦到, 參考 :

https://docs.python.org/2/library/subprocess.html 或
http://stackoverflow.com/questions/25028717/run-a-shell-script-from-python-gui

vega ho 提到...

import os

os.system("你的 os shell 指令")

Tony Huang 提到...

感謝您的分享

David Su 提到...

您好,我再看一個網路課程上面也有提到做按鈕
但是他的好複雜喔,到底哪裡不同啊?
懇請解惑
他的做法如連結
https://imgur.com/a/W2gxAxp

非常感謝

Tony Huang 提到...

他是用 OOP 寫法啦! 如果應用程式要用到大量按鈕就會將製作按鈕功能寫成類別, 大型程式都會用 OOP, 小程式就不需要啦!

David Su 提到...

oop的好處是什麼呢

Tony Huang 提到...

OOP 好比是軟體積木或 IC, 將通用邏輯做成像積木一樣的東西, 要疊成各種形狀都可以. 好處主要是 :
1. 可維護性高
2. 可重複使用
3. 可擴充性高
4. 提高軟體開發效率與品質

David Su 提到...

好的,那我還是只好OOP了~(有點難記,希望後面習慣會好一點
非常感謝您的回應,受益良多

Tony Huang 提到...

Python 基礎語法中有 OOP 部分, 建議將函數與物件部分熟習之後再來學 TK 較順.