wxPython - lines drawn with Device Context disappear when focus changes - python

I've written this small app that draws lines between two points selected by the user and it works but how do I keep the lines I draw from disappearing whenever the window is minimized or gets covered by another open window?
class SimpleDraw(wx.Frame):
def __init__(self, parent, id, title, size=(640, 480)):
self.points = []
wx.Frame.__init__(self, parent, id, title, size)
self.Bind(wx.EVT_LEFT_DOWN, self.DrawDot)
self.SetBackgroundColour("WHITE")
self.Centre()
self.Show(True)
def DrawDot(self, event):
self.points.append(event.GetPosition())
if len(self.points) == 2:
dc = wx.ClientDC(self)
dc.SetPen(wx.Pen("#000000", 10, wx.SOLID))
x1, y1 = self.points[0]
x2, y2 = self.points[1]
dc.DrawLine(x1, y1, x2, y2)
# reset the list to empty
self.points = []
if __name__ == "__main__":
app = wx.App()
SimpleDraw(None, -1, "Title Here!")
app.MainLoop()

Your issue is that you are only drawing when the user clicks. The resize/erase (when another window covers yours) problems are because your window doesn't maintain a "buffer" which it can redraw.
Here, I've modified your sample, it seems to be working okay.
import wx
class SimpleDraw(wx.Frame):
def __init__(self, parent, id, title, size=(640, 480)):
self.points = []
wx.Frame.__init__(self, parent, id, title, size)
self.Bind(wx.EVT_LEFT_DOWN, self.DrawDot)
self.Bind(wx.EVT_PAINT, self.Paint)
self.SetBackgroundColour("WHITE")
self.Centre()
self.Show(True)
self.buffer = wx.EmptyBitmap(640, 480) # draw to this
dc = wx.BufferedDC(wx.ClientDC(self), self.buffer)
dc.Clear() # black window otherwise
def DrawDot(self, event):
self.points.append(event.GetPosition())
if len(self.points) == 2:
dc = wx.BufferedDC(wx.ClientDC(self), self.buffer)
dc.Clear()
dc.SetPen(wx.Pen("#000000", 10, wx.SOLID))
x1, y1 = self.points[0]
x2, y2 = self.points[1]
dc.DrawLine(x1, y1, x2, y2)
# reset the list to empty
self.points = []
def Paint(self, event):
wx.BufferedPaintDC(self, self.buffer)
if __name__ == "__main__":
app = wx.App(0)
SimpleDraw(None, -1, "Title Here!")
app.MainLoop()

You have to structure your program differently in a GUI environment. Typically, you maintain a data structure called your model. In your case, you already have a start of one, self.points. Then you only draw on the window in response to a paint event. The windowing system will send you paint events when the window needs painting, including when it is first displayed, when it is maximized, and when it is revealed from beneath another window.
In your program, you'd bind the LeftDown event to a function that modifies self.points and invalidates the window, which would usually cause the windowing system to send you paint events. You'd bind the Paint event to a function that draws on the window.

Related

PyQt creating label that follows mouse

