Need help working with self.MemoryDC in wxPython - python

I'm trying to make a custom text widget that is double buffered (In order to avoid flicker).
However, I'd like to be able to do a few things. Yet, I'm unsure of the exact methods I should use.
The first two are easy I simply want to change the background and foreground color.
So more or less I want to be able to change the text color for self.Text in self.Draw().
Snippet:
self.Text = mdc.DrawText(self.TextString, 10, 0)
As sell as the Background (fill) color for self.MemoryDC.
Next, does anyone know how I could center self.Text? Finally, how do I configure self.Text after it has been created?
The widget thus far:
class DynamicText (wx.Panel):
def __init__(self, par):
self.Par = par
wx.Panel.__init__(self, self.Par)
self.Time = Time(self, func=self.SetTime)
self.Dim = self.Par.GetClientSize()
self.SetSize(self.Dim)
self.Bind(wx.EVT_SIZE, self.Resize)
self.Bind(wx.EVT_ERASE_BACKGROUND, self.Erase)
self.Bind(wx.EVT_PAINT, self.Paint)
def Set (self, text) :
self.TextString = text
def SetTime (self, time) :
self.Set(str(time))
self.Resize(None)
def Resize(self, event):
self.Width, self.Height = self.GetSize()
bitmap = wx.EmptyBitmap(self.Width, self.Height)
self.MemoryDC = wx.MemoryDC(bitmap)
''' Redraws **self.MemoryDC** '''
mdc = self.MemoryDC
''' Deletes everything from widget. '''
mdc.Clear()
fs = 11
font = wx.Font( fs, wx.DEFAULT, wx.NORMAL, wx.NORMAL)
mdc.SetFont(font)
self.Draw()
self.Refresh()
def Draw (self) :
mdc = self.MemoryDC
self.Text = mdc.DrawText(self.TextString, 10, 0)
def Erase(self, event):
''' Does nothing, as to avoid flicker. '''
pass
def Paint(self, event):
pdc = wx.PaintDC(self)
w, h = self.MemoryDC.GetSize()
pdc.Blit(0, 0, w, h, self.MemoryDC, 0, 0)

I don't understand what you mean by configuring self.Text after it was created. If you want to change the text after you've drawn it - you can't. Once you've drawn it to the DC it's there, and the only way to change it would be to clear the DC and repaint it. In your case, it seems all you need to do when the text is updated is to call Resize() again, forcing a redraw. Note that DrawText() retruns nothing, so the value of your self.Text would be None. You definitely can't use that to refer to the drawn text. :D
As for the rest, here's an example of a Draw() method that centers the text and paints it blue:
def Draw(self) :
mdc = self.MemoryDC
dc_width, dc_height = mdc.GetSizeTuple()
text_width, text_height, descent, externalLeading = mdc.GetFullTextExtent(self.TextString)
x = (dc_width - text_width) / 2
y = (dc_height - text_height) / 2
mdc.SetTextForeground('Blue')
mdc.DrawText(self.TextString, x, y)

Related

Making "Snipping Tool" from Python using Wx and PyAutoGui

I am trying to make a snipping tool using the Wx and PyAutoGui modules, I am stuck at a certain problem: The saved image file is at wrong position (see image below)
Picture Link
As you can see I am trying to grab a specific region, that is the red rectangle, and save those pixels in that region to the file "my_screenshot.png". However, the position/coordinates seems to be off (you can see the rectangles, which was supposed to be the region of the screenshot)
Here are the codes:
import wx
import pyautogui
class SelectableFrame(wx.Frame):
c1 = None
c2 = None
def __init__(self, parent=None, id=-1, title=""):
wx.Frame.__init__(self, parent, id, title, size=wx.DisplaySize())
self.panel = wx.Panel(self, size=self.GetSize())
self.panel.Bind(wx.EVT_MOTION, self.OnMouseMove)
self.panel.Bind(wx.EVT_LEFT_DOWN, self.OnMouseDown)
self.panel.Bind(wx.EVT_LEFT_UP, self.OnMouseUp)
self.panel.Bind(wx.EVT_PAINT, self.OnPaint)
self.SetCursor(wx.Cursor(wx.CURSOR_CROSS))
self.SetTransparent(50)
def OnMouseMove(self, event):
if event.Dragging() and event.LeftIsDown():
self.c2 = event.GetPosition()
self.Refresh()
def OnMouseDown(self, event):
self.c1 = event.GetPosition()
def OnMouseUp(self, event):
self.SetCursor(wx.Cursor(wx.CURSOR_ARROW))
region = (self.c1.x, self.c1.y, self.c2.x - self.c1.x, self.c2.y - self.c1.y)
pyautogui.screenshot('my_screenshot.png', region=region)
print("MouseUp: " + str(region))
self.Hide()
def OnPaint(self, event):
if self.c1 is None or self.c2 is None: return
dc = wx.PaintDC(self.panel)
dc.SetPen(wx.Pen('red', 1))
dc.SetBrush(wx.Brush(wx.Colour(0, 0, 0), wx.TRANSPARENT))
region = (self.c1.x, self.c1.y, self.c2.x - self.c1.x, self.c2.y - self.c1.y)
dc.DrawRectangle(self.c1.x, self.c1.y, self.c2.x - self.c1.x, self.c2.y - self.c1.y)
print("Draw: " + str(region))
def PrintPosition(self, pos):
return str(pos.x) + " " + str(pos.y)
class MyApp(wx.App):
def OnInit(self):
frame = SelectableFrame()
frame.Show(True)
self.SetTopWindow(frame)
return True
app = MyApp(0)
app.MainLoop()
From what I've gathered that, it took the width and height, but wrong x and y position, how can I fix that? Thank you
EDIT: There seems to be values difference between these functions
def OnMouseDown(self, event):
self.c1 = event.GetPosition()
print("MouseDown[event]: " + str(self.c1))
print("MouseDown[gui]: "+ str(pyautogui.position()))
Output:
MouseDown[event]: (729, 484)
MouseDown[gui]: Point(x=737, y=515)
The offset is +8 for x, and +31 for y. How did this incontinence happen? My hotfix would be to add those offsets to the pyautogui.screenshot command but I don't think that is the right fix and would not be guaranteed to be the same offset values for other screen sizes..
The wxpython MouseEvent doc tells
The position associated with a mouse event is expressed in the window
coordinates of the window which generated the event, you can use
wx.Window.ClientToScreen to convert it to screen coordinates and
possibly call wx.Window.ScreenToClient next to convert it to window
coordinates of another window.
pyautogui' screenshot() works with screen coordinates.
So, use wx.Window.ClientToScreen with both your c1 and c2.
BTW, you should update c2 also at OnMouseUp and check it not to be 'None' nor equal to c1.

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.

