Qt Desktop on Mac: QFormLayout sizing with subpanels - c++

TL;DR: I'm having a grow/shrink probably using embedded forms inside a MainWindow. I'm unsure what to try next.
Okay, I have another sizing problem.
This is a sample app of what I'm trying to do:
When I click on the various toolbar options, I intend to change the central widget contents accordingly. Maybe I should just use a tab widget, but I wanted to do it this way.
In the simplest form, with a widget layout like this:
I set the central widget's layout to Horizontal, and the Inner Widget to FormLayout then set the inner widget's expand rules to expand any expandable fields. As I resize the window, the simple line edit expands and contracts as desired.
When I click the bus icon in the toolbar, I swap out the contents of the central widget with a separate panel. That panel has a widget with a form layout, and is also set to expand and collapse. Here are the layout rules for the second panel:
My trigger code does this:
currentCenter = ui->innerWidget; // In the constructor
currentCenter->hide();
if (v1Form == nullptr) {
v1Form = new V1Form(ui->centralWidget);
}
v1Form->show();
currentCenter = v1Form;
I have tried various orders to this, and I tried using setCentralWidget(). In all cases, the new central area remains a fixed size, even though the original one expands and collapses.
What is working: I can readily change the inner contains for different forms. That's working great. (It took a while to figure it out.)
-or- I can make simple popup forms that grow and shrink properly.
What is not working is grow/shrink when I embed my form inside my central widget or if I use setCentralWidget.
I'm not sure what else to try.

Maybe I should just use a tab widget, but I wanted to do it this way.
You should definitely use a QTabWidget as your central widget. It is designed specifically for your use case, and it will greatly simplify your code.
My trigger code does this:
currentCenter = ui->innerWidget; // In the constructor
currentCenter->hide();
if (v1Form == nullptr) {
v1Form = new V1Form(ui->centralWidget);
}
v1Form->show();
currentCenter = v1Form;
With a QTabWidget, your trigger code can be simplified to:
ui->innerTabWidget->setIndex(1).
You don't need to dynamically construct a V1Form. Simply use Qt Designer to create multiple pages in your QTabWidget and implement all your subpanel widgets within your MainWindow.ui.
(Nonetheless, if you want to implement each subpanel in its own separate *.ui file, you can still promote each page in your QTabWidget to your custom widget.)
What is not working is grow/shrink when I embed my form inside my central widget or if I use setCentralWidget.
To address your original symptoms: Your widgets don't grow/shrink because you didn't put them inside a layout that is part of your main window.

I found a solution doing it the way I started. I had to add one line of code:
void MainWindow::switchForm(QWidget *widget) {
if (centralForm != widget) {
if (centralForm != nullptr) {
centralForm->hide();
centralForm = nullptr;
}
if (widget != nullptr) {
centralForm = widget;
centralForm->show();
ui->centralwidget->layout()->addWidget(centralForm);
}
}
}
void MainWindow::on_actionSetup_triggered()
{
if (setupForm == nullptr) {
setupForm = new SetupForm(ui->centralwidget);
}
switchForm(setupForm);
}
The missing line -- adding my new form to the layout:
ui->centralwidget->layout()->addWidget(centralForm);

Related

Why is my QStyledItemDelegate not visible?

