Strange painting bug in GraphicsView in PyQt 5.7 - qt

[updating with smaller example]
We just upgraded to PyQt 5.7 and we have one the last problem left to fix in our application. Here is a standalone example that I created from our application code. Run it and see how the ellipse gets drawn beyond the view borders. This did not occur in 5.5.1. Platform is Windows 7 64 bit (running in a VM). It looks like this:
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication, QGraphicsView, QGraphicsScene, QHBoxLayout, QWidget, QLabel
from PyQt5.QtWidgets import QGraphicsProxyWidget, QGraphicsObject, QGraphicsEllipseItem
from PyQt5.QtGui import QBrush
class MyGraphicsItem(QGraphicsObject):
def __init__(self):
QGraphicsObject.__init__(self)
# next line could be any type of graphics item:
rect_item = QGraphicsEllipseItem(0, 0, 100, 100, self)
# effect easier to see if paint black:
rect_item.setBrush(QBrush(Qt.SolidPattern))
label_item = QGraphicsProxyWidget(self)
# *** Next line must be there for effect to be visible, but could be any other type of widget
label_item.setWidget(QLabel('a'*30))
def paint(self, painter, option, widget=None):
return
def boundingRect(self):
return self.childrenBoundingRect()
def show_problem():
app = QApplication([])
widget = QWidget()
layout = QHBoxLayout()
widget.setLayout(layout)
view = QGraphicsView()
scene = QGraphicsScene()
view.setScene(scene)
scene.addItem(MyGraphicsItem()) # *** effect only there if more than 1 item
scene.addItem(MyGraphicsItem())
layout.addWidget(view)
widget.setGeometry(100, 100, 50, 50)
widget.show()
app.exec()
show_problem()

So Russell confirmed that this is a regression in 5.7 (i.e., problem was not present in PyQt 5.6).
I posted a C++ version of my Python example to Qt Forum. A fellow there was kind enough to fix compilation bugs and run it, and saw the bug: so this is indeed a Qt bug.
There is a bug report at bugreports.qt.io that is almost certainly the same issue. It is not resolved, but by an incredible luck, it links to a post where there is a workaround: set the opacity of proxy widget item to just below 1. In the example app of my post, I added
label_item.setOpacity(0.999999)
after setting label_item's widget. I our real app (110k PyQt code) I set the opacity of all QGraphicsProxyWidget instance and it seems to work.
This workaround seems to not have any other side effects, time will tell if there are deeper problems. But it appears to be at least be a temporary solution until the bug gets fixed in Qt. I have submitted a bug report.

Related

Why QDialog not shows any widgets until job completed? [duplicate]

