专栏/Qt + WindowsAPI 本地窗口嵌入并“完全”还原

Qt + WindowsAPI 本地窗口嵌入并“完全”还原

2020年07月30日 14:43--浏览 · --点赞 · --评论
粉丝:4418文章:4

目标:将本地窗口嵌入到自己的程序中,最后完全还原原来窗口


(虽然Qt主打跨平台,但是奈何只靠Qt做不到被迫用API。虽然在深入了解Windows的窗口之后,对于我目前的需求来说没有必要这么做了,但是还是记录一下万一需要用到。主要也没别的地方写,试了上千次试出来的一定得记下来qwq)

Windows窗口有很多种叫法:

  1. 父窗口(Parent)、子窗口(Child)、兄弟窗口(Sibling)、所有者(Owner)

  2. 活动窗口(Active)、焦点窗口(Focus)、前景窗口(Foreground)、后台窗口(Background)

  3. 层叠式窗口(OVERLAPPED)、弹出式窗口(POPUP)、子窗口(CHILD)

  4. 最小化窗口、最大化窗口、普通窗口、无边框全屏、真全屏

、父窗口(Parent)、子窗口(Child)、兄弟窗口(Sibling)、所有者(Owner)

  • 父窗口为桌面的窗口也叫顶级窗口(Top-Level Window)

  • 子窗口一定有父窗口,一定没有所有者,因为此时父窗口相当于所有者

  • 同属于一个父窗口的子窗口互为兄弟窗口

  • 子窗口的坐标相对于父窗口,子窗口会随着父窗口移动、隐藏、禁用等

  • 父窗口销毁时会销毁其子窗口,所有者销毁时会销毁被所有者

  • 子窗口永远显示在父窗口上面,被所有者永远显示在所有者上面

  • 子窗口可以是一个父窗口(即可以有子子窗口),但是子窗口不可以是一个所有者窗口

  • 所有者窗口一定是层叠式窗口WS_OVERLAPPED或弹出式窗口WS_POPUP

  • 由于所有者窗口不能是子窗口,所以所有者窗口的父窗口一定是桌面,所以所有者窗口一定是顶级窗口

  • QtQt::Widget风格的窗口,构造时的parent是父窗口

  • Qt中Qt::Window风格的窗口,构造时的parent是所有者

  • Qt中不管是Qt::Widget还是Qt::Window,构造后设置的setParent()都是父窗口

  • Windows提供了::GetWindow(hWnd, GW_OWNER)获得所有者,但是不提供设置所有者的方法

  • Windows提供了::GetParent(hWnd)获得父窗口或所有者,即如果是子窗口返回父窗口句柄,如果是被拥有返回所有者句柄

  • Windows提供了::SetParent(childHwnd, parentHwnd)方法设置父窗口

  • 设置父窗口之前需要移除WS_POPUP风格(可能本来就没有),然后添加WS_CHILD风格,否则会设置失败

  • Windows提供了::IsChild(parentHwnd, childHwnd)方法判断父子关系

  • Windows提供了::GetWindowLongW(hWnd, GWL_STYLE)获得窗口的样式,通过判断是否有WS_CHILD可以判断是否是子窗口

二、活动窗口(Active)、焦点窗口(Focus)、前景窗口(Foreground)、后台窗口(Background)

  • 每个线程都可能有很多个窗口,由线程自己维护一个活动窗口和一个焦点窗口

  • 活动窗口是顶级窗口

  • Windows整个系统只能同时有一个真正的活动窗口,这个窗口也叫前景窗口

  • Windows提供了::GetForegroundWindow()获得前景窗口

  • 前景窗口所在的线程也叫前景线程(前景线程似乎会有更高的优先级)

  • Windows提供了::GetActiveWindow()获得当前线程内的活动窗口,如果当前线程不是前景线程,则返回0

  • Windows提供了::GetTopWindow(parentHwnd)获得所有子窗口中Z序最上层的窗口

  • 焦点窗口所在的顶级窗口是活动窗口,用Qt的isActiveWindow()判断的话,顶级窗口内的窗口全都为true

  • 一般只有前景线程的焦点窗口可以获得键盘输入

