PySide.QtGui.QDialog issue - python

I want to draw a frame around my borderless dialog while it gets resized interactively but it seems like QDialog.setSizeGripEnabled(True) and QWidget.resizeEvent doen't work well together. The dialog can't be resized interactively, is this a known problem ?
class MyDialog(QtGui.QDialog):
def __init__(self,x,y):
QtGui.QDialog.__init__(self)
self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
self.setGeometry(x,y,250,300)
self.setSizeGripEnabled(True)
self.frame = QtGui.QFrame(self)
self.frame.setGeometry(self.rect())
self.frame.setFrameShape(QtGui.QFrame.StyledPanel)
self.frame.setFrameShadow(QtGui.QFrame.Plain)
etc...
def resizeEvent( self, event ):
r = self.rect()
s = event.size()
self.frame.setGeometry( r.x(), r.y(), s.width(), s.height() )

The problem here is, that you override the resizeEvent without passing it on, this seems to break the functionality of the generated size grip.
The simplest solution would be to call:
super(MyDialog, self).resizeEvent(event)
in your event handler. Another option is to add your own QResizeGrip to your frame:
class MyDialog(QtGui.QDialog):
def __init__(self,x,y):
# [...]
self.frame.setLayout(QtGui.QVBoxLayout())
self.sizeGrip = QtGui.QSizeGrip(self.frame)
self.frame.layout().addWidget(self.sizeGrip, 0,
QtCore.Qt.AlignRight | QtCore.Qt.AlignBottom)
self.setStyleSheet("QSizeGrip {background-color: #202020}") # make it visible
# [...]
This has the advantage that you can arrange the size grip freely.

Related

PyQt tablewidget change location of vertical scroll bar

I have a QTableWidget in a window. Currently, the table's scrollbar goes all the way up to the top of the table's headers:
However, I would like the scrollbar to start below the headers, basically moving it down in the y axis.
I need it to start at the position of the scroll bar in the image below:
Does anyone have any idea how I could do this?
The simplest solution is to set the geometry of the scroll bar whenever the table resizes:
class Table(QtWidgets.QTableWidget):
# ...
def resizeEvent(self, event):
super().resizeEvent(event)
self.verticalScrollBar().setGeometry(
bar.geometry().adjusted(
0, self.horizontalHeader().height(), 0, 0))
Another similar possibility is to use a scroll bar widget added on top of the scroll bar, but the concept remains almost the same:
class Table(QtWidgets.QTableWidget):
def __init__(self):
super().__init__(20, 20)
self.scrollBarSpacer = QtWidgets.QWidget()
self.addScrollBarWidget(self.scrollBarSpacer, QtCore.Qt.AlignTop)
def resizeEvent(self, event):
super().resizeEvent(event)
self.scrollBarSpacer.setFixedHeight(self.horizontalHeader().height())
Do note that in both cases it's mandatory to call the base implementation of resizeEvent() first.
If you don't want to subclass, add an event filter for the table:
class SomeWindow(QtWidgets.QMainWindow):
def __init__(self):
# ...
self.table.installEventFilter(self)
def eventFilter(self, source, event):
if source == self.table and event.type() == QtCore.QEvent.Resize:
# let the table handle the event
source.event(event)
source.verticalScrollBar().setGeometry(
source.verticalScrollBar().geometry().adjusted(
0, self.horizontalHeader().height(), 0, 0))
return True
return super().eventFilter(source, event)

how to make an overriden QGraphicsTextItem editable & movable?

