How to draw a transparent section within an opaque area - python

How would you go about cutting a transparent hole into the background of a wxpython window?
Would I have to draw the non transparent area manually, leaving a hole, instead of being able to use a background colour?

Adapting my answer to your previous question, though I'm not sure if this meets your needs 100%. Let me know?
Basically we're leaving the previous screen contents intact by not erasing the background. Then we handle the paint events and only draw on certain portions of the screen.
You could switch back to using SetTransparent if you need the drawn portions to be translucent and not opaque.
import wx
class Frame(wx.Frame):
def __init__(self):
super(Frame, self).__init__(None)
self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground)
self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
self.Bind(wx.EVT_PAINT, self.OnPaint)
def OnEraseBackground(self, event):
pass # do nothing
def OnLeftDown(self, event):
print event.GetPosition()
def OnKeyDown(self, event):
if event.GetKeyCode() == wx.WXK_ESCAPE:
self.Close()
else:
event.Skip()
def OnPaint(self, event):
w, h = self.GetSize()
dc = wx.PaintDC(self)
region = wx.RegionFromPoints([(0, 0), (w, 0), (w, h), (0, h)])
box = wx.RegionFromPoints([(100, 100), (500, 100), (500, 500), (100, 500)])
region.SubtractRegion(box)
dc.SetClippingRegionAsRegion(region)
dc.DrawRectangle(0, 0, w, h)
if __name__ == '__main__':
app = wx.PySimpleApp()
frame = Frame()
frame.ShowFullScreen(True)
app.MainLoop()

Related

How to continuously delete a rectangle from a previously drawn rectangle while drawing the new rectangle in PyQT5?

