Python Forum
[Tkinter] COMPLEX! Transparent Canvas Linux
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
[Tkinter] COMPLEX! Transparent Canvas Linux
#1
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()
Reply
#2
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.
Reply
#3
(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
Reply
#4
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.
Reply
#5
(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()
Sig:
>>> import this

The UNIX philosophy: "Do one thing, and do it well."

"The danger of computers becoming like humans is not as great as the danger of humans becoming like computers." :~ Konrad Zuse

"Everything should be made as simple as possible, but not simpler." :~ Albert Einstein
Reply
#6
Why do you want the canvas to be transparent? What is behind the canvas that you want to see?
Reply
#7
(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()
Reply
#8
(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
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  Transparent window background, but not text on it muzicman0 7 2,901 Feb-02-2024, 01:28 AM
Last Post: Joically
  [Tkinter] Transparent Canvas finndude 8 10,118 Oct-02-2022, 01:48 PM
Last Post: joe_momma
  win32gui not transparent a color, just transparent all the wwindow DQT 0 1,101 Jul-29-2022, 01:40 PM
Last Post: DQT
  Make Label Text background (default color) transparent using tkinter in python barry76 1 23,863 Nov-28-2019, 10:19 AM
Last Post: Larz60+
  [Tkinter] Resizing image inside Canvas (with Canvas' resize) Gupi 2 25,126 Jun-04-2019, 05:05 AM
Last Post: Gupi
  [Tkinter] How can we make the background transparent ? MrDrumX_ 1 11,258 Apr-27-2019, 09:57 PM
Last Post: Larz60+

Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020