2018年10月9日 星期二

Python 內建 GUI 模組 tkinter 測試 (二) : 對話框

自 2016 年開始接觸 Python 內建的 tkinter GUI 模組後只做了初步測試就而停下來了, 因為還有其他研究項目在忙, 而且也一直沒有實際需求驅動我去學習桌面應用程式, 工作上需要用 GUI 時我都用 Web 方式解決. 但 Web 存在一些問題, 例如存取本地檔案以及瀏覽器版本對 Javascript 的支持等等. 最近因玉蓉學姊說處理大量語音資料的 project 程式無法在新電腦的 Edge 瀏覽器執行, 所以才認真考慮用 Python 來改寫, 順便把 tkinter 模組徹底測試一番.

tkinter 是 Python 內建模組, 無須安裝即可使用, 它提供了 22 種元件 (widge), 另外其主題化模組 (themed) tkinter.ttk 則提供 17 種美化的元件, 美學至上的我當然要使用增強版的 ttk. 注意, tkinter 在 Python 2.x 之模組名稱是首字母大寫的 Tkinter, 但在 Python 3 則改為全小寫的 tkinter, 由於 Python 3 日漸普及, 故以下測試均使用 Python 3.

本系列之前的文章參考 :

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

本篇測試參考了下面幾本書 :

# Python GUI 設計活用 tkinter 之路王者歸來 (深石, 洪錦魁)
# Python 程式設計學習經典:工程分析x資料處理x專案開發 (碁峰, 黃立政等, 第 9 章)
# 科學運算 : Python 程式理論與應用 (上奇, 楊珮璐等, 第 12 章)
# Tkinter GUI Application Development Blueprints (Packt, Bhaskar Chaudhary)
# Tkinter GUI Application Development Hotshot (Packt, Bhaskar Chaudhary)
# Python GUI Programming Cookbook (Packt, Burkhard A. Meier)
# Python and Tkinter Programming (Manning, John E. Grayson)
Python GUI programming with Tkinter (Packt, Alan D Moore)
# Tkinter GUI Programming by Example (Packt, David Love)

tkinter 的說明文件參考 :

https://pythonspot.com/en/tkinter/
Graphical User Interfaces with Tk

在上一篇文章中對 Tkinter 的頂級視窗 (root window) 做了簡單測試, 建立一個 Tkinter GUI 視窗的程式只要先後匯入 tkinter 以及旗下的元件美化子模組 tkinter.ttk 即可, 最基本的 Tkinter GUI 程式架構模版如下 :




#1. 匯入模組與類別
import tkinter as tk
from tkinter import ttk

#2. 定義元件之事件處理函數
def event_handler():
    pass

#3. 建立最上層視窗, 設定標題與大小
root=tk.Tk()
root.title("ttk GUI")
root.geometry("400x300")

#4. 加入 tk/ttk 元件並指定事件處理函數
ttk.Button(root, text="OK", command=event_handler).pack()

#5. 啟始事件迴圈顯示視窗
root.mainloop()

注意, 為了避免汙染 (polute) 程式目前的命名空間, 盡量不要使用方便的 star import, 雖然那樣可以直接呼叫 tkinter 模組裡面的方法, 但很容易因為方法覆蓋 (method overwritting) 使得除錯困難, 應該直接使用類別名稱, 為了方便可使用簡單之別名, 例如 tkinter 使用別名 (alias) tk.

對話框是 GUI 應用程式最常用的介面之一,  用來輸出訊息或讀取使用者輸入之資料. Python 3 的 tkinter 模組提供了三種常用對話框子模組 (sub-module) :

 tkinter 對話框類別 說明
 messagebox 提供訊息輸出與確定, 取消, 是, 否等按鈕回應功能 (輸出)
 simpledialog 提供輸入框與確定, 取消等按鈕回應功能 (輸入)
 filedialog 提供檔案開啟與儲存對話框功能

注意, 在 Python 2 時, 對話框用的是 TkMessageBox; 但在 Python 3 則改為 messagebox, 但方法部分則大致維持不變. Tkinter 訊息框的文件參考 :

