Make an Item in a QTableView draggable but not selectable - python

A bit of background (not fully necessary to understand the problem):
In a PySide2 application I developed earlier this year, I have a QTableView that I'm using with a custom model.
Some items in there can be selected in order to then click a button to perform a bulk action on the selected items.
Some other items are there as a visual indication that the data exists, but the function that does the bulk action woudn't know how to handle them, so I made them non-selectable.
I have another area in my program that can accept drops, either from external files or from the items in my TableView. In that case, every single one of the items in the table should be able to be dragged and then dropped in the other area.
The problem:
When an Item is not selectable, it seems to also not be draggable.
What I have tried:
Not much in terms of code, as I don't really know how to approach that one.
I tried a lot of combinations of Flags with no success.
I have been googling and browsing StackOverflow for a few hours, but can't find anything relevant, I'm just inundated by posts of people asking how to re-order a table by drag and dropping.
Minimum reproduction code:
from PySide2 import QtCore, QtWidgets, QtGui
class MyWindow(QtWidgets.QWidget):
def __init__(self):
QtWidgets.QWidget.__init__(self)
self.setGeometry(300, 200, 570, 450)
self.setWindowTitle("Drag Example")
model = QtGui.QStandardItemModel(2, 1)
table_view = QtWidgets.QTableView()
selectable = QtGui.QStandardItem("I'm selectable and draggable")
selectable.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsDragEnabled)
model.setItem(0, 0, selectable)
non_selectable = QtGui.QStandardItem("I'm supposed to be draggable, but I'm not")
non_selectable.setFlags(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsDragEnabled)
model.setItem(1, 0, non_selectable)
table_view.setModel(model)
table_view.setDragDropMode(table_view.DragOnly)
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(table_view)
self.setLayout(layout)
app = QtWidgets.QApplication([])
win = MyWindow()
win.show()
app.exec_()
Does anyone have a suggestion as to how to make an Item drag-enabled but selection-disabled?
Thanks

I found a workaround, though not necessarily a perfect answer, it does the trick for me.
Instead of making the item non selectable, I make it visually look like it doesn't select when selecting it, using a custom data role.
I use that same data flag in my bulk function to filter out the selection.
To make it look like the items are not selected even though they are, I used a custom styled item delegate:
class MyDelegate(QtWidgets.QStyledItemDelegate):
def __init__(self, parent=None):
super(MyDelegate, self).__init__(parent)
def paint(self, painter, option, index):
if option.state & QtWidgets.QStyle.State_Selected:
if index.data(Qt.UserRole) == 2: # Some random flag i'm using
text_brush = option.palette.brush(QtGui.QPalette.Text)
if index.row() % 2 == 0:
bg_brush = option.palette.brush(QtGui.QPalette.Base)
else:
bg_brush = option.palette.brush(QtGui.QPalette.AlternateBase)
# print brush
option.palette.setBrush(QtGui.QPalette.Highlight, bg_brush)
option.palette.setBrush(QtGui.QPalette.HighlightedText, text_brush)
super(MyDelegate, self).paint(painter, option, index)
and in the table I set:
delegate = MyDelegate()
table_view.setItemDelegate(delegate)
I adapted this solution from this post: QTreeView Item Hover/Selected background color based on current color

Related

How to scroll Qt widgets by keyboard?

