首页 > 系统相关 >PyQt5 / PySide 2 + Pywin32 自定义标题栏窗口 + 还原 Windows 原生窗口边框特效

PyQt5 / PySide 2 + Pywin32 自定义标题栏窗口 + 还原 Windows 原生窗口边框特效

时间:2024-08-24 08:53:09浏览次数:11  
标签:窗口边框 自定义 PySide title self height maxbutton win32con import

Bug:

当窗口不处于顶层时,如果点击窗体试图将其置于顶层,窗体自带的白边框会突然显示,最长两秒。

完整性:

尚未添加窗口状态的过渡动画和淡入、淡出动画。

其他问题:

由于 Qt 官方在版本 6 去掉了 QtWin,目前暂未找到 PyQt 6 / PySide 6 的解决方案。

准备工作:

在同目录下放四张照片:
max.png
min.png
normal.png
close.png

完整代码:

main.py

import ctypes.wintypes
import sys
from ctypes import c_int,cast
from ctypes.wintypes import POINT,HWND,UINT,RECT,MSG
import win32api
import win32con
import win32gui
from PyQt5.QtCore import Qt, QSize, QTimer
from PyQt5.QtGui import QCursor, QCloseEvent, QFont, QIcon
from PyQt5.QtWidgets import QApplication, QPushButton, QWidget, QLabel, QMainWindow
from PyQt5.QtWinExtras import QtWin
from ctypes import Structure, POINTER


class MINMAXINFO(ctypes.Structure):
    _fields_ = [
        ("ptReserved", POINT),
        ("ptMaxSize", POINT),
        ("ptMaxPosition", POINT),
        ("ptMinTrackSize", POINT),
        ("ptMaxTrackSize", POINT),
    ]
class PWINDOWPOS(ctypes.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))
    ]