三、层叠式窗口(OVERLAPPED)、弹出式窗口(POPUP)、子窗口(CHILD)

这里主要涉及Windows窗口的风格属性,通过::GetWindowLongW(hWnd, GWL_STYLE)获得STYLE属性,::GetWindowLongW(hWnd, GWL_EXSTYLE)获得EX_STYLE属性,::SetWindowLongW(hWnd, GWL_STYLE, newStyle)设置STYLE属性,::SetWindowLongW(hWnd, GWL_EXSTYLE, newExStyle)设置EX_STYLE属性

  • WS_OVERLAPPED,0x00000000,如果不是WS_POPUP也不是WS_CHILD,就是WS_OVERLAPPED

  • WS_POPUP,0x80000000,指示窗口是一个弹出式窗口

  • WS_CHILD,0x40000000,指示窗口是一个子窗口,与WS_POPUP不兼容(可以强行添加出奇怪的情况)

  • WS_MINIMIZE,0x20000000,指示窗口当前是最小化状态

  • WS_VISIBLE,0x10000000,指示当前窗口可见

  • WS_DISABLED,0x08000000,指示当前窗口禁用即不接受键盘鼠标输入

  • WS_CLIPSIBLINGS,0x04000000,指示窗口绘制时不绘制被兄弟窗口遮挡的部分

  • WS_CLIPCHILDREN,0x02000000,指示窗口绘制时不绘制被子窗口遮挡的部分

  • WS_MAXIMIZE,0x01000000,指示窗口当前是最大化状态

  • WS_CAPTION,0x00C00000,其值等于WS_BORDER | WS_DLGFRAME

  • WS_BORDER,0x00800000,指示窗口有单边框

  • WS_DLGFRAME,0x00400000,指示窗口带对话框样式,不带标题框

  • WS_VSCROLL,0x00200000,指示窗口带有垂直滚动条

  • WS_HSCROLL,0x00100000,指示窗口带有水平滚动条

  • WS_SYSMENU,0x00080000,指示标题框上带有窗口菜单,需要WS_CAPTION

  • WS_THICKFRAME,0x00040000,指示窗口具有可调边框(可以改变窗口大小)

  • WS_MINIMIZEBOX,0x00020000,指示窗口有最小化按钮(可以最小化)

  • WS_MAXIMIZEBOX,0x00010000,指示窗口有最大化按钮(可以最大化)

  • WS_OVERLAPPEDWINDOW 组合样式即 WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX

  • WS_POPUPWINDOW 组合样式即 WS_POPUP | WS_BORDER | WS_SYSMENU

  • WS_CHILDWINDOW WS_CHILD

    WS_EX_STYLE有挺多但大都不常用,用spy++通常能看见的就是下面几个

  • WS_EX_LEFT,0x00000000,指示窗口是左对齐的,是缺省值

  • WS_EX_LTRREADING,0x00000000,指示文本是从左往右的,是缺省值

  • WS_EX_RIGHTSCROLLBAR,0x00000000,指示垂直滚动条显示在右边,是缺省值

  • WS_EX_NOPARENTNOTIFY,0x00000004,指示窗口创建/销毁时不通知父窗口

  • WS_EX_TOPMOST,0x00000008,指示窗口是一个置顶窗口,需要放在其他非置顶窗口上方

  • WS_EX_WINDOWEDGE,0x00000100,指示窗口具有凸起边框

  • WS_EX_TOOLWINDOW,0x00000080,工具条窗口,将不显示在任务栏、Alt+Tab中

  • WS_EX_APPWINDOW,0x00040000,当窗口可见时,强制在任务栏显示

  • WS_EX_CONTROLPARENT,0x00010000,允许用户用TAB键遍历窗口的子窗口

  • WS_EX_LAYERED,0x00080000,分层窗口,主要用于透明和异形窗口

  • WS_EX_NOREDIRECTIONBITMAP,0x00200000,指示窗口没有重定向表面

