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

Windows窗口有很多种叫法:
父窗口(Parent)、子窗口(Child)、兄弟窗口(Sibling)、所有者(Owner)
活动窗口(Active)、焦点窗口(Focus)、前景窗口(Foreground)、后台窗口(Background)
层叠式窗口(OVERLAPPED)、弹出式窗口(POPUP)、子窗口(CHILD)
最小化窗口、最大化窗口、普通窗口、无边框全屏、真全屏
一、父窗口(Parent)、子窗口(Child)、兄弟窗口(Sibling)、所有者(Owner)
父窗口为桌面的窗口也叫顶级窗口(Top-Level Window)
子窗口一定有父窗口,一定没有所有者,因为此时父窗口相当于所有者
同属于一个父窗口的子窗口互为兄弟窗口
子窗口的坐标相对于父窗口,子窗口会随着父窗口移动、隐藏、禁用等
父窗口销毁时会销毁其子窗口,所有者销毁时会销毁被所有者
子窗口永远显示在父窗口上面,被所有者永远显示在所有者上面
子窗口可以是一个父窗口(即可以有子子窗口),但是子窗口不可以是一个所有者窗口
所有者窗口一定是层叠式窗口WS_OVERLAPPED或弹出式窗口WS_POPUP
由于所有者窗口不能是子窗口,所以所有者窗口的父窗口一定是桌面,所以所有者窗口一定是顶级窗口
Qt中Qt::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_MINIMIZEBOX,WS_EX_LEFT | WS_EX_LTRREADING | WS_EX_RIGHTSCROLLBAR,由于没有边框没有标题栏,所以无法拖动位置、无法改变窗口大小,由于有WS_MINIMIZEBOX所以支持最小化,但是由于没有标题栏,只能点击任务栏图标最小化和"恢复"。总结就是只支持点击任务栏最小化和“恢复”的、和屏幕尺寸刚好匹配的、无法改变大小和位置的一种窗口化。无边框全屏由于没有标题栏,只有最小化状态
Qt的showFullScreen()也是伪全屏,风格是WS_POPUP | WS_VISIBLE | WS_CLIPSIBLINGS | WS_CLIPCHILDREN | WS_SYSMENU,WS_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的工作流程之前,先说一下普通窗口的层叠关系
系统级别的霸道画面(例如调任务管理器那个画面)
系统级别的一些霸道的置顶窗口(你的窗口无法成为这些窗口的子窗口)
有EX_TOPMOST风格的并且不停raise的窗口(例如弹幕姬)
有EX_TOPMOST风格的窗口(例如搜狗输入法)
有EX_TOPMOST风格的并且不停lower的窗口(例如没有例如,一般没有这样的窗口)
普通的前景窗口
普通的不停raise的窗口
普通窗口
Qt中设置Qt::WindowStaysOnBottomHint并且不停raise的窗口
Qt中设置Qt::WindowStaysOnBottomHint的窗口
Qt中设置Qt::WindowStaysOnBottomHint并且不停lower的窗口,和,普通的不停lower的窗口(这两个同级,叠在一起会闪烁)
系统级别的一些霸道的置底窗口
桌面(也就是那个所有顶级窗口的爸爸)
注1:层叠关系保证子窗口在父窗口上、被拥有窗口在拥有者窗口上,因此子窗口会获得父窗口的EX_TOPMOST的效果
注2:Qt::WindowStaysOnBottomHint的窗口在成为前景窗口的时候会自动lower
dwm.exe的工作原理:(网上七零八碎搜罗整理的,细节上可能不太对)
在Windows中,一般现在所有需要画面的程序都有两个缓冲区(双重缓冲),一个在前台,一个在后台,后台表面是程序自己绘制的表面,绘制完成后交换前后台(这个交换可能有不同的方式,如果本身是显卡加速的程序,那么表面都在显存里直接交换指针,可以认为不花时间;如果本身是CPU绘图的,那么需要将内存中的缓冲区拷贝到显存,这会占用很多显卡性能)。dwm.exe就是把这些前台表面,加上一些特效,依据上述的层叠关系混合到显卡中的后台缓冲区,混合完后交换前后台,显卡将前台缓冲区的内容显示到屏幕上,dwm.exe会强制进行垂直同步。(但是这个dwm.exe的垂直同步略不同于游戏的垂直同步,几乎没有鼠标等等的迟滞感)
独占全屏的工作原理:
游戏直接在显卡的后台缓冲区绘制,绘制完成后翻转前后台,显卡将前台缓冲区的内容显示到屏幕上

查看一个独占全屏的风格,一般是WS_VISIBLE | WS_CLIPSIBLINGS,WS_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是我用来隐式转换WHND和WId用的,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