目标:将本地窗口嵌入到自己的程序中,最后完全还原原来窗口
(虽然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的 isActive Window ()判断的话, 顶级窗口 内的窗口全都为 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
))
{
//判断是否是独占全屏,伪全屏算作普通窗口已经保存了。但是独占全屏是本地窗口自己控制并且保存的,无法完美还原
。
我选择还原成一个原分辨率的普通窗口
,这需要获得最小化或者窗口化的风格
sta te = 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