What I want to archive is a label that is created as soon as a button is pressed and follows the mouse until there is a 'click'.
My problem with that is that I can't seem to get the 'setMouseTracking(True)' command at the right widget...
import sys
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from PyQt5.QtCore import Qt
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.setGeometry(0,0,1000,1100)
self.main = QtWidgets.QLabel()
self.setCentralWidget(self.main)
self.label = QtWidgets.QLabel()
canvas = QtGui.QPixmap(900, 900)
canvas.fill(QtGui.QColor('#ffffff')) # Fill entire canvas.
self.label.setPixmap(canvas)
# self.last_x, self.last_y = None, None
self.button = QtWidgets.QPushButton('create Block')
self.button.clicked.connect(self.buttonAction)
vbox = QtWidgets.QVBoxLayout()
vbox.addWidget(self.label)
vbox.addWidget(self.button)
self.main.setLayout(vbox)
# self.label.setMouseTracking(True)
self.setWindowTitle('testing')
def mouseMoveEvent(self, e):
# if self.last_x is None: # First event.
# self.last_x = e.x()
# self.last_y = e.y()
# return # Ignore the first time.
# painter = QtGui.QPainter(self.label.pixmap())
# painter.drawLine(self.last_x, self.last_y, e.x(), e.y())
# painter.end()
try:
self.image.move(e.x(), e.y())
except:
pass
self.update()
# Update the origin for next time.
# self.last_x = e.x()
# self.last_y = e.y()
def mouseReleaseEvent(self, e):
# self.last_x = None
# self.last_y = None
def buttonAction(self):
block = QtGui.QPixmap(20, 20)
block.fill(QtGui.QColor('blue'))
self.image = QtWidgets.QLabel(self.label)
self.image.setPixmap(block)
self.image.move(20,20)
self.image.show()
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()
I don't know if my problem is that I attach setMouseTracking(True) to the wrong widget or if it is something else entirerly.
With clicking action it works, but that is not what I intend to do...
Edit: fixed some of the code issues
For clarification what my problem is: I have a canvas and a button inside an layout, as soon as the button is clicked a new canvas shall be created that follows the mouse pointer 'until' I click. So I don't want any kind of drag-and-drop action, but instead a small canvas that is following the mouse pointer.
This is needed as I intend to use the little canvas to show what an graphic would look like at a certain canvas position without printing it there. So the little canvas is something like a template.
There are some conceptual problems in your logic.
First of all, the mouse tracking only works for the widget it's set on. Also, if the widget accepts the mouse move event, the parent will not receive it.
In your case you are not receiving it because you are implementing the mouseMoveEvent in the main window, which by default ignores it if no mouse button is pressed (like most widgets).
While you could try to set it on the "target" widget and the parent (in your case, the canvas and the main window), you'll certainly have some issues at a certain point if any underlying widget accepts mouse movements; since you're going to need the "preview" only on the actual "canvas", there's no need to create a new widget, as you can just directly paint on the canvas instead, and finally draw on the actual pixmap only when needed.
This is a possible implementation:
class Canvas(QtWidgets.QLabel):
def __init__(self):
super().__init__()
pixmap = QtGui.QPixmap(900, 900)
pixmap.fill(QtCore.Qt.white)
self.setPixmap(pixmap)
self.setMouseTracking(True)
self.preview = False
def startPreview(self):
self.preview = True
self.update()
def drawMiniCanvas(self, pos):
pm = self.pixmap()
qp = QtGui.QPainter(pm)
qp.setBrush(QtCore.Qt.blue)
if self.size() != pm.size():
# if the pixmap is smaller than the actual size of the canvas, the position
# must be translated to its contents before painting
alignment = self.alignment()
pmRect = pm.rect()
if alignment == QtCore.Qt.AlignCenter:
pmRect.moveCenter(self.rect().center())
else:
if alignment & QtCore.Qt.AlignHCenter:
pmRect.moveLeft((self.width() - pm.width()) / 2)
elif alignment & QtCore.Qt.AlignRight:
pmRect.moveRight(self.width())
if alignment & QtCore.Qt.AlignVCenter:
pmRect.moveTop((self.height() - pm.height()) / 2)
elif alignment & QtCore.Qt.AlignBottom:
pmRect.moveBottom(self.height())
pos -= pmRect.topLeft()
qp.drawRect(pos.x(), pos.y(), 20, 20)
qp.end()
self.setPixmap(pm)
def mouseMoveEvent(self, event):
if self.preview:
self.update()
def mousePressEvent(self, event):
if self.preview:
if event.button() == QtCore.Qt.LeftButton:
self.drawMiniCanvas(event.pos())
self.preview = False
def paintEvent(self, event):
super().paintEvent(event)
if self.preview:
qp = QtGui.QPainter(self)
qp.drawRect(self.rect().adjusted(0, 0, -1, -1))
pos = self.mapFromGlobal(QtGui.QCursor.pos())
qp.setBrush(QtCore.Qt.blue)
qp.drawRect(pos.x(), pos.y(), 20, 20)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.setGeometry(0,0,1000,1100)
self.main = QtWidgets.QLabel()
self.setCentralWidget(self.main)
self.canvas = Canvas()
self.button = QtWidgets.QPushButton('create Block')
self.button.clicked.connect(self.canvas.startPreview)
vbox = QtWidgets.QVBoxLayout()
vbox.addWidget(self.canvas)
vbox.addWidget(self.button)
self.main.setLayout(vbox)
self.setWindowTitle('testing')
Note that I have left the main widget as a QLabel as per your code, but I strongly suggest to avoid so: QLabel has a complex management of its size, and even if you add a layout to it, the layout requirements will always be ignored; you should use a QWidget instead.
Finally, while the above code works, it's just a simple example based on your question; if you want to create a drawing tool, you should not use a QLabel, and for various reasons: for example, if you want to support scaling to fit the contents, not only the coordinate computation in drawMiniCanvas won't work (due to the scaling), but it will also not paint anything at all, and that's due to the way QLabel caches its contents whenever setScaledContents(True) is used (also, it won't respect the aspect ratio).
For advanced and interactive painting, it's usually better to use a QGraphicsScene shown inside a QGraphicsView.

Drawing a line in wxPython using mouse position