This question already has answers here:
Equivalent to time.sleep for a PyQt application
(5 answers)
Closed 2 years ago.
im new to pyqt5,i tried to open dialog and push some text into that dialog
my dialog contain one plaintext ,progressbar and pushbutton
when i run the code its popup the dialog but not shown any thing ,after code execution completes its showing all the widgets and with text
but i need to open the dialog and i want update progress bar
My code
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtWidgets import (QDialog,QPlainTextEdit,QScrollArea,QProgressBar,QPushButton)
import sys
import time
class PrograssDialog():
def ShowDialog(self,Dialogs):
try:
self.Pd=Dialogs
self.Pd.setWindowTitle("Script Excution... ")
self.Pd.resize(500,500)
self.ScrArea=QScrollArea(self.Pd)
self.ScrArea.move(0,0)
self.ScrArea.resize(500,300)
self.TextArea=QPlainTextEdit(self.Pd)
self.TextArea.move(0,0)
self.TextArea.resize(500,300)
self.TextArea.insertPlainText(str("Start : %s" % time.ctime())+"\n")
self.Prograssbar=QProgressBar(self.Pd)
self.Prograssbar.setGeometry(QtCore.QRect(0, 350, 450, 23))
self.Prograssbar.setMaximum(100)
self.Cancelbutton=QPushButton("Cancel",self.Pd)
self.Cancelbutton.setGeometry(QtCore.QRect(360, 400, 93, 28))
self.Cancelbutton.clicked.connect(self.StopExcution)
self.Pd.show()
except Exception as msg:
import sys
tb = sys.exc_info()[2]
print("Error_analysis " + str(msg)+ str(tb.tb_lineno))
def AddMessage(self,Message):
self.TextArea.insertPlainText(str(Message)+"\n")
# print("message added")
def SetPercentage(self,Number):
self.Prograssbar.setValue(Number)
# print("percent added")
def StopExcution(self):
sys.exit()
app = QApplication(sys.argv)
ui=PrograssDialog()
ui.ShowDialog(QDialog())
for i in range(100):
ui.AddMessage("Hello")
ui.SetPercentage(i)
time.sleep(0.5)
sys.exit(app.exec_())
There are various problems with your code, I'll try to address all of them.
The main reason for the issue you are facing is that no blocking functions (like time.sleep) should happen in the main Qt thread (which is the thread that shows the GUI elements and allow interactions with them); blocking functions prevent the UI to correctly draw and refresh its contents, if you want to do an operation at specific intervals, you have to use a QTimer;
You should not use a basic python object subclass for this kind of situations, especially since you're only using just one dialog; you should subclass from QDialog instead and implement
To "exit" your program you should not use sys.exit (you are already using it), but use QApplication.quit() instead; also, since you already imported sys at the beginning, there's no need to import it again in the exception;
Function and variable names should not be capitalized; while you can use any casing style you want for your own code, it's common (and highly suggested) practice to always use lowercase initials, and it's also a convention you should stick to when sharing code with others, especially on Q&A sites like StackOverflow; read more on the official Style Guide for Python Code;
Always avoid fixed geometries for children widgets: what others see on their computers will probably be very different from what you see on yours, and you might end up with an unusable interface; use layout managers instead, so that the widgets can resize themselves if required;
You added a scroll area but you never use it; since you're using the same geometry for the text area I believe that you thought you were using for that, but there's no need as the text area already is a scroll area;
Here is how the code could look like in order to achieve what you want:
import time
from PyQt5 import QtCore, QtWidgets
class ProgressDialog(QtWidgets.QDialog):
def __init__(self, parent=None):
super().__init__(parent)
layout = QtWidgets.QVBoxLayout(self)
self.textArea = QtWidgets.QPlainTextEdit()
layout.addWidget(self.textArea)
self.textArea.insertPlainText(str("Start : %s" % time.ctime())+"\n")
self.textArea.setReadOnly(True)
self.progressBar = QtWidgets.QProgressBar()
layout.addWidget(self.progressBar)
self.cancelButton = QtWidgets.QPushButton('Cancel')
layout.addWidget(self.cancelButton)
self.cancelButton.clicked.connect(QtWidgets.QApplication.quit)
self.countTimer = QtCore.QTimer()
self.countTimer.timeout.connect(self.timeout)
def startCounter(self, maximum, sleepSeconds):
self.progressBar.reset()
self.progressBar.setMaximum(maximum)
# QTimer interval is in milliseconds
self.countTimer.setInterval(sleepSeconds * 1000)
self.countTimer.start()
def timeout(self):
if self.progressBar.value() == self.progressBar.maximum():
self.countTimer.stop()
return
self.setPercentage(self.progressBar.value() + 1)
self.addMessage('Hello')
def setPercentage(self, value):
self.progressBar.setValue(value)
def addMessage(self, message):
self.textArea.insertPlainText(str(message) + '\n')
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
dialog = ProgressDialog()
dialog.show()
dialog.startCounter(100, .5)
sys.exit(app.exec_())

Moving the cursor in a PyQt5 text edit doesn't work

