PyQt5窗口最小化后,任务栏窗口预览(Windows速览)持续刷新(C++ Qt同理)

PyQt5 / Qt窗口最小化后,鼠标悬停,任务栏窗口预览(Windows速览)继续保持刷新状态

    • 问题描述
    • 解决方案
    • 完整代码

问题描述

最近在做一个PyQt5项目,其中有一个功能是窗口启动后开始计时,在计时的时候,要求能在Windows的任务栏上看到预览窗口也在持续刷新(Window速览功能),如下:

PyQt5窗口最小化后,任务栏窗口预览(Windows速览)持续刷新(C++ Qt同理)_第1张图片

可以看到,当鼠标悬停在系统任务栏的窗口图标上时,出现预览窗口。

然而当按下窗口最小化按钮的时候,该预览窗口只保留最小化之前的界面截图作为预览,而不会持续刷新,也就是说,当窗口处于最小化状态的时候,该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")

PyQt5窗口最小化后,任务栏窗口预览(Windows速览)持续刷新(C++ Qt同理)_第2张图片

spy++工具在下面这篇文章中有提供,也详细地阐述了win32guiFindWindow方法,可以看这里:

查找窗口的句柄方法

PyQt中,调用SetWindowPos()需要用到win32guiwin32con库,代码如下:

# 获得当前窗口,必须在窗口'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或反之。

窗口非激活状态 / 停用状态 / 取消激活状态如下:

点击任务栏上的窗口图标按钮,激活窗口

窗口激活状态如下:

窗口的激活状态

由此可知,可根据窗口的激活状态来判断用户是否激活了窗口,也就是用户是否点击了窗口在任务栏上的图标按钮。而判断窗口状态改变的方法恰好是QWidgetchangeEvent,于是我重写了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只是使窗口置于最底层,并没有改变状态,因此我们还需要人为地、在代码上主动地使窗口变为非激活状态

然而让我百思不得其解的是,PyQtQt中,有激活窗口的activateWindow方法,却没有deactivateWindowinactivateWindow方法,这就需要别的骚操作来解决了。

经过灵光一闪,我终于想到了如何主动使窗口处于非激活状态的方法,写了下面这篇文章,这里不赘述了,详情看这里:
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_())

你可能感兴趣的:(Python,PyQt,Qt)