I'm trying to make program in wxPython, that will draw a line in a position in which I clicked on a window but it doesn't work and I actually don't know why. How could I write this, that it will work?
import wx
global coord
coord = (30, 30)
class MyFrame(wx.Frame):
"""create a color frame, inherits from wx.Frame"""
global coord
def __init__(self, parent):
# -1 is the default ID
wx.Frame.__init__(self, parent, -1, "Click for mouse position", size=(400,300),
style=wx.DEFAULT_FRAME_STYLE | wx.NO_FULL_REPAINT_ON_RESIZE)
self.SetBackgroundColour('Goldenrod')
self.SetCursor(wx.StockCursor(wx.CURSOR_PENCIL))
# hook some mouse events
self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown)
self.Bind(wx.EVT_PAINT, self.OnPaint)
def OnLeftDown(self, event):
global coord
"""left mouse button is pressed"""
pt = event.GetPosition() # position tuple
print pt
coord = pt
self.SetTitle('LeftMouse = ' + str(pt))
def OnRightDown(self, event):
global coord
"""right mouse button is pressed"""
pt = event.GetPosition()
coord = pt
print pt
self.SetTitle('RightMouse = ' + str(pt))
def OnPaint(self, event):
global coord
dc = wx.PaintDC(self)
dc.Clear()
dc.SetPen(wx.Pen(wx.BLACK, 4))
dc.DrawLine(0, 0, int(str(coord[0])), int(str(coord[1])))
app = wx.PySimpleApp()
frame = MyFrame(None)
frame.Show(True)
app.MainLoop()
Call
self.Refresh()
at the end of OnLeftDown/OnRightDown. Otherwise the system does not know it should redraw so it won't redraw...
(also, generally speaking, it may be better to have coord as a member variable, not a global)

Persistent drawing + temporary overlay with wx.PaintDC

I'm trying to make something of a Microsoft Paint-esque app using wxPython.
Currently, the app 'draws' onto the screen with a circular brush during a mouse left-down event. This is great, and the desired behavior. But I also need a circle of the same radius to 'follow' the mouse, without it drawing persistently onto wx.PaintDC.
That is, a circle of some radius follows the mouse around the screen, but only when the left mouse button is held, should the circle be 'permanently' drawn onto the buffer.
The approaches I've taken either (1) have a circle following the mouse around, but draw onto the PaintDC instance regardless of mouse-down, (2) follow the mouse around but never draw persistently onto the PaintDC instance, or (3) do not follow the mouse around, but appear and are drawn persistently on left-mouse down (see the example below).
Thank you!
import wx
class MyPanel(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent, -1)
self.draw = False
self.radius = 50
self.Bind(wx.EVT_PAINT, self.OnPaint)
self.Bind(wx.EVT_MOTION, self.Draw)
self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp)
def OnPaint(self, event):
dc = wx.PaintDC(self)
def Draw(self, event):
if self.draw == True:
x = event.GetX()
y = event.GetY()
dc = wx.ClientDC(self)
pen = wx.Pen(wx.Colour(192,192,192,128), 2)
brush = wx.Brush(wx.Colour(192,192,192,128))
dc.SetPen(pen)
dc.SetBrush(brush)
dc.DrawCircle(x,y,self.radius)
def OnLeftDown(self, event):
self.draw = True
def OnLeftUp(self, event):
self.draw = False
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 the wx.Overlay class that does a pretty good job assisting with drawing temporary things on top of more permanent stuff. See the Overlay sample in the demo: https://github.com/wxWidgets/Phoenix/blob/master/demo/Overlay.py

Update position of custom QGraphicsItem on connected item change

