前言
在之前的博客《如何在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中自定義無邊框窗口》。以上~~