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 的外觀並無差異. 

參考 :


2 則留言 :

wang47 提到...

謝謝您這系列tkinter的文章, 很有幫助, 我之前在找text scroll bar的部份, 最後用了網路上搜尋到的ScrolledText也不錯用~

小狐狸事務所 提到...

感謝您回應, 我也來測試看看.