# File: canvasarrow.py
# http://infohost.nmt.edu/tcc/help/pubs/tkinter//canvas.html
from tkinter import *
from tkinter import ttk
from demopanels import MsgPanel, SeeDismissPanel
class CanvasArrowheadDemo(ttk.Frame):
def __init__(self, isapp=True, name='canvasarrowheaddemo'):
ttk.Frame.__init__(self, name=name)
self.pack(expand=Y, fill=BOTH)
self.master.title('Canvas Arrowhead Editor Demo')
self.isapp = isapp
self._create_widgets()
def _create_widgets(self):
if self.isapp:
MsgPanel(self,
["This widget allows you to experiment with different ",
"widths and arrowhead shapes for lines in canvases. ",
"To change the line width or the shape of the arrowhead, ",
"drag any of the three boxes attached to the oversized ",
"arrow. The arrows on the right give examples at normal ",
"scale. The text at the bottom shows the configuration ",
"options as you'd enter them for a canvas line item."])
SeeDismissPanel(self)
self._create_demo_panel()
def _create_demo_panel(self):
demoPanel = Frame(self)
demoPanel.pack(side=TOP, fill=BOTH, expand=Y)
self.canvas = Canvas(width=500, height=350, relief=SUNKEN,
borderwidth=2)
self.canvas.pack(in_=demoPanel, expand=Y, fill=BOTH)
self._define_arrow() # setup arrow dimensions and appearance
self._arrow_setup(None) # draw the arrow and descriptions
self._add_bindings() # bind the reshape rectangles
def _define_arrow(self):
# arrow feature definitions
# (a,b,c) define the arrowhead's shape
# 'motion' - method to use during drag
# of the active reshape rectangle
# 'x1' - used to position descriptive text
# 'x2' - used to position arrowhead
# 'y' - used to position arrowhead
# 'width' - width of arrowhead shaft
# 'box', 'active' - reshape rectangle styles
# value are used/re-defined during arrow edits
d = {'a': 8,
'b': 10,
'c': 3,
'motion': None,
'x1': 40,
'x2': 350,
'y': 150,
'width': 2,
'box': {'fill': ''},
'active': {'fill': 'red'}}
self.canvas.arrowInfo = d
# ================================================================================
# Canvas bindings
# ================================================================================
def _add_bindings(self):
# apply reshape rectangle fill colours on mouse enter/leave
self.canvas.tag_bind('box','<Enter>', self._box_enter)
self.canvas.tag_bind('box', '<Leave>', self._box_leave)
# ignore reshape rectangle enter/leave while rect is dragged
self.canvas.tag_bind('box', '<B1-Enter>', ' ')
self.canvas.tag_bind('box', '<B1-Leave>', ' ')
# capture selection of reshaping rectangles
self.canvas.tag_bind('vertex', '<1>', self._set_motion)
self.canvas.tag_bind('tip', '<1>', self._set_motion)
self.canvas.tag_bind('shaft', '<1>', self._set_motion)
# handle reshape rectangle dragging
self.canvas.bind('<B1-Motion>', lambda evt: self.canvas.arrowInfo['motion'](evt))
self.canvas.bind('<Button1-ButtonRelease>', self._arrow_setup)
# ================================================================================
# Bound methods - handle the arrow reshaping edits
# ================================================================================
def _set_motion(self, evt):
tags = self.canvas.gettags('current')
if 'box' not in tags:
return
# use Python's ability to reference a function through a variable to
# assign the appropriate motion method to the arrowInfo 'motion'
# dictionary key; the assigned method will be called when
# <B1-Motion> is detected
for t in tags:
if t == 'vertex':
self.canvas.arrowInfo['motion'] = self._move_vertex
elif t == 'tip':
self.canvas.arrowInfo['motion'] = self._move_tip
elif t == 'shaft':
self.canvas.arrowInfo['motion'] = self._move_shaft
def _move_vertex(self, evt):
# handle drag of the vertex reshape rectangle
# limited to horizontal motion
v = self.canvas.arrowInfo
newA = (v['x2'] + 5 - round(self.canvas.canvasx(evt.x)))//10
if newA < 0: newA = 0
if newA > 25: newA = 25
if newA != v['a']:
self.canvas.move('vertex', 10*(v['a']-newA), 0)
v['a'] = newA
def _move_tip(self, evt):
# handle drag of the tip reshape rectangle
# the tip can be dragged horizontally and vertically
v = self.canvas.arrowInfo
newB = (v['x2'] + 5 - round(self.canvas.canvasx(evt.x)))//10
if newB < 0: newB = 0
if newB > 25: newB = 25
newC = (v['y'] + 5 - round(self.canvas.canvasy(evt.y)) - 5 * v['width'])//10
if newC < 0: newC = 0
if newC > 20: newC = 20
if newB != v['b'] or newC != v['c']:
self.canvas.move('tip',
10*(v['b']-newB),
10*(v['c']-newC))
v['b'] = newB
v['c'] = newC
def _move_shaft(self, evt):
# handle drag of shaft reshape rectangle
# limited to vertical motion
v = self.canvas.arrowInfo
newWidth = (v['y'] + 2 - round(self.canvas.canvasy(evt.y)))//5
if newWidth < 0: newWidth = 0
if newWidth > 20: newWidth = 20
if newWidth != v['width']:
self.canvas.move('shaft', 0, 5*(v['width'] - newWidth))
v['width'] = newWidth
def _box_enter(self, evt):
# set fill colour to 'active' style
self.canvas.itemconfigure('current', self.canvas.arrowInfo['active'])
def _box_leave(self, evt):
# set fill colour to 'normal' style
self.canvas.itemconfigure('current', self.canvas.arrowInfo['box'])
def _arrow_setup(self, evt):
# this method is called when the canvas is created and
# whenever the arrow is edited; all objects are deleted
# and redrawn with each edit
# assign canvas and arrowInfo to temp variables for
# easier reading
c = self.canvas
v = self.canvas.arrowInfo
tags = c.gettags('current') # save existing tags, if any
c.delete(ALL) # remove all objects
# Create the arrow shaft and head
c.create_line(v['x1'], v['y'], v['x2'], v['y'],
fill='SkyBlue1',
arrow=LAST, width=10*v['width'],
arrowshape=(10*v['a'], 10*v['b'], 10*v['c']))
# draw black outline around arrowhead
xtip = v['x2'] - 10*v['b']
deltaY = 10*v['c'] + 5*v['width']
c.create_line(v['x2'], v['y'], xtip, v['y']+deltaY,
v['x2'] - 10*v['a'], v['y'], xtip, v['y']-deltaY,
v['x2'], v['y'], width=2, capstyle=ROUND,
joinstyle=ROUND)
# create boxes for reshaping the arrow
c.create_rectangle(v['x2']-10*v['a']-5, v['y']-5,
v['x2']-10*v['a']+5, v['y']+5,
outline='black', width=1,
tags=('vertex', 'box'))
c.create_rectangle(xtip-5, v['y']-deltaY-5,
xtip+5, v['y']-deltaY+5,
outline='black', width=1,
tags=('tip', 'box'))
c.create_rectangle(v['x1']-5, v['y']-5 * v['width']-5,
v['x1']+5, v['y']-5 * v['width']+5,
outline='black', width=1,
tags=('shaft', 'box'))
# if a reshape box is selected, set it to 'active' style
for t in tags:
if t in ('vertex', 'tip', 'shaft'):
c.itemconfigure('current', v['active'])
# create dividing line on the right
c.create_line(v['x2']+50, 0, v['x2']+50, 1000, width=2)
# create 3 arrows, normal size, with same parameters
start = v['x2']+100
arrowShape = (v['a'], v['b'], v['c'])
c.create_line(start, v['y']-125, start, v['y']-75,
width=v['width'], arrow=BOTH,
arrowshape=arrowShape)
c.create_line(start-25, v['y'], start+25, v['y'],
width=v['width'], arrow=BOTH,
arrowshape=arrowShape)
c.create_line(start-25, v['y']+75, start+25, v['y']+125,
width=v['width'], arrow=BOTH,
arrowshape=arrowShape)
# create small descriptive arrows with text
arrowShape = (5,5,2)
# half height of arrow head
# (changes when 'tip' box is dragged)
start = v['x2'] + 10
c.create_line(start, v['y']-5*v['width'], start, v['y']-deltaY,
arrow=BOTH, arrowshape=arrowShape)
c.create_text(v['x2']+15, v['y']-deltaY+5*v['c'],
text=v['c'], anchor=W)
# width of shaft (changes when 'shaft' box is dragged)
start = v['x1'] - 10
c.create_line(start, v['y']-5*v['width'], start, v['y']+5*v['width'],
arrow=BOTH, arrowshape=arrowShape)
c.create_text(v['x1']-15, v['y'], text=v['width'], anchor=E)
# centre width of arrowhead (changes when 'vertex' box is dragged)
start = v['y'] + 5 * v['width'] + 10 * v['c'] + 10
c.create_line(v['x2']-10*v['a'], start, v['x2'], start,
arrow=BOTH, arrowshape=arrowShape)
c.create_text(v['x2']-5*v['a'], start+5, text=v['a'], anchor=N)
# full width of arrowhead (changes when 'tip' box is dragged)
start = start + 25
c.create_line(v['x2']-10*v['b'], start, v['x2'], start,
arrow=BOTH, arrowshape=arrowShape)
c.create_text(v['x2']-5*v['b'], start+5, text=v['b'], anchor=N)
# create text describing current values of arrowInfo keys: a, b, c, width
c.create_text(v['x1'], 310, text='width: {}'.format(v['width']),
anchor=W, font=('Helv', 18))
c.create_text(v['x1'], 330,
text='arrowshape: ({}, {}, {})'.format(v['a'],v['b'],v['c']),
anchor=W, font=('Helv', 18))
if __name__ == '__main__':
CanvasArrowheadDemo().mainloop()
Sunday, August 12, 2012
Tkinter Canvas Arrow Demo
This code is based on the Tcl arrow.tcl demo; it creates a canvas widget with a large line and arrowhead that can be reshaped by the user.
Labels:
Tkinter Canvas,
Tkinter Demos