如何在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中自定義無邊框窗口》。以上~~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM