First of all my problem is reproducible only on device with touch screen,
with PC/mouse all works fine.
The problem that if I use QTableView + QScroller as standalone window all works fine - I move finger from bottom to top content scrolls down, from top to bottom scrolls up.
But if I put QTableView inside QWidget with Qt::Popup attribute, then scrolling changes direction! I move finger from bottom to top and it scrolls up, from top to bottom scrolls down.
Here is my code:
#include <QAbstractTableModel>
#include <QScroller>
#include <QTouchDevice>
#include <QVBoxLayout>
#include <QtDebug>
#include <QtWidgets/QApplication>
#include <QtWidgets/QTableView>
class MyModel : public QAbstractTableModel {
public:
MyModel(QObject *parent) : QAbstractTableModel(parent) {}
int rowCount(const QModelIndex &parent = QModelIndex()) const override {
return 100;
}
int columnCount(const QModelIndex &parent = QModelIndex()) const override {
return 3;
}
QVariant data(const QModelIndex &index,
int role = Qt::DisplayRole) const override {
if (role == Qt::DisplayRole) {
return QString("Row%1, Column%2")
.arg(index.row() + 1)
.arg(index.column() + 1);
}
return QVariant();
}
QVariant headerData(int section, Qt::Orientation orientation,
int role) const override {
if (role == Qt::DisplayRole) {
if (orientation == Qt::Horizontal) {
switch (section) {
case 0:
return QString("first");
case 1:
return QString("second");
case 2:
return QString("third");
}
}
}
return QVariant();
}
};
bool is_touch_screen_avaible() {
const auto devs = QTouchDevice::devices();
for (const auto &dev : devs) {
if (dev->type() == QTouchDevice::TouchScreen) {
return true;
}
}
return false;
}
void configure_scoller_for_item_view(QAbstractItemView *view) {
QScroller *scroller = QScroller::scroller(view);
view->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
view->setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel);
QScrollerProperties properties =
QScroller::scroller(scroller)->scrollerProperties();
QVariant overshootPolicy =
QVariant::fromValue<QScrollerProperties::OvershootPolicy>(
QScrollerProperties::OvershootAlwaysOff);
properties.setScrollMetric(QScrollerProperties::VerticalOvershootPolicy,
overshootPolicy);
scroller->setScrollerProperties(properties);
properties.setScrollMetric(QScrollerProperties::HorizontalOvershootPolicy,
overshootPolicy);
scroller->setScrollerProperties(properties);
if (is_touch_screen_avaible())
scroller->grabGesture(view, QScroller::TouchGesture);
else
scroller->grabGesture(view, QScroller::LeftMouseButtonGesture);
}
#define POPUP
int main(int argc, char *argv[]) {
QApplication a(argc, argv);
#ifdef POPUP
QWidget *mainWin = new QWidget;
mainWin->setWindowFlags(Qt::Popup);
auto lay = new QVBoxLayout(mainWin);
mainWin->setLayout(lay);
auto tableView = new QTableView(mainWin);
lay->addWidget(tableView);
#else
auto tableView = new QTableView;
#endif
MyModel myModel(nullptr);
tableView->setModel(&myModel);
tableView->setSelectionMode(QAbstractItemView::NoSelection);
tableView->setFocusPolicy(Qt::NoFocus);
configure_scoller_for_item_view(tableView);
#ifdef POPUP
mainWin->resize(500, 500);
mainWin->show();
#else
tableView->resize(500, 500);
tableView->show();
#endif
return a.exec();
}
Qt doesn't fully implement gestures in scrollable areas as explained in their own documentation:
Qt does not really reflect the system behavior wrt gestures on scrollable views (widget classes inheriting QAbstractItemView, QML classes) well.
[...]
In widgets, the pan recognizer is currently hard-coded to use 2 touch points. For touchscreens, it should be changed to one. But that can't be done as long as single-finger-panning is reserved for selecting text.
When using a touch screen, the selection in widgets is driven by mouse events synthesized from touch by the system (Windows) or Qt itself (other platforms). The same touch events drive QGestureManager.
On the other hand, there is a known (and old) undefined behavior with QTouchEvents and pop-up widgets:
The behavior of QTouchEvents is undefined when opening a pop-up or grabbing the mouse while there are more than one active touch points.
Probably the combination of both issues is the origin of your problem.
As a possible workaround (although not completely what you want), you can enable the two-fingers scroll with QWidget::grabGesture(Qt::PanGesture) as an alternative. Also, as mentioned by #mohammad-kanan in a comment, you may try with Qt::FramelessWindowHint | Qt::Tool instead of Qt::Popup.
Related
I'm wondering several things. I have subclassed QTableView to make a custom table. I'd like to be able to do several things.
First of all, I wanted the selected cells not to all have the "selected" color (blue by default) but instead have a frame around the selected cells (just like in Excel). To do this, I've used the following (in my custom QItemDelegate):
void MyDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
QModelIndex upIndex = index.sibling(index.row() - 1, index.column());
QModelIndex downIndex = index.sibling(index.row() + 1, index.column());
QModelIndex rightIndex = index.sibling(index.row(), index.column() + 1);
QModelIndex leftIndex = index.sibling(index.row(), index.column() - 1);
auto newOption = option;
if (option.state.testFlag(QStyle::State_Selected))
{
painter->save();
auto selIndexes = selM->selectedIndexes().toSet();
painter->setPen(QPen(Qt::red, 5));
if (!selIndexes.contains(rightIndex))
painter->drawLine(option.rect.topRight(), option.rect.bottomRight());
if (!selIndexes.contains(upIndex))
painter->drawLine(option.rect.topLeft(), option.rect.topRight());
if (!selIndexes.contains(downIndex))
painter->drawLine(option.rect.bottomLeft(), option.rect.bottomRight());
if (!selIndexes.contains(leftIndex))
painter->drawLine(option.rect.topLeft(), option.rect.bottomLeft());
painter->restore();
// newOption.palette.setBrush(QPalette::Normal, QPalette::Highlight, index.data(Qt::BackgroundRole).value<QColor>());
newOption.palette.setBrush(QPalette::Normal, QPalette::Highlight, Qt::gray);
}
QStyledItemDelegate::paint(painter, newOption, index);
}
This is probably not optimal, but I'd like to have something that works, before anything else.
Now it seems to be working, but it unfortunately isn't automatically repainted. What happens when I select cells is the following:
Which is not what I'm looking for at all. I guess it's not being repainted because (1) The points inside the zone are still red and (2) If I resize my window, I get the following:
Which is way closer to what I'm trying to achieve.
I've already tried to do this in my QTableView:
// selModel is my selection model
connect(selModel, &QItemSelectionModel::selectionChanged, [this]() {
for(const auto & selected : selModel->selectedIndexes())
{
update(visualRect(selected));
repaint(visualRect(selected));
}
}
(Before, I actually used setDirtyRegion but it didn't work either, so I figured I'd do something more... brutal.)
Finally, I have another question: why do I get those weird red "small lines" in my cell corners? Even on the "kind of" correct screenshot I get these lines that I can't explain:
Please suggest if you have any ideas for any of the issues.
The problem can be easily explained as follows.
Assume that the cell (0,0) is selected. Now the user selects the additional
cells (0,1), (1,0) and (1,1). Qt correctly repaints the additional 3 cells, that
became selected. But, it doesn't repaint the cell (0,0), as it was neither
selected nor deselected. Of course for your desired behavior you still need to redraw this cell.
This can easily be achieved by just redrawing all indices of your current selection.
MyDelegate.h
#pragma once
#include <QStyledItemDelegate>
#include <QItemSelectionModel>
class MyDelegate : public QStyledItemDelegate {
public:
virtual void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
void setSelectionModel(QItemSelectionModel* selectionModel);
private:
QItemSelectionModel* mSelectionModel{ nullptr };
};
MyDelegate.cpp
#include "MyDelegate.h"
#include <QPainter>
#include <QDebug>
void MyDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
if (!mSelectionModel) return;
auto newOption = option;
auto normalText = newOption.palette.brush(QPalette::ColorGroup::Normal, QPalette::ColorRole::Text);
newOption.palette.setBrush(QPalette::ColorGroup::Normal, QPalette::ColorRole::Highlight, QBrush(Qt::GlobalColor::blue, Qt::BrushStyle::NoBrush));
newOption.palette.setBrush(QPalette::ColorGroup::Normal, QPalette::ColorRole::HighlightedText, normalText);
QStyledItemDelegate::paint(painter, newOption, index);
QModelIndex upIndex = index.sibling(index.row() - 1, index.column());
QModelIndex downIndex = index.sibling(index.row() + 1, index.column());
QModelIndex rightIndex = index.sibling(index.row(), index.column() + 1);
QModelIndex leftIndex = index.sibling(index.row(), index.column() - 1);
//auto newOption = option;
//newOption.palette.setBrush(QPalette::Normal, QPalette::Highlight, Qt::transparent);
if (option.state.testFlag(QStyle::State_Selected))
{
painter->save();
painter->setClipRect(option.rect);
auto selIndexes = mSelectionModel->selectedIndexes().toSet();
painter->setPen(QPen(Qt::red, 5));
if (!selIndexes.contains(rightIndex))
painter->drawLine(option.rect.topRight(), option.rect.bottomRight());
if (!selIndexes.contains(upIndex))
painter->drawLine(option.rect.topLeft(), option.rect.topRight());
if (!selIndexes.contains(downIndex))
painter->drawLine(option.rect.bottomLeft(), option.rect.bottomRight());
if (!selIndexes.contains(leftIndex))
painter->drawLine(option.rect.topLeft(), option.rect.bottomLeft());
painter->restore();
}
}
void MyDelegate::setSelectionModel(QItemSelectionModel* selectionModel)
{
mSelectionModel=selectionModel;
}
main.cpp
#include <QApplication>
#include <QDebug>
#include <QStandardItemModel>
#include <QTableWidget>
#include "MyDelegate.h"
int main(int argc, char** args) {
QApplication app(argc, args);
auto widget = new QTableView;
QStandardItemModel model;
model.setRowCount(10);
model.setColumnCount(10);
for (auto i = 0; i < 10; i++) {
for (auto j = 0; j < 10; j++) {
model.setItem(i, j, new QStandardItem("Test"));
}
}
auto selModel = new QItemSelectionModel;
selModel->setModel(&model);
widget->setModel(&model);
widget->setSelectionModel(selModel);
auto delegate = new MyDelegate;
delegate->setSelectionModel(selModel);
widget->setItemDelegate(delegate);
// Ensures that also items are repainted, that where neither selected nor deselect, but will stay selected
// This solutions eventually paints elements twice
QObject::connect(selModel, &QItemSelectionModel::selectionChanged, [widget,selModel](auto &selected, auto& deselected) {
for (auto item : selModel->selectedIndexes()) {
widget->update(item);
}
});
widget->show();
app.exec();
}
As for the strange red line artifacts, you should paint the red line inside the allowed rectangle I guess. This is easily achieved by clipping to the item boundaries.
I am setting a QStyledItemDelegate on my model for a particular field, and returning a QComboBox from QStyledItemDelegate::createEditor
QComboBox* createEditor(QWidget* parent)
{
QComboBox* cb = new QComboBox(parent);
cb->addItem("UNDEFINED");
cb->addItem("TEST");
cb->addItem("OSE");
cb->addItem("TSE");
return cb;
}
void setEditorData(QWidget* editor, const QModelIndex& index)
{
QComboBox* cb = qobject_cast<QComboBox*>(editor);
if (!cb)
throw std::logic_error("editor is not a combo box");
QString value = index.data(Qt::EditRole).toString();
int idx = cb->findText(value);
if (idx >= 0)
cb->setCurrentIndex(idx);
cb->showPopup();
}
This is working fine, and when I select the field in question I am shown a combo box.
When I select an option from the drop-down list, the combobox closes and the item is displayed with a drop-down icon next to it:
At this point I would like the QStyledItemDelegate::setModelData function to be called, so that selecting an item in the list commits the data to the model.
However, I am required to first press Enter to commit the data (whereby the drop-down icon disappears)
void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index)
{
QComboBox* cb = qobject_cast<QComboBox*>(editor);
if (!cb)
throw std::logic_error("editor is not a combo box");
model->setData(index, cb->currentText(), Qt::EditRole);
}
Question:
How can I configure my QComboBox to automatically commit the data when the user selects an item in the list and the combobox list closes, rather than requiring the additional press of Enter?
You have to issue the signal commitData and closeEditor when an item is selected as shown in the following example:
#include <QApplication>
#include <QStandardItemModel>
#include <QListView>
#include <QStyledItemDelegate>
#include <QComboBox>
class ComboBoxDelegate: public QStyledItemDelegate{
public:
using QStyledItemDelegate::QStyledItemDelegate;
QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const{
Q_UNUSED(option)
Q_UNUSED(index)
QComboBox* editor = new QComboBox(parent);
connect(editor, QOverload<int>::of(&QComboBox::activated),
this, &ComboBoxDelegate::commitAndCloseEditor);
editor->addItems({"UNDEFINED", "TEST", "OSE", "TSE"});
return editor;
}
void setEditorData(QWidget *editor, const QModelIndex &index) const{
QComboBox* cb = qobject_cast<QComboBox*>(editor);
if (!cb)
throw std::logic_error("editor is not a combo box");
QString value = index.data(Qt::EditRole).toString();
int idx = cb->findText(value);
if (idx >= 0)
cb->setCurrentIndex(idx);
cb->showPopup();
}
private:
void commitAndCloseEditor(){
QComboBox *editor = qobject_cast<QComboBox *>(sender());
emit commitData(editor);
emit closeEditor(editor);
}
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QListView view;
QStandardItemModel model;
for(int i=0; i<10; i++){
model.appendRow(new QStandardItem("UNDEFINED"));
}
view.setItemDelegate(new ComboBoxDelegate(&view));
view.setModel(&model);
view.show();
return a.exec();
}
How to set checkbox on QTableWidget Header. How To Add Select All Checkbox in QHeaderView..
It does not displays a checkbox..
QTableWidget* table = new QTableWidget();
QTableWidgetItem *pItem = new QTableWidgetItem("All");
pItem->setCheckState(Qt::Unchecked);
table->setHorizontalHeaderItem(0, pItem);
Here, at Qt Wiki, it says there is no shortcut for it and you have to subclass headerView yourself.
Here is a summary of that wiki answer:
"Currently there is no API to insert widgets in the header, but you can paint the checkbox yourself in order to insert it into the header.
What you could do is to subclass QHeaderView, reimplement paintSection() and then call drawPrimitive() with PE_IndicatorCheckBox in the section where you want to have this checkbox.
You would also need to reimplement the mousePressEvent() to detect when the checkbox is clicked, in order to paint the checked and unchecked states.
The example below illustrates how this can be done:
#include <QtGui>
class MyHeader : public QHeaderView
{
public:
MyHeader(Qt::Orientation orientation, QWidget * parent = 0) : QHeaderView(orientation, parent)
{}
protected:
void paintSection(QPainter *painter, const QRect &rect, int logicalIndex) const
{
painter->save();
QHeaderView::paintSection(painter, rect, logicalIndex);
painter->restore();
if (logicalIndex == 0)
{
QStyleOptionButton option;
option.rect = QRect(10,10,10,10);
if (isOn)
option.state = QStyle::State_On;
else
option.state = QStyle::State_Off;
this->style()->drawPrimitive(QStyle::PE_IndicatorCheckBox, &option, painter);
}
}
void mousePressEvent(QMouseEvent *event)
{
if (isOn)
isOn = false;
else
isOn = true;
this->update();
QHeaderView::mousePressEvent(event);
}
private:
bool isOn;
};
int main(int argc, char **argv)
{
QApplication app(argc, argv);
QTableWidget table;
table.setRowCount(4);
table.setColumnCount(3);
MyHeader *myHeader = new MyHeader(Qt::Horizontal, &table);
table.setHorizontalHeader(myHeader);
table.show();
return app.exec();
}
Instead of above solution, You can simply put Push button in place of select all check box and give a name push button to "select all".
So If you press select all button then it called push button and go to whenever you go with push button(Here select all).
I have my custom implementation of QAbstractTableModel and of QSortFilterProxyModel, used for filtering. The table is shown in a QTableView.
The parent dialog of my QTableView has a QStatusBar, with a read-only QLineEdit widget.
In my overriding data() method of QAbstractTableModel, I'm setting the pertinent values for the Qt::StatusTipRole role.
Now I'm missing part of the plumbing: how do I get my per-cell StatusTipRole data to show up in my widget inside QStatusBar?
There is no need to override the view widget. Qt provides built-in support to show status tips for items in a model.
Normally, You just need to return a QString from your model's data() when role is QStatusTipRole, and that QString will be shown in the status bar when you hover your item.
You also need to turn on mouse tracking for the QTableView so that you get status bar updates without the mouse button being pressed. This is because when mouse tracking is disabled (by default), the widget receives mouse move events only while a mouse button is pressed.
Now, in order to display those status tips in your QLineEdit instead of the default status bar, You can override your main window's event function, intercept QStatusTipEvents, and show tips in your QLineEdit.
Here is an example implementation:
#include <QtWidgets>
//model to provide dummy data
class MyModel : public QAbstractTableModel{
public:
explicit MyModel(QObject* parent= nullptr):QAbstractTableModel(parent){}
~MyModel() = default;
int columnCount(const QModelIndex &parent) const{
if(parent.isValid()) return 0;
return 4;
}
int rowCount(const QModelIndex &parent) const{
if(parent.isValid()) return 0;
return 20;
}
QVariant data(const QModelIndex &index, int role) const{
QVariant val;
switch(role){
case Qt::DisplayRole: case Qt::EditRole:
val= QString("Display (%1, %2)")
.arg(index.row(), 2, 10, QChar('0'))
.arg(index.column(), 2, 10, QChar('0'));
break;
case Qt::ToolTipRole:
val= QString("Tooltip (%1, %2)")
.arg(index.row(), 2, 10, QChar('0'))
.arg(index.column(), 2, 10, QChar('0'));
break;
case Qt::StatusTipRole:
val= QString("StatusTip (%1, %2)")
.arg(index.row(), 2, 10, QChar('0'))
.arg(index.column(), 2, 10, QChar('0'));
break;
}
return val;
}
};
class MainWindow : public QMainWindow{
Q_OBJECT
public:
explicit MainWindow(QWidget* parent= nullptr):QMainWindow(parent){
//set up GUI
layout.addWidget(&lineEditFilter);
layout.addWidget(&tableView);
setCentralWidget(&cw);
lineEditStatusBar.setReadOnly(true);
statusBar()->addPermanentWidget(&lineEditStatusBar);
//set up models
filterModel.setSourceModel(&model);
tableView.setModel(&filterModel);
connect(&lineEditFilter, &QLineEdit::textChanged, this, &MainWindow::updateFilter);
//turn on mouse tracking for the table view
tableView.setMouseTracking(true);
}
~MainWindow()= default;
Q_SLOT void updateFilter(const QString& text){
filterModel.setFilterFixedString(text);
}
protected:
//in order to intercept QStatusTipEvents
//and show tips in the line edit instead of the normal status bar
bool event(QEvent *event){
if(event->type() != QEvent::StatusTip) return QMainWindow::event(event);
QStatusTipEvent* statusTipEvent= static_cast<QStatusTipEvent*>(event);
lineEditStatusBar.setText(statusTipEvent->tip());
statusTipEvent->ignore();
return true;
}
private:
QWidget cw;
QVBoxLayout layout{&cw};
QLineEdit lineEditFilter;
QTableView tableView;
MyModel model;
QSortFilterProxyModel filterModel;
QLineEdit lineEditStatusBar;
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow mw;
mw.show();
return a.exec();
}
#include "main.moc"
I derived a custem model from QAbstractItemModel and implemented drag&drop support by
overloading a flags() method that returns Qt::ItemIsDragEnabled where applicable,
overloading supportedDropActions and
implementing mimeData() to properly return a QMimeData object with some string set for the text/plain format.
When I setup this model in a QTreeView and enable dragging via setDragEnabled(true), I can pick items from the tree view and drag them. For example, I can drag an item and drop it onto a QLineEdit, which in turn will display the string that the model returned via mimeData().
However, when I drag an item and drop it onto another application's widgets (such as notepad or simply a second instance of my application) an empty string gets dropped.
I found another clue using the Drop site example shipped with Qt: When I drag an item over it, it correctly shows that there is text/plain available and it even displays the string associated with it. Yet, when I drop the item I get an empty string again in the dropEvent()...
Sourcecode
ssce.pro:
QT += core gui widgets
TARGET = ssce
TEMPLATE = app
SOURCES += main.cpp
main.cpp:
#include <QtCore>
#include <QtWidgets>
struct Model: public QAbstractItemModel {
int rowCount(const QModelIndex &parent) const {
return parent.isValid() ? 0 : 1;
}
int columnCount(const QModelIndex &parent) const {
return 1;
}
QModelIndex index(int row, int column, const QModelIndex &parent) const {
return parent.isValid() ? QModelIndex() : createIndex(row, column, static_cast<void *>(0));
}
QModelIndex parent(const QModelIndex &child) const {
return QModelIndex();
}
Qt::ItemFlags flags(const QModelIndex &index) const {
return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled;
}
QVariant data(const QModelIndex &index, int role) const {
return (role == Qt::DisplayRole) ? "Test item" : QVariant();
}
QStringList mimeTypes() const {
return QStringList("text/plain");
}
QMimeData *mimeData(const QModelIndexList &indexes) const {
QMimeData *d = new QMimeData();
d->setText("hello world");
return d;
}
Qt::DropActions supportedDropActions() const {
return Qt::CopyAction;
}
};
int main(int argc, char *argv[]) {
QApplication a(argc, argv);
QDialog d;
QVBoxLayout l;
QLineEdit e;
l.addWidget(&e);
QTreeView v;
Model m;
v.setModel(&m);
v.setDragEnabled(true);
l.addWidget(&v);
d.setLayout(&l);
d.show();
return a.exec();
}
Description:
Run this and you can drag the item from the tree view into the line edit, it will display hello world then. Drop the item anywhere else, including the line edit of a second instance of the sample application, and nothing will happen.