Qt - There is a bug in QPropertyAnimation? - c++

I face a very serious situation. By writing this question I hope that really professionals will express their opinion regarding to the problem I am going to describe. I have reported a bug in https://bugreports.qt.io/ :
I have created QPropertyAnimation for maximumWidth property of QTextEdit and it does not work (it immediately changes state from starting state to the end state), though it works for minimumWidth property.
Please see the attached code.
And have attached .h and .cpp files. See those files here (files are named new.h and new.cpp).
And I got the follwing response:
MaximumWidth is not the property you want to animate. It holds the maximum width that the widget can have, it's related to layouting and so on. Changing the maximumWidth (as well as the minimumWidth) does not necessarily trigger a relayout and repaint. You should animate the size.
Please explain me whether it is a bug or no? Please tell me how the minimumWith property is being animated but when it concerns to the maximumWidth property, then I should not work and that is OK? I just don't get their point... Please explain.
P.S. I have written this code because I wanted to close by animation the right QTextEdit and be sure that when I resize the main window, where the button and two QTextEdit are, the closed QTextEdit does not being restored.

Did you check the actual value of maximumWidth? You don't seem to set a specific maximumWidth in your code.
The default value for maximumWidth is 16777215, and you set a duration of 1 msec. for the closing animation. Fading from 16777215 to 3 in 1 msec. would look like "instant", I guess.

I don't think it is a bug; I'd call it "undefined behavior". That means that if you try to animate minimumWidth, nobody can tell you for sure what is supposed to happen, and maybe the code has some optimizations or corner cases where sometimes it works, others it doesn't.
Anyway, minimumWidth and maximumWidth aren't supposed to be used to define what the size of a QWidget is, only what it must not exceed; i.e. they weren't designed to do what you are trying to do, so it can be called a bug. If you want to animate, you have to use a deterministic approach, which in this case is using the geometry property.

This is not a bug, the response you got from the bug report pretty well explains the problem with your code and a solution.

Dear Sofahamster I have changed my code to the code below and it works fine. Thanks for your hint!
Header file
class MyWidget : public QWidget
{
Q_OBJECT
QTextEdit *m_textEditor1;
QTextEdit *m_textEditor2;
QPushButton *m_pushButton;
QHBoxLayout *m_layout;
QVBoxLayout *m_buttonLayout;
int m_deltaX;
bool m_isClosed;
public:
MyWidget(QWidget * parent = 0);
~MyWidget(){}
void resizeEvent( QResizeEvent * event );
private slots:
void closeOrOpenTextEdit2(bool isClosing);
};
Source file
MyWidget::MyWidget(QWidget * parent):QWidget(parent),m_deltaX(0)
{
m_pushButton = new QPushButton(this);
m_pushButton->setText(">");
m_pushButton->setCheckable(true);
m_pushButton->setFixedSize(16,16);
connect(m_pushButton, SIGNAL(clicked(bool)), this, SLOT(closeOrOpenTextEdit2(bool)));
m_textEditor1 = new QTextEdit(this);
m_textEditor1->setText("AAAAA AAAAAAAAAAA AAAAAAAAAAA AAAAAAA AAAAAAAAAAA AAAAAAAAAAA AA");
m_textEditor2 = new QTextEdit(this);
m_buttonLayout = new QVBoxLayout();
m_buttonLayout->addWidget(m_pushButton);
m_buttonLayout->addItem( new QSpacerItem(1, 1, QSizePolicy::Minimum, QSizePolicy::Expanding) );
m_layout = new QHBoxLayout;
m_layout->addWidget(m_textEditor1, 10);
m_layout->addSpacing(15);
m_layout->addLayout(m_buttonLayout);
m_layout->setSpacing(0);
m_layout->addWidget(m_textEditor2, 4);
setLayout(m_layout);
resize(800,500);
}
void MyWidget::closeOrOpenTextEdit2(bool isClosing)
{
m_isClosed = isClosing;
QPropertyAnimation *animation1 = new QPropertyAnimation(m_textEditor2, "maximumWidth");
if(isClosing) //close the second textEdit
{
m_textEditor2->setMaximumWidth(m_textEditor2->width());
int textEdit2_start = m_textEditor2->maximumWidth();
m_deltaX = textEdit2_start;
int textEdit2_end = 3;
animation1->setDuration(500);
animation1->setStartValue(textEdit2_start);
animation1->setEndValue(textEdit2_end);
m_pushButton->setText("<");
}
else //open
{
int textEdit2_start = m_textEditor2->maximumWidth();
int textEdit2_end = m_deltaX;
animation1->setDuration(500);
animation1->setStartValue(textEdit2_start);
animation1->setEndValue(textEdit2_end);
m_pushButton->setText(">");
}
animation1->start();
}
void MyWidget::resizeEvent( QResizeEvent * event )
{
if(!m_isClosed)
m_textEditor2->setMaximumWidth( QWIDGETSIZE_MAX );
}

