如何在窗口显示后调用函数?

45
使用Qt,我创建了一个QMainWindow,并希望在窗口显示后调用函数。当我在构造函数中调用该函数时,该函数(实际上是对话框)会在窗口显示之前被调用。

调用QMainWindow::show(),然后调用QMetaObject::invokeMethod()并执行任何其他你想要做的操作。 - Trilarion
9个回答

42

如果你想在小部件显示时执行某些操作,可以像这样覆盖QWidget::showEvent

class YourWidget : public QWidget { ...

void YourWidget::showEvent( QShowEvent* event ) {
    QWidget::showEvent( event );
    //your code here
} 

3
我已经考虑过实现自己的show函数,但从未想过在我的代码之前调用父类的show函数。扇自己一个耳光 - HWende
1
实际上,在这种情况下,无论您在哪里调用QWidget::showEvent(),都不会有任何区别,因为QWidget::showEvent()的实现为空且什么也不做(当然,这是一个实现细节,不应该依赖它)。 - Frank Osterfeld
7
我尝试了您建议的实现,但对话框仍在窗口显示之前出现。 - HWende
啊,什么?“对话框在窗口显示之前显示”的意思是什么?你想要实现什么? - Frank Osterfeld
2
当我的QMainWindow继承类被显示时,它应该显示一个询问对话框。但是我无法在主窗口显示在屏幕上之后显示对话框。 - HWende
3
如果你使用exec(),那是因为exec()使用了本地事件循环,而这是Qt中万恶之源。相比同步方式,你可以使用QDialog::open异步打开它,而不是使用exec()。 - Frank Osterfeld

18

在分析了以上解决方案后,发现它们全部都有问题,包括那些得票最高的解决方案。

许多人推荐像这样的方法:

class MyWidget : public QWidget {
    // ...
};

void MyWidget::showEvent(QShowEvent* event) {
    QWidget::showEvent(event);
    DoSomething();
}

void MyWidget::DoSomething() {
    // ...
}

只要在DoSomething中没有QCoreApplication::processEvents();,这个方法就能正常工作。 如果有一个这样的调用,它会处理队列中的所有事件,包括最初调用MyWidget::showEvent的QShowEvent。 当它到达原始的QShowEvent时,它会再次调用MyWidget::showEvent,导致无限循环。

如果出现这种情况,有三种解决方案:

解决方案1:避免在MyWidget::DoSomething中调用processEvents,而是在必要时调用updaterepaint。 如果DoSomething调用其他函数,这些函数也应避免使用processEvents

解决方案2:将DoSomething变成一个槽,并将对DoSomething()的直接调用替换为

QTimer::singleShot(0, this, SLOT(DoSomething()));

零间隔计时器仅在处理完队列中的所有事件后才会触发,它将处理所有事件,包括原始的 QShowEvent,从队列中删除它们,然后才调用 DoSomething。我最喜欢这种方式。

由于只有零间隔计时器会在处理完队列中的所有事件后才触发,因此您不应尝试通过延长间隔来“改进”它。

QTimer::singleShot(50, this, SLOT(DoSomething())); // WRONG!

由于50毫秒通常足以处理队列中的事件,因此这通常会起作用,导致难以重现的错误。

解决方案3.创建一个标志,防止第二次调用DoSomething:

class MyWidget : public QWidget {
    // ...
};

void MyWidget::showEvent(QShowEvent* event) {
    if (is_opening)
        return;
    is_opening = true;
    QWidget::showEvent(event);
    DoSomething();
    is_opening = false;
}

void MyWidget::DoSomething() {
    // ...
}

在这里,is_opening是一个布尔标志,应该在构造函数中初始化为false。


15

试试这个:

在mainwindow.h中:

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();  

protected:
      void showEvent(QShowEvent *ev);

private:
      void showEventHelper();
      Ui::MainWindow *ui;
}

在 mainwindow.cpp 中:

MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);
}

void MainWindow::showEvent(QShowEvent *ev)
{
    QMainWindow::showEvent(ev);
    showEventHelper();
}

void MainWindow::showEventHelper()
{
    // your code placed here
}

括号缺失,应该是 connect(this, SIGNAL(window_loaded()), this, SLOT(your_function())); - vsz
3
既神奇又有趣,为什么要使用这个额外的信号和槽?在 void MainWindow::showEvent() 内直接调用函数(例如 *doWork()*)。 - A.B.

13

按照Reza Ebrahimi的做法,但要记住:

不要省略connect()函数的第五个参数,该参数指定连接类型,确保它为QueuedConnection

即,

connect(this, SIGNAL(window_loaded), this, SLOT(your_function()), Qt::ConnectionType(Qt::QueuedConnection | Qt::UniqueConnection));

如果您按照这种方式做,我相信您可以实现所需的目标。

  • 信号槽连接有几种类型:AutoConnectionDirectConnectionQueuedConnectionBlockingQueuedConnection(+ 可选的 UniqueConnection)。详细信息请阅读手册。 :)