I'm trying to create a custom item delegate for a list of objects, but when I run my app the list view is blank. I know my model is populated, and it displays as expected with the standard default delegate. I can also tell that my custom delegate is being rendered in some sense, since space is taken up in the list, and the tooltips and statustips that I set work, but the contents are not visible (the listview appears to be blank white).
My code is based on this example: https://doc.qt.io/archives/qt-5.12/qtwidgets-itemviews-stardelegate-example.html
Qt 5.12 is the version that I'm stuck with, and I'm also stuck using visual studio without the normal Qt debugging tools, do to factors outside of my control.
Anyway, I create the widgets that should be displayed in the constructor, with default values for testing:
EntityListDelegate::EntityListDelegate(QWidget* parent /*= nullptr*/) : QStyledItemDelegate(parent)
{
ui = new QWidget(parent);
ui->setMinimumSize(QSize(200, 40));
QHBoxLayout* hLayout = new QHBoxLayout(ui);
iconLabel = new QLabel(ui);
iconLabel->setMinimumSize(QSize(20, 20));
iconLabel->setMaximumSize(QSize(40, 40));
iconLabel->setPixmap(QPixmap(":/icons/Placeholder.png"));
hLayout->addWidget(iconLabel);
QVBoxLayout* vLayout = new QVBoxLayout(ui);
nameLabel = new QLabel(ui);
nameLabel->setMinimumSize(QSize(150, 20));
nameLabel->setText("Unknown entity");
vLayout->addWidget(nameLabel);
typeLabel = new QLabel(ui);
typeLabel->setMinimumSize(QSize(150, 16));
typeLabel->setText("Unknown type");
vLayout->addWidget(typeLabel);
hLayout->addLayout(vLayout);
ui->setLayout(hLayout);
}
Note that each item should have a thick black border, which is not being rendered. (Edit: I changed the QFrame back to a QWidget)
I also implemented paint, where the real values should get set based on info in the model:
void EntityListDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const
{
nameLabel->setText(index.data(Qt::DisplayRole).toString());
switch (qvariant_cast<int>(index.data(UserRoles::Type)))
{
case EntityType::Foo:
{
iconLabel->setPixmap(QPixmap(":/icons/foo.png"));
typeLabel->setText("Foo");
break;
}
case EntityType::Bar:
{
iconLabel->setPixmap(QPixmap(":/icons/bar.png"));
typeLabel->setText("Bar");
break;
}
case EntityType::Baz:
{
iconLabel->setPixmap(QPixmap(":/icons/baz.png"));
typeLabel->setText("Baz");
break;
}
default:
break;
}
}
The other functions are just stubs, since the user shouldn't be able to edit this data.
Am I missing some step in my setup?
Update: I tried making the pointer to the delegate that gets passed into setItemDelegate() a member of the class that owns the the ListView. I now get one item on the list that renders correctly, in the top position in the list. I can sometimes get it to display another item by clicking where that item should be, but it's still in the same location.
The answer to the question as asked is that if you're trying to alter the default display, not the editor, you have to reimplement paint and basically draw the whole thing manually. This is such a massive PITA as to make QStyledItemDelegate functionally unusable for this use case, IMO. Use QtQuick if you can, but that's not an option in my case.
My actual solution, for the curious, was to use QSortFilterProxyModel and override data() to return "EntityName\nEntityType". That gets me ~80% of what I was trying to achieve, and hopefully I can get a little closer with styling when I get to that part.

How to resize a QLabel displayed by a QWidgetAction after changing it's text

