QTabWidget strange behavior - c++

I have two tabs where placed QTableWidget with cell widget. See image.
QTabWidget *tab = new QTabWidget(this);
for (int i = 0; i < 2; ++i) {
QTableWidget *t = new QTableWidget(1, 1);
QPushButton *btn = new QPushButton("Click on me!");
t->setCellWidget(0, 0, btn);
connect(btn, &QPushButton::clicked, [=]() {
btn->hide();
});
tab->addTab(t, QString::number(i + 1));
}
setCentralWidget(tab);

The behavior you indicate is predictable, you must first know that if a widget becomes visible your children will also be visible.
Let's analyze the case of QTabWidget, this widget is essentially a QTabBar + QStackedWidget, the QStackedWidget manages the visibility of the widgets, and the latter internally has a QStackedLayout that when it establishes that a certain widget is wanted to show then it hides the current widget and shows the new one current widget. So every time you select tabbar the widget that is displayed will call the show method of that widget (in your case QTableWidget), and it will make your children visible even if they are hidden.
So if you want that if you have the need that works as you suppose a workaround should happen it is to save the status of the visibility in a property and in the showEvent method to apply the change if necessary.

Related

Implementing a cell-based widget in Qt5

I'm trying to implement a cell-based widget similar to the one in Jupyter Notebook in Qt where multiple cells are stacked vertically. Each cell is made of a QTextEdit and multiple buttons and the cell's vertical size would grow by entering more text into the text edit (i.e. no vertical scrollbars in the text edit).
However I'm not sure what the best way would be to implement such a widget in Qt. I've tried a QListWidget with a custom widget (made of a QTextEdit and a couple of buttons) for each item, however the custom widget wouldn't grow vertically by entering more text and instead the QTextEdit scrollbars would show up. I also implemented a second version using QListView and QStyledItemDelegate however I encountered multiple issues including not being able to interact with the painted buttons (through the delegate).
Any suggestions/examples about how to implement such a cell-based widget?
You can subclass QTextEdit and listen for changes in the document size using document()->size() (not the size of the QTextEdit, mind). You can then resize the QTextEdit's parent to reflect the new size. To fill the parent with the widget, use a layout. I've used QVBoxLayout several times below for this purpose. (One, to hold multiple QTextEdit widgets in a vertical column; Two, to hold a single TextEdit).
#include <QtCore>
#include <QtWidgets>
class DyTextEdit : public QTextEdit
{
Q_OBJECT
public:
DyTextEdit(QWidget *parent = nullptr) :
QTextEdit(parent)
{
QTextDocument *doc = document();
currentSize = doc->size();
qDebug() << "[Init] Size:" << currentSize;
// listen to text/content changes
connect(this, &QTextEdit::textChanged, this, &DyTextEdit::on_textChanged);
// connect <> 💡
connect(this, &DyTextEdit::sizeChanged, this, &DyTextEdit::resizeParent);
doc->setTextWidth(-1);
emit sizeChanged(currentSize); // init
}
signals:
// emitted when document size changes
void sizeChanged(QSizeF size);
public slots:
void on_textChanged()
{
QSizeF newSize = document()->size();
qDebug() << "[TextChanged] Size:" << newSize;
// detect changes in the document size
if (newSize != currentSize)
emit sizeChanged(newSize); // emit signal💡
// update size
currentSize = newSize;
}
void resizeParent(QSizeF size)
{
// resize the parent's height, don't bother with the width
parentWidget()->setFixedHeight(size.height());
}
private:
QSizeF currentSize; // keeps track of current document size
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
// holds all dytextedits
QWidget aggregateWidget;
QVBoxLayout aggregateLayout(&aggregateWidget);
// note: for scalability, use a QList (or QLists)
// first textbox
QWidget widget;
QVBoxLayout vbLayout(&widget);
vbLayout.setMargin(0);
DyTextEdit dytext(&widget);
vbLayout.addWidget(&dytext);
// first button row
QHBoxLayout hbLayout(&widget);
hbLayout.setMargin(0);
QPushButton pb1a("Push this.", &widget);
hbLayout.addWidget(&pb1a);
QPushButton pb1b("Push this.", &widget);
hbLayout.addWidget(&pb1b);
// second textbox
QWidget widget2;
QVBoxLayout vbLayout2(&widget2);
vbLayout2.setMargin(0);
DyTextEdit dytext2(&widget2);
vbLayout2.addWidget(&dytext2);
// second button row
QHBoxLayout hbLayout2(&widget2);
hbLayout2.setMargin(0);
QPushButton pb2a("Push this.", &widget2);
hbLayout2.addWidget(&pb2a);
QPushButton pb2b("Push this.", &widget2);
hbLayout2.addWidget(&pb2b);
// add widgets to layout
aggregateLayout.addWidget(&widget); // cell 1
aggregateLayout.addLayout(&hbLayout); // |
aggregateLayout.addWidget(&widget2); // cell 2
aggregateLayout.addLayout(&hbLayout2); // |
aggregateLayout.setSizeConstraint(QLayout::SetMinAndMaxSize);
aggregateWidget.show();
return a.exec();
}
#include "main.moc" // include if there are QObject classes in main.cpp
Note that above, I've used rows of buttons using QHBoxLayout and added them to the overall, aggregate layout detached from the textbox widgets. You can still use a say, QGridLayout if you wish to set your buttons in a different format. You can also combine the textedits and button layouts as children under another widget (to serve as an umbrella) covering both. That would more clearly form a cell of widget and may be easier to move around. (I've uploaded a workable example of this on Github.)
Demo (macOS):
This is a screenshot of after typing content and copy-pasting content.
Note that the aggregate widget will contract on removal of lines of text.

