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. Hiding the original Windows title bar is easy, and so is overriding mouseMoveEvents to enable moving the frameless 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).
Today, I found a code example on GitHub that solved this problem. 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?
