How not to get a gtkdrawingarea cleared in the draw event? - python-3.x

My drawingarea is cleared everytime the draw event is called.
How to avoid a drawingarea to be cleared ?
Thanks
#!/usr/bin/env python3
import gi
gi.require_version('Gtk','3.0')
from gi.repository import Gtk, Gdk
import cairo
import math
class MouseButtons:
LEFT_BUTTON = 1
RIGHT_BUTTON = 3
class Example(Gtk.Window):
def __init__(self):
super(Example, self).__init__()
self.init_ui()
def init_ui(self):
self.darea = Gtk.DrawingArea()
self.darea.connect("draw", self.on_draw)
self.darea.set_events(Gdk.EventMask.BUTTON_PRESS_MASK)
self.add(self.darea)
self.set_title("Fill & stroke")
self.resize(230, 150)
self.set_position(Gtk.WindowPosition.CENTER)
self.connect("delete-event", Gtk.main_quit)
self.darea.connect("button-press-event", self.on_button_press)
self.coords = []
self.show_all()
def on_draw(self, wid, cr):
cr.set_source_rgb(0.6, 0.6, 0.6)
cr.arc(self.coords[0], self.coords[1], 40, 0, 2*math.pi)
cr.fill()
def on_button_press(self, w, e):
if e.type == Gdk.EventType.BUTTON_PRESS \
and e.button == MouseButtons.LEFT_BUTTON:
self.coords = [e.x, e.y]
self.darea.queue_draw()
def main():
app = Example()
Gtk.main()
if __name__ == "__main__":
main()
In this example, each time I click on the drawingarea, a circle is drawn. I would like to draw the new circle but without to redrawing the previous one.
Is it possible ?

I would recommend adjusting your mental model of the drawing area; don't think of it as being "cleared" every time the draw handler is called. Rather, think of it like this: the draw handler is called every time the drawing area needs to be redrawn from scratch (among other reasons: because some other window moved in front of it, or because your program asked for a draw update). The drawing area's contents, once drawn, are not persisted anywhere.
If you need persistent window contents, then you should use a backing store and draw that onto the screen in the draw handler, or you could use a canvas library if you want to treat existing drawn objects as independently existing.

I found he answer :
#!/usr/bin/env python3
import gi
gi.require_version('Gtk','3.0')
from gi.repository import Gtk, Gdk
import cairo
import math
class MouseButtons:
LEFT_BUTTON = 1
RIGHT_BUTTON = 3
class Example(Gtk.Window):
def __init__(self):
super(Example, self).__init__()
self.init_ui()
def init_ui(self):
self.darea = Gtk.DrawingArea()
self.darea.connect("draw", self.on_draw)
self.darea.set_events(Gdk.EventMask.BUTTON_PRESS_MASK)
self.add(self.darea)
self.set_title("Fill & stroke")
self.resize(230, 150)
self.set_position(Gtk.WindowPosition.CENTER)
self.connect("delete-event", Gtk.main_quit)
self.darea.connect("button-press-event", self.on_button_press)
self.show_all()
a = self.darea.get_allocation()
print (a.x, a.y, a.width, a.height)
self.img = cairo.ImageSurface(cairo.Format.RGB24, a.width, a.height)
def on_draw(self, wid, cr):
cr.set_source_surface(self.img, 0, 0)
cr.paint()
def on_button_press(self, w, e):
if e.type == Gdk.EventType.BUTTON_PRESS \
and e.button == MouseButtons.LEFT_BUTTON:
cr = cairo.Context(self.img)
cr.set_source_rgb(0.6, 0.6, 0.6)
cr.arc(e.x, e.y, 40, 0, 2*math.pi)
cr.fill()
self.darea.queue_draw()
def main():
app = Example()
Gtk.main()
if __name__ == "__main__":
main()

Related

How to use QStackedWidget()

