Best approach to show / hide dialogs - qt

I have encountered a theoretical question. I'm using pyqt5, but this is probably are very generalistic and framework independent question.
I have a QMainwindow sitting around waiting for the user to do stuff. The user can show / hide dialogues (subclasses of QDockwidgets) as he chooses using the QMenu and the associated shortcuts (it's a checkable QAction for each individual dialogue).
I have been struggling with showing / hiding the dialogues efficiently. Currently, I'm just initiating them all at start up, hiding those that I don't want to show up in the beginning. This makes triggering the dialogues easy, since I can just dialogue.show() /dialogue.hide() depending on the dialogues current visibility.
But I cannot believe that this is best practice and very efficient.
I have tried (I currently do not have my pyqt environment set up on this computer, so I had to strip down my actual code without being able to test if this runs):
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
class InfoPanel(QDockWidget):
def __init__(self, title='Tool Box'):
QDockWidget.__init__(self, title)
self.setFeatures(QDockWidget.DockWidgetFloatable | QDockWidget.DockWidgetMovable | QDockWidget.DockWidgetClosable)
self.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
frame = QFrame()
layout = QGridLayout()
self.canvas = QGraphicsView()
self.canvas.setBackgroundBrush(QtGui.QBrush(QtGui.QColor(40, 40, 40)))
layout.addWidget(self.canvas)
frame.setLayout(layout)
self.setWidget(frame)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.showpanelAct = QAction("&Show Panel", self, enabled=True,checkable=True, shortcut="F10")
self.showpanelAct.triggered.connect(lambda: self.showPanel(0))
self.viewMenu = QMenu("&View", self)
self.viewMenu.addAction(self.showpanelAct)
self.setDockOptions(QMainWindow.AnimatedDocks)
def showPanel(self,i:int = 0): # this is not so smart - should construct and deconstuct to save memory!?
if i == 0: #infopanel
dialogueExists = True
try: self.infoPanel
#except NameError: #does not catch the error
except:
dialogueExists = False
if dialogueExists:
print('destroy')
self.infoPanel.destroy()
else:
print('create')
self.infoPanel = InfoPanel() #init
self.infoPanel.show()
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()
Which works the first time, but after that, it only seems to trigger the destruction of the dialogue (which, surprisingly, does not crash anything it just keeps on going).
Why is that and is there a standard way to approach the showing hiding of dialogues?

I took the exposed MCVE of OP and tried to make it running in my cygwin64 on Windows 10.
At first I had to apply little fixes. (OP stated that he was not able to test it at the time of publishing.)
First, I inserted a “hut” at first line for convenient start in bash:
#!/usr/bin/python3
Second, the self.viewMenu didn't appear. Hence, I inserted a line after
self.viewMenu = QMenu("&View", self)
self.viewMenu.addAction(self.showpanelAct)
to add the viewMenu to main menu bar:
self.menuBar().addMenu(self.viewMenu)
which fixed it.
Third, when clicking the menu item I got:
Traceback (most recent call last):
File "./testQDockPanelShowHide.py", line 27, in <lambda>
self.showpanelAct.triggered.connect(lambda: self.showPanel(0))
File "./testQDockPanelShowHide.py", line 45, in showPanel
self.infoPanel = InfoPanel() #init
File "./testQDockPanelShowHide.py", line 17, in __init__
self.canvas.setBackgroundBrush(QtGui.QBrush(QtGui.QColor(40, 40, 40)))
NameError: name 'QtGui' is not defined
Aborted (core dumped)
I must admit that my Python knowledge is very limited. (I'm the guy who writes the Python bindings in C++ for the colleagues. So, my colleagues are the actual experts. At most, I play a little bit in Python when I test whether new implemented bindings do what's expected.) However, I modified
self.canvas.setBackgroundBrush(QtGui.QBrush(QtGui.QColor(40, 40, 40)))
to:
self.canvas.setBackgroundBrush(QBrush(QColor(40, 40, 40)))
which fixed this issue.
After this, I got the behavior described by OP and did a closer look where I (and OP) suspected the error:
def showPanel(self,i:int = 0): # this is not so smart - should construct and deconstuct to save memory!?
if i == 0: #infopanel
dialogueExists = True
try: self.infoPanel
#except NameError: #does not catch the error
except:
dialogueExists = False
if dialogueExists:
print('destroy')
self.infoPanel.destroy()
else:
print('create')
self.infoPanel = InfoPanel() #init
self.infoPanel.show()
I strongly believe that try: self.infoPanel doesn't do what OP thinks it would.
It tries to access self.infoPanel which isn't existing until the first call of this method. (Please, be aware, the member variable self.infoPanel isn't existing.) So, the except: branch is executed and sets dialogueExists = False which a few lines later causes self.infoPanel = InfoPanel() #init. Now, the member variable self.infoPanel is existing, and the try: self.infoPanel will never fail again until destruction of this MainWindow.
Out of curiosity, I had a look at QWidget.destroy() (to be sure not to tell something wrong):
QWidget.destroy (self, bool destroyWindow = True, bool destroySubWindows = True)
Frees up window system resources. Destroys the widget window if destroyWindow is true.
destroy() calls itself recursively for all the child widgets, passing destroySubWindows for the destroyWindow parameter. To have more control over destruction of subwidgets, destroy subwidgets selectively first.
This function is usually called from the QWidget destructor.
It definitely doesn't destroy the member variable self.infoPanel.
After having understood this, a fix was easy and obvious:
def showPanel(self,i:int = 0): # this is not so smart - should construct and deconstuct to save memory!?
if i == 0: #infopanel
try: self.infoPanel
#except NameError: #does not catch the error
except:
print('create')
self.infoPanel = InfoPanel() #init
if self.infoPanel.isVisible():
self.infoPanel.hide()
else:
self.infoPanel.show()
Btw. I replaced destroy() by hide() which makes a re-creation of the InfoPanel() obsolete.
I tested this by toggling the menu item multiple times – it works as expected now (at least, it looks like).
The complete sample finally:
#!/usr/bin/python3
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
class InfoPanel(QDockWidget):
def __init__(self, title='Tool Box'):
QDockWidget.__init__(self, title)
self.setFeatures(QDockWidget.DockWidgetFloatable | QDockWidget.DockWidgetMovable | QDockWidget.DockWidgetClosable)
self.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
frame = QFrame()
layout = QGridLayout()
self.canvas = QGraphicsView()
# self.canvas.setBackgroundBrush(QtGui.QBrush(QtGui.QColor(40, 40, 40)))
self.canvas.setBackgroundBrush(QBrush(QColor(40, 40, 40)))
layout.addWidget(self.canvas)
frame.setLayout(layout)
self.setWidget(frame)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.showpanelAct = QAction("&Show Panel", self, enabled=True,checkable=True, shortcut="F10")
self.showpanelAct.triggered.connect(lambda: self.showPanel(0))
self.viewMenu = QMenu("&View", self)
self.viewMenu.addAction(self.showpanelAct)
self.menuBar().addMenu(self.viewMenu)
self.setDockOptions(QMainWindow.AnimatedDocks)
def showPanel(self,i:int = 0): # this is not so smart - should construct and deconstuct to save memory!?
if i == 0: #infopanel
try: self.infoPanel
#except NameError: #does not catch the error
except:
print('create')
self.infoPanel = InfoPanel() #init
if self.infoPanel.isVisible():
self.infoPanel.hide()
else:
self.infoPanel.show()
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()