子窗口就是有WS_CHILD的窗口,弹出式窗口就是有WS_POPUP的窗口,层叠式窗口就是既没有WS_CHILD也没有WS_POPUP的窗口

四、最小化窗口、最大化窗口、普通窗口、无边框全屏、独占全屏

使用::ShowWindow(hWnd, SW_RESTORE)可以还原一个窗口,这个还原是指还原到上一个状态,或者称之为“恢复”。在最大化窗口标题栏上的"还原"按钮,是指还原成之前的普通窗口。有些窗口最小化之后还有一个窗口,这个上面的还原按钮是指“恢复”。有两种还原,区分一下。使用::GetWindowRect(hWnd, &rect)等类似的函数获得的都是当前状态的矩形

  • 最小化窗口是有WS_MINIMIZE的窗口,尺寸一般是类似 [-32000, -32000, 160x28] 的矩形,最小化状态调用Qt的getGeometry()或者getFrameGeometry()等函数获得的都是“恢复”之后的矩形

  • 最大化窗口是有WS_MAXIMIZE的窗口,尺寸一般是保留一部分标题栏高度、尽可能将客户区填满屏幕有效区域的窗口,最大化状态调用Qt的getGeometry()或者getFrameGeometry()等函数获得的都是最大化状态的矩形,无法获得"还原"成普通窗口的矩形

  • 普通窗口

  • 无边框全屏本质是一个无边框的窗口,位置、大小刚好和整个屏幕匹配。风格类似WS_POPUP | WS_VISIBLE | WS_CLIPSIBLINGS | WS_CLIPCHILDREN | WS_SYSMENU | WS_MINIMIZEBOXWS_EX_LEFT | WS_EX_LTRREADING | WS_EX_RIGHTSCROLLBAR,由于没有边框没有标题栏,所以无法拖动位置、无法改变窗口大小,由于有WS_MINIMIZEBOX所以支持最小化,但是由于没有标题栏,只能点击任务栏图标最小化和"恢复"总结就是只支持点击任务栏最小化和“恢复”的、和屏幕尺寸刚好匹配的、无法改变大小和位置的一种窗口化。无边框全屏由于没有标题栏,只有最小化状态

    Qt的showFullScreen()也是伪全屏,风格是WS_POPUP | WS_VISIBLE | WS_CLIPSIBLINGS | WS_CLIPCHILDREN | WS_SYSMENUWS_EX_LEFT | WS_EX_LTRREADING | WS_EX_RIGHTSCROLLBAR,和一般游戏的无边框全屏相比只少了个最小化WS_MINIMIZEBOX,甚至没有置顶。由于本质上伪全屏是一个普通窗口(所以不存在"还原"成普通窗口矩形),所以调用Qt的getGeometry()或者getFrameGeometry()等函数获得的就是无边框全屏的矩形。对于Windows无边框全屏就是普通窗口,但是Qt在showFullScreen()之后还可以showNormal()变回去,所以这些变化对于一个Qt控制的窗口应该是Qt自己内部保存和实现了

    很多游戏内设置的显示是“全屏”,实际上是无边框全屏,特点是切出切入游戏不会有黑屏,输入法等可以直接显示在游戏画面上层

  • 独占全屏在Windows上似乎只有DirectX能实现,Windows有一个程序叫dwm.exe(Desktop Window Manager),这个程序是将所有普通窗口的画面混合然后输出的程序(包括窗口的堆叠关系和一些透明特效等等),独占全屏则是直接绕过dwm.exe,游戏直接向显示器输出,会有一些性能优势

