Featured image of post Qt show()、raise()、activateWindow() 调用顺序区别

Qt show()、raise()、activateWindow() 调用顺序区别

从 Qt 源码角度分析 QWidget::show()、showNormal()、raise()、activateWindow() 的职责与推荐调用顺序。

-

在 Qt 开发中,经常会看到这样的代码:

1
2
3
widget->show();
widget->raise();
widget->activateWindow();

很多人会写,但不一定说得清这三个调用分别在做什么、为什么要按这个顺序调用。

如果只看现象,很容易把它们理解成“都和把窗口弄到前台有关”。但从 Qt 源码来看,这三个函数负责的是 三个不同层面 的事情:

  • show():让窗口变成可见
  • raise():调整 Z 轴顺序,让窗口尽量到上层
  • activateWindow():请求把窗口设为活动窗口,争取键盘焦点

这三件事并不等价,所以调用顺序也不是随便写的。

1. show() :先让窗口可见

先看 QWidget::show()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
void QWidget::show()
{
    // Note: We don't call showNormal() as not to clobber Qt::Window(Max/Min)imized
    if (!isWindow()) {
        setVisible(true);
    } else {
        const auto *platformIntegration = QGuiApplicationPrivate::platformIntegration();
        Qt::WindowState defaultState = platformIntegration->defaultWindowState(data->window_flags);
        if (defaultState == Qt::WindowFullScreen)
            showFullScreen();
        else if (defaultState == Qt::WindowMaximized)
            showMaximized();
        else
            setVisible(true);
    }
}

这里最关键的信息有两个。

1.1 show() 的本质是 setVisible(true)

对于普通子控件,show() 直接就是 setVisible(true)

对于顶层窗口,Qt 会先看平台和窗口标志,决定是否默认以:

  • 全屏 showFullScreen()
  • 最大化 showMaximized()
  • 普通可见 setVisible(true)

所以 show() 的核心职责只有一个:让窗口进入可见状态

它不负责:

  • 不负责提升层级
  • 不负责抢焦点
  • 不负责把最小化窗口恢复成普通状态

1.2 show() 不会主动清掉最小化/最大化状态

源码里这句注释其实非常关键:

1
// Note: We don't call showNormal() as not to clobber Qt::Window(Max/Min)imized

意思是:show() 不会主动调用 showNormal(),因为 Qt 不想把已有的最大化/最小化状态直接抹掉。

这意味着:如果一个窗口已经最小化,你再调 show(),它并不等于“恢复窗口”。这也是为什么很多场景下应该用 showNormal(),而不是只写 show()

2. showNormal() :从最小化/最大化/全屏恢复

再看 QWidget::showNormal()

1
2
3
4
5
6
7
8
void QWidget::showNormal()
{
    ensurePolished();
    setWindowState(windowState() & ~(Qt::WindowMinimized
                                     | Qt::WindowMaximized
                                     | Qt::WindowFullScreen));
    setVisible(true);
}

它做了两步:

  1. 清掉这几个状态位:
    • Qt::WindowMinimized
    • Qt::WindowMaximized
    • Qt::WindowFullScreen
  2. setVisible(true)

所以 showNormal() 的语义非常明确:把窗口恢复到普通状态并显示出来

这就是为什么下面这种场景,showNormal()show() 更合适:

  • 主窗口被最小化到了任务栏
  • 单例程序二次启动时,需要把已有窗口恢复出来
  • 点击托盘图标时,希望把隐藏/最小化窗口重新拉回桌面

换句话说:

  • show() 解决的是“看不看得见”
  • showNormal() 解决的是“是不是还卡在最小化/最大化/全屏状态里”

3. raise() :调整堆叠顺序,不等于激活窗口

QWidget::raise() 源码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void QWidget::raise()
{
    Q_D(QWidget);
    if (!isWindow()) {
        QWidget *p = parentWidget();
        const int parentChildCount = p->d_func()->children.size();
        if (parentChildCount < 2)
            return;
        const int from = p->d_func()->children.indexOf(this);
        Q_ASSERT(from >= 0);
        if (from != parentChildCount -1)
            p->d_func()->children.move(from, parentChildCount - 1);
        if (!testAttribute(Qt::WA_WState_Created) && p->testAttribute(Qt::WA_WState_Created))
            create();
        else if (from == parentChildCount - 1)
            return;
        QRegion region(rect());
        d->subtractOpaqueSiblings(region);
        d->invalidateBackingStore(region);
    }
    if (testAttribute(Qt::WA_WState_Created))
        d->raise_sys();
    if (d->extra && d->extra->hasWindowContainer)
        QWindowContainer::parentWasRaised(this);
    QEvent e(QEvent::ZOrderChange);
    QCoreApplication::sendEvent(this, &e);
}

这个函数要分成 子控件顶层窗口 两部分来看。

3.1 对子控件来说,raise() 主要是在父对象的 children 列表里挪位置

这一段最说明问题:

1
2
if (from != parentChildCount -1)
    p->d_func()->children.move(from, parentChildCount - 1);

