如何在pyqt中给无边框窗口添加DWM环绕阴影

前言

在之前的博客《如何在pyqt中通过调用SetWindowCompositionAttribute实现Win10亚克力效果》中,我们实现了窗口的亚克力效果,同时也用SetWindowCompositionAttribute() 给亚克力窗口加上了阴影。但是更多时候我们用不到亚克力效果,但又需要给无边框窗口加上阴影。一种方法是在当前窗口外再嵌套一层窗口,然后用 Qt 自带的 QGraphicsDropShadowEffect 给里面这层窗口加上阴影,还有一种就是重写 paintEvent()来绘制阴影。下面来讨论一下使用 dwmapi 来给无边框窗口添加阴影的方法。效果如下 (硝子太美啦٩(๑>◡<๑)۶ ):

如何在pyqt中给无边框窗口添加DWM环绕阴影_第1张图片

实现过程

  1. 为了实现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)]
    
  2. 从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),
        ]
    
  3. 准备工作完成,我们来看一下 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))
    
  4. 下面给出整个 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窗口动画效果》所言,无边框窗口意味着窗口动画的消失,之前用C++的 dll 解决了这个问题,以后有时间再试试 python 版本的吧。以上~~

你可能感兴趣的:(pyqt学习日记,pyqt5,python,windows,10)