I have a custom QGraphicsItem on a QgraphicsScene. And I'm trying to draw on QGraphicsItem following mouse events. Here is what I implemented:
The custom QGraphicsItem in implemented in GraphicsItem_custom.py
from PySide2.QtWidgets import QGraphicsItem
from PySide2.QtCore import QPointF, QRectF, Qt, QRect, QPoint
from PySide2.QtGui import QPen, QPainter, QPixmap
class GraphicsItem_custom(QGraphicsItem):
def __init__(self,*args, **kwargs):
super().__init__(*args, **kwargs)
self.start, self.end = QPoint(), QPoint()
self.rectangles = []
def boundingRect(self):
return QRectF(0, 0, 500, 300)
def paint(self, painter, option, widget):
painter.setPen(QPen(Qt.red, 6, Qt.SolidLine))
painter.drawText(QPointF(0, 10), "Hiya")
painter.drawRect(self.boundingRect())
for rectangle in self.rectangles:
painter.drawRect(rectangle)
if not self.start.isNull() and not self.end.isNull() and self.start != self.end:
rect = QRect(self.start, self.end)
print("paintEvent start: " + str(self.start) + ", end: " + str(self.end))
print("")
painter.drawRect(rect.normalized())
def mousePressEvent(self, event):
super().mousePressEvent(event)
print("Pressed")
if event.buttons() and Qt.LeftButton:
self.start = event.pos()
self.end = self.start
self.update()
print("left button")
def mouseMoveEvent(self, event):
print("Move")
super().mouseMoveEvent(event)
if event.buttons() and Qt.LeftButton:
self.end = event.pos()
self.update()
def mouseReleaseEvent(self, event):
super().mouseReleaseEvent(event)
print("Released")
if event.button() and Qt.LeftButton:
if self.start != self.end:
r = QRect(self.start, self.end).normalized()
self.rectangles.append(r)
self.start = self.end = QPoint()
self.update()
Then, in main.py I create the scene and view, and add the item onto the scene:
class MyApp(QMainWindow):
def __init__(self):
super().__init__()
self.window_width, self.window_height = 1200, 800
self.setMinimumSize(self.window_width, self.window_height)
self.scene = QGraphicsScene(self)
self.view = QGraphicsView(self.scene, self)
self.view.setGeometry(0,0,1000, 700)
self.item = GraphicsItem_custom()
self.scene.addItem(self.item)
if __name__ == '__main__':
# don't auto scale when drag app to a different monitor.
# QApplication.setAttribute(Qt.HighDpiScaleFactorRoundingPolicy.PassThrough)
app = QApplication(sys.argv)
app.setStyleSheet('''
QWidget {
font-size: 30px;
}
''')
myApp = MyApp()
myApp.show()
try:
sys.exit(app.exec_())
except SystemExit:
print('Closing Window...')
When I run this code, the mousePressEvent() is triggered when I press, however, mouseReleaseEvent() is not when triggered when I release. Can anyone tell me what I did wrong?
The documentation of mousePressEvent() explains:
If you do reimplement this function, event will by default be accepted (see QEvent::accept()), and this item is then the mouse grabber. This allows the item to receive future move, release and doubleclick events.
If you call the base implementation like this:
super().mousePressEvent(event)
the result will the same as not reimplementing the function (you're calling the default behavior), so the event will not accepted, meaning that the item will not receive move, release and doubleclick events, unless you've set specific flags that trigger those event handlers due to their nature (like Qt.ItemIsMovable).
You have to carefully decide if you actually need to call the base implementation or not (usually depending on the flags), and eventually add this at some point in the mousePressEvent() override:
event.setAccepted(True)
Related
I'm developing a GUI where you can connect nodes between them except in a few special cases. This implementation works perfectly fine most of the time, but after some testing i found that, when I connect one QGraphicsPixmapItem with another through a QGraphicsLineItem, and the user opens the contextual menu before completing the link, the line get stuck, and it cannot be deleted.
The process to link two elements is to first press the element, then keep pressing while moving the line and releasing when the pointer is over the other element. This is achieved using mousePressEvent, mouseMoveEvent and mouseReleaseEvent, respectively.
This code is an example:
#!/usr/bin/env python3
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
import sys
class Ellipse(QGraphicsEllipseItem):
def __init__(self, x, y):
super(Ellipse, self).__init__(x, y, 30, 30)
self.setBrush(QBrush(Qt.darkBlue))
self.setFlag(QGraphicsItem.ItemIsMovable)
self.setZValue(100)
def contextMenuEvent(self, event):
menu = QMenu()
first_action = QAction("First action")
second_action = QAction("Second action")
menu.addAction(first_action)
menu.addAction(second_action)
action = menu.exec(event.screenPos())
class Link(QGraphicsLineItem):
def __init__(self, x, y):
super(Link, self).__init__(x, y, x, y)
self.pen_ = QPen()
self.pen_.setWidth(2)
self.pen_.setColor(Qt.red)
self.setPen(self.pen_)
def updateEndPoint(self, x2, y2):
line = self.line()
self.setLine(line.x1(), line.y1(), x2, y2)
class Scene(QGraphicsScene):
def __init__(self):
super(Scene, self).__init__()
self.link = None
self.link_original_node = None
self.addItem(Ellipse(200, 400))
self.addItem(Ellipse(400, 400))
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
item = self.itemAt(event.scenePos(), QTransform())
if item is not None:
self.link_original_node = item
offset = item.boundingRect().center()
self.link = Link(item.scenePos().x() + offset.x(), item.scenePos().y() + offset.y())
self.addItem(self.link)
def mouseMoveEvent(self, event):
super().mouseMoveEvent(event)
if self.link is not None:
self.link.updateEndPoint(event.scenePos().x(), event.scenePos().y())
def mouseReleaseEvent(self, event):
super().mouseReleaseEvent(event)
if self.link is not None:
item = self.itemAt(event.scenePos(), QTransform())
if isinstance(item, (Ellipse, Link)):
self.removeItem(self.link)
self.link_original_node = None
self.link = None
class MainWindow(QMainWindow):
def __init__(self):
super(QMainWindow, self).__init__()
self.scene = Scene()
self.canvas = QGraphicsView()
self.canvas.setScene(self.scene)
self.setCentralWidget(self.canvas)
self.setGeometry(500, 200, 1000, 600)
self.setContextMenuPolicy(Qt.NoContextMenu)
app = QApplication(sys.argv)
win = MainWindow()
win.show()
sys.exit(app.exec())
How can I get rid off the line before/after the context menu event? I tried to stop them, but I do not know how.
Assuming that the menu is only triggered from a mouse button press, the solution is to remove any existing link item in the mouseButtonPress too.
def mousePressEvent(self, event):
if self.link is not None:
self.removeItem(self.link)
self.link_original_node = None
self.link = None
# ...
Note that itemAt for very small items is not always reliable, as the item's shape() might be slightly off the mapped mouse position. Since the link would be removed in any case, just do the same in the mouseReleaseEvent():
def mouseReleaseEvent(self, event):
super().mouseReleaseEvent(event)
if self.link is not None:
item = self.itemAt(event.scenePos(), QTransform())
if isinstance(item, Ellipse):
# do what you need with the linked ellipses
# note the indentation level
self.removeItem(self.link)
self.link_original_node = None
self.link = None
I'm trying to draw a circle instead of dots
Here is the class where it paints red dots on pressing event at any place in the widget
I want to draw a circle (hollow circle) as an outline not a solid circle that hide part of my picture
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
class Canvas(QWidget):
def __init__(self, photo, *args, **kwargs):
super().__init__(*
args, **kwargs)
self.image = QImage(photo)
self.setFixedSize(self.image.width(), self.image.height())
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
qp = QPainter(self.image)
qp.setRenderHint(QPainter.Antialiasing)
qp.setPen(QPen(Qt.red, 5))
qp.setBrush(Qt.red)
qp.drawPoint(event.pos())
self.update()
def paintEvent(self, event):
qp = QPainter(self)
rect = event.rect()
qp.drawImage(rect, self.image, rect)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
w = QWidget()
self.setCentralWidget(w)
grid = QGridLayout(w)
grid.addWidget(Canvas('photo.jpeg'))
if __name__ == '__main__':
app = QApplication(sys.argv)
gui = MainWindow()
gui.show()
sys.exit(app.exec_())
I believe this is what you're trying to do.
In which case you need to take advantage of which paint device to pass to QPainter. During mousePressEvent and mouseMoveEvent use QPainter(self) so anything painted will only last until the next update. Then in mouseReleaseEvent when satisfied with the size of the circle, you can paint on the QImage with QPainter(self.image) to permanently draw the circle.
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
class Canvas(QWidget):
def __init__(self, photo, *args, **kwargs):
super().__init__(*args, **kwargs)
self.image = QImage(photo)
self.setFixedSize(self.image.width(), self.image.height())
self.pressed = self.moving = False
self.revisions = []
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
self.pressed = True
self.center = event.pos()
self.update()
def mouseMoveEvent(self, event):
if event.buttons() & Qt.LeftButton:
self.moving = True
r = (event.pos().x() - self.center.x()) ** 2 + (event.pos().y() - self.center.y()) ** 2
self.radius = r ** 0.5
self.update()
def mouseReleaseEvent(self, event):
if event.button() == Qt.LeftButton:
self.revisions.append(self.image.copy())
qp = QPainter(self.image)
self.draw_circle(qp) if self.moving else self.draw_point(qp)
self.pressed = self.moving = False
self.update()
def paintEvent(self, event):
qp = QPainter(self)
rect = event.rect()
qp.drawImage(rect, self.image, rect)
if self.moving:
self.draw_circle(qp)
elif self.pressed:
self.draw_point(qp)
def draw_point(self, qp):
qp.setPen(QPen(Qt.black, 5))
qp.drawPoint(self.center)
def draw_circle(self, qp):
qp.setRenderHint(QPainter.Antialiasing)
qp.setPen(QPen(Qt.black, 3, Qt.DashLine))
qp.drawEllipse(self.center, self.radius, self.radius)
def undo(self):
if self.revisions:
self.image = self.revisions.pop()
self.update()
def reset(self):
if self.revisions:
self.image = self.revisions[0]
self.revisions.clear()
self.update()
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
w = QWidget()
self.setCentralWidget(w)
canvas = Canvas('photo.png')
grid = QGridLayout(w)
grid.addWidget(canvas)
QShortcut(QKeySequence('Ctrl+Z'), self, canvas.undo)
QShortcut(QKeySequence('Ctrl+R'), self, canvas.reset)
if __name__ == '__main__':
app = QApplication(sys.argv)
gui = MainWindow()
gui.show()
sys.exit(app.exec_())
I am trying to open another wx frame from a main frame UI. I followed the example shown here: https://wiki.wxpython.org/CallAfter but my main UI is still blocked.
Here is the event listener on the main UI:
def testShowGUI(self):
# This process is a long one
# It uses the vtk to read point cloud file and reconstruct the surface
file = "cache/d9c5e8ef-7b7f-485e-8fc8-23098c32afcb.ply"
reader = vtk.vtkPLYReader()
reader.SetFileName(file)
reader.Update()
delaunay = vtk.vtkDelaunay2D()
delaunay.SetAlpha(0.1)
delaunay.SetTolerance(0.0001)
delaunay.SetOffset(1.25)
delaunay.BoundingTriangulationOff()
delaunay.SetInputData(reader.GetOutput())
delaunay.Update()
#Once finish reading and processing the point cloud, pass to the next function for rendering
wx.CallAfter(self.AfterProcess, delaunay)
def AfterProcess(self, data):
meshVisGui = MesVisGUI.MeshVisGui(data)
meshVisGui.Show()
def OnEnter(self, event):
#Event listener when user click on Enter button
my_thread = threading.Thread(target=self.testShowGUI)
my_thread.start()
The code for the separate frame is as below:
class MeshVisGui(wx.Frame):
SPACING = 4
def __init__(self, delaunay, parent=None):
self.delaunayData = delaunay
self.title = "Mesh Visualization"
wx.Frame.__init__(self, None, -1, self.title)
self.Initialize()
def Initialize(self):
self.panel = wx.Panel(self, -1, size=(600, 400), style=wx.BORDER_RAISED)
self.widget3d = wxVTKRenderWindowInteractor(self.panel, -1)
self.widget3d.Enable()
self.render = vtk.vtkRenderer()
self.render.SetBackground(params.BackgroundColor)
self.widget3d.GetRenderWindow().AddRenderer(self.render)
self.interactor = self.widget3d.GetRenderWindow().GetInteractor()
self.interactor.GetInteractorStyle().SetCurrentStyleToTrackballCamera()
self.axesWidget = utils.createAxes(self.interactor)
self.meshActor = utils.build_actor(self.delaunayData)
self.render.AddActor(self.meshActor)
self.render.ResetCamera()
box = wx.BoxSizer(wx.VERTICAL)
box.Add(self.widget3d, 1, wx.EXPAND, self.SPACING)
self.panel.SetSizer(box)
self.Layout()
However, on my main UI, it is still showing a spinning icon and block the UI while it is trying to process the point cloud data. Can someone help me spot what I have done wrong?
Wxpython version: 4.0.1
Python version: 3.6.5
Following on from Mike Driscoll's answer and comment, here is a sample of a threaded task running from another panel.
The thread reports back to the 2nd panel (it's parent) using an event. This allows a progress bar to be updated.
The 2nd panel includes a "Cancel" option for the threaded task, whilst the main frame has a button to test if it is not frozen.
The use of wx.GetApp().Yield() may be slightly old fashioned but I have always found it to be reliable.
import time
import wx
from threading import Thread
import wx.lib.newevent
progress_event, EVT_PROGRESS_EVENT = wx.lib.newevent.NewEvent()
class ThreadFrame(wx.Frame):
def __init__(self, title, parent=None):
wx.Frame.__init__(self, parent=parent, title=title)
panel = wx.Panel(self)
self.btn = wx.Button(panel,label='Stop Long running process', size=(200,30), pos=(10,10))
self.btn.Bind(wx.EVT_BUTTON, self.OnExit)
self.progress = wx.Gauge(panel,size=(300,10), pos=(10,50), range=300)
#Bind to the progress event issued by the thread
self.Bind(EVT_PROGRESS_EVENT, self.OnProgress)
#Bind to Exit on frame close
self.Bind(wx.EVT_CLOSE, self.OnExit)
self.Show()
self.mythread = TestThread(self)
#Enable the GUI to be responsive by briefly returning control to the main App
while self.mythread.isAlive():
time.sleep(0.1)
wx.GetApp().Yield()
continue
try:
self.OnExit(None)
except:
pass
def OnProgress(self, event):
self.progress.SetValue(event.count)
#or for indeterminate progress
#self.progress.Pulse()
def OnExit(self, event):
if self.mythread.isAlive():
self.mythread.terminate() # Shutdown the thread
self.mythread.join() # Wait for it to finish
self.Destroy()
class TestThread(Thread):
def __init__(self,parent_target):
Thread.__init__(self)
self.parent = parent_target
self.stopthread = False
self.time = time.time()
self.start() # start the thread
def run(self):
# A loop that will run for 5 minutes then terminate
while self.stopthread == False:
curr_loop = int(time.time() - self.time)
if curr_loop < 300:
time.sleep(1)
evt = progress_event(count=curr_loop)
#Send back current count for the progress bar
try:
wx.PostEvent(self.parent, evt)
except: # The parent frame has probably been destroyed
self.terminate()
else:
self.terminate()
def terminate(self):
self.stopthread = True
class MyPanel(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
self.text_count = 0
self.parent=parent
btn = wx.Button(self, wx.ID_ANY, label='Start Long running process', size=(200,30), pos=(10,10))
btn.Bind(wx.EVT_BUTTON, self.Thread_Frame)
btn2 = wx.Button(self, wx.ID_ANY, label='Is the GUI still active?', size=(200,30), pos=(10,50))
btn2.Bind(wx.EVT_BUTTON, self.AddText)
self.txt = wx.TextCtrl(self, wx.ID_ANY, style= wx.TE_MULTILINE, pos=(10,90),size=(400,100))
def Thread_Frame(self, event):
frame = ThreadFrame(title='Threaded Task', parent=self.parent)
def AddText(self,event):
self.text_count += 1
txt = self.txt.GetValue()
txt += "More text " + str(self.text_count)+"\n"
self.txt.SetValue(txt)
class MainFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, title='Main Frame', size=(600,400))
panel = MyPanel(self)
self.Show()
if __name__ == '__main__':
app = wx.App(False)
frame = MainFrame()
app.MainLoop()
You do not need threads to open new frames / windows in your wxPython application. You just need to create a sub-class of wx.Frame to hold the code of your other frame. Then from your main application's frame, you can instantiate the other frame and show it. You use the same concept when you create a wx.Dialog or a wx.MessageDialog.
Here is a simple example:
import wx
class OtherFrame(wx.Frame):
"""
Class used for creating frames other than the main one
"""
def __init__(self, title, parent=None):
wx.Frame.__init__(self, parent=parent, title=title)
self.Show()
class MyPanel(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
btn = wx.Button(self, label='Create New Frame')
btn.Bind(wx.EVT_BUTTON, self.on_new_frame)
def on_new_frame(self, event):
frame = OtherFrame(title='SubFrame',
parent=wx.GetTopLevelParent(self))
class MainFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, title='Main Frame')
panel = MyPanel(self)
self.Show()
if __name__ == '__main__':
app = wx.App(False)
frame = MainFrame()
app.MainLoop()
In this example, I set the other frame's parent to the MainFrame instance by using wx.GetTopLevelParent(self). The benefit of setting a parent for the sub-frames is that if I close the main frame, it will cause the other frames to also be closed.
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_())
from PyQt4 import QtGui, QtCore
class Window(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self)
self.view = View(self)
self.button = QtGui.QPushButton('Clear View', self)
self.button.clicked.connect(self.handleClearView)
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.view)
layout.addWidget(self.button)
def handleClearView(self):
self.view.scene().clear()
class DragButton(QtGui.QPushButton):
def mousePressEvent(self, event):
self.__mousePressPos = None
self.__mouseMovePos = None
if event.button() == QtCore.Qt.LeftButton:
self.__mousePressPos = event.globalPos()
self.__mouseMovePos = event.globalPos()
#super(DragButton, self).mousePressEvent(event)
def mouseMoveEvent(self, event):
if event.buttons() == QtCore.Qt.LeftButton:
# adjust offset from clicked point to origin of widget
currPos = self.mapToGlobal(self.pos())
globalPos = event.globalPos()
diff = globalPos - self.__mouseMovePos
newPos = self.mapFromGlobal(currPos + diff)
self.move(newPos)
self.__mouseMovePos = globalPos
#super(DragButton, self).mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
if self.__mousePressPos is not None:
moved = event.globalPos() - self.__mousePressPos
if moved.manhattanLength() > 3:
event.ignore()
return
#super(DragButton, self).mouseReleaseEvent(event)
class View(QtGui.QGraphicsView):
def __init__(self, parent):
QtGui.QGraphicsView.__init__(self, parent)
self.setScene(QtGui.QGraphicsScene(self))
self.setSceneRect(QtCore.QRectF(self.viewport().rect()))
btn1=DragButton('Test1', self)
btn2=DragButton('Test2', self)
def mousePressEvent(self, event):
self._start = event.pos()
def mouseReleaseEvent(self, event):
start = QtCore.QPointF(self.mapToScene(self._start))
end = QtCore.QPointF(self.mapToScene(event.pos()))
self.scene().addItem(
QtGui.QGraphicsLineItem(QtCore.QLineF(start, end)))
for point in (start, end):
text = self.scene().addSimpleText(
'(%d, %d)' % (point.x(), point.y()))
text.setBrush(QtCore.Qt.red)
text.setPos(point)
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = Window()
window.resize(640, 480)
window.show()
sys.exit(app.exec_())
Here is my code. There are two movable buttons on the QGraphicsView and I can draw line on the QGraphicsView with mouse dragging. But what I want to do is to draw line between two buttons. For detail, If I right click the btn1(Test1) and then right click the btn2(Test2) , the line would be created between two buttons. I'm struggling this problem for a month. Plz Help!
I am assuming that the line you need to draw between the buttons must also be movable. If not it is just simple you can just use :
lines = QtGui.QPainter()
lines.setPen(self)
lines.drawLine(x1,y1,x2,y2)
So, if the line needs to be movable along with the buttons then first you create a mini widget consisting of Two Buttons and the Line, so you can move the whole widget. This might help!, in that case.