Commit 1a1a6ecd authored by dmMaze's avatar dmMaze
Browse files

frameless window refactory for windows

parent 2c9884ac
Loading
Loading
Loading
Loading
+3 −4
Original line number Diff line number Diff line
# To install pytorch cuda (gpu) version, please look https://pytorch.org/

PyQt6-Qt6>=6.6.2,<6.7.0 ; python_version > "3.8"
PyQt6>=6.6.1,<6.7.0 ; python_version > "3.8"
PyQt6-Qt6>=6.6.2 ; python_version > "3.8"
PyQt6>=6.6.1 ; python_version > "3.8"
PyQt5-Qt5>=5.15.2 ; python_version <= "3.8"
PyQt5>=5.15.10 ; python_version <= "3.8"
numpy<2
urllib3; sys_platform == 'win32' # https://github.com/psf/requests/issues/5740
urllib3; sys_platform == 'darwin' # fix urllib3.package.six.move module not found error
urllib3
jaconv
torch
torchvision
+2 −0
Original line number Diff line number Diff line
@@ -5,8 +5,10 @@ from utils import shared
if not shared.FLAG_QT6:

    from .fw_qt5.utils import startSystemMove
    from .fw_qt5 import utils as framelss_utils
    from .fw_qt5 import FramelessWindow

else:
    from .fw_qt6.utils import startSystemMove
    from .fw_qt6 import utils as framelss_utils
    from .fw_qt6 import FramelessWindow
 No newline at end of file
+1 −1
Original line number Diff line number Diff line
@@ -2,7 +2,7 @@
import sys

if sys.platform == "win32":
    from .win32_utils import WindowsMoveResize as MoveResize
    from ...win32_utils import WindowsMoveResize as MoveResize
elif sys.platform == "darwin":
    from .mac_utils import MacMoveResize as MoveResize
else:
+0 −310
Original line number Diff line number Diff line
# coding:utf-8
from ctypes import Structure, byref, sizeof, windll, c_int
from ctypes.wintypes import DWORD, HWND, LPARAM, RECT, UINT
from platform import platform
import sys

import win32api
import win32con
import win32gui
import win32print
from PyQt5.QtCore import QOperatingSystemVersion
from PyQt5.QtGui import QGuiApplication
from win32comext.shell import shellcon


def isMaximized(hWnd):
    """ Determine whether the window is maximized

    Parameters
    ----------
    hWnd: int or `sip.voidptr`
        window handle
    """
    windowPlacement = win32gui.GetWindowPlacement(hWnd)
    if not windowPlacement:
        return False

    return windowPlacement[1] == win32con.SW_MAXIMIZE


def isFullScreen(hWnd):
    """ Determine whether the window is full screen

    Parameters
    ----------
    hWnd: int or `sip.voidptr`
        window handle
    """
    if not hWnd:
        return False

    hWnd = int(hWnd)
    winRect = win32gui.GetWindowRect(hWnd)
    if not winRect:
        return False

    monitorInfo = getMonitorInfo(hWnd, win32con.MONITOR_DEFAULTTOPRIMARY)
    if not monitorInfo:
        return False

    monitorRect = monitorInfo["Monitor"]
    return all(i == j for i, j in zip(winRect, monitorRect))


def isCompositionEnabled():
    """ detect if dwm composition is enabled """
    bResult = c_int(0)
    windll.dwmapi.DwmIsCompositionEnabled(byref(bResult))
    return bool(bResult.value)


def getMonitorInfo(hWnd, dwFlags):
    """ get monitor info, return `None` if failed

    Parameters
    ----------
    hWnd: int or `sip.voidptr`
        window handle

    dwFlags: int
        Determines the return value if the window does not intersect any display monitor
    """
    monitor = win32api.MonitorFromWindow(hWnd, dwFlags)
    if not monitor:
        return

    return win32api.GetMonitorInfo(monitor)


