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...
Related
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_())
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.
I am working on a program that will allow someone to enter details in order to write a CV. I am using the Tkinter module (as extra practice) but am already stuck on the menu!
At the moment I have three different options the user can choose: Write CV, Review CV and Exit. I have created a button for each option and when the user presses the button it'll open, however the menu window remains open (there is a different subroutine for each option).
I understand that you need to do something like window.destroy(), however I'm not sure how to give a button two commands without doing something too fiddly like create more subroutines etc.?
The other option I think I'd prefer is is I could clear the menu screen?
Here is the programming I have at the moment:
def Main_Menu():
import tkinter
main_menu = tkinter.Tk()
main_menu.title("CV Writer")
main_menu.geometry("300x300")
main_menu.wm_iconbitmap('cv_icon.ico')
title = tkinter.Label(main_menu, text = "Main Menu", font=("Helvetica",25))
title.pack()
gap = tkinter.Label(main_menu, text = "")
gap.pack()
write_cv = tkinter.Button(main_menu, text = "1) Write CV", font=("Helvetica"), command=Write_CV)
write_cv.pack()
review_cv = tkinter.Button(main_menu, text = "2) Review CV", font=("Helvetica"), command=Review_CV)
review_cv.pack()
leave = tkinter.Button(main_menu, text = "3) Exit", font=("Helvetica"), command=Exit)
leave.pack()
main_menu.mainloop()
def Write_CV():
import tkinter
write_cv = tkinter.Tk()
write_cv.geometry("300x300")
write_cv.title("Write CV")
def Review_CV():
import tkinter
review_cv = tkinter.Tk()
review_cv.geometry("300x300")
review_cv.title("Review CV")
def Exit():
import tkinter
leave = tkinter.Tk()
leave.geometry("300x300")
leave.title("Exit")
Main_Menu()
Running the program should help make this question make more sense!
I am so sorry for the wordy question, but any kind of help would be appreciated! Please bear in mind I am only a GCSE student so simple language would also be so nice! Thank you!
I don't know why are you importing tkinter under each method, it's completely useless. Simply import it once at the beginning of your file with a syntax like this:
import tkinter as tk
So that you can refer to the widgets simply with the duo tk:
btn = tk.Button(None, text='I can simply refer to a widget with tk')
Apart from this, the structure of your program is really bad. In my opinion, you should not instantiate Tk inside your function Main_Menu, because it will only be visible inside it. If you want to refer to the master or root or whatever you want to call the instance of Tk, you can't, because it's a local instance, as I said above.
I usually instantiate Tk in the main function of my program, or in the following if __name__ == '__main__': construct:
if __name__ == '__main__':
master = tk.Tk() # note I am using "tk"
# create your objects or call your functions here
master.mainloop()
Your are creating an instance of Tkin each of your function, that is really a bad practice, never do that. You should only create one instance of Tk for each Tkinter application.
You should use the object-oriented paradigm or make all your widgets global to structure your application.
Except these details, you can simply call master.destroy() when you want to destroy your main window and all its children widgets, where master is the Tk instance.
In general, you have a lot of errors and bad practices. My advice is:
Read a tutorial on Python first and then on Tkinter, before
proceeding.
I try to test drag&drop with simple sequince: mousePress + mouseMove + mouseRelease. But it's not work.
I investigate qtest source and found, that move event tested through main dispatcher processEvent(). Also I found some bugs in qt bug-tracker: 1, 2
So, I think, that it's not possible to test drag&drop under latest stable Qt4. Have anybody success story with this?
I have had no luck simulating drag and drop via the QTest mouse functions, and digia says
they're not adding that functionality to QT4. I implemented drag/drop testing via a method similar to the one suggested in the above link:
create your mime_data, something like:
mime_data = widget_model.mimeData(indexes)
or
mime_data = QMimeData()
mime_data.setText(widget.text())
then use a function like this to drop the data:
def dropOnto(self, widget, mime_data):
action = Qt.CopyAction|Qt.MoveAction
pt = widget.rect().center()
drag_drop = QDropEvent(pt, action, mime_data, Qt.LeftButton, Qt.NoModifier)
drag_drop.acceptProposedAction()
widget.dropEvent(drag_drop)
mouseMoveEvent() handler starts and blocks at QDrag.exec(). In order to simulate the drop, you have to schedule it e.g. with QTimer:
def complete_qdrag_exec():
QTest.mouseMove(drop_target)
QTest.qWait(50)
QTest.mouseClick(drop_target, Qt.MouseButton.LeftButton)
QTimer.singleShot(1000, complete_qdrag_exec)
QTest.mousePress(drag_source, Qt.MouseButton.LeftButton, Qt.KeyboardModifier.NoModifier, QPoint(10, 10))
QTest.mouseMove(drag_source, QPoint(50, 50)) # Ensure distance sufficient for DND start threshol
I noticed an interesting thing - if I add a detailed text to QMessageBox (which adds "Show Details..." button) then executing it will show the system frame's close (X) button disabled and hence marking this window as non-closable (right click on frame -> Close disabled).
Here is some sample code:
QMessageBox box(QMessageBox::Critical, title, text, QMessageBox::Ok);
box.setDetailedText(detailedText); // comment this line to get close button enabled
box.exec();
I didn't even find a way to manually do this in Qt. Any ideas?
Thanks
I was having the same problem with Python 2.7 and PySide.
In this example, the red close button works as expected:
from PySide import QtGui, QtCore
import sys
app = QtGui.QApplication(sys.argv)
message_box = QtGui.QMessageBox()
message_box.setWindowTitle("Close Test")
message_box.setText("Testing whether or not the red X is enabled.")
ret = message_box.exec_()
Adding detailed text disables the close button:
from PySide import QtGui, QtCore
import sys
app = QtGui.QApplication(sys.argv)
message_box = QtGui.QMessageBox()
message_box.setWindowTitle("Close Test")
message_box.setText("Testing whether or not the red X is enabled.")
message_box.setDetailedText("These details disable the close button for some reason.")
ret = message_box.exec_()
The answer marked as the solution does not solve this problem. As you can see in this example, the close button remains disabled:
from PySide import QtGui, QtCore
import sys
app = QtGui.QApplication(sys.argv)
message_box = QtGui.QMessageBox()
message_box.setWindowTitle("Close Test")
message_box.setText("Testing whether or not the red X is enabled.")
message_box.setDetailedText("These details disable the close button for some reason.")
message_box.setWindowFlags(message_box.windowFlags() & ~QtCore.Qt.WindowCloseButtonHint)
ret = message_box.exec_()
The answer is to set the standard buttons and ALSO set the escape button:
from PySide import QtGui, QtCore
import sys
app = QtGui.QApplication(sys.argv)
message_box = QtGui.QMessageBox()
message_box.setWindowTitle("Close Test")
message_box.setText("Testing whether or not the red X is enabled.")
message_box.setDetailedText("These details disable the close button for some reason.")
message_box.setStandardButtons(QtGui.QMessageBox.Ok)
message_box.setDefaultButton(QtGui.QMessageBox.Ok)
message_box.setEscapeButton(QtGui.QMessageBox.Ok)
ret = message_box.exec_()
This restores the desired close button behavior.
I came across this recently on Qt 4.8 Linux. I found that whether or not the X was disabled depended on the ButtonRole I used on the call to QMessageBox::addButton(). The X was disabled when all roles were ActionRole - which is really supposed to be for buttons that affect the dialog, but do not accept or reject it. What the buttons did in my case is more accurately described as AcceptRole or RejectRole. When I changed the roles to have one RejectRole and the rest AcceptRole, the X started working. It looks as though QMessageBox was reluctant to accept a close when none of the buttons had roles that mapped to close.
You need to unset the Qt::WindowCloseButtonHint widget flag. Like this:
QMessageBox messageBox;
messageBox.setWindowFlags(messageBox.windowFlags() & ~Qt::WindowCloseButtonHint);
You may unset this flag Qt::WindowSystemMenuHint either.
Adds a window system menu, and possibly a close button (for example on
Mac). If you need to hide or show a close button, it is more portable
to use WindowCloseButtonHint.
http://qt-project.org/doc/qt-4.8/qt.html#WindowType-enum