https://pythonspot.com/tk-message-box/


一. 使用 messagebox 對話框輸出訊息 :

messagebox 是一種 modal (強制) 對話框, 亦即它會暫停目前的 GUI 應用程式, 並且獨佔所有桌面的控制權, 除非關閉或完成對話框, 其他 GUI 應用程式無法操作. messagebox 提供了 8 種方法來產生訊息輸出對話框, 其介面如下 :

messagebox.functionName(title, message [, detail])

第一參數 title 是對話框的標題, 第二參數 message 與備選的第三參數 detail 都是要輸出的訊息, message 主要是用作簡短之訊息摘要; 而 detail 則是用作詳細說明, 在某些平台 message 會自動以粗體顯示, 通常簡短之訊息只要用到 message 參數即可. 注意, 前兩個參數是必要參數, 不需要使用參數名稱; 但 detail 是備選參數必須指定參數名稱, 例如 detail="this is a detail info".


 messagebox 方法 說明 按鈕 圖示 回傳值
 showinfo(title, massage [, detail]) 通知對話框 確定 i "ok"
 showwarning(title, massage [, detail]) 警告對話框 確定 ! "ok"
 showerror(title, massage [, detail]) 錯誤對話框 確定 x "ok"
 askyesno(title, massage [, detail]) 詢問對話框 是/否 ? True/False
 askokcancel(title, massage [, detail]) 詢問對話框 是/取消 ?  True/None
 askyesnocancel(title, massage[, detail]) 詢問對話框 是/否/取消 ? True/False/None
 askquestion(title, massage[, detail]) 詢問對話框 是/否 ? "yes"/"no"
 askretrycancel(title, massage[, detail]) 詢問對話框 重試/取消 ? True/False


參考 :

tkinter showinfo python 3
https://pythonspot.com/tk-message-box/

使用這些方法產生對話框之前須先從 tkinter 模組匯入 messagebox 子模組, 可以用 as 定義別名較為精簡 :

import tkinter as tk 
from tkinter import messagebox as mb 

注意, 由於 messagebos 是子模組, 必須明確指定上層模組名稱 tkinter, 不可以使用 tkinter 的別名 tk, 例如下面這種匯入方式是錯誤的, 會找不到 tk 模組 :

>>> import tkinter as tk
>>> from tk import messagebox as mb
Traceback (most recent call last):
  File "<pyshell>", line 1, in <module>
ModuleNotFoundError: No module named 'tk'

直接呼叫 messagebox 的方法會自動建立一個 Tk 最上層視窗物件, 然後再彈出一個 modal 視窗, 例如 :


測試 1 : 直接呼叫 messagebox 之方法

import tkinter as tk
from tkinter import messagebox as mb

r=mb.showinfo("info", "this is message", detail="this is detail")
print(r)
print(type(r))




關閉對話框後才會執行 print(), 不管是按右上角的 X 鈕還是按 "確定" 鈕, showinfo() 的傳回值都是 "ok" 字串 :

>>> %Run test.py
ok
<class 'str'>


一般 GUI 應用需先建立一個 Tk 最上層視窗, 然後於元件的事件處理函數中再呼叫 messagebox 的方法來產生對話框, 例如 :


測試 2 : 使用按鈕觸發事件建立對話框 

import tkinter as tk
from tkinter import ttk
from tkinter import messagebox 

def event_handler():
    messagebox.showinfo("Info", "Hello World!")

root=tk.Tk()
root.title("ttk GUI")
button=ttk.Button(root, text="Hello World!", command=event_handler)
button.pack()
root.mainloop()




此範例中由於沒有幫 messagebox 取別名, 因此必須直接使用 messagebox 參考. 此例在視窗中放置一個按鈕 button, 並指定事件處理函數 event_handler() 來處理按鈕事件, 當按下按鈕時呼叫 messagebox 的 showinfo() 來產生一個訊息對話框.

下面範例 3 在視窗中放置 8 個按鈕, 分別觸發 messagebox 的 8 個方法 :