def getResizeBorderThickness(hWnd, horizontal=True):
    """ get resize border thickness of widget

    Parameters
    ----------
    hWnd: int or `sip.voidptr`
        window handle

    dpiScale: bool
        whether to use dpi scale
    """
    window = findWindow(hWnd)
    if not window:
        return 0

    frame = win32con.SM_CXSIZEFRAME if horizontal else win32con.SM_CYSIZEFRAME
    result = getSystemMetrics(hWnd, frame, horizontal) + getSystemMetrics(hWnd, 92, horizontal)

    if result > 0:
        return result

    thickness = 8 if isCompositionEnabled() else 4
    return round(thickness*window.devicePixelRatio())


def getSystemMetrics(hWnd, index, horizontal):
    """ get system metrics """
    if not hasattr(windll.user32, 'GetSystemMetricsForDpi'):
        return win32api.GetSystemMetrics(index)

    dpi = getDpiForWindow(hWnd, horizontal)
    return windll.user32.GetSystemMetricsForDpi(index, dpi)


def getDpiForWindow(hWnd, horizontal=True):
    """ get dpi for window

    Parameters
    ----------
    hWnd: int or `sip.voidptr`
        window handle

    dpiScale: bool
        whether to use dpi scale
    """
    if hasattr(windll.user32, 'GetDpiForWindow'):
        return windll.user32.GetDpiForWindow(hWnd)

    hdc = win32gui.GetDC(hWnd)
    if not hdc:
        return 96

    dpiX = win32print.GetDeviceCaps(hdc, win32con.LOGPIXELSX)
    dpiY = win32print.GetDeviceCaps(hdc, win32con.LOGPIXELSY)
    win32gui.ReleaseDC(hWnd, hdc)
    if dpiX > 0 and horizontal:
        return dpiX
    elif dpiY > 0 and not horizontal:
        return dpiY

    return 96


def findWindow(hWnd):
    """ find window by hWnd, return `None` if not found

    Parameters
    ----------
    hWnd: int or `sip.voidptr`
        window handle
    """
    if not hWnd:
        return

    windows = QGuiApplication.topLevelWindows()
    if not windows:
        return

    hWnd = int(hWnd)
    for window in windows:
        if window and int(window.winId()) == hWnd:
            return window


def isGreaterEqualVersion(version):
    """ determine if the windows version ≥ the specifics version

    Parameters
    ----------
    version: QOperatingSystemVersion
        windows version
    """
    return QOperatingSystemVersion.current() >= version


def isGreaterEqualWin8_1():
    """ determine if the windows version ≥ Win8.1 """
    return isGreaterEqualVersion(QOperatingSystemVersion.Windows8_1)


def isGreaterEqualWin10():
    """ determine if the windows version ≥ Win10 """
    return isGreaterEqualVersion(QOperatingSystemVersion.Windows10)


def isGreaterEqualWin11():
    """ determine if the windows version ≥ Win10 """
    return isGreaterEqualVersion(QOperatingSystemVersion.Windows10) and sys.getwindowsversion().build >= 22000


def isWin7():
    """ determine if the windows version is Win7 """
    return "Windows-7" in platform()


class APPBARDATA(Structure):
    _fields_ = [
        ('cbSize',            DWORD),
        ('hWnd',              HWND),
        ('uCallbackMessage',  UINT),
        ('uEdge',             UINT),
        ('rc',                RECT),
        ('lParam',            LPARAM),
    ]


