Python Forum
[Tkinter] COMPLEX! Transparent Canvas Linux - Printable Version

+- Python Forum (https://python-forum.io)
+-- Forum: Python Coding (https://python-forum.io/forum-7.html)
+--- Forum: GUI (https://python-forum.io/forum-10.html)
+--- Thread: [Tkinter] COMPLEX! Transparent Canvas Linux (/thread-40211.html)



COMPLEX! Transparent Canvas Linux - AceScottie - Jun-20-2023

Im looking to make my code cross compatible with Linux.
Unfortunatly i have some bits of code that rely on the win32 module and windows API calls to properly maintain.

One of these piceces is to create a transparent Canvas background so i can draw a circle without having a square border behind it.
The code for this was copied from SO: https://stackoverflow.com/a/70150296/3310078

Im trying to find a way to duplicate this in Linux using X11 but cannt find and resources as a starting location.

Has anyone tried anything like this before who can point me in the correct direction ?

import tkinter as tk
import win32gui
import win32con
import win32api

root = tk.Tk()
root.configure(bg="black")
canvas = tk.Canvas(root, bg='#010101')# not quirte full black
hwnd = canvas.winfo_id()
colorkey = win32api.RGB(1,1,1) #not quite full black in COLORREF structure
wnd_exstyle = win32gui.GetWindowLong(hwnd, win32con.GWL_EXSTYLE)
new_exstyle = wnd_exstyle | win32con.WS_EX_LAYERED
win32gui.SetWindowLong(hwnd,win32con.GWL_EXSTYLE,new_exstyle)
win32gui.SetLayeredWindowAttributes(hwnd,colorkey,255,win32con.LWA_COLORKEY)

canvas.create_rectangle(50,50,100,100,fill='blue')
canvas.pack()

root.mainloop()



RE: COMPLEX! Transparent Canvas Linux - deanhystad - Jun-20-2023

I do not understand this:
Quote:One of these piceces is to create a transparent Canvas background so i can draw a circle without having a square border behind it.
Drawing a circle on a canvas does result in a square border. Placing a widget on a canvas will do this. but drawing on the canvas does not.


RE: COMPLEX! Transparent Canvas Linux - AceScottie - Jun-21-2023

(Jun-20-2023, 11:47 PM)deanhystad Wrote: I do not understand this:
Quote:One of these piceces is to create a transparent Canvas background so i can draw a circle without having a square border behind it.
Drawing a circle on a canvas does result in a square border. Placing a widget on a canvas will do this. but drawing on the canvas does not.

what i mean by the square border is the canvas itself being that border. basically the square canvas will show behind the drawn circle.

The code i posted makes the background colour of the canvas transparent so when run it only shows the root widget in black and the drawn rectangle in blue.

without the win32 code (has border) https://prnt.sc/P_bmwQMuMM-u

with win32 code (no border) https://prnt.sc/cl6lzs3W-KAi


RE: COMPLEX! Transparent Canvas Linux - deanhystad - Jun-21-2023

I can make something that looks exactly the same by making the canvas the size of the window and drawing a blue rectangle. No need for win-anything.
import tkinter as tk

root = tk.Tk()
canvas = tk.Canvas(root, bg='black')
canvas.create_rectangle(50,50,100,100,fill='blue')
canvas.pack(expand=True, fill=tk.BOTH)
 
root.mainloop()
A blue rectangle on a black background is not doing a great job demonstrating what you are trying to achieve. If you want to create widgets that are partially transparent, that is easy to do with other GUI toolkits like Qt.


RE: COMPLEX! Transparent Canvas Linux - rob101 - Jun-21-2023

(Jun-20-2023, 10:55 PM)AceScottie Wrote: ... draw a circle without having a square border behind it.

Not too sure if this will help:

import tkinter as tk

root = tk.Tk()
root.geometry('800x600')
root.title('Canvas Demo')

canvas = tk.Canvas(root, width=600, height=400, bg='#F0F0F0')
canvas.pack(anchor=tk.CENTER, expand=True)


points = (
    (50, 150),
    (200, 300),
)
canvas.create_oval(*points, fill='purple')

root.mainloop()



RE: COMPLEX! Transparent Canvas Linux - deanhystad - Jun-21-2023

Why do you want the canvas to be transparent? What is behind the canvas that you want to see?


RE: COMPLEX! Transparent Canvas Linux - AceScottie - Jun-21-2023

(Jun-21-2023, 03:00 AM)deanhystad Wrote: I can make something that looks exactly the same by making the canvas the size of the window and drawing a blue rectangle. No need for win-anything.
import tkinter as tk

root = tk.Tk()
canvas = tk.Canvas(root, bg='black')
canvas.create_rectangle(50,50,100,100,fill='blue')
canvas.pack(expand=True, fill=tk.BOTH)
 
root.mainloop()
A blue rectangle on a black background is not doing a great job demonstrating what you are trying to achieve. If you want to create widgets that are partially transparent, that is easy to do with other GUI toolkits like Qt.
The code below is where it is implemented and is part of rapidTk which will be required for the mutitude of imports from rapidTk
It draws a circle over other widgets while keeping them visable.
The code i posted in my question is just a MCVE for testing purposes and is in no way linked to the actual use case.

As this is possible in windows it should also be possible in Linux.
I included "COMPLEX!" in my question title as this will not be a simple solution of creating a canvase the size of my application which covers evertything just to draw a small UI element somewhere in the middle.


The solution for this will require both an understanding of advanced Tk along with a good understanding of X11 or other Luinx APIs.
class TimePicker(cFrame, widgetBase_override):
	def __init__(self, master, **kwargs):
		pp = PackProcess()
		self._acl = None
		self._atx = None
		self.split = "am"
		self.master = master
		self.tformat = kwargs.pop('format', 24)
		self.min_interval = kwargs.pop('interval', 5)
		tp_bg = kwargs.pop('tp_bg', '#010101')
		self.width = self.height = self.radious = rd = kwargs.pop('radious', 100)*2

		layout = inline_layout(**kwargs)
		widget_args = layout.filter()
		super(TimePicker, self).__init__(master, **kwargs)

		self.hours, self.minutes = StringVar(), StringVar()
		self.hours.set('00')
		self.minutes.set('00')
		holder_frame= pp.add(cFrame(self, bg="green"),side=TOP)
		def pad(item):return f"0{item}" if len(item) == 1 else item
		self.hourE = pp.add(cSpinbox(holder_frame, textvariable=self.hours, width=3, values=[pad(str(x)) for x in range(24)], wrap=0), side=LEFT)
		self._centre = pp.add(cLabel(holder_frame, text=":"), side=LEFT)
		self.minutesE = pp.add(cSpinbox(holder_frame, textvariable=self.minutes, width=3, values=[pad(str(x)) for x in range(60)], wrap=1), side=LEFT)

		#holder_frame.pack(side=TOP)
		##setup focus bindings
		self.hourE.bind("<FocusIn>", self.popup)
		self.hourE.bind("<FocusOut>", self.__focus_loss)
		self.minutesE.bind("<FocusIn>", self.popup)
		self.minutesE.bind("<FocusOut>", self.__focus_loss)

		
		self.radious /= 4
		self.radious -=1 ##fixes clipping
		assert self.tformat in [12, 24], "Time Format must be '12' or '24'"
		pp.pack()
		
		self.sub_can = cCanvas(self.get_root(), bg=tp_bg, width=self.width+5, height=self.height+5, highlightbackground="#010101", highlightthickness=0)
		self.sub_can.bind("<FocusIn>", self.popup)
		hwnd = self.sub_can.winfo_id()
		colorkey = win32api.RGB(1,1,1) #full black in COLORREF structure
		wnd_exstyle = win32gui.GetWindowLong(hwnd, win32con.GWL_EXSTYLE)
		new_exstyle = wnd_exstyle | win32con.WS_EX_LAYERED
		win32gui.SetWindowLong(hwnd,win32con.GWL_EXSTYLE,new_exstyle)
		win32gui.SetLayeredWindowAttributes(hwnd, colorkey,255,win32con.LWA_COLORKEY)

		self.active_line = None
		self._main = self.create_center_circle(self.width/2, self.height/2, self.radious*2, fill="#DDDDDD", outline="#000", width=0)
		self.sub_can.tag_bind(self._main, "<Button-1>", self.popup)
		self.circle_numbers(self.width/2, self.height/2, self.radious*2-15, 10, [12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], 'Helvetica 11 bold', "Hours")
		self.circle_numbers(self.width/2, self.height/2, self.radious+5,  10, list(range(0, 60, self.min_interval)), 'Helvetica 11 bold', "Minutes")
		self.am_pm_switch()
		self.center = self.create_center_circle(self.width/2, self.height/2, 5, fill="#DDDDDD", width=0)

		if layout.method is not None:
			layout.inline(self)
	def __focus_loss(self, event):
		if self.get_root().focus_get() != self.sub_can:
			self.close(event)
		else:
			self.minutesE.focus_set()
	def _on_scroll(self, event, maxn=0):
		num = int(event.widget.get())
		event.widget.delete(0, END)
		if event.delta > 0 :
			if num >= maxn:
				event.widget.insert(0, '00')
			else:
				event.widget.insert(0, str(num+1).zfill(2))
		elif event.delta < 0:
			if num <= 0:
				event.widget.insert(0, str(maxn).zfill(2))
			else:
				event.widget.insert(0, str(num-1).zfill(2))	
	def am_pm_switch(self):
		ovall = 30
		ovalw = 40
		self.sub_can.create_oval(self.width/2-ovall, self.height/2+ovalw/4, self.width/2+ovall, self.height/2+ovalw, fill="#BBBBBB")
		sc, st = self.create_am()
		self.sub_can.tag_bind(sc, "<Button-1>", lambda e=Event(), a=sc, b=st:self._switcher(e, a, b))
		self.sub_can.tag_bind(st, "<Button-1>", lambda e=Event(), a=sc, b=st:self._switcher(e, a, b))
	def create_am(self):
		ovalr = 40
		am = self.create_center_circle(self.width/2-ovalr/1.75, self.height/2+ovalr/1.75, 12, fill='#0575DD', width=0)
		amtx = self.sub_can.create_text(self.width/2-ovalr/1.75, self.height/2+ovalr/1.75, font=('Helvetica 11 bold'), text="AM")
		self.split = "am"
		return am, amtx
	def create_pm(self):
		ovalr = 40
		pm = self.create_center_circle(self.width/2+ovalr/1.75, self.height/2+ovalr/1.75, 12, fill='#0575DD', width=0)
		pmtx = self.sub_can.create_text(self.width/2+ovalr/1.75, self.height/2+ovalr/1.75, font=('Helvetica 11 bold'), text="PM")
		self.split = "pm"
		return pm, pmtx
	def _switcher(self, event, sc, st):
		self.sub_can.delete(sc)
		self.sub_can.delete(st)
		if self.split == "am":
			sc, st = self.create_pm()
			self.sub_can.tag_bind(sc, "<Button-1>", lambda e=Event(), a=sc, b=st:self._switcher(e, a, b))
			self.sub_can.tag_bind(st, "<Button-1>", lambda e=Event(), a=sc, b=st:self._switcher(e, a, b))
		elif self.split == "pm":
			sc, st = self.create_am()
			self.sub_can.tag_bind(sc, "<Button-1>", lambda e=Event(), a=sc, b=st:self._switcher(e, a, b))
			self.sub_can.tag_bind(st, "<Button-1>", lambda e=Event(), a=sc, b=st:self._switcher(e, a, b))
	def create_center_circle(self, x, y, r, **kwargs):
		return self.sub_can.create_oval(x-r, y-r, x+r, y+r, **kwargs)
	def create_circle_arc(self, x, y, r, **kwargs):
		if "start" in kwargs and "end" in kwargs:
			kwargs["extent"] = kwargs["end"] - kwargs["start"]
			del kwargs["end"]
		return super().create_arc(x-r, y-r, x+r, y+r, **kwargs)
	def circle_numbers(self, x: int, y: int, r: int, cr:int, numbers: list, font: str, tp:str):
		_angle = 360/len(numbers)
		for i, n in enumerate(numbers):
			ax =  r * sin(pi * 2 * (360-_angle*i-180) / 360);
			ay = r * cos(pi * 2 * (360-_angle*i-180) / 360);
			tag = f'{tp}:{str(n)}'
			cl = self.create_center_circle(x+ax, y+ay, cr, fill="#DDDDDD", outline="#000", width=0, tag=tag)
			tx = self.sub_can.create_text(x+ax, y+ay, text=str(n).zfill(2), fill="black", font=(font), tag='tx'+tag )
			self.sub_can.tag_bind(f'tx{tp}:{str(n)}', '<Enter>', lambda e=Event(), cl=cl, tx=tx, c=(x+ax, y+ay), t=tag, s=True: self._hover(e, cl, tx, c, s, t))
			#self.sub_can.tag_bind(f'tx{tp}:{str(n)}', '<Leave>', lambda e=Event(), cl=cl, tx=tx, c=(x+ax, y+ay), t=tag, s=False: self._left(e, cl, tx, c, s, t))
			self.sub_can.tag_bind(f'{tp}:{str(n)}', '<Button-1>', lambda e=Event(), c=cl, s=tx, n=n, t=tp,: self._set_number(e, c, s, n, t))
			self.sub_can.tag_bind(f'tx{tp}:{str(n)}', '<Button-1>', lambda e=Event(), c=cl, s=tx, n=n, t=tp,: self._set_number(e, c, s, n, t))
	def _hover(self, event, cl, tx,  coords, state, tag):
		if self.active_line:
			self.sub_can.delete(self.active_line)
			self.sub_can.itemconfigure(self._acl, fill='#DDDDDD')
			self.sub_can.itemconfigure(self._atx, fill="black")
		self._acl = cl
		self._atx = tx
		self.sub_can.itemconfigure(cl, fill='#0797FF')
		self.sub_can.itemconfigure(tx, fill="white")

		self.sub_can.itemconfigure(self.center, fill='#0797FF')
		dx = (1 - 0.8) * self.width/2 + 0.8 * coords[0]
		dy = (1 - 0.8) * self.height/2 + 0.8 * coords[1]
		self.active_line = self.sub_can.create_line(self.width/2, self.height/2, dx, dy, fill="#0797FF", width=2, tag=None) ##create new line
		self.sub_can.tag_lower(self.active_line)
		self.sub_can.tag_lower(self._main)
	def _left(self, event, cl, tx, coords, state, tag):
		if self.active_line is None: ##if there is no line
			return
		self.sub_can.itemconfigure(cl, fill='#DDDDDD')
		self.sub_can.itemconfigure(tx, fill="black")
		self.sub_can.itemconfigure(self.center, fill='#DDDDDD')
		self.sub_can.delete(self.active_line)
		self.active_line = None
	def _set_number(self, event, cl, tx, number, tp):
		if tp == "Hours":
			if self.split == "pm":
				number = (number+12)%24
			self.hours.set(str(number).zfill(2))
			self.minutesE.focus()
		elif tp == "Minutes":
			self.minutes.set(str(number).zfill(2))
			self.close(event)
			self.master.focus()
	def get(self):
		return self.hours.get(), self.minutes.get()
	def popup(self, event):
		xpos = self._centre.winfo_rootx() - self.winfo_toplevel().winfo_rootx()
		ypos = self._centre.winfo_rooty() - self.winfo_toplevel().winfo_rooty() + self._centre.winfo_height()
		width = self._centre.winfo_width()/2
		self.sub_can.place(x=xpos-(self.width/2)+width, y=ypos)
	def close(self, event):
		self.sub_can.place_forget()



RE: COMPLEX! Transparent Canvas Linux - AceScottie - Jun-21-2023

(Jun-21-2023, 02:51 PM)deanhystad Wrote: Why do you want the canvas to be transparent? What is behind the canvas that you want to see?

looks like that but it can vary from application to application.
aka., the JS clock time picker
https://prnt.sc/VRmgWFKP5Omk