3.10 tkinter 之 Canvas

畫布(canvas)小部件管理著 2D 圖形對(duì)象(lines, circles, images, 其他更多小部件)組成的集合。Canvas 小部件是經(jīng)典的 Tk 小部件,不是 Ttk 小部件。

創(chuàng)建方法是:

from tkinter import Canvas
canvas = Canvas(parent)

注意:在 Canvas 中的坐標(biāo)系是以左上角作為原點(diǎn) (0,0),水平向右為 x 軸正方向,垂直向下為 y 軸正方向。

1 創(chuàng)建線段

創(chuàng)建線段是以 (x_0, y_0, x_1, y_1) 的形式傳入 creata_line 函數(shù)的。其中 (x_0, y_0),(x_1, y_1) 分別為起點(diǎn)和終點(diǎn)。比如:

item_id = canvas.create_line(10, 10, 200, 50)

函數(shù) creata_line 的返回值 item_id 是一個(gè)整數(shù),被用來(lái)作為該對(duì)象的引用的唯一標(biāo)識(shí)。

下面看一個(gè)例子:

class Segment(Canvas):
    def __init__(self, master=None, **kw):
        super().__init__(master=master, **kw)
        self.lastx, self.lasty = 0, 0
        self.bind("<Button-1>", self.xy) # 綁定鼠標(biāo)左鍵
        self.bind("<B1-Motion>", self.add_line) # 拖動(dòng)鼠標(biāo)左鍵
        
    def xy(self, event):
        '''更新坐標(biāo)'''
        self.lastx, self.lasty = event.x, event.y
        
    def add_line(self, event):
        '''畫一條線段'''
        self.create_line(self.lastx, self.lasty, event.x, event.y, fill='red', width=3)
        self.xy(event)
    
    def layout(self):
        self.grid(column=0, row=0, sticky='nwes')

root = Tk()
root.columnconfigure(0, weight=1)
root.rowconfigure(0, weight=1)
segment = Segment(root)
segment.layout()
root.mainloop()

該例子實(shí)現(xiàn)了在畫布上拖動(dòng)鼠標(biāo)左鍵來(lái)畫線段的目標(biāo)。其中的參數(shù) width 表示線段的寬度,fill 表示使用的畫筆的顏色。

可以做如下修改:

from tkinter import IntVar

class App(Tk):
    def __init__(self):
        super().__init__()
        self.columnconfigure(0, weight=1)
        self.rowconfigure(0, weight=1)
        self.segment = Segment(self)
        self.id_var = IntVar()
        
    def modify(self):
        item_id = self.id_var.get()
        self.segment.itemconfigure(item_id, fill='blue', width=10)
        
    def layout(self):
        entry = ttk.Entry(textvariable=self.id_var)
        button = ttk.Button(text='modify', command=self.modify)
        self.segment.layout()
        entry.grid(column=0, row=1)
        button.grid(column=1, row=1)
        
app = App()
app.layout()
app.mainloop()

效果圖見圖1:

圖1 Canvas 的一個(gè)例子

只要寫入文本框數(shù)字,然后點(diǎn)擊按鈕 modify,便可 使用 Canvas.itemconfigure 函數(shù)修改 item_id 對(duì)應(yīng)的點(diǎn)的配置。

2 綁定 item_id

除了可以使用 bind 函數(shù)綁定事件之外,還可以使用 tag_bind 函數(shù)綁定 item_id 來(lái)觸發(fā)事件。

下面的代碼可以使用鼠標(biāo)點(diǎn)擊顏色選擇塊來(lái)切換畫筆的顏色:

