I have a QCompleter on a QTableWidget column. As soon as the user starts editing I would like the completer to pop up, not waiting for them to enter text first. I subclassed the setEditorData function of the QStyledItemDelegate to do this which seems to make the most sense to me, however when I call completer.complete() nothing happens until I finish editing (at which point the popup fires).
Here is my code for the delegate:
class CompleterItemDelegate(QtGui.QStyledItemDelegate):
def createEditor(self, parent, option, index):
completer = QtGui.QCompleter(['test', 'test2'])
completer.setCompletionMode(completer.UnfilteredPopupCompletion)
edit = QtGui.QLineEdit(parent)
edit.setCompleter(completer)
return edit
def setEditorData(self, editor, index):
completer = editor.completer()
completer.complete() # does not fire until after editing is done
completer.popup().show() # no luck here either
print("setting editor data") # this however does work as expected...
super().setEditorData(editor, index)
You have to call complete() when the widget is displayed and for this you can use the showEvent() method:
from PyQt4 import QtCore, QtGui
class LineEdit(QtGui.QLineEdit):
def showEvent(self, event):
if self.completer() is not None:
QtCore.QTimer.singleShot(0, self.completer().complete)
super().showEvent(event)
class CompleterItemDelegate(QtGui.QStyledItemDelegate):
def createEditor(self, parent, option, index):
completer = QtGui.QCompleter(["test", "test2"])
completer.setCompletionMode(QtGui.QCompleter.UnfilteredPopupCompletion)
edit = LineEdit(parent)
edit.setCompleter(completer)
return edit
def main(args):
app = QtGui.QApplication(args)
w = QtGui.QTableWidget(4, 4)
delegate = CompleterItemDelegate(w)
w.setItemDelegate(delegate)
w.show()
ret = app.exec_()
return ret
if __name__ == "__main__":
import sys
sys.exit(main(sys.argv))
Related
I want to set the background in dim mode, when a QMessagebox is popped up.
Currently, I have tried to use a simple QMesssagebox, but the background shows as normal display, when it pops up.
The image for 1st page is as follow
When go to next slide is pushed, it goes to next index as follow
When going back to 1st index, the back button is pushed which pops up the messagebox as follow
However, the mainwindow seems to have no effect on its focus.
Therefore, what would I need to do to make it dimmer than the focused messagebox.
How can I do this? Any suggestions?
EDIT
import sys
from PyQt5 import uic
from PyQt5.QtWidgets import QApplication, QMainWindow, QMessageBox
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.ui = uic.loadUi("message.ui",self)
self.notification = QMessageBox()
self.ui.next_slide.clicked.connect(self.second_index)
self.ui.go_back.clicked.connect(self.alert_msg)
self.show()
def home(self):
self.ui.stackedWidget.setCurrentIndex(0)
def second_index(self):
self.ui.stackedWidget.setCurrentIndex(1)
def alert_msg(self):
self.notification.setWindowTitle("Exiting")
self.notification.setText("Are you sure, you want to exit")
self.notification.setIcon(QMessageBox.Critical)
self.notification.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
self.back = self.notification.exec_()
if self.back == QMessageBox.Yes:
self.home()
else:
pass
if __name__ == "__main__":
app=QApplication(sys.argv)
mainwindow=MainWindow()
app.exec_()
You can create a custom widget that is a direct child of the window that has to be "dimmed", ensure that it always has the same size as that window, and just paint it with the selected color:
class Dimmer(QWidget):
def __init__(self, parent):
parent = parent.window()
super().__init__(parent)
parent.installEventFilter(self)
self.setAttribute(Qt.WA_DeleteOnClose)
self.adaptToParent()
self.show()
def adaptToParent(self):
self.setGeometry(self.parent().rect())
def eventFilter(self, obj, event):
if event.type() == event.Resize:
self.adaptToParent()
return super().eventFilter(obj, event)
def paintEvent(self, event):
qp = QPainter(self)
qp.fillRect(self.rect(), QColor(127, 127, 127, 127))
class MainWindow(QMainWindow):
# ...
def alert_msg(self):
dimmer = Dimmer(self)
# ...
self.back = self.notification.exec_()
dimmer.close()
Note that, unless you plan to reuse the "dim widget", it must be destroyed either by calling close() as done above (see the WA_DeleteOnClose flag) or using deleteLater(). Hiding it will not be enough.
Simple solution may be:
self.placeholder_text = "......."
...
...
#trigger the following code when enabled state of self.widget_name is changed
if(self.widget_name.isEnabled()):
self.widget_name.setPlaceholderText(self.placeholder_text)
else:
self.widget_name.setPlaceholderText("")
But i have a lot of QLineEdit widgets, so i searching a solution to grap all the cases.
A simple solution could be to cycle through all QLineEdit children:
for lineEdit in self.findChildren(QtWidgets.QLineEdit):
if lineEdit.isEnabled():
lineEdit.setPlaceholderText(self.placeholder_text)
else:
lineEdit.setPlaceholderText("")
But that's probably not a good approach, as you have to constantly check for all widgets, and there might be some line edits for which you don't want this behavior.
A better solution could be to subclass the line edit and override its changeEvent():
class MyLineEdit(QtWidgets.QLineEdit):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._placeholderText = self.placeholderText()
def setPlaceholderText(self, text):
self._placeholderText = text
if self.isEnabled():
super().setPlaceholderText(text)
def changeEvent(self, event):
if event.type() == QtCore.QEvent.EnabledChange:
super().setPlaceholderText(
self._placeholderText if self.isEnabled() else '')
return super().changeEvent(event)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
w = QtWidgets.QWidget()
layout = QtWidgets.QVBoxLayout(w)
checkBox = QtWidgets.QCheckBox('Toggle enabled')
layout.addWidget(checkBox)
checkBox.setChecked(True)
lineEdit = MyLineEdit(placeholderText='placeholder')
layout.addWidget(lineEdit)
checkBox.toggled.connect(lineEdit.setEnabled)
w.show()
sys.exit(app.exec_())
The above code works both for line edits created with the placeholderText added to the constructor (like in the example) and with promoted widgets used in Designer.
Alternatively, you can add an event filter to all line edits for which you want to enable this feature.
self.widget_name.installEventFilter(self)
self.some_other_widget.installEventFilter(self)
# ...
def eventFilter(self, source, event):
if (isinstance(source, QtWidgets.QLineEdit) and
event.type() == QtCore.QEvent.EnabledChange):
source.setPlaceholderText(
self.placeholder_text if source.isEnabled() else '')
return super().eventFilter(source, event)
I want to enable drag and drop functionality in some custom widgets, to allow reordering and rearranging the widgets within a layout. The basic functionality is working, but I'd like to prevent dropping a widget on itself, or on a child of itself. This is straightforward in the dropEvent method, but I can't find a way to also show the "forbidden" cursor while dragging, such that the user is aware that a drop will not be allowed.
The example below shows a test implementation, where the widgets "One" to "Five" can be dragged and dropped (no rearrangement will happen, only a message will be printed on the terminal, you may need to press Ctrl or something to initiate a drag). The problem line is the first ev.accept() in the dragEnterEvent method. By accepting the event, the cursor is shown in an "allowed" state (a grabbing hand for me). For example, trying to drag "One" and drop it onto "Three" appears as allowed, although nothing will happen. However, ignoring the event results in the event being propagated to the parent, so in that case dragging "Three" and dropping it onto "Three" results in "One" getting the drop instead. Setting the WA_NoMousePropagation attribute does not seem to make any difference.
So, what I would need is a way to "accept" the event, so that it will not be propagated to the parent, but still show a "forbidden" cursor, as if nobody accepted the event. Any ideas?
#!/usr/bin/env python3
import sys
from PyQt5 import QtCore, QtWidgets
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
class WidgetMimeData(QtCore.QMimeData):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.itemObject = None
def hasFormat(self, mime):
if (self.itemObject and (mime == 'widgetitem')):
return True
return super().hasFormat(mime)
def setItem(self, obj):
self.itemObject = obj
def item(self):
return self.itemObject
class DraggableWidget(QGroupBox):
def __init__(self):
QWidget.__init__(self)
layout = QVBoxLayout()
self.setLayout(layout)
self.setAcceptDrops(True)
def addWidget(self, widget):
return self.layout().addWidget(widget)
def mouseMoveEvent(self, ev):
pixmap = QPixmap(self.size())
pixmap.fill(QtCore.Qt.transparent)
painter = QPainter()
painter.begin(pixmap)
painter.setOpacity(0.8)
painter.drawPixmap(0, 0, self.grab())
painter.end()
drag = QDrag(self)
mimedata = WidgetMimeData()
mimedata.setItem(self)
drag.setMimeData(mimedata)
drag.setPixmap(pixmap)
drag.setHotSpot(ev.pos())
drag.exec_(QtCore.Qt.MoveAction)
def dragEnterEvent(self, ev):
item = ev.mimeData().item()
if item.isAncestorOf(self):
#ev.ignore()
ev.accept()
else:
ev.accept()
def dropEvent(self, ev):
item = ev.mimeData().item()
if not item.isAncestorOf(self):
print('dropped on', self.layout().itemAt(0).widget().text())
ev.accept()
class HelloWindow(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
w1 = DraggableWidget()
w1.addWidget(QLabel('One'))
w2 = DraggableWidget()
w2.addWidget(QLabel('Two'))
w3 = DraggableWidget()
w3.addWidget(QLabel('Three'))
w4 = DraggableWidget()
w4.addWidget(QLabel('Four'))
w5 = DraggableWidget()
w5.addWidget(QLabel('Five'))
w1.addWidget(w3)
w1.addWidget(w4)
w2.addWidget(w5)
layout = QVBoxLayout()
layout.addWidget(w1)
layout.addWidget(w2)
layout.addStretch(1)
centralWidget = QWidget(self)
centralWidget.setLayout(layout)
self.setCentralWidget(centralWidget)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
mainWin = HelloWindow()
mainWin.show()
sys.exit( app.exec_() )
Probably the easiest way is to always accept the event in dragEnterEvent and in dragMoveEvent ignore it when the source of the event is an ancestor of self, i.e.
def dragEnterEvent(self, ev):
ev.accept()
def dragMoveEvent(self, ev):
item = ev.source()
if item.isAncestorOf(self):
ev.ignore()
By ignoring the event in dragMoveEvent you wouldn't need the check in dropEvent either and can simply do
def dropEvent(self, ev):
print('Dropped of', self.layout().itemAt(0).widget().text())
ev.accept()
I'm working on a QGIS plugin, where the UI is made with PyQt. I have a QListWidget and a function that fills it. I'd like to add a context menu for each item with only one option: to open another window.
I'm having trouble searching for info, since most of it works only on PyQt4 and I'm using version 5. The QListWidget that I want to add a context menu on is ds_list_widget. Here's some of the relevant code.
FORM_CLASS, _ = uic.loadUiType(os.path.join(
os.path.dirname(__file__), 'dialog_base.ui'))
class Dialog(QDialog, FORM_CLASS):
def __init__(self, parent=None):
...
self.p_list_widget = self.findChild(QListWidget, 'projects_listWidget')
self.p_list_widget.itemClicked.connect(self.project_clicked)
self.ds_list_widget = self.findChild(QListWidget, 'datasets_listWidget')
self.ds_list_widget.itemClicked.connect(self.dataset_clicked)
...
def project_clicked(self, item):
self.fill_datasets_list(str(item.data(Qt.UserRole)))
self.settings.setValue('projectIdValue', str(item.data(Qt.UserRole)))
def fill_datasets_list(self, project_id):
self.ds_list_widget.clear()
dataset_list = self.anotherClass.fetch_dataset_list(project_id)
for dataset in dataset_list:
#Query stuff from remote
...
item = QListWidgetItem(ds_name, self.ds_list_widget)
item.setIcon(self.newIcon(ds_img))
item.setData(Qt.UserRole, ds_id)
self.ds_list_widget.addItem(item)
self.ds_list_widget.setIconSize(self.iconSize)
Since your list-widget is created by Qt Designer, it is probably easiest to install an event-filter on it and trap the context-menu event. With that in place, the rest is quite straightforward - here is a simple demo:
import sys
from PyQt5 import QtCore, QtWidgets
class Dialog(QtWidgets.QDialog):
def __init__(self, parent=None):
super(Dialog, self).__init__()
self.listWidget = QtWidgets.QListWidget()
self.listWidget.addItems('One Two Three'.split())
self.listWidget.installEventFilter(self)
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(self.listWidget)
def eventFilter(self, source, event):
if (event.type() == QtCore.QEvent.ContextMenu and
source is self.listWidget):
menu = QtWidgets.QMenu()
menu.addAction('Open Window')
if menu.exec_(event.globalPos()):
item = source.itemAt(event.pos())
print(item.text())
return True
return super(Dialog, self).eventFilter(source, event)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
window = Dialog()
window.setGeometry(600, 100, 300, 200)
window.show()
sys.exit(app.exec_())
PS:
You should also note that code like this:
self.p_list_widget = self.findChild(QListWidget, 'projects_listWidget')
is completely unnecessary. All the widgets from Qt Designer are automatically added as attributes to the form class using the object-name. So your code can be simplified to this:
self.projects_listWidget.itemClicked.connect(self.project_clicked)
self.datasets_listWidget.itemClicked.connect(self.dataset_clicked)
there is no need to use findChild.
In addition to the answer above, you can also set multiple QAction() submenu items to do multiple things. As you would a normal menu.
One way is to edit your eventFilter so that menu.exec() becomes a variable:
def eventFilter(self, source, event):
if (event.type() == QtCore.QEvent.ContextMenu and source is self.listWidget):
menu = QtWidgets.QMenu()
open_window_1 = QAction("Open Window 1")
open_window_2 = QAction("Open Window 2")
menu.addAction(open_window_1)
menu.addAction(open_window_2)
menu_click = menu.exec(event.globalPos())
try:
item = source.itemAt(event.pos())
except Exception as e:
print(f"No item selected {e}")
if menu_click == open_window_1 :
print("Opening Window 1...")
# Your code here
if menu_click == open_window_2 :
print("Opening Window 2...")
# Your code here
# and so on... You can now add as many items as you want
return True
return super(Dialog, self).eventFilter(source, event)
I have a QTreeView displaying data from a QStandardItemModel. One of the columns of the tree is displayed with a delegate that lets the user edit and display rich text. Below is a SSCCE that limits the editing to bold (with keyboard shortcut).
When the user is editing one of the items, how can I set it up so that in addition to toggling boldness with keyboard shortcut (CTRL-B), the user can also toggle it using the toolbar icon?
Thus far, the keyboard shortcut works great (you can double click, edit text, and CTRL-B will toggle bold). However, I haven't figured out how to connect the bold button in the toolbar to the appropriate slot:
self.boldTextAction.triggered.connect(self.emboldenText)
where I have this just sitting there doing nothing:
def emboldenText(self):
print "Make selected text bold...How do I do this?"
Things would be easy if the main window's central widget was the text editor: I could directly invoke the text editor's toggle bold method. Unfortunately, the text editor is only generated transiently by the tree view's delegate when the user double-clicks to start editing the tree.
That is, we have this complicated relationship:
QMainWindow -> QTreeView -> Delegate.CreateEditor ->
QTextEdit.toggleBold()
How do I access toggleBold() from within the main window for use by the toolbar action, especially given that the editor is only created temporarily when opened by the user?
I realize this may not be a PySide/Qt question as much as a Python/OOP question, so I've included additional potentially relevant tags. Any help with improving my word choice/jargon would be appreciated too.
SSCCE
#!/usr/bin/env python
import platform
import sys
from PySide import QtGui, QtCore
class MainTree(QtGui.QMainWindow):
def __init__(self, tree, parent = None):
QtGui.QMainWindow.__init__(self)
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
self.setCentralWidget(tree)
self.createStatusBar()
self.createBoldAction()
self.createToolbar()
self.tree = tree
#self.htmlDelegate = self.tree.itemDelegateForColumn(1)
def createStatusBar(self):
self.status = self.statusBar()
self.status.setSizeGripEnabled(False)
self.status.showMessage("In editor, keyboard to toggle bold")
def createToolbar(self):
self.textToolbar = self.addToolBar("Text actions")
self.textToolbar.addAction(self.boldTextAction)
def createBoldAction(self):
self.boldTextAction = QtGui.QAction("Bold", self)
self.boldTextAction.setIcon(QtGui.QIcon("boldText.png"))
self.boldTextAction.triggered.connect(self.emboldenText)
self.boldTextAction.setStatusTip("Make selected text bold")
def emboldenText(self):
print "Make selected text bold...How do I do this? It's stuck in RichTextLineEdit"
class HtmlTree(QtGui.QTreeView):
def __init__(self, parent = None):
QtGui.QTreeView.__init__(self)
model = QtGui.QStandardItemModel()
model.setHorizontalHeaderLabels(['Task', 'Priority'])
rootItem = model.invisibleRootItem()
item0 = [QtGui.QStandardItem('Sneeze'), QtGui.QStandardItem('Low')]
item00 = [QtGui.QStandardItem('Tickle nose'), QtGui.QStandardItem('Low')]
item1 = [QtGui.QStandardItem('Get a job'), QtGui.QStandardItem('<b>High</b>')]
item01 = [QtGui.QStandardItem('Call temp agency'), QtGui.QStandardItem('<b>Extremely</b> <i>high</i>')]
rootItem.appendRow(item0)
item0[0].appendRow(item00)
rootItem.appendRow(item1)
item1[0].appendRow(item01)
self.setModel(model)
self.expandAll()
self.resizeColumnToContents(0)
self.setToolTip("Use keyboard to toggle bold")
self.setItemDelegate(HtmlPainter(self))
class HtmlPainter(QtGui.QStyledItemDelegate):
def __init__(self, parent=None):
QtGui.QStyledItemDelegate.__init__(self, parent)
def paint(self, painter, option, index):
if index.column() == 1:
text = index.model().data(index) #default role is display (for edit consider fixing Valign prob)
palette = QtGui.QApplication.palette()
document = QtGui.QTextDocument()
document.setDefaultFont(option.font)
#Set text (color depends on whether selected)
if option.state & QtGui.QStyle.State_Selected:
displayString = "<font color={0}>{1}</font>".format(palette.highlightedText().color().name(), text)
document.setHtml(displayString)
else:
document.setHtml(text)
#Set background color
bgColor = palette.highlight().color() if (option.state & QtGui.QStyle.State_Selected)\
else palette.base().color()
painter.save()
painter.fillRect(option.rect, bgColor)
document.setTextWidth(option.rect.width())
offset_y = (option.rect.height() - document.size().height())/2
painter.translate(option.rect.x(), option.rect.y() + offset_y)
document.drawContents(painter)
painter.restore()
else:
QtGui.QStyledItemDelegate.paint(self, painter, option, index)
def sizeHint(self, option, index):
fm = option.fontMetrics
if index.column() == 1:
text = index.model().data(index)
document = QtGui.QTextDocument()
document.setDefaultFont(option.font)
document.setHtml(text)
return QtCore.QSize(document.idealWidth() + 5, fm.height())
return QtGui.QStyledItemDelegate.sizeHint(self, option, index)
def createEditor(self, parent, option, index):
if index.column() == 1:
editor = RichTextLineEdit(parent)
editor.returnPressed.connect(self.commitAndCloseEditor)
return editor
else:
return QtGui.QStyledItemDelegate.createEditor(self, parent, option,
index)
def commitAndCloseEditor(self):
editor = self.sender()
if isinstance(editor, (QtGui.QTextEdit, QtGui.QLineEdit)):
self.commitData.emit(editor)
self.closeEditor.emit(editor, QtGui.QAbstractItemDelegate.NoHint)
class RichTextLineEdit(QtGui.QTextEdit):
returnPressed = QtCore.Signal()
def __init__(self, parent=None):
QtGui.QTextEdit.__init__(self, parent)
self.setLineWrapMode(QtGui.QTextEdit.NoWrap)
self.setTabChangesFocus(True)
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
fontMetrics = QtGui.QFontMetrics(self.font())
h = int(fontMetrics.height() * (1.4 if platform.system() == "Windows"
else 1.2))
self.setMinimumHeight(h)
self.setMaximumHeight(int(h * 1.2))
self.setToolTip("Press <b>Ctrl+b</b> to toggle bold")
def toggleBold(self):
self.setFontWeight(QtGui.QFont.Normal
if self.fontWeight() > QtGui.QFont.Normal else QtGui.QFont.Bold)
def sizeHint(self):
return QtCore.QSize(self.document().idealWidth() + 5,
self.maximumHeight())
def minimumSizeHint(self):
fm = QtGui.QFontMetrics(self.font())
return QtCore.QSize(fm.width("WWWW"), self.minimumHeight())
def keyPressEvent(self, event):
'''This just handles all keyboard shortcuts, and stops retun from returning'''
if event.modifiers() & QtCore.Qt.ControlModifier:
handled = False
if event.key() == QtCore.Qt.Key_B:
self.toggleBold()
handled = True
if handled:
event.accept()
return
if event.key() in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
self.returnPressed.emit()
event.accept()
else:
QtGui.QTextEdit.keyPressEvent(self, event)
def main():
app = QtGui.QApplication(sys.argv)
myTree = HtmlTree()
#myTree.show()
myMainTree = MainTree(myTree)
myMainTree.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
Note for those that want the Full Tree Experience, with the button in the toolbar, here it is you can put it in the same folder as the script (change the name to boldText.png:
I think from a design point of view the top window is a sort of global. You have already described a behaviour which is treating it in that way and (as ekhumoro has said) that pretty much requires you to provide access to that top window to the editor.
One very simple way to do that is to call parent.window() in the createEditor method. Maybe something like:
parent.window().boldTextAction.triggered.connect(editor.toggleBold)
That seems to work for me.