I use a QWidgetAction to add a header to a context menu (that will also show up on Windows, no matter what style is used, in contrast to addSection(), which does not always actually display the title).
The action's widget is a QLabel. It's text is changed by each invocation of the context menu. The menu is setup in the constructor of my class, and the QWidgetAction is added like so (all m_ variables are member variables declared in the header):
m_contextMenu = new QMenu(this);
m_menuTitle = new QLabel;
m_menuTitle->setAlignment(Qt::AlignCenter);
m_menuTitle->setMargin(4);
QWidgetAction *titleAction = new QWidgetAction(m_contextMenu);
titleAction->setDefaultWidget(m_menuTitle);
m_contextMenu->addAction(titleAction);
m_contextMenu->addSeparator();
When the menu is requested, the text of the label is changed and the menu is displayed like so:
m_menuTitle->setText(tr("%1 „%2“").arg(some_variable, some_other_variable));
...
m_contextMenu->exec(place_to_display);
When the label's text is set for the first time (with a short text the label's text is set to), everything is fine:
but when it's set to some longer text, the size remains the same and the text is cropped:
I tried to fix this, but the only working solution I found was to define the QActions displayed in the menu in the constructor, owned by this, setting the label's text, clearing the menu and adding the actions again, like so:
m_contextMenu->clear();
m_menuTitle->setText(tr("%1 „%2“").arg(some_variable, some_other_variable));
m_contextMenu->addAction(m_menuTitleAction);
m_contextMenu->addSeparator();
m_contextMenu->addAction(m_editAction);
m_contextMenu->addAction(m_deleteAction);
m_contextMenu->exec(place_to_display);
Is there a way to resize the title without rebuilding the menu each time?
The solution is to send a resize event instead:
m_menuTitle->setText(tr("%1 „%2“").arg(some_variable, some_other_variable));
...
QResizeEvent re(new_size, m_contextMenu->size());
qApp->sendEvent(m_contextMenu, &re);
This will set the QMenu's internal itemsDirty flag and will force geometry recalculation when the menu is shown. Note that the new size in the event does not matter, as the menu will aways resize based on its sizeHint()!
The QResizeEvent solution didn't really work for me (with a more complex widget), I found the generic solution by reading the QMenu and QAction source code.
m_widget->installEventFilter(this);
// and in my case m_widget->layout()->setSizeConstraint(QLayout::SetFixedSize);
bool SomeClass::eventFilter(QObject* watched, QEvent* event)
{
if (watched == m_widget && event->type() == QEvent::Resize) {
// Force QMenu to recalculate the geometry of this item
QActionEvent e(QEvent::ActionChanged, this);
qApp->sendEvent(m_contextMenu, &e);
}
...
}
QActionEvent triggers everything we need in QMenu: recalculating geometries, resizing the menu to its new size (if it's visible), etc.
This answer extends user362515's answer.
There is little more effort required if you change the size of a hidden widget action (e.g., because of its menu is currently collapsed).
Create a new class ActionWidget which derives publicly from QWidget.
Then override the showEvent method and implement it like this:
void ActionWidget::showEvent(QShowEvent* event)
{
QResizeEvent resize_event(QSize(), parentWidget()->size());
parentWidget()->adjustSize();
qApp->sendEvent(parentWidget(), &resize_event);
QWidget::showEvent(event);
}
Notice that adjustSize must be called on the parent widget of the action widget and the event must be sent to the parent widget.
Of course, you must also reimplement QWidgetAction::createWidget such that it returns an instance of the ActionWidget-class and make sure that ActionWidget reports a proper (updated) size hint.

How do you close a QMenu when its QWidgetAction is triggered?

I have a QMenu for which I've created a QColorModel action widget (It's effectively just a QStandardItemModel). My desired behavior is that when a user clicks one of the colors in the model, that the action should trigger, and the menu close. However, it doesn't seem to do that, even when I trigger the action manually.
I've tried manually hiding the menu, but it's a kludge because it won't hide parent menus which th menu may be attached to.
Here's the relevant section of code:
// color menu
m_colorMenu = new QMenu("color", this);
m_colorView = new QColorView(m_colorMenu);
m_colorViewAction = new QWidgetAction(m_colorMenu);
m_colorViewAction->setDefaultWidget(m_colorView);
m_colorView->setModel(new QStandardColorModel);
connect(m_colorView, &QColorView::clicked, [&](QModelIndex index)
{
QColor color = qvariant_cast<QColor>(index.data(Qt::DecorationRole));
if (m_pen.color() != color)
{
m_pen.setColor(color);
drawIcon();
drawColorIcon();
update();
}
//this->hide(); // kludge, didn't close all parent menus
m_colorViewAction->trigger(); // doesn't seem to cause menu closure
});
m_colorMenu->addAction(m_colorViewAction);
EDIT
I've also tried adding something to the effect of:
QMenu* menu = m_colorMenu;
do
{
menu->close();
menu = dynamic_cast<QMenu*>(menu->parent());
} while (menu);
but it also is fragile/kludgey because it assumes a) all widgets are properly parented, and b) that all the parents are actually supposed to be menus. In my case, they aren't.
If the containing menus aren't in the parentage tree, and the menu you want to close isn't the top level menu, there is no easy way to do this. That said, there is:
THE NUCLEAR OPTION
Adding this to the end of the lambda function
auto topLevelWidgets = qApp->topLevelWidgets();
for (auto widget : topLevelWidgets)
{
QMenu* menu = dynamic_cast<QMenu*>(widget);
if (menu)
{
menu->close();
}
}
will cause ALL top level menus to close once the action is triggered. This is a relatively OK way to accomplish what you want because:
one of the top level menus will contain the menu in question, and
never say never, but I can't think of a single case where you would have (or want) more than one menu open at a time, so most likely the only open menu tree you would close is the intended one.