class Taskbar:

    LEFT = 0
    TOP = 1
    RIGHT = 2
    BOTTOM = 3
    NO_POSITION = 4

    AUTO_HIDE_THICKNESS = 2

    @staticmethod
    def isAutoHide():
        """ detect whether the taskbar is hidden automatically """
        appbarData = APPBARDATA(sizeof(APPBARDATA), 0,
                                0, 0, RECT(0, 0, 0, 0), 0)
        taskbarState = windll.shell32.SHAppBarMessage(
            shellcon.ABM_GETSTATE, byref(appbarData))

        return taskbarState == shellcon.ABS_AUTOHIDE

    @classmethod
    def getPosition(cls, hWnd):
        """ get the position of auto-hide task bar

        Parameters
        ----------
        hWnd: int or `sip.voidptr`
            window handle
        """
        if isGreaterEqualWin8_1():
            monitorInfo = getMonitorInfo(
                hWnd, win32con.MONITOR_DEFAULTTONEAREST)
            if not monitorInfo:
                return cls.NO_POSITION

            monitor = RECT(*monitorInfo['Monitor'])
            appbarData = APPBARDATA(sizeof(APPBARDATA), 0, 0, 0, monitor, 0)
            positions = [cls.LEFT, cls.TOP, cls.RIGHT, cls.BOTTOM]
            for position in positions:
                appbarData.uEdge = position
                if windll.shell32.SHAppBarMessage(11, byref(appbarData)):
                    return position

            return cls.NO_POSITION

        appbarData = APPBARDATA(sizeof(APPBARDATA), win32gui.FindWindow(
            "Shell_TrayWnd", None), 0, 0, RECT(0, 0, 0, 0), 0)
        if appbarData.hWnd:
            windowMonitor = win32api.MonitorFromWindow(
                hWnd, win32con.MONITOR_DEFAULTTONEAREST)
            if not windowMonitor:
                return cls.NO_POSITION

            taskbarMonitor = win32api.MonitorFromWindow(
                appbarData.hWnd, win32con.MONITOR_DEFAULTTOPRIMARY)
            if not taskbarMonitor:
                return cls.NO_POSITION

            if taskbarMonitor == windowMonitor:
                windll.shell32.SHAppBarMessage(
                    shellcon.ABM_GETTASKBARPOS, byref(appbarData))
                return appbarData.uEdge

        return cls.NO_POSITION


class WindowsMoveResize:
    """ Tool class for moving and resizing Mac OS window """

    @staticmethod
    def startSystemMove(window, globalPos):
        """ resize window

        Parameters
        ----------
        window: QWidget
            window

        globalPos: QPoint
            the global point of mouse release event
        """
        win32gui.ReleaseCapture()
        win32api.SendMessage(
            int(window.winId()),
            win32con.WM_SYSCOMMAND,
            win32con.SC_MOVE | win32con.HTCAPTION,
            0
        )

    @classmethod
    def starSystemResize(cls, window, globalPos, edges):
        """ resize window

        Parameters
        ----------
        window: QWidget
            window

        globalPos: QPoint
            the global point of mouse release event

        edges: `Qt.Edges`
            window edges
        """
        pass
 No newline at end of file
+104 −38
Original line number Diff line number Diff line
@@ -4,17 +4,18 @@ from ctypes import cast
from ctypes.wintypes import LPRECT, MSG
from platform import platform

import win32api
import win32con
import win32gui
from PyQt5.QtCore import Qt
from PyQt5.QtCore import Qt, QSize, QRect
from PyQt5.QtGui import QCloseEvent, QCursor
from PyQt5.QtWidgets import QApplication, QWidget, QMainWindow

# from ..titlebar import TitleBar
from ..utils import win32_utils as win_utils
from ..utils.win32_utils import Taskbar
from .c_structures import LPNCCALCSIZE_PARAMS
from .window_effect import WindowsWindowEffect
from ... import win32_utils as win_utils
from ...win32_utils import Taskbar, isSystemBorderAccentEnabled, getSystemAccentColor
from ...win_c_structures import LPNCCALCSIZE_PARAMS
from ...win_window_effect import WindowsWindowEffect


