前言:
上一次的 Bug 已修复:
上一篇文章中指出了 PyQt 5 / PySide 2 的窗口界面加载延迟问题:
PyQt5 / PySide 2 + Pywin32 自定义标题栏窗口 + 还原 Windows 原生窗口边框特效(2)-CSDN博客https://blog.csdn.net/2402_84665876/article/details/141501729?spm=1001.2014.3001.5501
但当把 PyQt 5 / PySide 2 改为 PyQt6 / PySide 6 时,该问题自动修复,这或许是 PyQt 5 / PySide 2 与 Win32 API 兼容性问题导致的加载延缓。
又出现了新的 Bug:
如前所说,Qt 官方 在 Qt 6 去掉了对 Win32 API 的支持:QtWin,这导致无法给窗口直接添加系统自带的阴影;且当窗口最大化时,窗口的底部和右侧仍可拖拽改变大小,这会导致系统自带的白色边框强制显示。
解决方法:
给 NativeEvent 的改变大小片段的每个条件后添加 “and not self.isMaximized()” 即可。
if lx and ty and not self.isMaximized():
return True, win32con.HTTOPLEFT
elif rx and by and not self.isMaximized():
return True, win32con.HTBOTTOMRIGHT
elif rx and ty and not self.isMaximized():
return True, win32con.HTTOPRIGHT
elif lx and by and not self.isMaximized():
return True, win32con.HTBOTTOMLEFT
elif ty and not self.isMaximized():
return True, win32con.HTTOP
elif by and not self.isMaximized():
return True, win32con.HTBOTTOM
elif lx and not self.isMaximized():
return True, win32con.HTLEFT
elif rx and not self.isMaximized():
return True, win32con.HTRIGHT
完整代码:
main.py
import sys
import win32api
import win32con
import win32gui
from ctypes import (Structure, POINTER, c_int, cast)
from ctypes.wintypes import (POINT, HWND, UINT, RECT, MSG)
from PySide6.QtCore import (Qt, QSize, QTimer)
from PySide6.QtGui import (QCursor, QCloseEvent, QIcon, QGuiApplication)
from PySide6.QtWidgets import (QApplication, QPushButton, QLabel, QMainWindow)
class MINMAXINFO(Structure):
_fields_ = [
("ptReserved", POINT),
("ptMaxSize", POINT),
("ptMaxPosition", POINT),
("ptMinTrackSize", POINT),
("ptMaxTrackSize", POINT),
]
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))
]
class Window(QMainWindow):
BORDER_WIDTH = 5
def __init__(self, *args, **kwargs):
super(Window, self).__init__(*args, **kwargs)
# 主屏幕的可用大小(去掉任务栏)
self._rect = QGuiApplication.primaryScreen().availableGeometry()
self.title_height = 30
self.setWindowFlags(Qt.WindowType.Window |
Qt.WindowType.FramelessWindowHint |
Qt.WindowType.WindowMinimizeButtonHint |
Qt.WindowType.WindowMaximizeButtonHint |
Qt.WindowType.WindowCloseButtonHint |
Qt.WindowType.WindowSystemMenuHint)
self.timer = QTimer(self)
self.timer.timeout.connect(self.maxornormal2)
self.timer.start(50)
self.setStyleSheet("Window{background:rgb(255,255,255);}")
self.setwin32border()
def setwin32border(self):
style = win32gui.GetWindowLong(int(self.winId()), win32con.GWL_STYLE)
win32gui.SetWindowLong(int(self.winId()), win32con.GWL_STYLE,
style
| win32con.WS_THICKFRAME
| win32con.WS_MINIMIZEBOX
| win32con.WS_MAXIMIZEBOX
| win32con.WS_CAPTION
| win32con.CS_DBLCLKS)
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 15px "微软雅黑";'
"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 - 10, self.title_height - 10))
self.iconimage.setFixedSize(self.title_height, self.title_height)
self.iconimage.setStyleSheet("QPushButton {"
"border:none;"
"background-color:rgba(0,0,0,0);}")
self.iconimage.move(0,0)
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_()
self.maxbutton.raise_()
self.minbutton.raise_()
except:
pass
try:
self.iconimage.raise_()
except:
pass
def resizeEvent(self, a0):
self.__resizeEvent__()
def __resizeEvent__(self):
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 and not self.isMaximized():
return True, win32con.HTTOPLEFT
elif rx and by and not self.isMaximized():
return True, win32con.HTBOTTOMRIGHT
elif rx and ty and not self.isMaximized():
return True, win32con.HTTOPRIGHT
elif lx and by and not self.isMaximized():
return True, win32con.HTBOTTOMLEFT
elif ty and not self.isMaximized():
return True, win32con.HTTOP
elif by and not self.isMaximized():
return True, win32con.HTBOTTOM
elif lx and not self.isMaximized():
return True, win32con.HTLEFT
elif rx and not self.isMaximized():
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
# 获取显示器信息
work_area = (self._rect.x(), self._rect.y(), self._rect.width() + 5, self._rect.height())
# 将lParam转换为MINMAXINFO指针
info = cast(msg.lParam, POINTER(MINMAXINFO)).contents
# 调整窗口大小
info.ptMaxSize.x = work_area[2]
info.ptMaxSize.y = work_area[3]
info.ptMaxTrackSize.x = info.ptMaxSize.x
info.ptMaxTrackSize.y = info.ptMaxSize.y
# 修改左上角坐标
info.ptMaxPosition.x = 0
info.ptMaxPosition.y = 0
return True, 1
elif msg.message == win32con.WM_SYSKEYDOWN:
if msg.wParam == win32con.VK_F4:
QApplication.sendEvent(self, QCloseEvent())
return False, 0
return QMainWindow.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.setWindowIcon(QIcon("icon.png"))
w.setMinimumWidth(450)
w.resize(800,450)
w.show()
sys.exit(app.exec())