正在加载中...

如何在pyqt中在实现无边框窗口的同时保留Windows窗口动画效果(二)

前言

《如何在pyqt中在实现无边框窗口的同时保留Windows窗口动画效果(一)》中,我们通过调用 C++ 的 dll 实现了带窗口动画的无边框窗口并解决了最大化时的窗口大小问题。但是这个方法需要电脑上有装 MSVC,所以下面使用 ~ctypes.windllwin32 来重新实现上述无边框窗口效果。如何移动无边框窗口和还原无边框窗口的窗口动画,如何给无边框窗口加 DWM 环绕阴影,在前两篇博客《如何在pyqt中在实现无边框窗口的同时保留Windows窗口动画效果(一)》《如何在pyqt中给无边框窗口添加DWM环绕阴影》 做了很详细的解释,所以这里不再讨论,下面只讨论无边框窗口最大化时的窗口大小问题。先来看下效果(硝子太可爱啦٩(๑>◡<๑)۶ ):

无边框窗口

具体过程

nativeEvent 消息处理

正如我在《如何在pyqt中在实现无边框窗口的同时保留Windows窗口动画效果(一)》 中所言:

如果还原窗口动画,就会导致窗口最大化时尺寸超过正确的显示器尺寸,甚至最大化之后又会有新的标题栏跑出来,要解决这个问题必须在 nativeEvent 中处理两个消息,一个是WM_NCCALCSIZE,另一个则是 WM_GETMINMAXINFO

WM_GETMINMAXINFO 消息的处理在那篇博客中已经很好的解决了,并且没有依赖于 dll,所以下面我们只要处理好 WM_NCCALCSIZE 消息就行了,下面是在 nativeEvent() 中处理这个消息的代码:

def nativeEvent(self, eventType, message):
    msg = MSG.from_address(message.__int__())
    # 这里省略了对其他消息的处理
    if msg.message == win32con.WM_NCCALCSIZE:
        if self.isWindowMaximized(msg.hWnd):
            self.monitorNCCALCSIZE(msg)
        return True, 0
    return QWidget.nativeEvent(self, eventType, message)

def monitorNCCALCSIZE(self, msg: MSG):
    """ 处理 WM_NCCALCSIZE 消息 """
    monitor = win32api.MonitorFromWindow(msg.hWnd)
    # 如果没有保存显示器信息就直接返回,否则接着调整窗口大小
    if monitor is None and not self.monitor_info:
        return
    elif monitor is not None:
        self.monitor_info = win32api.GetMonitorInfo(monitor)
    # 调整窗口大小
    params = cast(msg.lParam, POINTER(NCCALCSIZE_PARAMS)).contents
    params.rgrc[0].left = self.monitor_info['Work'][0]
    params.rgrc[0].top = self.monitor_info['Work'][1]
    params.rgrc[0].right = self.monitor_info['Work'][2]
    params.rgrc[0].bottom = self.monitor_info['Work'][3]

结构体

上述代码用到的一些结构体的定义如下:

class PWINDOWPOS(Structure):
    _fields_ = [
        ('hWnd',            HWND),
        ('hwndInsertAfter', HWND),
        ('x',               c_int),
        ('y',               c_int),
        ('cx',              c_int),
        ('cy',              c_int),
        ('flags',           UINT)
    ]


class NCCALCSIZE_PARAMS(Structure):
    _fields_ = [
        ('rgrc', RECT*3),
        ('lppos', POINTER(PWINDOWPOS))
    ]

无边框窗口

下面是整个无边框窗口的代码,其他代码我放在了github 中,可以自取:

# coding:utf-8
from ctypes import POINTER, cast
from ctypes.wintypes import MSG

from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QWidget
from win32 import win32api, win32gui
from win32.lib import win32con

from titlebar import TitleBar
from windoweffect import WindowEffect, MINMAXINFO, NCCALCSIZE_PARAMS