QListWidget with custom widgets - not triggering itemClicked signal

I have listwidgetitem's that have a custom widget (step_widget) which contains 3 QPushButtons and a QLabel.
When I press one of the buttons in the widget, it is not triggering itemClicked which i need to see the index of the listwidget that was clicked. If i click the QLabel, the signal is triggered and i am able to obtain the index. How can trigger the signal when one of the buttons is pressed? Why does it not trigger?
QListWidgetItem *item = new QListWidgetItem();
stepWidget *step_widget = new stepWidget();
ui->listWidget->addItem(item);
ui->listWidget->setItemWidget(item,step_widget);
The itemClicked() signal is not emmited because the QPushButton consumes the click event.
There is the simplest solution I've found.
First, in your class stepWidget add a signal that will be emitted when any QPushButton is clicked, for example:
signals:
void widgetClicked();
Then connect the clicked() signal of every QPushButton with that signal. This code is in the constructor of the widget stepWidget:
connect(ui->pushButton1, &QPushButton::clicked, this, &stepWidget::widgetClicked);
connect(ui->pushButton2, &QPushButton::clicked, this, &stepWidget::widgetClicked);
connect(ui->pushButton3, &QPushButton::clicked, this, &stepWidget::widgetClicked);
To test it I added 10 widgets to a QListWidget that reside in the MainWindow class. You must connect that signal to a slot (in this case I use a C++11 Lambda Function, but you can create a slot instead, it's fine) where you establish the current item as the item under the widget. This code is located in the constructor of MainWindow:
for (int i = 0; i < 10; ++i) {
QListWidgetItem *item = new QListWidgetItem;
stepWidget *step_Widget = new stepWidget;
ui->listWidget->addItem(item);
ui->listWidget->setItemWidget(item, step_Widget);
connect(step_Widget, &stepWidget::widgetClicked, [=]() {
ui->listWidget->setCurrentItem(item);
});
}
Now, this will not make the itemClicked() signal to be emitted because the user not clicked the item. But I advice you to connect instead the signal currentRowChanged() as it will be emitted with this code and also it has the advantage that it allows you to directly obtain the row. Once you have the row you can obtain the item and the widget, evidently.
However, this procedure has the inconvenient that the currentRowChanged() signal will be emitted even when the user selects a row with the keyboard or when the user press and slide the mouse over the QListWidget without releasing it.
On workaround could be, for example, using a flag as an attribute for your MainWindow class that is always false except during this connection:
connect(ste_pWidget, &stepWidget ::widgetClicked, [=]() {
buttonPressed = true;
ui->listWidget->setCurrentItem(item);
buttonPressed = false;
});
Then you must manipulate two signals from the QListWidget: clicked() and currentRowChanged(). I choose clicked() instead of itemClicked() because it gives almost directly access to the row:
void MainWindow::on_listWidget_clicked(const QModelIndex &index)
{
ui->statusBar->showMessage(tr("Row clicked: %1").arg(index.row()), 1000);
}
void MainWindow::on_listWidget_currentRowChanged(int currentRow)
{
if (buttonPressed) {
ui->statusBar->showMessage(tr("Row clicked: %1").arg(currentRow), 1000);
}
}
what you can simply do is, make your qpushbutton to qlabel and then qpushbutton will become clickable.
You can then override this method :- on_listWidget_itemClicked(QListWidgetItem *item) for double click on qpushbutton.
I tried it & it worked perfectly, its quite simple workaround.

Custom widgets in scroll area not respecting minimum?

I'm getting closer to getting a QScrollArea working, but it's still shrinking my custom widgets as they are added. Everything is resizing fluidly, and if the scroll area is too small, then the scroll bar appears, so it clearly has some idea of a minimum size.
At start, with two custom widgets in the scroll area, you can see some shrinking:
Here's the same window below the critical point. The text is now completely gone, but it won't shrink the QLineEdit, so it finally adds a scrollbar. (the scroll area has a blue background, the content widget is the purple)
I started in Design, but everything below the scroll area's widget is in code, as I was having trouble getting the vertical layout to work right using design.
Here's the starting point:
There's a page in a StackWidget that has two elements in a vertical layout. The scroll area has a QWidget. The constructor for MainWindow defines a vertical layout, assigning it to the member scrollWidgetLayout, and giving that layout to the scroll area's widget:
scrollWidgetLayout = new QVBoxLayout(ui->scrollAreaWidgetContents);
ui->scrollAreaWidgetContents->setLayout(scrollWidgetLayout);
The app starts on the first page of the stack widget, the user logs in, and the app runs off to fetch records from the server. Each record is turned into a widget:
RecordFolderWidget::RecordFolderWidget(Record *r, QWidget *parent) : QWidget(parent)
{
record = r;
//setSizePolicy(QSizePolicy::Expanding, QSizePolicy::MinimumExpanding);
QGridLayout *layout = new QGridLayout();
pathLineEdit = new QLineEdit();
finderButton = new QPushButton("...");
QLabel *nameLabel = new QLabel(record->name);
layout->setSpacing(5);
layout->setMargin(3);
layout->addWidget(nameLabel, 0, 0, 0, 1, Qt::AlignCenter);
layout->addWidget(pathLineEdit, 1, 0);
layout->addWidget(finderButton, 1, 1);
setLayout(layout);
//setMinimumHeight(sizeHint().height());
}
Note that there are some lines commented out, these are things I have been playing with to try to get it to work. The sizeHint, btw, appears to be correct, and does not change.
Once that Widget is created, it gets added to the scroll area's widget:
RecordFolderWidget *rf = new RecordFolderWidget(record);
rf->setParent(ui->scrollAreaWidgetContents);
scrollWidgetLayout->addWidget(rf);
I tried here to also resize the scroll areas contents, with no luck:
ui->scrollAreaWidgetContents->resize(rfSize.width(), rfSize.height() * records.count());
where rfSize was pulled from the custom widget's sizeHint after it was created, and this line was called once after the loop to create/add all of the widgets.
Other than the setMinimumHeight and resize above, I've tried changing the SizePolicy for the scrollAreaWidgetContents from preferred to expanding to minimumexpanding and did not see any difference. I'm sure I've missed something trivial, but just can't find it.
The problem that I see in your code is when adding the QLabel you are setting the rowSpan to 0, I have changed it and I can observe it correctly. In the next part I show my test code and the result:
QVBoxLayout *scrollWidgetLayout = new QVBoxLayout(ui->scrollAreaWidgetContents);
for(int i=0; i<10; i++){
QWidget *widget = new QWidget;
QGridLayout *layout = new QGridLayout(widget);
QLineEdit *pathLineEdit = new QLineEdit;
QPushButton *finderButton = new QPushButton("...");
QLabel *nameLabel = new QLabel(QString("name %1").arg(i));
layout->setSpacing(5);
layout->setMargin(3);
layout->addWidget(nameLabel, 0, 0, 1, 1, Qt::AlignCenter);
layout->addWidget(pathLineEdit, 1, 0);
layout->addWidget(finderButton, 1, 1);
scrollWidgetLayout->addWidget(widget);
}

How can I adjust the window size of a QDialog to its content?

I have a dialog box with a QScrollArea to show an arbitrary amount of checkboxes. How can I make the dialog box adjust its width so that the QScrollArea does not have a horizontal scroll bar (if the content is not extremely wide).
std::vector<std::string> vec_strCheckboxLabel;
vec_strCheckboxLabel.push_back("Checkbox 1");
vec_strCheckboxLabel.push_back("Checkbox 2");
vec_strCheckboxLabel.push_back("Checkbox 3 is really long and causes a horizontal scroll bar to appear");
vec_strCheckboxLabel.push_back("Checkbox 4");
vec_strCheckboxLabel.push_back("Checkbox 5");
m_pWidget = new QDialog;
m_pWidget->setWindowTitle("My Dialog");
m_pWidget->setWindowModality(Qt::ApplicationModal);
m_pWidget->setMinimumWidth(400);
QVBoxLayout * pWidgetLayout = new QVBoxLayout(m_pWidget);
QLabel * pLabel = new QLabel("Hello");
pWidgetLayout->addWidget(pLabel);
QHBoxLayout * pTopButtonsLayout = new QHBoxLayout(m_pWidget);
pWidgetLayout->addLayout(pTopButtonsLayout);
QPushButton * pButton;
pButton = new QPushButton("Select all", m_pWidget);
connect(pButton, SIGNAL(clicked()), this, SLOT(slotSelectAll()));
pTopButtonsLayout->addWidget(pButton);
pButton = new QPushButton("Select none", m_pWidget);
connect(pButton, SIGNAL(clicked()), this, SLOT(slotSelectNone()));
pTopButtonsLayout->addWidget(pButton);
// the checkboxes in a scroll area
{
QFrame * pFrameCheckboxes = new QFrame(m_pWidget);
QVBoxLayout * pCheckboxesLayout = new QVBoxLayout(pFrameCheckboxes);
// this frame takes all available space in the QDialog
pFrameCheckboxes->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding);
for (unsigned int i = 0, iEnd = vec_strCheckboxLabel.size(); i != iEnd; ++i)
{
QCheckBox * poCheckBox = new QCheckBox(vec_strCheckboxLabel[i].c_str());
pCheckboxesLayout->addWidget(poCheckBox);
}
// put into scroll area
QScrollArea * pScrollAreaTheCheckboxes = new QScrollArea(m_pWidget);
pWidgetLayout->addWidget(pScrollAreaTheCheckboxes);
pScrollAreaTheCheckboxes->setWidget(pFrameCheckboxes);
}
The problem is the scroll area limits its size to take available space by default; it does not demand space from the layout. You have to explicitly tell it to change that behavior.
Check out QAbstractScrollArea::SizeAdjustPolicy. You are probably looking for AdjustToContentsOnFirstShow, like so:
pScrollAreaTheCheckboxes->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContentsOnFirstShow);
Unfortunately, you cannot set different adjust policies for horizontal and vertical aspects, if this is what you are after. Judicious use of QSizePolicy and layout settings (e.g. setStretch()) can fix this.
Qt4
As a workaround for Qt4, check the viewport's sizeHint() and set the scroll area's minimum size to that.
pScrollAreaTheCheckboxes->setMinimumSize(pScrollAreaTheCheckboxes->viewport()->sizeHint());
Do this after you've initialized all of your checkboxes.

