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

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

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

2020-07-30 22:43 --阅读 · --喜欢 · 云blade
粉丝: 4548 文章: 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

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

  • 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的工作流程之前,先说一下普通窗口的层叠关系

  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_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

投诉或建议
优豹LED头灯打整[完整版]
前段时间打整过一批头灯,也用了一段时间.....回想当年自己打整这些手电或头灯手法应该激进很多,而且上一篇专栏里找老的QQ空间照片,这头灯当时至少是把电路板舱都打开过.....不行,重新打整,顺带补点货,打整好了送两个给朋友。 于是杂七杂八一堆东西准备好,桌上一放就成这样.....各位应该也发现,这次打整还涉及到更换密封圈,而且也确实遇到些问题.....不管了,开搞,上一篇专栏就是简略版,这篇才是完整版,按这标准打整完的头灯,短时间下水不成问题。 这款头灯有个挺烦人的问题,镜片用的塑料,而且质量不太行。左