Use a pointer to switch between two other pointers in Qt Creator Ui

i do have two Labels in my applications. They are both in each tab. Now i want to witch between those labels by using an pointer that switchs between those labels when tab was changed.
my ui_mainwindow.h defines:
ProLabel *imageLabel;
ProLabel *imageLabel_1;
ProLabel *imageLabel_2;
but only imageLabel_1 and imageLabel_2 are shown in the GUI. I added a third Label (imageLabel) to use it as a variable to switch between the Labels (1/2). So I wrote follwing code in a slot which proves tab changed. So if tab is changed, the other imageLabel_1/2 (pointer) should be used in the hole code, when it says: imageLabel.
my slot when tab changed:
if(tab == 0)
{
this->ui->imageLabel = this->ui->imageLabel_1;
}
else{
this->ui->imageLabel = this->ui->imageLabel_2;
}
I also set the imageLabel_1 whe MainWindow is created. The Window disapear like normal. But if I try to load an image (than it should display the image in the imageLabel) it crashes.
Don't know if it was understandable. Does anybody has an idea if i can handle the pointers like that.. or how to do it different?
Thank you!
Two simpler ways: 1) Change the label contents, either the image or the text it contains, or 2) set the label to hidden with ui->label2->isVisible(false).

Is it possible to inherit QVBoxLayout and set it as layout in QWidget?

What I'm trying to accomplish:
Create 2 classes that inherit QVBoxLayout simply to set up each class with a series of different objects.
e.g.:
Class 1 (inherits QVBoxLayout), has QLabels to show an appointment and those labels set up with this->addWidget(labels);
Class 2 (inherits QVBoxLayout), has QLineEdits (and so on) to edit an appointment and those objects are also set up with this->addWidget(lineedits);
Is it possible to have a QWidget class then switch between these 2 layouts by calling this->setLayout(class1_object); and this->setLayout(class2_object);?
Or how would you suggest the swapping of the active objects on the widget (when clicking the edit button on the view-part or the save button on the edit-part)?
Simply use object->setShown(false);?
IMO, it's easier to use QTabWidget here.
Make a QTabWidget with 2 tabs. On Tab1, put your labels. On Tab2, put your edits. Call Tab2 something like "Edit the appointment". Now, use the currentChanged() slot for catching the tab switching.
If saving edits should be simple, all you will need to do is just to copy the edited data from edits to labels, and vice-versa.
If saving requires more than that, e.g. you want a confirmation dialog, you can permit the user to change back to Tab1 until some condition is met:
void MainWindow::on_tabWidget_currentChanged(int index)
{
//if the user is trying to go back to Tab1 (where the labels are)...
if(index == 0)
{
//...and if user didn't accept something, we just return him to the current tab
//It's probably a good idea to tell him what went wrong, too :P
if(!userAcceptedSaveDialog())
ui.tabWidget.setCurrentIndex(1);
}
}