I'm contributing to Frescboaldi, a PyQt5 application and experience problems interacting with the core text edit component.
It seems whatever I try I can't get either of setPosition or movePosition to work.
The code
cursor.insertText("Hello")
cursor.setPosition(cursor.position() - 5)
properly inserts the text Hello in the document but leaves the cursor at the end of the inserted text (instead of moving it to the left by 5 characters). The first line proves that cursor, textedit and document are set up properly. trying movePosition doesn't have any effect either.
The actual goal is to insert some text, have it selected and the cursor at the end of the selection as can be seen in https://github.com/wbsoft/frescobaldi/blob/master/frescobaldi_app/cursortools.py#L179
Am I doing anything wrong here? Could this be a bug in Qt/PyQt? Or could this be an issue in PyQt5?
[Edit:] I've now confirmed with a minimal app example that the problem can't be in the larger construction of the application. In the following mini app neither setPosition nor movePosition has any effect - while insertText works well:
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import sys
from PyQt5.QtWidgets import QApplication, QTextEdit
def main():
app = QApplication(sys.argv)
w = QTextEdit()
w.setWindowTitle('Manipulate cursor')
cursor = w.textCursor()
cursor.insertText("Hello World")
# neither of the following commands have any effect
cursor.setPosition(cursor.position() - 5)
cursor.movePosition(cursor.movePosition(cursor.Left, cursor.KeepAnchor, 3))
w.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
You are working on a local copy of the text cursor returned by w.textCursor. You should call w.setTextCursor(cursor) at the end to change the visible cursor.
A second problem is that you use the output of movePosition to call movePosition again, which is not allowed:
cursor.movePosition(cursor.movePosition(cursor.Left, cursor.KeepAnchor, 3))
should be
cursor.movePosition(cursor.Left, cursor.KeepAnchor, 3)
Note that I tested it in Qt (not PyQt), but that should not make any difference, which successfully selected lo of Hello world.

How to test drag and drop behavior in PyQt?

I want to unittest drag and drop for our widgets. At the moment, I instantiate a QDragEnterEvent, but this is discouraged with a big warning on the Qt documentation, because it relies on the Qt library internal state. In fact, I get segfaults that appear to be due to a violation of this Warning.
Given this premise, how can one test drag and drop behavior?
If using Unix we can use QTest, however to get a cross-platform solution, we can implement a solution where we circumvent Qt.
Using QTest
Although the Qt documentation for drag and drop says that it will not block the main event loop, a closer look at QDrag.exec will reveal that this is not true for Windows.
The call to QTest.mousePress causes the test to block until the mouse is physically moved by the user.
I got around this in Linux by using a timer to schedule the mouse move and release:
def testDragAndDrop(self):
QtCore.QTimer.singleShot(100, self.dropIt)
QtTest.QTest.mousePress(dragFromWidget, QtCore.Qt.LeftButton)
# check for desired behaviour after drop
assert something
def dropIt(self):
QtTest.QTest.mouseMove(dropToWidget)
QtTest.QTest.mouseRelease(dropToWidget, QtCore.Qt.LeftButton, delay=15)
For this solution, it is necessary to include a delay in the mouseRelease call, and to have called show on your widget.
Note that I have verified this works using pyqt4 and Python 2.7 on Fedora 20
Cross-Platform
You can use the mouse manipulation methods from the PyUserInput package. Put the mouse interaction in separate thread to avoid the locking up of the Qt main event loop. We can do this since we are not using Qt at all in our mouse control. Make sure that you have called show on the widgets you are dragging to/from.
from __future__ import division
import sys, time, threading
import numpy as np
from PyQt4 import QtGui, QtCore, QtTest
from pymouse import PyMouse
...
def mouseDrag(source, dest, rate=1000):
"""Simulate a mouse visible mouse drag from source to dest, rate is pixels/second"""
mouse = PyMouse()
mouse.press(*source)
# smooth move from source to dest
npoints = int(np.sqrt((dest[0]-source[0])**2 + (dest[1]-source[1])**2 ) / (rate/1000))
for i in range(npoints):
x = int(source[0] + ((dest[0]-source[0])/npoints)*i)
y = int(source[1] + ((dest[1]-source[1])/npoints)*i)
mouse.move(x,y)
time.sleep(0.001)
mouse.release(*dest)
def center(widget):
midpoint = QtCore.QPoint(widget.width()/2, widget.height()/2)
return widget.mapToGlobal(midpoint)
def testDragAndDrop(self):
# grab the center of the widgets
fromPos = center(dragFromWidget)
toPos = center(dropToWidget)
dragThread = threading.Thread(target=mouseDrag, args=((fromPos.x(),fromPos.y()), (toPos.x(), toPos.y())))
dragThread.start()
# cannot join, use non-blocking wait
while dragThread.is_alive():
QtTest.QTest.qWait(1000)
# check that the drop had the desired effect
assert dropToWidget.hasItemCount() > 0
Note I have tested this using PyQt4 and Python 2.7 on Fedora and Windows 7
Haven't tried, but if your drag & drop process is Qt internal (meaning, you're dragging from and to a Qt widget), QTest might help.
Basically by doing something along the lines:
QTest.mousePress(drag_widget, Qt.LeftButton) # simulate mouse press on whatever you want to drag
QTest.mouseMove(drop_widget) # move the mouse to the target - maybe this can be skipped
QTest.mouseRelease(drop_widget, Qt.LeftButton) # simulate mouse release where you want to drop
All functions may be supplied with further positional information (e.g. to click a list item within a widget) and with optional delays to emulate a human user.
Not a copy-pasteable answer, but maybe it serves as a starter...