在说dwm.exe的工作流程之前,先说一下普通窗口的层叠关系

  1. 系统级别的霸道画面(例如调任务管理器那个画面)

  2. 系统级别的一些霸道的置顶窗口(你的窗口无法成为这些窗口的子窗口)

  3. EX_TOPMOST风格的并且不停raise的窗口(例如弹幕姬)

  4. EX_TOPMOST风格的窗口(例如搜狗输入法)

  5. EX_TOPMOST风格的并且不停lower的窗口(例如没有例如,一般没有这样的窗口)

  6. 普通的前景窗口

  7. 普通的不停raise的窗口

  8. 普通窗口

  9. Qt中设置Qt::WindowStaysOnBottomHint并且不停raise的窗口

  10. Qt中设置Qt::WindowStaysOnBottomHint的窗口

  11. Qt中设置Qt::WindowStaysOnBottomHint并且不停lower的窗口,和,普通的不停lower的窗口(这两个同级,叠在一起会闪烁)

  12. 系统级别的一些霸道的置底窗口

  13. 桌面(也就是那个所有顶级窗口的爸爸)

  • 注1:层叠关系保证子窗口在父窗口上、被拥有窗口在拥有者窗口上,因此子窗口会获得父窗口的EX_TOPMOST的效果

  • 注2:Qt::WindowStaysOnBottomHint的窗口在成为前景窗口的时候会自动lower

dwm.exe的工作原理:(网上七零八碎搜罗整理的,细节上可能不太对)

    在Windows中,一般现在所有需要画面的程序都有两个缓冲区(双重缓冲),一个在前台,一个在后台,后台表面是程序自己绘制的表面,绘制完成后交换前后台(这个交换可能有不同的方式,如果本身是显卡加速的程序,那么表面都在显存里直接交换指针,可以认为不花时间;如果本身是CPU绘图的,那么需要将内存中的缓冲区拷贝到显存,这会占用很多显卡性能)。dwm.exe就是把这些前台表面,加上一些特效,依据上述的层叠关系混合到显卡中的后台缓冲区,混合完后交换前后台,显卡将前台缓冲区的内容显示到屏幕上,dwm.exe会强制进行垂直同步。(但是这个dwm.exe的垂直同步略不同于游戏的垂直同步,几乎没有鼠标等等的迟滞感)


独占全屏的工作原理:

    游戏直接在显卡的后台缓冲区绘制,绘制完成后翻转前后台,显卡将前台缓冲区的内容显示到屏幕上

查看一个独占全屏的风格,一般是WS_VISIBLE | WS_CLIPSIBLINGSWS_EX_LEFT | WS_EX_LTRREADING | WS_EX_RIGHTSCROLLBAR | WS_EX_TOPMOST,和伪全屏相比少了WS_MINIMIZEBOX,少了WS_POPUP,多了WS_EX_TOPMOST。这个可以帮助判断是独占全屏还是伪全屏。不过这里是什么风格已经无所谓了,这个时候游戏已经完全绕过了dwm.exe


独占全屏相比伪全屏的异同:

  • 都是前景窗口、前景线程,获得系统优待,能有更高的优先级,获得更多的系统资源

  • 伪全屏需要经过dwm.exe的混合(不清楚dwm.exe有没有对这种特殊情况针对性的处理,也许其实可以忽略)

  • 独占全屏能独占地获得显卡的全部资源(可能真全屏切入切出游戏的黑屏就是在处理这个独占资源,我的臆想是在切入游戏的时候,系统会把无关游戏的资源全部从显存扔到内存,这样可以让游戏获得更多的显卡资源)

  • 伪全屏由于经过dwm.exe,所以可以显示诸如输入法、弹幕姬之类的程序

  • 但即使是伪全屏,开发者可以选择充分利用系统资源,这和真全屏是一样的,当然这由开发者定。但是一般窗口化可能会收敛一点

  • 独占全屏下,一般来说显示的分辨率、色深等等都由游戏决定,但是伪全屏由系统决定


