I'm trying to make a program where the user can paint on the screen. So I want to make an invisible canvas window in fullscreen where only the user's pen marks on the canvas will be visible. The closest thing I found is this function: root.attributes("-transparentcolor","color code here")
, which will make all the parts of the window that's in the color you give transparent. So if I give the second parameter the background color of the canvas, then only the pen strokes on the canvas will be visible. This is so close to what I want, except for one thing, the transparent areas can't detect or block mouse clicks! Any mouse clicks will just go through to whatever is behind the tkinter window. Is there a way to make it so the transparent areas will still block mouse clicks? I really need help on this!

- 58
- 7
2 Answers
Here is a much better way to do this using only tkinter
. Explanation is in code comments. Basically uses two windows, one for "blocking" the mouse and being transparent using the "-alpha"
attribute and the other window for "hosting" canvas and having one completely transparent color while keeping others opaque using "-transparentcolor"
attribute. That also means that this is cross-platform solution too (except I think the -transparentcolor
attribute differs a little bit on other OS like Linux where I think it is -splash
or sth and maybe something different on MacOS):
from tkinter import Tk, Toplevel, Canvas
# setting the starting coordinate of the line so that
# on motion it is possible to immediately draw it
def set_first(event):
points.extend([event.x, event.y])
# on motion append new coordinates to the list and if there are
# 4 (the minimum), create a new line and save the id
# otherwise update the existing line
def append_and_draw(event):
global line
points.extend([event.x, event.y])
if len(points) == 4:
line = canvas.create_line(points, **line_options)
else:
canvas.coords(line, points)
# when released clear the list to not waste space
# and not necessarily but also set "id" to None
def clear_list(event=None):
global line
points.clear()
line = None
line = None # this is a reference to the current line (id)
points = [] # list to keep track of current line coordinates
line_options = {} # dictionary to allow easier change of line options
# just a variable to more easily store the transparent color
transparent_color = 'grey15'
# creating the root window which will help with drawing the line
# because it will "block" mouse because `-alpha` (0.01 seems to be the lowest value)
# attribute is used, however it makes everything transparent on the window
# so need another window to "host" the canvas
root = Tk()
root.attributes('-alpha', 0.01)
root.attributes('-topmost', True)
root.attributes('-fullscreen', True)
# just press Esc key to close the whole thing, otherwise
# it is only doable by pressing Alt + F4 or turning off
# the computer
root.bind('<Escape>', lambda e: root.quit())
# create the host window, because it allows to have only
# one transparent color while keeping the other opaque and
# visible
top = Toplevel(root)
top.attributes('-transparentcolor', transparent_color)
top.attributes('-topmost', True)
top.attributes('-fullscreen', True)
# set the focus to root because that is where events are bound
root.focus_set()
# create the canvas to draw on
canvas = Canvas(top, bg=transparent_color, highlightthickness=0)
canvas.pack(fill='both', expand=True)
# bind all the events to `root` which "blocks" mouse
# but is also almost (because it has a very small alpha value
# it is not entirely invisible but human eye won't notice much)
# invisible
root.bind('<Button-1>', set_first)
root.bind('<B1-Motion>', append_and_draw)
root.bind('<ButtonRelease-1>', clear_list)
root.mainloop()

