简体   繁体   English

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

[英]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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM