PyQt: How to create custom combined titlebar and menubar - python

I'd like to set the menu and title in one bar, but have no idea that how to layout the menu bar and title bar (or my own title bar).
# -*- coding:utf-8 -*-
from PyQt5 import QtWidgets,QtGui,QtCore
import sys
qss = ""
class UI(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.setui()
def setui(self):
#----------main-window----------------------
self.setGeometry(0,0,1366,768) #x,y,w,h
self.setWindowTitle('hello world')
self.setWindowFlag(QtCore.Qt.FramelessWindowHint)
#----------menu-bar---------------------
#--------file-menu-----
self.menu_file=self.menuBar().addMenu('file')
self.menu_file_open=self.menu_file.addAction('open')
self.menu_file_save=self.menu_file.addAction('save')
self.menu_file_saveas=self.menu_file.addAction('save as...')
self.menu_file_quit=self.menu_file.addAction('exit')
#-----------experient-menu----------
self.menu_work=self.menuBar().addMenu('work')
#-------------analysis-menu---------
self.menu_analysis=self.menuBar().addMenu('analysis')
#------------edit-menu--------------
self.menu_edit=self.menuBar().addMenu('edit')
#------------window-menu--------------
self.menu_window=self.menuBar().addMenu('window')
#------------help---menu--------------
self.menu_help=self.menuBar().addMenu('help')
#-------------set---qss----------------------
self.setStyleSheet(qss)
#-------functions--connect-------------------
self.menu_file_quit.triggered.connect(QtWidgets.qApp.quit)
self.show()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
ex = UI()
sys.exit(app.exec_())
I expect a bar include icon, menus, title and the three buttons, just like the menu bar of vscode.

While the answer provided by #SNick is fine, I'd like to add my own proposal.
It is a bit more complex, but has some features his solution is missing and that are quite important in my opinion.
it actually uses a QMainWindow;
it can be used with ui files created in Designer. I only tried using uic.loadUi, but I think it could work with files created with pyuic too;
since it can use ui files, you can create the menu directly in designer;
being a QMainWindow, the statusBar is an actual QStatusBar;
supports the current system theme style and colors;
under Windows, it supports the window system menu;
has no size contraints, as the menu and title resize themselves according to the available space
That said, it's not perfect. So far, the only drawback I found is that it doesn't support toolbars and dockwidgets in the top area; I think it could be done, but it would be a bit more complicated.
The trick is to create a QMenuBar (or use the one set in Designer) that is not a native one, which is very important for MacOS, and add a top margin to the central widget, then use an internal QStyleOptionTitleBar to compute its size and draw its contents.
Be aware that whenever you want to add menubar items/menu, set other properties, or manually set a central widget, you must ensure that you do that after the *** END OF SETUP *** comment.
import sys
if sys.platform == 'win32':
import win32gui
import win32con
from PyQt5 import QtCore, QtGui, QtWidgets
# a "fake" button class that we need for hover and click events
class HiddenButton(QtWidgets.QPushButton):
hover = QtCore.pyqtSignal()
def __init__(self, parent):
super(HiddenButton, self).__init__(parent)
# prevent any painting to keep this button "invisible" while
# still reacting to its events
self.setUpdatesEnabled(False)
self.setFocusPolicy(QtCore.Qt.NoFocus)
def enterEvent(self, event):
self.hover.emit()
def leaveEvent(self, event):
self.hover.emit()
class SpecialTitleWindow(QtWidgets.QMainWindow):
__watchedActions = (
QtCore.QEvent.ActionAdded,
QtCore.QEvent.ActionChanged,
QtCore.QEvent.ActionRemoved
)
titleOpt = None
__menuBar = None
__titleBarMousePos = None
__sysMenuLock = False
__topMargin = 0
def __init__(self):
super(SpecialTitleWindow, self).__init__()
self.widgetHelpers = []
# uic.loadUi('titlebar.ui', self)
# enable the system menu
self.setWindowFlags(
QtCore.Qt.FramelessWindowHint | QtCore.Qt.WindowSystemMenuHint
)
# set the WindowActive to ensure that the title bar is painted as active
self.setWindowState(self.windowState() | QtCore.Qt.WindowActive)
# create a StyleOption that we need for painting and computing sizes
self.titleOpt = QtWidgets.QStyleOptionTitleBar()
self.titleOpt.initFrom(self)
self.titleOpt.titleBarFlags = (
QtCore.Qt.Window | QtCore.Qt.MSWindowsOwnDC |
QtCore.Qt.CustomizeWindowHint | QtCore.Qt.WindowTitleHint |
QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowMinMaxButtonsHint |
QtCore.Qt.WindowCloseButtonHint
)
self.titleOpt.state |= (QtWidgets.QStyle.State_Active |
QtWidgets.QStyle.State_HasFocus)
self.titleOpt.titleBarState = (int(self.windowState()) |
int(QtWidgets.QStyle.State_Active))
# create "fake" buttons
self.systemButton = HiddenButton(self)
self.systemButton.pressed.connect(self.showSystemMenu)
self.minimizeButton = HiddenButton(self)
self.minimizeButton.hover.connect(self.checkHoverStates)
self.minimizeButton.clicked.connect(self.minimize)
self.maximizeButton = HiddenButton(self)
self.maximizeButton.hover.connect(self.checkHoverStates)
self.maximizeButton.clicked.connect(self.maximize)
self.closeButton = HiddenButton(self)
self.closeButton.hover.connect(self.checkHoverStates)
self.closeButton.clicked.connect(self.close)
self.ctrlButtons = {
QtWidgets.QStyle.SC_TitleBarMinButton: self.minimizeButton,
QtWidgets.QStyle.SC_TitleBarMaxButton: self.maximizeButton,
QtWidgets.QStyle.SC_TitleBarNormalButton: self.maximizeButton,
QtWidgets.QStyle.SC_TitleBarCloseButton: self.closeButton,
}
self.widgetHelpers.extend([self.minimizeButton, self.maximizeButton, self.closeButton])
self.resetTitleHeight()
# *** END OF SETUP ***
fileMenu = self.menuBar().addMenu('File')
fileMenu.addAction('Open')
fileMenu.addAction('Save')
workMenu = self.menuBar().addMenu('Work')
workMenu.addAction('Work something')
analysisMenu = self.menuBar().addMenu('Analysis')
analysisMenu.addAction('Analize action')
# just call the statusBar to create one, we use it for resizing purposes
self.statusBar()
def resetTitleHeight(self):
# minimum height for the menu can change everytime an action is added,
# removed or modified; let's update it accordingly
if not self.titleOpt:
return
# set the minimum height of the titlebar
self.titleHeight = max(
self.style().pixelMetric(
QtWidgets.QStyle.PM_TitleBarHeight, self.titleOpt, self),
self.menuBar().sizeHint().height()
)
self.titleOpt.rect.setHeight(self.titleHeight)
self.menuBar().setMaximumHeight(self.titleHeight)
if self.minimumHeight() < self.titleHeight:
self.setMinimumHeight(self.titleHeight)
def checkHoverStates(self):
if not self.titleOpt:
return
# update the window buttons when hovering
pos = self.mapFromGlobal(QtGui.QCursor.pos())
for ctrl, btn in self.ctrlButtons.items():
rect = self.style().subControlRect(QtWidgets.QStyle.CC_TitleBar,
self.titleOpt, ctrl, self)
# since the maximize button can become a "restore", ensure that it
# actually exists according to the current state, if the rect
# has an actual size
if rect and pos in rect:
self.titleOpt.activeSubControls = ctrl
self.titleOpt.state |= QtWidgets.QStyle.State_MouseOver
break
else:
# no hover
self.titleOpt.state &= ~QtWidgets.QStyle.State_MouseOver
self.titleOpt.activeSubControls = QtWidgets.QStyle.SC_None
self.titleOpt.state |= QtWidgets.QStyle.State_Active
self.update()
def showSystemMenu(self, pos=None):
# show the system menu on windows
if sys.platform != 'win32':
return
if self.__sysMenuLock:
self.__sysMenuLock = False
return
winId = int(self.effectiveWinId())
sysMenu = win32gui.GetSystemMenu(winId, False)
if pos is None:
pos = self.systemButton.mapToGlobal(self.systemButton.rect().bottomLeft())
self.__sysMenuLock = True
cmd = win32gui.TrackPopupMenu(sysMenu,
win32gui.TPM_LEFTALIGN | win32gui.TPM_TOPALIGN | win32gui.TPM_RETURNCMD,
pos.x(), pos.y(), 0, winId, None)
win32gui.PostMessage(winId, win32con.WM_SYSCOMMAND, cmd, 0)
# restore the menu lock to hide it when clicking the system menu icon
QtCore.QTimer.singleShot(0, lambda: setattr(self, '__sysMenuLock', False))
def actualWindowTitle(self):
# window title can show "*" for modified windows
title = self.windowTitle()
if title:
title = title.replace('[*]', '*' if self.isWindowModified() else '')
return title
def updateTitleBar(self):
# compute again sizes when resizing or changing window title
menuWidth = self.menuBar().sizeHint().width()
availableRect = self.style().subControlRect(QtWidgets.QStyle.CC_TitleBar,
self.titleOpt, QtWidgets.QStyle.SC_TitleBarLabel, self)
left = availableRect.left()
if self.menuBar().sizeHint().height() < self.titleHeight:
top = (self.titleHeight - self.menuBar().sizeHint().height()) // 2
height = self.menuBar().sizeHint().height()
else:
top = 0
height = self.titleHeight
title = self.actualWindowTitle()
titleWidth = self.fontMetrics().width(title)
if not title and menuWidth > availableRect.width():
# resize the menubar to its maximum, but without hiding the buttons
width = availableRect.width()
elif menuWidth + titleWidth > availableRect.width():
# if the menubar and title require more than the available space,
# divide it equally, giving precedence to the window title space,
# since it is also necessary for window movement
width = availableRect.width() // 2
if menuWidth > titleWidth:
width = max(left, min(availableRect.width() - titleWidth, width))
# keep a minimum size for the menu arrow
if availableRect.width() - width < left:
width = left
extButton = self.menuBar().findChild(QtWidgets.QToolButton, 'qt_menubar_ext_button')
if self.isVisible() and extButton:
# if the "extButton" is visible (meaning that some item
# is hidden due to the menubar cannot be completely shown)
# resize to the last visible item + extButton, so that
# there's as much space available for the title
minWidth = extButton.width()
menuBar = self.menuBar()
spacing = self.style().pixelMetric(QtWidgets.QStyle.PM_MenuBarItemSpacing)
for i, action in enumerate(menuBar.actions()):
actionWidth = menuBar.actionGeometry(action).width()
if minWidth + actionWidth > width:
width = minWidth
break
minWidth += actionWidth + spacing
else:
width = menuWidth
self.menuBar().setGeometry(left, top, width, height)
# ensure that our internal widget are always on top
for w in self.widgetHelpers:
w.raise_()
self.update()
# helper function to avoid "ugly" colors on menubar items
def __setMenuBar(self, menuBar):
if self.__menuBar:
if self.__menuBar in self.widgetHelpers:
self.widgetHelpers.remove(self.__menuBar)
self.__menuBar.removeEventFilter(self)
self.__menuBar = menuBar
self.widgetHelpers.append(menuBar)
self.__menuBar.installEventFilter(self)
self.__menuBar.setNativeMenuBar(False)
self.__menuBar.setStyleSheet('''
QMenuBar {
background-color: transparent;
}
QMenuBar::item {
background-color: transparent;
}
QMenuBar::item:selected {
background-color: palette(button);
}
''')
def setMenuBar(self, menuBar):
self.__setMenuBar(menuBar)
def menuBar(self):
# QMainWindow.menuBar() returns a new blank menu bar if none exists
if not self.__menuBar:
self.__setMenuBar(QtWidgets.QMenuBar(self))
return self.__menuBar
def setCentralWidget(self, widget):
if self.centralWidget():
self.centralWidget().removeEventFilter(self)
# store the top content margin, we need it later
l, self.__topMargin, r, b = widget.getContentsMargins()
super(SpecialTitleWindow, self).setCentralWidget(widget)
# since the central widget always uses all the available space and can
# capture mouse events, install an event filter to catch them and
# allow us to grab them
widget.installEventFilter(self)
def eventFilter(self, source, event):
if source == self.centralWidget():
# do not propagate mouse press events to the centralWidget!
if (event.type() == QtCore.QEvent.MouseButtonPress and
event.button() == QtCore.Qt.LeftButton and
event.y() <= self.titleHeight):
self.__titleBarMousePos = event.pos()
event.accept()
return True
elif source == self.__menuBar and event.type() in self.__watchedActions:
self.resetTitleHeight()
return super(SpecialTitleWindow, self).eventFilter(source, event)
def minimize(self):
self.setWindowState(QtCore.Qt.WindowMinimized)
def maximize(self):
if self.windowState() & QtCore.Qt.WindowMaximized:
self.setWindowState(
self.windowState() & (~QtCore.Qt.WindowMaximized | QtCore.Qt.WindowActive))
else:
self.setWindowState(
self.windowState() | QtCore.Qt.WindowMaximized | QtCore.Qt.WindowActive)
# whenever a window is resized, its button states have to be checked again
self.checkHoverStates()
def contextMenuEvent(self, event):
if event.pos() not in self.menuBar().geometry():
self.showSystemMenu(event.globalPos())
def mousePressEvent(self, event):
if not self.centralWidget() and (event.type() == QtCore.QEvent.MouseButtonPress and
event.button() == QtCore.Qt.LeftButton and event.y() <= self.titleHeight):
self.__titleBarMousePos = event.pos()
def mouseMoveEvent(self, event):
super(SpecialTitleWindow, self).mouseMoveEvent(event)
if event.buttons() == QtCore.Qt.LeftButton and self.__titleBarMousePos:
# move the window
self.move(self.pos() + event.pos() - self.__titleBarMousePos)
def mouseDoubleClickEvent(self, event):
if event.button() == QtCore.Qt.LeftButton:
self.maximize()
def mouseReleaseEvent(self, event):
super(SpecialTitleWindow, self).mouseReleaseEvent(event)
self.__titleBarMousePos = None
def changeEvent(self, event):
# change the appearance of the titlebar according to the window state
if event.type() == QtCore.QEvent.ActivationChange:
if self.isActiveWindow():
self.titleOpt.titleBarState = (
int(self.windowState()) | int(QtWidgets.QStyle.State_Active))
self.titleOpt.palette.setCurrentColorGroup(QtGui.QPalette.Active)
else:
self.titleOpt.titleBarState = 0
self.titleOpt.palette.setCurrentColorGroup(QtGui.QPalette.Inactive)
self.update()
elif event.type() == QtCore.QEvent.WindowStateChange:
self.checkHoverStates()
elif event.type() == QtCore.QEvent.WindowTitleChange:
if self.titleOpt:
self.updateTitleBar()
def showEvent(self, event):
if not event.spontaneous():
# update the titlebar as soon as it's shown, to ensure that
# most of the title text is visible
self.updateTitleBar()
def resizeEvent(self, event):
super(SpecialTitleWindow, self).resizeEvent(event)
# update the centralWidget contents margins, adding the titlebar height
# to the top margin found before
if (self.centralWidget() and
self.centralWidget().getContentsMargins()[1] + self.__topMargin != self.titleHeight):
l, t, r, b = self.centralWidget().getContentsMargins()
self.centralWidget().setContentsMargins(
l, self.titleHeight + self.__topMargin, r, b)
# resize the width of the titlebar option, and move its buttons
self.titleOpt.rect.setWidth(self.width())
for ctrl, btn in self.ctrlButtons.items():
rect = self.style().subControlRect(
QtWidgets.QStyle.CC_TitleBar, self.titleOpt, ctrl, self)
if rect:
btn.setGeometry(rect)
sysRect = self.style().subControlRect(QtWidgets.QStyle.CC_TitleBar,
self.titleOpt, QtWidgets.QStyle.SC_TitleBarSysMenu, self)
if sysRect:
self.systemButton.setGeometry(sysRect)
self.titleOpt.titleBarState = int(self.windowState())
if self.isActiveWindow():
self.titleOpt.titleBarState |= int(QtWidgets.QStyle.State_Active)
self.updateTitleBar()
def paintEvent(self, event):
qp = QtGui.QPainter(self)
self.style().drawComplexControl(QtWidgets.QStyle.CC_TitleBar, self.titleOpt, qp, self)
titleRect = self.style().subControlRect(QtWidgets.QStyle.CC_TitleBar,
self.titleOpt, QtWidgets.QStyle.SC_TitleBarLabel, self)
icon = self.windowIcon()
if not icon.isNull():
iconRect = QtCore.QRect(0, 0, titleRect.left(), self.titleHeight)
qp.drawPixmap(iconRect, icon.pixmap(iconRect.size()))
title = self.actualWindowTitle()
titleRect.setLeft(self.menuBar().geometry().right())
if title:
# move left of the rectangle available for the title to the right of
# the menubar; if the title is bigger than the available space, elide it
elided = self.fontMetrics().elidedText(
title, QtCore.Qt.ElideRight, titleRect.width() - 2)
qp.drawText(titleRect, QtCore.Qt.AlignCenter, elided)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
w = SpecialTitleWindow()
w.setWindowTitle('My window')
w.show()
sys.exit(app.exec_())

Try it:
import sys
from PyQt5.QtCore import pyqtSlot, QPoint, Qt, QRect
from PyQt5.QtWidgets import (QMainWindow, QApplication, QPushButton, QHBoxLayout,
QVBoxLayout, QTabWidget, QWidget, QAction,
QLabel, QSizeGrip, QMenuBar, qApp)
from PyQt5.QtGui import QIcon
class TitleBar(QWidget):
height = 35
def __init__(self, parent):
super(TitleBar, self).__init__()
self.parent = parent
self.layout = QHBoxLayout()
self.layout.setContentsMargins(0,0,0,0)
self.menu_bar = QMenuBar()
self.menu_bar.setStyleSheet("""
color: #fff;
background-color: #23272A;
font-size: 14px;
padding: 4px;
""")
self.menu_file = self.menu_bar.addMenu('file')
self.menu_file_open=self.menu_file.addAction('open')
self.menu_file_save=self.menu_file.addAction('save')
self.menu_file_saveas=self.menu_file.addAction('save as...')
self.menu_file_quit=self.menu_file.addAction('exit')
self.menu_file_quit.triggered.connect(qApp.quit)
self.menu_work=self.menu_bar.addMenu('work')
self.menu_analysis=self.menu_bar.addMenu('analysis')
self.menu_edit=self.menu_bar.addMenu('edit')
self.menu_window=self.menu_bar.addMenu('window')
self.menu_help=self.menu_bar.addMenu('help')
self.layout.addWidget(self.menu_bar)
self.title = QLabel("Hello World!")
self.title.setFixedHeight(self.height)
self.layout.addWidget(self.title)
self.title.setStyleSheet("""
background-color: #23272a; /* 23272a #f00*/
font-weight: bold;
font-size: 16px;
color: blue;
padding-left: 170px;
""")
self.closeButton = QPushButton(' ')
self.closeButton.clicked.connect(self.on_click_close)
self.closeButton.setStyleSheet("""
background-color: #DC143C;
border-radius: 10px;
height: {};
width: {};
margin-right: 3px;
font-weight: bold;
color: #000;
font-family: "Webdings";
qproperty-text: "r";
""".format(self.height/1.7,self.height/1.7))
self.maxButton = QPushButton(' ')
self.maxButton.clicked.connect(self.on_click_maximize)
self.maxButton.setStyleSheet("""
background-color: #32CD32;
border-radius: 10px;
height: {};
width: {};
margin-right: 3px;
font-weight: bold;
color: #000;
font-family: "Webdings";
qproperty-text: "1";
""".format(self.height/1.7,self.height/1.7))
self.hideButton = QPushButton(' ')
self.hideButton.clicked.connect(self.on_click_hide)
self.hideButton.setStyleSheet("""
background-color: #FFFF00;
border-radius: 10px;
height: {};
width: {};
margin-right: 3px;
font-weight: bold;
color: #000;
font-family: "Webdings";
qproperty-text: "0";
""".format(self.height/1.7,self.height/1.7))
self.layout.addWidget(self.hideButton)
self.layout.addWidget(self.maxButton)
self.layout.addWidget(self.closeButton)
self.setLayout(self.layout)
self.start = QPoint(0, 0)
self.pressing = False
self.maximaze = False
def resizeEvent(self, QResizeEvent):
super(TitleBar, self).resizeEvent(QResizeEvent)
self.title.setFixedWidth(self.parent.width())
def mousePressEvent(self, event):
self.start = self.mapToGlobal(event.pos())
self.pressing = True
def mouseMoveEvent(self, event):
if self.pressing:
self.end = self.mapToGlobal(event.pos())
self.movement = self.end-self.start
self.parent.move(self.mapToGlobal(self.movement))
self.start = self.end
def mouseReleaseEvent(self, QMouseEvent):
self.pressing = False
def on_click_close(self):
sys.exit()
def on_click_maximize(self):
self.maximaze = not self.maximaze
if self.maximaze: self.parent.setWindowState(Qt.WindowNoState)
if not self.maximaze:
self.parent.setWindowState(Qt.WindowMaximized)
def on_click_hide(self):
self.parent.showMinimized()
class StatusBar(QWidget):
def __init__(self, parent):
super(StatusBar, self).__init__()
self.initUI()
self.showMessage("showMessage: Hello world!")
def initUI(self):
self.label = QLabel("Status bar...")
self.label.setFixedHeight(24)
self.label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
self.label.setStyleSheet("""
background-color: #23272a;
font-size: 12px;
padding-left: 5px;
color: white;
""")
self.layout = QHBoxLayout()
self.layout.setContentsMargins(0,0,0,0)
self.layout.addWidget(self.label)
self.setLayout(self.layout)
def showMessage(self, text):
self.label.setText(text)
class MainWindow(QWidget):
def __init__(self):
super(MainWindow, self).__init__()
self.setFixedSize(800, 400)
self.setWindowFlags(self.windowFlags() | Qt.FramelessWindowHint)
self.setStyleSheet("background-color: #2c2f33;")
self.setWindowTitle('Code Maker')
self.title_bar = TitleBar(self)
self.status_bar = StatusBar(self)
self.layout = QVBoxLayout()
self.layout.setContentsMargins(0,0,0,0)
self.layout.addWidget(self.title_bar)
self.layout.addStretch(1)
self.layout.addWidget(self.status_bar)
self.layout.setSpacing(0)
self.setLayout(self.layout)
if __name__ == "__main__":
app = QApplication(sys.argv)
mw = MainWindow()
mw.show()
sys.exit(app.exec_())

I did a few patches so that the code provided by #musicamante can work on PySide6, but I found that when moving the window, it's shaking. When I looked at the mouseMoveEvent and read qt's document about event.pos() in QMouseEvent, which says:
If you move the widget as a result of the mouse event, use the global position returned by globalPos() to avoid a shaking motion.
So I replaced
self.move(self.pos() + event.pos() - self.__titleBarMousePos)
with
self.move(event.globalPos() - self.__titleBarMousePos)
in mouseMoveEvent()
Here is the PySide6 version:
import sys
if sys.platform == 'win32':
import win32gui
import win32con
from PySide6 import QtCore, QtGui, QtWidgets
# a "fake" button class that we need for hover and click events
class HiddenButton(QtWidgets.QPushButton):
hover = QtCore.Signal()
def __init__(self, parent):
super(HiddenButton, self).__init__(parent)
# prevent any painting to keep this button "invisible" while
# still reacting to its events
self.setUpdatesEnabled(False)
self.setFocusPolicy(QtCore.Qt.NoFocus)
def enterEvent(self, event):
self.hover.emit()
def leaveEvent(self, event):
self.hover.emit()
class SpecialTitleWindow(QtWidgets.QMainWindow):
__watchedActions = (
QtCore.QEvent.ActionAdded,
QtCore.QEvent.ActionChanged,
QtCore.QEvent.ActionRemoved
)
titleOpt = None
__menuBar = None
__titleBarMousePos = None
__sysMenuLock = False
__topMargin = 0
def __init__(self):
super(SpecialTitleWindow, self).__init__()
self.widgetHelpers = []
# uic.loadUi('titlebar.ui', self)
# enable the system menu
self.setWindowFlags(
QtCore.Qt.FramelessWindowHint | QtCore.Qt.WindowSystemMenuHint
)
# set the WindowActive to ensure that the title bar is painted as active
self.setWindowState(self.windowState() | QtCore.Qt.WindowActive)
# create a StyleOption that we need for painting and computing sizes
self.titleOpt = QtWidgets.QStyleOptionTitleBar()
self.titleOpt.initFrom(self)
self.titleOpt.titleBarFlags = (
QtCore.Qt.Window | QtCore.Qt.MSWindowsOwnDC |
QtCore.Qt.CustomizeWindowHint | QtCore.Qt.WindowTitleHint |
QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowMinMaxButtonsHint |
QtCore.Qt.WindowCloseButtonHint
)
self.titleOpt.state |= (QtWidgets.QStyle.State_Active |
QtWidgets.QStyle.State_HasFocus)
self.titleOpt.titleBarState = (int(self.windowState()) |
int(QtWidgets.QStyle.State_Active))
# create "fake" buttons
self.systemButton = HiddenButton(self)
self.systemButton.pressed.connect(self.showSystemMenu)
self.minimizeButton = HiddenButton(self)
self.minimizeButton.hover.connect(self.checkHoverStates)
self.minimizeButton.clicked.connect(self.minimize)
self.maximizeButton = HiddenButton(self)
self.maximizeButton.hover.connect(self.checkHoverStates)
self.maximizeButton.clicked.connect(self.maximize)
self.closeButton = HiddenButton(self)
self.closeButton.hover.connect(self.checkHoverStates)
self.closeButton.clicked.connect(self.close)
self.ctrlButtons = {
QtWidgets.QStyle.SC_TitleBarMinButton: self.minimizeButton,
QtWidgets.QStyle.SC_TitleBarMaxButton: self.maximizeButton,
QtWidgets.QStyle.SC_TitleBarNormalButton: self.maximizeButton,
QtWidgets.QStyle.SC_TitleBarCloseButton: self.closeButton,
}
self.widgetHelpers.extend([self.minimizeButton, self.maximizeButton, self.closeButton])
self.resetTitleHeight()
# *** END OF SETUP ***
fileMenu = self.menuBar().addMenu('File')
fileMenu.addAction('Open')
fileMenu.addAction('Save')
workMenu = self.menuBar().addMenu('Work')
workMenu.addAction('Work something')
analysisMenu = self.menuBar().addMenu('Analysis')
analysisMenu.addAction('Analize action')
# just call the statusBar to create one, we use it for resizing purposes
self.statusBar()
self.resize(400, 250)
def resetTitleHeight(self):
# minimum height for the menu can change everytime an action is added,
# removed or modified; let's update it accordingly
if not self.titleOpt:
return
# set the minimum height of the titlebar
self.titleHeight = max(
self.style().pixelMetric(
QtWidgets.QStyle.PM_TitleBarHeight, self.titleOpt, self),
self.menuBar().sizeHint().height()
)
self.titleOpt.rect.setHeight(self.titleHeight)
self.menuBar().setMaximumHeight(self.titleHeight)
if self.minimumHeight() < self.titleHeight:
self.setMinimumHeight(self.titleHeight)
def checkHoverStates(self):
if not self.titleOpt:
return
# update the window buttons when hovering
pos = self.mapFromGlobal(QtGui.QCursor.pos())
for ctrl, btn in self.ctrlButtons.items():
rect = self.style().subControlRect(QtWidgets.QStyle.CC_TitleBar,
self.titleOpt, ctrl, self)
# since the maximize button can become a "restore", ensure that it
# actually exists according to the current state, if the rect
# has an actual size
if rect and rect.contains(pos):
self.titleOpt.activeSubControls = ctrl
self.titleOpt.state |= QtWidgets.QStyle.State_MouseOver
break
else:
# no hover
self.titleOpt.state &= ~QtWidgets.QStyle.State_MouseOver
self.titleOpt.activeSubControls = QtWidgets.QStyle.SC_None
self.titleOpt.state |= QtWidgets.QStyle.State_Active
self.update()
def showSystemMenu(self, pos=None):
# show the system menu on windows
if sys.platform != 'win32':
return
if self.__sysMenuLock:
self.__sysMenuLock = False
return
winId = int(self.effectiveWinId())
sysMenu = win32gui.GetSystemMenu(winId, False)
if pos is None:
pos = self.systemButton.mapToGlobal(self.systemButton.rect().bottomLeft())
self.__sysMenuLock = True
cmd = win32gui.TrackPopupMenu(sysMenu,
win32gui.TPM_LEFTALIGN | win32gui.TPM_TOPALIGN | win32gui.TPM_RETURNCMD,
pos.x(), pos.y(), 0, winId, None)
win32gui.PostMessage(winId, win32con.WM_SYSCOMMAND, cmd, 0)
# restore the menu lock to hide it when clicking the system menu icon
QtCore.QTimer.singleShot(0, lambda: setattr(self, '__sysMenuLock', False))
def actualWindowTitle(self):
# window title can show "*" for modified windows
title = self.windowTitle()
if title:
title = title.replace('[*]', '*' if self.isWindowModified() else '')
return title
def updateTitleBar(self):
# compute again sizes when resizing or changing window title
menuWidth = self.menuBar().sizeHint().width()
availableRect = self.style().subControlRect(QtWidgets.QStyle.CC_TitleBar,
self.titleOpt, QtWidgets.QStyle.SC_TitleBarLabel, self)
left = availableRect.left()
if self.menuBar().sizeHint().height() < self.titleHeight:
top = (self.titleHeight - self.menuBar().sizeHint().height()) // 2
height = self.menuBar().sizeHint().height()
else:
top = 0
height = self.titleHeight
title = self.actualWindowTitle()
titleWidth = self.fontMetrics().boundingRect(title).width()
if not title and menuWidth > availableRect.width():
# resize the menubar to its maximum, but without hiding the buttons
width = availableRect.width()
elif menuWidth + titleWidth > availableRect.width():
# if the menubar and title require more than the available space,
# divide it equally, giving precedence to the window title space,
# since it is also necessary for window movement
width = availableRect.width() // 2
if menuWidth > titleWidth:
width = max(left, min(availableRect.width() - titleWidth, width))
# keep a minimum size for the menu arrow
if availableRect.width() - width < left:
width = left
extButton = self.menuBar().findChild(QtWidgets.QToolButton, 'qt_menubar_ext_button')
if self.isVisible() and extButton:
# if the "extButton" is visible (meaning that some item
# is hidden due to the menubar cannot be completely shown)
# resize to the last visible item + extButton, so that
# there's as much space available for the title
minWidth = extButton.width()
menuBar = self.menuBar()
spacing = self.style().pixelMetric(QtWidgets.QStyle.PM_MenuBarItemSpacing)
for i, action in enumerate(menuBar.actions()):
actionWidth = menuBar.actionGeometry(action).width()
if minWidth + actionWidth > width:
width = minWidth
break
minWidth += actionWidth + spacing
else:
width = menuWidth
self.menuBar().setGeometry(left, top, width, height)
# ensure that our internal widget are always on top
for w in self.widgetHelpers:
w.raise_()
self.update()
# helper function to avoid "ugly" colors on menubar items
def __setMenuBar(self, menuBar):
if self.__menuBar:
if self.__menuBar in self.widgetHelpers:
self.widgetHelpers.remove(self.__menuBar)
self.__menuBar.removeEventFilter(self)
self.__menuBar = menuBar
self.widgetHelpers.append(menuBar)
self.__menuBar.installEventFilter(self)
self.__menuBar.setNativeMenuBar(False)
self.__menuBar.setStyleSheet('''
QMenuBar {
background-color: transparent;
}
QMenuBar::item {
background-color: transparent;
}
QMenuBar::item:selected {
background-color: palette(button);
}
''')
def setMenuBar(self, menuBar):
self.__setMenuBar(menuBar)
def menuBar(self):
# QMainWindow.menuBar() returns a new blank menu bar if none exists
if not self.__menuBar:
self.__setMenuBar(QtWidgets.QMenuBar(self))
return self.__menuBar
def setCentralWidget(self, widget):
if self.centralWidget():
self.centralWidget().removeEventFilter(self)
# store the top content margin, we need it later
l, self.__topMargin, r, b = widget.contentsMargins()
super(SpecialTitleWindow, self).setCentralWidget(widget)
# since the central widget always uses all the available space and can
# capture mouse events, install an event filter to catch them and
# allow us to grab them
widget.installEventFilter(self)
def eventFilter(self, source, event):
if source == self.centralWidget():
# do not propagate mouse press events to the centralWidget!
if (event.type() == QtCore.QEvent.MouseButtonPress and
event.button() == QtCore.Qt.LeftButton and
event.y() <= self.titleHeight):
self.__titleBarMousePos = event.pos()
event.accept()
return True
elif source == self.__menuBar and event.type() in self.__watchedActions:
self.resetTitleHeight()
return super(SpecialTitleWindow, self).eventFilter(source, event)
def minimize(self):
self.setWindowState(QtCore.Qt.WindowMinimized)
def maximize(self):
if self.windowState() & QtCore.Qt.WindowMaximized:
self.setWindowState(
self.windowState() & (~QtCore.Qt.WindowMaximized | QtCore.Qt.WindowActive))
else:
self.setWindowState(
self.windowState() | QtCore.Qt.WindowMaximized | QtCore.Qt.WindowActive)
# whenever a window is resized, its button states have to be checked again
self.checkHoverStates()
def contextMenuEvent(self, event):
if not self.menuBar().geometry().contains(event.pos()):
self.showSystemMenu(event.globalPos())
def mousePressEvent(self, event):
if not self.centralWidget() and (event.type() == QtCore.QEvent.MouseButtonPress and
event.button() == QtCore.Qt.LeftButton and event.position().y() <= self.titleHeight):
self.__titleBarMousePos = event.position()
def mouseMoveEvent(self, event):
super(SpecialTitleWindow, self).mouseMoveEvent(event)
if event.buttons() == QtCore.Qt.LeftButton and self.__titleBarMousePos:
# move the window
self.move((event.globalPosition() - self.__titleBarMousePos).toPoint())
def mouseDoubleClickEvent(self, event):
if event.button() == QtCore.Qt.LeftButton:
self.maximize()
def mouseReleaseEvent(self, event):
super(SpecialTitleWindow, self).mouseReleaseEvent(event)
self.__titleBarMousePos = None
def changeEvent(self, event):
# change the appearance of the titlebar according to the window state
if event.type() == QtCore.QEvent.ActivationChange:
if self.isActiveWindow():
self.titleOpt.titleBarState = (
int(self.windowState()) | int(QtWidgets.QStyle.State_Active))
self.titleOpt.palette.setCurrentColorGroup(QtGui.QPalette.Active)
else:
self.titleOpt.titleBarState = 0
self.titleOpt.palette.setCurrentColorGroup(QtGui.QPalette.Inactive)
self.update()
elif event.type() == QtCore.QEvent.WindowStateChange:
self.checkHoverStates()
elif event.type() == QtCore.QEvent.WindowTitleChange:
if self.titleOpt:
self.updateTitleBar()
def showEvent(self, event):
if not event.spontaneous():
# update the titlebar as soon as it's shown, to ensure that
# most of the title text is visible
self.updateTitleBar()
def resizeEvent(self, event):
super(SpecialTitleWindow, self).resizeEvent(event)
# update the centralWidget contents margins, adding the titlebar height
# to the top margin found before
if (self.centralWidget() and
self.centralWidget().getContentsMargins()[1] + self.__topMargin != self.titleHeight):
l, t, r, b = self.centralWidget().getContentsMargins()
self.centralWidget().setContentsMargins(
l, self.titleHeight + self.__topMargin, r, b)
# resize the width of the titlebar option, and move its buttons
self.titleOpt.rect.setWidth(self.width())
for ctrl, btn in self.ctrlButtons.items():
rect = self.style().subControlRect(
QtWidgets.QStyle.CC_TitleBar, self.titleOpt, ctrl, self)
if rect:
btn.setGeometry(rect)
sysRect = self.style().subControlRect(QtWidgets.QStyle.CC_TitleBar,
self.titleOpt, QtWidgets.QStyle.SC_TitleBarSysMenu, self)
if sysRect:
self.systemButton.setGeometry(sysRect)
self.titleOpt.titleBarState = int(self.windowState())
if self.isActiveWindow():
self.titleOpt.titleBarState |= int(QtWidgets.QStyle.State_Active)
self.updateTitleBar()
def paintEvent(self, event):
qp = QtGui.QPainter(self)
self.style().drawComplexControl(QtWidgets.QStyle.CC_TitleBar, self.titleOpt, qp, self)
titleRect = self.style().subControlRect(QtWidgets.QStyle.CC_TitleBar,
self.titleOpt, QtWidgets.QStyle.SC_TitleBarLabel, self)
icon = self.windowIcon()
if not icon.isNull():
iconRect = QtCore.QRect(0, 0, titleRect.left(), self.titleHeight)
qp.drawPixmap(iconRect, icon.pixmap(iconRect.size()))
title = self.actualWindowTitle()
titleRect.setLeft(self.menuBar().geometry().right())
if title:
# move left of the rectangle available for the title to the right of
# the menubar; if the title is bigger than the available space, elide it
elided = self.fontMetrics().elidedText(
title, QtCore.Qt.ElideRight, titleRect.width() - 2)
qp.drawText(titleRect, QtCore.Qt.AlignCenter, elided)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
w = SpecialTitleWindow()
w.setWindowTitle('My window')
w.show()
sys.exit(app.exec())

Related

Drawing Line from QLabel to QLabel in PyQt

I'm fairly new to PyQt
I'm trying to drawing a line from 1 QLabel to another.
My 2 QLabel are located on another QLabel which acts as an image in my GUI.
I've managed to track the mouse event and move the label around, but I cannot draw the line between them using QPainter.
Thank you in advance :)
This is my MouseTracking class
class MouseTracker(QtCore.QObject):
positionChanged = QtCore.pyqtSignal(QtCore.QPoint)
def __init__(self, widget):
super().__init__(widget)
self._widget = widget
self.widget.setMouseTracking(True)
self.widget.installEventFilter(self)
#property
def widget(self):
return self._widget
def eventFilter(self, o, e):
if e.type() == QtCore.QEvent.MouseMove:
self.positionChanged.emit(e.pos())
return super().eventFilter(o, e)
This is my DraggableLabel class:
class DraggableLabel(QLabel):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.LabelIsMoving = False
self.setStyleSheet("border-color: rgb(238, 0, 0); border-width : 2.0px; border-style:inset; background: transparent;")
self.origin = None
# self.setDragEnabled(True)
def mousePressEvent(self, event):
if not self.origin:
# update the origin point, we'll need that later
self.origin = self.pos()
if event.button() == Qt.LeftButton:
self.LabelIsMoving = True
self.mousePos = event.pos()
# print(event.pos())
def mouseMoveEvent(self, event):
if event.buttons() == Qt.LeftButton:
# move the box
self.move(self.pos() + event.pos() - self.mousePos)
# print(event.pos())
def mouseReleaseEvent(self, event):
if event.button() == Qt.LeftButton:
print(event.pos())
def paintEvent(self, event):
painter = QPainter()
painter.setBrush(Qt.red)
# painter.setPen(qRgb(200,0,0))
painter.drawLine(10, 10, 200, 200)
This is my custom class for the QTabwigdet (since I need to control and track the position of 2 QLabels whenever the user add/insert a new Tab)
class DynamicTab(QWidget):
def __init__(self):
super(DynamicTab, self).__init__()
# self.count = 0
self.setMouseTracking(True)
self.setAcceptDrops(True)
self.bool = True
self.layout = QVBoxLayout(self)
self.label = QLabel()
self.layout.addChildWidget(self.label)
self.icon1 = DraggableLabel(parent=self)
#pixmap for icon 1
pixmap = QPixmap('icon1.png')
# currentTab.setLayout(QVBoxLayout())
# currentTab.layout.setWidget(QRadioButton())
self.icon1.setPixmap(pixmap)
self.icon1.setScaledContents(True)
self.icon1.setFixedSize(20, 20)
self.icon2 = DraggableLabel(parent=self)
pixmap = QPixmap('icon1.png')
# currentTab.setLayout(QVBoxLayout())
# currentTab.layout.setWidget(QRadioButton())
self.icon2.setPixmap(pixmap)
self.icon2.setScaledContents(True)
self.icon2.setFixedSize(20, 20)
#self.label.move(event.x() - self.label_pos.x(), event.y() - self.label_pos.y())
MainWindow and main method:
class UI_MainWindow(QMainWindow):
def __init__(self):
super(UI_MainWindow, self).__init__()
self.setWindowTitle("QHBoxLayout")
self.PictureTab = QTabWidget
def __setupUI__(self):
# super(UI_MainWindow, self).__init__()
self.setWindowTitle("QHBoxLayout")
loadUi("IIML_test2.ui", self)
self.tabChanged(self.PictureTab)
# self.tabChanged(self.tabWidget)
self.changeTabText(self.PictureTab, index=0, TabText="Patient1")
self.Button_ImportNew.clicked.connect(lambda: self.insertTab(self.PictureTab))
# self.PictureTab.currentChanged.connect(lambda: self.tabChanged(QtabWidget=self.PictureTab))
# self.tabWidget.currentChanged.connect(lambda: self.tabChanged(QtabWidget=self.tabWidget))
def tabChanged(self, QtabWidget):
QtabWidget.currentChanged.connect(lambda : print("Tab was changed to ", QtabWidget.currentIndex()))
def changeTabText(self, QTabWidget, index, TabText):
QTabWidget.setTabText(index, TabText)
def insertTab(self, QtabWidget):
# QFileDialog.getOpenFileNames(self, 'Open File', '.')
QtabWidget.addTab(DynamicTab(), "New Tab")
# get number of active tab
count = QtabWidget.count()
# change the view to the last added tab
currentTab = QtabWidget.widget(count-1)
QtabWidget.setCurrentWidget(currentTab)
pixmap = QPixmap('cat.jpg')
#currentTab.setLayout(QVBoxLayout())
#currentTab.layout.setWidget(QRadioButton())
# currentTab.setImage("cat.jpg")
currentTab.label.setPixmap(pixmap)
currentTab.label.setScaledContents(True)
currentTab.label.setFixedSize(self.label.width(), self.label.height())
tracker = MouseTracker(currentTab.label)
tracker.positionChanged.connect(self.on_positionChanged)
self.label_position = QtWidgets.QLabel(currentTab.label, alignment=QtCore.Qt.AlignCenter)
self.label_position.setStyleSheet('background-color: white; border: 1px solid black')
currentTab.label.show()
# print(currentTab.label)
#QtCore.pyqtSlot(QtCore.QPoint)
def on_positionChanged(self, pos):
delta = QtCore.QPoint(30, -15)
self.label_position.show()
self.label_position.move(pos + delta)
self.label_position.setText("(%d, %d)" % (pos.x(), pos.y()))
self.label_position.adjustSize()
# def SetupUI(self, MainWindow):
#
# self.setLayout(self.MainLayout)
if __name__ == '__main__':
app = QApplication(sys.argv)
UI_MainWindow = UI_MainWindow()
UI_MainWindow.__setupUI__()
widget = QtWidgets.QStackedWidget()
widget.addWidget(UI_MainWindow)
widget.setFixedHeight(900)
widget.setFixedWidth(1173)
widget.show()
try:
sys.exit(app.exec_())
except:
print("Exiting")
My concept: I have a DynamicTab (QTabWidget) which acts as a picture opener (whenever the user press Import Now). The child of this Widget are 3 Qlabels: self.label is the picture it self and two other Qlabels are the icon1 and icon2 which I'm trying to interact/drag with (Draggable Label)
My Problem: I'm trying to track my mouse movement and custom the painter to paint accordingly. I'm trying that out by telling the painter class to paint whenever I grab the label and move it with my mouse (Hence, draggable). However, I can only track the mouse position inside the main QLabel (the main picture) whenever I'm not holding or clicking my left mouse.
Any help will be appreciated here.
Thank you guys.
Painting can only happen within the widget rectangle, so you cannot draw outside the boundaries of DraggableLabel.
The solution is to create a further custom widget that shares the same parent, and then draw the line that connects the center of the other two.
In the following example I install an event filter on the two draggable labels which will update the size of the custom widget based on them (so that its geometry will always include those two geometries) and call self.update() which schedules a repainting. Note that since the widget is created above the other two, it might capture mouse events that are intended for the others; to prevent that, the Qt.WA_TransparentForMouseEvents attribute must be set.
class Line(QWidget):
def __init__(self, obj1, obj2, parent):
super().__init__(parent)
self.obj1 = obj1
self.obj2 = obj2
self.obj1.installEventFilter(self)
self.obj2.installEventFilter(self)
self.setAttribute(Qt.WA_TransparentForMouseEvents)
def eventFilter(self, obj, event):
if event.type() in (event.Move, event.Resize):
rect = self.obj1.geometry() | self.obj2.geometry()
corner = rect.bottomRight()
self.resize(corner.x(), corner.y())
self.update()
return super().eventFilter(obj, event)
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(painter.Antialiasing)
painter.setPen(QColor(200, 0, 0))
painter.drawLine(
self.obj1.geometry().center(),
self.obj2.geometry().center()
)
class DynamicTab(QWidget):
def __init__(self):
# ...
self.line = Line(self.icon1, self.icon2, self)
Notes:
to simplify things, I only use resize() (not setGeometry()), in this way the widget will always be placed on the top left corner of the parent and we can directly get the other widget's coordinates without any conversion;
the custom widget is placed above the other two because it is added after them; if you want to place it under them, use self.line.lower();
the painter must always be initialized with the paint device argument, either by using QPainter(obj) or painter.begin(obj), otherwise no painting will happen (and you'll get lots of errors in the output);
do not use layout.addChildWidget() (which is used internally by the layout), but the proper addWidget() function of the layout;
the stylesheet border syntax can be shortened with border: 2px inset rgb(238, 0, 0);;
the first lines of insertTab could be simpler: currentTab = DynamicTab() QtabWidget.addTab(currentTab, "New Tab");
currentTab.label.setFixedSize(self.label.size());
QMainWindow is generally intended as a top level widget, it's normally discouraged to add it to a QStackedWidget; note that if you did that because of a Youtube tutorial, that tutorial is known for suggesting terrible practices (like the final try/except block) which should not be followed;
only classes and constants should have capitalized names, not variables and functions which should always start with a lowercase letter;

PyQt5 grabWindow captures black screen instead of the selected area

I have an application where I have a transparent window, I am capturing the screen underneath and then displaying the same once user release the left mouse button.
But the problem is I see only black screen, I tried saving the selected screenshot but still same black screen.
Here is my code :
from PyQt5 import QtWidgets as qtw
from PyQt5 import QtCore as qtc
from PyQt5 import QtGui as qtg
import sys
class MainWindow(qtw.QMainWindow):
def __init__(self, *arg, **kwargs):
super().__init__()
self.setWindowFlag(qtc.Qt.FramelessWindowHint)
self.setAttribute(qtc.Qt.WA_TranslucentBackground)
borderWidget = qtw.QWidget(objectName='borderWidget')
self.setCentralWidget(borderWidget)
bgd = self.palette().color(qtg.QPalette.Window)
bgd.setAlphaF(.005)
self.setStyleSheet('''
#borderWidget {{
border: 3px solid blue;
background: {bgd};
}}
'''.format(bgd=bgd.name(bgd.HexArgb)))
self.setGeometry(100, 100, 400, 300)
self.showFullScreen()
self.setCursor(qtc.Qt.CrossCursor)
self.begin = None
self.end = None
self.show()
def paintEvent(self, event):
if self.begin:
qpbox = qtg.QPainter(self)
br = qtg.QBrush(qtg.QColor(100, 10, 10, 40))
qpbox.setBrush(br)
qpbox.drawRect(qtc.QRect(self.begin, self.end))
# close on right click
def mouseReleaseEvent(self, QMouseEvent):
if QMouseEvent.button() == qtc.Qt.RightButton:
self.close()
elif QMouseEvent.button() == qtc.Qt.LeftButton:
screen = qtw.QApplication.primaryScreen()
img = screen.grabWindow(self.winId(), self.begin.x(), self.end.y(), self.end.x() - self.begin.x() , self.end.y()-self.begin.y())
img.save('screenshot.png', 'png')
self.setStyleSheet("")
self.central_widget = qtw.QWidget()
label = qtw.QLabel(self)
label.setPixmap(img)
self.resize(img.width(), img.height())
self.setCentralWidget(label)
def mousePressEvent(self, QMouseEvent):
if QMouseEvent.button() == qtc.Qt.LeftButton:
self.begin = QMouseEvent.pos()
self.end = QMouseEvent.pos()
self.update()
def mouseMoveEvent(self, QMouseEvent):
self.end = QMouseEvent.pos()
self.update()
if __name__ == '__main__':
app = qtw.QApplication(sys.argv)
w = MainWindow()
sys.exit(app.exec_())
You're grabbing from the current window, not from the desktop. While what you see is the desktop (due to the transparency), specifying a window id results in grabbing only that window without considering the background composition or any other foreign window.
If you want to grab from the screen, you need to use the root window's id, which is 0.
Also note that:
the coordinates are wrong, as you used self.end for the y coordinate;
if the user selects a negative rectangle, the result is unexpected; you should use a normalized rectangle instead (which always have positive width and height);
you should hide the widget before properly taking the screenshot, otherwise you will also capture the darkened background of the capture area;
there's no need to always replace the central widget, just use an empty QLabel and change its pixmap;
an alpha value of 0.005 is practically pointless, just make it transparent;
the capture rectangle should be cleared after the screenshot has been taken;
class MainWindow(qtw.QMainWindow):
def __init__(self, *arg, **kwargs):
super().__init__()
self.setWindowFlags(self.windowFlags() | qtc.Qt.FramelessWindowHint)
self.setAttribute(qtc.Qt.WA_TranslucentBackground)
# use a QLabel
borderWidget = qtw.QLabel(objectName='borderWidget')
self.setCentralWidget(borderWidget)
self.setStyleSheet('''
#borderWidget {{
border: 3px solid blue;
background: transparent;
}}
''')
# pointless, you're showing the window in full screen
# self.setGeometry(100, 100, 400, 300)
# variables that are required for painting must be declared *before*
# calling any show* function; while this is generally not an issue,
# as painting will actually happen "later", it's conceptually wrong
# to declare a variable after it's (possibly) required by a function.
self.captureRect = None
self.showFullScreen()
self.setCursor(qtc.Qt.CrossCursor)
# unnecessary, you've already called showFullScreen
# self.show()
def paintEvent(self, event):
if self.captureRect:
qpbox = qtg.QPainter(self)
br = qtg.QBrush(qtg.QColor(100, 10, 10, 40))
qpbox.setBrush(br)
qpbox.drawRect(self.captureRect)
def mouseReleaseEvent(self, event):
if event.button() == qtc.Qt.RightButton:
self.close()
elif event.button() == qtc.Qt.LeftButton:
self.hide()
screen = qtw.QApplication.primaryScreen()
img = screen.grabWindow(0, *self.captureRect.getRect())
self.show()
img.save('screenshot.png', 'png')
self.setStyleSheet('')
self.centralWidget().setPixmap(img)
self.captureRect = None
def mousePressEvent(self, event):
if event.button() == qtc.Qt.LeftButton:
self.begin = event.pos()
self.captureRect = qtc.QRect(self.begin, qtc.QSize())
def mouseMoveEvent(self, event):
self.captureRect = qtc.QRect(self.begin, event.pos()).normalized()
self.update()
Note that I changed the event handler argument: QMouseEvent is a class, and even though you're using the module (so the actual Qt class would be qtg.QMouseEvent), that might be confusing and risky if you eventually decide to directly import classes; besides, only class and constant names should have capitalized names, not variables or functions.

Run Method on Widget Move

I am attempting to design a label class that inherits from the PyQt5 base QLabel class that is able to track another widget. Here is the current code for my class:
class AttachedLabel(QLabel):
def __init__(self, attachedTo, *args, side="left", ** kwargs):
super().__init__(*args, **kwargs) # Run parent initialization
# Define instance variables
self.attached = attachedTo
self.side = side
# Update label position
self.updatePos()
def updatePos(self):
# Get "attached widget" position and dimensions
x = self.attached.geometry().x()
y = self.attached.geometry().y()
aWidth = self.attached.geometry().width()
aHeight = self.attached.geometry().height()
# Get own dimensions
width = self.geometry().width()
height = self.geometry().height()
if self.side == "top": # Above of attached widget
self.setGeometry(x, y-height, width, height)
elif self.side == "bottom": # Below attached widget
self.setGeometry(x, y+height+aHeight, width, height)
elif self.side == "right": # Right of attached widget
self.setGeometry(x + width + aWidth, y, width, height)
else: # Left of attached widget
self.setGeometry(x - width, y, width, height)
I want to be able to instantiate the label like so:
AttachedLabel(self.pushButton, self.centralwidget)
where self.pushButton is the widget it is supposed to be following. The issue is that I don't know how to detect when the widget moves in order to run my updatePos() function. I would ideally only update the label position when the other widget moves, but I want to refrain from havign to add extra code to the class of the widget that is being tracked. I have tried overriding the paintEvent, but that only triggers when the object itself needs to be redrawn, so it doesn't even function as a sub-optimal solution.
Is there some built-in method I can use/override to detect when the widget moves or when the screen itself is updated?
You have to use an eventFilter intersecting the QEvent::Move event and you should also track the resize through the QEvent::Resize event.
from dataclasses import dataclass, field
import random
from PyQt5 import QtCore, QtWidgets
class GeometryTracker(QtCore.QObject):
geometryChanged = QtCore.pyqtSignal()
def __init__(self, widget):
super().__init__(widget)
self._widget = widget
self.widget.installEventFilter(self)
#property
def widget(self):
return self._widget
def eventFilter(self, source, event):
if self.widget is source and event.type() in (
QtCore.QEvent.Move,
QtCore.QEvent.Resize,
):
self.geometryChanged.emit()
return super().eventFilter(source, event)
#dataclass
class TrackerManager:
widget1: field(default_factory=QtWidgets.QWidget)
widget2: field(default_factory=QtWidgets.QWidget)
alignment: QtCore.Qt.Alignment = QtCore.Qt.AlignLeft
enabled: bool = True
valid_alignments = (
QtCore.Qt.AlignLeft,
QtCore.Qt.AlignRight,
QtCore.Qt.AlignHCenter,
QtCore.Qt.AlignTop,
QtCore.Qt.AlignBottom,
QtCore.Qt.AlignVCenter,
)
def __post_init__(self):
self._traker = GeometryTracker(self.widget1)
self._traker.geometryChanged.connect(self.update)
if not any(self.alignment & flag for flag in self.valid_alignments):
raise ValueError("alignment is not valid")
def update(self):
if not self.enabled:
return
r = self.widget1.rect()
p1 = r.center()
c1 = r.center()
if self.alignment & QtCore.Qt.AlignLeft:
p1.setX(r.left())
if self.alignment & QtCore.Qt.AlignRight:
p1.setX(r.right())
if self.alignment & QtCore.Qt.AlignTop:
p1.setY(r.top())
if self.alignment & QtCore.Qt.AlignBottom:
p1.setY(r.bottom())
p2 = self.convert_position(p1)
c2 = self.convert_position(c1)
g = self.widget2.geometry()
g.moveCenter(c2)
if self.alignment & QtCore.Qt.AlignLeft:
g.moveRight(p2.x())
if self.alignment & QtCore.Qt.AlignRight:
g.moveLeft(p2.x())
if self.alignment & QtCore.Qt.AlignTop:
g.moveBottom(p2.y())
if self.alignment & QtCore.Qt.AlignBottom:
g.moveTop(p2.y())
self.widget2.setGeometry(g)
def convert_position(self, point):
gp = self.widget1.mapToGlobal(point)
if self.widget2.isWindow():
return gp
return self.widget2.parent().mapFromGlobal(gp)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.button = QtWidgets.QPushButton("Press me", self)
self.label = QtWidgets.QLabel(
"Tracker\nLabel", self, alignment=QtCore.Qt.AlignCenter
)
self.label.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents, True)
self.label.setFixedSize(200, 200)
self.label.setStyleSheet(
"background-color: salmon; border: 1px solid black; font-size: 40pt;"
)
self.resize(640, 480)
self.manager = TrackerManager(
widget1=self.button,
widget2=self.label,
alignment=QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter,
)
self.move_button()
def move_button(self):
pos = QtCore.QPoint(*random.sample(range(400), 2))
animation = QtCore.QPropertyAnimation(
targetObject=self.button,
parent=self,
propertyName=b"pos",
duration=1000,
startValue=self.button.pos(),
endValue=pos,
)
animation.finished.connect(self.move_button)
animation.start(QtCore.QAbstractAnimation.DeleteWhenStopped)
def main():
import sys
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()