class WindowsFramelessWindow(QWidget):
@@ -22,32 +23,37 @@ class WindowsFramelessWindow(QWidget):

    BORDER_WIDTH = 5


    def __init__(self, parent=None):
        super().__init__(parent=parent)
        self.windowEffect = WindowsWindowEffect(self)
        # self.titleBar = TitleBar(self)
        self._isSystemButtonVisible = False
        self._isResizeEnabled = True

        # remove window border
        self.updateFrameless()

        # solve issue #5
        self.windowHandle().screenChanged.connect(self.__onScreenChanged)

        # self.resize(500, 500)
        # self.titleBar.raise_()

    def updateFrameless(self):
        """ update frameless window """
        stayOnTop = Qt.WindowStaysOnTopHint if self.windowFlags() & Qt.WindowStaysOnTopHint else 0

        if not win_utils.isWin7():
            self.setWindowFlags(self.windowFlags() | Qt.FramelessWindowHint)
        elif parent:
            self.setWindowFlags(parent.windowFlags() | Qt.FramelessWindowHint | Qt.WindowMinMaxButtonsHint)
        elif self.parent():
            self.setWindowFlags(self.parent().windowFlags() | Qt.FramelessWindowHint | Qt.WindowMinMaxButtonsHint | stayOnTop)
        else:
            self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowMinMaxButtonsHint)
            self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowMinMaxButtonsHint | stayOnTop)

        # add DWM shadow and window animation
        self.windowEffect.addWindowAnimation(self.winId())
        if not isinstance(self, AcrylicWindow):
            self.windowEffect.addShadowEffect(self.winId())

        # solve issue #5
        self.windowHandle().screenChanged.connect(self.__onScreenChanged)

        # self.resize(500, 500)
        # self.titleBar.raise_()

    # def setTitleBar(self, titleBar):
    #     """ set custom title bar

@@ -57,6 +63,7 @@ class WindowsFramelessWindow(QWidget):
    #         title bar
    #     """
    #     self.titleBar.deleteLater()
    #     self.titleBar.hide()
    #     self.titleBar = titleBar
    #     self.titleBar.setParent(self)
    #     self.titleBar.raise_()
@@ -65,10 +72,45 @@ class WindowsFramelessWindow(QWidget):
        """ set whether resizing is enabled """
        self._isResizeEnabled = isEnabled

    def setStayOnTop(self, isTop: bool):
        """ set the stay on top status """
        if isTop:
            self.setWindowFlags(self.windowFlags() | Qt.WindowStaysOnTopHint)
        else:
            self.setWindowFlags(self.windowFlags() & ~Qt.WindowStaysOnTopHint)

        self.updateFrameless()
        self.show()

    def toggleStayOnTop(self):
        """ toggle the stay on top status """
        if self.windowFlags() & Qt.WindowStaysOnTopHint:
            self.setStayOnTop(False)
        else:
            self.setStayOnTop(True)

    # def resizeEvent(self, e):
    #     super().resizeEvent(e)
    #     self.titleBar.resize(self.width(), self.titleBar.height())

    def isSystemButtonVisible(self):
        """ Returns whether the system title bar button is visible """
        return self._isSystemButtonVisible

    def setSystemTitleBarButtonVisible(self, isVisible):
        """ set the visibility of system title bar button, only works for macOS """
        pass

    def systemTitleBarRect(self, size: QSize) -> QRect:
        """ Returns the system title bar rect, only works for macOS

        Parameters
        ----------
        size: QSize
            original system title bar rect
        """
        return QRect(0, 0, size.width(), size.height())

    def nativeEvent(self, eventType, message):
        """ Handle the Windows message """
        msg = MSG.from_address(message.__int__())