Am trying to use QStackedWidget() to switch to my next window but when i do that i get some errors that don't have when i run my ".py" files separately.
what my app should do is ... activate my group box with a click, then if i click the button, a new transparent window should pop-up with a mouse-cross listener, then when you click something it should stops returning the mouse cursor to normal and closing the transparent window.
window1.py
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtGui import QColor, QKeySequence, QIcon, QCursor
from window2 import *
class Ui_Form(QtWidgets.QWidget):
def __init__(self):
super(Ui_Form, self).__init__()
##if i use this method, it does not show my transparent window, also it shows an error that i dont get when run it separately.
def goToTransparentWindowMethod(self):
self.goToTransparentWindow = TransparentWindowClass()
myWindow.addWidget(self.goToTransparentWindow)
myWindow.setCurrentIndex(myWindow.currentIndex()+1)
def setupUi(self, myWindow):
myWindow.setObjectName("myWindow")
myWindow.resize(627, 327)
self.horizontalLayoutWidget = QtWidgets.QWidget(myWindow)
self.horizontalLayoutWidget.setGeometry(QtCore.QRect(10, 10, 300, 270))
self.horizontalLayoutWidget.setObjectName("horizontalLayoutWidget")
self.horizontalLayout = QtWidgets.QHBoxLayout(self.horizontalLayoutWidget)
self.horizontalLayout.setContentsMargins(0, 0, 0, 0)
self.horizontalLayout.setObjectName("horizontalLayout")
self.gpb_main = QtWidgets.QGroupBox(self.horizontalLayoutWidget)
self.gpb_main.setCheckable(True)
self.gpb_main.setChecked(False)
self.gpb_main.setObjectName("gpb_spell_main")
self.btn_main = QtWidgets.QPushButton(self.gpb_main)
self.btn_main.setGeometry(QtCore.QRect(10, 40, 88, 27))
self.btn_main.setObjectName("btn_main")
self.btn_main.clicked.connect(self.goToTransparentWindowMethod)
self.horizontalLayout.addWidget(self.gpb_main)
self.retranslateUi(myWindow)
QtCore.QMetaObject.connectSlotsByName(myWindow)
def retranslateUi(self, myWindow):
_translate = QtCore.QCoreApplication.translate
myWindow.setWindowTitle(_translate("myWindow", "Window1"))
self.gpb_main.setTitle(_translate("myWindow", "example"))
self.btn_main.setText(_translate("myWindow", "click me"))
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
myWindow = QtWidgets.QStackedWidget()
ui = Ui_Form()
ui.setupUi(myWindow)
myWindow.show()
sys.exit(app.exec_())
window2.py
from PyQt5 import QtCore, QtGui, QtWidgets
from pynput.mouse import Listener
import pyautogui
class TransparentWindowThreadClass(QtCore.QObject):
def __init__(self, parent=None):
super(TransparentWindowThreadClass, self).__init__()
#QtCore.pyqtSlot()
def on_click_main(self, x, y, button, pressed):
try:
if pressed:
self.pos_main = pyautogui.position()
self.get_rgb_main = pyautogui.pixel(self.pos_main[0], self.pos_main[1])
r = self.get_rgb_main.red
g = self.get_rgb_main.green
b = self.get_rgb_main.blue
self.get_rgb_main = r,g,b
Transparent_Window.unsetCursor()#error here when is called from window1
Transparent_Window.close()#error here when is called from window1
#Pressed
self.pressed_Msg = 'Pressed at x:{0} y:{1} RGB:{2}'.format(x, y, self.get_rgb_main)
print(self.pressed_Msg)
else:
#Released msg
self.released_Msg = 'Released at x:{0} y:{1} RGB:{2}'.format(x, y, self.get_rgb_main)
print(self.released_Msg)
if not pressed:
return False
except KeyboardInterrupt:
print('you pressed ctrl+c')
except NameError:
print("Error on_click_main")
except RuntimeError:
print('run time error')
except TypeError:
print('ype error')
except AttributeError:
print('Attribute Error')
#QtCore.pyqtSlot()
def loop_onclick_listener(self):
try:
with Listener(on_click=self.on_click_main) as listener:
listener.join()
except KeyboardInterrupt:
print('you pressed ctrl+c')
except NameError:
print("Error onclick_listener")
except RuntimeError:
print('run time error')
except TypeError:
print('ype error')
except AttributeError:
print('Attribute Error')
class TransparentWindowClass(QtWidgets.QWidget):
def __init__(self, parent=None):
super(TransparentWindowClass, self).__init__()
self.monitorResolution = pyautogui.size()
# create a QThread and start the thread that handles
self.thread = QtCore.QThread()
self.thread.start()
# create the worker without a parent so you can move it
self.worker = TransparentWindowThreadClass()## my custom thread class
# the worker is moved to another thread
self.worker.moveToThread(self.thread)
# if the thread started, connect it
self.thread.started.connect(self.worker.loop_onclick_listener)
def setupUi(self, Transparent_Window):
Transparent_Window.setObjectName("Transparent_Window")
Transparent_Window.resize(self.monitorResolution[0], self.monitorResolution[1])
Transparent_Window.setWindowFlags(QtCore.Qt.FramelessWindowHint)
Transparent_Window.setAttribute(QtCore.Qt.WA_TranslucentBackground, True)
Transparent_Window.setCursor(QtGui.QCursor(QtCore.Qt.CrossCursor))
self.retranslateUi(Transparent_Window)
QtCore.QMetaObject.connectSlotsByName(Transparent_Window)
def retranslateUi(self, Transparent_Window):
_translate = QtCore.QCoreApplication.translate
Transparent_Window.setWindowTitle(_translate("Transparent", "Transparent Window"))
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
Transparent_Window = QtWidgets.QWidget()
ui = TransparentWindowClass()
ui.setupUi(Transparent_Window)
Transparent_Window.show()
sys.exit(app.exec_())
You are making things too complex.
Also, even assuming that no other possible solution could be used, you must remember that widgets are not thread-safe, and can not be directly accessed from external threads. The only safe and correct way to communicate between threads is by using signals and slots.
That said, there is no need for pyautogui nor pynput if you just want to get the color of a pixel on the screen.
If you want to grab a pixel on the screen, you can use the QScreen function grabWindow(), using 0 as window id (which matches the whole screen), and with a single-pixel area.
Then you can use grabMouse() to ensure that you always receive mouse events, even if the mouse is outside of the widget and no mouse button was being pressed. Note that grabMouse() can only work on visible widgets, so we need to "hide" the window by moving it off-screen.
Then, overriding mousePressEvent() you can use grabWindow() with the global position of the mouse, it will return a QPixmap that can be converted to a QImage and get the pixelColor() of the grab above.
The grabbing can be canceled by pressing Esc.
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
class GrabTest(QWidget):
def __init__(self):
super().__init__()
self.resize(200, 200)
self.button = QPushButton('Grab!')
self.colorLabel = QLabel('No color', alignment=Qt.AlignCenter)
self.colorLabel.setFixedSize(120, 30)
self.colorLabel.setStyleSheet('border: 1px solid black;')
layout = QVBoxLayout(self)
layout.addStretch()
layout.addWidget(self.button, alignment=Qt.AlignCenter)
layout.addWidget(self.colorLabel, alignment=Qt.AlignCenter)
layout.addStretch()
self.button.clicked.connect(self.startGrab)
def mousePressEvent(self, event):
if QWidget.mouseGrabber() == self:
self.getPixel(event.globalPos())
else:
super().mousePressEvent(event)
def keyPressEvent(self, event):
if QWidget.mouseGrabber() == self and event.key() == Qt.Key_Escape:
self.stopGrab()
else:
super().keyPressEvent(event)
def startGrab(self):
self.grabMouse(Qt.CrossCursor)
self.oldPos = self.pos()
deskRect = QRect()
for screen in QApplication.screens():
deskRect |= screen.geometry()
# move the window off screen while keeping it visible
self.move(deskRect.bottomRight())
def stopGrab(self):
self.releaseMouse()
self.move(self.oldPos)
def getPixel(self, pos):
screen = QApplication.screens()[0]
pixmap = screen.grabWindow(0, pos.x(), pos.y(), 1, 1)
color = pixmap.toImage().pixelColor(0, 0)
if color.lightnessF() > .5:
textColor = 'black'
else:
textColor = 'white'
self.colorLabel.setStyleSheet('''
color: {};
background: {};
'''.format(textColor, color.name()))
self.colorLabel.setText(color.name())
self.stopGrab()
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
grabber = GrabTest()
grabber.show()
sys.exit(app.exec())