I have two subclassed QGraphicsRectItems that are supposed to be connected with a line that adjusts based on the position of the textboxes.
In the diagramscene example of the Qt docus the itemChanged method of a subclassed QGraphicsPolygonItem calls a updatePosition method of the connected arrow which calls setLine to update the arrow's position. In my case I cannot call setLine as I am subclassing QGraphicsItem instead of QGraphicsLineItem.
How should I implement updatePosition method in the Arrow class below to update the position of my QGraphicsItem? The following is a runnable example that shows what happens currently when the textboxes are clicked and moved.
import sys
from PyQt4.QtGui import *
from PyQt4.QtCore import *
class Arrow(QGraphicsItem):
def __init__(self, startItem, endItem, parent=None, scene=None):
super().__init__(parent, scene)
self.startItem = startItem
self.endItem = endItem
def boundingRect(self):
p1 = self.startItem.pos() + self.startItem.rect().center()
p3 = self.endItem.pos() + self.endItem.rect().center()
bounds = p3 - p1
size = QSizeF(abs(bounds.x()), abs(bounds.y()))
return QRectF(p1, size)
def paint(self, painter, option, widget=None):
p1 = self.startItem.pos() + self.startItem.rect().center()
p3 = self.endItem.pos() + self.endItem.rect().center()
pen = QPen()
pen.setWidth(1)
painter.setRenderHint(QPainter.Antialiasing)
if self.isSelected():
pen.setStyle(Qt.DashLine)
else:
pen.setStyle(Qt.SolidLine)
pen.setColor(Qt.black)
painter.setPen(pen)
painter.drawLine(QLineF(p1, p3))
painter.setBrush(Qt.NoBrush)
def updatePosition(self):
#Not sure what to do here...
class TextBox(QGraphicsRectItem):
def __init__(self, text, position, rect=QRectF(0, 0, 200, 100),
parent=None, scene=None):
super().__init__(rect, parent, scene)
self.setFlags(QGraphicsItem.ItemIsFocusable |
QGraphicsItem.ItemIsMovable |
QGraphicsItem.ItemIsSelectable)
self.text = QGraphicsTextItem(text, self)
self.setPos(position)
self.arrows = []
def paint(self, painter, option, widget=None):
painter.setPen(Qt.black)
painter.setRenderHint(QPainter.Antialiasing)
painter.setBrush(Qt.white)
painter.drawRect(self.rect())
def addArrow(self, arrow):
self.arrows.append(arrow)
def itemChange(self, change, value):
if change == QGraphicsItem.ItemPositionChange:
for arrow in self.arrows:
arrow.updatePosition()
return value
if __name__ == "__main__":
app = QApplication(sys.argv)
view = QGraphicsView()
scene = QGraphicsScene()
scene.setSceneRect(0, 0, 500, 1000)
view.setScene(scene)
textbox1 = TextBox("item 1", QPointF(50, 50), scene=scene)
textbox1.setZValue(1)
textbox2 = TextBox("item 2", QPointF(100, 500), scene=scene)
textbox2.setZValue(1)
arrow = Arrow(textbox1, textbox2, scene=scene)
arrow.setZValue(0)
textbox1.addArrow(arrow)
textbox2.addArrow(arrow)
view.show()
sys.exit(app.exec_())
The position of the item doesn't actually matter - it can remain at 0,0 - providing the bounding box is correct (which it will be according to your Arrow::boundingBox implementation). Hence, I think if you simply trigger a bounding box change, and a redraw in updatePosition, everything will work as you want.
Of course, if you care about the position of the arrow being at the head or tail of the line, you can move it in updatePosition, and adjust the bounding box / paint coordinates accordingly - but that's entirely up to you if that makes sense or not.

Drawing a circle on the current window that moves (Python)

I'm facing a problem with python.
i want to draw on my monitor a circle, that can move around.
let's say i have my browser open, i want to be able to make the circle go around on his own AND be able to use the mouse to press any button i want.
the idea is that the circle is connected to my hands movement thanks to Leap Motion, and i want to display the gestures i make while being able to use the mouse.
my worries are that i have to make a trasparent window which doesn't let me to use the mouse because i would clik on the trasparent window.
Thanks!
Found the solution, i used wxPython
Down below the code:
import wx, inspect, os, sys, win32gui
IMAGE_PATH = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) + '\cerchio.png'
class Cursor(wx.Frame):
def __init__(self, parent, log):
self.log = log
self.delta = wx.Point(0,0)
wx.Frame.__init__(self, parent, -1, "Shaped Window",
style =
wx.FRAME_SHAPED
| wx.SIMPLE_BORDER
| wx.FRAME_NO_TASKBAR
| wx.STAY_ON_TOP
)
self.timer = wx.Timer(self)
self.Bind(wx.EVT_TIMER, self.update, self.timer)
self.Bind(wx.EVT_PAINT, self.OnPaint)
self.x=0
self.y=0
self.hasShape = False
self.SetClientSize( (50, 50) )
image = wx.Image(IMAGE_PATH, wx.BITMAP_TYPE_PNG)
image.SetMaskColour(255,255,255)
image.SetMask(True)
self.bmp = wx.BitmapFromImage(image)
self.SetWindowShape()
self.timer.Start(1)
dc = wx.ClientDC(self)
dc.DrawBitmap(self.bmp, 0, 0, True)
def OnExit(self, evt):
self.Close()
def SetWindowShape(self, *evt):
# Use the bitmap's mask to determine the region
r = wx.RegionFromBitmap(self.bmp)
self.hasShape = self.SetShape(r)
def OnPaint(self, evt):
dc = wx.PaintDC(self)
dc.DrawBitmap(self.bmp, 0, 0, True)
def OnExit(self, evt):
self.Close()
def update(self, event):
#self.x, self.y = win32gui.GetCursorPos()
self.SetPosition((self.x,self.y))
if __name__ == '__main__':
app = wx.App( False )
frm = Cursor(None, -1)
frm.Show()
app.MainLoop()
This maps 50x50 png image with white background (modify IMAGE_PATH) to the cursor position (you won't be able to click though)

Categories

Resources