This is a portion of a Python class that I wrote:
def keyPressEvent(self, event):
if event.text() == 'n':
self.widget.scroll(0, -self.height())
elif event.text() == 'p':
self.widget.scroll(0, self.height())
When the user presses n, the widget widget (which has a scrollbar), will scroll downwards by self.height() pixels (which is the entire height of the window; i.e. next page). Opposite is when p is pressed: returns to previous page.
But the problem is that:
Usually, a proper scroller will stop when it reaches page's top. But pressing p will keep going backwards towards infinity in the back, where there is an indefinitely deep void of nothingness!
Opposite is true for n; jumps into some unseen abyss into the distant future, beyond the communication horizon.
Question. How to make a proper scroller using keyboard bindings? A proper scroller is one where it saturates at page's boundaries.
Appendix
This is more code (still trimmed to be readable). My goal here is to show what kind of widgets I am using to address a question in a comment.
#!/usr/bin/python3
import sys
import random
try:
from PySide6 import QtCore, QtWidgets
except ModuleNotFoundError:
from PySide2 import QtCore, QtWidgets
# ... <omitted for brevity> ...
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
# create ui's elements
self.scroll = QtWidgets.QScrollArea()
self.widget = QtWidgets.QWidget()
self.hbox = QtWidgets.QHBoxLayout()
# link ui's elements
self.setCentralWidget(self.scroll)
self.scroll.setWidget(self.widget)
self.widget.setLayout(self.hbox)
# configure ui's elements
self.scroll.setWidgetResizable(True)
self.scroll.setVerticalScrollBarPolicy(
QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.scroll.setHorizontalScrollBarPolicy(
QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.columns = []
self.add_column()
self.show()
# ... <omitted for brevity> ...
def add_column(self):
# ... <omitted for brevity> ...
# ... <omitted for brevity> ...
def keyPressEvent(self, event):
# ... <omitted for brevity> ...
if event.text() == c['key_next_page']:
self.widget.scroll(0, -self.height())
elif event.text() == c['key_prev_page']:
self.widget.scroll(0, self.height())
# ... <omitted for brevity> ...
The scroll() function rarely needs to be directly used.
QScrollBar already calls it internally, by overriding the virtual function of its inherited class (QAbstractScrollArea), scrollContentsBy(). As the documentation says:
Calling this function in order to scroll programmatically is an error, use the scroll bars instead (e.g. by calling QScrollBar::setValue() directly).
So, the solution is usually quite simple: instead of scrolling the widget, set the value of the scroll bar. Note that this will always work, even when scroll bars are hidden.
Before providing the final code, please note two aspects:
when checking for keys, it's usually better to use the key enumeration instead of the text() value of the key event; there are many reasons for this, but, most importantly, it's related to the fact that "keys" are usually mapped to numeric values, and using the actual internal value is generally more accurate and reliable than comparing strings;
you shall not override the keyPressEvent() of the parent (unless you really know what you're doing and why): if the parent has other widgets that only handle certain keys but not what you're looking for, you may end up in scrolling the area for the wrong reason; suppose you have complex layout that also has a QSpinBox outside of the scroll area: that control doesn't generally handle the P key, but the user might press it by mistake (trying to press 0), and they'll see an unexpected behavior: they typed the wrong key, and another, unrelated widget got scrolled;
The solution is to subclass QScrollArea and override its keyPressEvent() instead, then set the value of the scroll bar using its pageStep() property (which, for QScrollArea, equals to the width or height of the scrolled widget).
from PyQt5 import QtCore, QtWidgets
class KeyboardScrollArea(QtWidgets.QScrollArea):
def keyPressEvent(self, event):
if event.key() in (QtCore.Qt.Key_N, QtCore.Qt.Key_P):
sb = self.verticalScrollBar()
if (event.key() == QtCore.Qt.Key_P
and sb.value() > 0):
sb.setValue(sb.value() - sb.pageStep())
return
elif (event.key() == QtCore.Qt.Key_N
and sb.value() < sb.maximum()):
sb.setValue(sb.value() + sb.pageStep())
return
super().keyPressEvent(event)
if __name__ == '__main__':
app = QtWidgets.QApplication([])
scroll = KeyboardScrollArea()
widget = QtWidgets.QWidget()
scroll.setWidget(widget)
scroll.setWidgetResizable(True)
layout = QtWidgets.QVBoxLayout(widget)
for i in range(50):
layout.addWidget(QtWidgets.QPushButton(str(i + 1)))
scroll.show()
app.exec_()
Note that I explicitly checked that the scroll bar value was greater than 0 (for "page up") and lass then the maximum. That is because when an event is not handled, it should normally be propagated to the parent. Suppose you have a custom scroll area inside another custom scroll area: you may want to be able to keep moving up/down the outer scroll area whenever the inner one cannot scroll any more.
Another possibility is to install an event filter on the scroll area, and listen for KeyPress events, then do the same as above (and, in that case, return True when the scroll actually happens). The concept is basically the same, the difference is that you don't need to subclass the scroll area (but a subclass will still be required, as the event filter must override eventFilter()).

pyqt5 - connect a function when QComboBox is clicked [duplicate]

I have been trying to get a QComboBox in PyQt5 to become populated from a database table. The problem is trying to find a method that recognizes a click event on it.
In my GUI, my combo-box is initially empty, but upon clicking on it I wish for the click event to activate my method for communicating to the database and populating the drop-down list. It seems so far that there is no built-in event handler for a click-event for the combo-box. I am hoping that I am wrong on this. I hope someone will be able to tell me that there is a way to do this.
The best article I could find on my use-case here is from this link referring to PyQt4 QComboBox:
dropdown event/callback in combo-box in pyqt4
I also found another link that contains a nice image of a QComboBox.
The first element seems to be a label followed by a list:
Catch mouse button pressed signal from QComboBox popup menu
You can override the showPopup method to achieve this, which will work no matter how the drop-down list is opened (i.e. via the mouse, keyboard, or shortcuts):
from PyQt5 import QtCore, QtWidgets
class ComboBox(QtWidgets.QComboBox):
popupAboutToBeShown = QtCore.pyqtSignal()
def showPopup(self):
self.popupAboutToBeShown.emit()
super(ComboBox, self).showPopup()
class Window(QtWidgets.QWidget):
def __init__(self):
super(Window, self).__init__()
self.combo = ComboBox(self)
self.combo.popupAboutToBeShown.connect(self.populateConbo)
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(self.combo)
def populateConbo(self):
if not self.combo.count():
self.combo.addItems('One Two Three Four'.split())
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec_())
However, for your particular use-case, I think a better solution might be to set a QSqlQueryModel on the combo-box, so that the items are updated from the database automatically.
Alternative Solution I :
We can use frame click, the code is to be used in the container of the combo box (windows/dialog/etc.)
def mousePressEvent(self, event):
print("Hello world !")
or
def mousePressEvent():
print("Hello world !")
Alternative Solution II :
We could connect a handler to the pressed signal of the combo's view
self.uiComboBox.view().pressed.connect(self.handleItemPressed)
...
def handleItemPressed(self, index):
item = self.uiComboBox.model().itemFromIndex(index)
print("Do something with the selected item")
Why would you want to populate it when it's activated rather than when the window is loaded?
I am currently developing an application with PySide (another Python binding for the Qt framework), and I populate my comboboxes in the mainwindow class __init__ function, which seems to be the way to go, judging by many examples.
Look at the example code under "QCombobox" over at Zetcode.

How to create an interactive text menu in Python?

I'm not sure what to call it. I want to know how to make one of those menus where you use the arrow keys to highlight your options and press enter to accept it.
The questioner acknowledged being unsure how to clearly state what they're after (I know the feeling!) and it is a little while after the question was posted, but in light of their comment suggesting they were after text, I believe something like python-prompt-toolkit, which is used to provide text-based autocomplete in a number of Python projects, may offer a solution.
There is documentation here which has this example of using a WordCompleter:
from prompt_toolkit import prompt
from prompt_toolkit.completion import WordCompleter
html_completer = WordCompleter(['<html>', '<body>', '<head>', '<title>'])
text = prompt('Enter HTML: ', completer=html_completer)
print('You said: %s' % text)
That produces output like this:
The example above is still relatively like a ComboBox merely rendered in text, but there are ways to produce other styles of menu as shown in the gallery here.
If that isn't sufficient then another option is looking into something that wraps ncurses, like https://bitbucket.org/npcole/npyscreen or http://urwid.org/ .
The answer to your question including the examples you provided is curses. This package is very much dependent on the underlying operating system. So if platform independence is key then you will get issues. There is for example a Windows port UniCurses but your implementation has to handle this switch if necessary.
There are also tools built on top of curses. Four example Urwid.
I personally have some experience with curses and if you have Linux as the underlying system this can be fun if your requirements are robust and simple. Like your menu requirement. I have to say though that the learning curve is pretty steep.
Hope this helps. But given the broad question thus is the only level of detail to provide at this stage.
I think you mean Combobox.
Here is a short example based on this and PyQt:
from PyQt4 import QtGui, QtCore
import sys
class CheckableComboBox(QtGui.QComboBox):
def __init__(self):
super(CheckableComboBox, self).__init__()
self.view().pressed.connect(self.handleItemPressed)
self.setModel(QtGui.QStandardItemModel(self))
def handleItemPressed(self, index):
item = self.model().itemFromIndex(index)
if item.checkState() == QtCore.Qt.Checked:
item.setCheckState(QtCore.Qt.Unchecked)
else:
item.setCheckState(QtCore.Qt.Checked)
class Dialog_01(QtGui.QMainWindow):
def __init__(self):
super(QtGui.QMainWindow,self).__init__()
myQWidget = QtGui.QWidget()
myBoxLayout = QtGui.QVBoxLayout()
myQWidget.setLayout(myBoxLayout)
self.setCentralWidget(myQWidget)
self.toolbutton = QtGui.QToolButton(self)
self.toolbutton.setText('Select Categories ')
self.toolmenu = QtGui.QMenu(self)
for i in range(3):
action = self.toolmenu.addAction("Category " + str(i))
action.setCheckable(True)
self.toolbutton.setMenu(self.toolmenu)
self.toolbutton.setPopupMode(QtGui.QToolButton.InstantPopup)
myBoxLayout.addWidget(self.toolbutton)
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
combo = Dialog_01()
combo.show()
combo.resize(480,320)
sys.exit(app.exec_())

QBoxLayouts with Python

Because of their high customizability I've been relying on using multiple GroupBoxes while building app GUIs. But it appears QGroupBoxes make a certain impact on how fast an interface builds.
Now with layout's .insertLayout() method I can build an entire graphics interface placing the widgets any where I want. The dialogs feel very lightweight and extremely fast to re-draw. Unfortunately I can't find a way to control their appearance. I would appreciate if you would give me some clue on how to control the layout visual properties. I am particularly interested in knowing:
How to draw layout border, how to control a border line width,
How to place a layout title (similar to what QGroupBox's .setTitle() does)
How to control the layout outside and inside margins.
How to make layout minimizable/size-restorable (So the user could click some minus/arrow icon to fold/unfold layout when they need or don't need certain widgets belonging to the same layout.
The example with three nested layouts is posted below. As it is seen on dialog screenshot there is no way to visually differentiate one layout from another since there are no border, no titles, no dividers and etc.
from PyQt4 import QtGui, QtCore
class Dialog_01(QtGui.QMainWindow):
def __init__(self):
super(QtGui.QMainWindow,self).__init__()
tabWidget = QtGui.QTabWidget()
tabGroupBox = QtGui.QGroupBox()
tabLayout = QtGui.QVBoxLayout()
tabLayout.setContentsMargins(0, 0, 0, 0)
tabLayout.setSpacing(0)
subLayoutA=QtGui.QVBoxLayout()
tabLayout.insertLayout(0, subLayoutA)
tabGroupBox.setLayout(tabLayout)
tabWidget.addTab(tabGroupBox,' Tab A ')
listWidgetA = QtGui.QListWidget()
for i in range(3):
QtGui.QListWidgetItem( 'Item '+str(i), listWidgetA )
subLayoutA.addWidget(listWidgetA)
subLayoutB=QtGui.QHBoxLayout()
tabLayout.insertLayout(1, subLayoutB)
subLayoutB.addWidget(QtGui.QLineEdit('LineEdit 1'))
subLayoutB.addWidget(QtGui.QLineEdit('LineEdit 2'))
subLayoutC=QtGui.QVBoxLayout()
tabLayout.insertLayout(2, subLayoutC)
subLayoutC.addWidget(QtGui.QPushButton('PushButton 1'))
subLayoutC.addWidget(QtGui.QPushButton('PushButton 2'))
self.setCentralWidget(tabWidget)
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
dialog_1 = Dialog_01()
dialog_1.show()
dialog_1.resize(480,320)
sys.exit(app.exec_())
EDITED LATER
I inserted two lines into an example code to implement one of the suggestions made by sebastian. A Spacing-Margins method combos can be effectively used to get some additional tweaks done. Here is a screenshot (still could not get rid of the spacing around pushButtons):
QLayout sublcasses don't have a visual representation, which becomes clear by the fact that QLayout classes do not inherit QWidget. They only calculate the positions of the widgets they are responsible for in the context of their "parent" widget.
So the answer to questions 1,2 and 4 is basically: You can't.
You'll always have to have a QWidget in combination with a QLayout.
E.g. to group your two buttons into a frame with a box use a QFrame:
subLayoutC=QtGui.QVBoxLayout()
buttonFrame = QtGui.QFrame()
buttonFrame.setFrameStyle(QtGui.QFrame.Plain |QtGui.QFrame.Box)
buttonFrame.setLayout(subLayoutC)
subLayoutC.addWidget(QtGui.QPushButton('PushButton 1'))
subLayoutC.addWidget(QtGui.QPushButton('PushButton 2'))
# now we add the QFrame widget - not subLayoutC to the tabLayout
tabLayout.addWidget(buttonFrame) # I think your suggested edit was correct here
self.setCentralWidget(tabWidget)
Concerning question 3, check the docs:
http://qt-project.org/doc/qt-4.8/qlayout.html#setContentsMargins
http://qt-project.org/doc/qt-4.8/qboxlayout.html#setSpacing

Smooth lazy loading and unloading of custom IndexWidget in QListView

I am writing an application that makes use of a custom QWidget in place of regular listitems or delegates in PyQt. I have followed the answer in Render QWidget in paint() method of QWidgetDelegate for a QListView -- among others -- to implement a QTableModel with custom widgets. The resulting sample code is at the bottom of this question. There are some problems with the implementation that I do not know how to solve:
Unloading items when they are not being displayed. I plan to build my application for a list that will have thousands of entries, and I cannot keep that many widgets in memory.
Loading items that are not yet in view or at least loading them asynchronously. Widgets take a moment to render, and the example code below has some obvious lag when scrolling through the list.
When scrolling the list in the implementation below, each newly loaded button when loading shows up at the top left corner of the QListView for a split second before bouncing into position. How can that be avoided?
--
import sys
from PyQt4 import QtGui, QtCore
from PyQt4.QtCore import Qt
class TestListModel(QtCore.QAbstractListModel):
def __init__(self, parent=None):
QtCore.QAbstractListModel.__init__(self, parent)
self.list = parent
def rowCount(self, index):
return 1000
def data(self, index, role):
if role == Qt.DisplayRole:
if not self.list.indexWidget(index):
button = QtGui.QPushButton("This is item #%s" % index.row())
self.list.setIndexWidget(index, button)
return QtCore.QVariant()
if role == Qt.SizeHintRole:
return QtCore.QSize(100, 50)
def columnCount(self, index):
pass
def main():
app = QtGui.QApplication(sys.argv)
window = QtGui.QWidget()
list = QtGui.QListView()
model = TestListModel(list)
list.setModel(model)
list.setVerticalScrollMode(QtGui.QAbstractItemView.ScrollPerPixel)
layout = QtGui.QVBoxLayout(window)
layout.addWidget(list)
window.setLayout(layout)
window.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
You can use a proxy model to avoid to load all widgets. The proxy model can calculate the row count with the heights of the viewport and the widget. He can calculate the index of the items with the scrollbar value.
It is a shaky solution but it should work.
If you modify your data() method with:
button = QtGui.QPushButton("This is item #%s" % index.row())
self.list.setIndexWidget(index, button)
button.setVisible(False)
The items will not display until they are moved at their positions (it works for me).
QTableView only requests data to the model for items in its viewport, so the size of your data doesn't really affect the speed. Since you already subclassed QAbstractListModel you could reimplement it to return only a small set of rows when it's initialized, and modify its canFetchMore method to return True if the total amount of records hasn't been displayed. Although, with the size of your data, you might want to consider creating a database and using QSqlQueryModel or QSqlTableModel instead, both of them do lazy loading in groups of 256.
To get a smoother load of items you could connect to the valueChanged signal of your QTableView.verticalScrollBar() and depending on the difference between it's value and maximum have something like:
while xCondition:
if self.model.canFetchMore():
self.model.fetchMore()
Using setIndexWidget is slowing up your application considerably. You could use a QItemDelegate and customize it's paint method to display a button with something like:
class MyItemDelegate(QtGui.QItemDelegate):
def __init__(self, parent=None):
super(MyItemDelegate, self).__init__(parent)
def paint(self, painter, option, index):
text = index.model().data(index, QtCore.Qt.DisplayRole).toString()
pushButton = QtGui.QPushButton()
pushButton.setText(text)
pushButton.setGeometry(option.rect)
painter.save()
painter.translate(option.rect.x(), option.rect.y())
pushButton.render(painter)
painter.restore()
And setting it up with:
myView.setItemDelegateForColumn(columnNumber, myItemDelegate)

Categories