class Segment(Canvas):
    def __init__(self, master=None, **kw):
        super().__init__(master=master, **kw)
        self.lastx, self.lasty = 0, 0
        self.color = "black"
        self.bind("<Button-1>", self.xy)
        self.bind("<B1-Motion>", self.add_line)
        
    def set_color(self, new_color):
        self.color = new_color
    
    def xy(self, event):
        '''更新坐標(biāo)'''
        self.lastx, self.lasty = event.x, event.y
        
    def add_line(self, event):
        self.create_line((self.lastx, self.lasty, event.x, event.y), fill=self.color)
        self.xy(event)

    def change(self):
        # 創(chuàng)建 3 個(gè)顏色選擇塊
        red_id = self.create_rectangle((10, 10, 30, 30), fill="red")
        blue_id = self.create_rectangle((10, 35, 30, 55), fill="blue")
        black_id = self.create_rectangle((10, 60, 30, 80), fill="black")
        # 綁定事件
        self.tag_bind(red_id, "<Button-1>", lambda x: self.set_color("red"))
        self.tag_bind(blue_id, "<Button-1>", lambda x: self.set_color("blue"))
        self.tag_bind(black_id, "<Button-1>", lambda x: self.set_color("black"))
    
root = Tk()
seg = Segment(root)
seg.grid()
seg.change()
root.mainloop()

效果圖:

圖2 可改變顏色的畫布

3 標(biāo)記:Tags

使用配置選項(xiàng) "tags" 可以為任何一個(gè) item_id 代表的對(duì)象做標(biāo)記,這里的標(biāo)記可以是一個(gè)或者多個(gè)。

為什么要做標(biāo)記呢?這是因?yàn)椋隽藰?biāo)記的 item_id 將會(huì)更方便管理。比如,您可以指定所有被標(biāo)記為 "line" 的 item_id 統(tǒng)一修改其畫筆顏色。

除了使用配置選項(xiàng) "tags" 創(chuàng)建標(biāo)記之外,您可以在 item_id 創(chuàng)建之后使用 add_tag 函數(shù)創(chuàng)建或者添加新的標(biāo)記。移除標(biāo)記可以使用方法 dtag。您也可以使用 gettags(item_id) 方式獲取 item_id 的全部標(biāo)記列表。可以使用 find_withtag 函數(shù)獲取指定的標(biāo)記的全部 item_id 列表。

直接看一個(gè)例子:

class Segment(Canvas):
    def __init__(self, master=None, **kw):
        super().__init__(master=master, **kw)
        self.lastx, self.lasty = 0, 0
        self.color = "黑色"
        self.bind("<Button-1>", self.xy)
        self.bind("<B1-Motion>", self.add_line)
        # 設(shè)定調(diào)色板的線寬
        self.itemconfigure('調(diào)色板', width=5) 
        # 釋放鼠標(biāo)觸發(fā)事件
        self.bind('<B1-ButtonRelease>', self.done_stroke)
    
    @property
    def color_map(self):
        return {
            '紅色': 'red',
            '藍(lán)色': 'blue',
            '黑色': 'black'
        }
        
    def set_color(self, new_color):
        self.color = new_color
        self.dtag('all', '被選中的調(diào)色板')
        self.itemconfigure('調(diào)色板', outline='white')
        self.addtag('被選中的調(diào)色板', 'withtag', f"{self.color}調(diào)色板")
        self.itemconfigure('被選中的調(diào)色板', outline='#999999')
    
    def xy(self, event):
        '''更新坐標(biāo)'''
        self.lastx, self.lasty = event.x, event.y
        
    def add_line(self, event):
        loc = (self.lastx, self.lasty, event.x, event.y)
        color = self.color_map[self.color]
        self.create_line(loc, fill=color, width=5, tags='當(dāng)前的線段')
        self.xy(event)
        
    def palette(self, loc, color):
        kw = {
            'fill': color,
            'tags': ('調(diào)色板', f'{color}調(diào)色板')
        }
        return self.create_rectangle(loc, **kw)

    def change(self):
        # 創(chuàng)建 3 個(gè)顏色選擇塊
        red_id = self.palette((10, 10, 30, 30), "red")
        blue_id = self.palette((10, 35, 30, 55), "blue")
        black_id = self.palette((10, 60, 30, 80), "black")
        # 添加標(biāo)記
        self.addtag('被選中的調(diào)色板', 'withtag', black_id)
        # 綁定事件
        self.tag_bind(red_id, "<Button-1>", lambda x: self.set_color("紅色"))
        self.tag_bind(blue_id, "<Button-1>", lambda x: self.set_color("藍(lán)色"))
        self.tag_bind(black_id, "<Button-1>", lambda x: self.set_color("黑色"))
        
    def done_stroke(self, event):
        self.itemconfigure('當(dāng)前的線段', width=1)
    