class FramelessWindow(QWidget):

    BORDER_WIDTH = 5

    def __init__(self, parent=None):
        super().__init__(parent)
        self.monitor_info = None
        self.titleBar = TitleBar(self)
        self.windowEffect = WindowEffect()
        # 取消边框
        self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowSystemMenuHint |
                            Qt.WindowMinimizeButtonHint | Qt.WindowMaximizeButtonHint)
        # 添加阴影和窗口动画
        self.windowEffect.addShadowEffect(self.winId())
        self.windowEffect.addWindowAnimation(self.winId())
        self.resize(500, 500)
        self.titleBar.raise_()

    def isWindowMaximized(self, hWnd) -> bool:
        """ 判断窗口是否最大化 """
        # 返回指定窗口的显示状态以及被恢复的、最大化的和最小化的窗口位置,返回值为元组
        windowPlacement = win32gui.GetWindowPlacement(hWnd)
        if not windowPlacement:
            return False
        return windowPlacement[1] == win32con.SW_MAXIMIZE

    def resizeEvent(self, e):
        self.titleBar.resize(self.width(), 40)

    def nativeEvent(self, eventType, message):
        """ 处理windows消息 """
        msg = MSG.from_address(message.__int__())
        if msg.message == win32con.WM_NCHITTEST:
            xPos = win32api.LOWORD(msg.lParam) - self.frameGeometry().x()
            yPos = win32api.HIWORD(msg.lParam) - self.frameGeometry().y()
            w, h = self.width(), self.height()
            lx = xPos < self.BORDER_WIDTH
            rx = xPos + 9 > w - self.BORDER_WIDTH
            ty = yPos < self.BORDER_WIDTH
            by = yPos > h - self.BORDER_WIDTH
            if lx and ty:
                return True, win32con.HTTOPLEFT
            elif rx and by:
                return True, win32con.HTBOTTOMRIGHT
            elif rx and ty:
                return True, win32con.HTTOPRIGHT
            elif lx and by:
                return True, win32con.HTBOTTOMLEFT
            elif ty:
                return True, win32con.HTTOP
            elif by:
                return True, win32con.HTBOTTOM
            elif lx:
                return True, win32con.HTLEFT
            elif rx:
                return True, win32con.HTRIGHT
        elif msg.message == win32con.WM_NCCALCSIZE:
            if self.isWindowMaximized(msg.hWnd):
                self.monitorNCCALCSIZE(msg)
            return True, 0
        elif msg.message == win32con.WM_GETMINMAXINFO:
            if self.isWindowMaximized(msg.hWnd):
                window_rect = win32gui.GetWindowRect(msg.hWnd)
                if not window_rect:
                    return False, 0
                # 获取显示器句柄
                monitor = win32api.MonitorFromRect(window_rect)
                if not monitor:
                    return False, 0
                # 获取显示器信息
                monitor_info = win32api.GetMonitorInfo(monitor)
                monitor_rect = monitor_info['Monitor']
                work_area = monitor_info['Work']
                # 将lParam转换为MINMAXINFO指针
                info = cast(msg.lParam, POINTER(MINMAXINFO)).contents
                # 调整窗口大小
                info.ptMaxSize.x = work_area[2] - work_area[0]
                info.ptMaxSize.y = work_area[3] - work_area[1]
                info.ptMaxTrackSize.x = info.ptMaxSize.x
                info.ptMaxTrackSize.y = info.ptMaxSize.y
                # 修改左上角坐标
                info.ptMaxPosition.x = abs(window_rect[0] - monitor_rect[0])
                info.ptMaxPosition.y = abs(window_rect[1] - monitor_rect[1])
                return True, 1
        return QWidget.nativeEvent(self, eventType, message)

    def resizeEvent(self, e):
        """ 改变标题栏大小 """
        super().resizeEvent(e)
        self.titleBar.resize(self.width(), 40)
        # 更新最大化按钮图标
        if self.isWindowMaximized(int(self.winId())):
            self.titleBar.maxBt.setMaxState(True)

    def monitorNCCALCSIZE(self, msg: MSG):
        """ 调整窗口大小 """
        monitor = win32api.MonitorFromWindow(msg.hWnd)
        # 如果没有保存显示器信息就直接返回,否则接着调整窗口大小
        if monitor is None and not self.monitor_info:
            return
        elif monitor is not None:
            self.monitor_info = win32api.GetMonitorInfo(monitor)
        # 调整窗口大小
        params = cast(msg.lParam, POINTER(NCCALCSIZE_PARAMS)).contents
        params.rgrc[0].left = self.monitor_info['Work'][0]
        params.rgrc[0].top = self.monitor_info['Work'][1]
        params.rgrc[0].right = self.monitor_info['Work'][2]
        params.rgrc[0].bottom = self.monitor_info['Work'][3]

后记

这样无边框窗口的解决方案就介绍完毕了,算是对自己所学知识的一点总结,完整解决方案参见《如何在pyqt中自定义无边框窗口》。如果博客对你有帮助的话就点个赞吧,以上(~ ̄▽ ̄)~

posted @ 2021-04-11 16:06  之一Yo  阅读(1231)  评论(2编辑  收藏  举报