Related

unable to make an intro before showing anything in the window and after the window is polished

I am working on Unix 19.02 and Qt 5.13.0 .
I am trying to make an introduction for my application before anything is shown up in the window. So, what I thought is to make the QPainter change the background color in a while-loop. I tried it too many times and I had a problem in the timing : "When is the moment or the event that I should make the qpainter start the introduction?". I searched and found out the best thing is to reimplement this function :
void showEvent(QShowEvent* event) {...}
inside that function, I call "repaint()", and it will do all the introduction.
I tried all of these ways :
Making the while-loop inside the "paintEvent" function.
Making the while-loop outside the "paintEvent" function, and every time we change the color of the background I call "repaint()". Surely I made a small timeout for the computer to slow it down a bit.
-Note: using these two ways above, the window shows up, but it's background-color is black and nothing shows up neither the color doesn't change until the introduction is finished!
Lastely, a while-loop that does not use "QPainter", it uses stylesheet instead and after we change the "background-color" property. We call : ui->centeralWidget->style->polish(ui->centeralWidget);
The last way, forces the window not to show up until the introduction is finished completely (it takes 5 seconds to execute the introduction, the while-loop). this is the code :
QString ss; // for opacity
QString ss_prefix = "background-color: rgba(255, 255, 255, ";
QString ss_suffix = ");";
while(i<=50) {
i++;
Opacity += 5;
qDebug() << "Changing opacity : " << ss_prefix+std::to_string(Opacity).c_str()+ss_suffix;
ui->centralWidget->setStyleSheet(
ss_prefix+std::to_string(Opacity).c_str()+ss_suffix);
ui->centralWidget->style()->polish(ui->centralWidget);
QThread::currentThread()->msleep(100); // 100 * 50 = 5000/1000 = 5 seconds to execute!
}
Nevertheless, there are other ways to make an introduction, but I really would want to know what is happening and fix this problem!
The last equation is meant to calculate the time to execute :
100(sleep) * 50(The code will get executed 50 times) = 5000 / 1000 (it is in milli, so divide by 1000) = 5 seconds
Wish you understood my situation.
You're not providing complete code, so I am partially guessing here.
You need to wait for the main app event loop to get started before starting your animation. If you call that animation routine (with the msleep() at the end) from the QMainWindow (or whatever your top-level QWidget is) constructor, the main window is never given a chance to show itself (until the animation is done).
If I'm right, you could consider starting your animation from the first QWidget::showEvent() by using a bool flag to know when the first show happens. Another way would be to start a QTimer::singleShot(0, this, &MainWindow::animate) in the constructor (the timer will trigger once the main event loop starts). There are other ways, but the main point is that you need to let the QApplication event loop to be running and the window to have already been initialized.
Having said all that, I'd probably take a different approach, maybe with a QSplashScreen or a custom QWidget to show the animation before, or while, loading the main window in the background.
Also, look into QString template/replacement system, specifically the arg() methods and how they can be used to simplify string construction.
As #MaximPaperno points out, the problem is while-loop + msleep() since they block the eventloop preventing the GUI from working normally. For these cases, Qt provides strategies such as QTimer, QXAnimation, etc. In this case a simple solution is to use QVariantAnimation:
*.h
// ...
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr);
~MainWindow();
private Q_SLOTS:
void applyOpacity(const QVariant &value);
// ...
*.cpp
// ...
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
QVariantAnimation *animation = new QVariantAnimation(this);
animation->setStartValue(0);
animation->setEndValue(255);
animation->setDuration(5 * 1000);
connect(animation, &QVariantAnimation::valueChanged, this, &MainWindow::applyOpacity);
animation->start(QAbstractAnimation::DeleteWhenStopped);
}
// ...
void MainWindow::applyOpacity(const QVariant & value)
{
bool ok;
int opacity = value.toInt(&ok);
if(ok) {
QString qss = QString("background-color: rgba(255, 255, 255, %1)").arg(opacity);
ui->centralWidget->setStyleSheet(qss);
}
}
It's a trivial situation. Just show welcome window first. When it is closed, show your main window. Use signals to achieve it.

QGroupBox find selected Radio Button