root = Tk()
seg = Segment(root)
seg.grid()
seg.change()
root.mainloop()

顯示的效果圖:

圖3 可改變線條的畫布

該例子展示了一種使用 tags 修改 item 的手段,達(dá)到的效果是:在鼠標(biāo)釋放之前線條比較粗,釋放之后線條會(huì)變細(xì)。

4 修改 item

我們可以使用 delete 方法刪除 item,使用 coords 方法改變 item 的尺寸和位置(允許變換坐標(biāo)系)??梢允褂?move 方法移動(dòng) item。還有 "raise""lower" 方法可以改變不同畫布的排列布局。比如:

4.1 改變 item 屬性

from tkinter import *

root=Tk()

cv=Canvas(root,bg='white')
rt1=cv.create_rectangle(10,10,110,110,tags=('r1','r2','r3'))
rt2=cv.create_rectangle(20,20,80,80,tags=('s1','s2','s3'))
rt3=cv.create_rectangle(30,30,70,70,tags=('y1','y2','y3'))

cv.tag_lower(rt3)
cv.tag_raise(rt1)
cv.itemconfig(cv.find_above(rt2),outline='red')
cv.itemconfig(cv.find_below(rt2),outline='green')
cv.pack()

root.mainloop()

4.2 刪除 item

root=Tk()

cv=Canvas(root,bg='white')
rt1=cv.create_rectangle(10,10,110,110,tags=('r1','r2','r3'))
rt2=cv.create_rectangle(20,20,80,80,tags=('s1','s2','s3'))
rt3=cv.create_rectangle(30,30,70,70,tags=('s1','y2','y3'))

cv.delete(rt1)
cv.delete('s1')
cv.pack()

root.mainloop()

4.3 縮放 item

root=Tk()
cv=Canvas(root,bg='white')
rt1=cv.create_rectangle(10,10,110,110,tags=('r1','r2','r3'))
cv.scale(rt1,0,0,1,2)
cv.pack()

root.mainloop()

5 滾動(dòng)鼠標(biāo)

在許多應(yīng)用程序中,您希望畫布大于屏幕上顯示的內(nèi)容。 您可以通過(guò) "xview" 和 "yview" 方法以通常的方式將水平和垂直滾動(dòng)條附加到畫布上。

至于畫布的大小,您既可以指定希望在屏幕上顯示的大小,也可以指定需要滾動(dòng)才能看到的畫布的完整大小。 畫布小部件的 "width" 和 "height" 配置選項(xiàng)將從幾何管理器請(qǐng)求給定的空間量。"scrollregion" 配置選項(xiàng)(例如 "0 0 1000 1000")告訴 Tk 畫布表面有多大。

針對(duì)鼠標(biāo)滾動(dòng),"canvasx" 和 "canvasy" 方法會(huì)將屏幕上的位置(正在報(bào)告的綁定)轉(zhuǎn)換為畫布上的實(shí)際點(diǎn)。如果將它們直接添加到事件綁定中(而不是從事件綁定中調(diào)用),請(qǐng)注意引用和替換,以確保在事件觸發(fā)時(shí)完成轉(zhuǎn)換。

下面的例子很好的說(shuō)明了這種機(jī)制:

