Tkinter安裝到入土
前情提要:在尋找怎麼用Python寫有GUI的小程式的時候找到了可以用tkinter
。
本來我最熟悉的GUI應該只有網頁了,但是要求的環境是單機,想說沒有必要這樣弄出一個單機的網頁,這樣很麻煩,而且上次的經驗是,我的開發經驗太少,做網頁會有不同的瀏覽器環境跟作業系統,這樣要多寫很多tags(我都手code…),在學會用方便一點的套件之前,還是不要好了。另一個主要原因是跟我一起合作的夥伴比較熟系Python。
Python tkinter安裝
聽起來很蠢,但是我們在安裝的時候就發生問題了。tkinter
的模組是依附在Python裡面的,所以理論上不需要用pip
多安裝東西。我們都是使用python3
我跟夥伴總共有三個開發環境是Ubuntu on windows,windows 10 , MacOS,在三個環境下執行都不一樣
Ubuntu上面是執行
apt-get install python3-tk
MacOS 跟 windows10理論上都不用安裝,但是我在MacOS使用scroll的功能的時候會出現這個錯誤
self.tk.mainloop(n) UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 0: incalid start byte
查了一番 Stackoverflow一番 ,發現是Homebrew在搞事,某一個由Homebrew發布的python3
裡面的Tk/Tcl是有錯的。於是我就更新了一下Homebrew
brew update
brew upgrade python3
但是怎麼搞的都會出現一樣的錯誤 我後來解決方法是…把brew版本的python3弄掉,用Xcode原生提供的Framework
brew uninstall python3
確認一下是由Xcode提供的python
> where python3
/Library/Frameworks/Python.framework/Versions/3.8/bin/python3
/usr/local/bin/python3
/usr/bin/python3
原本以為沒事了,結果在win10上面測試的時候又出現問題了,我要安裝Python時出現問題。
因為電腦上只裝了python2,在Powershell上面執行python3
會直接打開Microsoft store的python3.7頁面然後要我安裝。但是不知道怎樣Microsoft store下載之後必定安裝錯誤,完全搞不懂,微軟搞事= =。
最後我用Python官網下載以後安裝,然後要記得修改環境參數。 在windows下,使用GUI改動環境參數比較簡單,搜尋「環境變數」,點選「編輯系統環境變數」,或是從
設定 > 系統 > 關於 > 系統資訊 > 變更設定 > 環境變數 > PATH > 編輯
這樣設定會比在Powershell上面下指令好懂也方便,主要是windows的制令超難用…
經過一番折磨終於可以使用Tkinter了。
Tkinter 簡介
TKinter是古老的GUI指令Tk/Tcl在python環境下的module,可以參考的文件都滿破碎的,這邊記下,之後可以供自己參考。
Tkinter的基本觀念是由物件組成的,每個東西都是小部件(widgets),呼叫後對每個物件操作。 然後對應物件,將物件掛在物件上面,之後才會講到如何定位
import tkinter as tk # Ptyhon3.x tkinter, Python2.x Tkinter
from tkinter import ttk
app = tk.Tk()
# Frame created under app
frame = ttk.Frame(app)
# Button created under Frame
button = ttk.Button(frame,text="button",ommand=func())
但是由於Python是直譯的語言,不像是HTML這樣的tag包起來,不容易讀物件在哪裡,而且tkinter做出的物件還會需要再加上各種函式、調整位置的時候也要對那個物件操作,所以code會很醜,主要呼叫物件的部分一定要跟其他運算e.g.按下按鈕的操作之類的分開,不然整理畫面跟函式的時候會崩潰。
那tk跟ttk的主要差異是,ttk是themed tk,也就是比較好看,可以對ttk的物件加上style,同時大量的改變外觀,不然tk預設的外觀真的很醜。
Widgets
這邊就記些widgets,主要是我們這次有用的。沒用到的請參考Tkdocs,是我找到目前最完整的資源。另外全部有哪些widgets請參考Tk/Tcl documents或是用Python內建的dir()
來列出來。
- Tk
- the main Tk
-
# creating the application main window and its name app = tk.Tk() app.geometry('600x300') app.title("Color2Texture")
- Frame
- like
<div>
in HTML, for containing widgets together, easy to organize groups of widgets, not visible default -
# the Frame is under app, the main window frame = ttk.Frame(app)
- like
- Button
- Just a button can change text on it and append function on it
-
# text shows the woords on the button,command if the button is pressed load = ttk.Button(frame, text="Load", command=lambda: load_func(event))
- CheckButton
- [] just a check button
-
# var, variablet that the checkbutton controls,on/offvalue,command cont = ttk.Checkbutton(frame, text="Contour", var=cont_state, onvalue=1, offvalue=0, command=lambda: chk_contour(cont_state))
- Scale
- A scale that can be dragged, can be vertical or horizontal
-
# similar to checkbutton, orient for ver/hor,from_/to,variable as variable canny = ttk.Scale(frame, orient="horizontal", from_=1, to=100, variable=var_canny, command=lambda var_canny: adjst_edge(var_canny))
- Label
- A text block that shows text, not pretty sure the difference with text
-
label_scl = ttk.Label(frame, text="the text used for scale")
- Canvas
- canvas only supports tk, because it can’t be themed I supposed.
- a canvas bloack that can draw and create graphics, too complicated noted under.
-
# height and width is for the griding systeml,not px. bg for background's color,can be "black" or "#AAA", "#a145ff" canvas = tk.Canvas(frame, , height=4, width=4, bg="#AAA")
Basic tkinter concepts
接近寫完自己的小app,打這篇文章的時候才找到其實tkinter book有線上…可以參考tkinterook introduction 下面就記下我自己整理的
架構 Structure
Object structure
Main window(`Tk()`)
├─── Widgets (share the same grid)
│ └ widgets (share the same grid under same parent)
│
├────New Windows `Toplevel()`
│
└─── Standard dialogs
Coding structure
initial part:
# create the app
app = tk.Tk()
# call the widget,can init options or command if the widget is modifyed
widget = tk.widget(parent,options, command= func())
# modify options
widget.configure(option='value')
widget['option']='value'
# bind the fuctions on buttons
# WARNING: Buttons differs by OS, please check
widget.bind("<Button-1>",func())
# bind more fuction on same button and widget
widget.bind("<Button-1",func2(),add="+")
# Show the widgets by calling grid()
# also you can modify their position by grid
widget.grid(row=1,column=1)
at last:
# This starts up everything
app.mainloop()
# out of main loop
function part:
# user did some aciton
def func():
# change some widget
widget.standardfunction(args)
widget.configure(option='value')
# new widget
widget = tk.widget(parent,options, command= func())
# call tkinter(usually the main app Tk()) to update the change, if necessary
app.update_idletasks()
# new Window
new_window = Toplevel()
# may have to use update tu pop up the new windows if there are tasks after, be carefule with update()
new_window.update()
# some tasks running
pass
Tkinter的架構大致上就是這樣,物件上的層級沒有很複雜,通常在主要的Tk之下有很多widgets,你可以輕易的用呼叫建立他們,然後綁到一些函式,這樣基本的GUI app就完成了。
為了定位方便,可以利用frame來區分不同區塊的物件,這樣較容易同時對多個物件做事。
function的部分我猜測tkinter的運作模式是
mainloop --call--> function --done-> mainloop
這樣的方式,就是主要的回圈會跳出去執行完function之後再會來,而GUI的更新會在回到主要迴圈才做,所以如果在外面的function寫一些UI更新的東西,注意,他會在函式結束才更新畫面。所以會需要特別用update_idletasks()
來更新畫面。
Commands , basic Tk to Tkinter
這段是參考python3 docs
Tkinter 是Tk/Tcl在python上面的實作module,所以在找不到文件的時候可以去翻Tk/Tcl的文件,對我幫助滿大的。
import tkinter as tk
from tkinter import ttk
from tkinter import messagebox
object_name = tk.Widget(parent,option='value',option='value',command=func())
object_name = ttk.Widget(parent,option='value',option='value',command=func())
object_name.standardfunction()
# Standard windows
warning = messagebox.showwarning(message="WARNING: this is a warning")
options 參數
TKinter的widgets通常都有很多參數可以設定,除了在呼叫的時候定義以外,有兩種改變的方式
# set the option
widget['option'] = value
# standard function configure
widget.configure(option=value)
# show the value
print(widget['option'])
參數改動後,tkinter會更新這個物件。
Event handling 事件處理
按鈕按下應該要有改變,不然就是空的UI而已。我們會需要在物件上面綁定函式,除了一開始定義的,可以在不同按鈕按到時有不同的函式。實際上tkinter運作是有事件迴圈event loop在等待這個事件發生,如果發生了就會有一個callback,而我們就會將對應的code放在這個callback內。所以在無限迴圈中盡量不要呼叫無限迴圈,除非你知道自己在幹嘛。
button = ttk.Button(app,text="button",lambda event: command=func(event))
# bind to button1
button.bind(<Button-1>, button.configure(text="I am clicked"))
# add different funciton to same event
# covers the before fuctions if add="+" is not givrn.
button.bind(<Button-1>, func2(button), add="+")
Geometry manager 繪製畫面
Tkinter的畫面管理不太直觀,相較我習慣的HTML配上CSS,他有三種管理畫面的方式,我這次幾乎全部都用frame+grid()來完成。下面也會說明。
1. pack()
稍舊的tkinter會看到pack()
,它的用處是讓tkinter知道要重新render他的大小,依照裡面的字體等等,然後顯示在畫面上。如果沒有調整,所有東西都是預設大小
但是他依靠的是相對位置,然後有下面幾種狀況
- f填滿他的容器(frame或是主畫面等容器),參數fill:有以下的值:BOTH,X,Y,參數expand:非零參數代表需要多餘空間
- 上下疊放
- 左右擺放
label = ttk.Label(app,text="label")
label1 = ttk.Label(app,text="label1")
label2 = ttk.Label(app,text="label2")
# fill the whole container
label.pack(fill=BOTH, expand=1)
# do nothing the stack with others(up and down)
label1.pack()
# side by side with other widgets
label2.pack(side=LEFT)
button = ttk.Label(app,text="button")
button.pack(side=LEFT)
2. grid()
我幾乎都用這個,因為他比較直覺也比較好控制,他的概念是用表格直行橫列來擺放,你指定他在哪一格,grid()
就會自動幫你把預設大小的物件置中的擺在你定義的行列,而沒有任何物件的空行列會被忽略。他的概念跟HTML的table一樣,較容易理解。
語法大概如下
label = ttk.Label(app,text="label")
label1 = ttk.Label(app,text="label1")
e1 = Entry(app)
e2 = Entry(app)
label.grid(row=0,column=0)
# row and column defaults to 0
label.grid(row=1)
e1.grid(row=0,column=1)
e1.grid(row=1,column=1)
這會創造四格的畫面,分別填入物件。
預設的物件都會置中在自己的空格當中,所以如果想要偏向哪邊的話要用sticky這個選項,而使用的值是N,S,W,E,非常的智障….,會什麼不用上下左右要用東西南北完全搞不懂。可以四個方向都篇向,這樣沒有固定大小的物件就會自動的展開到填滿
# stick to left
label.grid(row=0,sticky="w")
# stick to all direction
label1.grid(row=0,sticky="nswe")
有些文件會說要用N+W+E+S
這樣的語法,但是我執行時會出錯。
如同<table>
你也可以橫跨好幾個空格
# add a image at the right which is 2 rows tall
image.grid(row=0,column=2,rowspan=2)
# or sth 3 columns width
label2.grid(row=2,column=0,columnspan=3)
3. place()
我在寫這篇的時候才查到這個,模式是定義絕對座標,可以參考這裡tkinterbook。因為我也沒用過就直接參考記下
Canvas and scrollbar
這段是參考tkdocs canvas
Canvas是tkinter裡面比較特別的物件,他可以如同畫布在上面畫圖,是操作空間比較大的物件。可以在上面建立基本圖形,如方塊、圓形、線段都是可行的,而我們這次用到是在上面呈現圖案,並且可以在上面畫線。為了方便使用,圖案是可以放大縮小的,所以內容是會比canvas的大小還大,這時候我們就需要用到滾動條,tkinter是沒有內建的,所以如果物件內部的內容比預設的大小還多,就會發生看不到的狀況。除了利用bind()
的full,或是grid()
的sticky選項填滿容器物件以外,還是會發生需要捲動的情況就會用到捲動條了,要呼叫scrollbar這個物件並且對應到需要捲動的物件上。
定義的時候如下:
# parent is a frame
canvas = tk.Canvas(canvas_frame, height=4, width=4, bg="#AAA")
canvas_h_scroll = ttk.Scrollbar(canvas_frame, orient=HORIZONTAL)
canvas_v_scroll = ttk.Scrollbar(canvas_frame, orient=VERTICAL)
canvas['scrollregion'] = (0,0,0,0)
canvas['yscrollcommand'] = canvas_v_scroll.set
canvas['xscrollcommand'] = canvas_h_scroll.set
canvas_h_scroll['command'] = canvas.xview
canvas_v_scroll['command'] = canvas.yview
canvas_h_scroll.grid(row=1,column=0, sticky="we")
canvas_v_scroll.grid(row=0,column=1, sticky="ns")
上面這段定義出了Canvas本身和兩個scrollbar的widget,grid的部分就在canvas的右邊下邊加上各別直的橫的卷軸。
而scrollregion
這項變數是宣告canvas可捲動的區域,也就是定義一個矩形範圍的左上右下1是可以捲動的區域,如果小於canvas的大小,什麼事情都不會發生,而大於canvas的大小,scrollbar就會可以拖曳canvas裡面這個矩形。可以理解為定義出canvas內容的真正大小。
Create items on canvas
當我們有canvas的時候就可以在上面畫圖了,因為我們只要畫簡單的線,所以如果是畫上矩形、圓形等形狀請參考其他資料,基本概念是一樣的。
# bind the funcitons
canvas.bind("<Button-1>",lambda event, canvas=canvas,item=0: xy(event, canvas))
canvas.bind("<B1-Motion>",lambda event, canvas=canvas: add_line(event, canvas))
canvas.bind("<B1-ButtonRelease>",lambda event, canvas=canvas: done_stroke(event, canvas))
# use to identify the actual x,y on canvas that is pressed
lastx, lasty = 0,0
radius = 10
def xy(event,canvas):
global lastx,lasty,radius
lastx, lasty = canvas.canvasx(event.x),canvas.canvasy(event.y)
# draw the circle first
canvas.create_oval((lastx-radius, lasty-radius, lastx+radius, lasty+radius,),fill="white", tags='eraser',outline="")
# add line by white to be a eraser
def add_line(event, canvas):
global lastx,lasty,radius
x, y = canvas.canvasx(event.x),canvas.canvasy(event.y)
# the tkinter draw
canvas.create_line((lastx,lasty,x,y), fill="white",width=radius*2,tags='eraser')
canvas.create_oval((x-radius, y-radius, x+radius, y+radius,),fill="white", tags='eraser',outline="")
lastx, lasty = x, y
# Do something when the line is done
def done_stroke(event, canvas):
# draw the circle last
global lastx,lasty,radius
canvas.create_oval((lastx-radius, lasty-radius, lastx+radius, lasty+radius,),fill="white", tags='eraser',outline="")
# whe configure the line after that
pass
首先,因為我們的canvas是可以捲動的,所以得到的座標需要利用canvasx()
canvasy()
這兩個來得到canvas上面正確的座標,這樣畫布移動的時候他才會畫在正確的地方。然後我們偵測滑鼠移動的時候就會畫出線段,create_line()
這個函式是可以給一長串座標最後在畫線的,這樣畫出的線條會必較圓潤也不會因為大轉折破碎。但是因為我們需要的是及時可以看到的線條,所以我們的做法是畫上圓圈,在點下去跟放開滑鼠的時候畫上圓圈,中間畫的時候也加上這樣的線條看起來就會非常圓滑。
其他創建圖形基本上也差不多
canvas.create_item((x,y)...,option='value'...)
有這些東西可以創建:
- ARC ITEMS
- BITMAP ITEMS
- IMAGE ITEMS
- LINE ITEMS
- OVAL ITEMS
- POLYGON ITEMS
- TEXT ITEMS
- WINDOW ITEMS
所以可以在canvas做出很多變化。
Canvas tag
那上面那段可以看到我們給我們的線段加上tag="eraser"
這樣的tag,所有在canvas建立的圖形、線都是物件,所以你可以對單一的圖案進行刪除、上下疊加的不同。那最方便的就是加上很像HTML的class的東西,進行一次多項的變更。
例如你想對建立的白色線段(還有圓圈)改變粗細、顏色或是刪除它們,可以這樣做
# change width,doesn't work with oval
canvas.itemconfigure('eraser',width=1)
# change color
canvas.itemconfigure('eraser',fill="blue")
# delete all items with tag 'eraser'
canvas.delete('eraser')
如果想對tag做操作也是可以的
# delete the tag with the items selected
# dtag(tagOrId,tagToDelet)
# delete eraser tag on all items
canvas.dtag('all','eraser')
# add tags see manpage for more info
# add white tag to items with eraser tag
canvas.addtag('white','withtag','eraser')
更強的是,可以在這些items上bind函式,也就是理論上可以用canvas畫出整個UI介面,不過這樣就比較難管理
cavas.tag_bind('eraser',"<Button-1", lambda x: func(x))
Theme
this will be finished, last edited at 2020/2/29, I may want to move the Theme section to another post.
Table of Content