如何在pyqt中给无边框窗口添加DWM环绕阴影
前言
在之前的博客《如何在pyqt中通过调用SetWindowCompositionAttribute实现Win10亚克力效果》中,我们实现了窗口的亚克力效果,同时也用SetWindowCompositionAttribute()
给亚克力窗口加上了阴影。但是更多时候我们用不到亚克力效果,但又需要给无边框窗口加上阴影。一种方法是在当前窗口外嵌套一层窗口,然后用 QGraphicsDropShadowEffect
给里面的窗口加上阴影,还有一种就是重写 paintEvent()
来绘制阴影。下面来讨论一下使用 dwmapi
来给无边框窗口添加阴影的方法。效果如下 (硝子太美啦٩(๑>◡<๑)۶ ):
实现过程
接口函数
为了实现DWM
环绕阴影,需要调用dwmapi
中的两个函数:
HRESULT DwmSetWindowAttribute (HWND hwnd, DWORD dwAttribute, LPCVOID pvAttribute, DWORD cbAttribute)
,用来设置窗口的桌面窗口管理器(DWM)非客户端呈现属性的值,可以参见文档 DwmSetWindowAttribute函数;HRESULT DwmExtendFrameIntoClientArea (HWND hWnd, const MARGINS *pMarInset)
,用来将窗口框架扩展到工作区,参见文档DwmExtendFrameIntoClientArea函数 和 DWM模糊概述;
在调用这两个函数之前,我们需要先在WindowEffect
的构造函数中声明一下他们的函数原型
self.dwmapi = WinDLL("dwmapi")
self.DwmExtendFrameIntoClientArea = self.dwmapi.DwmExtendFrameIntoClientArea
self.DwmSetWindowAttribute = self.dwmapi.DwmSetWindowAttribute
self.DwmExtendFrameIntoClientArea.restype = LONG
self.DwmSetWindowAttribute.restype = LONG
self.DwmSetWindowAttribute.argtypes = [c_int, DWORD, LPCVOID, DWORD]
self.DwmExtendFrameIntoClientArea.argtypes = [c_int, POINTER(MARGINS)]
结构体和枚举类
从MSDN文档可以得知,传入 DwmExtendFrameIntoClientArea()
的第二个参数 pMarInset
是一个结构体 MARGIN
的指针,所以我们下面定义一下 MARGIN
,同时定义一些要用到的枚举类(其他关于亚克力效果的结构体和枚举类见《如何在pyqt中通过调用SetWindowCompositionAttribute实现Win10亚克力效果》):
# coding: utf-8
from ctypes import Structure, c_int
from enum import Enum
class DWMNCRENDERINGPOLICY(Enum):
DWMNCRP_USEWINDOWSTYLE = 0
DWMNCRP_DISABLED = 1
DWMNCRP_ENABLED = 2
DWMNCRP_LAS = 3
class DWMWINDOWATTRIBUTE(Enum):
DWMWA_NCRENDERING_ENABLED = 1
DWMWA_NCRENDERING_POLICY = 2
DWMWA_TRANSITIONS_FORCEDISABLED = 3
DWMWA_ALLOW_NCPAINT = 4
DWMWA_CAPTION_BUTTON_BOUNDS = 5
DWMWA_NONCLIENT_RTL_LAYOUT = 6
DWMWA_FORCE_ICONIC_REPRESENTATION = 7
DWMWA_FLIP3D_POLICY = 8
DWMWA_EXTENDED_FRAME_BOUNDS = 9
DWMWA_HAS_ICONIC_BITMAP = 10
DWMWA_DISALLOW_PEEK = 11
DWMWA_EXCLUDED_FROM_PEEK = 12
DWMWA_CLOAK = 13
DWMWA_CLOAKED = 14
DWMWA_FREEZE_REPRESENTATION = 25
DWMWA_LAST = 16
class MARGINS(Structure):
_fields_ = [
("cxLeftWidth", c_int),
("cxRightWidth", c_int),
("cyTopHeight", c_int),
("cyBottomHeight", c_int),
]
WindowEffect 类
准备工作完成,我们来看一下 WindowEffect
中拿来给无边框窗口添加环绕阴影的函数:
def addShadowEffect(self, hWnd):
""" 给窗口添加阴影
Parameter
----------
hWnd: int or `sip.voidptr`
窗口句柄
"""
hWnd = int(hWnd)
self.DwmSetWindowAttribute(
hWnd,
DWMWINDOWATTRIBUTE.DWMWA_NCRENDERING_POLICY.value,
byref(c_int(DWMNCRENDERINGPOLICY.DWMNCRP_ENABLED.value)),
4,
)
margins = MARGINS(-1, -1, -1, -1)
self.DwmExtendFrameIntoClientArea(hWnd, byref(margins))
下面给出整个 WindowEffect
类的代码,这里面包括了设置亚克力效果的方法、给窗口添加阴影的方法和移动窗口的方法:
# coding:utf-8
from ctypes import POINTER, c_bool, c_int, pointer, sizeof, WinDLL, byref
from ctypes.wintypes import DWORD, HWND, LONG, LPCVOID
from win32 import win32api, win32gui
from win32.lib import win32con
from .c_structures import (
ACCENT_POLICY,
ACCENT_STATE,
MARGINS,
DWMNCRENDERINGPOLICY,
DWMWINDOWATTRIBUTE,
WINDOWCOMPOSITIONATTRIB,
WINDOWCOMPOSITIONATTRIBDATA,
)
class WindowEffect:
""" 调用windows api实现窗口效果 """
def __init__(self):
# 调用api
self.user32 = WinDLL("user32")
self.dwmapi = WinDLL("dwmapi")
self.SetWindowCompositionAttribute = self.user32.SetWindowCompositionAttribute
self.DwmExtendFrameIntoClientArea = self.dwmapi.DwmExtendFrameIntoClientArea
self.DwmSetWindowAttribute = self.dwmapi.DwmSetWindowAttribute
self.SetWindowCompositionAttribute.restype = c_bool
self.DwmExtendFrameIntoClientArea.restype = LONG
self.DwmSetWindowAttribute.restype = LONG
self.SetWindowCompositionAttribute.argtypes = [
c_int,
POINTER(WINDOWCOMPOSITIONATTRIBDATA),
]
self.DwmSetWindowAttribute.argtypes = [c_int, DWORD, LPCVOID, DWORD]
self.DwmExtendFrameIntoClientArea.argtypes = [c_int, POINTER(MARGINS)]
# 初始化结构体
self.accentPolicy = ACCENT_POLICY()
self.winCompAttrData = WINDOWCOMPOSITIONATTRIBDATA()
self.winCompAttrData.Attribute = WINDOWCOMPOSITIONATTRIB.WCA_ACCENT_POLICY.value[0]
self.winCompAttrData.SizeOfData = sizeof(self.accentPolicy)
self.winCompAttrData.Data = pointer(self.accentPolicy)
def setAcrylicEffect(self, hWnd: int, gradientColor: str = "F2F2F230",
isEnableShadow: bool = True, animationId: int = 0):
""" 给窗口开启Win10的亚克力效果
Parameters
----------
hWnd: int
窗口句柄
gradientColor: str
十六进制亚克力混合色,对应rgba四个分量
isEnableShadow: bool
控制是否启用窗口阴影
animationId: int
控制磨砂动画
"""
# 亚克力混合色
gradientColor = (
gradientColor[6:]
+ gradientColor[4:6]
+ gradientColor[2:4]
+ gradientColor[:2]
)
gradientColor = DWORD(int(gradientColor, base=16))
# 磨砂动画
animationId = DWORD(animationId)
# 窗口阴影
accentFlags = DWORD(0x20 | 0x40 | 0x80 |
0x100) if isEnableShadow else DWORD(0)
self.accentPolicy.AccentState = ACCENT_STATE.ACCENT_ENABLE_ACRYLICBLURBEHIND.value[
0
]
self.accentPolicy.GradientColor = gradientColor
self.accentPolicy.AccentFlags = accentFlags
self.accentPolicy.AnimationId = animationId
# 开启亚克力
self.SetWindowCompositionAttribute(hWnd, pointer(self.winCompAttrData))
def setAeroEffect(self, hWnd: int):
""" 给窗口开启Aero效果
Parameter
----------
hWnd : 窗口句柄
"""
self.accentPolicy.AccentState = ACCENT_STATE.ACCENT_ENABLE_BLURBEHIND.value[0]
# 开启Aero
self.SetWindowCompositionAttribute(hWnd, pointer(self.winCompAttrData))
def moveWindow(self, hWnd: int):
""" 移动窗口
Parameter
----------
hWnd : 窗口句柄
"""
win32gui.ReleaseCapture()
win32api.SendMessage(
hWnd, win32con.WM_SYSCOMMAND, win32con.SC_MOVE + win32con.HTCAPTION, 0
)
def addShadowEffect(self, hWnd):
""" 给窗口添加阴影
Parameter
----------
hWnd: int or `sip.voidptr`
窗口句柄
"""
hWnd = int(hWnd)
self.DwmSetWindowAttribute(
hWnd,
DWMWINDOWATTRIBUTE.DWMWA_NCRENDERING_POLICY.value,
byref(c_int(DWMNCRENDERINGPOLICY.DWMNCRP_ENABLED.value)),
4,
)
margins = MARGINS(-1, -1, -1, -1)
self.DwmExtendFrameIntoClientArea(hWnd, byref(margins))
测试
# coding:utf-8
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication, QWidget
from my_window_effect import WindowEffect
class Demo(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.resize(500, 500)
self.windowEffect = WindowEffect()
# 取消窗口边框
self.setWindowFlags(Qt.FramelessWindowHint)
# 添加环绕阴影
self.windowEffect.addShadowEffect(self.winId())
def mousePressEvent(self, QMouseEvent):
self.windowEffect.moveWindow(self.winId())
if __name__ == "__main__":
app = QApplication(sys.argv)
demo = Demo()
demo.show()
sys.exit(app.exec_())
后记
关于如何给无边框窗口添加DWM环绕阴影的介绍到此结束,有帮助的话就点个赞吧 []~( ̄▽ ̄)~*。当然正如我在《如何在pyqt中在实现无边框窗体的同时保留Windows窗口动画效果(一)》所言,无边框窗口意味着窗口动画的消失,要解决这个问题参见《如何在pyqt中在实现无边框窗口的同时保留Windows窗口动画效果(二)》 和《如何在pyqt中自定义无边框窗口》。以上~~