How can I use PyQt4 "considering QtGui.QPainter" through procedural programming?

as I am moving from tkinter to PyQt4 and I don't like OOP that much.
in tkinter you can actually set your window , buttons , entrybox ,...etc using procedural programming
when I tried to do the same ,I just could create the GUI but couldn't do any actual work out of it.. why this code doesn't work as it's supposed to ??!
I have another version of it using OOP and it works fine but I'm not a big fan of OOP plus I really want to know why I can't get PyQt4 to work using procedural programming
here is the code :-
from PyQt4 import QtGui, QtCore
import sys
app = QtGui.QApplication(sys.argv)
widget = QtGui.QWidget()
widget.setGeometry(300, 300, 300, 300)
widget.setWindowTitle('draw')
bt=QtGui.QPushButton('qyuit',widget) ##create the button
paint = QtGui.QPainter(widget) ## create "paint" object enables me to draw
def draw_line():
print 'start draw line'
paint.begin(widget)
pen = QtGui.QPen(QtCore.Qt.red, 20, QtCore.Qt.SolidLine)
pen.setCapStyle(QtCore.Qt.RoundCap)
pen.setJoinStyle(QtCore.Qt.RoundJoin)
paint.setPen(pen)
paint.drawLine(30, 70, 250, 90) ## drawing the line
paint.end()
widget.update()
bt.clicked.connect(draw_line) ## connecting the clicked event with "draw_line" method
widget.show()
sys.exit(app.exec_())
So when I press the button , there is no line drawn.. It just prints the 'start draw line' in python shell .. Why this line can't be drawn ??!
Thanks in advance .

Why doesn't menu get added?