5
假设您想在窗口显示后在UI线程中运行代码,您可以使用以下相对紧凑的代码。
class MainWindow : public QMainWindow
{
        // constructors etc omitted.

protected:
    void showEvent(QShowEvent *ev)
    {
        QMainWindow::showEvent(ev);
        // Call slot via queued connection so it's called from the UI thread after this method has returned and the window has been shown
        QMetaObject::invokeMethod(this, "afterWindowShown", Qt::ConnectionType::QueuedConnection);
    }

private slots:
    void afterWindowShown()
    {
        // your code here
        // note this code will also be called every time the window is restored from a minimized state
    }
};

它确实通过名称调用afterWindowShown,但这种做法在Qt中是相当常见的。有一些避免此类情况的方法,但它们会更加冗长。

请注意,此代码适用于任何QWidget派生类,不仅仅是QMainWindow派生类。

理论上,一个非常快速的用户可能会在调用afterWindowShown之前对显示窗口的UI执行某些操作,但这似乎很不可能。这是需要牢记并进行代码防御的事项。


2
我喜欢这个解决方案,谢谢。顺便说一下,在当前的 Qt 版本中,你可以这样写: QMetaObject::invokeMethod(this, &MainWindow::afterWindowShown, Qt::ConnectionType::QueuedConnection); 这样你就不用使用字符串函数名了。 - Youda008
我想知道这种方式和QTimer::singleShot( 0, ... );方法有何不同。 - Youda008
我添加了一个布尔保护,以便该操作仅在第一次发生。这使得事件看起来更像是Delphi中的OnShow事件。 - Bill99

2
我在这个问题中找到了一个好的答案,即使使用Sleep()函数也能很好地工作。
所以我尝试了这个:
//- cpp-file ----------------------------------------

#include "myapp.h"
#include <time.h>
#include <iosteream>

MyApp::MyApp(QWidget *parent)
    : QMainWindow(parent, Qt::FramelessWindowHint)
{
    ui.setupUi(this);
}

MyApp::~MyApp()
{

}

void MyApp::showEvent(QShowEvent *event) {
    QMainWindow::showEvent(event);
    QTimer::singleShot(50, this, SLOT(window_shown()));
    return;
}

void MyApp::window_shown() {
    std::cout << "Running" << std::endl;
    Sleep(10000);
    std::cout << "Delayed" << std::endl;
    return;
}

//- h-file ----------------------------------------

#ifndef MYAPP_H
#define MYAPP_H

#include <QtWidgets/QMainWindow>
#include <qtimer.h>
#include <time.h>
#include "ui_myapp.h"


class MyApp : public QMainWindow
{
    Q_OBJECT

public:
    MyApp(QWidget *parent = 0);
    ~MyApp();

protected:
    void showEvent(QShowEvent *event);


private slots:
    void window_shown();

private:
    Ui::MyAppClass ui;
};

#endif // MYAPP_H

1

我使用Paint事件解决了它,没有使用计时器。至少在Windows上对我有效。

// MainWindow.h
class MainWindow : public QMainWindow
{
    ...
    bool event(QEvent *event) override;
    void functionAfterShown();
    ...
    bool functionAfterShownCalled = false;
    ...
}

// MainWindow.cpp
bool MainWindow::event(QEvent *event)
{
    const bool ret_val = QMainWindow::event(event);
    if(!functionAfterShownCalled && event->type() == QEvent::Paint)
    {
        functionAfterShown();
        functionAfterShownCalled = true;
    }
    return ret_val;
}

谢谢。这是唯一对我有效的解决方案(首先显示“请稍候”窗口,然后启动一个长时间任务,定期调用QApplication::processEvents()以避免UI冻结)。 showEvent、排队信号和立即计时器(0毫秒)都不好:我怀疑它们在事件堆栈中太早了,最多只能得到屏幕上的垃圾。显然,任何基于任意计时器(例如50毫秒)的解决方案都高度依赖目标系统的速度,这意味着它不可靠。 - syam

1
我的最佳解决方案是仅计算一次绘制事件: .H
public:
void paintEvent(QPaintEvent *event);

.CPP

#include "qpainter.h"
#include <QMessageBox> // example

int contPaintEvent= 0;

void Form2::paintEvent(QPaintEvent* event)
{
  if (contPaintEvent ==0 )
  {
  QPainter painter(this); 
  QMessageBox::information(this, "title", "1 event paint"); // example
  // actions
  contPaintEvent++;
  }
}

0
重新实现方法void show(),如下所示:
void MainWindow::show()
{
    QMainWindow::show();
    // Call your special function here.
}

6
温馨提示:对于任何人来说,QWidget::show()不是虚函数,因此尝试在向上转型的指针上调用这个“重新实现”的方法必然会失败。请注意避免这种情况。 - user35443

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接