class SegmentScroll(Segment):
    def __init__(self, master=None, **kw):
        super().__init__(master=master, **kw)
        self.master = master
        
    def scroll(self):
        self._h = ttk.Scrollbar(orient='horizontal')
        self._v = ttk.Scrollbar(orient='vertical')
        # 告訴 Tk 畫布表面有多大
        self['scrollregion'] = (0, 0, 1000, 1000)
        self.itemconfig('scrollregion', )
        self.configure(yscrollcommand=self._v.set, xscrollcommand=self._h.set)
        self._h['command'] = self.xview
        self._v['command'] = self.yview
        
    def custom(self):
        ttk.Sizegrip(root).grid(column=1, row=1, sticky=(S,E))
        self.grid(column=0, row=0, sticky=(N,W,E,S))
        self._h.grid(column=0, row=1, sticky=(W,E))
        self._v.grid(column=1, row=0, sticky=(N,S))
        self.master.grid_columnconfigure(0, weight=1)
        self.master.grid_rowconfigure(0, weight=1)
        
    def xy(self, event):
        self.lastx, self.lasty = self.canvasx(event.x), self.canvasy(event.y)
        
    def add_line(self, event):
        x, y = self.canvasx(event.x), self.canvasy(event.y)
        color = self.color_map[self.color]
        self.create_line((self.lastx, self.lasty, x, y), fill=color, width=5, tags='當(dāng)前的線段')
        self.xy(event)
        
root = Tk()
seg = SegmentScroll(root)
seg.change()
seg.scroll()
seg.custom()
root.mainloop()

顯示效果:

圖4 帶滾動(dòng)條和縮放的畫布

6 更改線段的樣式

我們也可以設(shè)定參數(shù) arrowarrowshape 來(lái)改變線段的樣式:

root = Tk()

cv = Meta(root, bg='white')
d = [(0, 'none'), (1, 'first'), (2, 'last'), (3, 'both')]
for i in d:
    cv.create_line((10, 10+i[0]*20, 110, 110+i[0]*20),
                   arrow=i[1], arrowshape='40 30 10')

cv.grid()
root.mainloop()

顯示效果圖:

圖5 畫出不同樣式的線段

還有:

root = Tk()

cv = Canvas(root, bg='white')
d = [(0, 'none', 'bevel'), (1, 'first', 'miter'),
     (2, 'last', 'round'), (3, 'both', 'round')]
for i in d:
    cv.create_line((10, 10+i[0]*20, 110, 110+i[0]*20),
                   arrow=i[1], arrowshape='8 10 3', joinstyle=i[2])

cv.grid()
root.mainloop()

效果圖:

圖6 直線的 joinstyle 參數(shù)設(shè)定

7 畫布的其他設(shè)定

Canvas 除了支持 "line", "rectangle",還支持 "oval", "arc", "polygon", "bitmap" (位圖,可用于分割物體), "image","text",甚至還支持 "window" 的嵌入。

7.1 繪制位圖

root = Tk()
self = Canvas(root, background='white')
bitmap = ('error', 'info', 'question', 'hourglass',
          "warning", "gray12",
         "gray25", "gray50", "gray75", "questhead")

for k, name in enumerate(bitmap):
    location = [20*(k+1)]*2
    self.create_bitmap(location, bitmap=name)
    
self.grid()
root.mainloop()
圖7 位圖的繪制

7.2 繪制多邊形

root = Tk()
self = Canvas(root)
# 點(diǎn)的坐標(biāo)
points = (10, 10), (10, 200), (90, 200), (200, 160)
self.create_polygon(points, fill='red')
    
self.grid()
root.mainloop()
圖8 繪制多邊形

7.3 繪制文本

root = Tk()
self = Canvas(root)
location = 50, 50
text = self.create_text(location, text='一個(gè)文本:永不言敗!', anchor='sw', fill='blue', font='italic 15')
# 選中文本
self.select_from(text, 5)
self.select_to(text, 8)
self.grid()
root.mainloop()
圖9 繪制文本

7.4 創(chuàng)建組件

from tkinter import Canvas, ttk, Tk

root = Tk()
self = Canvas(root)
def print_text():
    print("你好")
    
bt = ttk.Button(self, text='點(diǎn)我', command=print_text)
self.create_window((10, 10), window=bt, anchor='w')
self.create_line(30, 30, 50, 90)
self.grid()
root.mainloop()
圖10 創(chuàng)建組件
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容