QWidget not displaying the QLabel - c++

I have a QWidget which I want to use like a Dialog on top of another QWidget.
What I'm trying to do is a simple "Please wait while yadda yadda..." dialog with no buttons.
The code section is as follows:
void NewWindow::on_combobox_New_currentIndexChanged(int index) //slot function
{
QWidget* box = new QWidget();
box->setWindowModality(Qt::ApplicationModal);
box->setWindowTitle("Wait...");
QHBoxLayout* layout = new QHBoxLayout();
box->setLayout(layout);
QLabel* lbl = new QLabel();
lbl->setText("Loading...");
layout->addWidget(lbl);
box->show();
for (int var = 0; var < st.size(); ++var)
{
//Some heavy lifting here
}
box->close();
}
Normally I would expect this dialogue box to appear with the proper text and disappear after the loop ends. In fact it does that too but with one difference: The label does not display. The widget looks empty. Then disappears.
If I copy the code into a different area (for example to the MainWindow constructor) it displays properly with the message in it.
I sense that the loop blocks the draw operation but then the widget itself should be missing too. Why it is only the label eludes me.
Thanks for any insight.

Since you are creating and displaying this widget in a QObject slot, and then before returning from the slot, closing the widget, by the time Qt goes through the process of executing all your instructions, the last one is close, and so the widget disappears from view.
Underneath your slot, Qt is running in an event loop. Since control is never returned to the event loop, Qt never has an opportunity to render the graphics you've requested of it.
When you create widgets, add labels, etc, you are actually registering a bunch of commands with the event loop, which will only later be processed.
If you want Qt to render any changes you have made whilst in a slot, before returning to the event loop, you have to call processEvents.
Without doing so, you won't see those changes until control passes back to the Qt event loop.
So what is happening here, is that since you're also closing the widget at the end of your slot, Qt will create the widget, render its contents, and then immediately close it, and you won't see anything.
The reason for this is so that Qt can do calculations on what is visible, what isn't, be smart about what it renders etc, and only decide to draw what is necessary.
If it just rendered everything immediately, without waiting for control to return to it so it can process the "next batch of updates", it would likely be horribly inefficient.
So you need to put processEvents inside your callback slot.
void NewWindow::on_combobox_New_currentIndexChanged(int index) //slot function
{
QWidget* box = new QWidget();
box->setWindowModality(Qt::ApplicationModal);
box->setWindowTitle("Wait...");
QHBoxLayout* layout = new QHBoxLayout();
box->setLayout(layout);
QLabel* lbl = new QLabel();
lbl->setText("Loading...");
layout->addWidget(lbl);
box->show();
QCoreApplication::processEvents(); // cause the box to be displayed
for (int var = 0; var < st.size(); ++var)
{
//Some heavy lifting here
// if you do anything here to change the widget, such as
// updating a progress bar, you need to `processEvents` again
QCoreApplication::processEvents();
}
box->close();
}
As for why the widget window appears, but not the contents, this is likely because when creating the widget Qt sends a message to the Window Manager (in this case MS Windows), which will create the window on which Qt can render its contents.
So you see the result of Windows creating a new window for Qt, but nothing from Qt painting on that window itself.

Related

Details Scroll area's setwidget function shows a toolbar

I have a scrollarea and I'm setting a basic QWidget as its widget, However when I do this I get a toolbar on top of the widget I just set;
This is the slot I use to create the QWidget and set it to the scroll area, this happens during runtime and the qwidget is deleted and recreated repeatedly during runtime.
void SlotCreateDetailsWidget()
{
if (m_DetailsWidget == nullptr)
{
m_DetailsWidget = new DetailsWidget(); // QWidget
m_Ui->DetailsScrollArea->setWidget(m_DetailsWidget);
m_DetailsWidget->show();
}
}
What do I need to do to get rid of this toolbar?
From https://doc.qt.io/qt-5/qwidget.html :
A widget without a parent widget is always an independent window
(top-level widget).
What you see depends on the underlying windowing system but you can customize it up to a certain point with void QWidget::setWindowFlag(Qt::WindowType flag, bool on = true) for example. You can also look at https://doc.qt.io/qt-5/qtwidgets-widgets-windowflags-example.html.

