2021年8月31日 星期二

Python 內建 GUI 模組 tkinter 測試 (八) : Text 與 Scrollbar 元件

Text 元件功能如下 :      
  • 可輸入多行文字
  • 可在文字中嵌入圖片
  • 提供文字格式化功能 (字型, 顏色, 大小, 樣式) 
  • 可與捲軸元件 Scrollbar 相結合為文字編輯器
Text 元件甚至可做為簡易型的網頁瀏覽器用. 

本系列之前的文章參考 :




1. Text 元件的用法 : 

呼叫 tkinter 模組的 Text() 函數即可建立一個文字區塊並傳回一個 Text 物件 :

tk.Text(父容器物件 [, options])     

其中父容器物件可以是 Tk 視窗物件, Frame, 或 TopLevel 等容器物件. options 是可選參數群, 注意, Text 元件在 ttk 子模組沒有提供, 大概是因為此元件外觀沒有甚麼需要美化之處. 常用的可選參數群 options 與物件如下表 : 


 Text 元件參數 說明
 width 寬度 (單位=字元數)
 height 高度 (單位=字元數)
 bg/background 背景色, 可用標準顏色名稱字串例如 'blue' 或 "#0000ff"
 fg/foreground 前景色 (文字顏色), 可用標準顏色名稱字串例如 'blue' 或 "#0000ff"
 font 字型與尺寸 (px), 粗體 (bold) 或斜體 (italic), 底線或刪除線等 
 padx 元件與容器的水平間距 (px)
 pady 元件與容器的垂直間距 (px)
 state 可用狀態, NORMAL=可編輯, DISABLED=不可編輯
 wrap 換行方式, CHAR (預設)=字元, WORD=字, NONE=不換行
 relief 外框型式, sunken (預設), flat, groove, raised, ridge, solid
 xscrollcommand 綁定之水平捲軸之 set 方法
 yscrollcommand 綁定之垂直捲軸之 set 方法


這些參數除了可以在建立 Text 物件時透過傳入參數群 options 設定外, 也可以在建立物件後用 [] 運算子或呼叫 config() 方法去設定或更改. 注意, 不能用 . 運算子存取這些參數, 例如應該用 text["bg"]="ivory", 不能用 text.bg="ivory", 因為它們不是物件的屬性. 

參數 xscrollcommand 與 yscrollcommand 是用來綁定 (呼叫) Scrollbar 元件的 set() 方法的, 這樣就可以為 Text 元件加上捲軸了. 不過這種綁定是雙向的, 在 Scrollbar 元件上還必須設定其 command 參數來呼叫 Text 元件的 xview() 或 yview() 方法, 這樣當捲動 Scrollbar 時 Text 元件的內容才會同步捲動. 這些參數都可用 Text 物件的 [] 運算子存取, 但設定則必須用 config() 方法. 

建立 Text 物件後可用下列常用方法進行操作 :


 Text 元件常用方法 說明
 insert(type, str) 以指定型式 type=INSERT/END 將字串 str 插入原內容後面
 config(**option) 設定物件屬性 options
 index(pos) 傳回位置字串 pos 之 "列.行" 格式索引位置 (str)
 get(from, to) 取得字元索引 from 到 to 之間的字元 (不含 to)
 delete(from, to) 刪除字元索引 from 到 to 之間的字元 (不含 to)


注意, 以上參數或方法中的大寫常數前面必須加上簡名 tk 才能存取, 例如 tk.NORM, tk.INSERT 等. 若使用 from tkinter import * 匯入則不必加 tk, 但不建議此種匯入法. 

insert(type, str) 用來將字串插入目前內容的最後面, 由於 Text 元件無法在建立物件時設定內容初始值 (因為它沒有像 Entry 元件那樣有 text 屬性), 其內容必須在建立物件之後呼叫 insert() 方法加以設定. type=tk.INSERT 用來將字串插入到原內容的最後面, 而 tk.END 則是在插入後終結方塊內容. 其實用 tk.END 後再繼續用 tk,INSERT 還是可以的. 除了使用 tkinter 常數外, type 參數也可以用位置字串 "insert" 與 "end".

config(**options) 方法用來在建立 Text() 物件後進行參數設定, 因此並不需要在呼叫 Text() 就傳入全部要設定之參數, 可以不傳入任何選用參數 (即先套用預設值), 再呼叫 config() 來修改. 

index(pos) 方法用來將位置字串轉換成 "列.行" 的索引格式, 傳回值是字串. Text 元件的內容字串基本上是使用 "列.行" 的矩陣索引格式來定位, 其中列從 1 起算, 行從 0 起算, 因此左上角第一個字元的索引位置是 "1.0", 第二列的第三行的位置是 "2.2", 除此之外 tinker 模組也定義了一些特定的位置字串, 其中較常用的如下表 : 


 常用的位置字串 說明
 "1.0" 文字區塊內容的開頭字元 (1 列 0 行) 索引
 "3.6" 文字區塊內容的 3 列 6 行的字元索引
 "1.end" 文字區塊內容第 1 列末尾字元的索引
 "insert" 文字區塊內容的插入位置 (預設是末尾字元) 索引
 "end" 文字區塊內容的末尾字元索引
 "sel.first" 文字區塊內容中被選取區域的開頭字元索引
 "sel.last" 文字區塊內容中被選取區域的末尾字元索引


注意, 內容為全數字的位置字串也可以用浮點數表示, 例如 "1.0" 可用 1.0 表示. 位置字串 pos 的詳細用法參考 : 


get(from, to) 方法會傳回指定索引 from 到 to 的字串, 要取得 Text 元件之內容只能透過此方法, 因為 Text 元件並沒有像 Entry 元件那樣有 textvariable 參數將內容綁定到 StringVar 物件上. 取得 Text 元件的全部內容可用方法如下 :
  • get(1.0, "end") 
  • get("1.0", "end")
  • get("1.0", tk.END)
  • get(1.0, tk.END)