也就是说,对于非窗口控件,raise() 首先会把当前控件移动到父控件 children 列表的最后面,从而改变绘制和堆叠顺序。

后面又会:

  • 必要时 create()
  • 重新计算区域
  • invalidateBackingStore(region) 触发重绘

所以对子控件来说,raise() 的核心含义是:把我放到兄弟控件的最上层

3.2 对顶层窗口来说,真正关键的是 d->raise_sys()

对于顶层窗口,关键是这句:

1
2
if (testAttribute(Qt::WA_WState_Created))
    d->raise_sys();

这说明两件事:

  1. raise() 最终要落到平台层,也就是请求窗口系统提升窗口层级
  2. 前提是窗口已经创建,也就是 Qt::WA_WState_Created 为真

这就是一个非常容易忽略的点:

如果顶层窗口还没有创建,单独调用 raise() 可能什么都不会发生。

而顶层窗口通常是在 show() / setVisible(true) 之后才真正进入“已创建”状态。因此从源码角度看,show()raise() 是合理的。

3.3 raise() 不等于“获得焦点”

raise() 做的是层级调整,不是焦点激活。

即使窗口已经被放到上层,也不代表它已经成为活动窗口,更不代表它已经拿到键盘输入焦点。

所以只写:

1
widget->raise();

只能表达“请把这个窗口提到前面一点”,不能表达“让用户马上输入”。

4. activateWindow() :请求激活顶层窗口

再看 QWidget::activateWindow()

1
2
3
4
5
6
void QWidget::activateWindow()
{
    QWindow *const wnd = window()->windowHandle();
    if (wnd)
        wnd->requestActivate();
}

4.1 它操作的不是当前控件本身,而是它所属的顶层窗口

源码里写的是:

1
window()->windowHandle()

也就是说,即使你在一个子控件上调用 activateWindow(),最后激活的也不是这个子控件,而是 包含它的顶层窗口

所以 activateWindow() 的语义并不是“让这个 widget 获得焦点”,而是:

让这个 widget 所在的顶层窗口成为活动窗口。

4.2 它只是调用 requestActivate(),不是强制激活

源码最后只是:

1
wnd->requestActivate();

注意这个名字叫 requestActivate(),不是 forceActivate()

也就是说,Qt 能做的是向底层窗口系统发起“激活请求”,至于系统是否接受,要看平台策略。

Qt 自己的注释已经说得很清楚:

  • 在 X11 上,结果依赖 Window Manager
  • 如果希望窗口也被堆叠到上面,应该额外调用 raise()
  • 窗口必须是可见的,否则 activateWindow() 没有效果
  • 在 Windows 上,如果当前应用不是活动应用,系统通常不会允许你直接抢走前台焦点,只会在任务栏上提示用户

这也解释了为什么很多人觉得 activateWindow() “有时有效,有时无效”。

不是 Qt 不执行,而是 Qt 只能请求,不能绕过操作系统的前台窗口策略

5. 为什么推荐顺序是 showNormal() / show()raise()activateWindow()

从上面的源码可以直接推导出推荐顺序。

5.1 第一步先让窗口可见

activateWindow() 的注释里已经明确写了:

the window must be visible, otherwise activateWindow() has no effect.

同时 activateWindow() 内部还依赖:

1
window()->windowHandle()

如果窗口还没显示,windowHandle() 很可能还没准备好,调用就直接空转。

所以必须先 show()showNormal()

5.2 第二步再调整层级

raise() 的职责是把窗口提到更上层。

Qt 注释里对 activateWindow() 也专门说了:

If you want to ensure that the window is stacked on top as well you should also call raise().

也就是说,激活置顶到当前堆叠前面 是两件事。

如果你只调用 activateWindow(),某些平台上未必会自动帮你把窗口提到你想要的位置;而 raise() 就是在补这个动作。

5.3 第三步再请求激活

等窗口已经:

  • 处于可见状态
  • 层级也尽量被抬上来

再调用 activateWindow() 去请求焦点,语义最完整。

所以推荐顺序是:

1
2
3
widget->show();
widget->raise();
widget->activateWindow();

如果窗口可能处于最小化状态,则更推荐:

1
2
3
widget->showNormal();
widget->raise();
widget->activateWindow();

6. 总结

从 Qt 源码看,这几个 API 的职责边界非常清楚:

  • show():让窗口可见,但不负责恢复普通状态,也不负责焦点
  • showNormal():清掉最小化/最大化/全屏状态,再显示
  • raise():调整 Z 轴顺序,尽量把窗口放到更上层
  • activateWindow():向窗口系统请求激活顶层窗口

因此:

  • 想恢复最小化窗口:showNormal()
  • 想把窗口提到前面:raise()
  • 想请求焦点:activateWindow()
  • 想完整表达“显示并尽量切到前台”:showNormal() / show() + raise() + activateWindow()

最常用、也最符合源码语义的调用顺序就是:

1
2
3
widget->showNormal();
widget->raise();
widget->activateWindow();

如果窗口本来不是最小化状态,再把 showNormal() 换成 show() 即可。