I've already looked heavily to see how to properly layer my ui and haven't found out how to layer my windows so it comes off looking somewhat like this:
I want to have my background layer which I have set as a label with an image and then have a qt widget with login centered in the middle of it almost popping out at the user however when I do this it comes out with the widget behind my main window, it doesn't align properly and it also doesnt "follow" the window when I move it around image provided:
import PyQt5.QtWidgets
import sys
class LoginPanel(PyQt5.QtWidgets.QWidget):
def __init__(self):
PyQt5.QtWidgets.QWidget.__init__(self)
self.setFixedSize(600,400)
self.setWindowFlags(PyQt5.QtCore.Qt.FramelessWindowHint | PyQt5.QtCore.Qt.WindowStaysOnTopHint)
self.setStyleSheet("""
QWidget {
background-color: #CBCAB7;
border-radius: 50px;
}
""")
self.show()
class Auth(PyQt5.QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Login")
self.setFixedSize(1200,800)
self.setWindowFlags(PyQt5.QtCore.Qt.WindowCloseButtonHint | PyQt5.QtCore.Qt.WindowMinimizeButtonHint)
self.setWindowIcon(PyQt5.QtGui.QIcon("assets\\login.ico"))
self.background = PyQt5.QtWidgets.QLabel("", self)
self.layout = PyQt5.QtWidgets.QGridLayout()
self.layout.addWidget(LoginPanel(), 0, 1)
self.set_background()
self.show()
#self.layout.setAlignment(PyQt5.QtCore.Qt.AlignCenter)
def set_background(self):
img = PyQt5.QtGui.QPixmap("assets\\background.png")
pixmap = img.scaled(self.width(), self.height())
self.background.setPixmap(img)
self.background.resize(self.width(), self.height())
if __name__ == "__main__":
app = PyQt5.QtWidgets.QApplication(sys.argv)
a = Auth()
sys.exit(app.exec())
Here is my current code, I just wanted some help sense while looking online I was unable to find any great examples or references.
My recommendation is not to create a new window but to set it as a child of the window, and to raise it above any other child, you must use raise_() method, also add a QGraphicsDropShadowEffect to establish the floating window effect:
from PyQt5 import QtCore, QtGui, QtWidgets
import sys
class LoginPanel(QtWidgets.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setFixedSize(600, 400)
self.container = QtWidgets.QWidget(self)
self.container.setStyleSheet(
"""
background-color: #CBCAB7;
border-radius: 50px;
"""
)
offset = 30
self.container.setGeometry(
self.rect().adjusted(offset, offset, -offset, -offset)
)
effect = QtWidgets.QGraphicsDropShadowEffect(
blurRadius=50, offset=QtCore.QPointF(0, 0)
)
self.container.setGraphicsEffect(effect)
lay = QtWidgets.QFormLayout(self)
lay.setContentsMargins(2 * offset, 2 * offset, 2 * offset, 2 * offset)
lay.addRow("Username:", QtWidgets.QLineEdit())
lay.addRow("Email:", QtWidgets.QLineEdit())
class Auth(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Login")
self.setFixedSize(1200, 800)
self.setWindowFlags(
QtCore.Qt.WindowCloseButtonHint | QtCore.Qt.WindowMinimizeButtonHint
)
self.setWindowIcon(QtGui.QIcon("assets\\login.ico"))
self.background = QtWidgets.QLabel(self)
self.set_background()
self.panel = LoginPanel(self)
self.center_panel()
def set_background(self):
img = QtGui.QPixmap("assets\\background.png")
pixmap = img.scaled(self.size())
self.background.setPixmap(pixmap)
self.background.resize(self.size())
def resizeEvent(self, event):
super().resizeEvent(event)
self.center_panel()
def center_panel(self):
g = self.panel.geometry()
g.moveCenter(self.rect().center())
self.panel.setGeometry(g)
self.panel.raise_()
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
a = Auth()
a.show()
sys.exit(app.exec())
Related
I want Qt.TextWrapAnywhere for my QLabel in a Layout.
I followed This instruction.My code is also same to give a minimal code
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPainter
from PyQt5.QtWidgets import QApplication, QLabel, QMainWindow, QStyleOption, QVBoxLayout, QWidget, QStyle
class SuperQLabel(QLabel):
def __init__(self, *args, **kwargs):
super(SuperQLabel, self).__init__(*args, **kwargs)
self.textalignment = Qt.AlignLeft | Qt.TextWrapAnywhere
self.isTextLabel = True
self.align = None
def paintEvent(self, event):
opt = QStyleOption()
opt.initFrom(self)
painter = QPainter(self)
self.style().drawPrimitive(QStyle.PE_Widget, opt, painter, self)
self.style().drawItemText(painter, self.rect(),
self.textalignment, self.palette(), True, self.text())
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
self.resize(100, 200)
self.label = QLabel()
self.label.setWordWrap(True)
self.label.setText("11111111111111111111\n2222222211111111")
self.slabel = SuperQLabel()
self.slabel.setMinimumWidth(10)
self.slabel.setText("111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111")
self.centralwidget = QWidget()
self.setCentralWidget(self.centralwidget)
self.mainlayout = QVBoxLayout()
self.mainlayout.addWidget(self.label)
self.mainlayout.addWidget(self.slabel)
self.centralwidget.setLayout(self.mainlayout)
if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
I changed little bit that code self.slabel.setMinimumWidth(10) otherwise resizing Label according to width wont work.
It is perfectly wrapping the text according to width.But the Problem is when height is considered self.label = QLabel() Normal QLabel auto adjust height according to content with layout.
For example if i add one \n with text that means Qlabel must show 2 lines.
But with this new Custom Label e.g.self.slabel = SuperQLabel() wrapping is good as long as there is space for height in layout.
I think i have to use setminimumHeight() but dont know how to get proper height after custom wrapping.
As long as the label is shown in a scroll area (which will not create issues with the top level layout), a better solution is to use a QTextEdit subclass, with the following configuration:
readOnly must be True;
scroll bars are disabled;
the vertical size policy must be Preferred (and not Expanding);
both minimumSizeHint() and sizeHint() should use the internal QTextDocument to return a proper height, with a minimum default width;
any change in size or contents must trigger updateGeometry() so that the parent layout will know that the hint has changed and geometries could be computed again;
the hint must include possible decorations of the scroll area (which is a QFrame);
This allows avoiding the paintEvent() override, and provide a better and easier implementation of the size mechanism due to the features provided by QTextDocument, while minimizing the possibility of recursion to an acceptable level.
class WrapLabel(QtWidgets.QTextEdit):
def __init__(self, text=''):
super().__init__(text)
self.setStyleSheet('''
WrapLabel {
border: 1px outset palette(dark);
border-radius: 8px;
background: palette(light);
}
''')
self.setReadOnly(True)
self.setSizePolicy(QtWidgets.QSizePolicy.Preferred,
QtWidgets.QSizePolicy.Maximum)
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.textChanged.connect(self.updateGeometry)
def minimumSizeHint(self):
doc = self.document().clone()
doc.setTextWidth(self.viewport().width())
height = doc.size().height()
height += self.frameWidth() * 2
return QtCore.QSize(50, height)
def sizeHint(self):
return self.minimumSizeHint()
def resizeEvent(self, event):
super().resizeEvent(event)
self.updateGeometry()
class ChatTest(QtWidgets.QScrollArea):
def __init__(self):
super().__init__()
self.messages = []
container = QtWidgets.QWidget()
self.setWidget(container)
self.setWidgetResizable(True)
layout = QtWidgets.QVBoxLayout(container)
layout.addStretch()
self.resize(480, 360)
for i in range(1, 11):
QtCore.QTimer.singleShot(1000 * i, lambda:
self.addMessage('1' * randrange(100, 250)))
def addMessage(self, text):
self.widget().layout().addWidget(WrapLabel(text))
QtCore.QTimer.singleShot(0, self.scrollToBottom)
def scrollToBottom(self):
QtWidgets.QApplication.processEvents()
self.verticalScrollBar().setValue(
self.verticalScrollBar().maximum())
Update: HTML and QTextDocument
When using setHtml() and setDocument(), the source could have pre formatted text that doesn't allow wrapping. To avoid that, it's necessary to iterate through all QTextBlocks of the document, get their QTextBlockFormat, check the nonBreakableLines() property and eventually set it to False and set the format back with a QTextCursor.
class WrapLabel(QtWidgets.QTextEdit):
def __init__(self, text=None):
super().__init__()
if isinstance(text, str):
if Qt.mightBeRichText(text):
self.setHtml(text)
else:
self.setPlainText(text)
elif isinstance(text, QtGui.QTextDocument):
self.setDocument(text)
# ...
def setHtml(self, html):
doc = QtGui.QTextDocument()
doc.setHtml(html)
self.setDocument(doc)
def setDocument(self, doc):
doc = doc.clone()
tb = doc.begin() # start a QTextBlock iterator
while tb.isValid():
fmt = tb.blockFormat()
if fmt.nonBreakableLines():
fmt.setNonBreakableLines(False)
# create a QTextCursor for the current text block,
# then set the updated format to override the wrap
tc = QtGui.QTextCursor(tb)
tc.setBlockFormat(fmt)
tb = tb.next()
super().setDocument(doc)
Be aware, though, that this could not be enough whenever objects with predefined or minimum width are used: images and tables. The result will be that if the object is larger than the available space, it will be cropped on its right (or left for RightToLeft text layouts).
After Some Research,I successfully fixed it.
There is a trick
This is Full Responsive With/Without Emoji😅
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPainter,QFontMetrics,QFont
from PyQt5.QtWidgets import QApplication, QLabel, QMainWindow, QStyleOption, QVBoxLayout, QWidget, QStyle
import math
class SuperQLabel(QLabel):
def __init__(self, *args, **kwargs):
super(SuperQLabel, self).__init__(*args, **kwargs)
self.textalignment = Qt.AlignLeft | Qt.TextWrapAnywhere
self.isTextLabel = True
self.align = None
def paintEvent(self, event):
opt = QStyleOption()
opt.initFrom(self)
painter = QPainter(self)
self.style().drawPrimitive(QStyle.PE_Widget, opt, painter, self)
self.style().drawItemText(painter, self.rect(),
self.textalignment, self.palette(), True, self.text())
fm=QFontMetrics(self.font())
#To get unicode in Text if using Emoji(Optional)
string_unicode = self.text().encode("unicode_escape").decode()
##To remove emoji/unicode from text while calculating
string_encode = self.text().encode("ascii", "ignore")
string_decode = string_encode.decode()
#If Unicode/Emoji is Used
if string_unicode.count("\\U0001") > 0:
height=fm.boundingRect(self.rect(),Qt.TextWordWrap,string_decode).height()+1
# +1 is varrying according to Different font .SO set different value and test.
else:
height=fm.boundingRect(self.rect(),Qt.TextWordWrap,string_decode).height()
row=math.ceil(fm.horizontalAdvance(self.text())/self.width())
self.setMinimumHeight(row*height)
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
self.resize(100, 200)
self.label = QLabel()
self.label.setWordWrap(True)
self.label.setStyleSheet("background:red;")
self.label.setText("11111111111111111111\n2222222211111111")
self.emoji_font = QFont("Segoe UI Emoji",15,0,False)
self.emoji_font.setBold(True)
self.slabel = SuperQLabel()
self.slabel.setMinimumWidth(10)
self.slabel.setStyleSheet("background:green;")
self.slabel.setFont(self.emoji_font)
########### Plain Text ######################
# self.slabel.setText("111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111")
#################### Or Using Emoji ############
self.slabel.setText("111111111111111111😉111ABCDDWAEQQ111111111111😅1111111111😉111111wqewqgdfgdfhyhtyhy1111111😅111111111111😉1111111111111111111")
self.centralwidget = QWidget()
self.setCentralWidget(self.centralwidget)
self.mainlayout = QVBoxLayout()
self.mainlayout.addWidget(self.label)
self.mainlayout.addWidget(self.slabel)
self.centralwidget.setLayout(self.mainlayout)
if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
Starting the program, the QIcon is aligned on the left (it's standard i guess) with the text right to it.
Instead I want the icon to be centered on top with the text below it.
I tried using setStyleSheet with show_all.setStyleSheet("QIcon { vertical-align: top }") and show_all.setStyleSheet("QPushButton { text-align: bottom }").
How can I achieve this?
QPushButton doesn't allow to choose the layout of its icon and label. Also, remember that while Qt features style sheets to style widgets, not all CSS known properties and selectors are available. Furthermore, style sheets only work on widgets, so using the QIcon selector isn't supported, since QIcon is not a QWidget subclass.
The most simple solution is to use a QToolButton and set the toolButtonStyle correctly:
self.someButton = QtWidgets.QToolButton()
# ...
self.someButton.setToolButtonStyle(QtCore.Qt.ToolButtonTextUnderIcon)
The alternative is to subclass the button, provide a customized paint method and reimplement both sizeHint() and paintEvent(); the first is to ensure that the button is able to resize itself whenever required, while the second is to paint the button control (without text!) and then paint both the icon and the text.
Here's a possible implementation:
from PyQt5 import QtCore, QtGui, QtWidgets
class CustomButton(QtWidgets.QPushButton):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._icon = self.icon()
if not self._icon.isNull():
super().setIcon(QtGui.QIcon())
def sizeHint(self):
hint = super().sizeHint()
if not self.text() or self._icon.isNull():
return hint
style = self.style()
opt = QtWidgets.QStyleOptionButton()
self.initStyleOption(opt)
margin = style.pixelMetric(style.PM_ButtonMargin, opt, self)
spacing = style.pixelMetric(style.PM_LayoutVerticalSpacing, opt, self)
# get the possible rect required for the current label
labelRect = self.fontMetrics().boundingRect(
0, 0, 5000, 5000, QtCore.Qt.TextShowMnemonic, self.text())
iconHeight = self.iconSize().height()
height = iconHeight + spacing + labelRect.height() + margin * 2
if height > hint.height():
hint.setHeight(height)
return hint
def setIcon(self, icon):
# setting an icon might change the horizontal hint, so we need to use a
# "local" reference for the actual icon and go on by letting Qt to *think*
# that it doesn't have an icon;
if icon == self._icon:
return
self._icon = icon
self.updateGeometry()
def paintEvent(self, event):
if self._icon.isNull() or not self.text():
super().paintEvent(event)
return
opt = QtWidgets.QStyleOptionButton()
self.initStyleOption(opt)
opt.text = ''
qp = QtWidgets.QStylePainter(self)
# draw the button without any text or icon
qp.drawControl(QtWidgets.QStyle.CE_PushButton, opt)
rect = self.rect()
style = self.style()
margin = style.pixelMetric(style.PM_ButtonMargin, opt, self)
iconSize = self.iconSize()
iconRect = QtCore.QRect((rect.width() - iconSize.width()) / 2, margin,
iconSize.width(), iconSize.height())
if self.underMouse():
state = QtGui.QIcon.Active
elif self.isEnabled():
state = QtGui.QIcon.Normal
else:
state = QtGui.QIcon.Disabled
qp.drawPixmap(iconRect, self._icon.pixmap(iconSize, state))
spacing = style.pixelMetric(style.PM_LayoutVerticalSpacing, opt, self)
labelRect = QtCore.QRect(rect)
labelRect.setTop(iconRect.bottom() + spacing)
qp.drawText(labelRect,
QtCore.Qt.TextShowMnemonic|QtCore.Qt.AlignHCenter|QtCore.Qt.AlignTop,
self.text())
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
w = CustomButton('Alles anzeigen', icon=QtGui.QIcon.fromTheme('document-new'))
w.setIconSize(QtCore.QSize(32, 32))
w.show()
sys.exit(app.exec_())
Alternatively, try it:
import sys
from PyQt5.QtGui import QIcon
from PyQt5.QtCore import Qt, QSize
from PyQt5.QtWidgets import (QApplication, QWidget, QGridLayout,
QToolBar, QAction)
class Widget(QWidget):
def __init__(self, parent=None):
super(Widget, self).__init__(parent)
add_action = QAction(QIcon("img/add.png"), "Add", self)
add_action.triggered.connect(self.addValue)
sub_action = QAction(QIcon("img/min.png"), "Sub", self)
sub_action.triggered.connect(self.subValue)
toolbar = QToolBar()
toolbar.setContentsMargins(0, 0, 0, 0)
toolbar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon | Qt.AlignLeading)
toolbar.setIconSize(QSize(50, 50))
toolbar.addAction(add_action)
toolbar.addAction(sub_action)
rootGrid = QGridLayout(self)
rootGrid.addWidget(toolbar)
def addValue(self):
print("def addValue:")
def subValue(self):
print("def subValue:")
if __name__ == '__main__':
app = QApplication(sys.argv)
main = Widget()
main.show()
sys.exit(app.exec_())
There have been similar questions asked about overriding the QCompleter popup position but i'll still not found a working solution. I simply want to move the popup down around 5px (I have some specific styling requirements)
I've tried subclassing a QListView and using that as my popup using setPopup(). I then override the showEvent and move the popup down in Y. I also do this on the resizeEvent since I believe this is triggered when items are filtered and the popup resizes. However this doesn't work.. I then used a singleshot timer to trigger the move after 1ms. This does kind of work but it seems quite inconsistent - the first time it shows is different to subsequent times or resizing.
Below is my latest attempt (trying to hack it by counting the number of popups..), hopefully someone can show me what i'm doing wrong or a better solution
import sys
import os
from PySide2 import QtCore, QtWidgets, QtGui
class QPopup(QtWidgets.QListView):
def __init__(self, parent=None):
super(QPopup, self).__init__(parent)
self.popups = 0
def offset(self):
y = 3 if self.popups < 2 else 7
print('y: {}'.format(y))
self.move(self.pos().x(), self.pos().y() + y)
self.popups += 1
def showEvent(self, event):
print('show')
# self.offset()
QtCore.QTimer.singleShot(1, self.offset)
def resizeEvent(self, event):
print('resize')
# self.offset()
QtCore.QTimer.singleShot(1, self.offset)
class MyDialog(QtWidgets.QDialog):
def __init__(self, parent=None):
super(MyDialog, self).__init__(parent)
self.create_widgets()
self.create_layout()
self.create_connections()
def create_widgets(self):
self.le = QtWidgets.QLineEdit('')
self.completer = QtWidgets.QCompleter(self)
self.completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
self.completer.setCompletionMode(QtWidgets.QCompleter.PopupCompletion)
self.completer.setMaxVisibleItems(10)
self.completer.setFilterMode(QtCore.Qt.MatchContains)
self.completer.setPopup(QPopup())
popup = QPopup(self)
self.completer.setPopup(popup)
self.model = QtCore.QStringListModel()
self.completer.setModel(self.model)
self.le.setCompleter(self.completer)
self.completer.model().setStringList(['one','two','three'])
def create_layout(self):
main_layout = QtWidgets.QVBoxLayout(self)
main_layout.addWidget(self.le)
def create_connections(self):
pass
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
my_dialog = MyDialog()
my_dialog.show() # Show the UI
sys.exit(app.exec_())
One solution could be to make a subclass of QLineEdit and override keyPressEvent to display the popup with an offset:
PySide2.QtWidgets.QCompleter.complete([rect=QRect()])
For PopupCompletion and QCompletion::UnfilteredPopupCompletion modes, calling this function displays the popup displaying the current completions. By default, if rect is not specified, the popup is displayed on the bottom of the widget() . If rect is specified the popup is displayed on the left edge of the rectangle.
see doc.qt.io -> QCompleter.complete.
Complete, self-contained example
The rect is calculated based on the y-position of the cursor rect. The height of the popup window is not changed. The width is adjusted to the width of the ZLineEdit widget.
rect = QtCore.QRect(0,
self.cursorRect().y() + 4,
self.width(),
self.completer().widget().height())
Your code, slightly modified using the points mentioned above, could look like this:
import sys
from PySide2 import QtCore, QtWidgets
from PySide2.QtWidgets import QLineEdit, QDialog, QCompleter
class ZLineEdit(QLineEdit):
def __init__(self, string, parent=None):
super().__init__(string, parent)
def keyPressEvent(self, event):
super().keyPressEvent(event)
if len(self.text()) > 0:
rect = QtCore.QRect(0,
self.cursorRect().y() + 4,
self.width(),
self.completer().widget().height())
self.completer().complete(rect)
class MyDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.le = ZLineEdit('')
autoList = ['one', 'two', 'three']
self.completer = QCompleter(autoList, self)
self.setup_widgets()
self.create_layout()
self.create_connections()
def setup_widgets(self):
self.completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
self.completer.setCompletionMode(QtWidgets.QCompleter.PopupCompletion)
self.completer.setMaxVisibleItems(10)
self.completer.setFilterMode(QtCore.Qt.MatchContains)
self.le.setCompleter(self.completer)
def create_layout(self):
main_layout = QtWidgets.QVBoxLayout(self)
main_layout.addWidget(self.le)
def create_connections(self):
pass
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
my_dialog = MyDialog()
my_dialog.show()
sys.exit(app.exec_())
Test
On the left side you see the default behavior. On the right side the popup is moved down 4px:
I am struggling to add a side menu to my application.
I have a QMainWindow instance to which I was hoping to add a QDrawer object and achieve an effect similar to this sample.
Unfortunately, it seems that PySide2 only provides QMenu, QTooltip and QDialog widgets which inherit from the Popup class, and QDrawer is nowhere to be found. However, using a Drawer tag in a QML file works just fine. Shouldn't it be possible to also create an instance of QDrawer programmatically?
As another try, I tried to load a Drawer instance from a QML file and attach it to my QMainWindow. Unfortunately I can't quite understand what should I specify as parent, what should I wrap it in, what parameters should I use etc. - any advice would be appreciated (although I would much rather create and configure it programatically).
My goal is to create a QMainWindow with a toolbar, central widget and a QDrawer instance as a side navigation menu (such as in this sample). Can you please share some examples or explain what to do?
One possible solution is to implement a Drawer using Qt Widgets, the main feature is to animate the change of width for example using a QXAnimation, the other task is to set the anchors so that it occupies the necessary height. A simple example is the one shown in the following code:
import os
from PySide2 import QtCore, QtGui, QtWidgets
class Drawer(QtWidgets.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setFixedWidth(0)
self.setContentsMargins(0, 0, 0, 0)
# self.setFixedWidth(0)
self._maximum_width = 0
self._animation = QtCore.QPropertyAnimation(self, b"width")
self._animation.setStartValue(0)
self._animation.setDuration(1000)
self._animation.valueChanged.connect(self.setFixedWidth)
self.hide()
#property
def maximum_width(self):
return self._maximum_width
#maximum_width.setter
def maximum_width(self, w):
self._maximum_width = w
self._animation.setEndValue(self.maximum_width)
def open(self):
self._animation.setDirection(QtCore.QAbstractAnimation.Forward)
self._animation.start()
self.show()
def close(self):
self._animation.setDirection(QtCore.QAbstractAnimation.Backward)
self._animation.start()
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowFlag(QtCore.Qt.FramelessWindowHint)
central_widget = QtWidgets.QWidget()
self.setCentralWidget(central_widget)
self.tool_button = QtWidgets.QToolButton(
checkable=True, iconSize=QtCore.QSize(36, 36)
)
content_widget = QtWidgets.QLabel(alignment=QtCore.Qt.AlignCenter)
content_widget.setText("Content")
content_widget.setStyleSheet("background-color: green")
lay = QtWidgets.QVBoxLayout(central_widget)
lay.setSpacing(0)
lay.setContentsMargins(0, 0, 0, 0)
lay.addWidget(self.tool_button)
lay.addWidget(content_widget)
self.resize(640, 480)
self.drawer = Drawer(self)
self.drawer.move(0, self.tool_button.sizeHint().height())
self.drawer.maximum_width = 200
self.drawer.raise_()
content_lay = QtWidgets.QVBoxLayout()
content_lay.setContentsMargins(0, 0, 0, 0)
label = QtWidgets.QLabel(alignment=QtCore.Qt.AlignCenter)
label.setText("Content\nDrawer")
label.setStyleSheet("background-color: red")
content_lay.addWidget(label)
self.drawer.setLayout(content_lay)
self.tool_button.toggled.connect(self.onToggled)
self.onToggled(self.tool_button.isChecked())
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.onCustomContextMenuRequested)
#QtCore.Slot()
def onCustomContextMenuRequested(self):
menu = QtWidgets.QMenu()
quit_action = menu.addAction(self.tr("Close"))
action = menu.exec_(QtGui.QCursor.pos())
if action == quit_action:
self.close()
#QtCore.Slot(bool)
def onToggled(self, checked):
if checked:
self.tool_button.setIcon(
self.style().standardIcon(QtWidgets.QStyle.SP_MediaStop)
)
self.drawer.open()
else:
self.tool_button.setIcon(
self.style().standardIcon(QtWidgets.QStyle.SP_MediaPlay)
)
self.drawer.close()
def resizeEvent(self, event):
self.drawer.setFixedHeight(self.height() - self.drawer.pos().y())
super().resizeEvent(event)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
I'm working on an opensource markdown supported minimal note taking application for Windows/Linux. I'm trying to remove the title bar and add my own buttons. I want something like, a title bar with only two custom buttons as shown in the figure
Currently I have this:
I've tried modifying the window flags:
With not window flags, the window is both re-sizable and movable. But no custom buttons.
Using self.setWindowFlags(QtCore.Qt.FramelessWindowHint), the window has no borders, but cant move or resize the window
Using self.setWindowFlags(QtCore.Qt.CustomizeWindowHint), the window is resizable but cannot move and also cant get rid of the white part at the top of the window.
Any help appreciated. You can find the project on GitHub here.
Thanks..
This is my python code:
from PyQt5 import QtCore, QtWidgets, QtWebEngineWidgets, uic
import sys
import os
import markdown2 # https://github.com/trentm/python-markdown2
from PyQt5.QtCore import QRect
from PyQt5.QtGui import QFont
simpleUiForm = uic.loadUiType("Simple.ui")[0]
class SimpleWindow(QtWidgets.QMainWindow, simpleUiForm):
def __init__(self, parent=None):
QtWidgets.QMainWindow.__init__(self, parent)
self.setupUi(self)
self.markdown = markdown2.Markdown()
self.css = open(os.path.join("css", "default.css")).read()
self.editNote.setPlainText("")
#self.noteView = QtWebEngineWidgets.QWebEngineView(self)
self.installEventFilter(self)
self.displayNote.setContextMenuPolicy(QtCore.Qt.NoContextMenu)
#self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
def eventFilter(self, object, event):
if event.type() == QtCore.QEvent.WindowActivate:
print("widget window has gained focus")
self.editNote.show()
self.displayNote.hide()
elif event.type() == QtCore.QEvent.WindowDeactivate:
print("widget window has lost focus")
note = self.editNote.toPlainText()
htmlNote = self.getStyledPage(note)
# print(note)
self.editNote.hide()
self.displayNote.show()
# print(htmlNote)
self.displayNote.setHtml(htmlNote)
elif event.type() == QtCore.QEvent.FocusIn:
print("widget has gained keyboard focus")
elif event.type() == QtCore.QEvent.FocusOut:
print("widget has lost keyboard focus")
return False
The UI file is created in the following hierarchy
Here are the steps you just gotta follow:
Have your MainWindow, be it a QMainWindow, or QWidget, or whatever [widget] you want to inherit.
Set its flag, self.setWindowFlags(Qt.FramelessWindowHint)
Implement your own moving around.
Implement your own buttons (close, max, min)
Implement your own resize.
Here is a small example with move around, and buttons implemented. You should still have to implement the resize using the same logic.
import sys
from PyQt5.QtCore import QPoint
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWidgets import QHBoxLayout
from PyQt5.QtWidgets import QLabel
from PyQt5.QtWidgets import QPushButton
from PyQt5.QtWidgets import QVBoxLayout
from PyQt5.QtWidgets import QWidget
class MainWindow(QWidget):
def __init__(self):
super(MainWindow, self).__init__()
self.layout = QVBoxLayout()
self.layout.addWidget(MyBar(self))
self.setLayout(self.layout)
self.layout.setContentsMargins(0,0,0,0)
self.layout.addStretch(-1)
self.setMinimumSize(800,400)
self.setWindowFlags(Qt.FramelessWindowHint)
self.pressing = False
class MyBar(QWidget):
def __init__(self, parent):
super(MyBar, self).__init__()
self.parent = parent
print(self.parent.width())
self.layout = QHBoxLayout()
self.layout.setContentsMargins(0,0,0,0)
self.title = QLabel("My Own Bar")
btn_size = 35
self.btn_close = QPushButton("x")
self.btn_close.clicked.connect(self.btn_close_clicked)
self.btn_close.setFixedSize(btn_size,btn_size)
self.btn_close.setStyleSheet("background-color: red;")
self.btn_min = QPushButton("-")
self.btn_min.clicked.connect(self.btn_min_clicked)
self.btn_min.setFixedSize(btn_size, btn_size)
self.btn_min.setStyleSheet("background-color: gray;")
self.btn_max = QPushButton("+")
self.btn_max.clicked.connect(self.btn_max_clicked)
self.btn_max.setFixedSize(btn_size, btn_size)
self.btn_max.setStyleSheet("background-color: gray;")
self.title.setFixedHeight(35)
self.title.setAlignment(Qt.AlignCenter)
self.layout.addWidget(self.title)
self.layout.addWidget(self.btn_min)
self.layout.addWidget(self.btn_max)
self.layout.addWidget(self.btn_close)
self.title.setStyleSheet("""
background-color: black;
color: white;
""")
self.setLayout(self.layout)
self.start = QPoint(0, 0)
self.pressing = False
def resizeEvent(self, QResizeEvent):
super(MyBar, 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.setGeometry(self.mapToGlobal(self.movement).x(),
self.mapToGlobal(self.movement).y(),
self.parent.width(),
self.parent.height())
self.start = self.end
def mouseReleaseEvent(self, QMouseEvent):
self.pressing = False
def btn_close_clicked(self):
self.parent.close()
def btn_max_clicked(self):
self.parent.showMaximized()
def btn_min_clicked(self):
self.parent.showMinimized()
if __name__ == "__main__":
app = QApplication(sys.argv)
mw = MainWindow()
mw.show()
sys.exit(app.exec_())
Here are some tips:
Option 1:
Have a QGridLayout with widget in each corner and side(e.g. left, top-left, menubar, top-right, right, bottom-right, bottom and bottom left)
With the approach (1) you would know when you are clicking in each border, you just got to define each one size and add each one on their place.
When you click on each one treat them in their respective ways, for example, if you click in the left one and drag to the left, you gotta resize it larger and at the same time move it to the left so it will appear to be stopped at the right place and grow width.
Apply this reasoning to each edge, each one behaving in the way it has to.
Option 2:
Instead of having a QGridLayout you can detect in which place you are clicking by the click pos.
Verify if the x of the click is smaller than the x of the moving pos to know if it's moving left or right and where it's being clicked.
The calculation is made in the same way of the Option1
Option 3:
Probably there are other ways, but those are the ones I just thought of. For example using the CustomizeWindowHint you said you are able to resize, so you just would have to implement what I gave you as example. BEAUTIFUL!
Tips:
Be careful with the localPos(inside own widget), globalPos(related to your screen). For example: If you click in the very left of your left widget its 'x' will be zero, if you click in the very left of the middle(content)it will be also zero, although if you mapToGlobal you will having different values according to the pos of the screen.
Pay attention when resizing, or moving, when you have to add width or subtract, or just move, or both, I'd recommend you to draw on a paper and figure out how the logic of resizing works before implementing it out of blue.
GOOD LUCK :D
While the accepted answer can be considered valid, it has some issues.
using setGeometry() is not appropriate (and the reason for using it was wrong) since it doesn't consider possible frame margins set by the style;
the position computation is unnecessarily complex;
resizing the title bar to the total width is wrong, since it doesn't consider the buttons and can also cause recursion problems in certain situations (like not setting the minimum size of the main window); also, if the title is too big, it makes impossible to resize the main window;
buttons should not accept focus;
setting a layout creates a restraint for the "main widget" or layout, so the title should not be added, but the contents margins of the widget should be used instead;
I revised the code to provide a better base for the main window, simplify the moving code, and add other features like the Qt windowTitle() property support, standard QStyle icons for buttons (instead of text), and proper maximize/normal button icons. Note that the title label is not added to the layout.
class MainWindow(QWidget):
def __init__(self):
super(MainWindow, self).__init__()
self.setWindowFlags(self.windowFlags() | Qt.FramelessWindowHint)
self.titleBar = MyBar(self)
self.setContentsMargins(0, self.titleBar.height(), 0, 0)
self.resize(640, self.titleBar.height() + 480)
def changeEvent(self, event):
if event.type() == event.WindowStateChange:
self.titleBar.windowStateChanged(self.windowState())
def resizeEvent(self, event):
self.titleBar.resize(self.width(), self.titleBar.height())
class MyBar(QWidget):
clickPos = None
def __init__(self, parent):
super(MyBar, self).__init__(parent)
self.setAutoFillBackground(True)
self.setBackgroundRole(QPalette.Shadow)
# alternatively:
# palette = self.palette()
# palette.setColor(palette.Window, Qt.black)
# palette.setColor(palette.WindowText, Qt.white)
# self.setPalette(palette)
layout = QHBoxLayout(self)
layout.setContentsMargins(1, 1, 1, 1)
layout.addStretch()
self.title = QLabel("My Own Bar", self, alignment=Qt.AlignCenter)
# if setPalette() was used above, this is not required
self.title.setForegroundRole(QPalette.Light)
style = self.style()
ref_size = self.fontMetrics().height()
ref_size += style.pixelMetric(style.PM_ButtonMargin) * 2
self.setMaximumHeight(ref_size + 2)
btn_size = QSize(ref_size, ref_size)
for target in ('min', 'normal', 'max', 'close'):
btn = QToolButton(self, focusPolicy=Qt.NoFocus)
layout.addWidget(btn)
btn.setFixedSize(btn_size)
iconType = getattr(style,
'SP_TitleBar{}Button'.format(target.capitalize()))
btn.setIcon(style.standardIcon(iconType))
if target == 'close':
colorNormal = 'red'
colorHover = 'orangered'
else:
colorNormal = 'palette(mid)'
colorHover = 'palette(light)'
btn.setStyleSheet('''
QToolButton {{
background-color: {};
}}
QToolButton:hover {{
background-color: {}
}}
'''.format(colorNormal, colorHover))
signal = getattr(self, target + 'Clicked')
btn.clicked.connect(signal)
setattr(self, target + 'Button', btn)
self.normalButton.hide()
self.updateTitle(parent.windowTitle())
parent.windowTitleChanged.connect(self.updateTitle)
def updateTitle(self, title=None):
if title is None:
title = self.window().windowTitle()
width = self.title.width()
width -= self.style().pixelMetric(QStyle.PM_LayoutHorizontalSpacing) * 2
self.title.setText(self.fontMetrics().elidedText(
title, Qt.ElideRight, width))
def windowStateChanged(self, state):
self.normalButton.setVisible(state == Qt.WindowMaximized)
self.maxButton.setVisible(state != Qt.WindowMaximized)
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
self.clickPos = event.windowPos().toPoint()
def mouseMoveEvent(self, event):
if self.clickPos is not None:
self.window().move(event.globalPos() - self.clickPos)
def mouseReleaseEvent(self, QMouseEvent):
self.clickPos = None
def closeClicked(self):
self.window().close()
def maxClicked(self):
self.window().showMaximized()
def normalClicked(self):
self.window().showNormal()
def minClicked(self):
self.window().showMinimized()
def resizeEvent(self, event):
self.title.resize(self.minButton.x(), self.height())
self.updateTitle()
if __name__ == "__main__":
app = QApplication(sys.argv)
mw = MainWindow()
layout = QVBoxLayout(mw)
widget = QTextEdit()
layout.addWidget(widget)
mw.show()
mw.setWindowTitle('My custom window with a very, very long title')
sys.exit(app.exec_())
This is for the people who are going to implement custom title bar in PyQt6 or PySide6
The below changes should be done in the answer given by #musicamante
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
# self.clickPos = event.windowPos().toPoint()
self.clickPos = event.scenePosition().toPoint()
def mouseMoveEvent(self, event):
if self.clickPos is not None:
# self.window().move(event.globalPos() - self.clickPos)
self.window().move(event.globalPosition().toPoint() - self.clickPos)
if __name__ == "__main__":
app = QApplication(sys.argv)
mw = MainWindow()
mw.show()
# sys.exit(app.exec_())
sys.exit(app.exec())
References:
QMouseEvent.globalPosition(),
QMouseEvent.scenePosition()
This method of moving Windows with Custom Widget doesn't work with WAYLAND. If anybody has a solution for that please post it here for future reference
Working functions for WAYLAND and PyQT6/PySide6 :
def mousePressEvent(self, event):
if event.button() == Qt.MouseButton.LeftButton:
self._move()
return super().mousePressEvent(event)
def _move(self):
window = self.window().windowHandle()
window.startSystemMove()
Please check.