I am using PyQt and I'm trying to re-implement a QGraphicsTextItem, but it seems I'm missing something.
I would like to make the NodeTag item's text editable. I have tried setting flags such as Qt.TextEditorInteraction and QGraphicsItem.ItemIsMovable , but those seem to be ignored...
Here is a Minimal Reproducible Example :
import sys
from PyQt5.QtWidgets import QGraphicsScene, QGraphicsView, QMainWindow, QApplication, QGraphicsItem, QGraphicsTextItem
from PyQt5.QtCore import *
from PyQt5.QtGui import QPen
class NodeTag(QGraphicsTextItem):
def __init__(self,text):
QGraphicsTextItem.__init__(self,text)
self.text = text
self.setPos(0,0)
self.setTextInteractionFlags(Qt.TextEditorInteraction)
# self.setFlag(QGraphicsItem.ItemIsFocusable, True) # All these flags are ignored...
# self.setFlag(QGraphicsItem.ItemIsSelectable, True)
self.setFlag(QGraphicsItem.ItemIsMovable, True)
def boundingRect(self):
return QRectF(0,0,80,25)
def paint(self,painter,option,widget):
painter.setPen(QPen(Qt.blue, 2, Qt.SolidLine))
painter.drawRect(self.boundingRect())
painter.drawText(self.boundingRect(),self.text)
def mousePressEvent(self, event):
print("CLICK!")
# self.setTextInteractionFlags(Qt.TextEditorInteraction) # make text editable on click
# self.setFocus()
class GView(QGraphicsView):
def __init__(self, parent, *args, **kwargs):
super().__init__(*args, **kwargs)
self.parent = parent
self.setGeometry(100, 100, 700, 450)
self.show()
class Scene(QGraphicsScene):
def __init__(self, parent):
super().__init__(parent)
self.parent = parent
tagItem = NodeTag("myText") # create a NodeTag item
self.addItem(tagItem)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__() # create default constructor for QWidget
self.setGeometry(900, 70, 1000, 800)
self.createGraphicView()
self.show()
def createGraphicView(self):
self.scene = Scene(self)
gView = GView(self)
scene = Scene(gView)
gView.setScene(scene)
# Set the main window's central widget
self.setCentralWidget(gView)
# Run program
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec_())
As you can see I have tried overriding the mousePressEvent and setting flags there too, but no luck so far.
Any help appreciated!
All QGraphicsItem subclasses have a paint method, and all items that paint some contents have that method overridden so that they can actually paint themselves.
The mechanism is the same as standard QWidgets, for which there is a paintEvent (the difference is that paint of QGraphicsItem receives an already instanciated QPainter), so if you want to do further painting other than what the class already provides, the base implementation must be called.
Consider that painting always happen from bottom to top, so everything that needs to be drawn behind the base painting has to be done before calling super().paint(), and everything that is going to be drawn in front of the default painting has to be placed after.
Depending on the situation, overriding might require that the default base implementation is called anyway, and that's important in your case for boundingRect too. QGraphicsTextItem automatically resizes itself when its contents change, so you should not always return a fixed QRect. If you need to have a minimum size, the solution is to merge a minimum rectangle with those provided by the default boundingRect() function.
Then, editing on a QGraphicsTextItem happens when the item gets focused, but since you also want to be able to move the item, things get trickier as both actions are based on mouse clicks. If you want to be able to edit the text with a single click, the solution is to make the item editable only when the mouse button has been released and has not been moved by some amount of pixels (the startDragDistance() property), otherwise the item is moved with the mouse. This obviously makes the ItemIsMovable flag useless, as we're going to take care of the movement internally.
Finally, since a minimum size is provided, we also need to override the shape() method in order to ensure that collision and clicks are correctly mapped, and return a QPainterPath that includes the whole bounding rect (for normal QGraphicsItem that should be the default behavior, but that doesn't happen with QGraphicsRectItem).
Here's a full implementation of what described above:
class NodeTag(QGraphicsTextItem):
def __init__(self, text):
QGraphicsTextItem.__init__(self, text)
self.startPos = None
self.isMoving = False
# the following is useless, not only because we are leaving the text
# painting to the base implementation, but also because the text is
# already accessible using toPlainText() or toHtml()
#self.text = text
# this is unnecessary too as all new items always have a (0, 0) position
#self.setPos(0, 0)
def boundingRect(self):
return super().boundingRect() | QRectF(0, 0, 80, 25)
def paint(self, painter, option, widget):
# draw the border *before* (as in "behind") the text
painter.setPen(QPen(Qt.blue, 2, Qt.SolidLine))
painter.drawRect(self.boundingRect())
super().paint(painter, option, widget)
def shape(self):
shape = QPainterPath()
shape.addRect(self.boundingRect())
return shape
def focusOutEvent(self, event):
# this is required in order to allow movement using the mouse
self.setTextInteractionFlags(Qt.NoTextInteraction)
def mousePressEvent(self, event):
if (event.button() == Qt.LeftButton and
self.textInteractionFlags() != Qt.TextEditorInteraction):
self.startPos = event.pos()
else:
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
if self.startPos:
delta = event.pos() - self.startPos
if (self.isMoving or
delta.manhattanLength() >= QApplication.startDragDistance()):
self.setPos(self.pos() + delta)
self.isMoving = True
return
super().mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
if (not self.isMoving and
self.textInteractionFlags() != Qt.TextEditorInteraction):
self.setTextInteractionFlags(Qt.TextEditorInteraction)
self.setFocus()
# the following lines are used to correctly place the text
# cursor at the mouse cursor position
cursorPos = self.document().documentLayout().hitTest(
event.pos(), Qt.FuzzyHit)
textCursor = self.textCursor()
textCursor.setPosition(cursorPos)
self.setTextCursor(textCursor)
super().mouseReleaseEvent(event)
self.startPos = None
self.isMoving = False
As a side note, remember that QGraphicsTextItem supports rich text formatting, so even if you want more control on the text painting process you should not use QPainter.drawText(), because you'd only draw the plain text. In fact, QGraphicsTextItem draws its contents using the drawContents() function of the underlying text document.
Try it:
...
class NodeTag(QGraphicsTextItem):
def __init__(self, text, parent=None):
super(NodeTag, self).__init__(parent)
self.text = text
self.setPlainText(text)
self.setFlag(QGraphicsItem.ItemIsMovable)
self.setFlag(QGraphicsItem.ItemIsSelectable)
def focusOutEvent(self, event):
self.setTextInteractionFlags(QtCore.Qt.NoTextInteraction)
super(NodeTag, self).focusOutEvent(event)
def mouseDoubleClickEvent(self, event):
if self.textInteractionFlags() == QtCore.Qt.NoTextInteraction:
self.setTextInteractionFlags(QtCore.Qt.TextEditorInteraction)
super(NodeTag, self).mouseDoubleClickEvent(event)
def paint(self,painter,option,widget):
painter.setPen(QPen(Qt.blue, 2, Qt.SolidLine))
painter.drawRect(self.boundingRect())
# painter.drawText(self.boundingRect(),self.text)
super().paint(painter, option, widget)
...

Have 2 pyqt buttons move synchronized when mouse moves

I am currently making a program where a user selects an image qpushbutton. I already have superseded mouseMoveEvent, mousePressEvent, and mouseReleaseEvent in the button class to get a movable button. The buttons are currently moving independently, but I would like the buttons to move so that the horizontal distance between them stays the same.
So currently in pseudo code I have:
import stuff
import mvbutton as mv
class MasterClass(QWidget):
def __init__(self, *args):
QWidget.__init__(self, *args)
#more setup stuff, layout, etc
self.addbutton(image,name,size)
def addbutton(#args):
self.button=mv.dragbutton(#args)
#some more setup
#now rename so that each button has its own name
if name== "name1":
self.name1=self.button
else:
self.name2=self.button
self.button=""
#more code to set up
I supersede the mouse motion/press/release functions in the dragbutton class. I cannot, therefore reference the new self.name# there. So the self.move(pos) in my dragbutton class cannot get the self.name# because it is a different self. Any ideas on how I could get this to work? Thanks.
Done something very rough after trying to understand your requirement.
Hope this helps.
EDIT
tried to add more accuracy in moving. Won't do real time moving cause it has problems with lag and update. I guess the moving won't be jittery any more.
from PyQt4 import QtGui
import sys
class MultiButton(QtGui.QWidget):
def __init__(self, *args, **kwargs):
QtGui.QWidget.__init__(self, *args, **kwargs)
self._b1 = QtGui.QPushButton("B1")
self._b2 = QtGui.QPushButton("B2")
self._arrangeWidgets()
self.setStyleSheet("background-color: rgb(0, 0, 0);\n"+\
"color: rgb(255, 255, 255);\n"+\
"border:1px solid #7F462C ;\n")
self._moveStart = False
self._startX = 0
self._startY = 0
def _arrangeWidgets(self):
layout = QtGui.QHBoxLayout()
layout.addWidget(self._b1)
#horizontal spacing remains constant now
layout.addSpacing(90)
layout.addWidget(self._b2)
self.setLayout(layout)
def mousePressEvent(self,event):
self._moveStart = True
self._startX = event.pos().x() - self.pos().x()
self._startY = event.pos().y() - self.pos().y()
return QtGui.QWidget.mousePressEvent(self, event)
def mouseReleaseEvent(self, event):
if self._moveStart:
self.setGeometry(event.pos().x() - self._startX,event.pos().y() - self._startY,self.width(),self.height())
self._moveStart = False
self._startX = 0
self._startY = 0
return QtGui.QWidget.mouseReleaseEvent(self, event)
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
wd = QtGui.QMainWindow()
wd.resize(500,500)
mb = MultiButton()
mb.setFixedSize(200,50)
wd.setCentralWidget(mb)
wd.show()
sys.exit(app.exec_())
here the MultiButton widget moves the two buttons keeping the horizontal space between the two always constant.

How to add widgets dynamically after parent is created

I'm trying to build an interface using custom widgets, and have run into the following problem.
I have a widget Rectangle which I want to use as an interactive element in my interface. To define a rectangle I just need to give it a parent, so it knows what window to draw itself in, and a position [x,y, width, height] defining its position and size. (I know that some of you will say "You should be using layouts as opposed to absolute positioning" but I am 100% sure that I need absolute positioning for this particular application).
from PySide.QtCore import *
from PySide.QtGui import *
import sys
class Rectangle(QWidget):
def __init__(self, parent, *args):
super(self.__class__,self).__init__(parent)
print parent, args
#expect args[0] is a list in the form [x,y,width,height]
self.setGeometry(*args[0])
def enterEvent(self, e):
print 'Enter'
def leaveEvent(self, e):
print 'Leave'
def paintEvent(self, e):
print 'Painted: ',self.pos
painter = QPainter(self)
painter.setPen(Qt.NoPen)
painter.setBrush(QColor(200,100,100))
painter.drawRect(0,0,self.width()-1, self.height()-1)
painter.end()
I also have a Window widget which is the canvas on which my visualization is to be drawn. In the Window's __init__() definition I create a rectangle A at 20,40.
class Window(QWidget):
def __init__(self):
super(self.__class__, self).__init__()
self.widgets = [Rectangle(self,[20,40,100,80])]
self.setMouseTracking(True)
self.setGeometry(300,300,800,600)
self.setWindowTitle('Window')
self.show()
def addWidget(self,Widget, *args):
self.widgets += [Widget(self, *args)]
self.update()
def mousePressEvent(self, e):
for widget in self.widgets:
print widget.geometry()
Since I am building a visualization, I want to create my Window and then add widgets to it afterwords, so I create an instance mWindow, which should already have rectangle A defined. I then use my window's addWidget() method to add a second rectangle at 200,200 - call it rectangle B.
if __name__ == "__main__":
app= QApplication(sys.argv)
mWindow = Window()
mWindow.addWidget(Rectangle, [200,200,200,80])
sys.exit(app.exec_())
The issue I have is that only rectangle A actually gets drawn.
I know that both rectangle A and **rectangle B are getting instantiated and both have myWindow as their parent widgets, because of the output of print parent in the constructor for Rectangle.
However, when I resize the window to force it to repaint itself, the paintEvent() method is only called on rectangle A, not rectangle B. What am I missing?
You just forgot to show the rectangle. In addWidget, add this before self.update():
self.widgets[-1].show()
The reason why you don't need show for the first rectangle object is because it is
created in the Window constructor. Then, Qt itself is making sure objects are properly
shown (which is misleading, I agree...).

Object-based paint/update in Qt/PyQt4

I'd like to tag items by drawing polygons over an image in Python using PyQt4. I was able to implement the image viewer with QGraphicsScene but I don't understand the concept behind painting/updating objects.
What I'd like to do is a Polygon class, what supports adding and editing. What confuses me is the QGraphicsScene.addItem and the different paint or update methods. What I'd like to implement is to
draw a polygon as lines while not complete
draw it as a filled polygon once complete
The algorithm part is OK, what I don't understand is that how do I implement the paint or update functions.
Here is my confusion
In the original example file: graphicsview/collidingmice there is a special function def paint(self, painter, option, widget): what does the painting. There is no function calling the paint function, thus I'd think it's a special name called by QGraphicsView, but I don't understand what is a painter and what should a paint function implement.
On the other hand in numerous online tutorials I find def paintEvent(self, event): functions, what seems to follow a totally different concept compared to the graphicsview / paint.
Maybe to explain it better: for me the way OpenGL does the scene-update is clear, where you always clean everything and re-draw elements one by one. There you just take care of what items do you want to draw and draw the appropriate ones. There is no update method, because you are drawing always the most up-to-date state. This Qt GUI way is new to me. Can you tell me what happens with an item after I've added it to the scene? How do I edit something what has been added to the scene, where is the always updating 'loop'?
Here is my source in the smallest possible form, it creates the first polygon and starts printing it's points. I've arrived so far that the paint method is called once (why only once?) and there is this error NotImplementedError: QGraphicsItem.boundingRect() is abstract and must be overridden. (just copy any jpg file as big.jpg)
from __future__ import division
import sys
from PyQt4 import QtCore, QtGui
class Polygon( QtGui.QGraphicsItem ):
def __init__(self):
super(Polygon, self).__init__()
self.points = []
self.closed = False
def addpoint( self, point ):
self.points.append( point )
print self.points
def paint(self, painter, option, widget):
print "paint"
class MainWidget(QtGui.QWidget):
poly_drawing = False
def __init__(self):
super(MainWidget, self).__init__()
self.initUI()
def initUI(self):
self.scene = QtGui.QGraphicsScene()
self.img = QtGui.QPixmap( 'big.jpg' )
self.view = QtGui.QGraphicsView( self.scene )
self.view.setRenderHint(QtGui.QPainter.Antialiasing)
self.view.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.view.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.pixmap_item = QtGui.QGraphicsPixmapItem( self.img, None, self.scene)
self.pixmap_item.mousePressEvent = self.pixelSelect
self.mypoly = Polygon()
layout = QtGui.QVBoxLayout()
layout.addWidget( self.view )
self.setLayout( layout )
self.resize( 900, 600 )
self.show()
def resizeEvent(self, event):
w_scale = ( self.view.width() ) / self.img.width()
h_scale = ( self.view.height() ) / self.img.height()
self.scale = min( w_scale, h_scale)
self.view.resetMatrix()
self.view.scale( self.scale, self.scale )
def pixelSelect(self, event):
if not self.poly_drawing:
self.poly_drawing = True
self.mypoly = Polygon()
self.scene.addItem( self.mypoly )
point = event.pos()
self.mypoly.addpoint( point )
def main():
app = QtGui.QApplication(sys.argv)
ex = MainWidget()
sys.exit(app.exec_())
if __name__ == '__main__':
main()

Categories