QScrollArea issue with vertical scroll

I have read few pages about QScrollArea, and I couldn't solve my issue. I have the next code:
QDialog *window = new QDialog;
window->resize(300, 300);
for(int i = 0; i < 50; ++i)
{
QLabel *label = new QLabel(window);
label->move(10, i * 15);
label->setText("Text");
}
QScrollArea *area = new QScrollArea;
area->setWidget(window);
area->show();
It seems that the vertical scroll from QScrollArea doesn't appear. I can't use QVBoxLayout because on my QDialog I don't have only QLabels aligned vertically ( this is just a simplified version of my QDialog ).
The QScrollArea won't get scrollbars unless the QWidget inside grows. Just moving some QLabels out of bounds doesn't make the parent QWidget grow, especially without a QLayout.
But if you manually resize them so that the QWidget is bigger than the QScrollAreay, you'll get scroll bars as expected :
QDialog *window = new QDialog;
window->resize(300, 600); //< 600px high widget
for(int i = 0; i < 50; ++i)
{
QLabel *label = new QLabel(window);
label->move(10, i * 15);
label->setText("Text");
}
QScrollArea *area = new QScrollArea;
area->setWidget(window);
area->resize(300,300); //< Inside a 300px high scrollarea, expect scrollbars!
area->show();
Note that now you will have both scroll bars, because the vertical scroll bar means there isn't enough room for our 300px width anymore. You can forcefully hide the horizontal scroll bar with area->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
You could also always force a vertical scroll bar to appear with area->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);, but this by itself wouldn't solve your problem. You'd still have a 300px widget inside a 300px area, and the scrollbar wouldn't have any space to move.
Making sure the QWidget is big enough for everything it contains is what you'll want to do, the QScrollArea will adapt. Usually we use layouts for that, but you can make it work by hand as well.