[英]PySide2 - Frameless window with custom titlebar - issues when dragging it to other screen (Windows)
From time to time, I tried to create a custom title bar for my PyQt5/PySide2 application but haven't figured out how to do it properly.有时,我尝试为我的 PyQt5/PySide2 应用程序创建一个自定义标题栏,但还没有弄清楚如何正确地做到这一点。 Hiding the original Windows title bar is easy, and so is overriding mouseMoveEvents
to enable moving the frameless window.隐藏原来的 Windows 标题栏很容易,重写mouseMoveEvents
以启用无框 window 的移动也很容易。 Resizing was a bit more challenging, but also possible.调整大小更具挑战性,但也是可能的。 But the biggest problem for me was that I couldn't figure out how to maintain the Windows Aero Snap functionality (the ability to snap Windows in place by pressing the Windows key + an arrow key, or by dragging it to the screen borders).但对我来说最大的问题是我不知道如何维护 Windows Aero Snap 功能(通过按 Windows 键 + 箭头键将 Windows 卡入到位的能力),或将其拖动到屏幕上。
Today, I found a code example on GitHub that solved this problem.今天在GitHub上找到了一个解决这个问题的代码示例。 It's working just perfectly, except when I move my application to another screen... The moment I'm dragging it to my second screen, I can't move it anymore and I can't press any button anymore.它工作得非常完美,除非我将我的应用程序移动到另一个屏幕......当我将它拖到我的第二个屏幕时,我不能再移动它并且我不能再按任何按钮了。 I'm just able to resize it.我只能调整它的大小。 If I resize it back to the main screen, I'm able to drag it again.如果我将其调整回主屏幕,我可以再次拖动它。
Here's the code:这是代码:
import sys
import ctypes
from ctypes import wintypes
import win32api
import win32con
import win32gui
from PySide2.QtCore import Qt, QRect
from PySide2.QtGui import QColor, QWindow, QScreen, QCursor
from PySide2.QtWidgets import QWidget, QPushButton, QApplication, \
QVBoxLayout, QSizePolicy, QHBoxLayout
from PySide2.QtWinExtras import QtWin
class MINMAXINFO(ctypes.Structure):
_fields_ = [
("ptReserved", wintypes.POINT),
("ptMaxSize", wintypes.POINT),
("ptMaxPosition", wintypes.POINT),
("ptMinTrackSize", wintypes.POINT),
("ptMaxTrackSize", wintypes.POINT),
]
class TitleBar(QWidget):
def __init__(self):
super().__init__()
self._layout = QHBoxLayout()
# set size
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.setMinimumHeight(50)
self.button = QPushButton("EXIT", clicked=app.exit)
self.button.setStyleSheet("""
QPushButton{
border: none;
outline: none;
background-color: rgb(220,0,0);
color: white;
padding: 6px;
width: 80px;
font: 16px consolas;
}
QPushButton:hover{
background-color: rgb(240,0,0);
}
""")
# set background color
self.setAutoFillBackground(True)
p = self.palette()
p.setColor(self.backgroundRole(), QColor("#212121"))
self.setPalette(p)
self._layout.addStretch()
self._layout.addWidget(self.button)
self.setLayout(self._layout)
class Window(QWidget):
BorderWidth = 5
def __init__(self):
super().__init__()
# get the available resolutions without taskbar
self._rect = QApplication.instance().desktop().availableGeometry(self)
self.resize(800, 600)
self.setWindowFlags(Qt.Window
| Qt.FramelessWindowHint
| Qt.WindowSystemMenuHint
| Qt.WindowMinimizeButtonHint
| Qt.WindowMaximizeButtonHint
| Qt.WindowCloseButtonHint)
self.current_screen = None
# Create a thin frame
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 Shadow
QtWin.extendFrameIntoClientArea(self, -1, -1, -1, -1)
pass
else:
QtWin.resetExtendedFrame(self)
# Window Widgets
self._layout = QVBoxLayout()
self._layout.setContentsMargins(0, 0, 0, 0)
self._layout.setSpacing(0)
self.titleBar = TitleBar()
self.titleBar.setObjectName("titleBar")
# main widget is here
self.mainWidget = QWidget()
self.mainWidgetLayout = QVBoxLayout()
self.mainWidgetLayout.setContentsMargins(0, 0, 0, 0)
# content
self.test_button = QPushButton('Test')
self.test_button.clicked.connect(self.on_test_button_clicked)
self.mainWidgetLayout.addWidget(self.test_button)
self.mainWidget.setLayout(self.mainWidgetLayout)
self.mainWidget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
# set background color
self.mainWidget.setAutoFillBackground(True)
p = self.mainWidget.palette()
p.setColor(self.mainWidget.backgroundRole(), QColor("#272727"))
self.mainWidget.setPalette(p)
self._layout.addWidget(self.titleBar)
self._layout.addWidget(self.mainWidget)
self.setLayout(self._layout)
self.show()
def on_test_button_clicked(self):
self.updateGeometry()
def nativeEvent(self, eventType, message):
retval, result = super().nativeEvent(eventType, message)
# if you use Windows OS
if eventType == "windows_generic_MSG":
msg = ctypes.wintypes.MSG.from_address(message.__int__())
# Get the coordinates when the mouse moves.
x = win32api.LOWORD(ctypes.c_long(msg.lParam).value) - self.frameGeometry().x()
y = win32api.HIWORD(ctypes.c_long(msg.lParam).value) - self.frameGeometry().y()
# Determine whether there are other controls(i.e. widgets etc.) at the mouse position.
if self.childAt(x, y) is not None and self.childAt(x, y) is not self.findChild(QWidget, "titleBar"):
# passing
if self.width() - 5 > x > 5 and y < self.height() - 5:
return retval, result
if msg.message == win32con.WM_NCCALCSIZE:
# Remove system title
return True, 0
if msg.message == win32con.WM_GETMINMAXINFO:
# This message is triggered when the window position or size changes.
info = ctypes.cast(
msg.lParam, ctypes.POINTER(MINMAXINFO)).contents
# Modify the maximized window size to the available size of the main screen.
info.ptMaxSize.x = self._rect.width()
info.ptMaxSize.y = self._rect.height()
# Modify the x and y coordinates of the placement point to (0,0).
info.ptMaxPosition.x, info.ptMaxPosition.y = 0, 0
if msg.message == win32con.WM_NCHITTEST:
w, h = self.width(), self.height()
lx = x < self.BorderWidth
rx = x > w - self.BorderWidth
ty = y < self.BorderWidth
by = y > h - self.BorderWidth
if lx and ty:
return True, win32con.HTTOPLEFT
if rx and by:
return True, win32con.HTBOTTOMRIGHT
if rx and ty:
return True, win32con.HTTOPRIGHT
if lx and by:
return True, win32con.HTBOTTOMLEFT
if ty:
return True, win32con.HTTOP
if by:
return True, win32con.HTBOTTOM
if lx:
return True, win32con.HTLEFT
if rx:
return True, win32con.HTRIGHT
# Title
return True, win32con.HTCAPTION
return retval, result
def moveEvent(self, event):
if not self.current_screen:
print('Initial Screen')
self.current_screen = self.screen()
elif self.current_screen != self.screen():
print('Changed Screen')
self.current_screen = self.screen()
self.updateGeometry()
win32gui.SetWindowPos(int(self.winId()), win32con.NULL, 0, 0, 0, 0,
win32con.SWP_NOMOVE | win32con.SWP_NOSIZE | win32con.SWP_NOZORDER |
win32con.SWP_NOOWNERZORDER | win32con.SWP_FRAMECHANGED | win32con.SWP_NOACTIVATE)
event.accept()
if __name__ == '__main__':
app = QApplication(sys.argv)
w = Window()
sys.exit(app.exec_())
Does someone have an idea of how to fix this issue?有人知道如何解决这个问题吗?
Hey I am the owner of that github account.嘿,我是那个 github 帐户的所有者。 I fixed it along some other issues, you can check it out here .我修复了其他一些问题,你可以在这里查看。 The solution is simple.解决方案很简单。 you need to convert the x,y variables which are unsigned int to int.您需要将无符号 int 的 x,y 变量转换为 int。 That's all.就这样。
if you print x,y when the window is on the second monitor, you can see the values are in the 65XXX range.如果您在 window 在第二台显示器上时打印 x,y,您可以看到值在 65XXX 范围内。 since python does not have built-in unsigned int type, you need to convert them manually, like this:由于 python 没有内置的 unsigned int 类型,您需要手动转换它们,如下所示:
x = win32api.LOWORD(ctypes.c_long(msg.lParam).value)
if x & 32768: x = x | -65536
y = win32api.HIWORD(ctypes.c_long(msg.lParam).value)
if y & 32768: y = y | -65536
x = x - self.frameGeometry().x()
y = y - self.frameGeometry().y()
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.