測試 3 : 使用按鈕觸發事件建立 8 種對話框

import tkinter as tk
from tkinter import ttk
from tkinter import messagebox as mb

def showinfo_handler():
    mb.showinfo("showinfo", "This is showinfo!")
def showwarning_handler():
    mb.showwarning("showwarning", "This is showwarning!")
def showerror_handler():
    mb.showerror("showerror", "This is showerror!")
def askyesno_handler():
    mb.askyesno("askyesno", "This is askyesno!")
def askokcancel_handler():
    mb.askokcancel("askokcancel", "This is askokcancel!")
def askyesnocancel_handler():
    mb.askyesnocancel("askyesnocancel", "This is askyesnocancel!")
def askquestion_handler():
    mb.askquestion("askquestion", "This is askquestion!")
def askretrycancel_handler():
    mb.askretrycancel("askquestion", "This is askretrycancel!")

root=tk.Tk()
root.title("messagebox 測試")
ttk.Button(root, text="showinfo", command=showinfo_handler)\
    .grid(row=0, column=0)
ttk.Button(root, text="showwarning", command=showwarning_handler)\
    .grid(row=0, column=1)
ttk.Button(root, text="showerror", command=showerror_handler)\
    .grid(row=0, column=2)
ttk.Button(root, text="askyesno", command=askyesno_handler)\
    .grid(row=0, column=3)
ttk.Button(root, text="askokcancel", command=askokcancel_handler)\
    .grid(row=1, column=0)
ttk.Button(root, text="askyesnocancel", command=askyesnocancel_handler)\
    .grid(row=1, column=1)
ttk.Button(root, text="askquestion", command=askquestion_handler)\
    .grid(row=1, column=2)
ttk.Button(root, text="askretrycancel", command= askretrycancel_handler)\
    .grid(row=1, column=3)
root.mainloop()





比較長的訊息可以存在字串變數中以提高可讀性, 也可以使用 \n 控制跳行, 例如 :


測試 4 : 使用字串變數儲存訊息

import tkinter as tk
from tkinter import messagebox as mb

url="http://www.myweb.com/renewpassword"
message="Authentification Failed!"
detail="Your account or password is not valid. Please try again or visit "\
       "\n{}".format(url)
r=mb.showinfo("info", message, detail=detail)




注意, 對話框沒有卷軸功能, 因此若字串太長或太多會使對話框大小增大, 甚至超過螢幕大小導致操作困難. 有卷軸的對話框需使用 Canvas, Frame, ScrollBar 等元件來客製化, 在 "Tkinter GUI Programming by Example" 照本書的第 8 章有探討.


二. 使用 simpledialog 對話框讀取使用者輸入資料 :

在命令列介面讀取單一輸入使用 input() 函數; 在 GUI 介面則可使用 tkinter.simpledialog 類別來讀取, 由於 simpledialog 是 tkinter 的子模組, 其匯入方法與 messagebox 一樣 :

from tkinter import simpledialog

或者使用別名 :

from tkinter import simpledialog as sd

simpledialog 提供三個方法可分別讀取字串, 整數, 以及浮點數 :

 simpledialog 的方法 說明 回傳值
 askstring(title, message) 顯示字串輸入對話框 字串
 askinteger(title, message) 顯示整數輸入對話框 整數
 askfloat(title, message) 顯示浮點數輸入對話框 浮點數

除了 title 與 message 這兩個必要參數外, 數值輸入的 askinteger() 與 askfloat() 還有 minvalue(最小值), maxvalue(最大值), 以及 initialvalue(預設初始值) 等參數可設定輸入值的範圍與預設值, 輸入值若超出範圍會出現提示對話框.


測試 5 : 使用 simpledialog 讀取字串與數值

import tkinter as tk
from tkinter import ttk
from tkinter import simpledialog as sd

def do_askstring():
    str_input=sd.askstring("askstring", "請輸入姓名")
    print("{} {}".format(str_input, type(str_input)))