@@ -76,27 +118,40 @@ class WindowsFramelessWindow(QWidget):
            return super().nativeEvent(eventType, message)

        if msg.message == win32con.WM_NCHITTEST and self._isResizeEnabled:
            pos = QCursor.pos()
            yPos = pos.y() - self.y()
            if yPos < self.BORDER_WIDTH:
            xPos, yPos = win32gui.ScreenToClient(msg.hWnd, win32api.GetCursorPos())
            clientRect = win32gui.GetClientRect(msg.hWnd)

            w = clientRect[2] - clientRect[0]
            h = clientRect[3] - clientRect[1]

            # fixes issue https://github.com/zhiyiYo/PyQt-Frameless-Window/issues/98
            bw = 0 if win_utils.isMaximized(msg.hWnd) or win_utils.isFullScreen(msg.hWnd) else self.BORDER_WIDTH
            lx = xPos < bw  # left
            rx = xPos > w - bw  # right
            ty = yPos < bw  # top
            by = yPos > h - bw  # bottom
            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 msg.message == win32con.WM_NCCALCSIZE:
            if msg.wParam:
                rect = cast(msg.lParam, LPNCCALCSIZE_PARAMS).contents.rgrc[0]
            else:
                rect = cast(msg.lParam, LPRECT).contents

            top = rect.top

            # make window resizable
            ret = win32gui.DefWindowProc(msg.hWnd, win32con.WM_NCCALCSIZE, msg.wParam, msg.lParam)
            if ret != 0:
                return True, ret

            # restore top to remove title bar
            rect.top = top

            isMax = win_utils.isMaximized(msg.hWnd)
            isFull = win_utils.isFullScreen(msg.hWnd)

@@ -104,6 +159,11 @@ class WindowsFramelessWindow(QWidget):
            if isMax and not isFull:
                ty = win_utils.getResizeBorderThickness(msg.hWnd, False)
                rect.top += ty
                rect.bottom -= ty

                tx = win_utils.getResizeBorderThickness(msg.hWnd, True)
                rect.left += tx
                rect.right -= tx

            # handle the situation that an auto-hide taskbar is enabled
            if (isMax or isFull) and Taskbar.isAutoHide():
@@ -119,6 +179,12 @@ class WindowsFramelessWindow(QWidget):

            result = 0 if not msg.wParam else win32con.WVR_REDRAW
            return True, result
        elif msg.message == win32con.WM_SETFOCUS and isSystemBorderAccentEnabled():
            self.windowEffect.setBorderAccentColor(self.winId(), getSystemAccentColor())
            return True, 0
        elif msg.message == win32con.WM_KILLFOCUS:
            self.windowEffect.removeBorderAccentColor(self.winId())
            return True, 0

        return super().nativeEvent(eventType, message)

@@ -134,13 +200,18 @@ class AcrylicWindow(WindowsFramelessWindow):
    def __init__(self, parent=None):
        super().__init__(parent=parent)
        self.__closedByKey = False
        self.setStyleSheet("AcrylicWindow{background:transparent}")

    def updateFrameless(self):
        super().updateFrameless()
        self.windowEffect.enableBlurBehindWindow(self.winId())

        if win_utils.isWin7() and parent:
            self.setWindowFlags(parent.windowFlags() | Qt.FramelessWindowHint | Qt.WindowMinMaxButtonsHint)
        stayOnTop = Qt.WindowStaysOnTopHint if self.windowFlags() & Qt.WindowStaysOnTopHint else 0

        if win_utils.isWin7() and self.parent():
            self.setWindowFlags(self.parent().windowFlags() | Qt.FramelessWindowHint | Qt.WindowMinMaxButtonsHint | stayOnTop)
        else:
            self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowMinMaxButtonsHint)
            self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowMinMaxButtonsHint | stayOnTop)

        self.windowEffect.addWindowAnimation(self.winId())

@@ -152,11 +223,6 @@ class AcrylicWindow(WindowsFramelessWindow):
            if win_utils.isGreaterEqualWin11():
                self.windowEffect.addShadowEffect(self.winId())

        self.setStyleSheet("AcrylicWindow{background:transparent}")

        # don't remove this line
        self.resize(400, 400)

    def nativeEvent(self, eventType, message):
        """ Handle the Windows message """
        msg = MSG.from_address(message.__int__())
Loading