以上只是刚了解了Windows的窗口,对于一个窗口,如果需要在进行了一通操作之后还原,那么需要记录一些信息。Windows记录的信息主要是还原矩形(也就是普通窗口的坐标大小),STYLE和EXSTYLE,父窗口等信息。如果我们需要完美还原,也就需要把这些信息记录下来。(主程序是用Qt的)


第一步初始化一些必须手动初始化的变量,变量都定义在类里面(方便阅读这里加了变量类型),以下是装载函数installNativeWindow(MyWindowSetAPI::EasyHWND windowHandle),其中EasyHWND是我用来隐式转换WHNDWId用的,MyWindowSetAPI是我需要用到的一些API和一些debug用写的函数的命名空间,根据函数命名应该就理解了,可以选择搜索引擎查一下相关API

MyWindowSetAPI::EasyHWND hwnd = windowHandle; //本地窗口句柄

Qt::WindowState state = Qt::WindowState::WindowNoState; //窗口初始状态 或者 最小化还原后的状态

LONG nativeWindowStyle = WS_OVERLAPPEDWINDOW;

LONG nativeWindowExStyle = WS_EX_LEFT | WS_EX_RIGHTSCROLLBAR | WS_EX_LTRREADING; //其实就是初始化为0

QScreen* appScreen = nullptr; //原窗口所在屏幕

QWindow* nativeWindow = QWindow::fromWinId(hwnd); //用QWindow类来操控本地窗口


QWindow控制一个外部窗口可以带来一些方便,例如计算坐标等等,但是也引入了一些问题,大概因为QWindow自己内部的实现和Windows不同步

第二步开始获得本地窗口的状态

  if (nativeWindow !=nullptr) {

      setWindowTitle(tr(u8"窗口容器-") + MyWindowSetAPI::getWindowInfoFromHWND(hwnd).title); //获取一下窗口标题,可以不需要,用来反馈显示一下的,这里用Qt的QWindow::title()是获取不到的

      nativeWindowStyle = MyWindowSetAPI::getWindowStyle(hwnd); //保存普通窗口/无边框全屏/最大化时的风格

      nativeWindowExStyle = MyWindowSetAPI::getWindowExStyle(hwnd); //同理,保存Ex风格

      nativeNormalRect = nativeWindow->geometry(); //保存普通窗口/无边框全屏时的坐标

      nativeNormalFlags = nativeWindow->flags(); //保存普通窗口/无边框全屏时的flag,Qt似乎是通过这个计算坐标的

      if (nativeWindowStyle & WS_MINIMIZE) } //先判断是否是最小化状态

          nativeMinimized = true; //这个变量用来保存最初是否是最小化的

          MyWindowSetAPI::showWindowRestore(hwnd); //将窗口“恢复”

          nativeWindowStyle = MyWindowSetAPI::getWindowStyle(hwnd); //获取恢复之后的风格

      }

      appScreen = QGuiApplication::screenAt(nativeNormalRect.center()); //获取本地窗口所在的屏幕

    if (appScreen) {

        if (nativeWindowStyle & WS_MAXIMIZE) { //判断窗口是否是最大化

            state = Qt::WindowState::WindowMaximized;

            nativeMaximizedRect = nativeWindow->geometry(); //保存最大化矩形,免去自己算坐标的麻烦,况且最大化不一定是填满有效桌面

            nativeMaximizedFlags = nativeWindow->flags(); //保存最大化flag

            nativeWindow->showNormal(); //“还原”普通窗口

            nativeNormalRect = nativeWindow->geometry(); //保存普通窗口矩形

            nativeNormalFlags = nativeWindow->flags(); //保存普通窗口flag

        }

        else if (nativeWindow->frameGeometry() == appScreen->geometry() && !(nativeWindowStyle & WS_POPUP)) { //判断是否是独占全屏,伪全屏算作普通窗口已经保存了。但是独占全屏是本地窗口自己控制并且保存的,无法完美还原我选择还原成一个原分辨率的普通窗口,这需要获得最小化或者窗口化的风格

            state = Qt::WindowState::WindowFullScreen;

            if (!nativeMinimized) { //如果最初不是最小化,那么保存一下最小化的状态信息,还原时用

                MyWindowSetAPI::showWindow(hwnd, Qt::WindowState::WindowMinimized); //Qt的showMinimized()不管用,只好直接调用系统API

                nativeFullScreenMinFlags = nativeWindow->flags();

                nativeWindowStyle = MyWindowSetAPI::getWindowStyle(hwnd);

                nativeWindowExStyle = MyWindowSetAPI::getWindowExStyle(hwnd);

            }

        }

    }

      nativeWindowStyle = nativeWindowStyle & (~(WS_MINIMIZE | WS_MAXIMIZE)); //需要后期手动加上

      installNativeWindowInfoSaved(); //装载本地窗口的其他操作

  }

  else {

      setWindowTitle(tr(u8"窗口容器-无"));

  }