Undo functionality for QPainter drawEllipse() function

I'm new to PyQt and started playing around with some online code. The drawEllipse() function found in QPainter class draws an ellipse based on defined parameters. My question is once we call the function and it draws it on our scene, how do I undo this operation? There seems to be no such function that can do this in the documentation.
Example drawing code:
def draw(self, x, y):
painter = QPainter()
painter.begin(self.image)
painter.setPen(QPen(Qt.red, 5, Qt.SolidLine))
painter.drawEllipse(QPoint(y,x),10,10)
painter.end()
self.scene.addPixmap(QPixmap.fromImage(self.image))
So if a keystroke of Ctrl+Z is pressed this drawn circle should disappear, is this possible?
I do not understand why you use QImage because if you want to graph circles you should use QGraphicsEllipseItem.
To implement the undo method we must store the items in a list, then when you call the undo method you get the last element and remove it from the QGraphicsScene using removeItem().
In the following example, a circle is added each time you click.
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
class Widget(QtWidgets.QWidget):
def __init__(self, parent=None):
QtWidgets.QWidget.__init__(self, parent)
lay = QtWidgets.QVBoxLayout(self)
self.gv = QtWidgets.QGraphicsView()
lay.addWidget(self.gv)
self.scene = QtWidgets.QGraphicsScene(0, 0, 400, 400)
self.gv.setScene(self.scene)
self.gv.installEventFilter(self)
self.items = []
shortcut = QtWidgets.QShortcut(QtGui.QKeySequence("Ctrl+Z"), self)
shortcut.activated.connect(self.undo)
def eventFilter(self, obj, event):
if obj == self.gv and event.type() == QtCore.QEvent.MouseButtonPress:
p = self.gv.mapToScene(event.pos())
self.draw(p)
return QtWidgets.QWidget.eventFilter(self, obj, event)
def draw(self, p):
it = QtWidgets.QGraphicsEllipseItem(0, 0, 10, 10)
it.setPen(QtGui.QPen(QtCore.Qt.red, 5, QtCore.Qt.SolidLine))
self.scene.addItem(it)
it.setPos(p)
self.items.append(it)
def undo(self):
if self.items:
it = self.items.pop()
self.scene.removeItem(it)
del it
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
w = Widget()
w.show()
sys.exit(app.exec_())