def do_askinteger():
    int_input=sd.askinteger("askinteger", "請輸入年齡", minvalue=0, maxvalue=120)
    print("{} {}".format(int_input, type(int_input)))
def do_askfloat():
    float_input=sd.askfloat("askfloat", "請輸入體重", initialvalue=50.0)
    print("{} {}".format(float_input, type(float_input)))

root=tk.Tk()
root.title("simpledialog")
ttk.Button(root, text="askstring", command=do_askstring).pack()
ttk.Button(root, text="askinteger", command=do_askinteger).pack()
ttk.Button(root, text="askfloat", command=do_askfloat).pack()
root.mainloop()

此程式放置了三個按鈕分別觸發 askstring(), askinteger(), 與 askfloat() 方法, 事件處理函數會顯示所輸入之資料以及其資料型態.







依序執行結果輸出如下 :

南志鉉 <class 'str'>
23 <class 'int'>
40.6 <class 'float'>

可見這三個函數都分別處理好傳回值得資料型態了.

上面程式中年齡部分有用 minvalue 與 maxvalue 參數設定 0~120 範圍, 如果輸入此範圍外之數值, 將出現提示訊息框 :




參考 :

Single Value Data Entry
https://svn.python.org/projects/python/tags/r32/Lib/tkinter/simpledialog.py



三. 使用 filedialog 對話框選取或輸入檔案名稱 :

tkinter 的檔案開啟儲存對話框功能寫在 filedialog 類別裡, 使用前需先匯入 (可用別名) :

import tkinter.filedialog as fd



from tkinter import filedialog as fd

from tkinter.filedialog import askopenfilename

filedialog 類別的方法如下表 :

 filedialog 的方法 說明 回傳值
 askopenfilename(initialdir, title, filetypes) 顯示檔案開啟對話框 (單選) 檔案路徑字串
 askopenfilenames(initialdir, title, filetypes)  顯示檔案開啟對話框 (複選) 檔案路徑字串 tuple
 asksaveasfilename(initialdir, title, filetypes) 顯示檔案儲存對話框 檔案路徑字串
 askdirectory() 顯示資料夾開啟對話框 資料夾路徑字串
 askopenfile(initialdir, title, filetypes) 顯示檔案開啟對話框 (單選) 檔案 IO 串流物件
 askopenfiles(initialdir, title, filetypes)  顯示檔案開啟對話框 (複選) 檔案 IO 串流物件 list
 asksaveasfile(initialdir, title, filetypes) 顯示檔案儲存對話框  檔案 IO 串流物件

參數 initialdir, title, filetypes 都是可有可無的參數, 說明如下 :
  1. initialdir : 指定預設開啟之目錄, 例如 "/", "D:\\Python", "D:/Python"
  2. title : 設定對話框標題, 例如 "開啟檔案"
  3. filetypes : 篩選檔案型態之 tuple, 例如 (("text files","*.txt"),("all files","*.*"))
注意, 開啟檔案有分單選與複選 (同時按下 Ctrl 鍵), 儲存檔案與選取目錄只有單選.

參考 :

https://pythonspot.com/tk-file-dialogs/
Python:GUI之tkinter學習筆記之messagebox、filedialog


下列範例程式測試 filedialog 的七種方法 :

測試 6 : filedialog 對話框函數測試

import tkinter as tk
from tkinter import ttk
from tkinter import filedialog as fd

def do_askopenfilename():
    file=fd.askopenfilename()
    print(type(file))
    print(file)
def do_askopenfilenames():
    files=fd.askopenfilenames()
    print(type(files))
    print(files)
def do_asksaveasfilename():
    file=fd.asksaveasfilename()
    print(type(file))
    print(file)
def do_askdirectory():
    dir=fd.askdirectory()
    print(type(dir))
    print(dir)
def do_askopenfile():
    file=fd.askopenfile()
    print(type(file))
    print(file) 
def do_askopenfiles():
    files=fd.askopenfiles()
    print(type(files))
    print(files)
def do_asksaveasfile():
    file=fd.asksaveasfile()
    print(type(file))
    print(file)     