I'm clearly missing something here; why doesn't the File menu get added in this little example app?
import sys
from PySide.QtGui import *
class Window(QMainWindow):
def __init__(self):
super(Window, self).__init__()
self.setWindowTitle('Test')
layout = QHBoxLayout()
self.widget = QWidget()
self.widget.setLayout(layout)
self.setCentralWidget(self.widget)
self.exitAction = QAction('Exit', self, shortcut=QKeySequence.Quit, triggered=self.close)
self.fileMenu = self.menuBar().addMenu('File')
self.fileMenu.addAction(self.exitAction)
app = QApplication(sys.argv)
w = Window()
w.show()
sys.exit(app.exec_())
EDIT:
Ok, it looks like this is actually a unicode issue.
Here's another example app:
from __future__ import unicode_literals, print_function, division
import sys
from PySide.QtCore import *
from PySide.QtGui import *
class Window(QMainWindow):
def __init__(self):
super(Window, self).__init__()
self.dummyAction = QAction(self.tr('dummy'), self, triggered=self.dummy)
self.gameMenu = self.menuBar().addMenu(self.tr('ddddummy'))
print (self.tr('dummy'))
self.gameMenu.addAction(self.dummyAction)
layout = QHBoxLayout()
self.widget = QWidget()
self.widget.setLayout(layout)
self.setCentralWidget(self.widget)
def dummy(self):
pass
locale = QLocale.system().name()
qtTranslator = QTranslator()
app = QApplication(sys.argv)
if qtTranslator.load('qt_' + locale, ':/'):
app.installTranslator(qtTranslator)
w = Window()
w.show()
sys.exit(app.exec_())
This app doesn't have 'File' or 'Quit' or 'Exit' -- but it works if I comment out the from __future__ line, or surround the quoted strings like self.tr(str('foo')) instead of self.tr('foo')
EDIT 2:
from __future__ import unicode_literals
import sys
from PySide.QtGui import *
class Window(QMainWindow):
def __init__(self):
super(Window, self).__init__()
print self.tr('foo')
app = QApplication(sys.argv)
Window().show()
sys.exit(app.exec_())
This should print 'foo', but prints nothing.
At first glance, your code seems perfectly normal, and it does function just as expected on windows or linux. The issue here is that on OSX, the operating system enforces a standard interface on the menu. Whereas on other operating systems the menu is nested right under your app, and your app owns it...on OSX, the operating system owns it. Hence it is shown in the global menu area.
That being said, OSX is filtering out some reserved keywords like "Quit" or "Exit". The reason for this is because the quit functionality is a standard that is automatically placed in your Application menu. When you run it as a basic python script, the menu will be called "Python". But if you bundle it into an app, it will be named accordingly for your bundled app.
This link here, while not an exact explanation, does mention the differences for a menu on OSX.
For a quick example of fixing your menu, see what happens when you do:
self.exitAction = QAction('Kwit', self)
OSX will not filter out that one. But I suppose its better to follow the native standards which make all app experiences the same on the platform. You would definitely include the "Quit" menu action as you have it now, so that your app will be cross-platform if run on linux or windows and just expect that OSX will relocate it for you.
I've come across this thread because I'm struggling with a similar issue. Here's what I've found...
About your EDIT 2:
Your code will correctly print 'foo' if you substitute the line
Window().show()
for the lines
w = Window()
w.show()
as you had in your original code. Apparently the return type on the constructor causes chaining to be an issue in python?
I was able to reproduce your EDIT 1 by commenting out the from __future__ line. Otherwise, the code below works as expected in OS X (Mountain Lion 10.8.3 with brewed python). Specifically, the following code puts the "About" action under the "Python" application menu that OS X creates, and also creates a "Help" menu containing the "Website" action.
import sys
from PySide.QtGui import QApplication,QMainWindow, QWidget, QAction
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.create_menus()
self.create_main_frame()
def create_menus(self):
self.aboutAction = QAction('About', self, triggered=self.on_about)
self.websiteAction = QAction('Website', self, triggered=self.on_website)
self.help_menu = self.menuBar().addMenu('Help')
self.help_menu.addAction(self.aboutAction)
self.help_menu.addAction(self.websiteAction)
def create_main_frame(self):
self.mainWidget = QWidget()
self.setCentralWidget(self.mainWidget)
def on_website(self):
pass
def on_about(self):
pass
app = QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
I should draw attention to an important point that this thread helped me discover. I've seen a lot of advice for OS X that indicates you should create menu_bar = QMenuBar() independently of QMainWindow and then bind with self.setMenuBar(menu_bar) where self represents QMainWindow. This, in fact, didn't work for me. Instead, what did work, is grabbing the menu bar reference directly from the QMainWindow class itself. For example, and like above, when adding a menu, use self.help_menu = self.menuBar().addMenu('Help') as above.

Resources