PyQt5 - How to draw a dot on mouse click position?

I am trying to draw a dot on my main window, but the dot is not shown.
I've tried bounding mousePressEvent to paintEvent, but it didn't work as well. Here's current version of my code(which is not working too). Also I tried place a point with drawPoint method and it didn't work too.
import sys
from PyQt5 import QtWidgets, QtGui, QtCore, uic
class GUI(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
uic.loadUi('gui.ui', self)
self.setFixedSize(self.size())
self.show()
def mousePressEvent(self, e):
print(e.pos())
qp = QtGui.QPainter()
qp.begin(self)
qp.setPen(QtCore.Qt.red)
qp.drawEllipse(e.pos().x(), e.pos().y(), 10, 10)
qp.end()
self.update()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
window = GUI()
sys.exit(app.exec_())
I know that mousePressEvent is working since I get coords of the click.
I am okay to change methods of dot-placing or type of dots to place, but it should have customizable color and size.
You should only draw within the paintEvent method, and this paint does not save memory so if you want to graph several points you must store them in some container, for example using QPolygon.
paintEvent() is called every time you call update() or repaint(), for example it is called every time it is resized, the window is moved, etc.
import sys
from PyQt5 import QtWidgets, QtGui, QtCore, uic
class GUI(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
uic.loadUi('gui.ui', self)
self.setFixedSize(self.size())
self.show()
self.points = QtGui.QPolygon()
def mousePressEvent(self, e):
self.points << e.pos()
self.update()
def paintEvent(self, ev):
qp = QtGui.QPainter(self)
qp.setRenderHint(QtGui.QPainter.Antialiasing)
pen = QtGui.QPen(QtCore.Qt.red, 5)
brush = QtGui.QBrush(QtCore.Qt.red)
qp.setPen(pen)
qp.setBrush(brush)
for i in range(self.points.count()):
qp.drawEllipse(self.points.point(i), 5, 5)
# or
# qp.drawPoints(self.points)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
window = GUI()
sys.exit(app.exec_())

pyqt5: why the mimeData().text() returns nothing?

learning PyQt5 recently, I've tried to drag a QPushButton learning this tutorial Drag & drop a button widget, and made some improvements to place the button more accurate, so I add
mime = e.mimeData().text()
x, y = mime.split(',')
according to #Avaris for this question, but I found e.mimeData().text() returned nothing which supposed to be the coordinate of local position of the cursor with respect to the button, i tried to print(mime), and got a blank line with nothing, then i print(mime.split(',')) and got ['']。
here's the code:
import sys
from PyQt5.QtWidgets import QPushButton, QWidget, QApplication, QLabel
from PyQt5.QtCore import Qt, QMimeData
from PyQt5.QtGui import QDrag
from PyQt5 import QtCore
class Button(QPushButton):
def __init__(self, title, parent):
super().__init__(title, parent)
def mouseMoveEvent(self, e):
if e.buttons() != Qt.RightButton:
return
mimeData = QMimeData()
drag = QDrag(self)
drag.setMimeData(mimeData)
dropAction = drag.exec_(Qt.MoveAction)
def mousePressEvent(self, e):
QPushButton.mousePressEvent(self, e)
if e.button() == Qt.LeftButton:
print('press')
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setAcceptDrops(True)
self.button = Button('Button', self)
self.button.move(100, 65)
self.setWindowTitle('Click or Move')
self.setGeometry(300, 300, 280, 150)
def dragEnterEvent(self, e):
e.accept()
def dropEvent(self, e):
position = e.pos()
mime = e.mimeData().text()
x, y = mime.split(',')
#print(mime.split(','))
self.button.move(position - QtCore.QPoint(int(x), int(y)))
e.setDropAction(Qt.MoveAction)
e.accept()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
ex.show()
app.exec_()
In the answer of #Avaris, you will notice they set the mimedata with the button position in the mouseMoveEvent:
mimeData = QtCore.QMimeData()
# simple string with 'x,y'
mimeData.setText('%d,%d' % (e.x(), e.y()))
The mimedata does not contain anything by default. You have to set everything yourself! Have a look at the documentation for QMimeData to see what else you can do (other than setting arbitrary text)
Drag and Drop in Camera View
def dragEnterEvent(self, event): # Drag lines
mimeData = QtCore.QMimeData()
if mimeData.hasText:
event.accept()
else:
event.ignore()
def dropEvent(self, event): # Drop lines
mimeData = QtCore.QMimeData()
format = 'application/x-qabstractitemmodeldatalist'
data=event.mimeData().data(format) # Drag Drop get data's name
name_str = codecs.decode(data,'utf-8') # Convert byte to string
mimeData.setText(name_str)
# print(name_str[26:].replace('\x00','').strip("")) # remove white space
if mimeData.hasText:
print(name_str)
# write what you will do

PyQt: QGraphicsItem added at a wrong position

I have a subclass of QGraphicsItem and I want to add instances of it to the scene on 'Control+LMB click'. The trouble is that the item is added at the position with coordinates that are two times larger than they should be. At the same time adding ellipses with scene.addEllipse(...) works fine.
#!/usr/bin/env python
import sys
from PyQt4.QtCore import (QPointF, QRectF, Qt, )
from PyQt4.QtGui import (QApplication, QMainWindow, QGraphicsItem,
QGraphicsScene, QGraphicsView, QPen, QStyle)
MapSize = (512, 512)
class DraggableMark(QGraphicsItem):
def __init__(self, position, scene):
super(DraggableMark, self).__init__(None, scene)
self.setFlags(QGraphicsItem.ItemIsSelectable | QGraphicsItem.ItemIsMovable)
self.rect = QRectF(position.x(), position.y(), 15, 15)
self.setPos(position)
scene.clearSelection()
def boundingRect(self):
return self.rect
def paint(self, painter, option, widget):
pen = QPen(Qt.SolidLine)
pen.setColor(Qt.black)
pen.setWidth(1)
if option.state & QStyle.State_Selected:
pen.setColor(Qt.blue)
painter.setPen(pen)
painter.drawEllipse(self.rect)
class GraphicsScene(QGraphicsScene):
def __init__(self, parent=None):
super(GraphicsScene, self).__init__(parent)
self.setSceneRect(0, 0, *MapSize)
def mousePressEvent(self, event):
super(GraphicsScene, self).mousePressEvent(event)
if event.button() != Qt.LeftButton:
return
modifiers = QApplication.keyboardModifiers()
pos = event.scenePos()
if modifiers == Qt.ControlModifier:
print("Control + Click: (%d, %d)" % (pos.x(), pos.y()))
DraggableMark(pos, self)
self.addEllipse(QRectF(pos.x(), pos.y(), 10, 10))
else:
print("Click: (%d, %d)" % (pos.x(), pos.y()))
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.scene = GraphicsScene(self)
self.scene.addRect(QRectF(0, 0, *MapSize), Qt.red)
self.view = QGraphicsView()
self.view.setScene(self.scene)
self.view.resize(self.scene.width(), self.scene.height())
self.setCentralWidget(self.view)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
rect = QApplication.desktop().availableGeometry()
window.resize(int(rect.width()), int(rect.height()))
window.show()
app.exec_()
I see you have answered your own question. However I would like to explain why this works.
Every QGraphicsItem has its own local coordinate system. So when you do
self.rect = QRectF(position.x(), position.y(), 15, 15)
you basically start from the (0, 0) of the item's local coordinate system and go to the given x and y which you take from position. This basically means that your rectangle will be drawn at position.x() + position.x() and position.y() + position.y() with the first position.x()/position.y() being the position of the QGraphicsItem inside your scene and the second position.x()/position.y() being the position inside the local coordinate system of your item.
If you want to start from the origin of the QGraphicsItem, you have to use
self.rect = QRectF(0, 0, 15, 15)
This ensures that you start from the origin of the local coordinate system.
This issue is particularly tricky due to the fact that by default objects are added to the (0, 0) of a scene. So position.x() + position.x() and position.y() + position.y() in this case will actually not show the issue at hand since 0+0 is always equal to 0. It is the moment you change the default position to something else when the problem will occur.
Here is a 3D figure that visualizes what I'm describing above (I was unable to find a 2D example but the principle is the same :P):
The world here is the scene while the object is the QGraphicsItem residing in that scene.
Changing
self.rect = QRectF(position.x(), position.y(), 15, 15)
to
self.rect = QRectF(0, 0, 15, 15)
solved the problem

Resources