PDA

View Full Version : How to shrink a QMainWindow when its central widget is shrinked



franky
21st July 2019, 17:15
Hi all,

Please take a look at these files:

List.h:


#ifndef LIST_H
#define LIST_H

#include <QMainWindow>
#include "task.h"
#include <QVector>

class QLabel;
class QPushButton;
class QHBoxLayout;
class QVBoxLayout;

class List : public QMainWindow
{
Q_OBJECT

public:
explicit List(QWidget *parent = nullptr);
void updateStatus();
~List();

public slots:
void addTask();
void removeTask(Task*);
void taskStatusChanged();

private:
QVector<Task*> mTasks;
QLabel* statusLabel;
QPushButton* addTaskButtun;
QHBoxLayout* hBox;
QVBoxLayout* vBox;
};

#endif // LIST_H


List.cpp:


#include "list.h"
#include <QPushButton>
#include <QLabel>
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QString>
#include <QInputDialog>
#include <QLineEdit>

List::List(QWidget *parent)
: QMainWindow(parent), mTasks()
{
statusLabel = new QLabel(tr("Status: 0 todo / 0 done"));
addTaskButtun = new QPushButton(tr("Add task"));

hBox = new QHBoxLayout;
hBox->addWidget(statusLabel);
hBox->addStretch();
hBox->addWidget(addTaskButtun);

vBox = new QVBoxLayout;
vBox->addLayout(hBox);
vBox->addStretch();

QWidget *widget = new QWidget;
widget->setLayout(vBox);
setCentralWidget(widget);

connect(addTaskButtun, &QPushButton::clicked, this, &List::addTask);
updateStatus();
}

//************************************************

void List::addTask() {
bool ok;
QString name = QInputDialog::getText(this, tr("Add Task"), tr("Task name"),
QLineEdit::Normal, tr("Untitled task"), &ok);

if(ok && !name.isEmpty()) {
Task* task = new Task(name);
connect(task, &Task::removed, this, &List::removeTask);
connect(task, &Task::statusChanged, this, &List::taskStatusChanged);
mTasks.append(task);
vBox->addWidget(task);
updateStatus();
}
}

//*********************************************

void List::removeTask(Task* task) {
mTasks.removeOne(task);
vBox->removeWidget(task);
delete task;
updateStatus();
}

//**************************************

void List::taskStatusChanged() {
updateStatus();
}

//************************************

void List::updateStatus() {
int completedCount = 0;

for(auto t: mTasks)
if(t->isCompleted())
completedCount++;

int todoCount = mTasks.size() - completedCount;
statusLabel->setText(QString("Statuse: %1 todo / %2 completed")
.arg(todoCount) .arg(completedCount));
}

//*****************************

List::~List()
{
delete vBox;
}



Task.h:



#ifndef TASK_H
#define TASK_H

#include <QWidget>
#include <QString>

class QCheckBox;
class QPushButton;
class QHBoxLayout;

class Task : public QWidget
{
Q_OBJECT

public:
explicit Task(const QString&, QWidget* parent = nullptr);

void setName(const QString&);
QString name() const;
bool isCompleted() const;

~Task();

public slots:
void rename();

signals:
void removed(Task*);
void statusChanged(Task*);

private slots:
void checked(bool);

private:
QCheckBox* checkbox;
QPushButton* editButton;
QPushButton* removeButton;
QHBoxLayout* hBox;
};

#endif // TASK_H


Task.cpp:



#include "task.h"
#include "list.h"
#include <QInputDialog>
#include <QWidget>
#include <QHBoxLayout>
#include <QPushButton>
#include <QCheckBox>

Task::Task(const QString& name, QWidget* parent) : QWidget (parent)
{
checkbox = new QCheckBox;
editButton = new QPushButton(tr("Edit"));
removeButton = new QPushButton(tr("Remove"));

hBox = new QHBoxLayout;
hBox->addWidget(checkbox);
hBox->addStretch();
hBox->addWidget(editButton);
hBox->addWidget(removeButton);

setLayout(hBox);

setName(name);

connect(editButton, &QPushButton::clicked, this, &Task::rename);
connect(removeButton, &QPushButton::clicked, [this]()->void {
emit removed(this);
});
connect(checkbox, &QCheckBox::toggled, this, &Task::checked);
};

//*************************************