Custom QPushButton inside QStyledItemDelegate

Problem:
I'm able to add a QPushButton to a QStyledItemDelegate just fine. I'm faking the button press inside the delegate's editorEvent method so when you press it an action happens. I'm having trouble getting my QPushButton's style sheet working though- It only reads the first background parameter which is "red" and doesn't change on mouse hover or press.
It's unclear how I should go about setting up button click and hover detection to make the button act like a real button on the delegate. Do I need to set up an eventFilter? Should I do this at the view level? Do I do this inside the delegate's paint method? A combination of everything?
Goals:
Mouse hover over the list time will show the button button's icon.
Mouse hover over the button will change its background color.
Mouse clicks on the button will darken the background color to show a a click happened.
I'd like to set these parameters in a style sheet if possible, but I also don't mind doing it all within a paint function. Whatever works!
Current implementation
The button widget is red with a folder icon. The items correctly change color on select and hover (I want to keep that), but the item's buttons don't change at all.
Thanks!
Here's what I've put together so far:
import sys
from PySide2 import QtCore
from PySide2 import QtGui
from PySide2 import QtWidgets
class DelegateButton(QtWidgets.QPushButton):
def __init__(self, parent=None):
super(DelegateButton, self).__init__(parent)
# self.setLayout(QHBoxLayout())
size = 50
self.setFixedSize(size, size)
self.setIcon(self.style().standardIcon(QtWidgets.QStyle.SP_DialogOpenButton))
self.setStyleSheet("""
QPushButton{
background:red;
height: 30px;
font: 12px "Roboto Thin";
border-radius: 25
}
QPushButton:hover{
background: green;
}
QPushButton:hover:pressed{
background: blue;
}
QPushButton:pressed{
background: yellow;
}
""")
class MainWindow(QtWidgets.QWidget):
def __init__(self):
super(MainWindow, self).__init__()
self.resize(300, 300)
# Model/View
entries = ['one', 'two', 'three']
model = QtGui.QStandardItemModel()
delegate = ListItemDelegate()
self.listView = QtWidgets.QListView(self)
self.listView.setModel(model)
self.listView.setItemDelegate(delegate)
for i in entries:
item = QtGui.QStandardItem(i)
model.appendRow(item)
# Layout
main_layout = QtWidgets.QVBoxLayout()
main_layout.addWidget(self.listView)
self.setLayout(main_layout)
# Connections
delegate.delegateButtonPressed.connect(self.on_delegate_button_pressed)
def on_delegate_button_pressed(self, index):
print('"{}" delegate button pressed'.format(index.data(QtCore.Qt.DisplayRole)))
class ListItemDelegate(QtWidgets.QStyledItemDelegate):
delegateButtonPressed = QtCore.Signal(QtCore.QModelIndex)
def __init__(self):
super(ListItemDelegate, self).__init__()
self.button = DelegateButton()
def sizeHint(self, option, index):
size = super(ListItemDelegate, self).sizeHint(option, index)
size.setHeight(50)
return size
def editorEvent(self, event, model, option, index):
# Launch app when launch button clicked
if event.type() == QtCore.QEvent.MouseButtonRelease:
click_pos = event.pos()
rect_button = self.rect_button
if rect_button.contains(click_pos):
self.delegateButtonPressed.emit(index)
return True
else:
return False
else:
return False
def paint(self, painter, option, index):
spacing = 10
icon_size = 40
# Item BG #########################################
painter.save()
if option.state & QtWidgets.QStyle.State_Selected:
painter.setBrush(QtGui.QColor('orange'))
elif option.state & QtWidgets.QStyle.State_MouseOver:
painter.setBrush(QtGui.QColor('black'))
else:
painter.setBrush(QtGui.QColor('purple'))
painter.drawRect(option.rect)
painter.restore()
# Item Text ########################################
rect_text = option.rect
QtWidgets.QApplication.style().drawItemText(painter, rect_text, QtCore.Qt.AlignVCenter | QtCore.Qt.AlignLeft, QtWidgets.QApplication.palette(), True, index.data(QtCore.Qt.DisplayRole))
# Custom Button ######################################
self.rect_button = QtCore.QRect(
option.rect.right() - icon_size - spacing,
option.rect.bottom() - int(option.rect.height() / 2) - int(icon_size / 2),
icon_size,
icon_size
)
option = QtWidgets.QStyleOptionButton()
option.initFrom(self.button)
option.rect = self.rect_button
# Button interactive logic
if self.button.isDown():
option.state = QtWidgets.QStyle.State_Sunken
else:
pass
if self.button.isDefault():
option.features = option.features or QtWidgets.QStyleOptionButton.DefaultButton
option.icon = self.button.icon()
option.iconSize = QtCore.QSize(30, 30)
painter.save()
self.button.style().drawControl(QtWidgets.QStyle.CE_PushButton, option, painter, self.button)
painter.restore()
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
Try:
https://doc.qt.io/qt-4.8/qstyle.html#StateFlag-enum
import sys
import PySide.QtCore as core
import PySide.QtGui as gui
QPushButton#pushButton {
background-color: yellow;
}
QPushButton#pushButton:hover {
background-color: rgb(224, 255, 0);
}
QPushButton#pushButton:pressed {
background-color: rgb(224, 0, 0);
}
Your custom QStyledItemDelegate catches the mouse event, so that it is not passed to the QListView. So in the QStyledItemDelegate.editor(Event) one simply needs to add.
if event.type() == core.QEvent.MouseButtonPress:
return False
Now the selection is recognizable in the paint()-method using option.state & gui.QStyle.State_Selected.
if __name__ == '__main__':
app = gui.QApplication(sys.argv)
app.setStyleSheet('QListView::item:hover {background: none;}')
mw = gui.QMainWindow()
model = MyListModel()
view = gui.QListView()
view.setItemDelegate(MyListDelegate(parent=view))
view.setSpacing(5)
view.setModel(model)
mw.setCentralWidget(view)
mw.show()
sys.exit(app.exec_())
class MyListDelegate(gui.QStyledItemDelegate):
w = 300
imSize = 90
pad = 5
h = imSize + 2*pad
sepX = 10
def __init__(self, parent=None):
super(MyListDelegate, self).__init__(parent)
def paint(self, painter, option, index):
mouseOver = option.state in [73985, 73729]
if option.state & QStyle.State_MouseOver::
painter.fillRect(option.rect, painter.brush())
pen = painter.pen()
painter.save()
x,y = (option.rect.x(), option.rect.y())
dataRef = index.data()
pixmap = dataRef.pixmap()
upperLabel = dataRef.upperLabel()
lowerLabel = dataRef.lowerLabel()
if mouseOver:
newPen = gui.QPen(core.Qt.green, 1, core.Qt.SolidLine)
painter.setPen(newPen)
else:
painter.setPen(pen)
painter.drawRect(x, y, self.w, self.h)
painter.setPen(pen)
x += self.pad
y += self.pad
painter.drawPixmap(x, y, pixmap)
font = painter.font()
textHeight = gui.QFontMetrics(font).height()
sX = self.imSize + self.sepX
sY = textHeight/2
font.setBold(True)
painter.setFont(font)
painter.drawText(x+sX, y-sY,
self.w-self.imSize-self.sepX, self.imSize,
core.Qt.AlignVCenter,
upperLabel)
font.setBold(False)
font.setItalic(True)
painter.setFont(font)
painter.drawText(x+sX, y+sY,
self.w-self.imSize-self.sepX, self.imSize,
core.Qt.AlignVCenter,
lowerLabel)
painter.restore()
def sizeHint(self, option, index):
return core.QSize(self.w, self.imSize+2*self.pad)
def editorEvent(self, event, model, option, index):
if event.type() == core.QEvent.MouseButtonRelease:
print 'Clicked on Item', index.row()
if event.type() == core.QEvent.MouseButtonDblClick:
print 'Double-Clicked on Item', index.row()
return True
window.button->setAutoFillBackground(false);
window.button->setAutoFillBackground(true);
window.button->setPalette(*palette_red);
Another Solution To Set CSS:
import sys
from PyQt5 import Qt as qt
class TopLabelNewProject(qt.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
layout = qt.QHBoxLayout(self)
layout.setContentsMargins(40, 0, 32, 0)
self.setLayout(layout)
self.setFixedHeight(80)
self.label = qt.QLabel("Dashboard")
layout.addWidget(self.label, alignment=qt.Qt.AlignLeft)
# self.newProjectButton = Buttons.DefaultButton("New project", self)
self.newProjectButton = qt.QPushButton("New project", self)
layout.addWidget(self.newProjectButton, alignment=qt.Qt.AlignRight)
style = '''
QWidget {
background-color: white;
}
QLabel {
font: medium Ubuntu;
font-size: 20px;
color: #006325;
}
QPushButton {
background-color: #006325;
color: white;
min-width: 70px;
max-width: 70px;
min-height: 70px;
max-height: 70px;
border-radius: 35px;
border-width: 1px;
border-color: #ae32a0;
border-style: solid;
}
QPushButton:hover {
background-color: #328930;
}
QPushButton:pressed {
background-color: #80c342;
}
'''
if __name__ == '__main__':
app = qt.QApplication(sys.argv)
app.setStyleSheet(style)
ex = TopLabelNewProject()
ex.show()
sys.exit(app.exec_())

drag and drop a Tab from a QtabBar to other QtabBar in a splitted Widget PyQt Qt

How could I archive this:
- I need to drag and drop a tab from its tabBar to other tabBar in a splitted widget?
I already subclass the QtabBar and implement the drag and drop events, i already can drag it with the right pixmap and etc, and also i can drop it into the same tabBar, but not in the other one ..
got this error in the output telling me that im not providing the right arguments, here is the code, that i simplified for make it and example, and plus a .JPG of the window.
class EsceneTest(qg.QMainWindow):
def __init__(self,parent=getMayaWindow()):
super(EsceneTest,self).__init__(parent)
#---------------------------------------------------------#
#check for open Window first
winName = windowTitle
if cmds.window(winName, exists =1):
cmds.deleteUI(winName, wnd=True)
self.setAttribute(qc.Qt.WA_DeleteOnClose)
self._initUI()
def _initUI(self):
self.setObjectName(windowObject)
self.setWindowTitle(windowTitle)
self.setMinimumWidth(450)
self.setMinimumHeight(500)
self.resize(1080, 800) # re-size the window
centralWidget = qg.QWidget()
centralWidget.setObjectName('centralWidget')
self.setCentralWidget(centralWidget)
central_layout = qg.QVBoxLayout(centralWidget)
######################
# tab container
#
self.tabWidget = qg.QTabWidget()
self.tabWidget.setAcceptDrops(True)
self.tab_layout = qg.QVBoxLayout(self.tabWidget)
central_layout.addWidget(self.tabWidget)
#######################
# TabBar
#
custom_tabbar = ColtabBar()
self.tabWidget.setTabBar(custom_tabbar)
#######################
# ViewportTab
#
tabCentral_wdg = qg.QWidget()
self.top_lyt = qg.QVBoxLayout(tabCentral_wdg)
self.tab_layout.addLayout(self.top_lyt)
fixedHBox_lyt = qg.QHBoxLayout()
self.top_lyt.addLayout(fixedHBox_lyt)
self.tabWidget.addTab(tabCentral_wdg,'- Viewport')
#######################
# Example ExtraTab
#
tabTwo_wdg = qg.QWidget()
tabTwo_wdg_lyt = qg.QHBoxLayout(tabTwo_wdg)
self.tab_layout.addLayout(tabTwo_wdg_lyt)
label = qg.QLabel(' -- This is an example -- ')
label.setStyleSheet("""
background : qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 rgb(53, 57, 60), stop:1 rgb(33, 34, 36));
border-style : none;
font-size: 40px;
font-family: Calibri;
color : rgb(200,200,100);
""")
label.setAlignment(qc.Qt.AlignVCenter | qc.Qt.AlignHCenter )
tabTwo_wdg_lyt.addWidget(label)
tab_panel_lyt = qg.QVBoxLayout(label)
self.tabWidget.addTab(tabTwo_wdg,'- ExtraExample')
############################
# Q Splitter Widget to insert the dragged Tabs
#
split = qg.QSplitter(qc.Qt.Orientation.Vertical, self)
central_layout.addWidget(split)
tab_splitted = qg.QTabWidget()
split.setLayout(qg.QVBoxLayout())
split.insertWidget(0,tab_splitted)
tabBar_2 = ColtabBar()
tab_splitted.setTabBar(tabBar_2)
tabBar_2.addTab('- Insert-Here')
#---------------------------------------------------------------------------------------------#
class ColtabBar(qg.QTabBar):
def __init__(self):
super(ColtabBar, self).__init__()
self.indexTab = None
self.setAcceptDrops(True)
##################################
# Events
def mouseMoveEvent(self, e):
if e.buttons() != qc.Qt.MiddleButton:
return
globalPos = self.mapToGlobal(e.pos())
posInTab = self.mapFromGlobal(globalPos)
self.indexTab = self.tabAt(e.pos())
tabRect = self.tabRect(self.indexTab)
pixmap = qg.QPixmap(tabRect.size())
self.render(pixmap,qc.QPoint(),qg.QRegion(tabRect))
mimeData = qc.QMimeData()
drag = qg.QDrag(self)
drag.setMimeData(mimeData)
drag.setPixmap(pixmap)
cursor = qg.QCursor(qc.Qt.OpenHandCursor)
drag.setHotSpot(e.pos() - posInTab)
drag.setDragCursor(cursor.pixmap(),qc.Qt.MoveAction)
dropAction = drag.exec_(qc.Qt.MoveAction)
def mousePressEvent(self, e):
#super(qg.QWidget).mousePressEvent(e)
if e.button() == qc.Qt.RightButton:
print('press')
if e.button() == qc.Qt.LeftButton:
globalPos = self.mapToGlobal(e.pos())
posInTab = self.mapFromGlobal(globalPos)
self.indexTab = self.tabAt(e.pos())
self.setCurrentIndex(self.indexTab)
def dragEnterEvent(self, e):
e.accept()
def dropEvent(self, e):
e.setDropAction(qc.Qt.MoveAction)
e.accept()
self.insertTab(self.indexTab, self.tabText(self.indexTab))
self.removeTab(self.indexTab)
the ColtabBar is the subclass where im doing the drag and drop events.
IMAGE - >
After many hours and have eaten many manyyyy pages of Qt today over the web, I did it in my way, now I can drag and drop tabs from one tabBar to the other and vice-versa and not just from selection the current tab, i could select every tab that I want in my tab bar and will show me the pixmap of the little tab while dragging...
Here is the code:
** EDITED **
I made it more bullet proof, I had a bug when I was using more than 2 tabs with the index, now is working better, and when I drop it in the same widget it return the event and not execute the code, plus the hovering tabs select with the right mouse button as well .. I hope this can help anybody in the future.
TABINDEX = int()
def getTabIndex(index):
global TABINDEX
if index == -1 or index == TABINDEX:
return
TABINDEX = index
print (TABINDEX)
return TABINDEX
class ColtTab(qg.QTabWidget):
def __init__(self):
super(ColtTab,self).__init__()
self.setAcceptDrops(True)
self.tabBar = self.tabBar()
self.tabBar.setMouseTracking(True)
self.setDocumentMode(True)
self.indexTab = int()
self.setMovable(True)
self.setStyleSheet(style_sheet_file)
# test for hovering and selecting tabs automatic while mouser over then - not working for now...
def eventFilter(self, obj, event):
if obj == self.tabBar:
if event.type() == qc.QEvent.MouseMove:
index=self.tabBar.tabAt(event.pos())
self.tabBar.setCurrentIndex (index)
return True
else:
return
else:
return
##################################
# Events
#
def mouseMoveEvent(self, e):
if e.buttons() != qc.Qt.MiddleButton:
return
globalPos = self.mapToGlobal(e.pos())
#print(globalPos)
tabBar = self.tabBar
#print(tabBar)
posInTab = tabBar.mapFromGlobal(globalPos)
#print(posInTab)
self.indexTab = tabBar.tabAt(e.pos())
#print(self.indexTab)
tabRect = tabBar.tabRect(self.indexTab)
#print(tabRect)
#print(tabRect.size())
pixmap = qg.QPixmap(tabRect.size())
tabBar.render(pixmap,qc.QPoint(),qg.QRegion(tabRect))
mimeData = qc.QMimeData()
drag = qg.QDrag(tabBar)
drag.setMimeData(mimeData)
drag.setPixmap(pixmap)
cursor = qg.QCursor(qc.Qt.OpenHandCursor)
drag.setHotSpot(e.pos() - posInTab)
drag.setDragCursor(cursor.pixmap(),qc.Qt.MoveAction)
dropAction = drag.exec_(qc.Qt.MoveAction)
def mousePressEvent(self, e):
if e.button() == qc.Qt.RightButton:
self.tabBar.installEventFilter(self)
print('Right button pressed')
super(ColtTab, self).mousePressEvent(e)
def dragEnterEvent(self, e):
e.accept()
if e.source().parentWidget() != self:
return
# Helper function for retrieving the Tab index into a global Var
getTabIndex(self.indexOf(self.widget(self.indexTab)))
def dragLeaveEvent(self,e):
e.accept()
def dropEvent(self, e):
if e.source().parentWidget() == self:
return
e.setDropAction(qc.Qt.MoveAction)
e.accept()
counter = self.count()
if counter == 0:
self.addTab(e.source().parentWidget().widget(TABINDEX),e.source().tabText(TABINDEX))
else:
self.insertTab(counter + 1 ,e.source().parentWidget().widget(TABINDEX),e.source().tabText(TABINDEX))
print ('Tab dropped')
def mouseReleaseEvent(self, e):
if e.button() == qc.Qt.RightButton:
print('Right button released')
self.tabBar.removeEventFilter(self)
super(ColtTab, self).mouseReleaseEvent(e)
#---------------------------------------------------------------------------------#
Pic ->
Found this thread useful. Used your solution to create a self contained generic example in PyQt5. May help someone in the future.
import sys
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
class Tabs(QTabWidget):
def __init__(self, parent):
super().__init__(parent)
self.parent = parent
self.setAcceptDrops(True)
self.tabBar = self.tabBar()
self.tabBar.setMouseTracking(True)
self.indexTab = None
self.setMovable(True)
self.addTab(QWidget(self), 'Tab One')
self.addTab(QWidget(self), 'Tab Two')
def mouseMoveEvent(self, e):
if e.buttons() != Qt.RightButton:
return
globalPos = self.mapToGlobal(e.pos())
tabBar = self.tabBar
posInTab = tabBar.mapFromGlobal(globalPos)
self.indexTab = tabBar.tabAt(e.pos())
tabRect = tabBar.tabRect(self.indexTab)
pixmap = QPixmap(tabRect.size())
tabBar.render(pixmap,QPoint(),QRegion(tabRect))
mimeData = QMimeData()
drag = QDrag(tabBar)
drag.setMimeData(mimeData)
drag.setPixmap(pixmap)
cursor = QCursor(Qt.OpenHandCursor)
drag.setHotSpot(e.pos() - posInTab)
drag.setDragCursor(cursor.pixmap(),Qt.MoveAction)
dropAction = drag.exec_(Qt.MoveAction)
def dragEnterEvent(self, e):
e.accept()
if e.source().parentWidget() != self:
return
print(self.indexOf(self.widget(self.indexTab)))
self.parent.TABINDEX = self.indexOf(self.widget(self.indexTab))
def dragLeaveEvent(self,e):
e.accept()
def dropEvent(self, e):
print(self.parent.TABINDEX)
if e.source().parentWidget() == self:
return
e.setDropAction(Qt.MoveAction)
e.accept()
counter = self.count()
if counter == 0:
self.addTab(e.source().parentWidget().widget(self.parent.TABINDEX),e.source().tabText(self.parent.TABINDEX))
else:
self.insertTab(counter + 1 ,e.source().parentWidget().widget(self.parent.TABINDEX),e.source().tabText(self.parent.TABINDEX))
class Window(QWidget):
def __init__(self):
super().__init__()
self.TABINDEX = 0
tabWidgetOne = Tabs(self)
tabWidgetTwo = Tabs(self)
layout = QHBoxLayout()
self.moveWidget = None
layout.addWidget(tabWidgetOne)
layout.addWidget(tabWidgetTwo)
self.setLayout(layout)
if __name__ == '__main__':
app = QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec_())
This is modified from someone elses code, perhaps one of the examples above.
Anyhow, it's a minimal code for tab-to-tab or tab-to-window drag / droping of tab's contents.
from PyQt5.QtWidgets import QTabWidget
from PyQt5.QtCore import Qt, QPoint, QMimeData
from PyQt5.QtGui import QPixmap, QRegion, QDrag, QCursor
class TabWidget(QTabWidget):
def __init__(self, parent=None, new=None):
super().__init__(parent)
self.setAcceptDrops(True)
self.tabBar().setMouseTracking(True)
self.setMovable(True)
if new:
TabWidget.setup(self)
def __setstate__(self, data):
self.__init__(new=False)
self.setParent(data['parent'])
for widget, tabname in data['tabs']:
self.addTab(widget, tabname)
TabWidget.setup(self)
def __getstate__(self):
data = {
'parent' : self.parent(),
'tabs' : [],
}
tab_list = data['tabs']
for k in range(self.count()):
tab_name = self.tabText(k)
widget = self.widget(k)
tab_list.append((widget, tab_name))
return data
def setup(self):
pass
def mouseMoveEvent(self, e):
if e.buttons() != Qt.RightButton:
return
globalPos = self.mapToGlobal(e.pos())
tabBar = self.tabBar()
posInTab = tabBar.mapFromGlobal(globalPos)
index = tabBar.tabAt(e.pos())
tabBar.dragged_content = self.widget(index)
tabBar.dragged_tabname = self.tabText(index)
tabRect = tabBar.tabRect(index)
pixmap = QPixmap(tabRect.size())
tabBar.render(pixmap,QPoint(),QRegion(tabRect))
mimeData = QMimeData()
drag = QDrag(tabBar)
drag.setMimeData(mimeData)
drag.setPixmap(pixmap)
cursor = QCursor(Qt.OpenHandCursor)
drag.setHotSpot(e.pos() - posInTab)
drag.setDragCursor(cursor.pixmap(),Qt.MoveAction)
drag.exec_(Qt.MoveAction)
def dragEnterEvent(self, e):
e.accept()
#self.parent().dragged_index = self.indexOf(self.widget(self.dragged_index))
def dragLeaveEvent(self,e):
e.accept()
def dropEvent(self, e):
if e.source().parentWidget() == self:
return
e.setDropAction(Qt.MoveAction)
e.accept()
tabBar = e.source()
self.addTab(tabBar.dragged_content, tabBar.dragged_tabname)
if __name__ == '__main__':
from PyQt5.QtWidgets import QWidget, QApplication, QHBoxLayout
import sys
class Window(QWidget):
def __init__(self):
super().__init__()
self.dragged_index = None
tabWidgetOne = TabWidget(self)
tabWidgetTwo = TabWidget(self)
tabWidgetOne.addTab(QWidget(), "tab1")
tabWidgetTwo.addTab(QWidget(), "tab2")
layout = QHBoxLayout()
self.moveWidget = None
layout.addWidget(tabWidgetOne)
layout.addWidget(tabWidgetTwo)
self.setLayout(layout)
app = QApplication(sys.argv)
window = Window()
window1 = Window()
window.show()
window1.show()
sys.exit(app.exec_())

Categories