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


前言

在之前的博客中,我们实现了窗口的亚克力效果,同时也用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 ,同时定义一些要用到的枚举类(其他关于亚克力效果的结构体和枚举类见):

# 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环绕阴影的介绍到此结束,有帮助的话就点个赞吧 []~( ̄▽ ̄)~*。当然正如我在所言,无边框窗口意味着窗口动画的消失,要解决这个问题参见《 和《》。以上~~