I have ran into a problem that i suspect has to do painting/drawing elements in cairo.
I have a borderless window in pygtk, but i draw two rectangles with cairo.a black rectangle, and a grey rectangle inside. When the window is resized, there seems to be parts of the inner rectangle doesn't get drawn/painted. I have included 3 screenshots to show this issue.
As you can see in the second and third picture, some pieces of the window do not get painted grey. One way to fix this, is to call pygtk's window's present() method..but this makes my program extrmely slow, as the height of the window changes with pretty much each keystroke. So i was wondering what alternatives i have to fix this.
below is the relevant cairo code i use
def expose(self, widget, e):
cr = widget.window.cairo_create()
# Draw the background
cr.set_operator(cairo.OPERATOR_SOURCE)
# Create black rectangle with 60% opacity (serves as border)
(width, height) = widget.get_size()
cr.set_source_rgba(0, 0, 0, 0.6)
cr.rectangle(0, 0, width, height)
cr.fill()
# Inside the black rectangle, put a lighter one (will hold widgets)
(width, height) = widget.get_size()
cr.set_source_rgb(205/255, 205/255, 193/255)
cr.rectangle(10, 10, width-20, height-20)
cr.fill()
return False
def screen_changed(self, widget, old_screen = None):
screen = widget.get_screen()
colormap = screen.get_rgba_colormap()
widget.set_colormap(colormap)
It's basically a GTK+ bug, I believe. When a window is resized, GTK+ doesn't always queue the entire window for repainting. As a workaround, you can call window.queue_draw() at the place where you cause the window to be resized.
Try using the following right after you create your cairo widget:
cr.set_source_rgb(0,0,0)
cr.paint()
This will ensure you are always having a clean canvas.
Related
I have made a simulation in pygame. It makes use of a controlled value that the user can set through a tkinter slider and also has a graph displaying some of its statistics (made with matplotlib). Since the pygame, matplotlib and tkinter were all separate windows and I wanted them all in one place as in an app, I first embedded the matplotlib graph into the tk box using FigureCanvasTkAgg. I then tried the same with the pygame window. Having not found an answer on the internet, I then cobbled together a solution:
class PygameWin():
def __init__(self, root):
self.surface = pygame.display.set_mode(
[globalvars.screen_width, globalvars.screen_height])
self.surface.fill((255, 255, 255))
self.canvas = tk.Canvas(
root, height=globalvars.screen_height, width=globalvars.screen_width)
def update(self):
globalvars.amoebas.update()
self.surface.fill((255, 255, 255))
globalvars.amoebas.draw(self.surface)
pygame.image.save(self.surface, 'pygamewin.png')
self.img = ImageTk.PhotoImage(Image.open("pygamewin.png"))
self.canvas.create_image(250,275, image=self.img)
self.canvas.pack()
It basically saves the pygame window as an image and then adds it into a canvas in the tk interface. It works as intended except for the fact that it is very slow and only updates every 100 ticks (times the amoeba are updated). Could anyone help me make it more efficient? I realise that other parts of my code will be impacting the speed, and I can add the full program in if needed.
P.S. Although I can see the pygame window in the tk box as planned, it is still showing up as a separate window as well, so if anyone could tell me how to get rid of that it would be much appreciated.
The reason why your solution is slow is that you are saving the pygame surface as an image and then reloading it every time you want to update the display in tkinter. This is an unnecessary overhead that can slow down the application, especially if you are doing it every 100 ticks.
Instead of saving and reloading the image, you can create a pygame surface that acts as a buffer, draw your amoebas onto that surface, and then convert the surface to a PhotoImage to display it in your tkinter canvas. This approach will reduce the overhead of saving and reloading the image.
Here's an example implementation of this approach:
class PygameWin():
def __init__(self, root):
self.surface = pygame.Surface(
(globalvars.screen_width, globalvars.screen_height))
self.surface.fill((255, 255, 255))
self.canvas = tk.Canvas(
root, height=globalvars.screen_height, width=globalvars.screen_width)
# create a buffer to hold the image data
self.buffer = pygame.Surface(
(globalvars.screen_width, globalvars.screen_height))
def update(self):
globalvars.amoebas.update()
# draw the amoebas onto the buffer
self.buffer.fill((255, 255, 255))
globalvars.amoebas.draw(self.buffer)
# convert the buffer to a PhotoImage and display it in the canvas
img = ImageTk.PhotoImage(Image.frombytes(
'RGB', (self.buffer.get_width(), self.buffer.get_height()),
pygame.image.tostring(self.buffer, 'RGB')))
self.canvas.create_image(0, 0, image=img, anchor='nw')
self.canvas.pack()
# update the pygame display
pygame.display.update()
Regarding the issue of the pygame window showing up as a separate window, you can try setting the position of the window off-screen using the following code:
os.environ['SDL_VIDEO_WINDOW_POS'] = '-1000,-1000'
This should position the window off-screen and prevent it from being visible. You can add this code before the pygame initialization.
I'm trying to display an image on the screen, without any window/application popping up/containing it. I'm pretty close with TKinter, but the method for removing the background color of the canvas is hacky and has some undesired effects.
import tkinter as tk
import ctypes
user32 = ctypes.windll.user32
screen_size = user32.GetSystemMetrics(0), user32.GetSystemMetrics(1)
root = tk.Tk()
root.overrideredirect(True)
root.config(bg="blue", bd=0, highlightthickness=0)
root.attributes("-transparentcolor", "#FEFCFD")
root.attributes("-topmost", True)
tk_img = tk.PhotoImage(file="image.png")
canvas = tk.Canvas(root, bg="#FEFCFD", bd=0, highlightthickness=0, width=screen_size[0], height=screen_size[1])
canvas.pack()
img = canvas.create_image(0, 0, image=tk_img, anchor="nw")
root.mainloop()
The -transparentcolor flag mostly removes the background, but if an image has any partially transparent pixels it will tint them. Plus, if that color exists in the image, it will be removed; that choice of color was in hopes of minimizing exact matches in an image while also being mostly white, to hopefully have the least noticeable affect on the images. Here's an image of what it looks like currently; very close to what I want, but you can see some missing pixels in the white areas of the dice, and they all seem to have a white border around them due to their edges being partially transparent. This is what the image should look like.
I've also tried to achieve this effect using wxPython, but I can't remove the background of the window, leading to transparent images always being backed by some color. I used this answer; I've modified it slightly but nothing I've done has improved it.
So, is there a way to draw an image on the screen without any background at all with Python?
Thanks to the suggestion from Kartikeya, I was able to solve my own question.
Using PyQt5, this code will display an image with transparency and no border or background at all
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPixmap
from PyQt5.QtWidgets import QMainWindow, QApplication, QLabel
app = QApplication(sys.argv)
window = QMainWindow()
window.setAttribute(Qt.WA_TranslucentBackground, True)
window.setAttribute(Qt.WA_NoSystemBackground, True)
window.setWindowFlags(Qt.FramelessWindowHint)
label = QLabel(window)
pixmap = QPixmap('image.png')
label.setPixmap(pixmap)
label.setGeometry(0, 0, pixmap.width(), pixmap.height())
window.label = label
window.resize(pixmap.width(),pixmap.height())
window.show()
sys.exit(app.exec_())
Once I was looking for PyQt5, I found this question and only needed to modify the code slightly. Here is what it looks like now.
So, is there a way to draw an image on the screen without any
background at all with Python?
Using Tkinter, for this image, no, you cannot achieve the desired result. (You can look for other modules like 'PyQT5', 'Kivy', 'wxPython', or 'turtle' maybe.)
See, transparentcolor Specifies the transparent color index of the toplevel.
If you want to do the best in Tkinter, here are some changes to your code:
root.attributes('-transparentcolor', '#d4d4e2')
root.attributes("-topmost", True)
tk_img = tk.PhotoImage(file="image.png")
canvas = tk.Canvas(root, bg="#d4d4e2", bd=0, highlightthickness=0, width=screen_size[0], height=screen_size[1])
So, this will display a window that contains a canvas with transparent background. Very close to what you wanted, you can see very less missing pixels in the white areas of the dice, but still, this isn't the solution.
but if an image has any partially transparent pixels it will tint them
Yes, it's true, if that color exists in the image, it will be removed, as that color is being used to mark what needs to be used as transparent color.
that choice of color was in hopes of minimizing exact matches in an
image while also being mostly white, to hopefully have the least
noticeable effect on the images.
For the edges/borders of this image to retain the partially 'white' transparent background, the choice of color needs to be some shade of 'white'. So, the color used to make it transparent is #d4d4e2 (For this image, there was only one place where this color pixel was used, so it goes unnoticable.) Still, the edges will have sharp corners and cuts.
I am creating a canvas that overlays everything else on the screen using a widget without a window that is lifted with the topmost attribute.
However, I would like a transparent background.
Here is what I've got:
import Tkinter
vsize = vw, vh = 600, 350
w = Tkinter.Canvas(width=vw, height=vh, highlightthickness=0)
w.master.overrideredirect(True)
w.master.geometry("+0+0")
w.master.lift()
w.master.wm_attributes("-topmost", True)
w.master.wm_attributes("-disabled", True)
w.create_rectangle(0, 0, vw, vh, fill="black")
w.pack()
w.mainloop()
I tried adding the following attribute to the canvas and adding stipple="gray25" to the rectangle:
w.master.wm_attributes("-transparentcolor", "gray")
This for some reason didn't make the gray transparent, and even if this did work it would look awful using stipple.
Does anyone have any ideas on how to achieve this?
EDIT:
I tried configuring the background as black, and giving it some transparency, but I would only like the background to be black so I can have non-transparent items inside of the canvas.
w.configure(background="black")
w.master.wm_attributes("-alpha", 0.5)
There is currently no way to achieve this in Tkinter I believe.
If you are looking for a way to overlay things on your screen, why not use individual windows placed on the screen? Else, I would suggest looking at other solutions outside of Tkinter.
I have three questions that I could really use some help on. Hope I'm not asking too much.
1) I am designing a simple GUI that contains one frame and one panel. Let's say I have two images that I draw on the panel using dc. One image will be continually fade in and out (on a timer), and the second is stationary (doesn't change). The fading is accomplished by changing the opacity of the image and use dc.Clear() before redrawing the new version of the image.
My question is this: how would I draw the fading in/out image without affecting the second image which does not change? It seems like this causes unnecessary drawing as the stationary image will be redrawn alongside the fading image. Could I selectively clear just the first image without affecting the second? This is my drawing function:
def on_paint(self, event):
dc = wx.PaintDC(self)
dc = wx.BufferedDC(dc)
brush = wx.Brush('#3B3B3B')
dc.SetBackground(brush)
dc.Clear()
# Draw the first image (stationary)
dc.DrawBitmap(stationaryBitmap, 120, 0, True)
# Draw the second image (fading)
image = self.image.AdjustChannels(1, 1, 1, self.factoralpha)
fadingBitmap = wx.BitmapFromImage(image)
dc.DrawBitmap(fadingBitmap, 120, 0, True)
2) How can I bind an event to a wx.Image object? I would like to be able to click on the fading in/out image, though I can't seem to assign it an id. The goal is to bind an event similar to what I could do with a wx.StaticBitmap.
self.image = wx.Image("C:\image.png", wx.BITMAP_TYPE_PNG)
# Trying to bind an event, but no ID is assigned
self.Bind(wx.EVT_BUTTON, self.go_button, id=self.image.GetId())
3) Is it possible to place wx.DrawBitmap in a sizer? It appears that it only takes an x,y coordinate pair.
dc.DrawBitmap(bitmap, 120, 0, True)
Thanks everyone.
1) For the Performance, I would recommend using a MemoryDC and update the Drawing only it is required. See here: BufferedCanvas. You may want to use more than 2 buffers because you are using 2 images (see example).
2) I don't know about this, but have you tried to do the binding to a panel and fade the panel in/out?
You can directly paint on a wx.Panel.
Regards
i'm trying to make a GTK application in python where I can just draw a loaded image onto the screen where I click on it. The way I am trying to do this is by loading the image into a pixbuf file, and then drawing that pixbuf onto a drawing area.
the main line of code is here:
def drawing_refresh(self, widget, event):
#clear the screen
widget.window.draw_rectangle(widget.get_style().white_gc, True, 0, 0, 400, 400)
for n in self.nodes:
widget.window.draw_pixbuf(widget.get_style().fg_gc[gtk.STATE_NORMAL],
self.node_image, 0, 0, 0, 0)
This should just draw the pixbuf onto the image in the top left corner, but nothing shows but the white image. I have tested that the pixbuf loads by putting it into a gtk image. What am I doing wrong here?
You can make use of cairo to do this. First, create a gtk.DrawingArea based class, and connect the expose-event to your expose func.
class draw(gtk.gdk.DrawingArea):
def __init__(self):
self.connect('expose-event', self._do_expose)
self.pixbuf = self.gen_pixbuf_from_file(PATH_TO_THE_FILE)
def _do_expose(self, widget, event):
cr = self.window.cairo_create()
cr.set_operator(cairo.OPERATOR_SOURCE)
cr.set_source_rgb(1,1,1)
cr.paint()
cr.set_source_pixbuf(self.pixbuf, 0, 0)
cr.paint()
This will draw the image every time the expose-event is emited.
I found out I just need to get the function to call another expose event with widget.queue_draw() at the end of the function. The function was only being called once at the start, and there were no nodes available at this point so nothing was being drawn.