Rendering text to a kivy canvas

I am trying to draw my own graphic within a kivy 'canvas'. For now I have a red or green rectangle which changes colour once per second, but I want to add a changing text label.
After a little searching it appears that there isn't a "Text" Instruction which can be added to the canvas. I have found a few references to using a Label() widget as well as the canvas Instructions, but this does not seem ideal, and also I can't seem to get it to render more than once.
Here's my object as it stands at the moment:
class HVObject(BoxLayout):
def __init__(self, **kwargs):
BoxLayout.__init__(self, **kwargs)
self.colour = 1
self.label = Label()
self.render()
self.add_widget(self.label)
self.bind(size=self._update_rect, pos=self._update_rect)
Clock.schedule_interval(self.callevery, 1)
def render(self):
self.canvas.clear()
self.rect = Rectangle(size=self.size, pos=self.pos)
self.canvas.add(Color(1-self.colour, self.colour, 0, 1))
self.canvas.add(self.rect)
self.label.text = "COL %d" % self.colour
self.canvas.ask_update()
def callevery(self, x):
self.colour = 1-self.colour
self.render()
def _update_rect(self, instance, value):
self.rect.pos = instance.pos
self.rect.size = instance.size
self.label.pos = instance.pos
Is there an easy way to achieve the effect I need?
Thank you
Answering my own question:
After a little look around the [kivy] garden, I found Tickline (and Tick). and the use of CoreLabel() and Rectangle(texture=...)
Here's my updated render() method which adds the text object I need.
def render(self):
self.canvas.clear()
self.canvas.add(Color(1-self.colour, self.colour, 0, 1))
self.rect = Rectangle(size=self.size, pos=self.pos)
self.canvas.add(self.rect)
label = CoreLabel(text="COL %d" % self.colour, font_size=20)
label.refresh()
text = label.texture
self.canvas.add(Color(self.colour, 1-self.colour,0, 1))
pos = list(self.pos[i] + (self.size[i] - text.size[i]) / 2 for i in range(2))
self.canvas.add(Rectangle(size=text.size, pos=pos, texture=text))
self.canvas.ask_update()
Which works for me, albeit a little clunky!

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()

Tkinter Canvas - Draw text on top

I have this class to create a Statusbar:
class Statusbar(Canvas):
'''Creates a statusbar widget'''
def __init__(self, master = None, **options):
if not master: master = Tk()
self.master, self.options = master, options
self.barFill, self.addText, self.value = self.options.get('barFill', 'red'), self.options.get('addText', True), 0
for option in ('barFill', 'addText'):
if option in self.options: del self.options[option]
Canvas.__init__(self, master, **self.options)
self.offset = self.winfo_reqwidth() / 100
self.height = self.winfo_reqwidth()
if self.addText: self.text = self.create_text(self.winfo_reqwidth()/2, self.winfo_reqheight()/2, text = '0%')
self.bar = self.create_rectangle(0, 0, self.value, self.height, fill = self.barFill)
def setValue(self, value):
'''Sets the value of the status bar as a percent'''
self.value = value * self.offset
self.coords(self.bar, 0, 0, self.value, self.height)
if self.addText: self.itemconfigure(self.text, text = str(self.value/self.offset) + '%')
def change(self, value):
'''Changes the value as a percent'''
self.value += (value * self.offset)
self.coords(self.bar, 0, 0, self.value, self.height)
if self.addText: self.itemconfigure(self.text, text = str(self.value/self.offset) + '%')
My issue is that the text is always drawn under rectangle. So when the rectangle reaches the text, you can't see the text anymore. How can I fix this? Thanks in advance.
The fact that one object sits atop another is called the stacking order. By default, objects created later have a higher stacking order than those that were created earlier. So, one solution is to draw the rectangle and then draw the text.
You can also move things up or down the stacking order using the lift and lower commands of a canvas. You must give it an id or tag of what you want to lift or lower, and optionally an id or tag of the object you want the first object(s) to be above or below.
So, for example, you could raise the text above the rectangle like this:
self.lift(self.text, self.bar)
If you want to get really fancy, you can create the notion of layers. I gave an example in another answer, here: https://stackoverflow.com/a/9576938/7432
In my programming class, we said put whatever text you don't want to be blocked drawn last. So put the text at the bottom of what function you are using to draw with

Categories