接下来是卸载本地窗口的方法,整个过程基本是装载过程倒过来

  if (hwnd != 0 && nativeWindow) {

        nativeWindow->setVisible(false);

        nativeWindow->setParent(nullptr); //由于主程序之前把本地窗口嵌入了,这里还原一下

        nativeWindow->lower();

        if (state == Qt::WindowState::WindowFullScreen && appScreen) { //独占全屏无法还原除非....,这里选择变成一个原分辨率的窗口

          nativeWindow->setFlags(nativeFullScreenMinFlags); //先设置flags,因为Qt似乎通过这个计算坐标

        MyWindowSetAPI::setWindowStyle(hwnd, nativeWindowStyle); //之后再设置风格,因为似乎Windows通过这个计算坐标

        MyWindowSetAPI::setWindowExStyle(hwnd, nativeWindowExStyle);

        QRect screenRect = appScreen->geometry();

        nativeWindow->setFramePosition(screenRect.topLeft() + QPoint(40,40)); //这个40,40的偏移是为了让窗口标题栏在屏幕内,不然鼠标就点不到了

        nativeWindow->resize(screenRect.size());

        nativeWindow->showNormal(); //很关键

    }

    else {

            //Normal or BorderlessFullScreen,窗口化或者无边框的恢复

        nativeWindow->setFlags(nativeNormalFlags);

        MyWindowSetAPI::setWindowStyle(hwnd, nativeWindowStyle);

        MyWindowSetAPI::setWindowExStyle(hwnd, nativeWindowExStyle);

          nativeWindow->setGeometry(nativeNormalRect); //一定要在设置flags和style之后设置坐标才能正确恢复

            if (state == Qt::WindowState::WindowMaximized) { //如果之前有最大化状态,那么从普通窗口化切到最大化

            nativeWindow->setFlags(nativeMaximizedFlags);

           nativeWindow->setGeometry(nativeMaximizedRect); //先flag和style再geometry,style已经在普通窗口设置了

            MyWindowSetAPI::addWindowStyle(hwnd, WS_MAXIMIZE); //要在先设置坐标后再添加最大化风格,这样点击还原按钮才能正确“还原”

        }

    }

    nativeWindow->raise(); //在扔掉它之前再最后利用一下

    delete  nativeWindow; //尽快扔掉,因为QWindow会干扰我们对本地窗口的设置

    nativeWindow = nullptr;

    if (nativeMinimized) //如果最初是最小化的,那么最小化,需要调用API才有效,况且nativeWindow已经delete掉了

        MyWindowSetAPI::showWindow(hwnd, Qt::WindowState::WindowMinimized);

    MyWindowSetAPI::addWindowStyle(hwnd, WS_VISIBLE); //很关键

    hwnd = 0;

  }

这样就能实现装载/卸载本地窗口了。一般类型的顶级窗口都可以了。非顶级窗口一般不乱改吧。

需要windows.h,User32.lib

投诉或建议