How can I delete this QGraphicsLineItem when context menu is open in a QGraphicsPixmapItem? - python-3.x

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

Related

PySide2: "MouseReleaseEvernt" not triggered in QGraphicsItem

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)

How to add scrollable regions with Drag & drop widgets with PyQt?

Please add a scrollable region to this code so that when I run the code, I can scroll down through the window and see the last items in the window and replace them.
As this is a drag and drop code I could not add scroll to it.
Moreover, during the drag and drop process, the scroll should work and does not interrupt the drag and drop process.
from PyQt5.QtWidgets import QApplication, QScrollArea, QHBoxLayout, QWidget, QLabel, QMainWindow, QVBoxLayout
from PyQt5.QtCore import Qt, QMimeData, pyqtSignal
from PyQt5.QtGui import QDrag, QPixmap
class DragItem(QLabel):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setContentsMargins(25, 5, 25, 5)
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.setStyleSheet("border: 1px solid black;")
# Store data separately from display label, but use label for default.
self.data = self.text()
def set_data(self, data):
self.data = data
def mouseMoveEvent(self, e):
if e.buttons() == Qt.LeftButton:
drag = QDrag(self)
mime = QMimeData()
drag.setMimeData(mime)
pixmap = QPixmap(self.size())
self.render(pixmap)
drag.setPixmap(pixmap)
drag.exec_(Qt.MoveAction)
class DragWidget(QWidget):
"""
Generic list sorting handler.
"""
orderChanged = pyqtSignal(list)
def __init__(self, *args, orientation=Qt.Orientation.Horizontal, **kwargs):
super().__init__()
self.setAcceptDrops(True)
# Store the orientation for drag checks later.
self.orientation = orientation
if self.orientation == Qt.Orientation.Vertical:
self.blayout = QVBoxLayout()
else:
self.blayout = QHBoxLayout()
self.setLayout(self.blayout)
def dragEnterEvent(self, e):
e.accept()
def dropEvent(self, e):
pos = e.pos()
widget = e.source()
for n in range(self.blayout.count()):
# Get the widget at each index in turn.
w = self.blayout.itemAt(n).widget()
if self.orientation == Qt.Orientation.Vertical:
# Drag drop vertically.
drop_here = pos.y() < w.y() + w.size().height() // 2
else:
# Drag drop horizontally.
drop_here = pos.x() < w.x() + w.size().width() // 2
if drop_here:
# We didn't drag past this widget.
# insert to the left of it.
self.blayout.insertWidget(n-1, widget)
self.orderChanged.emit(self.get_item_data())
break
e.accept()
def add_item(self, item):
self.blayout.addWidget(item)
def get_item_data(self):
data = []
for n in range(self.blayout.count()):
# Get the widget at each index in turn.
w = self.blayout.itemAt(n).widget()
data.append(w.data)
return data
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.drag = DragWidget(orientation=Qt.Orientation.Vertical)
self.scroll = QScrollArea() ###########
self.blayout = QHBoxLayout()
self.widget = QWidget()
for n, l in enumerate(['Art', 'Boo', 'EOA', 'Hel', \
'Hyg', 'Lei', 'Lei','Lei','Lei','Lei','Lei','Lei','Lei','Lei','Lei','Lei','Lei', 'Med',\
'Nut','Nut','Nut','Rel','Rel','Rel','Sle','SLN','Spo','Spo','Spo','Spo','Spo','Thi','Thi',\
'Wor','Wor','Wor','Wor','Wor','Wor','Wor','Wor','Wor','Wor','Wor','Wor','Wor','Wor']):
item = DragItem(l)
item.set_data(n) # Store the data.
self.drag.add_item(item)
def myFun():
print('hi', self.drag.get_item_data())
# Print out the changed order.
# self.drag.orderChanged.connect(print)
# add by me
self.drag.orderChanged.connect(myFun)
#Scroll Area Properties
self.scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
self.scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.scroll.setWidgetResizable(True)
self.scroll.setWidget(self.widget)
container = QWidget()
layout = QVBoxLayout()
layout.addStretch(1)
layout.addWidget(self.drag)
layout.addStretch(1)
container.setLayout(layout)
self.setCentralWidget(container)
# self.setCentralWidget(self.scroll)
app = QApplication([])
w = MainWindow()
w.show()
app.exec_()

How to make tkinter toggle button class by deriving from tkinter.Button?

