最近在做一个PyQt5项目,其中有一个功能是窗口启动后开始计时,在计时的时候,要求能在Windows的任务栏上看到预览窗口也在持续刷新(Window速览功能),如下:
可以看到,当鼠标悬停在系统任务栏的窗口图标上时,出现预览窗口。
然而当按下窗口最小化按钮的时候,该预览窗口只保留最小化之前的界面截图作为预览,而不会持续刷新,也就是说,当窗口处于最小化状态的时候,该Windows速览窗口并不会持续刷新,而是保持在最小化前的界面。(注意是手动按下最小化按钮,而不是切换至其他窗口)
这个问题是Windows的机制,我以为只有我的程序会这样,然而当我用暴风影音
、迅雷影音
试验时,也同样出现了这个问题。当这俩媒体播放器正在播放视频时,被最小化后,Windows的速览窗口也不再刷新,而是保持在视频最小化前的画面。
为了解决这个问题,踩了很多坑,也看了很多Qt的代码,最后才算是解决了这个问题。
我的解决思路是,不使用窗口的最小化功能,取而代之的是把窗口置于最底层,当需要显示的时候,才将窗口置顶。也就是说,当按下最小化按钮时,将窗口置于最底层,当需要恢复时,将窗口置顶。以此来模拟窗口最小化和最大化的功能。
熟悉PyQt
或者Qt
的都知道,将窗口置于底层和置顶的方法可以通过动态调用setWindowFlags()
方法来实现,如下:
# 置于最底层
self.setWindowFlags(Qt.WindowStaysOnBottomHint)
# 置顶
self.setWindowFlags(Qt.WindowStaysOnTopHint)
然而PyQt
或者Qt
中,当窗口第二次调用setWindowFlags()
方法的时候,窗口会自动隐藏起来,这是因为第二次调用时,在内部调用hide()
方法,详情可以看这里:
Qt之使用setWindowFlags方法遇到的问题
文中提到了两种方法,一种就是我上面说过的用setWindowFlags()
方法,另一种是调用Windows的api,通过窗口的类名和标题来获取到窗口,再用Windows提供的apiSetWindowPos()
方法来使窗口置顶或置底。注意此类名
并非在代码中我们的定义的窗口class的类名。
经过笔者试验,用setWindowFlags()
的方式并不能很好的解决问题,动态调用setWindowFlags()
会导致任务栏上的窗口图标先关闭再重新生成,十分影响用户体验。没办法,只好尝试第二种方法了。
根据类名和标题获取窗口,首先要知道窗口的类名和标题,如何获得窗口的类名和标题呢?这里要用到一个工具spy++
,该工具可以查看到所有窗口的类名和标题。其中标题即是我们给窗口设置的标题。
self.setWindowTitle("Timer")
spy++
工具在下面这篇文章中有提供,也详细地阐述了win32gui
的FindWindow
方法,可以看这里:
查找窗口的句柄方法
在PyQt
中,调用SetWindowPos()
需要用到win32gui
和win32con
库,代码如下:
# 获得当前窗口,必须在窗口'show'之后才能获取到,第一个参数是窗口的类名,第二个是标题
self.hwnd = win32gui.FindWindow("Qt5QWindowIcon", "Timer")
# 窗口置于最底层,最小化按钮被按下时调用
win32gui.SetWindowPos(self.hwnd, win32con.HWND_BOTTOM, self.x(), self.y(), self.width(),self.height(), win32con.SWP_SHOWWINDOW)
# 窗口置顶,恢复窗口时调用
win32gui.SetWindowPos(self.hwnd, win32con.HWND_TOPMOST, self.x(), self.y(), self.width(), self.height(), win32con.SWP_SHOWWINDOW)
这里之所以用到self
,是因为代码是写在自定义的QWidget
类里面,在C++
的代码中,作用类似于this
。
可以看到置底和置顶的方法,区别只在于SetWindowPos
方法的第二个参数,可以根据需求自行修改,恢复窗口中传入了win32con.HWND_TOPMOST
,是为了使窗口一直处于最顶层。而Qt
的代码这里就只能提供链接了:
SetwindowPos怎么用
在我的项目中,由于是自定义窗口,所以我用了一个QPushButton
来作为最小化的按钮,响应鼠标点击事件来模拟最小化事件。代码如下:
# 模拟最小化的按钮
self.minimumBtn = QPushButton()
self.minimumBtn.setParent(self)
self.minimumBtn.clicked.connect(self.showMini)
# 按钮被鼠标点击后执行的方法
def showMini(self):
win32gui.SetWindowPos(self.hwnd, win32con.HWND_BOTTOM, self.x(), self.y(), self.width(), self.height(), win32con.SWP_SHOWWINDOW)
但是又如何来模拟窗口恢复呢?
我们知道,当点击系统任务栏上的窗口图标按钮时,会显示相应的窗口,即恢复窗口,而窗口的状态也会有所改变,由非激活状态Deactivate
转为激活状态Activate
或反之。
窗口非激活状态 / 停用状态 / 取消激活状态如下:
窗口激活状态如下:
由此可知,可根据窗口的激活状态来判断用户是否激活了窗口,也就是用户是否点击了窗口在任务栏上的图标按钮。而判断窗口状态改变的方法恰好是QWidget
的changeEvent
,于是我重写了changeEvent
方法。
def changeEvent(self, event):
if event.type() == QEvent.ActivationChange:
# 当窗口被激活,也就是当用户点击了窗口在任务栏上的图标按钮
if self.isActiveWindow():
# showNor是我定义的方法,和showMini对应,相当于显示窗口
self.showNor()
def showNor(self):
win32gui.SetWindowPos(self.hwnd, win32con.HWND_TOPMOST, self.x(), self.y(), self.width(), self.height(), win32con.SWP_SHOWWINDOW)
到这里可还没完呢,还要注意的是,实际上showMini
的逻辑并没有改变窗口的激活状态,按道理说,原本的窗口最小化后,应该会失去激活状态,处于停用状态,但showMini
只是使窗口置于最底层,并没有改变状态,因此我们还需要人为地、在代码上主动地使窗口变为非激活状态
。
然而让我百思不得其解的是,PyQt
或Qt
中,有激活窗口的activateWindow
方法,却没有deactivateWindow
或inactivateWindow
方法,这就需要别的骚操作来解决了。
经过灵光一闪,我终于想到了如何主动使窗口处于非激活状态的方法,写了下面这篇文章,这里不赘述了,详情看这里:
PyQt5 / Qt 人为地、主动地使窗口处于取消激活状态
请务必要看链接中的文章,因为我们在showMini
模拟最小化后,还要使窗口失去激活状态,于是showMini
的代码修改如下
def showMini(self):
win32gui.SetWindowPos(self.hwnd, win32con.HWND_BOTTOM, self.x(), self.y(), self.width(), self.height(), win32con.SWP_SHOWWINDOW)
# 激活一个看不见的窗口,相当于是主窗口失去激活状态
self.invisibleWidget.activateWindow()
到这里就大功告成了,当点击“最小化”按钮后,任务栏的预览窗口仍然在刷新。
#!/usr/bin/python
# -*- coding: UTF-8 -*-
import sys
import win32gui
import win32con
from PyQt5.QtWidgets import QWidget, QApplication, QPushButton
# 看不见的“工具人”窗口类,副窗口
class InvisibleWidget(QWidget):
def __init__(self, fatherWidget):
super().__init__()
# 传入的fatherWidget是主窗口,注意这里主窗口不是副窗口parent
self.fatherWidget = fatherWidget
self.setGeometry(-500, -500, 1, 1)
self.setFixedSize(self.width(), self.height())
# 设置窗口鼠标事件穿透,相当于不接收鼠标事件
self.setAttribute(Qt.WA_TransparentForMouseEvents, True)
# 设置窗口背景透明
self.setAttribute(Qt.WA_TranslucentBackground, True)
# 设置窗口为无边框、在任务栏
self.setWindowFlags(Qt.FramelessWindowHint | Qt.Tool)
# 设置透明度,0为全透明
self.setWindowOpacity(0)
# 主窗口类
class MainWin(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("Timer")
self.setGeometry(400, 300, 400, 500)
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
# 模拟最小化的按钮
self.minimizeBtn = QPushButton()
self.minimizeBtn.setParent(self)
self.minimizeBtn.setGeometry(20, 20, 40, 30)
# 当按下minimizeBtn按钮时,执行showMini方法
self.minimizeBtn.clicked.connect(self.showMini)
# 副窗口
self.invisibleWidget = InvisibleWidget(self)
# self.invisibleWidget.hide()
# 显示主窗口
self.show()
# 获得当前窗口,必须在窗口'show'之后才能获取到,第一个参数是窗口的类名,第二个是标题
self.hwnd = win32gui.FindWindow("Qt5QWindowIcon", "Timer")
def changeEvent(self, event):
if event.type() == QEvent.ActivationChange:
# 当窗口被激活,也就是当用户点击了窗口在任务栏上的图标按钮
if self.isActiveWindow():
# showNor是我定义的方法,和showMini对应,相当于显示窗口
self.showNor()
def showMini(self):
win32gui.SetWindowPos(self.hwnd, win32con.HWND_BOTTOM, self.x(), self.y(), self.width(), self.height(), win32con.SWP_SHOWWINDOW)
# 激活一个看不见的窗口,相当于是主窗口失去激活状态
self.invisibleWidget.activateWindow()
def showNor(self):
win32gui.SetWindowPos(self.hwnd, win32con.HWND_TOPMOST, self.x(), self.y(), self.width(), self.height(), win32con.SWP_SHOWWINDOW)
if __name__ == '__main__':
# 进入 PyQt5 的 UI 循环
app = QApplication(sys.argv)
# 创建窗口的实例
win = MainWin()
# 退出窗口的条件
sys.exit(app.exec_())