void Task::setName(const QString& name) {
checkbox->setText(name);
}

//********************************************

QString Task::name() const {
return checkbox->text();
}

//*****************************

bool Task::isCompleted() const {
return checkbox->isChecked();
}

//****************************************

void Task::rename() {
bool ok;
QString value = QInputDialog::getText(this, tr("Edit task"), tr("Task name"),
QLineEdit::Normal, this->name(), &ok);
if(ok && !value.isEmpty())
setName(value);
}

//************************************

void Task::checked(bool checked) {
QFont font(checkbox->font());
font.setStrikeOut(checked);
checkbox->setFont(font);

emit statusChanged(this);
}

//**************************************************

Task::~Task() {
delete hBox;
}


When I add tasks "untitled task 1" to "untitled task 3", image number 1 below:

13193

then remove "untitled task 2", the frame/window doesn't shrink and the space for that task is just vacant, image number 2. I like it to decrease and fit the remained tasks, like image number 3. When a task is added, the window increases in size, so when one is removed, it should decrease in size too.

I tested the followings one by one in void List::updateStatus() {, but none worked as expected!

What should be used instead, please?



this->sizeHint();
this->update();
this->resize(sizeHint());
this->adjustSize();

d_stranz
21st July 2019, 23:28
I think it is because in the List constructor, you add stretch to the vbox layout. The Task widgets you add go below that, so when you remove one, the stretch just expands to fill the empty space in between the hbox and the first remaining task.

Try not using the stretch at all. If that doesn't work, try using QBoxLayout::insertWidget() instead of addWidget() (which appends the new widget to the end of the layout, thus "trapping" the stretch between it and the start of the layout). In this case, your code would look something like this (assuming you still add the stretch during construction):



vbox->insertWidget( vbox->count() - 1, task );


I don't know if this will solve the non-shrink problem, but at least you won't have a gap when you remove a task because the stretch will always be at the end of the layout.

franky
22nd July 2019, 07:10
Neither worked unfortunately! :(

Without vBox->addStretch();, shrink won't be applied but merely we have a sparsely populated area, after removing some tasks:

13194

And when I keep vBox->addStretch();, and use vBox->insertWidget(vBox->count() - 1, task); instead of vBox->addWidget(task);, we will have this, once again:

13195

d_stranz
22nd July 2019, 16:44
Yes, your second screen shot is what I expected to see.

What you will probably have to do is implement a resize() on your List window when you remove a Task. In that slot, get the size of the List window -before- removing the Task, get the size of the Task itself, remove it, then resize the List height to be the original height minus the Task height.

franky
22nd July 2019, 17:10
Good, thanks. There's always a way in programming! :)

I added the slot in List, but I think inside it, vBox should be resized, not? (And not the central widget of QMainWindow)

d_stranz
22nd July 2019, 20:31
The QMainWindow is in charge of resizing its contents. I am pretty sure it asks the layout for the central widget for its size hint and adds the frame, menu bars, toolbars, status bars sizes as appropriate. So if the vbox layout never shrinks, it will return a size hint that includes the empty space.

But try it out to resize the vbox. If it works, it works. Riht now, I don't see any method in the QVBoxLayout class (or its base classes) that allows you to directly set the size.

franky
23rd July 2019, 16:35
Thanks.

I was actually much dealt with height() and width() in the List.cpp file, but these two both belong to the central widget and hence, I couldn't make the job done using them.
How to access the height/width of the base class (QMainWindow) which is indeed in charge of the overall size, please, so that we can modify it when a remove is accomplished?

d_stranz
23rd July 2019, 19:21
Your List class is derived from QMainWindow, isn't it? If your slots to add / remove new Task widgets are in the List class, then you can just call "height()" in those slots and you will get the height of the List widget. You then just call resize() after you compute the new height.

You don't want to change the height using the pointer to the central widget because QMainWindow (List) and the layout won't let it happen.

franky
24th July 2019, 08:50
Your List class is derived from QMainWindow, isn't it? If your slots to add / remove new Task widgets are in the List class, then you can just call "height()" in those slots and you will get the height of the List widget. You then just call resize() after you compute the new height.

I added resize(height() - 100, width()) to removeTask, (-100, just to test):


void List::removeTask(Task* task) {
mTasks.removeOne(task);
vBox->removeWidget(task);
delete task;
updateStatus();

resize(height() - 100, width());
}

When I hover the mouse on height(), this message is shown:
inline int QWidget::height() const , so that QWidget in our example is List, right?

So in that slot, when we use resize via a smaller height, the widget List must resize normally, and since this slot is called each time we remove a Task, therefore List is expected to shrink that way.

But it doesn't work as expected!

d_stranz
24th July 2019, 17:52
But it doesn't work as expected!

Well, what does happen? Anything? Nothing?

You should probably call "task->deleteLater()" instead of "delete task". You are in the middle of a slot, and control needs to return to the event loop so that Qt can clean up properly and take whatever other action is needed to redraw the UI. deleteLater() will delete the widget when the event loop runs.

franky
25th July 2019, 08:50
Very good, thanks for the precious info. I use the function this way:


void List::removeTask(Task* task) {
mTasks.removeOne(task);
vBox->removeWidget(task);
task->deleteLater();
updateStatus();

resize(height() - 100, width());
}

After entering 5 tasks:

13198

And then deleting the first four ones, I got this! It's actually the way I meant I doesn't work. :(

13199

d_stranz
26th July 2019, 00:57
Your last screen shot looks like you still have the spacer item as the first thing in the vbox. If the spacer item was at the end, task 5 would be pushed to the top of the layout. Instead, it looks like the spacer item is first and is pushing everything down.

Please show your code for adding a new task to the list.

franky
26th July 2019, 08:44
Your last screen shot looks like you still have the spacer item as the first thing in the vbox.

Yes. We firstly have a button and label of the list on the top, and to hold it on top we need the spacer which comes after these. Afterwards, in the addTask slot, we have this code, as before:



void List::addTask() {
bool ok;
QString name = QInputDialog::getText(this, tr("Add Task"), tr("Task name"),
QLineEdit::Normal, tr("Untitled task"), &ok);

if(ok && !name.isEmpty()) {
Task* task = new Task(name);
connect(task, &Task::removed, this, &List::removeTask);
connect(task, &Task::statusChanged, this, &List::taskStatusChanged);
mTasks.append(task);
vBox->addWidget(task);
updateStatus();
}
}

d_stranz
26th July 2019, 18:03
We firstly have a button and label of the list on the top, and to hold it on top we need the spacer which comes after these.

Well, that's the problem. Your code calls addWidget() instead of insertWidget() (see my post from earlier in this thread), which means that the spacer stays at the top of the vbox and pushes both up and down. The button and label get pushed up and the tasks get pushed down. When you add new tasks, this results in the window expanding. When you remove a task, the spacer just expands to add more space between the top and the remaining tasks and the window doesn't shrink.

When you add a task, you need to insert it -above- the spacer so that the spacer is always the last thing in the vbox. When you remove a task, you need to resize the window so the spacer will shrink.

franky
27th July 2019, 08:39
Yes, thank you. These are right in theory, but when tested in practice, the result is quite different!

I did these:
Firstly removed vBox->addStretch(); in List's constructor. Then changed void List::addTask() to this:



void List::addTask() {
bool ok;
QString name = QInputDialog::getText(this, tr("Add Task"), tr("Task name"),
QLineEdit::Normal, tr("Untitled task"), &ok);

if(ok && !name.isEmpty()) {
Task* task = new Task(name);
connect(task, &Task::removed, this, &List::removeTask);
connect(task, &Task::statusChanged, this, &List::taskStatusChanged);
mTasks.append(task);
vBox->insertWidget(vBox->count()-1, task);
vBox->addStretch();
updateStatus();
}
}

And here's also:


void List::removeTask(Task* task) {
mTasks.removeOne(task);
vBox->removeWidget(task);
task->deleteLater();
updateStatus();

resize(height() - 100, width());
}

The result is:

13204

Why don't you test the project once?

d_stranz
29th July 2019, 18:16
Why don't you test the project once?

Because I have my own code to write...

In your addTask() slot, lines 11 and 12, you are adding a new stretch item every time you add a new task, but in the removeTask() slot, you are only removing the task widget. So you end up with a vbox that contains one stretch item for every task you add, and they never go away. So when you call insertWidget(), you are probably putting the new task in between two stretch items and then sticking a new stretch item onto the end of that.

Going back to your original post in this thread, you are building the central widget by:

1 - creating a generic QWidget to act as the central widget.
2 - adding a vbox as its layout.
3 - inserting an hbox layout with your buttons, etc. as the first item in the vbox
4 - adding a stretch to the end of the vbox to push the hbox to the top of the vertical layout

OK so far. Your desired behavior is for the main window to grow as you add Task items (widgets) to it and to shrink as they are removed.

To add a new task, you want to insert it between the stretch item (which is always at the bottom of the vbox) and whatever is above it (the hbox, more tasks, whatever).

After the initial construction of the vbox, the item count is 2 (the hbox and the stretch). So you want to call insertWidget() with the argument count - 1. You do not add new stretch and you do not change the position of the stretch that you added at the start. It always stays at the bottom of the vbox and everything else goes above it and below the hbox.

To add more tasks to the list, you do exactly the same - insertWidget with count - 1 as the position argument.

To remove a task, your existing code should be OK. Find the task widget in the vbox, take it, then resize the vbox to remove the space taken up by the widget you just removed. The stretch still stays at the bottom and the hbox at the top, unchanged.

franky
30th July 2019, 08:24
Only these parts are changed compared to the first code:


List::List(QWidget *parent)
: QMainWindow(parent), mTasks()
{
statusLabel = new QLabel(tr("Status: 0 todo / 0 done"));
addTaskButtun = new QPushButton(tr("Add task"));
cWidget = new QWidget;

hBox = new QHBoxLayout;
hBox->addWidget(statusLabel);
hBox->addStretch();
hBox->addWidget(addTaskButtun);

vBox = new QVBoxLayout;
vBox->addLayout(hBox);
vBox->addStretch();

cWidget->setLayout(vBox);
setCentralWidget(cWidget);

connect(addTaskButtun, &QPushButton::clicked, this, &List::addTask);
updateStatus();
}

//************************************************

void List::addTask() {
bool ok;
QString name = QInputDialog::getText(this, tr("Add Task"), tr("Task name"),
QLineEdit::Normal, tr("Untitled task"), &ok);

if(ok && !name.isEmpty()) {
Task* task = new Task(name);
connect(task, &Task::removed, this, &List::removeTask);
connect(task, &Task::statusChanged, this, &List::taskStatusChanged);
mTasks.append(task);
vBox->insertWidget(vBox->count()-1, task);
updateStatus();
}
}

//*********************************************

void List::removeTask(Task* task) {
mTasks.removeOne(task);
vBox->removeWidget(task);
task->deleteLater();
updateStatus();
resize(height() - task->height(), task->width()); // Here we resize the window, List
}


The outcome is not very bad but still far from a decent app!

13208

13209

d_stranz
30th July 2019, 17:39
OK, here's one last thing to try. The stretch at the bottom really seems to be messing things up when trying to dynamically resize the main window.

In removeTask(), try this:

- remove the task.
- remove the stretch
- resize the height
- add the stretch back in

If this still doesn't work, then I will implement some code and solve it. It can't be this hard.

franky
31st July 2019, 09:04
A question, in vBox->insertWidget(vBox->count()-1, task);
Isn't "count" one beyond the last element? And "count-1" the last indeed element? Also, isn't "stretch" counted as an element in the layout?

Anyway, I used this for the function:


void List::removeTask(Task* task) {
mTasks.removeOne(task);
vBox->removeWidget(task);
task->deleteLater();
delete vBox->takeAt(vBox->count()-1);
resize(height() - task->height(), task->width()); // Here we resize the window, List
vBox->addStretch();
updateStatus();
}

And after adding a number of task and removing them continually reaching the fist task, the following result emerged:

13214

d_stranz
31st July 2019, 16:45
The "index" argument in insertWidget() is the place where the new widget will be placed. So if the vbox has two items in it (the hbox and the stretch), count = 2, count - 1 = 1, so the widget will be inserted at index 1, which is between the hbox and the stretch. The stretch becomes index 3. The hbox stays at index 0.

So in order for insertWidget( vbox->count() - 1 ) to work correctly, there has to be a minimum of two items in the vbox, so you have to build it with the hbox and the stretch in place before you start adding tasks. If you don't have the stretch, then the first task will be added at 0, on top of the hbox. Subsequent tasks will be added on top of the hbox as well.

If I can find time, I will work on this sometime before the end of the week.

franky
31st July 2019, 17:34
Thank you.
I'm looking forward to seeing your solution.