I have created a simple UI consisting of a QGroupBox with a bunch of QRadioButtons (32 to be exact) and I want to be able to find the selected one.
I've looked at forums and things, but the answers I've found don't work, and one referenced documentation on a nonexistant method of QGroupBox.
Given the below snippet, how would I find the selected QRadioButton, if any?
QGroupBox* thingGroup = ui->thingGroupBox;
If you want to get it when you select one of them you could use the toogled signal, connect it to some slot and use the sender () function and convert it to QRadioButton.
*.h
public slots:
void onToggled(bool checked);
*.cpp
QGroupBox *thingGroup = ui->groupBox;
QVBoxLayout *lay = new QVBoxLayout;
thingGroup->setLayout(lay);
for(int i = 0; i < 32; i++){
QRadioButton *radioButton = new QRadioButton(QString::number(i));
lay->addWidget(radioButton);
connect(radioButton, &QRadioButton::toggled, this, &{your Class}::onToggled);
}
slot:
void {your Class}::onToggled(bool checked)
{
if(checked){
//btn is Checked
QRadioButton *btn = static_cast<QRadioButton *>(sender());
}
}
I guess it is easier to identify which button is checked using a QButtonGroup, just remember to select buttons in "Design mode" then right-click and select assign to button group :
To identify the checked button, you can use QButtonGroup's checkedButton method:
QAbstractButton *button = ui->buttonGroup->checkedButton();

Strange behavior with QTreeView / QFileSystemModel / QLineEdit

I have a QTreeView linked to a QFileSystemModel. I also have a QLineEdit with two purposes :
when the user click the QTreeView, the QLineEdit is populate with the item clicked (path & filename if applied)
when the user edit the QLineEdit and press enter, I want to :
a. synchronize the QTreeView to that position if the path exist
b. if the path do not exist, I want to repopulate the QLineEdit with the current position of the QTreeView
Everything is working well except when the condition 2a above is met, I want to :
return focus on QTreeView
expand the current folder
resize the 1st column to contents
and scroll the view to that index (scroll to top)
Here is the code I put in place :
QDirectorySelector::QDirectorySelector(QString const & title, QWidget *parent)
: QWidget(parent)
{
mDirectoryModel = new QFileSystemModel;
mDirectoryModel->setFilter(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot);
mDirectoryModel->setRootPath("");
mDirectoryTitle = new QLabel(title);
mDirectoryView = new QTreeView;
mDirectoryView->setModel(mDirectoryModel);
mDirectoryView->setSortingEnabled(true);
mDirectoryView->sortByColumn(0, Qt::AscendingOrder);
mDirectoryView->setSelectionMode(QAbstractItemView::ExtendedSelection);
mDirectoryEdit = new QLineEdit;
QVBoxLayout * layout = new QVBoxLayout;
layout->addWidget(mDirectoryTitle);
layout->addWidget(mDirectoryView);
layout->addWidget(mDirectoryEdit);
layout->setContentsMargins(0, 0, 0, 0);
setLayout(layout);
connect(mDirectoryView, &QTreeView::clicked, this, &QDirectorySelector::synchronizeEditFromTree);
connect(mDirectoryEdit, &QLineEdit::returnPressed, this, &QDirectorySelector::synchronizeTreeFromEdit);
connect(this, &QDirectorySelector::synchronizationDone, this, &QDirectorySelector::reactToExpansionStep1, Qt::QueuedConnection);
connect(this, &QDirectorySelector::synchronizationStep1Done, this, &QDirectorySelector::reactToExpansionStep2, Qt::QueuedConnection);
}
void QDirectorySelector::synchronizeEditFromTree(QModelIndex const & index)
{
QString directory{ mDirectoryModel->fileInfo(index).absoluteFilePath() };
mDirectoryEdit->setText(directory);
}
void QDirectorySelector::synchronizeTreeFromEdit()
{
if (QDir(mDirectoryEdit->text()).exists()) {
mDirectoryView->setFocus();
mDirectoryView->setCurrentIndex(mDirectoryModel->index(mDirectoryEdit->text()));
mDirectoryView->setExpanded(mDirectoryView->currentIndex(), true);
emit synchronizationDone(mDirectoryView->currentIndex());
} else {
selectDirectory(mDirectoryView->currentIndex());
}
}
void QDirectorySelector::reactToExpansionStep1(QModelIndex const & index)
{
mDirectoryView->resizeColumnToContents(0);
emit synchronizationStep1Done(mDirectoryView->currentIndex());
}
void QDirectorySelector::reactToExpansionStep2(QModelIndex const & index)
{
mDirectoryView->scrollTo(mDirectoryView->currentIndex(), QAbstractItemView::PositionAtTop);
}
If I type an existing path and press enter I got this unexpected behavior :
the focus is done (ok)
the current index of the view is done correctly (ok)
the expension of the sub folder is done (ok)
the resize of the 1st column is done only at the path level without including the expended part (not expected)
the scroll to is not done at all (not ok)
At this point, if I return to the QLineEdit and press enter again, everything is done correctly. In fact, I observed this pattern, if the specified folder was never "open" (seen/loaded/expended), I get the unexpected behavior. If the specified was "open" (seen/loaded/expended), I get the expected behavior.
I also try to call two times the code in the finishDirectoryEditing without any success. Everything is the same.
Is anyone have any Idea of what is happening here? And better, any suggestion to solve this unexpected behavior.
I think that the QTreeView working with the QFileSystemModel is already too awkward in term of behavior. I mean, it never hide the expansion mark of an empty folder. May be it's possible to change this but I got no success either with the fetch more strategy I found elsewhere on the net.
Thanks to all!