After taking a break from coding the solution to my problem was very obvious. Going back to my original code (which did not produce the expected output of creating / destroying the dialogue self.infoPanel on demand):
dialogueExists = True
try: self.infoPanel
#except NameError: #does not catch the error
except:
dialogueExists = False
if dialogueExists:
print('destroy')
self.infoPanel.destroy()
else:
print('create')
self.infoPanel = InfoPanel() #init
self.infoPanel.show()
My main problem was that I confused two separate things. Qt destroyed the widget contained in the object self.infoPanel when I called self.infoPanel.destroy(). But that doesn't mean the object self.infoPanel does not exist (that's exactly what I use try: ... for, to see if the object exists). The simple and obvious way to create and destroy dialogues on demand obviously involves deleting the object from the environment (del self.infoPanel).
The working code is:
dialogueExists = True
try:
self.infoPanel.destroy() #not sure this is needed, but I guess it doesn't hurt
del self.infoPanel #this is the deletion of the actual object
except:
dialogueExists = False
if not dialogueExists :
self.infoPanel = InfoPanel()
Cheers and many thanks for the helpful advice on deciding whether to show / hide dialogues or to create / destroy them!

Related

PyQt5 - encapsulate / make a component reusable - example: QTimer/QLabel implementation outside of parent class

I implement a label that displays the current time in several of my PyQt5 applications. Here is a MRE:
import sys
import logging
from PyQt5 import QtWidgets, QtCore, QtGui
__log__ = logging.getLogger()
class App(QtWidgets.QApplication):
def __init__(self, sys_argv):
super(App, self).__init__(sys_argv)
self.main_view = MainView()
self.main_view.show()
class MainView(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.setObjectName("MreUi")
self.resize(300, 100)
self.setWindowTitle('MreUi')
self.label = QtWidgets.QLabel()
self.setCentralWidget(self.label)
config_clock(self.label)
self.start_clocks()
def start_clocks(self):
timer = QtCore.QTimer(self)
timer.timeout.connect(self.show_time)
timer.start(1000)
def show_time(self):
current_time = QtCore.QTime.currentTime()
clock_label_time = current_time.toString('hh:mm:ss')
self.label.setText(clock_label_time)
def config_clock(label):
label.setAlignment(QtCore.Qt.AlignCenter)
font = QtGui.QFont('Arial', 24, QtGui.QFont.Bold)
label.setFont(font)
if __name__ == '__main__':
logging.basicConfig()
app = App(sys.argv)
try:
sys.exit(app.exec_())
except Exception as e:
__log__.error('%s', e)
As I implemented a similar clock in several of my PyQt apps, I thought it would be nice to implement it as a component / encapsulate it. First I thought of doing this by calling a config_clock function from any QWidget, and have that function do ~ALL of the work implementing the clock for the specified label. This would avoid having to repeat myself in multiple applications from writing/calling start_clocks and show_time instance methods of MainView. but as I started to code that ...
# from inside my QWidget:
config_clock(self.label)
# this function would live outisde the class, thus reusable by diff Qt apps:
def config_clock(label):
# start the clock
# set default font, etc for label
# instantiate QtCore.QTimer
# # but that's when I realized I've always passed self to QtCore.QTimer and that maybe encapsulating this isn't as trivial as I thought.
Should I create some kind of ClockLabel() object of my own that gets passed a QtWidget's label and can also be an instance attribute of each QtWidget that might need it? That smells kind of clunky to me. But surely there must be a way to make a 'reusable component' in PyQt, I just don't know how...
I also am not certain if the MainView(QtWidgets.QMainWindow) could rightly be referred to as the 'parent class' if I were to pass it as a parameter to a ClockLabel() class I write or a config_clock function whose signature could look like:
def config_clock(label, parent_qtwidget):
# also feels clunky and not sure if parent would be the right term
Thanks
With QtWidgets it is normal to specialize widgets by inheritance. Here is an example of how you might rearrange your code to produce a reusable widget:
class ClockLabel(QtWidgets.QLabel):
def __init__(self, parent=None):
super().__init__(parent)
self.setAlignment(QtCore.Qt.AlignCenter)
font = QtGui.QFont('Arial', 24, QtGui.QFont.Bold)
self.setFont(font)
self._timer = QtCore.QTimer(self)
self._timer.timeout.connect(self._show_time)
def start(self):
self._timer.start(1000)
def stop(self):
self._timer.stop()
def _show_time(self):
current_time = QtCore.QTime.currentTime()
clock_label_time = current_time.toString('hh:mm:ss')
self.setText(clock_label_time)

How to reimplement a QDialog's accept and reject slots?

I'm working on a GUI project where the user is faced with the following QDialog:
class StockSelectorDialog(QDialog, stockselector_ui):
def __init__(self, parent_, *args, **kwargs):
super(StockSelectorDialog, self).__init__(*args, **kwargs)
self.setAttribute(Qt.WA_DeleteOnClose, on=True)
self.setupUi(self)
self.dialogButtonBox.accepted.connect(self.accept)
self.dialogButtonBox.rejected.connect(self.reject)
self.parent_ = parent_
self.symbolsbuffer = parent_.symbols.copy()
self.symbolmodel = SymbolListModel(self)
self.listView.setModel(self.symbolmodel)
self.symbolAddButton.clicked.connect(self.onAddButtonClicked)
self.symbolDeleteButton.clicked.connect(self.onDeleteButtonClicked)
def onAddButtonClicked(self, s):
symbol = self.symbolEdit.text()
if symbol:
self.symbolsbuffer.append(symbol)
self.symbolmodel.layoutChanged.emit()
self.symbolEdit.setText("")
def onDeleteButtonClicked(self, s):
indexes = self.listView.selectedIndexes()
if indexes:
for i in indexes:
del self.symbolsbuffer[i.row()]
self.symbolmodel.layoutChanged.emit()
self.listView.clearSelection()
def accept(self):
self.parent_.symbols = self.symbolsbuffer
self.parent_.onSymbolsChanged()
self.close()
def reject(self):
self.close()
The UI file is here: https://github.com/danib-prog/stockmarket-helper/blob/master/stockselector.ui
Everything worked fine until I added the buffer sysytem, for which I had to reimplement the accept and the reject slots of the dialog (although I'm not very sure about the latter). Now the dialog opens when necessary and the ListView works fine with all the buttons and the LineEdit, but my dialogButtonBox is not responding.
Why is this happening? And what is a solution to the problem?
You should not call close() on a dialog for that, mostly because it causes itself a call to reject(); luckily Qt is smart enough to prevent recursion, but the point remains: both those methods are expected to set the dialog's result and use done(), of close(), so that their event loop correctly exits from its exec_().
If you need to override a class function to do something other than the default behavior, you should always remember to call the base implementation too in order to correctly achieve the expected result.
def accept(self):
self.parent_.symbols = self.symbolsbuffer
self.parent_.onSymbolsChanged()
super(StockSelectorDialog, self).accept()
def reject(self):
super(StockSelectorDialog, self).reject()
Also, note that Qt Designer already connects the buttons of a QDialogButtonBox when it's created using the default dialog templates with buttons, so you should not connect them again, otherwise both accept and reject would be called twice.

Tkinter TTK OptionMenu won't open

Please see my edit at the bottom, this issue is now OS specific.
A gif of the problem in action
So I'm having an issue with an instance of ttk.OptionMenu. I've successfully implemented the widget before, however I'm trying to use it here as a dropdown for available files in a pop-up window, and I can't seem to get it to work right.
The issue
Only the first option in the menu is seen, and no other options are available
It doesn't respond to the mouse other than dimming when clicked
When it is clicked, no errors or output are seen in the terminal, making it hard to trace
The Code
The actual call is made from another file, let's say myproject/main.py
from classes.load_window import *
start_load_menu()
The class for this is stored in a file at myproject/classes/load_window.py, and it accesses save files stored in myproject/saved/
import tkinter
import tkinter.ttk as ttk
from os import listdir
from os.path import join, isfile
class LoadMenu(object):
def __init__(self):
root = self.root = tkinter.Tk()
root.title("Save Manager")
root.overrideredirect(True)
""" MAIN FRAME """
frm_1 = ttk.Frame(root)
frm_1.pack(ipadx=2, ipady=2)
""" MESSAGE LABEL """
self.msg = str("Would you like to load from a save file?")
message = ttk.Label(frm_1, text=self.msg)
message.pack(padx=8, pady=8)
""" INNER FRAME """
frm_2 = ttk.Frame(frm_1)
frm_2.pack(padx=4, pady=4)
""" TEST IMPLEMENTAITON [DOES NOT WORK] """
mylist = ['1', '2', '3', '4', '5', '6', '7']
test_var = tkinter.StringVar(frm_2)
test_var.set(mylist[3])
test_dropdown = ttk.OptionMenu(frm_2, test_var, *mylist)
test_dropdown.pack(padx=4, pady=4)
print(mylist) # Results in ['1', '2', '3', '4', '5', '6', '7']
""" REAL IMPLEMENTATION [ALSO DOES NOT WORK] """
files = [f for f in listdir('saved') if isfile(join('saved', f))]
file_var = tkinter.StringVar(frm_2)
file_var.set(files[3])
file_dropdown = ttk.OptionMenu(frm_2, file_var, *files)
file_dropdown.pack(padx=4, pady=4)
print(files) # Results in ['DS_Store', 'test1', 'test2', 'test3']
""" BUTTON FUNCTIONALITY """
btn_1 = ttk.Button(frm_2, width=8, text="Load File")
btn_1['command'] = self.b1_action
btn_1.pack(side='left')
btn_2 = ttk.Button(frm_2, width=8, text="Cancel")
btn_2['command'] = self.b2_action
btn_2.pack(side='left')
btn_3 = ttk.Button(frm_2, width=8, text="Create New")
btn_3['command'] = self.b3_action
btn_3.pack(side='left')
btn_2.bind('<KeyPress-Return>', func=self.b3_action)
root.update_idletasks()
""" Position the window """
xp = (root.winfo_screenwidth() // 2) - (root.winfo_width() // 2)
yp = (root.winfo_screenheight() // 2) - (root.winfo_height() // 2)
geom = (root.winfo_width(), root.winfo_height(), xp, yp)
root.geometry('{0}x{1}+{2}+{3}'.format(*geom))
root.protocol("WM_DELETE_WINDOW", self.close_mod)
root.deiconify()
def b1_action(self, event=None):
print("B1")
def b2_action(self, event=None):
self.root.quit()
def b3_action(self, event=None):
print("B3")
def nothing(self):
print("nothing")
def close_mod(self):
pass
def time_out(self):
print ("TIMEOUT")
def to_clip(self, event=None):
self.root.clipboard_clear()
self.root.clipboard_append(self.msg)
def start_load_menu():
menu = LoadMenu()
menu.root.mainloop()
menu.root.destroy()
return menu.returning
Notes
This code is based on a response here for a pop up window that I'm in the process of adapting for a specific purpose (the load menu).
I distilled this code to the minimum to reproduce the issue, but you can probably ignore the function definitions and window geometry.
Everything works fine other than this; the window is displayed center screen, and the button with actual functionality closes the window, it's just this odd quirk with the OptionMenu that I can't seem to find anyone else struggling with, either on here, or other forums.
In case you didn't see the link at the top, you can find a demonstration of the troublesome behavior at this link.
I'm using Python 3.6.4 on OSX 10.12.6
EDIT:
I've since tested this code in a VM running Hydrogen Linux, and it works fine. My question then changes a little:
How can I ensure that this code translates well to OSX? Is there reading available on the discrepancies between running TKinter on different platforms?
I have found this page on the issues regarding Python, TKinter, and OSX, but even when using the recommended TCL packages with the latest stable release of Python, this issue persists.
EDIT 2:
Just to update, I have since found a workaround for the problem. It doesn't answer the question of the odd behavior of the OptionMenu, but I figured I would edit. Honestly, I think Listbox is probably better suited for what I wanted to do anyways. Here it is in action.
Please let me know if I need to make any edits for clarity, or provide additional info. As I'm new to stackoverflow, I don't have much experience sharing issues here. Thank you!
Doing nothing other than changing "files" to a hard-coded list allows the program to run on my machine. I can help you no further.
import tkinter
import tkinter.ttk as ttk
from os import listdir
from os.path import join, isfile
class LoadMenu(object):
def __init__(self):
root = self.root = tkinter.Tk()
root.title("Save Manager")
root.overrideredirect(True)
""" MAIN FRAME """
frm_1 = ttk.Frame(root)
frm_1.pack(ipadx=2, ipady=2)
""" MESSAGE LABEL """
self.msg = str("Would you like to load from a save file?")
message = ttk.Label(frm_1, text=self.msg)
message.pack(padx=8, pady=8)
""" INNER FRAME """
frm_2 = ttk.Frame(frm_1)
frm_2.pack(padx=4, pady=4)
""" TEST IMPLEMENTAITON [DOES NOT WORK] """
mylist = ['1', '2', '3', '4', '5', '6', '7']
test_var = tkinter.StringVar(frm_2)
test_var.set(mylist[3])
test_dropdown = ttk.OptionMenu(frm_2, test_var, *mylist)
test_dropdown.pack(padx=4, pady=4)
print(mylist) # Results in ['1', '2', '3', '4', '5', '6', '7']
""" REAL IMPLEMENTATION [ALSO DOES NOT WORK] """
##files = [f for f in listdir('saved') if isfile(join('saved', f))]
files=['a', 'b', 'c', 'd', 'e', 'f']
file_var = tkinter.StringVar(frm_2)
file_var.set(files[3])
file_dropdown = ttk.OptionMenu(frm_2, file_var, *files)
file_dropdown.pack(padx=4, pady=4)
print(files) # Results in ['DS_Store', 'test1', 'test2', 'test3']
""" BUTTON FUNCTIONALITY """
btn_1 = ttk.Button(frm_2, width=8, text="Load File")
btn_1['command'] = self.b1_action
btn_1.pack(side='left')
btn_2 = ttk.Button(frm_2, width=8, text="Cancel")
btn_2['command'] = self.b2_action
btn_2.pack(side='left')
btn_3 = ttk.Button(frm_2, width=8, text="Create New")
btn_3['command'] = self.b3_action
btn_3.pack(side='left')
btn_2.bind('<KeyPress-Return>', func=self.b3_action)
root.update_idletasks()
""" Position the window """
xp = (root.winfo_screenwidth() // 2) - (root.winfo_width() // 2)
yp = (root.winfo_screenheight() // 2) - (root.winfo_height() // 2)
geom = (root.winfo_width(), root.winfo_height(), xp, yp)
root.geometry('{0}x{1}+{2}+{3}'.format(*geom))
root.protocol("WM_DELETE_WINDOW", self.close_mod)
root.deiconify()
def b1_action(self, event=None):
print("B1")
def b2_action(self, event=None):
self.root.quit()
def b3_action(self, event=None):
print("B3")
def nothing(self):
print("nothing")
def close_mod(self):
pass
def time_out(self):
print ("TIMEOUT")
def to_clip(self, event=None):
self.root.clipboard_clear()
self.root.clipboard_append(self.msg)
##def start_load_menu():
menu = LoadMenu()
menu.root.mainloop()
## menu.root.destroy()
## return menu.returning
I figured it out!
After digging into this problem further, I distilled the code to the minimum (which I probably should have done before posting here...) and was able to pinpoint root.overrideredirect(True) as the offending line.
When using overrideredirect(True), it's necessary to also use update_idletasks() before, in order to ensure that the Widget refreshes properly. While it seems Linux can still produce normal behavior without manually updating idle tasks, OS X cannot, so it becomes necessary to preface the code with
root.update_idletasks()
Here's a good excerpt from the documentation that I found on Billal BEGUERADJ's response on an overrideredirect() question.
If you want to force the display to be updated before the application next idles, call the w.update_idletasks() method on any widget.
Some tasks in updating the display, such as resizing and redrawing widgets, are called idle tasks because they are usually deferred until the application has finished handling events and has gone back to the main loop to wait for new events.
If you want to force the display to be updated before the application next idles, call the w.update_idletasks() method on any widget.
While I still don't understand why exactly this widget breaks without update_idletasks() on OSX, I do understand now why it's good practice to use update_idletasks() in conjunction with overrideredirect() to ensure consistent behavior.
Hope this helps anyone else who may get hung up on this.

QTextEdit shift-tab wrong behaviour

shift+tab behaves as tab in QTextEdit/QPlainTextEdit.
Looks like a common problem with no good solution.
Is there any "classical" way to enable this functionality when tab increases indentation level and shift-tab decreases it?
This is a bit of an old question, but I got this figured out.
You just need to reimplement QPlainTextEdit (or QTextEdit) with your own class that inherits from it, and override the keyPressEvent.
By default a tab inserts a tabstop, but the below code catches a Qt.Key_Backtab event, which as near as I can tell is the event that occurs when you press Shift+Tab.
I tried and failed to catch Qt.Key_Tab and a Qt.Key_Shift or Qt.Key_Tab and a Shift modifier, so this must be the way to do it.
import sys
from PyQt4 import QtCore, QtGui
class TabPlainTextEdit(QtGui.QTextEdit):
def __init__(self,parent):
QtGui.QTextEdit.__init__(self, parent)
def keyPressEvent(self, event):
if event.key() == QtCore.Qt.Key_Backtab:
cur = self.textCursor()
# Copy the current selection
pos = cur.position() # Where a selection ends
anchor = cur.anchor() # Where a selection starts (can be the same as above)
# Can put QtGui.QTextCursor.MoveAnchor as the 2nd arg, but this is the default
cur.setPosition(pos)
# Move the position back one, selection the character prior to the original position
cur.setPosition(pos-1,QtGui.QTextCursor.KeepAnchor)
if str(cur.selectedText()) == "\t":
# The prior character is a tab, so delete the selection
cur.removeSelectedText()
# Reposition the cursor with the one character offset
cur.setPosition(anchor-1)
cur.setPosition(pos-1,QtGui.QTextCursor.KeepAnchor)
else:
# Try all of the above, looking before the anchor (This helps if the achor is before a tab)
cur.setPosition(anchor)
cur.setPosition(anchor-1,QtGui.QTextCursor.KeepAnchor)
if str(cur.selectedText()) == "\t":
cur.removeSelectedText()
cur.setPosition(anchor-1)
cur.setPosition(pos-1,QtGui.QTextCursor.KeepAnchor)
else:
# Its not a tab, so reset the selection to what it was
cur.setPosition(anchor)
cur.setPosition(pos,QtGui.QTextCursor.KeepAnchor)
else:
return QtGui.QTextEdit.keyPressEvent(self, event)
def main():
app = QtGui.QApplication(sys.argv)
w = TabPlainTextEdit(None)
w.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
I'm still refining this, but the rest of the code is on GitHub.

How do I respond to an internal drag-and-drop operation using a QListWidget?

I've got a Qt4 application (using the PyQt bindings) which contains a QListWidget, initialized like so:
class MyList(QtGui.QListWidget):
def __init__(self):
QtGui.QListWidget.__init__(self)
self.setDragDropMode(self.InternalMove)
I can add items, and this allows me to drag and drop to reorder the list. But how do I get notification when the list gets reordered by the user? I tried adding a dropMimeData(self, index, data, action) method to the class, but it never gets called.
I have an easier way. :)
You can actually access the listwidget's internal model with myList->model() - and from there there are lots of signals available.
If you only care about drag&drop, connect to layoutChanged.
If you have move buttons (which usually are implemented with remove+add) connect to rowsInserted too.
If you want to know what moved, rowsMoved might be better than layoutChanged.
I just had to deal with this and it's a pain in the ass but here's what to do:
You have to install an eventFilter on your ListWidget subclass and then watch for the ChildRemoved event. This event covers moves as well as removal, so it should work for re-arranging items with drag and drop inside a list.
I write my Qt in C++, but here's a pythonification version:
class MyList(QtGui.QListWidget):
def __init__(self):
QtGui.QListWidget.__init__(self)
self.setDragDropMode(self.InternalMove)
self.installEventFilter(self)
def eventFilter(self, sender, event):
if (event.type() == QEvent.ChildRemoved):
self.on_order_changed()
return False # don't actually interrupt anything
def on_order_changed(self):
# do magic things with our new-found knowledge
If you have some other class that contains this list, you may want to move the event filter method there. Hope this helps, I know I had to fight with this for a day before figuring this out.
I found Trey Stout's answer did work however I was obviously getting events when the list order had not actually changed. I turned to Chani's answer which does work as required but with no code it took me a little work to implement in python.
I thought I would share the code snippet to help out future visitors:
class MyList(QListWidget):
def __init__(self):
QListWidget.__init__(self)
self.setDragDropMode(self.InternalMove)
list_model = self.model()
list_model.layoutChanged.connect(self.on_layout_changed)
def on_layout_changed(self):
print "Layout Changed"
This is tested in PySide but see no reason it wouldn't work in PyQt.
I know this is old, but I was able to get my code to work using Trey's answer and wanted to share my python solution. This is for a QListWidget inside a QDialog, not one that is sub-classed.
class NotesDialog(QtGui.QDialog):
def __init__(self, notes_list, notes_dir):
QtGui.QDialog.__init__(self)
self.ui=Ui_NotesDialog()
# the notesList QListWidget is created here (from Qt Designer)
self.ui.setupUi(self)
# install an event filter to catch internal QListWidget drop events
self.ui.notesList.installEventFilter(self)
def eventFilter(self, sender, event):
# this is the function that processes internal drop in notesList
if event.type() == QtCore.QEvent.ChildRemoved:
self.update_views() # do something
return False # don't actually interrupt anything
Not a solution, but some ideas:
You should probably check what is returned by supportedDropActions method. It might be that you need to overwrite that method, to include Qt::MoveAction or Qt::CopyAction.
You have QListView::indexesMoved signal, but I am not sure whether it will be emitted if you're using QListWidget. It worths checking.
The QListWidget.model() approach seemed the most elegant of the proposed solutions but did not work for me in PyQt5. I don't know why, but perhaps something changed in the move to Qt5. The eventFilter approach did work, but there is another alternative that is worth considering: over-riding the QDropEvent and checking if event.source is self. See the code below which is an MVCE with all of the proposed solutions coded in for checking in PyQt5:
import sys
from PyQt5 import QtGui, QtWidgets, QtCore
class MyList(QtWidgets.QListWidget):
itemMoved = QtCore.pyqtSignal()
def __init__(self):
super(MyList, self).__init__()
self.setDragDropMode(self.InternalMove)
list_model = self.model()
# list_model.layoutChanged.connect(self.onLayoutChanged) # doesn't work
# self.installEventFilter(self) # works
self.itemMoved.connect(self.onLayoutChanged) # works
def onLayoutChanged(self):
print("Layout Changed")
def eventFilter(self, sender, event):
"""
Parameters
----------
sender : object
event : QtCore.QEvent
"""
if event.type() == QtCore.QEvent.ChildRemoved:
self.onLayoutChanged()
return False
def dropEvent(self, QDropEvent):
"""
Parameters
----------
QDropEvent : QtGui.QDropEvent
"""
mime = QDropEvent.mimeData() # type: QtCore.QMimeData
source = QDropEvent.source()
if source is self:
super(MyList, self).dropEvent(QDropEvent)
self.itemMoved.emit()
app = QtWidgets.QApplication([])
form = MyList()
for text in ("one", "two", "three"):
item = QtWidgets.QListWidgetItem(text)
item.setFlags(item.flags() | QtCore.Qt.ItemIsUserCheckable)
item.setFlags(item.flags() | QtCore.Qt.ItemIsEditable)
item.setCheckState(QtCore.Qt.Checked)
form.addItem(item)
form.show()
sys.exit(app.exec_())

Resources