Adding tools to Scribble app with PYQT4 - python

I am trying to write an app which works as MS Paint or other simple graphics editor. My goal is to provide such funcionality as: different tools( drawing lines, figures etc.), posibility to change colors, width, size and undo/redo function. I thought that the easiest way to do it was to create separete classes for drawing and viewing objects. Unfortunetly, it is not working properly. Here`s my code:
import sys
from PyQt4 import QtGui, QtCore
class Example(QtGui.QWidget):
def __init__(self):
super(Example, self).__init__()
self.update()
self.view = View(self)
self.action = Action(self.view.scene)
self.UI()
def UI(self):
self.setGeometry(300,300,300,300)
self.setWindowTitle('Pen styles')
self.undo = QtGui.QPushButton("undo", self)
self.undo.clicked.connect(self.action.undoStack.undo)
self.redo = QtGui.QPushButton("redo", self)
self.redo.clicked.connect(self.action.undoStack.redo)
self.redo.move(0,50)
self.tool = QtGui.QPushButton("tool", self)
self.tool.setCheckable(True)
self.tool.clicked[bool].connect(self.new)
self.tool.move(0,100)
layout = QtGui.QVBoxLayout()
layout.addWidget(self.view)
self.setLayout(layout)
self.show()
def new(self):
pass
class Action(QtGui.QGraphicsScene):
def __init__(self, scene):
QtGui.QGraphicsScene.__init__(self)
self.undoStack = QtGui.QUndoStack(self)
self.scene = scene
self.scene.addLine(0,0,150,150)
self.undoStack = QtGui.QUndoStack(self)
def mousePressEvent(self, event):
if event.button() == QtCore.Qt.LeftButton:
self.start = event.pos()
self.scene.addLine(0,0,150,250)
def mouseReleaseEvent(self, event):
start = QtCore.QPointF(self.mapToScene(self.start))
end = QtCore.QPointF(self.mapToScene(event.pos()))
self.drawing(start, end)
def drawing(self, start, end):
self.line = QtGui.QGraphicsLineItem(QtCore.QLineF(start, end))
self.line.setPen(QtGui.QPen(QtCore.Qt.blue,3,
QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin))
self.scene.addLine(0,0,150,300)
command = Draw(self.line, self.scene)
self.undoStack.push(command)
class Draw(QtGui.QUndoCommand):
def __init__(self, line, scene):
QtGui.QUndoCommand.__init__(self)
self.line = line
self.scene = scene
def redo(self):
self.scene.addItem(self.line)
def undo(self):
self.scene.removeItem(self.line)
class View(QtGui.QGraphicsView):
def __init__(self, parent):
QtGui.QGraphicsView.__init__(self, parent)
self.scene = QtGui.QGraphicsScene()
self.action = Action(self.scene)
self.setScene(self.scene)
self.setSceneRect(QtCore.QRectF(self.viewport().rect()))
def main():
app = QtGui.QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
My problem is that i cannot draw anything using my mouse. If class Action and View are combined everything is working fine, but when I put them away nothing happens(no error as well). The reason I did this separation is that I simple want to add another classes with other functionality (drawing elipses, rects...) and swap them with Action class. I added line to the scene (in Action init) and it`s being painted corectly, but the MouseEvents dont work at all.The button "tool" was made for changing the drawing tool.
This way, in my opinion, is the best way to achive ability to scribble with different tools on the same canvas(scene). My way aint good so I am asking for help. What can I do fix this code, so it works as I want? Where are the mistakes? or maybe the whole approach is wrong and it should be done another way? I use PYQT 4 and Python 3.4 .

Your current code has two instances of a QGraphicsScene created inside View.__init__. One is a standard QGraphicsScene and the other is your subclass Action. But your view can only be attached to one scene, so one of these is redundant and not functioning correctly. You have attached it to the QGraphicsScene, so the Action object is the one not working.
Instead, you should kill off the plain boring QGraphicsScene, and only instantiate the Action class. Use the instantiated Action object in the call to view.setScene(). Similarly, in the Action class, there is no need to pass in another scene. Just use itself (so replace all instances of self.scene with self in the Action class)

