Enabling Cursor User Interaction#
Cursor on HSV Wheel at Red#
As stated before we need to bind the cursor to the mouse buttons:
canHsv.bind('<Button-1>',self.Click_ring)
canHsv.tag_bind('ring','<B1-Motion>',self.Drag_ring)
Make the event handlers part of HsvSelect class. Make sure that the necessary math functions are imported to enable the polar conversions. The events use the coordinates of the cursor when the mouse is clicked then translated into coordinates relative to the wheel centre. Ensure that the coordinates are within the colour wheel. When clicking inside the wheel seek the ring, then move the ring to the mouse. Note the hue and saturation values and update the tk variables, then redraw the gradients.
def click_ring(self, event):
X = event.x
Y = event.y
ring_radius = self.ring_radius
cx = self.wheel_w // 2
dx, dy = X - cx, Y - cx
rad = self.wheel_iw // 2
if (dx)**2 + (dy)**2 < rad**2:
for search in self.can_hsv.find_withtag("ring"):
self.can_hsv.coords(search, X - ring_radius, Y - ring_radius,
X + ring_radius, Y + ring_radius)
hue, sat = cart2polar(X, Y, self.wheel_w, self.wheel_iw)
self.hvar.set(hue)
self.svar.set(sat)
ring = hue, sat
self.door_bell(ring)
def drag_ring(self, event):
X = event.x
Y = event.y
ring_radius = self.ring_radius
cx = self.wheel_w // 2
dx, dy = X - cx, Y - cx
rad = self.wheel_iw // 2
if (dx)**2 + (dy)**2 < rad**2:
self.can_hsv.coords(self.ring, X - ring_radius, Y - ring_radius,
X + ring_radius, Y + ring_radius)
hue, sat = cart2polar(X, Y, self.wheel_w, self.wheel_iw)
self.hvar.set(hue)
self.svar.set(sat)
ring = hue, sat
self.door_bell(ring)
The door_bell function is used to draw the saturation and value gradients:
def door_bell(self, ring):
# calls from bind
hue, sat = ring
value = self.vvar.get()
from_colour = hsv_to_rgb(*(hue, 0, value))
to_colour = hsv_to_rgb(*(hue, 100, value))
draw_gradient(self.scan, from_colour, to_colour,
width=self.canvas_w, height=self.canvas_h)
from_colour = hsv_to_rgb(*(hue, sat, 0))
to_colour = hsv_to_rgb(*(hue, sat, 100))
draw_gradient(self.vcan, from_colour, to_colour,
width=self.canvas_w, height=self.canvas_h)
Clean up some of those magic numbers, the colour wheel image size, the colour wheel size and ring radius and width.
Move the Ring using Scale#
At present the ring changes hue and saturation, reverse the process so that these components change the ring position, obviously value has no effect. Add the following code to both the handle functions for hue and saturation:
X, Y = polar2cart(hue, sat, self.wheel_w, self.wheel_iw)
ring_radius = self.ring_radius
for i in self.can_hsv.find_withtag("ring"):
self.can_hsv.coords(
i,
X - ring_radius,
Y - ring_radius,
X + ring_radius,
Y + ring_radius)
After those changes our user can move the ring either by clicking on it and
dragging or else clicking in the colour wheel. Also the ring should change
in position whenever the hue or saturation scale or spinbox is altered.
There is a bit of difficulty in starting to drag the ring, maybe a bit of
feedback is required to show that the ring is ready. Add
activeoutline to see the ring change in colour when the mouse cursor
passes over the ring, what is apparent is that the centre is not activated
only when the mouse passes over the ring itself.
Show/Hide Code 11user2ring.py
""" Construction three gradients in hsv using PPM image
spinbox validation, working with modified Scale
adding colour wheel and creating cursor
"""
from tkinter import Tk, Canvas, Label, IntVar, Frame, PhotoImage
from tkinter.ttk import LabelFrame, Scale, Style, Spinbox
from PIL import Image, ImageDraw, ImageTk
from colourTools import draw_gradient, hsv_to_rgb, hue_gradient, circle, \
polar2cart, cart2polar
def sb_okay(text, input_, upper): # '%P','%S'
"""Validation for colour components
Parameters
----------
text : str
text if accepted
input_ : str
current input
upper : int
upper limit
Returns
-------
boolean
"""
if input_.isdigit():
if 0 < len(text) < 4:
return bool(0 <= int(text) <= int(upper))
return False
return False
class TtkScale(Scale):
"""Class to draw themed Scale widget
Parameters
----------
parent : str
parent widget
from_ : int
start of scale
to : int
end of scale
length : int
length in pixels
orient : str
orientation
variable : str
tk variable
digits : int
length variable when converted to string
tickinterval : float or int
how many digits show up in tick interval
sliderlength : int
what it says
command : str
procedure called when slider moves
"""
def __init__(self, parent, from_=0, to=255, length=300, orient='horizontal',
variable=0, digits=None, tickinterval=None, sliderlength=16,
command=None, enlargement=1):
self.from_ = from_
self.to = to
self.variable = variable
super().__init__(parent, length=length + sliderlength, orient=orient,
variable=variable, from_=from_, to=to, command=command)
self.digits = digits
self.length = length
self.e = enlargement
self.build(parent, from_, to, sliderlength, tickinterval, length)
def build(self, parent, from_, to, sliderlength, tickinterval, length):
"""Create ticks
Parameters
----------
parent : str
parent widget
from_ : int
start of scale
to : int
end of scale
length : int
length in pixels
tickinterval : float or int
"""
sc_range = to - from_
if tickinterval:
for i in range(from_, to + 2, tickinterval):
item = Label(parent, text=i, bg='#EFFEFF')
item.place(in_=self, bordermode='outside',
relx=sliderlength*self.e / length*self.e / 2 +
i / sc_range * (1 - sliderlength*self.e / length*self.e),
rely=1, anchor='n')
class HsvSelect:
"""Class to construct hsv gradients and colour wheel
Parameters
----------
fr0 : str
parent widget
Returns
-------
None
"""
def __init__(self, fr0, enlargement):
self.fr0 = fr0
self.e = enlargement
self.hvar = IntVar()
self.svar = IntVar()
self.vvar = IntVar()
self.scale_l = 300*self.e
self.canvas_w = self.scale_l
self.canvas_h = 26*self.e
self.cursor_w = 16*self.e
self.wheel_w = 317*self.e
self.wheel_iw = 299*self.e
self.ring_radius = 10*self.e
self.ring_width = 3*self.e
self.hvar.set(0)
self.svar.set(100)
self.vvar.set(100)
self.build()
def hhandle(self, evt=None):
"""command callback for hue
Parameters
----------
None
Results
-------
None
"""
hue = self.hvar.get()
self.hvar.set(int(0.5 + hue))
sat = self.svar.get()
value = self.vvar.get()
from_colour = hsv_to_rgb(*(hue, 0, value))
to_colour = hsv_to_rgb(*(hue, 100, value))
draw_gradient(self.scan, from_colour, to_colour,
width=self.canvas_w, height=self.canvas_h)
from_colour = hsv_to_rgb(*(hue, sat, 0))
to_colour = hsv_to_rgb(*(hue, sat, 100))
draw_gradient(self.vcan, from_colour, to_colour,
width=self.canvas_w, height=self.canvas_h)
X, Y = polar2cart(hue, sat, self.wheel_w, self.wheel_iw)
ring_radius = self.ring_radius
for i in self.can_hsv.find_withtag("ring"):
self.can_hsv.coords(
i,
X - ring_radius,
Y - ring_radius,
X + ring_radius,
Y + ring_radius)
def shandle(self, evt=None):
"""command callback for saturation
Parameters
----------
None
Results
-------
None
"""
hue = self.hvar.get()
sat = self.svar.get()
self.svar.set(int(0.5 + sat))
value = self.vvar.get()
from_colour = hsv_to_rgb(*(hue, 0, value))
to_colour = hsv_to_rgb(*(hue, 100, value))
draw_gradient(self.scan, from_colour, to_colour,
width=self.canvas_w, height=self.canvas_h)
from_colour = hsv_to_rgb(*(hue, sat, 0))
to_colour = hsv_to_rgb(*(hue, sat, 100))
draw_gradient(self.vcan, from_colour, to_colour,
width=self.canvas_w, height=self.canvas_h)
X, Y = polar2cart(hue, sat, self.wheel_w, self.wheel_iw)
ring_radius = self.ring_radius
for i in self.can_hsv.find_withtag("ring"):
self.can_hsv.coords(
i,
X - ring_radius,
Y - ring_radius,
X + ring_radius,
Y + ring_radius)
def vhandle(self, evt=None):
"""command callback for value
Parameters
----------
None
Results
-------
None
"""
hue = self.hvar.get()
sat = self.svar.get()
value = self.vvar.get()
self.vvar.set(int(0.5 + value))
from_colour = hsv_to_rgb(*(hue, 0, value))
to_colour = hsv_to_rgb(*(hue, 100, value))
draw_gradient(self.scan, from_colour, to_colour,
width=self.canvas_w, height=self.canvas_h)
from_colour = hsv_to_rgb(*(hue, sat, 0))
to_colour = hsv_to_rgb(*(hue, sat, 100))
draw_gradient(self.vcan, from_colour, to_colour,
width=self.canvas_w, height=self.canvas_h)
def door_bell(self, ring):
"""Calling procedure from cursor binds
Parameters
----------
ring : tuple of int
hue, saturation values
"""
# calls from bind
hue, sat = ring
value = self.vvar.get()
from_colour = hsv_to_rgb(*(hue, 0, value))
to_colour = hsv_to_rgb(*(hue, 100, value))
draw_gradient(self.scan, from_colour, to_colour,
width=self.canvas_w, height=self.canvas_h)
from_colour = hsv_to_rgb(*(hue, sat, 0))
to_colour = hsv_to_rgb(*(hue, sat, 100))
draw_gradient(self.vcan, from_colour, to_colour,
width=self.canvas_w, height=self.canvas_h)
def build(self):
"""widget construction
Parameters
----------
None
Results
-------
None
"""
fr4 = LabelFrame(self.fr0, text='hsv')
fr4.grid(column=2, row=0, sticky='ns')
hl0 = Label(fr4, text='hue ')
hl0.grid(column=0, row=0, sticky='s')
self.hcan = Canvas(fr4, width=self.canvas_w, height=self.canvas_h, bd=0,
highlightthickness=0)
self.hcan.grid(column=1, row=0, sticky='s')
hsc = TtkScale(fr4, from_=0, to=360, variable=self.hvar, orient='horizontal',
length=self.scale_l, command=self.hhandle, tickinterval=30,
enlargement=1)
hsc.grid(column=1, row=1, sticky='nw')
vcmdsb = root.register(sb_okay)
hsb = Spinbox(fr4, from_=0, to=360, textvariable=self.hvar, validate='key',
validatecommand=(vcmdsb, '%P', '%S', 360), command=self.hhandle, width=5)
hsb.grid(column=2, row=1, sticky='nw')
hsb.bind('<KeyRelease>', self.checksbh)
hel = Label(fr4, height=1)
hel.grid(column=2, row=2)
sl0 = Label(fr4, text='sat ')
sl0.grid(column=0, row=3)
self.scan = Canvas(fr4, width=self.canvas_w, height=self.canvas_h, bd=0,
highlightthickness=0)
self.scan.grid(column=1, row=3, sticky='s')
ssc = TtkScale(fr4, from_=0, to=100, variable=self.svar, orient='horizontal',
length=self.scale_l, command=self.shandle, tickinterval=10,
enlargement=1)
ssc.grid(column=1, row=4, sticky='nw')
ssb = Spinbox(fr4, from_=0, to=100, textvariable=self.svar, validate='key',
validatecommand=(vcmdsb, '%P', '%S', 100), command=self.shandle, width=5)
ssb.grid(column=2, row=4, sticky='nw')
ssb.bind('<KeyRelease>', self.checksb100)
sel = Label(fr4, height=1)
sel.grid(column=2, row=5)
vl0 = Label(fr4, text='value')
vl0.grid(column=0, row=6, sticky='s')
self.vcan = Canvas(fr4, width=self.canvas_w, height=self.canvas_h, bd=0,
highlightthickness=0)
self.vcan.grid(column=1, row=6, sticky='n')
vsc = TtkScale(fr4, from_=0, to=100, variable=self.vvar, orient='horizontal',
length=self.scale_l, command=self.vhandle, tickinterval=10,
enlargement=1)
vsc.grid(column=1, row=7, sticky='nw')
vsb = Spinbox(fr4, from_=0, to=100, textvariable=self.vvar, validate='key',
validatecommand=(vcmdsb, '%P', '%S', 100), command=self.vhandle, width=5)
vsb.grid(column=2, row=7, sticky='nw')
vsb.bind('<KeyRelease>', self.checksb100)
vel = Label(fr4, height=1)
vel.grid(column=2, row=8)
# assume initial setting 0,100,100 hsv
to_colour = hsv_to_rgb(*(0, 100, 100))
hue_gradient(self.hcan, width=self.canvas_w, height=self.canvas_h)
draw_gradient(self.scan, (255, 255, 255), to_colour,
width=self.canvas_w, height=self.canvas_h)
draw_gradient(self.vcan, (0, 0, 0), to_colour,
width=self.canvas_w, height=self.canvas_h)
self.can_hsv = can_hsv = Canvas(fr4, width=self.wheel_w,
height=self.wheel_w, bg='#d9d9d9')
can_hsv.grid(column=1, row=9, pady=25*self.e, sticky='n')
self.hsv_gamut = PhotoImage(file='../../figures/colour_wheel'+str(self.e)+'.png')
can_hsv.create_image(0, 0, anchor='nw', image=self.hsv_gamut)
self.ring = circle(can_hsv, 307*self.e, 158*self.e, self.ring_radius,
width=self.ring_width, activeoutline='#555555', tags='ring')
can_hsv.bind('<Button-1>', self.click_ring)
can_hsv.tag_bind('ring', '<B1-Motion>', self.drag_ring)
def click_ring(self, event):
"""Procedure called when mouse clicks in colour wheel
Parameters
----------
evt : str
bind handle
Returns
-------
calls handle for cursor
"""
X = event.x
Y = event.y
ring_radius = self.ring_radius
cx = self.wheel_w // 2
dx, dy = X - cx, Y - cx
rad = self.wheel_iw // 2
if (dx)**2 + (dy)**2 < rad**2:
for search in self.can_hsv.find_withtag("ring"):
self.can_hsv.coords(search, X - ring_radius, Y - ring_radius,
X + ring_radius, Y + ring_radius)
hue, sat = cart2polar(X, Y, self.wheel_w, self.wheel_iw)
self.hvar.set(hue)
self.svar.set(sat)
ring = hue, sat
self.door_bell(ring)
def drag_ring(self, event):
"""Procedure called when mouse drags cursor
Parameters
----------
evt : str
bind handle
Returns
-------
calls handle for cursor
"""
X = event.x
Y = event.y
ring_radius = self.ring_radius
cx = self.wheel_w // 2
dx, dy = X - cx, Y - cx
rad = self.wheel_iw // 2
if (dx)**2 + (dy)**2 < rad**2:
self.can_hsv.coords(self.ring, X - ring_radius, Y - ring_radius,
X + ring_radius, Y + ring_radius)
hue, sat = cart2polar(X, Y, self.wheel_w, self.wheel_iw)
self.hvar.set(hue)
self.svar.set(sat)
ring = hue, sat
self.door_bell(ring)
def checksbh(self, evt):
"""Procedure called by hue spinbox
Parameters
----------
evt : str
bind handles
Results
-------
None
"""
hue = self.hvar.get()
sat = self.svar.get()
value = self.vvar.get()
from_colour = hsv_to_rgb(*(hue, 0, value))
to_colour = hsv_to_rgb(*(hue, 100, value))
draw_gradient(self.scan, from_colour, to_colour,
width=self.canvas_w, height=self.canvas_h)
from_colour = hsv_to_rgb(*(hue, sat, 0))
to_colour = hsv_to_rgb(*(hue, sat, 100))
draw_gradient(self.vcan, from_colour, to_colour,
width=self.canvas_w, height=self.canvas_h)
def checksb100(self, evt):
"""Procedure called by s,v spinboxes
Parameters
----------
evt : str
bind handles
Results
-------
None
"""
hue = self.hvar.get()
sat = self.svar.get()
value = self.vvar.get()
from_colour = hsv_to_rgb(*(hue, 0, value))
to_colour = hsv_to_rgb(*(hue, 100, value))
draw_gradient(self.scan, from_colour, to_colour,
width=self.canvas_w, height=self.canvas_h)
from_colour = hsv_to_rgb(*(hue, sat, 0))
to_colour = hsv_to_rgb(*(hue, sat, 100))
draw_gradient(self.vcan, from_colour, to_colour,
width=self.canvas_w, height=self.canvas_h)
if __name__ == "__main__":
root = Tk()
winsys = root.tk.call("tk", "windowingsystem")
BASELINE = 1.33398982438864281 if winsys != 'aqua' else 1.000492368291482
scaling = root.tk.call("tk", "scaling")
enlargement = e = int(scaling / BASELINE + 0.5)
img = Image.new("RGBA", (16*e, 10*e), '#00000000')
trough = ImageTk.PhotoImage(img)
# constants for creating upward pointing arrow
WIDTH = 17*e
HEIGHT = 17*e
OFFSET = 5*e
ST0 = WIDTH // 2, HEIGHT - 1 - OFFSET
LIGHT = 'GreenYellow'
MEDIUM = 'LawnGreen'
DARK = '#5D9B90'
# normal state
im = Image.new("RGBA", (WIDTH, HEIGHT), '#00000000')
rdraw = ImageDraw.Draw(im)
rdraw.polygon([ST0[0], ST0[1], 0, HEIGHT - 1,
WIDTH - 1, HEIGHT - 1], fill=LIGHT)
rdraw.polygon([ST0[0], ST0[1], ST0[0], 0, 0, HEIGHT - 1], fill=MEDIUM)
rdraw.polygon([ST0[0], ST0[1], WIDTH - 1,
HEIGHT - 1, ST0[0], 0], fill=DARK)
slider = ImageTk.PhotoImage(im)
# pressed state
imp = Image.new("RGBA", (WIDTH, HEIGHT), '#00000000')
draw = ImageDraw.Draw(imp)
draw.polygon([ST0[0], ST0[1], 0, HEIGHT - 1,
WIDTH - 1, HEIGHT - 1], fill=LIGHT)
draw.polygon([ST0[0], ST0[1], ST0[0], 0, 0, HEIGHT - 1], fill=DARK)
draw.polygon([ST0[0], ST0[1], WIDTH - 1,
HEIGHT - 1, ST0[0], 0], fill=MEDIUM)
sliderp = ImageTk.PhotoImage(imp)
style = Style()
style.theme_settings('default', {
'Horizontal.Scale.trough': {"element create":
('image', trough,
{'border': 0, 'sticky': 'wes'})},
'Horizontal.Scale.slider': {"element create":
('image', slider,
('pressed', sliderp),
{'border': 3, 'sticky': 'n'})}})
style.theme_use('default')
style.configure('TSpinbox', arrowsize=10*e)
#root.geometry("500x600+200+100")
fr = Frame(root)
fr.grid(row=0, column=0, sticky='nsew')
HsvSelect(fr, enlargement)
root.mainloop()
Now that the HSV part is operational join it to the RGB part, modifying some of the original functions as necessary.