I am trying to make a toggle button class by deriving from the tkinter.Button object. To that end, I am using this StackOverflow answer and these code examples.
The problem is that I get my desired toggle behavior from the button only after I click it twice; the first two clicks, it does not enact the self.config(relief="sunken"). I tried using the command keyword argument sample from this answer and that works from the start.
import tkinter as tk
class ToggleButton(tk.Button):
def __init__(self, parent=None, toggle_text="Toggled", toggle_bg_color="green", **kwargs):
tk.Button.__init__(self, parent, **kwargs)
self.toggled = False
self.default_bg_color = self['bg']
self.default_text = self["text"]
self.toggle_bg_color = toggle_bg_color
self.toggle_text = toggle_text
self.bind("<Button-1>", self.toggle, add="+")
def toggle(self, *args):
if self["relief"] == "sunken":
self["bg"] = self.default_bg_color
self["text"] = self.default_text
self.config(relief="raised")
# self["relief"] = "raised"
self.toggled = False
else:
self["bg"] = self.toggle_bg_color
self["text"] = self.toggle_text
# self["relief"] = "sunken"
self.config(relief="sunken")
self.toggled = True
def button_placeholder():
print("TO BE IMPLEMENTED")
root = tk.Tk()
button = ToggleButton(parent=root,
toggle_text="ON", toggle_bg_color="green",
text="OFF", command=button_placeholder)
button.pack()
root.mainloop()
Here are screenshots of the behavior of the buttons after numerous clicks
After the first two clicks on the button, the expected behavior occurs. However, if the user focuses on another window (for instance by minimizing the tkinter window) and then back, again the first two clicks do not cause the desired behavior.
Can some explain this? If not, can someone provide a solution where I can have consistent behavior on toggling my button?
Information about my system
Windows 10; 64 bit
Python 3.7.3 (64 bit)
Tkinter 8.6
The problem you seem to have is that the bg parameter is not defined when you first create the button; it only gets a value assigned upon the first button press.
Then, the logic to toggle is hard to follow: you have a self.toggled boolean, yet you are testing if the button is sunken or not to differentiate between states...
I reorganized the logic to make it easier to follow; after all, toggle is a binary change from one state to another. I therefore placed the definition of the ON and OFF states in the body of the class (into two class dictionaries), and the code swaps the two configs upon toggling.
On Windows:
import tkinter as tk
class ToggleButton(tk.Button):
ON_config = {'bg': 'green',
'text': 'button is ON',
'relief': 'sunken',
}
OFF_config = {'bg': 'white',
'text': 'button is OFF',
'relief': 'raised',
}
def __init__(self, parent, *args, **kwargs):
super().__init__(parent, *args, **kwargs)
self.toggled = False
self.config = self.OFF_config
self.config_button()
self.bind("<Button-1>", self.toggle)
def toggle(self, *args):
if self.toggled: # True = ON --> toggle to OFF
self.config = self.OFF_config
else:
self.config = self.ON_config
self.toggled = not self.toggled
return self.config_button()
def config_button(self):
self['bg'] = self.config['bg']
self['text'] = self.config['text']
self['relief'] = self.config['relief']
return "break"
def __str__(self):
return f"{self['text']}, {self['bg']}, {self['relief']}"
def button_placeholder():
print('toggling now!')
if __name__ == '__main__':
root = tk.Tk()
button = ToggleButton(root)
button.pack()
root.mainloop()
On OSX:
Where the buttons aspect is fixed, using a tk.Label can mimic the desired behavior:
import tkinter as tk
class ToggleButtonLBL(tk.Label):
ON_config = {'bg': 'green',
'text': 'button is ON',
'relief': 'sunken',
}
OFF_config = {'bg': 'white',
'text': 'button is OFF',
'relief': 'raised',
}
def __init__(self, parent, *args, command=None, **kwargs):
super().__init__(parent, *args, **kwargs)
self.toggled = False
self.config = self.OFF_config
self.config_button()
self.bind("<Button-1>", self._toggle_helper)
self.bind("<ButtonRelease-1>", self._toggle)
self.command = command
def _toggle_helper(self, *args):
return 'break'
def _toggle(self, dummy_event):
self.toggle()
self.cmd()
def toggle(self, *args):
if self.toggled: # True = ON --> toggle to OFF
self.config = self.OFF_config
else:
self.config = self.ON_config
self.toggled = not self.toggled
self.config_button()
return 'break'
def config_button(self):
self['bg'] = self.config['bg']
self['text'] = self.config['text']
self['relief'] = self.config['relief']
return "break"
def __str__(self):
return f"{self['text']}, {self['bg']}, {self['relief']}"
def cmd(self):
self.command()
def button_placeholder():
print('toggling now!')
if __name__ == '__main__':
root = tk.Tk()
button = ToggleButtonLBL(root, command=button_placeholder)
button.pack()
root.mainloop()

PYQT how to draw line between two buttons

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.

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

Resources