Related

PyQt6: How can I fetch position of mouse pointer from QGraphicsScene and use the variables in the other class?

I'm new to pyqt6 and even to python. As the title indicates, I got stuck with handling mouse position variables. I was expecting to show coodinates of mouse pointer in the QLabel correspond to the mouse movement. I was able to fetch coordinates from mouseMoveEvent in the QGraphicsScene class to getPosition in the Window class. But, that coodinates couldn't be passed to the other function in Window class.
Here is my code so far.
import sys
from PyQt6 import QtWidgets
from PyQt6.QtWidgets import QVBoxLayout, QWidget, QLabel
class GraphicsScene(QtWidgets.QGraphicsScene):
def __init__(self, parent=None):
super().__init__(parent)
def mouseMoveEvent(self, event):
self.posX = event.scenePos().x()
Window.getPosition(Window, event.scenePos().x())
class GraphicsView(QtWidgets.QGraphicsView):
def __init__(self, parent=None):
super(GraphicsView, self).__init__(parent)
self.setMouseTracking(True)
scene = GraphicsScene(self)
self.setScene(scene)
class Window(QWidget):
def __init__(self):
super().__init__()
self.Layout = QVBoxLayout()
self.gw = GraphicsView() # an image is meant to be set here.
self.Layout.addWidget(self.gw)
self.label = QLabel("Coordinate: x") # wanna show coorinates here correspond to the mouse movement.
self.Layout.addWidget(self.label)
self.setLayout(self.Layout)
def getPosition(self, posX):
self.label.setText("Coordinate: x" + str(self.posX))
self.repaint()
print(posX)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
w = Window()
w.show()
sys.exit(app.exec())
and got an ERROR like this:
AttributeError: type object 'Window' has no attribute 'label'
It seemas the self in the getPosition function is set to GraphicsScene class after being called (unexpectedly for me). And I have no idea this approach works or not after reading several web documents and asking help for chatGPT. Possibly, I took wrong approach to layout of Widgets.
Any suggestion would be helpful because I'm stuck whole this week with dipression.
Thanks.
Using class Window as parameter to setPosition was wrong, one needs to use an instance of this class. I did this by climbing up the parent() methods and there may be prettier ways to achieve the same. However, it works for now and I did not want to throw in too many changes.
I marked notable changes with comments.
#!/usr/bin/env python3
import sys
from PyQt6 import QtWidgets
class GraphicsScene(QtWidgets.QGraphicsScene):
def __init__(self, parent=None):
super().__init__(parent)
def mouseMoveEvent(self, event):
self.posX = event.scenePos().x()
self.parent().parent().setPosition(event.scenePos().x()) # <-- crawl up the ancestry
class GraphicsView(QtWidgets.QGraphicsView):
def __init__(self, parent=None):
super(GraphicsView, self).__init__(parent)
self.setMouseTracking(True)
scene = GraphicsScene(self)
self.setScene(scene)
class Window(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.Layout = QtWidgets.QVBoxLayout()
self.gw = GraphicsView(self) # <-- pass self here
self.Layout.addWidget(self.gw)
self.label = QtWidgets.QLabel("Coordinate: x") # wanna show coorinates here correspond to the mouse movement.
self.Layout.addWidget(self.label)
self.setLayout(self.Layout)
def setPosition(self, posX): # <-- this is a setter, not a getter
self.label.setText("Coordinate: x" + str(posX)) # <-- use argument posX
self.repaint()
print(posX)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
w = Window()
w.show()
sys.exit(app.exec())

Removing QLineEdit proxy widget from QGraphicsScene on returnPressed

What I would like to do is have a QLineEdit appear in my QGraphicsView when a QGraphicsTextItem is added to the scene. The line edit is only being used to set the text of the QGraphicsItem when double-clicked and needs to be removed when the return key is pressed.
I'm having trouble getting the QLineEdit to be deleted. I've tried simply deleting it when the return key is pressed but it is still not removed. Here is the code to reproduce this behaviour:
class Text(QGraphicsTextItem):
def __init__(self, text, position=QPointF(0,0), parent=None, scene=None):
super().__init__(text, parent=parent, scene=scene)
self.parent = parent
self.setFlag(QGraphicsItem.ItemIsSelectable, True)
self.height = self.document().size().height()
self.width = self.document().size().width()
self.text_center = QPointF(-self.width/2, -self.height/2)
if parent:
self.parent_center = self.parent.boundingRect().center()
self.scene = self.parent.scene
self.setPos(text_center)
else:
self.setFlag(QGraphicsItem.ItemIsMovable)
self.scene = scene
self.setPos(position - self.text_center)
def mouseDoubleClickEvent(self, mouseEvent):
self.Editing = True
self.nameEdit = NameEditor(self)
self.nameEditProxy = self.scene.addWidget(self.nameEdit)
self.nameEditProxy.setPos(self.mapToScene(QPointF(0, 0)))
class NameEditor(QLineEdit):
def __init__(self, textItem):
super().__init__(textItem.toPlainText())
self.setMaximumWidth(200)
self.setFixedWidth(200)
self.selectAll()
self.grabKeyboard()
self.textItem = textItem
def returnPressed(self):
self.textItem.setPlainText(self.text())
del self
if __name__ == "__main__":
app = QApplication(sys.argv)
view = QGraphicsView()
scene = QGraphicsScene()
scene.setSceneRect(0, 0, 500, 500)
view.setScene(scene)
text = Text("Example", position=QPointF(250, 250), scene=scene)
view.show()
sys.exit(app.exec_())
I am deleting the subclassed QLineEdit via del self in the returnPressed method in this attempt. I've also tried deleting the QGraphicsProxyWidget that contains it via del self.nameEditProxy in the mouseDoubleClick method of the Text class.
My question is how can I delete the QLineEdit on returnPressed?
In Qt, a simple method of removing widgets from the screen is to hide them, that is if you are not worried by that; perhaps you just want to get rid of it, otherwise use
myWidget.setVisible(False)
or
myWidget.hide();
You can reuse that widget by calling
myWidget.setVisible(True)
or
myWidget.show()
and then repositioning it anywhere you want.

How to add a second pixmap to an abstract button when pushed in PySide/PyQt

I'm very new to PySide/PyQt environment. I'm trying to make a menu of buttons on top and assign a task to each so that when they are clicked a function draws a painting on the central window. But I also want to make the button change when they are clicked.
I think this might be an straighforward problem to solve if I use QPushButton, but my buttons are images and I'm using the method suggested HERE and use QAbstractButton to create them.
It is mentioned there that
You can add second pixmap and draw it only when the mouse pointer is
hover over button.
And I'm trying to do exactly that. My question is this:
what are possible ways to achieve this? Are the same methods in QPushButtons applicable here? If so, are there any examples of it somewhere?
Here is a snippet of my code:
import sys
from PySide import QtGui, QtCore
BACKGROUND_COLOR = '#808080'
ICON_PATH_ACTIVE = 'icons/activ'
ICON_PATH_PASSIVE = 'icons/pasiv'
class MainWindow(QtGui.QMainWindow):
def __init__(self, app=None):
super(MainWindow, self).__init__()
self.initUI()
def initUI(self):
dockwidget = QtGui.QWidget()
self.setGeometry(200, 200, 400, 300)
hbox = QtGui.QHBoxLayout()
1_button = PicButton(QtGui.QPixmap("icons/pasiv/1.png"))
2_button = PicButton(QtGui.QPixmap("icons/pasiv/2.png"))
3_button = PicButton(QtGui.QPixmap("icons/pasiv/3.png"))
hbox.addWidget(1_button)
hbox.addWidget(2_button)
hbox.addWidget(3_button)
vbox = QtGui.QVBoxLayout()
vbox.addLayout(hbox)
vbox.setAlignment(hbox, QtCore.Qt.AlignTop)
dockwidget.setLayout(vbox)
self.setCentralWidget(dockwidget)
class PicButton(QtGui.QAbstractButton):
def __init__(self, pixmap, parent=None):
super(PicButton, self).__init__(parent)
self.pixmap = pixmap
self.setFixedSize(100, 100)
def paintEvent(self, event):
painter = QtGui.QPainter(self)
painter.drawPixmap(event.rect(), self.pixmap)
def main():
app = QtGui.QApplication(sys.argv)
central = MainWindow()
central.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Thank you.
Use a regular QPushButton with an icon.
iplay = QtGui.QIcon("path/play_icon.png")
ipause = QtGui.QIcon("path/pause_icon.png")
btn = QtGui.QPushButton(ipause, "", None)
def toggle_play():
if btn.icon() == ipause:
btn.setIcon(iplay)
# Do Pause Action
else:
btn.setIcon(ipause)
# Do Play Action
btn.clicked.connect(toggle_play)
btn.show()
If you want hover functionality then you will have to subclass the QPushButton
class MyButton(QtGui.QPushButton):
custom_click_signal = QtCore.Signal()
def enterEvent(self, event):
super().enterEvent(event)
# Change icon hove image here
def leaveEvent(self, event):
super().leaveEvent(event)
# Change icon back to original image here.
def mousePressEvent(self, event):
super().mousePressEvent(event)
self.custom_click_signal.emit()
# connect to signal btn.custom_click_signal.connect(method)
Icons are probably the easiest way instead of manually managing the paint event. There are also mousePressEvent and mouseReleaseEvents if you want the icon to change for someone holding the button down.

Can I override the inputs to the __lt__ comparator of a QListWidgetItem? (PyQt)

I am setting the widgets inside a QListWidget to my own ProductItemWidget class to be able to have more customization over the items in the list. This works fine, but I am unable to get the list to sort automatically. I call
my_list.setSortingEnabled(True)
and then I try to overwrite the "<" comparator of the ProductListItem, which I created specifically to be able to overwrite this function. In the comparator, I try to access the widget corresponding to the two items that are compared and call their getText() functions. The issue here is, the __ lt __ function by default casts the second argument to QListWidgetItem, so in my code I get ProductListItem for self, but I get QListWidgetItem for otherItem. The otherItem does not let me access the ProductItemWidget that it corresponds to because the only way to access it is by passing in the ProductListItem to the QListWidget's itemWidget() call. Here is my code:
class ProductListItem(QListWidgetItem):
def __init__(self, parent=None):
super(ProductListItem, self).__init__(parent)
def __lt__(self, otherItem):
this_item_widget = self.listWidget().itemWidget(self)
other_item_widget = otherItem.listWidget().itemWidget(otherItem)
return this_item_widget.getText() < other_item_widget.getText()
class ProductItemWidget(QWidget):
def __init__(self, product_name, parent=None):
super(ProductItemWidget, self).__init__(parent)
self.label = QLabel(product_name)
... setup code ...
def getText(self):
return self.label.text()
Is there any way to prevent the call to __ lt __ from casting the otherItem to QListWidgetItem?
I've been stuck on this problem for a while so any tips are appreciated. I am willing to change my entire approach.
QListWidget is one of the "convenience" classes (like QTreeWidget and QTableWidget). Using them is fine as long as your requirements are quite simple. But as soon as you want something a little more sophisticated, the inflexibility soon begins to show.
You can solve your problems fairly easily by switching to the more generic QListView class with a a QStandardItemModel. This requires a little more work to set up, but it will immediately bring a lot more flexibility.
Here's a demo of that approach based on your sample code:
from PyQt4 import QtGui
class ProductListItem(QtGui.QStandardItem):
def __lt__(self, other):
listview = self.model().parent()
this_widget = listview.indexWidget(self.index())
other_widget = listview.indexWidget(other.index())
return this_widget.getText() < other_widget.getText()
class ProductItemWidget(QtGui.QWidget):
def __init__(self, product_name, parent=None):
super(ProductItemWidget, self).__init__(parent)
self.label = QtGui.QLabel(product_name, self)
layout = QtGui.QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(self.label)
def getText(self):
return self.label.text()
class Window(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self)
self.list = QtGui.QListView(self)
layout = QtGui.QHBoxLayout(self)
layout.addWidget(self.list)
# model must have the listview as parent
model = QtGui.QStandardItemModel(self.list)
self.list.setModel(model)
for key in 'MHFCLNIBJDAEGK':
item = ProductListItem()
model.appendRow(item)
widget = ProductItemWidget('Item %s' % key, self.list)
self.list.setIndexWidget(item.index(), widget)
model.sort(0)
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = Window()
window.setGeometry(500, 300, 150, 300)
window.show()
sys.exit(app.exec_())

PyQt: QGraphicsScene events are not propagated down to GraphicItems

I have a GraphicScene inside a QGraphicView populated with many rectangles (items). I would like each rectangle to respond to the mouse click but I'm not able to find the hook to attach the event handler to the correct object and to get the event propagated to it.
I attached an event handler to the scene:
scene.event = myfunction
and it worked (it was firing every event) but I was unable to attach the same function to one of its children. Can you give me an insight on where to search for such an entry point?
So - I'm not really sure what you're doing there, but I can't think of anything in PyQt where you should be mapping a custom function directly to a scene's event method.
Do you have an actual example?
If you're doing:
scene.mousePressEvent = my_mouse_function
Then that is not how you want to do that.
You can look into using an event filters (http://doc.qt.nokia.com/4.7-snapshot/eventsandfilters.html#event-filters).
Best way to get what you want is to subclass the QGraphicsItem (whichever one you are using - QGraphicsRectItem, QGraphicsPathItem, etc.) and overload the mousePressEvent method on it.
http://doc.qt.nokia.com/4.7-snapshot/qgraphicsitem.html#mousePressEvent
For instance:
from PyQt4.QtGui import QGraphicsRectItem
class MyItem(QGraphicsRectItem):
def mousePressEvent(self, event):
super(MyItem, self).mousePressEvent(event)
print 'overloaded'
scene.addItem(MyItem())
Either subclass the view, scene, item etc and reimplement mousePressEvent and/or mouseReleaseEvent; or install an event filter on those items.
For an example that uses an event filter on a scene, see this answer.
Here's a demo which reimplements mouseReleaseEvent on the view:
from PyQt4 import QtGui, QtCore
class Window(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self)
self.view = View(self)
self.label = QtGui.QLabel(self)
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.view)
layout.addWidget(self.label)
class View(QtGui.QGraphicsView):
def __init__(self, parent):
QtGui.QGraphicsView.__init__(self, parent)
self.setScene(QtGui.QGraphicsScene(self))
for index, name in enumerate('One Two Three Four Five'.split()):
item = QtGui.QGraphicsRectItem(
index * 60, index * 60, 50, 50)
item.setData(0, name)
self.scene().addItem(item)
def mouseReleaseEvent(self, event):
pos = event.pos()
item = self.itemAt(pos)
if item is not None:
text = 'Rectangle <b>%s</b>' % item.data(0).toString()
else:
text = 'No Rectangle (%d, %d)' % (pos.x(), pos.y())
self.parent().label.setText(text)
QtGui.QGraphicsView.mouseReleaseEvent(self, event)
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = Window()
window.resize(400, 400)
window.show()
sys.exit(app.exec_())

Categories

Resources