- 5,970
- 2
- 12
- 29
-
Just out of curiosity why did you choose to use a `dict` to store the `points`, `line` and `line_options` when you could have just used global variables. From what I can see, you don't actually use those variables names so there is no point in not making them global. Also `dict`s are slower than global variables from what I know because python has to compute the `hash` of the key each time you want to use it. – TheLizzard Sep 12 '21 at 19:22
-
@TheLizzard I just usually prefer using anything but `global`, tho I have learned that they are not that horrible, they do have their downsides for example I would have had to declare them in each function, but a dictionary is more expandable, it is easier to save it to a file, read from file. The slowness is really not noticeable anyways but that is a completely different argument (and I usually like to make everything faster). Just avoiding `global` where possible, normally I would probably write a class but usually at first for smaller concepts I use functional approach like here – Matiiss Sep 12 '21 at 19:29
-
@TheLizzard and continuing about "choosing anything but `global` in a functional approach", I would choose `dict`s because the keys can be strings which means I can give them proper names, also they kinda keep related stuff together if used in that way similarly to classes – Matiiss Sep 12 '21 at 19:32
-
So you are saving variables in a dictionary to avoid saving them in a dictionary (`globals()`) where they can be accessed more easily. Also when using functional event driven programming, you must have global variables. So the question is if you would put a `dict` (`data`) inside another `dict` (`globals()`) instead of just putting it in `globals()`. And even when I use classes, I convert the variables to `dict` before saving them. I don't get why people fear global variables so much. I prefer classes but for short programs, I have no problems having global variables. – TheLizzard Sep 12 '21 at 19:36
-
Technically classes are just `dict`s with special functions attached to them (methods) – TheLizzard Sep 12 '21 at 19:37
-
@TheLizzard that perspective moves Python's classes closer to js objects. Ok, I got your point about the dicts (hadn't thought about that either) and `global`s (I guess I don't want to use that keyword? for no particular reason), the fear tho... it is not so much anymore since I learned more about the whole _why?_ they are bad, but still probably takes a while to get used to a concept you previously thought was an absolutely horrible idea to use. Ok, will change the code as I agree that it will also make it easier to read (and faster) – Matiiss Sep 12 '21 at 19:40
-
JS objects have more in common with Python classes than C/C++ classes. Also the comment about the fear of global variables wasn't directed at you, it's the whole programming community. Also if you think about it `locals()`, `globals()`, and any python object is just a `dict`. But the problem is that `dict` lookup isn't that fast. One of the reasons why python is slower than C/C++. – TheLizzard Sep 12 '21 at 19:45
-
@TheLizzard that makes sense, also changed code and it is not as bad as I thought it would be since only `line` changes reference (which is the reason `global` has to be used like so and not for the list and dictionary because for those only object itself changes or so I understand) – Matiiss Sep 12 '21 at 19:51
-
You are correct, both `list` and `dict` are mutable so to change the internals, there is no need for `global`. Also an interesting thing: `global ...` doesn't actually take any CPU time when the program is running. For more info look at python bytecode. – TheLizzard Sep 12 '21 at 19:55
Here is an improvable example (you may need to pip install pyautogui
, ctypes
is a built-in library), it is also Windows
only as far as I know:
Note: The other answer using two windows, however, is a lot better but I will keep this too just for the information.
from tkinter import Tk, Canvas
import pyautogui as pag
import ctypes
data = {
'draw': True,
'cur_line_points': [],
'cur_line_id': None
}
# function taken mainly from here: https://stackoverflow.com/a/46596592/14531062
def is_pressed(btn: str = 'left') -> bool:
if btn == 'left':
btn = 0x01
elif btn == 'right':
btn = 0x02
else:
raise Warning("incorrect argument, should be 'left' or 'right'")
return ctypes.windll.user32.GetKeyState(btn) not in (0, 1)
def draw_line(canvas_):
if not data['draw']:
root.after(10, draw_line, canvas_)
return
pressed = is_pressed('left')
cur_line_points = data['cur_line_points']
cur_line_id = data['cur_line_id']
if not pressed:
if cur_line_id is not None:
canvas_.coords(cur_line_id, cur_line_points)
data['cur_line_id'] = None
cur_line_points.clear()
else:
mouse_x, mouse_y = pag.position()
cur_line_points.extend((mouse_x, mouse_y))
len_points = len(cur_line_points)
if len_points == 4:
data['cur_line_id'] = canvas_.create_line(cur_line_points)
elif len_points > 4:
canvas_.coords(cur_line_id, cur_line_points)
root.after(10, draw_line, canvas_)
transparent_color = 'grey15'
root = Tk()
root.config(bg=transparent_color)
root.attributes('-transparentcolor', transparent_color)
root.attributes('-topmost', True)
root.attributes('-fullscreen', True)
canvas = Canvas(root, bg=transparent_color, highlightthickness=0)
canvas.pack(fill='both', expand=True)
draw_line(canvas)
root.mainloop()
Basically detects if mouse button is pressed using the built-in library ctypes
and if it is adds the current mouse coordinates (does that using pyautogui
library which may need be installed) to a list and then draws a line based on that list (it also keeps the reference of the currently drawn line and simply changes its coordinates instead of drawing a new line each time it loops), the only slight issue is that while drawing the mouse is also interacting with the window below, highlighting text and stuff, couldn't really figure out how to remove that yet but at least you get to draw a line.

- 7,248
- 2
- 11
- 31

- 5,970
- 2
- 12
- 29