PySide2 - 带有自定义标题栏的无框架窗口 - 将其拖动到其他屏幕(Windows)时出现问题

问题描述 投票:0回答:1

有时,我尝试为我的 PyQt5/PySide2 应用程序创建自定义标题栏,但尚未弄清楚如何正确执行。隐藏原始 Windows 标题栏很容易,覆盖

mouseMoveEvents
以启用移动无框窗口也很容易。调整大小更具挑战性,但也是可能的。但对我来说最大的问题是我不知道如何维护 Windows Aero Snap 功能(通过按 Windows 键 + 箭头键或将其拖动到屏幕边框来将 Windows 固定到位的能力)。

今天,我在GitHub上找到了解决这个问题的代码示例。它工作得非常完美,除了当我将应用程序移动到另一个屏幕时...... 当我将其拖动到第二个屏幕时,我无法再移动它,也无法再按任何按钮。我只能调整它的大小。如果我将其大小调整回主屏幕,我可以再次拖动它。

这是代码:

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_())


有人知道如何解决这个问题吗?

python ctypes pywin32 pyside2
1个回答
1
投票

嘿,我是那个 github 帐户的所有者。我修复了它以及其他一些问题,您可以在这里查看。解决方案很简单。您需要将 unsigned int 的 x,y 变量转换为 int。仅此而已。

如果当窗口位于第二个监视器上时打印 x,y,您可以看到这些值在 65XXX 范围内。由于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()
© www.soinside.com 2019 - 2024. All rights reserved.