class Window(QMainWindow):
    BORDER_WIDTH = 3

    def __init__(self, *args, **kwargs):
        super(Window, self).__init__(*args, **kwargs)
        # 主屏幕的可用大小(去掉任务栏)
        self._rect = QApplication.instance().desktop().availableGeometry(self)
        self.title_height = 30
        self.setWindowFlags(Qt.Window |
                            Qt.FramelessWindowHint |
                            Qt.WindowMinimizeButtonHint |
                            Qt.WindowMaximizeButtonHint |
                            Qt.WindowCloseButtonHint |
                            Qt.WindowSystemMenuHint)
        self.timer = QTimer(self)
        self.timer.timeout.connect(self.maxornormal2)
        self.timer.start(50)
        self.setStyleSheet("Window{background:transparent}")
        # 增加薄边框
        style = win32gui.GetWindowLong(int(self.winId()), win32con.GWL_STYLE)
        win32gui.SetWindowLong(int(self.winId()), win32con.GWL_STYLE,
                               style | win32con.WS_THICKFRAME)
        if QtWin.isCompositionEnabled():
            # 加上 Aero 边框阴影
            QtWin.extendFrameIntoClientArea(self, -1, -1, -1, -1)
        else:
            QtWin.resetExtendedFrame(self)

    def setWindowTitle(self, title: str):
        super().setWindowTitle(title)
        self.titlebackground = QLabel(self)
        self.titlebackground.setStyleSheet("QLabel {"
                                           "background-color:rgba(30,30,30,1);"
                                           "}")
        self.titleBar = QLabel(f" {title}", self)
        self.titleBar.setStyleSheet("QLabel {"
                                    'font: normal normal 14px "微软雅黑";'
                                    "background-color:rgba(0,0,0,0);"
                                    "color:rgb(255,255,255)"
                                    "}")
        self.titlebackground.setGeometry(0,0,self.size().width(), self.title_height)
        self.titleBar.setGeometry(self.title_height, 0, self.size().width(), self.title_height)
        self.raiseEvent()
        self.setMinimumHeight(self.title_height)
        self.setthreebutton()

    def setWindowIcon(self, icon):
        super().setWindowIcon(icon)
        self.iconimage = QPushButton(self)
        self.iconimage.setIcon(icon)
        self.iconimage.setIconSize(QSize(self.title_height, self.title_height))
        self.iconimage.setFixedSize(self.title_height, self.title_height)
        self.raiseEvent()

    def setthreebutton(self):
        clqss = ("QPushButton {"
               "background-color:rgba(0,0,0,0);"
               "border:none;"
               "}"
               "QPushButton:hover {"
               "background-color:rgba(255,0,0,1);"
               "}"
               "QPushButton:pressed {"
               "background-color:rgba(255,100,100,1);"
               "}"
               )
        blqss = ("QPushButton {"
                 "background-color:rgba(0,0,0,0);"
                 "border:none;"
                 "}"
                 "QPushButton:hover {"
                 "background-color:rgba(60,60,60,1);"
                 "}"
                 "QPushButton:pressed {"
                 "background-color:rgba(100,100,100,1);"
                 "}"
                 )
        psize = QSize(self.title_height + 15, self.title_height)
        isize = QSize(self.title_height - 15, self.title_height - 15)
        self.exitbutton = QPushButton(self)
        self.exitbutton.setIcon(QIcon("close.png"))
        self.exitbutton.setIconSize(isize)
        self.exitbutton.resize(psize)
        self.exitbutton.setStyleSheet(clqss)
        self.exitbutton.clicked.connect(self.close)
        self.maxbutton = QPushButton(self)
        self.maxbutton.setIcon(QIcon("max.png"))
        self.maxbutton.setIconSize(isize)
        self.maxbutton.resize(psize)
        self.maxbutton.setStyleSheet(blqss)
        self.maxbutton.clicked.connect(self.maxornormal)
        self.minbutton = QPushButton(self)
        self.minbutton.setIcon(QIcon("min.png"))
        self.minbutton.setIconSize(isize)
        self.minbutton.resize(psize)
        self.minbutton.setStyleSheet(blqss)
        self.minbutton.clicked.connect(self.showMinimized)

    def showNormal(self):
        super().showNormal()
        self.maxbutton.setIcon(QIcon("max.png"))
        QCursor.setPos(QCursor.pos().x(), QCursor.pos().y() - 1)

    def showMaximized(self):
        super().showMaximized()
        self.maxbutton.setIcon(QIcon("normal.png"))
        QCursor.setPos(QCursor.pos().x(), QCursor.pos().y() - 1)

    def maxornormal(self):
        if self.isMaximized():
            self.showNormal()
        else:
            self.showMaximized()

    def maxornormal2(self):
        if self.isMaximized():
            self.maxbutton.setIcon(QIcon("normal.png"))
        else:
            self.maxbutton.setIcon(QIcon("max.png"))

    def raiseEvent(self):
        try:
            self.titlebackground.raise_()
            self.titleBar.raise_()
            self.exitbutton.raise_()
        except:
            pass
        try:
            self.iconimage.raise_()
        except:
            pass

    def resizeEvent(self, a0):
        super().resizeEvent(a0)
        try:
            self.titlebackground.setGeometry(0, 0, self.size().width(), self.title_height)
            self.titleBar.setGeometry(self.title_height, 0, self.size().width(), self.title_height)
            self.exitbutton.move(self.size().width() - self.exitbutton.size().width(), 0)
            self.maxbutton.move(self.exitbutton.pos().x() - self.maxbutton.size().width(), 0)
            self.minbutton.move(self.maxbutton.pos().x() - self.minbutton.size().width(), 0)
        except:
            pass

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

    def nativeEvent(self, eventType, message):
        """ 处理windows消息 """
        msg = MSG.from_address(message.__int__())
        pos = QCursor.pos()
        x = pos.x() - self.frameGeometry().x()
        y = pos.y() - self.frameGeometry().y()
        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 self.pos().y() <= QCursor.pos().y() <= self.pos().y() + self.title_height and self.childAt(x,y) == self.titleBar:
                return True, win32con.HTCAPTION
        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] + 10
                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
        elif msg.message == win32con.WM_SYSKEYDOWN:
            if msg.wParam == win32con.VK_F4:
                QApplication.sendEvent(self, QCloseEvent())
                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]


if __name__ == '__main__':
    app = QApplication(sys.argv)
    w = Window()
    w.setWindowTitle("Win32PyQtFramelessWindow 示例")
    w.resize(800,450)
    w.show()
    sys.exit(app.exec_())