delete(from, to) 方法可以刪除指定字元索引 from 到 to 之間的字元, 因此若要刪除 Text 元件的全部內容, 下面四種方式均可 :
  • delete(1.0, "end") 
  • delete("1.0", "end")
  • delete("1.0", tk.END)
  • delete(1.0, tk.END)
但如果要刪除第 1 列一整列, 可用下面兩種做法 :
  • delete("1.0", "1.end")    
  • delete("1.0", "2.0")
第一種做法的 "1.end" 表示第一列最後一個字元的下一個字元 (即跳行字元 \n) 的位置索引, 所以只刪除第一列的內容, 但不刪除跳行字元, 會留下一個空白列. 第二種作法 "2.0" 表示第二列的開頭, 所以會把整第一列包含跳行字元都刪除. 這些用法在開發文字編輯器時很有用, 參考 :



測試 1-1 : 建立文字區塊並插入內容 [看原始碼]

import tkinter as tk

win=tk.Tk()                                                       # 建立根視窗容器
win.title("tkinter GUI 測試")                            # 設定視窗標題
win.geometry("400x200")                                 # 設定視窗大小
text=tk.Text(win)                                               # 建立 Text 物件
text.insert(tk.INSERT, "Hello World!\n")          # 插入字串於原內容之後  
text.insert(tk.INSERT, "width=" + str(text["width"]) + \n")    # 預設寬度      
text.insert(tk.INSERT, "height=" + text["height"] + \n")         # 預設高度   
text.insert(tk.INSERT, "font=" + str(text["font"]) + "\n")        # 預設字型
text.insert(tk.INSERT, "padx=" + str(text["padx"]) + "\n")     # 預設水平間距
text.insert(tk.INSERT, "pady=" + str(text["pady"]) + "\n")     # 預設垂直間距
text.insert(tk.INSERT, "bg=" + str(text["bg"]) + "\n")             # 預設背景色
text.insert(tk.INSERT, "fg=" + str(text["fg"]) + "\n")              # 預設前景色
text.insert(tk.END, "你是在說哈囉嗎?")          # 插入字串於原內容之後
text.pack()
win.mainloop()

此例先建立 Text 物件然後插入內容, 順便檢視 width, height, 與 font 參數的預設值, 結果如下 :




可見 Text 元件的預設參數 width 為 80 個字元, height 為 24 個字元, font 為 TkFixedFont, 水平與垂直間距都是 1px, 背景與前景都是 Windows 預設值 (bg=白色, fg=黑色).  

下面範例是在建立物件後用 [] 運算子或 config() 方法去設定元件參數 : 


測試 1-2 : 建立文字區塊時傳入參數與呼叫 config() 設定參數 [看原始碼]

import tkinter as tk
win=tk.Tk()                                                      
win.title("tkinter GUI 測試")                           
win.geometry("400x300")                                 
text=tk.Text(win, bg="#00ffff", fg="blue", width=30, height=10)    # 建立物件時傳入參數
text.config(padx=5, pady=5, font="Helvetic 12 bold italic")     # 呼叫 config() 方法設定參數
text.insert(tk.INSERT, "Hello World!\n")
text.insert(tk.INSERT, "width=" + str(text["width"]) + "\n")             
text.insert(tk.INSERT, "height=" + str(text["height"]) + "\n")
text.insert(tk.INSERT, "font=" + str(text["font"]) + "\n")
text.insert(tk.INSERT, "padx=" + str(text["padx"]) + "\n")
text.insert(tk.INSERT, "pady=" + str(text["pady"]) + "\n")
text.insert(tk.INSERT, "bg=" + str(text["bg"]) + "\n")
text.insert(tk.INSERT, "fg=" + str(text["fg"]) + "\n")
text.insert(tk.INSERT, "你是在說哈囉嗎?")
text.pack()
win.mainloop()

此例分別在建立物件時傳入參數以及之後呼叫 config() 方法來設定文字區塊的屬性, 兩種做法均可. 注意, bg 參數可用顏色名稱 (例如 "ivory") 或顏色碼 (例如 "#00ff00"), font 參數可以使用字串形式, 也可以使用元組形式, 即 font=("Helvetic", 12, "bold", "italic"), 效果是一樣的, 結果如下 :




下面範例利用 delete() 方法來刪除全部或部分內容 :


測試 1-3 : 呼叫 delete() 方法刪除全部或部份內容 [看原始碼]

import tkinter as tk
from tkinter import ttk

win=tk.Tk()                                                      
win.title("tkinter GUI 測試")                           
win.geometry("400x300")                                 
text=tk.Text(win, width=30, height=10, bg="aqua")
text.config(padx=5, pady=5, font="Helvetic 12")
text.insert(tk.INSERT, "Hello World!\n")
text.insert(tk.INSERT, "width=" + str(text["width"]) + "\n")             
text.insert(tk.INSERT, "height=" + str(text["height"]) + "\n")
text.insert(tk.INSERT, "font=" + str(text["font"]) + "\n")
text.insert(tk.INSERT, "padx=" + str(text["padx"]) + "\n")
text.insert(tk.INSERT, "pady=" + str(text["pady"]) + "\n")
text.insert(tk.INSERT, "bg=" + str(text["bg"]) + "\n")
text.insert(tk.INSERT, "fg=" + str(text["fg"]) + "\n")
text.insert(tk.END, "你是在說哈囉嗎?")
text.pack()

def delete_all():
    text.delete(1.0, "end")      # 刪除全部內容

def delete_row_1():
    text.delete(1.0, 2.0)          # 刪除第一列 (含跳行)
    
btn1=ttk.Button(win, text="刪除全部", command=delete_all)
btn1.pack(side=tk.LEFT)
btn2=ttk.Button(win, text="刪除第一列", command=delete_row_1)
btn2.pack(side=tk.LEFT)
win.mainloop()

此例在文字方塊下添加兩個按鈕, 用 command 參數分別綁定 delete_all() 與 delete_row_one() 來刪除全部內如與第一列, 結果如下 : 




下面的範例用來測試 state 參數 :


測試 1-4 : 設定 state 參數以啟用或禁用 Text 元件 [看原始碼]

import tkinter as tk
from tkinter import ttk

win=tk.Tk()                                                      
win.title("tkinter GUI 測試")                           
win.geometry("400x300")                                 
text=tk.Text(win, width=30, height=10, bg="aqua")
text.config(padx=5, pady=5, font="Helvetic 12")
text.insert(tk.INSERT, "Hello World!\n")
text.pack()

def disable_widget():
    text.config(state=tk.DISABLED, bg='gray', fg='yellow')   # 將 Text 元件禁用

def normal_widget():
    text.config(state=tk.NORMAL, bg='aqua', fg='black')       # 將 Text 元件啟用
    
btn1=ttk.Button(win, text="Disabled", command=disable_widget)
btn1.pack(side=tk.LEFT)
btn2=ttk.Button(win, text="Normal", command=normal_widget)
btn2.pack(side=tk.LEFT)
win.mainloop()

此例中用兩個按鈕來呼叫 config() 方法將 state 參數設為 tk.NORMAL 或 tk.DISABLED 來啟用或禁用 Text 元件 (同時切換前景與背景色), 結果如下 :





此處我們使用 config() 方法來設定 state, bg, 與 sg 參數, 另一種方式是直接用 [] 運算子更改元件之屬性, 例如上面的 normal_widget() 可改寫為 :

def normal_widget():
    text["state"]=tk.NORMAL
    text["bg"]='aqua'
    text["fg"]='black'

效果是一樣的. 

Text 元件沒有像 Entry 元件那樣具有 textvariable 參數可將其內容綁定到 StringVar 物件上, 欲取得其內容必須利用其 get() 方法, 下列範例利用 index("sel.first") 與 index("sel.last") 分別取得選取區域的起始與結束索引, 再將這兩個索引傳入 get() 方法中即可取得所選取的文本內容 :


測試 1-5 : 呼叫 get() 方法擷取 Text 元件的內容 [看原始碼]

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

win=tk.Tk()                                                      
win.title("tkinter GUI 測試")                           
win.geometry("400x300")                                 
text=tk.Text(win, width=30, height=10, bg="aqua")
text.config(padx=5, pady=5, font="Helvetic 12")
text.insert("insert", "Hello World!\n")          # type 參數使用字串
text.insert("end", "你是在說哈囉嗎?")       # type 參數使用字串
text.pack()

def get_all_content():
    content=text.get(1.0, "end")
    msgbox.showinfo("Info", content)

def get_marked_content():
    try:
        start=text.index("sel.first")              # 取得選取區域的開頭索引
        stop=text.index("sel.last")               # 取得選取區域的結尾索引
        content=text.get(start, stop)             # 取得選取區域之內容
        msgbox.showinfo("Info", content)
    except:
        msgbox.showinfo("Info", "請選取內容")
    
    
btn1=ttk.Button(win, text="顯示全部內容", command=get_all_content)
btn1.pack(side=tk.LEFT)
btn2=ttk.Button(win, text="顯示選擇內容", command=get_marked_content)
btn2.pack(side=tk.LEFT)
win.mainloop()

注意, 呼叫 index() 與 get() 方法時有可能出現例外 (例如沒有選取導致無法傳回索引), 因此必須放在 try-except 區塊中以處理可能發生的例外. 結果如下 : 





下面範例則是測試 index() 方法的傳回值 :


測試 1-6 : 呼叫 index() 方法取得選取字串的索引位置 [看原始碼]

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

win=tk.Tk()                                                      
win.title("tkinter GUI 測試")                           
win.geometry("400x300")                                 
text=tk.Text(win, width=30, height=10, bg="aqua")
text.config(padx=5, pady=5, font="Helvetic 12")
text.insert("insert", "Hello World!\n")
text.insert("insert", "你是在說哈囉嗎?")
text.pack()

def get_all_content():
    content=text.get(1.0, "end")
    msgbox.showinfo("Info", content)    

def get_marked_index():
    try:
        start=text.index("sel.first")
        stop=text.index("sel.last")
        content="起始字元索引=" + start + " 結束字元索引=" + stop
        msgbox.showinfo("Info", content)
    except:
        msgbox.showinfo("Info", "請選取內容")
    
    
btn1=ttk.Button(win, text="顯示選擇內容起訖索引", command=get_marked_index)
btn1.pack()
win.mainloop()

此例利用 index() 將位置字串 "sel.first" 與 "sel.last" 轉成 "列.行" 索引格式傳回, 可以得知被選取的文字起訖索引, 結果如下 : 




"說" 的索引為 "2.3" 沒錯, "囉" 是 "2.5", 此處結束字元索引卻是 "2.6", 因為位置字串 "sel.last" 代表的是被選取文本末尾字元的下一個字元索引.  




2. Scrollbar 捲軸元件用法 : 

上面範例中所建立的 Text 元件當內容很長時, 必須使用向下鍵才能看到下面的內容, tkinter 提供 了 Scrollbar 捲軸元件可以跟 Text 元件綁定在一起, 這樣即可使用滑鼠捲動來檢視內容. Scrollbar 在 tk 與 ttk 都有 (但外觀其實沒甚麼差別), 通常用來跟 Canvas, Text 與 Listbox 搭配作為這些元件的捲軸, 另外, 水平捲軸也可以在 Entry 元件上. 

呼叫 tk 的 Scrollbar() 函數即可建立一個捲軸物件 : 

tk.Scrollbar(父容器物件 [, options])     

其中父容器物件可以是 Tk 視窗物件, Frame, 或 TopLevel 等容器物件. options 是可選參數群 : 


 Scrollbar 元件常用參數 說明
 command 當捲軸捲動時呼叫之函式名稱 (不含括號)
 orient 捲軸方向=tk.HORIZONTAL/tk.VERTICAL (預設)
 width 捲軸寬度 (單位 px)
 bg 滑桿與箭頭的背景色


其中 command 用來綁定其它元件的相對應外觀函式, 例如垂直捲軸要綁定 Text 或 Listbox 元件的 yview() 函式; 而水平捲軸則是要綁定 xview() 函式.

Scrollbar 元件的方法如下表 : 


 Scrollbar 元件的方法 說明
 get() 傳回表示捲軸位置的元組 (a, b), a=左/上, b=右/下
 set(first, last) 將捲軸綁定到另一元件之 xscrollcommand 或 yscrollcommand 參數


其中 get() 方法較少用, 而 set() 方法最重要, 用來將捲軸綁定到 Canvas, Text 或 Listbox 元件上, 這兩個元件都有 xscrollcommand 與 yscrollcommand 這兩個參數, 只要將這兩個參數指定為呼叫 Scrllbar 元件的 set 方法, 並將 Scrollbar 的 command 參數設定為呼叫 Canvas, Text 或 Listbox 的 xview 或 yview 即完成雙向綁定, 其配對如下表所示 :


  Text/Listbox/Canvas 元件 Scrollbar 元件
 垂直捲軸 yscrollcommand=scollbar.set command=text.yview
 水平捲軸 xscrollcommand=scollbar.set command=text.xview


不過對於 Text 元件而言, 因為內容會自動換行 (使用 wrap 參數可設定是以 CHAR/WORD 為單位), 所以幾乎用不到到垂直捲軸. 注意, 由於 Text 元件與 Scrollbar 元件互相用 

參考 :


例如 : 


測試 2-1 : 綁定右方捲軸的 Text 元件 [看原始碼]

import tkinter as tk
from tkinter import ttk

win=tk.Tk()                                                      
win.title("tkinter GUI 測試")                           
win.geometry("500x300")
scrollbar=tk.Scrollbar(win)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)     #捲軸靠右填滿 Y 軸
text=tk.Text(win, height=10, bg="aqua")
text.config(padx=5, pady=5, font="Helvetic 12")  
text.config(yscrollcommand=scrollbar.set)    #Text 綁定捲軸
scrollbar.config(command=text.yview)           #捲軸綁定 Text
str="Hello World!\n你是在說哈囉嗎?"     #基本內容字串
strlist=[]                                                       #放內容的空串列
for i in range(10):                                        #用迴圈複製字串串列
    strlist.append(str)
text.insert("insert", "\n".join(strlist))           #將字串串列內容用跳行字元串接
text.pack(side=tk.LEFT, fill=tk.Y)     #Text 靠左填滿 Y 軸 (要在捲軸之後 pack)
win.mainloop()

此例建立了一個 Text 與 Scrollbar 元件, 使用 pack() 排版時要注意將 fill 參數設為 Y 以填滿 Y 軸, side 參數兩者要配合, 此處 Text 設為 LEFT 在左, 則 Scrollbar 就要設為 RIGHT 在右. 注意, Scrollbar 要比 Text 先放進視窗容器內, 否則會看不到捲軸. 最重要的是 Text 與 Scrollbar 彼此透過 yscrollcommand 與 command 參數互相綁定, 結果如下 : 




只要將 Text 與 Scrollbar 的位置交換, 捲軸就會換成左邊了, 例如 :


測試 2-2 : 綁定左方捲軸的 Text 元件 [看原始碼]

import tkinter as tk
from tkinter import ttk

win=tk.Tk()                                                      
win.title("tkinter GUI 測試")                           
win.geometry("500x300")
scrollbar=tk.Scrollbar(win)
scrollbar.pack(side=tk.LEFT, fill=tk.Y)
text=tk.Text(win, height=10, bg="aqua")
text.config(padx=5, pady=5, font="Helvetic 12")
text.config(yscrollcommand=scrollbar.set)
scrollbar.config(command=text.yview)
str="Hello World!\n你是在說哈囉嗎?"
strlist=[]
for i in range(10):
    strlist.append(str)
text.insert("insert", "\n".join(strlist))
text.pack(side=tk.RIGHT, fill=tk.Y)
win.mainloop()

此例程式與上例的差異只是 side 參數值交換而已, 結果如下 :





測試 2-3 : 綁定上方水平捲軸的 Text 元件 [看原始碼]

import tkinter as tk
from tkinter import ttk

win=tk.Tk()                                                      
win.title("tkinter GUI 測試")                           
win.geometry("500x300")
scrollbar=ttk.Scrollbar(win, orient=tk.HORIZONTAL)   #建立水平捲軸
scrollbar.pack(side=tk.TOP, fill=tk.X)
text=tk.Text(win, height=10, bg="aqua")
text.config(padx=5, pady=5, font="Helvetic 12", wrap=tk.NONE)   #不換行
text.config(xscrollcommand=scrollbar.set)    #Text 綁定水平捲軸
scrollbar.config(command=text.xview)           #Scrollbar 綁定 Text 的水平方向
str="Hello World!你是在說哈囉嗎?"
strlist=[]
for i in range(10):
    strlist.append(str)
text.insert("insert", "".join(strlist))          #將字串串列串接為長字串
text.pack(side=tk.BOTTOM, fill=tk.X)
win.mainloop()


此例的 Scrollbar 元件用 orient 參數指定為水平捲軸, 並在呼叫 pack() 時指定 side 參數為上方 (TOP), fill 參數指定 X 軸填滿. Text 元件則用 side 參數指定放在下方 (BOTTOM) 與 X 軸填滿. 因為是水平捲軸, 因此 Text 與 Scrollbar 互相綁定時 Text 要改用 xscrollcommand 參數去連結 Scrollbar 的 set() 方法, 而 Scrollbar 的 command 參數則是要綁定 Text 的 xview() 方法, 結果如下 :




注意此例的捲軸改用了 ttk 版的 Scrollbar 元件, 但其實與 tk 的外觀並無差異. 

參考 :


2021年8月30日 星期一

Python 內建 GUI 模組 tkinter 測試 (七) : Entry 元件

今天繼續測試 tkinter 的 Entry 元件, 本系列之前的文章參考 :   

Entry 元件用來輸入單行文字, 亦即換行字元在此元件中是無效的. 其語法如下 :

Entry(父容器, **參數列)   

參考 :


Entry 元件常用參數如下表 : 


 Entry 元件常用屬性 說明
 textvariable 動態綁定文字欄位內容的類別變數物件名稱
 show 輸入資料時顯示的固定字元, 例如用於密碼欄位
 width 寬度 (單位=字元數)
 height 高度 (單位=字元數)
 bg/background 背景色, 可用標準顏色名稱字串例如 'blue' 或 "#0000ff"
 fg/foreground 前景色 (文字顏色), 可用標準顏色名稱字串例如 'blue' 或 "#0000ff"
 font 設定字型與尺寸 (px), 粗體 (bold) 或斜體 (italic), 底線或刪除線等 
 padx 元件與容器的水平間距 (px)
 pady 元件與容器的垂直間距 (px)
 state 設定按鈕狀態=tk.NORMAL (預設可用)/tk.DISABLED (不可用)


對於 Entry 元件而言, 最重要的參數是動態綁定其內容值的 textvariable, 可透過 StringVar, IntVar, DoubleVar, BoolVar 這四種類別變數的 set() 與 get() 來存取其內容. 例如下面這個 tk 的四則運算器範例 : 


測試 1 : 四則運算器 (tk) [看原始碼]

import tkinter as tk
from tkinter import ttk

def add():
    c.set(a.get() + b.get())
def sub():
    c.set(a.get() - b.get())
def mul():
    c.set(a.get() * b.get())
def div():
    if b.get() != 0:                    //取得類別變數 (除數) 之值
        c.set(a.get() / b.get())     //除數不為 0 才除
    else:
        c.set(float("inf"))           //除以 0 顯示為無窮大

win=tk.Tk()                                                      
win.title("tkinter GUI 測試")                           
win.geometry("500x200")
a=tk.DoubleVar()
b=tk.DoubleVar()
c=tk.DoubleVar()
tk.Entry(win, width=10, textvariable=a).grid(row=0, column=0)    //輸入運算元 a
tk.Label(text="+", width=5).grid(row=0, column=1)
tk.Entry(win, width=10, textvariable=b).grid(row=0, column=2)    //輸入運算元 b
tk.Button(win, text="=", width=5, command=add).grid(row=0, column=3)    //加法按鈕
tk.Entry(win, width=10, textvariable=c).grid(row=0, column=4)     //儲存運算結果 c
tk.Label(win, text="-", width=5).grid(row=1, column=1)
tk.Button(win, text="=", width=5, command=sub).grid(row=1, column=3)    //減法按鈕
tk.Label(win, text="*", width=5).grid(row=2, column=1)
tk.Button(win, text="=", width=5, command=mul).grid(row=2, column=3)    //乘法按鈕
tk.Label(win, text="/", width=5).grid(row=3, column=1)
tk.Button(win, text="=", width=5, command=div).grid(row=3, column=3)      //除法按鈕
win.mainloop()

此例定義了 a, b, c 三個 DoubleVar 類別變數分別與三個 Entry 元件綁定, 並使用 grid 排版來放置 Label, Entry, 以及 Button 元件, 當按下 +, -, *, / 相對應的 = 按鈕就會在第三個 Entry 元件上顯示四則運算之值, 其中除法需要特別處理除數為 0 的情況, 結果如下 : 





下面是上例的 ttk 版本, 只是將建立元件的 tk 改成 ttk 而已 : 


測試 2 : 四則運算器 (ttk) [看原始碼]

import tkinter as tk
from tkinter import ttk

def add():
    c.set(a.get() + b.get())
def sub():
    c.set(a.get() - b.get())
def mul():
    c.set(a.get() * b.get())
def div():
    if b.get() != 0:
        c.set(a.get() / b.get())
    else:
        c.set(float("inf"))

win=tk.Tk()                                                      
win.title("tkinter GUI 測試")                           
win.geometry("500x200")
a=tk.DoubleVar()
b=tk.DoubleVar()
c=tk.DoubleVar()
ttk.Entry(win, width=10, textvariable=a).grid(row=0, column=0)
ttk.Label(text="+", width=5).grid(row=0, column=1)
ttk.Entry(win, width=10, textvariable=b).grid(row=0, column=2)
ttk.Button(text="=", width=5, command=add).grid(row=0, column=3)
ttk.Entry(win, width=10, textvariable=c).grid(row=0, column=4)
ttk.Label(text="-", width=5).grid(row=1, column=1)
ttk.Button(text="=", width=5, command=sub).grid(row=1, column=3)
ttk.Label(text="*", width=5).grid(row=2, column=1)
ttk.Button(text="=", width=5, command=mul).grid(row=2, column=3)
ttk.Label(text="/", width=5).grid(row=3, column=1)
ttk.Button(text="=", width=5, command=div).grid(row=3, column=3)
win.mainloop()

結果如下 : 





下面是模擬登入頁面的範例 : 


測試 3 : tk 模擬登入頁面 [看原始碼]

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

def clear():
    account.set("")
    pwd.set("")
def login():
    if account.get() == "admin" and pwd.get() == "admin":
        msgbox.showinfo("Info", "登入成功")
    else:
        msgbox.showinfo("Info", "帳號或密碼錯誤")

win=tk.Tk()                                                      
win.title("tkinter GUI 測試")                           
win.geometry("300x150")

account=tk.StringVar()
pwd=tk.StringVar()
tk.Label(win, text="帳號").grid(row=0, column=0)
tk.Entry(win, textvariable=account).grid(row=0, column=1)
tk.Label(win, text="密碼").grid(row=1, column=0)
tk.Entry(win, textvariable=pwd).grid(row=1, column=1)
tk.Button(text="清除", command=clear).grid(row=2, column=0)
tk.Button(text="登入", command=login).grid(row=2, column=1)
win.mainloop()

此例中的兩個 Entry 用來收集使用者輸入之帳號密碼, 用 textvariable 參數分別綁定到 account 與 pwd 這兩個 StringVar 類別變數, 當按下登入鈕時呼叫類別變數的 get() 方法取得輸入值, 經比對帳密都是 admin 就以 messagebox 訊息盒顯示登入成功. 按下清除鈕則呼叫類別變數的 set() 方法將其內容清空, 結果如下 : 




一般來說, 密碼欄位通常會用 * 字元顯示, 這可以透過 show 參數來設定. 另外如登入按鈕所示 grid 版面管理員預設是置中對齊, 這可以用 sticky 參數設為 tk.E, tk.W, tk.S, 或 tk.N 來對齊 (也可以用 + 來複合), 例如 : 


測試 4 : tk 模擬登入頁面 (使用 show 密碼參數) [看原始碼]

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

def clear():
    account.set("")
    pwd.set("")
def login():
    if account.get() == "admin" and pwd.get() == "admin":
        msgbox.showinfo("Info", "登入成功")
    else:
        msgbox.showinfo("Info", "帳號或密碼錯誤")

win=tk.Tk()                                                      
win.title("tkinter GUI 測試")                           
win.geometry("300x150")

account=tk.StringVar()
pwd=tk.StringVar()
tk.Label(win, width=5, text="帳號").grid(row=0, column=0)
tk.Entry(win, textvariable=account).grid(row=0, column=1)
tk.Label(win, width=5, text="密碼").grid(row=1, column=0)
tk.Entry(win, show="*", textvariable=pwd).grid(row=1, column=1)
tk.Button(text="清除", command=clear).grid(row=2, column=0, sticky=tk.W)
tk.Button(text="登入", command=login).grid(row=2, column=1, sticky=tk.W)
win.mainloop()

此例將 Label 的寬度設為 5 個字元較能看出按鈕的 sticky 效果, 結果如下 : 




下面是上面範例的 ttk 版本 :


測試 5 : ttk 模擬登入頁面 [看原始碼]

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

def clear():
    account.set("")
    pwd.set("")
def login():
    if account.get() == "admin" and pwd.get() == "admin":
        msgbox.showinfo("Info", "登入成功")
    else:
        msgbox.showinfo("Info", "帳號或密碼錯誤")

win=tk.Tk()                                                      
win.title("tkinter GUI 測試")                           
win.geometry("350x150")

account=tk.StringVar()
pwd=tk.StringVar()
ttk.Label(win, text="帳號").grid(row=0, column=0)
ttk.Entry(win, textvariable=account).grid(row=0, column=1)
ttk.Label(win, text="密碼").grid(row=1, column=0)
ttk.Entry(win, show="*", textvariable=pwd).grid(row=1, column=1)
ttk.Button(text="清除", command=clear).grid(row=2, column=0, sticky=tk.W)
ttk.Button(text="登入", command=login).grid(row=2, column=1, sticky=tk.W)
win.mainloop()

由於 ttk 按鈕預設寬度較大, 故視窗寬度放寬為 350px, 結果如下 : 




下面這個範例是做華氏與攝式溫度互轉 :


測試 6 : 華氏與攝氏溫度互轉 [看原始碼]

import tkinter as tk
from tkinter import ttk

def f2c():
    c.set((f.get() - 32) * 5/9)
def c2f():
    f.set(c.get() * 9/5 + 32)

win=tk.Tk()                                                      
win.title("tkinter GUI 測試")                           
win.geometry("500x150")

f=tk.DoubleVar()
c=tk.DoubleVar()
tk.Label(win, text="華氏溫度").grid(row=0, column=0)
tk.Label(win, text="攝氏溫度").grid(row=0, column=2)
tk.Entry(win, textvariable=f).grid(row=1, rowspan=2, column=0)
tk.Button(win, text="=>", command=f2c).grid(row=1, column=1)
tk.Entry(win, textvariable=c).grid(row=1, rowspan=2, column=2)
tk.Button(win, text="<=", command=c2f).grid(row=2, column=1)
win.mainloop()

此例使用兩個 Entry 元件來輸入華氏與攝氏溫度, 他們用 textvariable 參數分別與兩個 DoubleVar 類別變數綁定, 以便能在程式中動態更改其內容. 另外還有兩個用 command 參數指定事件處理函式的方向按鈕用來互轉溫度, 結果如下 : 




下面是計算 BMI 的範例 : 


測試 7 : 計算 BMI (身高體重指數) [看原始碼]

import tkinter as tk
from tkinter import ttk

def calculate_bmi():
    h=float(height.get()) / 100
    w=float(weight.get())
    bmi=w / h ** 2
    if bmi < 18.5:
        comment="體重過輕"
    elif bmi >= 18.5 and bmi < 24:
        comment="體重適當"
    else:
        comment="體重過重"    
    msg=f"BMI 指數={bmi:.2f}, {comment}"
    result.set(msg)

win=tk.Tk()                                                      
win.title("tkinter GUI 測試")                           
win.geometry("350x150")

height=tk.DoubleVar()
weight=tk.DoubleVar()
result=tk.StringVar()
tk.Label(win, text="身高 (公分)").grid(row=0, column=0)
tk.Label(win, text="體重 (公斤)").grid(row=1, column=0)
tk.Entry(win, textvariable=height).grid(row=0, column=1)
tk.Entry(win, textvariable=weight).grid(row=1, column=1)
tk.Button(text="計算 BMI", command=calculate_bmi).grid(row=2, column=1)
tk.Label(win, textvariable=result).grid(row=3, columnspan=2, column=0)
win.mainloop()

此例使用兩個 Entry 元件取得使用者輸入的身高與體重, 分別綁定到 height 與 weight 這兩個 DoubleVar 變數類別物件, 當按下按鈕時呼叫 calculate_bmi() 來計算 BMI 數值並判斷體重是否過重, 最後將結果透過綁定到 Label 元件的 StringVar 變數類別物件動態更新 Label 的內容 :




參考 :


料理實驗 : 試做紅燒板豆腐

我每周都會向高橋邊的豆腐嫂買板豆腐, 平常都只是煎到兩面赤黃起鍋淋上日式醬油而已, 昨天參考肥大叔的食譜做了這道紅燒板豆腐換換口味.





材料 :
  • 板豆腐 一塊
  • 蒜末 適量
  • 薑絲 適量
  • 辣椒 一條
  • 蔥 一條
  • 胡椒
  • 米酒
  • 醬油
  • 香油
作法 : 
  1. 起油鍋切塊的板豆腐放下去煎至兩面金黃後起鍋.
  2. 將蒜末, 薑絲, 辣椒, 蔥白下鍋爆香, 加入些許米酒, 水, 醬油, 胡椒, 半湯匙糖, 然後將煎好的豆腐下鍋, 放入蔥綠兩面紅燒約 6 分鐘後即可起鍋. 




紅燒真的更下飯, 特別是放冰箱超過三天的板豆腐若只是乾煎會有一點苦味, 紅燒後就被蓋掉啦. 

2021年8月29日 星期日

2021 年第 35 周記事

鄉下家的牆面油漆本周已大致完成, 剩下二樓祖堂前的 PU 防漏, 由於前方露臺的地板磚有膨起現象, 需挖起重新黏合後再上 PU, 但施工得看日子, 故週二早上請假回去阻止油漆的老闆貿然動工並談價錢, 議定全部費用 23 萬元 (昨晚來取訂金又增一萬元漆欄杆與窗框, 共 24 萬元). 此次修繕是爸去阿泉伯那邊聽人介紹而來, 原本只談外牆與鐵皮屋油漆共 10 萬元, 但東加頂樓 PU, 西加祖堂前 PU 以及洗石部分就膨脹為原先的2.4 倍. 

姊姊上週去信義區一家遊戲軟體公司面試美術設計錄取, 周一開始去上班了 (她的說法是開始社畜生涯). 姊姊應該算是今年畢業, 但為了去俄羅斯當交換生, 故意欠修體育一門 3 學分來延畢, 但因為疫情之故, 看來是無法成行了.

萬萬猫周四進宮淨身, 因為它已是兩歲的成貓了, 最近似乎發情常找沒放好的衣物尿尿來宣示主權, 預約了好久終於成行. 我以為只是綁輸精管而已, 沒想到居然是割蛋蛋, 所以我現在都叫它萬公公, 哈.

前陣子連續下豪雨, 菜園的五棵木瓜倒下三棵, 剩下的兩棵其中一個也開始從根部開始腐爛, 所以今天去種子行花買了一株接枝的, 下午爸將其種在百香果架旁, 老闆囑咐一個月內不可施肥, 之後可用廚餘堆肥. 希望這次能種起來. 

2021年8月28日 星期六

TensorFlow.js 學習筆記 (二) : Javascript ES6 的新語法 (中)

由於篇幅太長了, 將 ES6 新語法測試拆分成三部分, 這是第二部分. 

本系列之前的文章參考 :


參考書籍 :

Javascript 函數活用範例速查辭典 (博碩 2015, 山田祥寬)
Javascript Tensorflow.js 人工智慧教本 (碁峰 2020, 陳會安)


4. 解構指定 (destructing assignment) :

所謂解構指定就是指定敘述可以一次對多個變數賦值, Python 就有這個方便語法, 又稱為多重指定  (multiple assignment), 例如 :

a, b, c=1, True, "Hello"  
 
這敘述可以同時將 1 指派給 a, True 指派給 b, "Hello" 指派給 c. 有了這語法, 在 Python 要將兩個變數交換就很簡單, 不需要透過一個中間變數暫存, 只要一條敘述就解決 :

x, y=y, x

Javascript 在 ES6 也添加了這項功能, 其語法為 :

let [x1. x2. x3, ...]=[a1, a2, a3, ...]

等號兩邊個數不一定要相同, 若左邊比右邊少, 則右邊多出來的會被丟棄 (不會有語法錯誤); 反之, 若左邊的比右邊多, 則左邊多出來的變數其值為 undefined. 若要避免出現 undefined, 可在左邊的變數設定預設值, 例如 : 

let [x, y=0, z='a']=[1, 2]      //x=1, y=2, z='a'

也可以用逗號將對應的變數跳過去不承接, 例如 :

let [x, , z]=[1, 2, 3]   //x=1, z= 3, 其中 2 無變數承接

如果兩變數要交換, 語法如下 :

[x, y]=[y, x]

例如 : 


測試 4-1 : 變數的解構指定 [看原始碼]  

<!doctype html>
<html>
  <head>
   <meta charset="UTF-8">
   <title>ES6 測試</title>
  </head>
  <body>
    <script>
      let [a, b, c, d]=[1, 2, 3, 4];
      document.write("[a, b, c, d]=[1, 2, 3, 4]" + "<br>");
      document.write("a=" + a + "<br>");      //a=1
      document.write("b=" + b + "<br>");     //b=2
      document.write("c=" + c + "<br>");      //c=3
      document.write("d=" + d + "<br>");      //d=4
      [a, b, c, d]=[d, c, b, a];
      document.write("[a, b, c, d]=[d, c, d, a]" + "<br>");
      document.write("a=" + a + "<br>");       //a=4
      document.write("b=" + b + "<br>");      //b=3
      document.write("c=" + c + "<br>");       //c=2
      document.write("d=" + d + "<br>");      //d=1
      [a, b=0, c='a', d]=[1, 2];       //設定預設值
      document.write("[a, b=0, c='a', d]=[1, 2]" + "<br>");
      document.write("a=" + a + "<br>");      //a=1
      document.write("b=" + b + "<br>");      //b=2   (有指定, 不使用預設值)
      document.write("c=" + c + "<br>");      //c='a'  (無指定, 使用預設值)
      document.write("d=" + d + "<br>");      //d=undefined   (沒有預設值, 多出來的變數)
    </script>
  </body>
</html>

結果如下 :

[a, b, c, d]=[1, 2, 3, 4]
a=1
b=2
c=3
d=4
[a, b, c, d]=[d, c, d, a]
a=4
b=3
c=2
d=1
[a, b=0, c='a', d]=[1, 2]
a=1
b=2
c=a
d=undefined

解構指定也可以用在字串, 將字串解構指定給多個變數時, 字串將以字元為單位拆解依序指定給左方的多個變數, 例如 :



<!doctype html>
<html>
  <head>
   <meta charset="UTF-8">
   <title>ES6 測試</title>
  </head>
  <body>
    <script>
      let [a, b, c, d, e, f, g]="Hello World";
      document.write("[a, b, c, d, e, f, g]='Hello World'" + "<br>");
      document.write("a=" + a + "<br>");
      document.write("b=" + b + "<br>");
      document.write("c=" + c + "<br>");
      document.write("d=" + d + "<br>");
      document.write("e=" + e + "<br>");
      document.write("f=" + f + "<br>");
      document.write("g=" + g + "<br>");
      [a, , c, , e, , g]="Hello World";
      document.write("[a, , c, , e, , g]='Hello World'" + "<br>");
      document.write("a=" + a + "<br>");
      document.write("c=" + c + "<br>");
      document.write("e=" + e + "<br>");
      document.write("g=" + g + "<br>");
    </script>
  </body>
</html>

此例中第二次解構指定跳過了 b, d, f 變數不予承接, 結果如下 :

[a, b, c, d, e, f, g]='Hello World'
a=H
b=e
c=l
d=l
e=o
f=
g=W
[a, , c, , e, , g]='Hello World'
a=H
c=l
e=o
g=W


5. 樣板字串內插 :

Python 有很好用的字串內插功能, 可在字串中嵌入變數或運算式達到動態更改字串局部內容的功能. ES6 也支援這種功能, 這種字串外面不是用單引號或雙引號括起來, 而是使用反引號 (即 Tab 鍵上方那個按鍵), 變數或運算式使用 ${variable|expression} 方式嵌入字串中, 例如 :

let msg=`Hello ${name} !`   

這種字串又稱為樣版字面值 (template literals) 或樣版字串 (template string). 

例如 :



<!doctype html>
<html>
  <head>
   <meta charset="UTF-8">
   <title>ES6 測試</title>
  </head>
  <body>
    <script>
      let name="Tony";
      let msg=`Hello ${name} !`    //嵌入變數
      document.write(msg + "<br>");
      let [a, b, c, d]=[1, 2, 3, 4];
      msg=`Math.max(1, 2, 3, 4)=${Math.max(a, b, c, d)}`;   //嵌入運算式
      document.write(msg + "<br>");
    </script>
  </body>
</html>

結果如下 : 

Hello Tony !
Math.max(1, 2, 3, 4)=4


6. 展開運算子 :
 
ES6 新增了三個連續點號 ... 的運算子稱為展開運算子 (spread operator), 它可用在陣列或字串上, 其用途如下 :
  • 展開陣列元素
  • 複製, 串接, 合併陣列
  • 將字串之字元展開變成陣列
  • 展開陣列元素後傳入函式做為位置參數
例如 : 



<!doctype html>
<html>
  <head>
   <meta charset="UTF-8">
   <title>ES6 測試</title>
  </head>
  <body>
    <script>
      let arr1=[1, 2, 3, 4, 5];
      document.write(arr1 + "<br>");        //直接輸出陣列內容
      document.write(...arr1 + "<br>");     //輸出展開後的陣列內容
      let arr2=[...arr1];                                //複製陣列 arr1 為 arr2=arr1.concat()
      document.write(...arr2 + "<br>");
      let arr3=[11, 12, ...arr1, 13, 14];        //展開後插入到其他陣列中
      document.write(...arr3 + "<br>");
      let arr4=[...arr1, ...arr2];                     //串接兩個陣列=arr1.concat(arr2)
      document.write(...arr4 + "<br>");
      let arr5=[[1, 2, 3], [4, 5, 6]];              //二維陣列
      let arr6=[...arr5];                                //複製二維陣列
      document.write(arr6 + "<br>");        
      document.write(...arr6 + "<br>");     //展開二維陣列
      let str="你在說哈囉嗎?"; 
      let arr7=[...str];                                  //展開字串中的每一字元
      document.write(...arr7 + "<br>");
    </script>
  </body>
</html>

此例演示了展開運算子在陣列與字串上的操作, 高維陣列一樣可展開, 結果如下 : 

1,2,3,4,5
1,2,3,4,5
1,2,3,4,5
11,12,1,2,3,4,5,13,14
1,2,3,4,5,1,2,3,4,5
你,在,說,哈,囉,嗎,?

輸出在網頁上結果看起來似乎有展開跟沒展開一樣, 輸出到控制台就可看出差異, 例如 : 



<!doctype html>
<html>
  <head>
   <meta charset="UTF-8">
   <title>ES6 測試</title>
  </head>
  <body>
    <script>
      let arr1=[1, 2, 3, 4, 5];
      console.log(arr1);
      document.write(...arr1 + "<br>");
      let arr2=[...arr1];
      console.log(...arr2);
      let arr3=[11, 12, ...arr1, 13, 14];
      console.log(...arr3);
      let arr4=[...arr1, ...arr2];
      console.log(...arr4);
      let arr5=[[1, 2, 3], [4, 5, 6]];
      let arr6=[...arr5];
      console.log(arr6);
      console.log(...arr6);
      let str="你在說哈囉嗎?";
      let arr7=[...str];
      console.log(...arr7);
    </script>
  </body>
</html>

此例與上例差別僅在於輸出到控制台 (console) 而已, 結果如下 : 




可見二維陣列直接輸出與展開後再輸出是不一樣的, 點前面的箭頭可顯示其差異 : 




展開運算子也可以用來將陣列元素展開後傳入函式做為位置參數, 例如 : 



<!doctype html>
<html>
  <head>
   <meta charset="UTF-8">
   <title>ES6 測試</title>
  </head>
  <body>
    <script>
      function get_sum(a, b, c) {
        return a + b + c;
        }
      let arr=[1, 2, 3];
      let sum=get_sum(...arr);     //將陣列元素展開後傳入函式中
      document.write("sum=" + sum + "<br>");
    </script>
  </body>
</html>

結果如下 :

sum=6