root=tk.Tk()
root.title("檔案開啟儲存")
ttk.Button(root, text="askopenfilename", command=do_askopenfilename).pack()
ttk.Button(root, text="askopenfilenames", command=do_askopenfilenames).pack()
ttk.Button(root, text="asksaveasfilename", command=do_asksaveasfilename).pack()
ttk.Button(root, text="askdirectory", command=do_askdirectory).pack()
ttk.Button(root, text="askopenfile", command=do_askopenfile).pack()
ttk.Button(root, text="askopenfiles", command=do_askopenfiles).pack()
ttk.Button(root, text="asksaveasfile", command=do_asksaveasfile).pack()
root.mainloop()






依序按鈕操作輸出如下 :

呼叫 askopenfilename() : 單選
<class 'str'>
D:/app.php

呼叫 askopenfilenames() : 複選
<class 'tuple'> 
('D:/app.php', 'D:/dir.txt', 'D:/test.avi')

呼叫 asksaveasfilename() :
<class 'str'>
D:/test.txt

呼叫 askdirectory() :
<class 'str'>
D:/APK

呼叫 askopenfile() :
<class '_io.TextIOWrapper'>
<_io.TextIOWrapper name='D:/dir.txt' mode='r' encoding='cp950'>

呼叫 askopenfiles() :
<class 'list'>
[<_io.TextIOWrapper name='D:/dir.txt' mode='r' encoding='cp950'>, <_io.TextIOWrapper name='D:/tensorflow.txt' mode='r' encoding='cp950'>]

呼叫 askopenfiles() :
<class '_io.TextIOWrapper'>
<_io.TextIOWrapper name='D:/keras.py' mode='w' encoding='cp950'>

可見 askopenfile(), askopenfiles(), 與 asksaveasfile() 這三個函數的傳回值是 IO 串流物件, 可用傳回值參考來存取其中的屬性. 注意, askopenfile() 與 asksaveasfile() 這兩個對話框若按 "取消" 會傳回 None, 其餘方法按 "取消" 都是傳回空字串.

<class 'NoneType'>
None
<class 'str'>


上面範例只是測試 filedialog 的函數呼叫, 下面則實際測試文字檔案之讀取與儲存. 參考 :

https://stackoverflow.com/questions/19476232/save-file-dialog-in-tkinter


測試 7 : 檔案開啟與儲存對話框

import tkinter as tk
from tkinter import ttk
from tkinter import filedialog as fd

def open_file():
    openfilename=fd.askopenfilename(initialdir="/",title="Select file",\
                 filetypes = (("text files","*.txt"),("all files","*.*")))
    print (openfilename)
    try:
        with open(openfilename,'r') as file:
            print(file.read())
    except:
        print("檔案不存在!") 
def save_file():
    f=fd.asksaveasfile(mode='w', defaultextension=".txt")
    if f is None:
        return
    f.write("Hello World!")
    f.close() 

root=tk.Tk()
root.title("檔案開啟 & 儲存檔案")
ttk.Button(root, text="開啟檔案", command=open_file).pack()
ttk.Button(root, text="儲存檔案", command=save_file).pack()
root.mainloop()






檔案儲存後輸出如下 :

D:/hello.txt
Hello World!


參考 :

Tkinter tkFileDialog module
Tkinter Standard Dialog Boxes
https://github.com/PacktPublishing/Python-GUI-Programming-with-Tkinter/
Learn Tkinter in 20 Minutes
Python:GUI之tkinter學習筆記之messagebox、filedialog

2 則留言 :

Eric Chao 提到...

大大您好,非常感謝您的教學,我參考您的方法寫了一個askfloat的變數輸入。
但我後續需要用該變數當作數值做數據上的運算,想請問該如何將輸入後的數字輸出出來呢?
換個角度問就是我的數字在輸入後是儲存在哪個地方。

謝謝您。

小狐狸事務所 提到...

Hi, 放在指定的全域變數裡, 或者用 return 傳回去即可.