I am trying to code a gui for highlighting areas of a screen (specifically, greying out areas of an image surrounding a clear rectangle).
I have implemented the generation of a fullscreen transparent widget created after a button press. The widget is covered by a translucent grey rectangle. The user can still see the underlying active screen image which allows them to select a starting point for drawing a rectangle.
The mouse move event after a click event triggers the Update() function which allows the drawing of a new red rectangle.
The problem here is the previously drawn overlay rectangle is disappearing.
How do I fix the following code to draw the red rectangle over the translucent overlay and continually cut the area of the new rectangle from the previous overlay while drawing the rectangle?
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QBrush, QColor, QPainter, QPen
from PyQt5.QtWidgets import QApplication, QPushButton, QWidget, QDesktopWidget
class MainWidget(QWidget):
def __init__(self):
super().__init__()
# Set the window properties
self.setWindowTitle("Main Widget")
self.setGeometry(100, 100, 200, 200)
# Create a button
self.screenshotButton = QPushButton("Start", self)
self.screenshotButton.move(50, 50)
# Connect the button's clicked signal to the showTransparentWidget slot
self.screenshotButton.clicked.connect(self.openTransparentWidget)
def openTransparentWidget(self):
# Close the main widget
self.close()
# Create and show the transparent widget
self.transparentWidget = TransparentWidget()
self.transparentWidget.show()
class TransparentWidget(QWidget):
def __init__(self):
super().__init__()
# Get the screen dimensions
desktop = QDesktopWidget()
screenWidth = desktop.screenGeometry().width()
screenHeight = desktop.screenGeometry().height()
# Set the size of the widget to the screen dimensions
self.setGeometry(0, 0, screenWidth, screenHeight)
# Set the window flags to make the widget borderless and topmost
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
# Set the window transparency
self.setAttribute(Qt.WA_TranslucentBackground)
# Initialize the starting and ending positions of the box to -1
self.startX = -1
self.startY = -1
self.endX = -1
self.endY = -1
#call the paintEvent to generate an overlay
self.update()
def mousePressEvent(self, event):
# Store the starting position of the mouse when it is clicked
# Set the flag to True
self.mouseClicked = True
self.startX = event.x()
self.startY = event.y()
print(self.startX, self.startY)
def mouseMoveEvent(self, event):
if self.mouseClicked:
# Store the current position of the mouse as it is being dragged
self.endX = event.x()
self.endY = event.y()
# Redraw the widget to update the box
self.update()
def mouseReleaseEvent(self, event):
# Set the flag to False
self.mouseClicked = False
def paintEvent(self, event):
# Create a QPainter object and set it up for drawing
painter = QPainter(self)
# Draw translucent overlay over the transparent widget
if self.startX == -1 and self.endX == -1:
brush = QBrush(QColor(200, 200, 200, 128))
painter.setBrush(brush)
painter.drawRect(0, 0, self.width(), self.height())
# Set the composition mode to clear
#painter.setCompositionMode(QPainter.CompositionMode_Clear)
# Draw the box if the starting and ending positions are valid
if self.startX != -1 and self.endX != -1:
# Calculate the top-left and bottom-right corners of the box
topLeftX = min(self.startX, self.endX)
topLeftY = min(self.startY, self.endY)
bottomRightX = max(self.startX, self.endX)
bottomRightY = max(self.startY, self.endY)
# Set the composition mode to source over - these options seem to have no effect
#painter.setCompositionMode(QPainter.CompositionMode_SourceOver)
#painter.setCompositionMode(QPainter.CompositionMode_Clear)
#painter.setCompositionMode(QPainter.CompositionMode_DestinationOut)
pen = QPen(QColor(255 ,0, 0))
brush = QBrush(QColor(255, 255, 255, 0))
painter.setPen(pen)
painter.setBrush(brush)
# Draw the empty box (eraseRect also not working)
painter.drawRect(topLeftX, topLeftY, bottomRightX - topLeftX, bottomRightY - topLeftY)
app = QApplication(sys.argv)
mainWidget = MainWidget()
mainWidget.show()
sys.exit(app.exec_())
Edit: Here's a sample image I found that shows what I am trying to achieve. (It's actually from a snipping tool which is very similar to what I am trying to achieve)
Whenever paintEvent is called the entire widget is redrawn.
To overcome this, when drawing anything new, also re-draw the previous item.
The short solution is to update paintEventto draw the overlay and clear the new rectangle in the same call.
brush = QBrush(QColor(200, 200, 200, 128))
painter.setBrush(brush)
painter.drawRect(0, 0, self.width(), self.height())
painter.setCompositionMode(QPainter.CompositionMode_Clear)
Thanks to #musicamante for your support via the comments section.
Here is the full code:
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QBrush, QColor, QPainter, QPen
from PyQt5.QtWidgets import QApplication, QPushButton, QWidget, QDesktopWidget
class MainWidget(QWidget):
def __init__(self):
super().__init__()
# Set the window properties
self.setWindowTitle("Main Widget")
self.setGeometry(100, 100, 200, 200)
# Create a button
self.screenshotButton = QPushButton("Start", self)
self.screenshotButton.move(50, 50)
# Connect the button's clicked signal to the showTransparentWidget slot
self.screenshotButton.clicked.connect(self.openTransparentWidget)
def openTransparentWidget(self):
# Close the main widget
self.close()
# Create and show the transparent widget
self.transparentWidget = TransparentWidget()
self.transparentWidget.show()
class TransparentWidget(QWidget):
def __init__(self):
super().__init__()
# Get the screen dimensions
desktop = QDesktopWidget()
screenWidth = desktop.screenGeometry().width()
screenHeight = desktop.screenGeometry().height()
# Set the size of the widget to the screen dimensions
self.setGeometry(0, 0, screenWidth, screenHeight)
# Set the window flags to make the widget borderless and topmost
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
# Set the window transparency
self.setAttribute(Qt.WA_TranslucentBackground)
# Initialize the starting and ending positions of the box to -1
self.startX = -1
self.startY = -1
self.endX = -1
self.endY = -1
#call the paintEvent to generate an overlay
self.update()
def mousePressEvent(self, event):
# Store the starting position of the mouse when it is clicked
# Set the flag to True
self.mouseClicked = True
self.startX = event.x()
self.startY = event.y()
print(self.startX, self.startY)
def mouseMoveEvent(self, event):
if self.mouseClicked:
# Store the current position of the mouse as it is being dragged
self.endX = event.x()
self.endY = event.y()
# Redraw the widget to update the box
self.update()
def mouseReleaseEvent(self, event):
# Set the flag to False
self.mouseClicked = False
def paintEvent(self, event):
# Create a QPainter object and set it up for drawing
painter = QPainter(self)
# Draw translucent overlay over the transparent widget
if self.startX == -1 and self.endX == -1:
brush = QBrush(QColor(200, 200, 200, 128))
painter.setBrush(brush)
painter.drawRect(0, 0, self.width(), self.height())
# Set the composition mode to clear
#painter.setCompositionMode(QPainter.CompositionMode_Clear)
# Draw the box if the starting and ending positions are valid
if self.startX != -1 and self.endX != -1:
# Calculate the top-left and bottom-right corners of the box
topLeftX = min(self.startX, self.endX)
topLeftY = min(self.startY, self.endY)
bottomRightX = max(self.startX, self.endX)
bottomRightY = max(self.startY, self.endY)
brush = QBrush(QColor(200, 200, 200, 128))
painter.setBrush(brush)
painter.drawRect(0, 0, self.width(), self.height())
painter.setCompositionMode(QPainter.CompositionMode_Clear)
pen = QPen(QColor(255 ,0, 0))
brush = QBrush(QColor(0, 0, 0, 0))
painter.setPen(pen)
painter.setBrush(brush)
# Draw the empty box
painter.drawRect(topLeftX, topLeftY, bottomRightX - topLeftX, bottomRightY - topLeftY)
app = QApplication(sys.argv)
mainWidget = MainWidget()
mainWidget.show()
sys.exit(app.exec_())
UI drawing (at the low level) normally happens using a frame buffer, which is eventually cleared in a specific area in which new painting is going to happen.
This means that you cannot rely on contents previously drawn in another paint event: even when requesting to update a specific region of the widget (ie: using update(QRect)), that region will be cleared from the buffer, and previous contents doesn't exist any more, and the buffer is also cleared anyway whenever the window is hidden and shown again, like after minimizing and restoring it, or after switching virtual desktop.
In your case, it means that the "background" rectangle will only be painted at start up (when the coordinates are -1), not after that.
The solution is to always draw all the contents, and eventually cut out the area using setClipRegion().
class TransparentWidget(QWidget):
area = reference = None
def __init__(self):
super().__init__()
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
self.setAttribute(Qt.WA_TranslucentBackground)
self.setMouseTracking(True)
screenArea = QRect()
for screen in QApplication.screens():
screenArea |= screen.geometry()
self.setGeometry(screenArea)
def mousePressEvent(self, event):
if event.button() != Qt.LeftButton:
return
pos = event.pos()
if self.area:
span = QRect(-5, -5, 10, 10)
if span.translated(self.area.topLeft()).contains(pos):
self.reference = self.area.setTopLeft
elif span.translated(self.area.topRight()).contains(pos):
self.reference = self.area.setTopRight
elif span.translated(self.area.bottomRight()).contains(pos):
self.reference = self.area.setBottomRight
elif span.translated(self.area.bottomLeft()).contains(pos):
self.reference = self.area.setBottomLeft
else:
self.reference = None
if not self.reference:
self.area = QRect(pos, QSize(1, 1))
self.reference = self.area.setBottomRight
self.update()
def mouseMoveEvent(self, event):
if self.reference:
self.reference(event.pos())
self.update()
elif self.area:
pos = event.pos()
span = QRect(-5, -5, 10, 10)
cursor = None
if span.translated(self.area.topLeft()).contains(pos):
cursor = Qt.SizeFDiagCursor
elif span.translated(self.area.topRight()).contains(pos):
cursor = Qt.SizeBDiagCursor
elif span.translated(self.area.bottomRight()).contains(pos):
cursor = Qt.SizeFDiagCursor
elif span.translated(self.area.bottomLeft()).contains(pos):
cursor = Qt.SizeBDiagCursor
if cursor is not None:
self.setCursor(cursor)
else:
self.unsetCursor()
def mouseReleaseEvent(self, event):
self.reference = None
if self.area is not None:
self.area = self.area.normalized()
self.update()
def paintEvent(self, event):
painter = QPainter(self)
if self.area is not None:
r = QRegion(self.rect())
r ^= QRegion(self.area.normalized().adjusted(1, 1, 0, 0))
painter.setClipRegion(r)
painter.fillRect(self.rect(), QColor(200, 200, 200, 128))
if self.area is not None:
painter.setPen(QColor(255 ,0, 0))
painter.drawRect(self.area.normalized())
Notes:
QDesktopWidget is obsolete in Qt5, use QScreen instead;
you should always consider the case of multiple screen computers; if you specifically do not want to show your widget in all screens, then just use `showFullScreen();
whenever possible and it makes sense, use Qt objects functions, which are normally quite fast and provide better readability (for instance, using QPoint, QRect and functions like QRect.normalized());
calling self.update() in the __init__ is pointless: update() doesn't immediately redraw the widget, it only schedules an update, and since the first painting will happen anyway as soon as the widget is shown, there's no point in doing it;

Using wx.EVT_PAINT and wx.PaintDC

You use wx.EVT_PAINT and wx.PaintDC to draw shapes, so that when window is resized (redrawn) shapes will not be lost. This works when the window is created. But, how will I preserve the shapes that I create after window is created?
Below, I present you a code, when the app first starts, a rectangle is drawn on the window. When user double clicks somewhere on the window, another rectangle is created. The initial rectangle is always preserved because it is bind to wx.EVT_PAINT event, so that it will be redrawn every time the window is redrawn.
But the second rectangle is not associated to the wx.EVT_PAINT, therefore it is lost when window is redrawn. How do I preserve the second rectangle as well?
import wx
class MyPanel(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent, -1)
self.Bind(wx.EVT_PAINT, self.OnPaint)
self.Bind(wx.EVT_LEFT_DCLICK, self.on_left_double_click)
def OnPaint(self, evt):
dc = wx.PaintDC(self)
dc.DrawRectangle(50, 60, 90, 40)
def on_left_double_click(self, evt):
x = evt.GetX()
y = evt.GetY()
dc = wx.ClientDC(self)
dc.SetBrush(wx.Brush("yellow"))
dc.DrawRectangle(x, y, 90, 40)
class MyForm(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, wx.ID_ANY, "Test",style=wx.DEFAULT_FRAME_STYLE,size=wx.Size(400, 300))
self.main_panel = MyPanel(self)
if __name__ == "__main__":
app = wx.App(False)
frame = MyForm()
frame.Show()
app.MainLoop()
There is no universal solution to this except handling every drawing operation in a wx.PaintDC. You would do something along the lines of the following:
def __init__(self, parent):
# ...
self.show_yellow_box = False
self.box_pos = None
def OnPaint(self, evt):
dc = wx.PaintDC(self)
dc.DrawRectangle(50, 60, 90, 40)
if self.show_yellow_box:
x, y = self.box_pos
dc.SetBrush(wx.Brush("yellow"))
dc.DrawRectangle(x, y, 90, 40)
def on_left_double_click(self, evt):
x = evt.GetX()
y = evt.GetY()
self.box_pos = (x, y)
self.show_yellow_box = True
self.Refresh() # important, to trigger EVT_PAINT on panel
If the operations in the paint event are more expensive, you probably will end up collecting the expensive drawing operations on the DC in a wx.MemoryDC and blit the bitmap content back onto the panel in the MyPanel.OnPaint.
There is a temporary DC (wx.Overlay/wx.OverlayDC), which is however only useful to apply temporary changes between paint events.

wxPython Paint Damaged, Clipped area

I have the following simple code (click the pink box and you can move it around with your mouse while holding down the left mouse button).
import wx
class AppPanel(wx.Panel):
def __init__(self, parent, id):
wx.Panel.__init__(self, parent, id)
p = MovablePanel(self, -1)
self.i = 0
self.Bind(wx.EVT_PAINT, self.OnPaint, self)
def OnPaint(self, event):
dc = wx.PaintDC(self)
self.i = self.i+10
c = self.i % 255
c = (0, 0, c)
dc.SetPen(wx.Pen(c))
dc.SetBrush(wx.Brush(c))
dc.DrawRectangle(0, 0, 10000,10000)
class MovablePanel(wx.Panel):
def __init__(self, parent, id):
wx.Panel.__init__(self, parent, id)
self.SetMinSize((500,500))
self.SetSize((500,500))
self.SetBackgroundColour("PINK")
self.LEFT_DOWN = False
self.Bind(wx.EVT_MOTION, self.OnMove, self)
self.Bind(wx.EVT_LEFT_DOWN,
self.OnClickDown,
self)
self.Bind(wx.EVT_LEFT_UP,
self.OnClickUp,
self)
def OnClickUp(self, event):
self.LEFT_DOWN = False
self.Refresh()
def OnClickDown(self, event):
self.LEFT_DOWN = True
self.Refresh()
def OnMove(self, event):
if self.LEFT_DOWN:
p = self.GetTopLevelParent().ScreenToClient(wx.GetMousePosition())
self.SetPosition(p)
if __name__ == "__main__":
app = wx.App(False)
f = wx.Frame(None, -1, size = (700, 700))
p = AppPanel(f, -1)
f.Show()
f.Maximize()
app.MainLoop()
and it is suppose to look like the following (simply resize the frame)
However after moving the pink box around you will see it really looks like this
I have tried the following
dc.Clear()
dc.DestroyClippingRegion()
wx.FULL_REPAINT_ON_RESIZE
wx.EVT_ERASE_BACKGROUND
I'm pretty sure it has to do with it being a panel, and therefore the PaintEvent only marking it partially damaged. This part is colored differently making the 'ghosting' or 'smearing' obvious. Perhaps I'm using the wrong words because I was unable to find a solution (and I this seems to be a non complex issue simply having to do with the 'damaged' region).
Ok I found the problem, but I'll try to post more details later.
Basically the goal of this code is to move a panel around and then update the parent panel. SetPosition calls Move which going through the wxWidget code calls DoMoveWindow, all of this leads to a change in position and a repaint call (not sure what calls the repaint yet). Great. However the repaint only marks a certain 'area' as it tries to be efficient. That is why some of the issue can be solved by having the panel go over the 'ghosted' area. What you have to do is after the SetPosition, call GetParent().Refresh(), which will send a 'full' paint without any excluded area.
Another thing to note is there are TWO terms for this 'damaged' or 'clipped' area. One is 'damage' however there is another, 'dirty'. Damage is used in the wx PaintDC information
Using wx.PaintDC within EVT_PAINT handlers is important because it
automatically sets the clipping area to the damaged area of the
window. Attempts to draw outside this area do not appear.
Trusting the documentation you will be mostly lost. However in one of the wxPython DoubleBuffer how to's the lingo changes (but it is the same thing as 'damage')
Now the OnPaint() method. It's called whenever ther is a pain event
sent by the system: i.e. whenever part of the window gets dirty.
Knowing this if you Google wx Window dirty you will get the following
Mark the specified rectangle (or the whole window) as "dirty" so it
will be repainted. Causes an EVT_PAINT event to be generated and sent
to the window.
Take the following three print cycles where an EVT_PAINT was fired after a SetPosition call (this is WITHOUT the GetParent().Refresh() call)
# first EVT_PAINT
Drawing
Panel Size (1440, 851)
Clipping Rect (0, 0, 1440, 851)
Client Update Rect (x=0, y=6, w=500, h=501) # the only place getting update is
# directly below the panel
# (that is (500, 500) )
# second
Drawing
Panel Size (1440, 851)
Clipping Rect (0, 0, 1440, 851)
Client Update Rect (x=0, y=6, w=910, h=845) # however this time the update area is
# bigger, this is also right before
# the move
# i believe what it is doing is
# drawing from (0,6) to (910, 851)
# why? because the panel is moving to
# (410, 390) and the bottom right
# corner of the panel (after moved)
# is (410+500, 390+461) = (910, 851)
# or about where the edge of the panel
# will be
# third
Drawing
Panel Size (1440, 851)
Clipping Rect (0, 0, 1440, 851)
Client Update Rect (x=410, y=390, w=500, h=461)
Here is the update code to play around with, hopefully this will help others.
import wx
instructions = """
How to use.
1) Hover your mouse over the pink panel.
2) Click down (left click)
3) While holding down drag mouse around
4) Release mouse button to stop.
"""
class AppPanel(wx.Panel):
def __init__(self, parent, id):
wx.Panel.__init__(self, parent, id)
self.sizer = wx.BoxSizer(wx.VERTICAL)
self.settings_sizer = wx.BoxSizer(wx.HORIZONTAL)
p = MovablePanel(self, -1)
self.c = wx.CheckBox(self, -1, label = "Ghosting On?")
self.p = p
self.i = 0
self.settings_sizer.Add(self.c)
self.sizer.Add(self.settings_sizer)
self.sizer.Add(self.p)
self.SetSizer(self.sizer)
self.Layout()
self.Bind(wx.EVT_CHECKBOX, self.OnCheck, self.c)
self.Bind(wx.EVT_PAINT, self.OnPaint, self)
self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnErase, self)
def OnCheck(self, event):
print "CHECK\n\n\n\n\n"
v = self.c.GetValue()
self.p.r = v
print v
def OnErase(self, event):
pass
def OnPaint(self, event):
print "Drawing"
dc = wx.PaintDC(self)
print "Panel Rect, ", self.p.GetPosition(),
print self.p.GetSize()
print "Clipping Rect", dc.GetClippingBox()
print "Client Update Rect", self.GetUpdateClientRect()
print "----------------------------"
self.i = self.i+10
c = self.i % 255
c = (0, 0, c)
dc.SetPen(wx.Pen(c))
dc.SetBrush(wx.Brush(c))
dc.DrawRectangle(0, 0, 10000,10000)
self.SetBackgroundColour(c)
dc.SetPen(wx.Pen("WHITE"))
dc.SetBrush(wx.Brush("WHITE"))
dc.DrawRectangle(0, 0, self.GetSize()[0], self.c.GetSize()[1])
class MovablePanel(wx.Panel):
def __init__(self, parent, id):
wx.Panel.__init__(self, parent, id)
self.SetMinSize((300,300))
self.SetSize((300,300))
txt = wx.StaticText(self, -1, label = "CLICK AND DRAG ME!")
inst = wx.StaticText(self, -1, label = instructions)
font = wx.Font(18, wx.SWISS, wx.NORMAL, wx.BOLD)
txt.SetFont(font)
inst.SetFont(font)
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(txt, flag = wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_CENTRE_HORIZONTAL)
sizer.Add(inst, flag = wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_CENTRE_HORIZONTAL)
self.SetSizer(sizer)
self.SetBackgroundColour("PINK")
self.LEFT_DOWN = False
self.r = False
self.Bind(wx.EVT_MOTION, self.OnMove, self)
self.Bind(wx.EVT_LEFT_DOWN,
self.OnClickDown,
self)
self.Bind(wx.EVT_LEFT_UP,
self.OnClickUp,
self)
def OnClickUp(self, event):
self.LEFT_DOWN = False
self.Refresh()
def OnClickDown(self, event):
self.LEFT_DOWN = True
self.Refresh()
def OnMove(self, event):
if self.LEFT_DOWN:
p = self.GetTopLevelParent().ScreenToClient(wx.GetMousePosition())
self.SetPosition(p)
if not self.r:
self.GetParent().Refresh()
if __name__ == "__main__":
app = wx.App(False)
f = wx.Frame(None, -1, size = (700, 700))
p = AppPanel(f, -1)
f.Show()
app.MainLoop()

Flicker-free drawable ScrolledWindow

I'm trying to build a ScrolledWindow that you can draw on using the mouse, and it's working too, but I'm getting a nasty flicker when the user is drawing on the window while the scrollbars aren't in the "home" position..
To reproduce, run the attached program, scroll a bit down (or to the right) and "doodle" a bit by keeping the left mouse button pressed. You should see a flickering now and then..
import wx
class MainFrame(wx.Frame):
""" Just a frame with a DrawPane """
def __init__(self, *args, **kw):
wx.Frame.__init__(self, *args, **kw)
s = wx.BoxSizer(wx.VERTICAL)
s.Add(DrawPane(self), 1, wx.EXPAND)
self.SetSizer(s)
########################################################################
class DrawPane(wx.PyScrolledWindow):
""" A PyScrolledWindow with a 1000x1000 drawable area """
VSIZE = (1000, 1000)
def __init__(self, *args, **kw):
wx.PyScrolledWindow.__init__(self, *args, **kw)
self.SetScrollbars(10, 10, 100, 100)
self.prepare_buffer()
self.Bind(wx.EVT_PAINT, self.on_paint)
self.Bind(wx.EVT_LEFT_DOWN, self.on_mouse_down)
self.Bind(wx.EVT_MOTION, self.on_motion)
def prepare_buffer(self):
self.buffer = wx.EmptyBitmap(*DrawPane.VSIZE)
dc = wx.BufferedDC(None, self.buffer)
dc.Clear()
dc.DrawLine(0, 0, 999, 999) # Draw something to better show the flicker problem
def on_paint(self, evt):
dc = wx.BufferedPaintDC(self, self.buffer, wx.BUFFER_VIRTUAL_AREA)
def on_mouse_down(self, evt):
self.mouse_pos = self.CalcUnscrolledPosition(evt.GetPosition()).Get()
def on_motion(self, evt):
if evt.Dragging() and evt.LeftIsDown():
dc = wx.BufferedDC(wx.ClientDC(self), self.buffer)
newpos = self.CalcUnscrolledPosition(evt.GetPosition()).Get()
coords = self.mouse_pos + newpos
dc.DrawLine(*coords)
self.mouse_pos = newpos
self.Refresh()
if __name__ == "__main__":
app = wx.PySimpleApp()
wx.InitAllImageHandlers()
MainFrame(None).Show()
app.MainLoop()
I tried using SetBackgroundStyle(wx.BG_STYLE_CUSTOM), or binding EVT_ERASE_BACKGROUND, or using RefreshRect instead of Refresh, but the flicker is still there.. Any idea on what I might try next?
My environment: Xubuntu 9.04, wxPython 2.8.9.1
(but tested on Ubuntu 10.04 too)
Many thanks for your time!
From Robin Dunn himself:
First, a Refresh() by default will
erase the background before sending
the paint event (although setting the
BG style or catching the erase event
would have taken care of that.) The
second and probably most visible
problem in this case is that in your
on_motion handler you are not
offsetting the ClientDC by the scroll
offsets, just the position in the
buffer that you are drawing the line
segment at. So when the buffer is
flushed out to the client DC it is
drawn at the physical (0,0), not the
virtual (0,0). In other words, the
flicker you are seeing is coming from
drawing the buffer at the wrong
position after every mouse drag event,
and then it immediately being drawn
again at the right position in the
on_paint triggered by the
Refresh().
You should be able to fix this by
calling PrepareDC on the client DC
before using it, like this:
cdc = wx.CLientDC(self)
self.PrepareDC(cdc)
dc = wx.BufferedDC(cdc, self.buffer)
However since you are doing a
Refresh or RefreshRect anyway,
there is no need to use a client DC
here at all, just let the flushing of
the buffer to the screen be done in
on_paint instead:
dc = wx.BufferedDC(None, self.buffer)
Using Joril recomendations and removing Refresh(), there is no flicker anymore (even enlarging the frame).
import wx
class MainFrame(wx.Frame):
""" Just a frame with a DrawPane """
def __init__(self, *args, **kw):
wx.Frame.__init__(self, *args, **kw)
s = wx.BoxSizer(wx.VERTICAL)
s.Add(DrawPane(self), 1, wx.EXPAND)
self.SetSizer(s)
########################################################################
class DrawPane(wx.PyScrolledWindow):
""" A PyScrolledWindow with a 1000x1000 drawable area """
VSIZE = (1000, 1000)
def __init__(self, *args, **kw):
wx.PyScrolledWindow.__init__(self, *args, **kw)
self.SetScrollbars(10, 10, 100, 100)
self.prepare_buffer()
cdc = wx.ClientDC(self)
self.PrepareDC(cdc)
dc = wx.BufferedDC(cdc, self.buffer)
self.Bind(wx.EVT_PAINT, self.on_paint)
self.Bind(wx.EVT_LEFT_DOWN, self.on_mouse_down)
self.Bind(wx.EVT_MOTION, self.on_motion)
def prepare_buffer(self):
self.buffer = wx.EmptyBitmap(*DrawPane.VSIZE)
cdc = wx.ClientDC(self)
self.PrepareDC(cdc)
dc = wx.BufferedDC(cdc, self.buffer)
dc.Clear()
dc.DrawLine(0, 0, 999, 999) # Draw something to better show the flicker problem
def on_paint(self, evt):
dc = wx.BufferedPaintDC(self, self.buffer, wx.BUFFER_VIRTUAL_AREA)
def on_mouse_down(self, evt):
self.mouse_pos = self.CalcUnscrolledPosition(evt.GetPosition()).Get()
def on_motion(self, evt):
if evt.Dragging() and evt.LeftIsDown():
newpos = self.CalcUnscrolledPosition(evt.GetPosition()).Get()
coords = self.mouse_pos + newpos
cdc = wx.ClientDC(self)
self.PrepareDC(cdc)
dc = wx.BufferedDC(cdc, self.buffer)
dc.DrawLine(*coords)
self.mouse_pos = newpos
if __name__ == "__main__":
app = wx.PySimpleApp()
wx.InitAllImageHandlers()
MainFrame(None).Show()
app.MainLoop()

wxPython - Drawing an unfilled rectangle with the DC

dc.SetPen(wx.Pen(wx.BLACK, 0))
dc.SetBrush(wx.Brush("C0C0C0"))
dc.DrawRectangle(50,50,50,50)
This is my best attempt at drawing a 50x50, gray box with no border. However, setting the pen width to 0 doesn't seem to accomplish anything, and setting the brush only changes the fill from pure white to pure black.
Here's it in the context of a panel, in case it's part of the problem:
class DrawRect(wx.Panel):
def __init__(self,parent=None,id=-1,pos=(-1,-1),size=(-1,-1),style=0):
wx.Panel.__init__(self,parent,id,size,pos,style)
self.SetBackgroundColour("#D18B47")
self.Bind(wx.EVT_PAINT,self.onPaint)
def onPaint(self, event):
event.Skip()
dc = wx.PaintDC(event.GetEventObject())
self.drawRect(dc)
def drawRect(self,dc):
dc.SetPen(wx.Pen("FFCE8A", 0))
dc.SetBrush(wx.Brush("C0C0C0"))
dc.DrawRectangle(50,50,50,50)
This makes a grey rectangle:
import wx
class MyPanel(wx.Panel):
""" class MyPanel creates a panel to draw on, inherits wx.Panel """
def __init__(self, parent, id):
# create a panel
wx.Panel.__init__(self, parent, id)
self.SetBackgroundColour("white")
self.Bind(wx.EVT_PAINT, self.OnPaint)
def OnPaint(self, evt):
"""set up the device context (DC) for painting"""
self.dc = wx.PaintDC(self)
self.dc.BeginDrawing()
self.dc.SetPen(wx.Pen("grey",style=wx.TRANSPARENT))
self.dc.SetBrush(wx.Brush("grey", wx.SOLID))
# set x, y, w, h for rectangle
self.dc.DrawRectangle(250,250,50, 50)
self.dc.EndDrawing()
del self.dc
app = wx.PySimpleApp()
# create a window/frame, no parent, -1 is default ID
frame = wx.Frame(None, -1, "Drawing A Rectangle...", size = (500, 500))
# call the derived class, -1 is default ID
MyPanel(frame,-1)
# show the frame
frame.Show(True)
# start the event loop
app.MainLoop()
In order to draw a non-filled rectangle you need to set brush transparent as the brush does the filling and the pen draws the outline. The example below draws a blue non-filled rectangle alongside a red filled one.
import wx
class MyPanel(wx.Panel):
""" class MyPanel creates a panel to draw on, inherits wx.Panel """
def __init__(self, parent, id):
wx.Panel.__init__(self, parent, id)
self.SetBackgroundColour("white")
self.Bind(wx.EVT_PAINT, self.OnPaint)
def OnPaint(self, event):
"""set up the device context (DC) for painting"""
dc = wx.PaintDC(self)
#blue non-filled rectangle
dc.SetPen(wx.Pen("blue"))
dc.SetBrush(wx.Brush("blue", wx.TRANSPARENT)) #set brush transparent for non-filled rectangle
dc.DrawRectangle(10,10,200,200)
#red filled rectangle
dc.SetPen(wx.Pen("red"))
dc.SetBrush(wx.Brush("red"))
dc.DrawRectangle(220,10,200,200)
app = wx.App()
frame = wx.Frame(None, -1, "Drawing A Rectangle...", size=(460, 300))
MyPanel(frame,-1)
frame.Show()
frame.Centre()
app.MainLoop()

Categories