Qt: how to freeze some buttons for a specific amount of time?

I'm trying to build a simple memory game with Qt 5.11.1 and C++, where you get a few tiles on screen and you have to click on two and try to match the images they show.
My tiles are implemented as QPushButtons. Each time you click on one an image is displayed (by calling a showImage()method that changes the button background). When a second tile is clicked, if there is a match the two buttons are disabled so you can't click on them again (and you get a higher score). However, if you didn't get a match, the two tiles you just clicked will go back to their initial state (showing no image) after 1 second (this allows the user to "memorize" which image was showing up on each tile).
Whenever you click on a "tile" (button) it becomes disabled (button->setEnabled(false)). If after clicking a second tile there was no match, then both tiles are turned back and then setEnabled(true) again. I'm using a single shot QTimer to call the method that will turn back the tiles:
QTimer::singleShot(1000, this, SLOT(turnTilesBack()));
firstTile->setEnabled(true);
secondTile->setEnabled(true);
Everything is working as expected, except for one thing: as QTimer runs in its own thread (or so I understand from what I read) all of the available tiles remain enabled during the 1000 milisecond lapse, allowing the user to continue clicking on them. However, when there is no match, I'd like to "freeze" the buttons until the QTimer has timed out so the user can't continue playing until the tiles have turned back.
So instead of using the QTimer I've trying this solution which I saw on this question (How do I create a pause/wait function using Qt?):
QTime dieTime= QTime::currentTime().addSecs(1);
while (QTime::currentTime() < dieTime)
turnTilesBack();
although I removed this line: QCoreApplication::processEvents(QEventLoop::AllEvents, 100); as this would cause the main thread not to freeze and buttons would still be clickable.
But with this approach, whenever the user clicks on the second tile, if there is no match the image is not even displayed, even when my showImage() method is called before the code above, and I'm not sure why this is. So the user knows there was no match because after 1 second the tiles go back to their initial state, but they never got to see the image on the second button.
As another approach, I also though of disabling all buttons and then after the single shot QTimer times out, re-enabling back only the ones that have not been matched yet. But this would require additional logic to keep track of which tiles have been matched. So for now I'm sticking to the
Is there a cleaner solution? Maybe there's a way to make the QTimer freeze the main thread until it times out?
An easy way to enable/disable the entire group of QPushButtons is to place them on an intermediate widget (in the below example I've used a QFrame)
If you want to disable all the QPushButtons, you just disable the frame, and all its child widgets will be disabled.
When you want to re-enable them, you enable the frame.
Any widgets inside the frame which are already disabled won't be enabled when the frame is re-enabled, so you won't lose your enabled/disabled state on the individual buttons
Here is a simple example. Note that I've used explicit enable/disable buttons which act as a proxy for your timer.
#include <QApplication>
#include <QMainWindow>
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QPushButton>
#include <QFrame>
int main(int argc, char** argv)
{
QApplication* app = new QApplication(argc, argv);
QMainWindow* window = new QMainWindow();
window->setFixedSize(1024, 200);
QWidget* widget = new QWidget();
QHBoxLayout layout(widget);
QPushButton* enable = new QPushButton("enable");
QPushButton* disable = new QPushButton("disable");
QFrame* frame = new QFrame();
layout.addWidget(enable);
layout.addWidget(disable);
layout.addWidget(frame);
QVBoxLayout frame_layout(frame);
for (int i = 0; i < 5; ++i)
frame_layout.addWidget(new QPushButton("click"));
// this shows that an already disabled button remains disabled
QPushButton* already_chosen = new QPushButton("click");
frame_layout.addWidget(already_chosen);
already_chosen->setEnabled(false);
QObject::connect(enable, &QPushButton::clicked, [&]{ frame->setEnabled(true); });
QObject::connect(disable, &QPushButton::clicked, [&]{ frame->setEnabled(false); });
window->setCentralWidget(widget);
window->show();
return app->exec();
}

In QT "ui->scrollArea->horizontalScrollBar()->setValue(0)" has no effect

So in my UI designer I have a ScrollArea Widget, I then in my MainWindow.cpp, I create a QGraphicScene and a QGraphics View. I create a new widget and give that widget a QVBoxLayout so that it will auto-size(which is correct to my understanding).
I then use ui->scrollArea->setWidget(widget); to make this new widget the child of my scrollView.
All of this seems correct because I have scroll bars that I can navigate my scene with. However; using the line ui->scrollArea->horizontalScrollBar()->setValue(0); still has no effect on the scroll bar values.
scene = new QGraphicsScene();
scene->setSceneRect(0,0,2500,2500);
view = new QGraphicsView(scene);
QWidget *widget = new QWidget;
view->setBackgroundBrush(Qt::white);
QVBoxLayout* bLayout = new QVBoxLayout(widget);
ui->scrollArea->setWidget(widget);
bLayout->addWidget(view);
widget->show();
ui->scrollArea->horizontalScrollBar()->setValue(0);
ui->scrollArea->verticalScrollBar()->setValue(0);
I just had the this problem. Then, after debugging with ui->scrollArea->verticalScrollBar()->value() I realized that the scrolling area has no size before the component is show on the screen, i.e., it does not change the scrolling because it is not visible yet.
This is a sample Python code, but the is the same for C++, except the Language Syntax.
from PyQt5.QtWidgets import QDialog
...
dialog = QDialog()
...
verticalScrollBar = dialog.QPlainTextEdit.verticalScrollBar()
horizontalScrollBar = dialog.QPlainTextEdit.horizontalScrollBar()
# Has no effect, if you print the values, you will see always 0
verticalScrollBar.setValue( horizontalScrollBar.maximum() )
horizontalScrollBar.setValue( horizontalScrollBar.minimum() )
dialog.show()
# Now it has effect
verticalScrollBar.setValue( horizontalScrollBar.maximum() )
horizontalScrollBar.setValue( horizontalScrollBar.minimum() )
Autoscroll PyQT QTextWidget
If you are sure to address the correct scrollbar (as pointed in the comments by thuga), maybe check if your scroll area is modified after that init. I mean I'm not sure of the bahaviour, but if you modified some text in your widget for example, I think the scrollbar will be impacted.
You may need to catch some of your widget's event to reapply those:
ui->scrollArea->horizontalScrollBar()->setValue(0);
ui->scrollArea->verticalScrollBar()->setValue(0);
If it doesn't help, you should try to debug by tracking scrollbar value with
ui->scrollArea->verticalScrollBar()->value()
and see if your set is well applied, and maybe when it is overriden
Just in case, you may also try some of the methods indicated here, but it's probably not relevant to your issue: setValue of ScrollBar Don't work at first time

QPaintEvent painting region in Qt?

This is a basic doubt regarding the Painting done in Qt.
I have a QScrollArea as my centralWidget in Main Window of my Application. I have added a QFrame frame to the scrollarea. The Layout of the QFrame is QGridLayout.
When I add widgets to the layout like this:
MainWindow::AddLabel()
{
setUpdatesEnabled(false);
QGridLayout *myGrid = (QGridLayout *)ui->frame->layout();
for(int i = 0; i < 1000; i++)
{
QLabel *label = new QLabel();
QString str;
str.SetNum(i);
label->SetText(str);
myGrid->AddWidget(label, 0, i, 0);//add label to i'th column of row 0
}
setUpdatesEnabled(true);
repaint();
}
Please dont worry about the memory leak as it is not the focus of the question.
So my doubt's are:
Is setting the updates disabled during adding widgets to layout any helpful?
Even if I maximise the window not all the QLabel's will be visible to me. So when the code flow leaves the above function & goes to the event loop then are all the QLabel's & the enormous area of QFrame painted? Or only those QLabel's which are visible & only that much area of QFrame which is visible painted?
If you are using a form (.ui) then the widgets inside the ui are not children of your widget MainWindow. Well , setUpdatesEnabled() only affect the current widget as well as its children, so the object ui->frame will still receive updates after myGrid->AddWidget. Change to
ui->frame->setUpdatesEnabled(false);
...
ui->frame->setUpdatesEnabled(true);
Btw, when you enable updates, then screen will be updated. So you dont need to call repaint(); on any widget.

How to control the Widgets in QGridLayout that should be painted in GUI?

In my application I have a QGridLayout which covers the majority of the Window. In it I have added a sequence of QLineEdit & QLabel objects. Currently when the no of QLineEdit objects > 500 && QLabel objects > 500 the GUI is significantly slow & for greater values does not operate properly. Also most of these widgets are not visible in the window, they need to be scrolled to be viewd.
Since I am adding so many widgets in the grid layout (by looping & calling repaint after the loop) the painting takes lots of time.
So I have an idea for the solution that even though my widgets are added in the Grid Layout not everybody are painted. I want to have a rectangle within which all widgets are painted & the co-ordinates of the rectangle will be updated whenever the the window is scrolled. But I dont know how to do this. So I wanted to know is it possible to do that?
And if possible please add a small sample code so that I can understand how to implement that.
Thank You.
UPDATE : Adding an image to depict the sitatuion.
Black Rectangle = QGridLayout say myGid.
Red Rectangle = Bounding Rectangle which is approximately same size as Main Window of my Application.
Green Rectangle = Widgets in myGrid.
Green Rectangle filled with yellow = Widgets shown in Main Window (only these widgets should be considered for call to repaint), rest of the unfilled rectangles are widgets present in myGrid but not be considered for call to repaint.
Thus when I scroll in my main application, the co-ordinates of red rectangle are updated & all the widgets bounded by it are considered for repaint.
I hope I made the problem simple to understand.
I understand you don't want to discard your code. I'd try one of these, starting from the easiest:
Are you using a QScrollArea or are you simulating it using scroll bars? The QScrollArea probably already discards paint events to children widgets that are off the viewport. Assemble the grid off-screen. Otherwise, Qt will recalculate and repaint the layout every time you add a new widget. (Here is a complete example.)
QWidget* widget = new QWidget(); // This is an invisible widget.
for (int i = 0; i < 100; i++) {
for (int j = 0; j < 100; j++) {
QLineEdit* lineEdit = new QLineEdit();
lineEdit->setText(QString("I am the line edit at (%1, %2)").arg(i).arg(j));
layout->addWidget(lineEdit, i, j);
if (j % 10 == 0) {
// Do not block the UI thread while the UI is being assembled.
qApp->processEvents();
}
}
}
// The layout will be calculated only once, here:
scrollArea->setWidget(widget);
widget->show();
If that doesn't work, create an event filter that has a reference the visible rectangle. Event filtering is a useful technique in which you can intercept events targeted at one or more widgets and decide if they should be discarded before being handled.
In your case, when you intercept a QPaintEvent, check if the target widget intersects the visible rectangle. If it does, pass the event along to the target widget. If it doesn't, discard the event.
I don't know the specifics of how you scroll your UI, so I leave the calculating the visible rectangle up to you. The event filter code would be something like this.
bool MyClass::eventFilter(QObject *obj, QEvent *event)
{
if (event->type() == QEvent::Paint) {
if (QWidget* widget = qobject_cast<QWidget*>(object)) {
QRect visibleRectangle = somehowGetVisibleRectangle();
if (visibleRectangle.intersects(widget->geometry())) {
return false;
} else {
// Returning true means "drop this event."
return true;
}
}
}
// Assuming MyClass extends QWidget. Adjust as necessary.
return QWidget::eventFilter(obj, event);
}
As a last resort, relayout your UI using QGraphicsScene, QGraphicsWidget, QGraphicsGridLayout, and QGraphicsView. The scene graph might be better at discarding unnecessary UI repaints.
First of all. Are you sure you are solving your problem in the right way? Perhaps you will be happier with QTableWidget? Its cells can be editable and then QTableWidget will take care of creating and maintaining QLineEdit for the cell that is being edited.