Crash while calling setItemWidget

I am using a QTreeWidget and setting a widget for the QTreeWidgetItem in the QTreeWidget. It is working fine but when I do the same for second time, the application is crashing.
The below is working fine.
QTreeWidget* treewidget = new QTreeWidget();
QTreeWidgetItem* item0 = new QTreeWidgetItem((QTreeWidget*)0, QStringList(QString("item0")));
treewidget->insertTopLevelItem(0,item0);
QSlider* slider0 = new QSlider();
treewidget->setItemWidget(item0, 0, slider0);
But if I add the last line once again, it is crashing when running the application.
The below is crashing.
QTreeWidget* treewidget = new QTreeWidget();
QTreeWidgetItem* item0 = new QTreeWidgetItem((QTreeWidget*)0, QStringList(QString("item0")));
treewidget->insertTopLevelItem(0,item0);
QSlider* slider0 = new QSlider();
treewidget->setItemWidget(item0, 0, slider0);
treewidget->setItemWidget(item0, 0, slider0); // Intentionally added to simulate the issue
The above is an example to show the issue, but in my application, based on some events, I delete the tree widget items and add it later. When I set the item widget (after adding the items later), I am getting the crash.
I could not figure out why. Any ideas? FYI, I am using Qt 5.3.2 MSVC 2010, 32 bit.
treewidget->setItemWidget(item0, 0, slider0);
treewidget->setItemWidget(item0, 0, slider0);// Intentionally added to simulate the issue
I look at Qt code (4.x):
void QTreeWidget::setItemWidget(QTreeWidgetItem *item, int column, QWidget *widget)
{
Q_D(QTreeWidget);
QAbstractItemView::setIndexWidget(d->index(item, column), widget);
}
and QAbstractItemView::setIndexWidget:
void QAbstractItemView::setIndexWidget(const QModelIndex &index, QWidget *widget)
{
Q_D(QAbstractItemView);
if (!d->isIndexValid(index))
return;
if (QWidget *oldWidget = indexWidget(index)) {
d->persistent.remove(oldWidget);
d->removeEditor(oldWidget);
oldWidget->deleteLater();
}
so if you add slider0 two times, then at first call it was added,
at seconds call Qt call for it deleteLater, and then added it,
are sure that this is what you want?
You have to set correct parent in the constructor of QTreeWidgetItem. Try this:
QTreeWidgetItem* item0 = new QTreeWidgetItem(treewidget);
Also it is important to understand who is owner of the slider0 after calling of setItemWidget(): the owner is your table, so 1) you don't need to delete this object; 2) the object will be deleted if you call setItemWidget for the same cell again. So, double call of treewidget->setItemWidget(item0, 0, slider0); seems very strange (second time you are setting the deleted object into that cell).

How to make a QMdiArea subwindow widget non-resizeable?

So the non-QMdiArea version of my code,
MyWidget::MyWidget(QWidget* parent)
{
...
layout()->setSizeConstraint( QLayout::SetFixedSize );
}
MainWindow::MainWindow(...)
{
...
MyWidget* wgt = new MyWidget(NULL);
wgt->show();
}
works just fine and produces a widget that the user can't resize. But when the MainWindow code is replaced with
MainWindow::MainWindow(...)
{
...
MyWidget* wgt = new MyWidget(ui->mdiArea); //Or MyWidget(NULL), same result
ui->mdiArea->addSubWindow(wgt);
}
the window, now within the QMdiArea, is resizeable. It doesn't seem to be an issue of Qt::WindowFlags, they don't handle resize policy. Surely there is a way to do this? NB I cant use something like setFixedSize(ht, wd) since the size of the widget can change programmatically (subwidgets are added and removed). But the user should not be able to resize it.
Even though MyWidget is not resizeable, when you call:
ui->mdiArea->addSubWindow(wgt);
The widget is put in a QMdiSubWindow which is resizeable. All you have to do is get the window that's created and fix its size:
QMdiSubWindow* subWindow = ui->mdiArea->addSubWindow(wgt);
subWindow->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
This should work, but I haven't tried this code myself.
EDIT: well... apparently that doesn't fix the size. :(
The following worked for me:
MyWidget* wgt = new MyWidget(ui->mdiArea);
QMdiSubWindow* subWindow = ui->mdiArea->addSubWindow(wgt);
subWindow->setFixedSize(wgt->size());
wgt->show();