PyQt5 应用在 TeamViewer 下无法使用全屏模式
问题描述
使用 PyQt5 (版本为 5.7)中的 QtWebEngineView 构建的桌面应用,部署到远程机器(Windows 7 平台)上以全屏模式运行时,通过 teamviewer 观察到远程桌面没有变化,但是鼠标右键后会弹出菜单选项,与正常打开的 QtWebEngineView 弹出的菜单选项一致。而且直接在远程机器上运行的应用程序在显示器上显示正常,但是无法通过 teamviewer 远程观察。
部分代码:
view = QtWebEngineView()
view.setWindowFlags(Qt.FramelessWindowHint)
view.setUrl(QUrl("http://localhost:{}/".format(port)))
view.show()
分析问题并解决新产生的问题
推测是 teamviewer 无法捕捉到应用全屏后的界面,使用 Print Screen 按键截图后显示的图片也表示该应用的界面无法被捕捉。由于应用的使用场景需要,应用必须全屏,运行时不能漏出 Windows 的任务栏。所以尝试了多种解决方案:
view.showFullScreen()这种方法尝试过,但很快就发现有问题,并且不仅仅是 PyQt5 构建的应用无法远程监控,包括直接使用 Qt C++ 的代码编译的程序也无法通过远程监控。推测是 QWebEngineView 的原因导致的。- 根据目标机器的屏幕分辨率,调整应用窗口的大小,比如目标机器的分辨率为 1024x768,在高度上减少一个像素,使应用窗口为 1024x767,然后使用
view.show()
,即可显示,并能通过 teamviewer 远程观察。
第二种方法是可行的,但它仍然存在一个问题,Windows 桌面底部的任务栏会把应用程序的界面挡住。为了让这个应用程序界面置顶,需要设置窗口的标志位为 Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint
。
然后程序就可以正常运行了。但是新的问题又出现了,使用了 Qt.WindowStaysOnTopHint
标志位后,程序确实能够置顶,并且不会被任务栏挡住,但是当使用 alt + tab 组合键切换程序时,其他程序也会被该应用遮挡住,导致无法显示。(在实际使用中没有关系,但是在开发过程中有些头疼)
为了保持应用窗口的置顶效果,并使切换应用时,PyQt 应用不再置顶,可以定义一个 QWebEngineView 的子类,重写 changeEvent
方法:
class MyWebView(QWebEngineView):
def __init__(self, app):
super(MyWebView, self).__init__()
self.app = app
def changeEvent(self, event):
if event.type() == QEvent.ActivationChange:
active = self.isActiveWindow()
if active:
self.setWindowFlags( self.windowFlags() | Qt.WindowStaysOnTopHint )
if not self.isVisible():
self.show()
self.activateWindow()
else:
self.setWindowFlags( self.windowFlags() & ~Qt.WindowStaysOnTopHint )
if not self.isVisible():
self.show()
在 changeEvent
中,处理窗口激活事件,当窗口处于激活状态时,在标志位中增加 Qt.WindowStaysOnTopHint
,否则就去除这个标志位。当窗口的标志位改变时,窗口将会处于不可见的状态(可以打印 self.isVisible()
的值查看),所以需要再次调用 self.show()
方法显示窗口。
这里有个很坑的地方,如果不设置窗口的属性,调用 show() 方法后会自动激活窗口,即 self.isActiveWindow()
会返回 True,首次打开应用时这是理所应当的。但是当切换几次后就会发现窗口将会永远置顶(或者没法置顶了)。分析一下 changeEvent
的代码流程就清楚了。
永远置顶的情况是:
- 首次打开应用时,窗口处于 visible、active 状态;
- 切换应用时,此时将会触发 changeEvent 事件,调用 MyWebView 实例的
changeEvent()
方法,判断当前窗口是非激活状态的,因此修改标志位为self.windowFlags() & ~Qt.WindowStaysOnTopHint
,修改标志位后重新显示窗口,调用self.show()
方法,显示窗口。 - 由于
show()
方法调用后,会自动激活窗口,Qt 的事件循环机制导致changeEvent()
方法再次被调用,此时窗口处于激活状态,将重新修改标志位为self.windowFlags() | Qt.WindowStaysOnTopHint
,使得切换窗口后,其他程序仍然会被本应用遮挡。
永不置顶的情况是最后不论是哪种状态,标志位都会被改成不含 Qt.WindowStaysOnTopHint 的,因此就永远无法置顶了。
为了解决这个问题,就必须要将 show()
的调用和窗口激活分开,为此需要设置窗口的属性为:
view.setAttribute( Qt.WA_ShowWithoutActivating )
这样就可以实现在激活应用时窗口置顶,切换到其他程序时,应用窗口不置顶(如果其他应用显示的窗口下,可以看到本应用将堆叠在其他程序下面)。
关闭程序
上述代码解决了窗口置顶和显示其他程序时不置顶的需求,但是当你打开任务栏想要关闭程序的时候,发现程序关不掉了。只好重写 closeEvent()
方法:
def closeEvent(self, event):
self.activateWindow()
self.destroy()
self.app.quit()
from time import sleep
sleep(1)
sys.exit(0)
当点击窗口关闭按钮(实际上是没有的,因为会被设置成无边框窗口),或在任务栏中点击右键关闭时,程序将会终止,退出 Python 进程。
其他值得提及的部分
在主函数中,有行代码是特意加上去的:
view.settings().setAttribute( QWebEngineSettings.Accelerated2dCanvasEnabled, False)
这是因为远程机器没有显卡,因此无法使用显卡加速功能,但 QWebEngineView 如果默认会启用 QWebEngineSettings.Accelerated2dCanvasEnabled
这个属性,导致程序在远程机器上运行的时候,图表显示的功能会出现异常。因此需要显式关闭该属性。
小结
Qt 的界面为 Python 桌面应用开发带来了很多方便,但是由于环境的差异导致程序会出现一些不可名状的异常。在资料比较少的情况下,自己尝试摸索出代码的执行流程并且给出解决方案其实是挺耗时间的。不过好在最终成功解决了问题。
参考代码可在 github 上获取。