Issue
I am making a search filter for my PyQt6 project. One of the filter elements is supposed to display all available options in a search category.
Each option is a QCheckBox that is added into QVBoxLayout. After the layout is populated with options, it is installed as a layout manager on QWidget that is then set as a QScrollArea widget. I want a QCheckBox option to be moved above other unchecked options upon being clicked.
What is the way to do that? And on a related note, is it possible to sort QCheckBox'es alphabetically by means of PyQt (for now I am just passing a sorted list with texts for options)? I guess I'll need it if I want checked options to be sorted as well, and when an option is unchecked for it to be put back in an alphabetically correct position.
Here is my code so far:
from PyQt6 import QtCore, QtWidgets
from database_handler import DatabaseHandler
class SearchParamGroup(QtWidgets.QWidget):
def __init__(self, group_title, db_column):
super(SearchParamGroup, self).__init__()
title_label = QtWidgets.QLabel(group_title)
reset_button = QtWidgets.QLabel('RESET') # It is just a placeholder for now
title_and_reset_hbox = QtWidgets.QHBoxLayout()
title_and_reset_hbox.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
title_and_reset_hbox.addWidget(title_label)
title_and_reset_hbox.addWidget(reset_button)
options_box = self.create_options_box(db_column)
group_vbox = QtWidgets.QVBoxLayout()
group_vbox.addLayout(title_and_reset_hbox)
group_vbox.addWidget(options_box)
self.setLayout(group_vbox)
@staticmethod
def create_options_box(db_column_from_init):
scroll_area_widget_vbox = QtWidgets.QVBoxLayout()
group_options = DatabaseHandler.get_distinct_values(db_column_from_init)
for option_name in group_options:
option_checkbox = QtWidgets.QCheckBox(option_name)
scroll_area_widget_vbox.addWidget(option_checkbox)
scroll_area = QtWidgets.QScrollArea()
scroll_area_widget = QtWidgets.QWidget()
scroll_area_widget.setLayout(scroll_area_widget_vbox)
scroll_area.setWidget(scroll_area_widget)
return scroll_area
Solution
A basic solution
The simplicity of QBoxLayout allows to easily change the order of items just by calling or insertWidget()
or addWidget()
(which internally calls insertWidget(-1, widget)
), because that always guarantees that the widget will be put at the given position, even if it is already part of the layout manager.
Considering that, you can add the check boxes to an internal list that will preserve the original order, and then "add" them again based on their checked state.
from string import ascii_lowercase as letters
from PyQt5.QtWidgets import *
class CheckBoxSort(QScrollArea):
def __init__(self):
super().__init__()
container = QWidget()
self.setWidget(container)
self.setWidgetResizable(True)
layout = QVBoxLayout(container)
self.checks = []
for l in letters:
check = QCheckBox(l * 10)
self.checks.append(check)
layout.addWidget(check)
check.toggled.connect(self.sortChecked)
def sortChecked(self):
checked = []
unchecked = []
for check in self.checks:
if check.isChecked():
checked.append(check)
else:
unchecked.append(check)
layout = self.widget().layout()
for check in checked + unchecked:
layout.addWidget(check)
app = QApplication([])
test = CheckBoxSort()
test.show()
app.exec()
An alternative solution, using item views
Note that what you're showing is a "list of items", and Qt actually provides that: QListView (with a related model) or even QListWidget (a lower level class that implements the model on its own).
Such lists are normally better suited for situations where lots of items are being shown, as they provide a more abstract and powerful way to interact with their contents.
Here is a possible implementation, similar to the above:
from string import ascii_lowercase as letters
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
class ListCheckSort(QListWidget):
def __init__(self):
super().__init__()
self.allItems = []
for l in letters:
item = QListWidgetItem(l * 10)
item.setCheckState(Qt.Unchecked)
self.addItem(item)
self.allItems.append(item)
self.itemChanged.connect(self.sortChecked)
def sortChecked(self):
checked = []
unchecked = []
for item in self.allItems:
if item.checkState():
checked.append(item)
else:
unchecked.append(item)
# Qt views normally scroll to the item when it's selected and
# moved, so we keep a reference to the vertical scroll bar value
# and restore it right after
vb = self.verticalScrollBar().value()
for item in checked + unchecked:
self.addItem(self.takeItem(self.row(item)))
self.verticalScrollBar().setValue(vb)
app = QApplication([])
test = ListCheckSort()
test.show()
app.exec()
There's one slight difference: item views normally change their "check state" only when the user exactly clicks on the check box rectangle, which is different from normal check boxes that also toggle their state when the user clicks the "label" next to its indicator.
We can work around that too:
class ListCheckSort(QListWidget):
def __init__(self):
# ...
self.itemClicked.connect(self.toggleItem)
# ...
def toggleItem(self, item):
if not item:
return
if item.checkState():
item.setCheckState(Qt.Unchecked)
else:
item.setCheckState(Qt.Checked)
Note that if not item
: that is caused by the fact that clicked
signal is fired after the item check state has been toggled, and since the view emits the signal with the item under the mouse cursor when the mouse button has been released (and at that point, the item is not there anymore), item
becomes invalid.
UX considerations
The above brings us to a different issue, related to the User Experience (UX).
In my experience (not just as an user with personal experience, but considering the UX subject objectively), such an approach would not be very good, for many reasons:
- users need a visual confirmation of their actions, not an assumption: it's quite easy to click the wrong item in a very long list, and it's even easier to do that with touch devices (including touch pads used in laptops) and the user might click on the wrong item without knowing; moving an item when it's toggled (possibly outside of the currently visible list) means that the user has to assume that their action was successful or that they didn't do anything at all:
- if they clicked an unchecked item, they probably wouldn't realize it immediately ("I clicked but nothing happened"), and if they checked lots of items they might not see an unwanted item in the "checked" list;
- if they clicked a checked item by mistake, they will need to scroll down to it in order to check it again: that's assuming that they remember what item that was, and that items are sorted in a logical way: in either cases, that's annoying (and the user should not be annoyed by the UI);
- never forget about keyboard navigation: while most users use touch and mouse to interact with a GUI, a lot of them still use tabs and arrows in order to easily navigate the UI; both of the solutions provided above would be very confusing to them:
- with the layout approach, the focus will be kept on the checked item, and Qt doesn't update the focus order when widgets are moved within the same layout: while this means that using tab/arrow keys will focus on the next/previous check box of the original order, that would also be inconsistent, because the tab order will just "jump around" the container widgets;
- with the item approach, the focus is kept on the selected items, meaning that it will be moved on top (or the middle) of the item list, making keyboard interaction extremely annoying; note that the same would happen even if the above layout aspect would update the focus chain order;
In conclusion, I would highly discourage such an approach, both as a user and as a developer. A better solution could be to provide a further widget that visually displays the current selection (for instance, a basic QLineEdit set in read only mode that lists all selected items, or a separate QListWidget that does the same), or eventually find another alternative.
A possibility could be to create "proxy items" that would be always aligned on top when items are checked, and deleted when they (or their "original" item) is unchecked. While that would certainly be a much complex implementation, it would also provide a UX that would have a more consistent behavior, keeping proper interaction and user satisfaction.
Answered By - musicamante
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.