标签:窗口边框,自定义,PySide,title,self,height,maxbutton,win32con,import
From: https://blog.csdn.net/2402_84665876/article/details/141487635

相关文章

  • 注册一种自定义文件类型
     网页端代码<ahref="sppcexe:PI;242700623010">PI配置</a> 类调用    new注册文件类型();=====================================================================操作类:usingMicrosoft.Win32;usingSystem;usingSystem.Diagnostics;publicclass注册文......
  • 应用程自定义协议与序列化反序列化
        本篇将主要介绍在应用层中自定义的协议,自定义协议的同时还需要将我们的数据继续序列化和反序列化,所以本篇的重点为序列化、反序列化的原因以及如何自定义协议,接着探讨了关于为什么tcp协议可以支持全双工协议。还根据用户自定义协议结合tcpsocket编写了一份三......
  • 【pytest】 在启动任务时将自定义参数传入代码中
    1.使用 pytest_addoption 钩子函数你可以在 conftest.py 文件中使用 pytest_addoption 钩子函数来定义自定义命令行参数。然后,你可以在你的测试文件中通过 request fixture来访问这些参数。conftest.py#contentofconftest.pyimportpytestdefpytest_ad......
  • Flutter 自定义日期范围选择组件,使用更加灵活,满足UI设计需要
    一、实现的效果图二、虽然Flutter也为我们提供了日期范围选择组件showDateRangePicker,但是毕竟系统的UI不符合我们的设计风格,所以被迫只能自己实现一个了系统样式三、日历整体实现逻辑其实也很简单,如下:首先获取每个月份具体有多少天int_getMonthDays(DateTimetim......
  • 在 Monaco Editor 中自定义右键菜单并支持多级菜单
    在MonacoEditor中自定义右键菜单能够提供更灵活的功能选项。以下是如何在MonacoEditor中实现自定义右键菜单,并支持多级菜单的步骤及关键代码示例。1.初始化MonacoEditor实例首先,需要初始化MonacoEditor实例,并设置基本的编辑器配置。constinitEditor=()=......
  • 自定义安装Nginx
    nginx下载地址:https://nginx.org/download/1.下载wgethttps://nginx.org/download/nginx-1.18.0.tar.gz2.安装依赖yum-yinstallgccgcc-c++makeautomakeautoconfpcrepcre-develzlibzlib-developensslopenssl-devellibtool3.解压tar-vxfnginx-1.18.0.tar.......
  • 自定义安装Mysql版本
    自定义安装Mysql版本mysql下载地址:https://downloads.mysql.com/archives/community/1.下载wgethttps://downloads.mysql.com/archives/get/p/23/file/mysql-5.7.28-1.el7.x86_64.rpm-bundle.tar--2024-08-2010:15:39--https://downloads.mysql.com/archives/get/p/23/......
  • 关于在得帆云数据中台如何自定义函数
    UDF使用示例场景说明:使用udf编写一个函数Unit_Conversion(value)。在函数中根据value的值进行单位转化,并进行类型转化。1、导入依赖在pom.xml中将如下依赖进行导入。<dependency><groupId>org.apache.hive</groupId><artifactId>hive-exec<......
  • WPF 模拟UWP原生窗口样式——亚克力|云母材质、自定义标题栏样式、原生DWM动画 (附我封
    先看一下最终效果,左图为使用亚克力材质并添加组合颜色的效果;右图为MicaAlt材质的效果。两者都自定义了标题栏并且最大限度地保留了DWM提供的原生窗口效果(最大化最小化、关闭出现的动画、窗口阴影、拖拽布局器等)。接下来把各部分的实现一个个拆开来讲讲。一、使用窗口材质特......
  • 探索HarmonyOS中的列表组件及其自定义特性
    在现代移动应用中,List组件是数据列表的关键元素。HarmonyOS中的List组件不仅具备传统的列表功能,还提供了丰富的自定义选项,允许开发者根据需求灵活调整列表的行为和外观展示。本文将探讨HarmonyOS中列举组件的自定义特性,包括自定义项布局、动态加载数据、多列布局、拖拽排序......