繁体   English   中英

带有框架的自定义标题栏 PyQt5

[英]Custom Titlebar with frame in PyQt5

我正在为 Windows/Linux 开发一个开源 markdown 支持的最小笔记记录应用程序。 我正在尝试删除标题栏并添加我自己的按钮。 我想要一个只有两个自定义按钮的标题栏,如图所示在此处输入图像描述

目前我有这个:

在此处输入图像描述

我试过修改 window 标志:

  • 没有 window 标志,window 既可以调整大小也可以移动。 但是没有自定义按钮。
  • 使用self.setWindowFlags(QtCore.Qt.FramelessWindowHint) , window 没有边框,但无法移动或调整 window 的大小
  • 使用self.setWindowFlags(QtCore.Qt.CustomizeWindowHint) ,window 可以调整大小但不能移动,也无法摆脱 window 顶部的白色部分。

任何帮助表示赞赏。 您可以在此处找到有关 GitHub 的项目。

谢谢..

这是我的 python 代码:

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

UI 文件在以下层次结构中创建

在此处输入图像描述

以下是您必须遵循的步骤:

  1. 拥有您的 MainWindow,无论是 QMainWindow 还是 QWidget,或者您想要继承的任何 [widget]。
  2. 设置它的标志,self.setWindowFlags(Qt.FramelessWindowHint)
  3. 实施自己的走动。
  4. 实现您自己的按钮(关闭、最大、最小)
  5. 实现您自己的调整大小。

这是一个带有移动和按钮实现的小示例。 您仍然应该使用相同的逻辑来实现调整大小。

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_())

以下是一些提示:

选项1:

  1. 在每个角落和侧面都有一个带有小部件的 QGridLayout(例如左侧,左上角,菜单栏,右上角,右下角,右下角,左下角和左下角)
  2. 使用方法 (1),您会知道当您单击每个边框时,您只需定义每个尺寸并将每个尺寸添加到它们的位置。
  3. 当你点击每一个时,以各自的方式对待它们,例如,如果你点击左边的一个并向左拖动,你必须把它放大,同时向左移动它,这样它就会看起来像停在正确的地方并增加宽度。
  4. 将此推理应用于每条边,每条边都按其必须的方式行事。

选项 2:

  1. 您可以通过单击位置检测您在哪个位置单击,而不是使用 QGridLayout。

  2. 验证点击的 x 是否小于移动位置的 x 以了解它是向左还是向右移动以及它被点击的位置。

  3. 计算方法同Option1

选项 3:

  1. 可能还有其他方法,但这些是我刚刚想到的方法。 例如,使用您说可以调整大小的 CustomizeWindowHint,因此您只需要实现我给您的示例即可。 美丽的!

提示:

  1. 小心localPos(在自己的小部件内),globalPos(与您的屏幕相关)。 例如:如果您单击左侧小部件的最左侧,则其 'x' 将为零,如果您单击中间(内容)的最左侧,它也将为零,但如果您 mapToGlobal,您将具有不同的值根据屏幕的位置。
  2. 在调整大小或移动时要注意,当你必须增加宽度或减少,或者只是移动,或两者兼而有之时,我建议你在纸上画画并弄清楚调整大小的逻辑是如何工作的,然后再实施它。

祝你好运:D

这是为要在 PyQt6 或 PySide6 中实现自定义标题栏的人准备的

以下更改应在@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())

参考资料: QMouseEvent.globalPosition() , QMouseEvent.scenePosition()

这种使用自定义小部件移动 Windows 的方法不适用于 WAYLAND。 如果有人对此有解决方案,请在此处发布以供将来参考

虽然接受的答案可以被认为是有效的,但它有一些问题。

  • 使用setGeometry()是不合适的(并且使用它的原因是错误的),因为它不考虑样式设置的可能的帧边距;
  • 位置计算过于复杂;
  • 将标题栏调整为总宽度是错误的,因为它不考虑按钮,并且在某些情况下还会导致递归问题(例如设置主窗口的最小大小); 此外,如果标题太大,则无法调整主窗口的大小;
  • 设置布局会为“主小部件”或布局创建约束,因此不应添加标题,而应使用小部件的内容边距;

我修改了代码以为主窗口提供更好的基础,简化移动代码,并添加其他功能,如 Qt windowTitle()属性支持、按钮的标准 QStyle 图标(而不是文本)和正确的最大化/正常按钮图标. 请注意,标题标签不会添加到布局中。

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)
            layout.addWidget(btn)
            btn.setFixedSize(btn_size)

            iconType = getattr(style, 
                'SP_TitleBar{}Button'.format(target.capitalize()))
            btn.setIcon(style.standardIcon(iconType))

            color = 'red' if target == 'close' else 'gray'
            btn.setStyleSheet('background-color: {};'.format(color))

            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.pos()

    def mouseMoveEvent(self, event):
        if self.clickPos is not None:
            self.window().move(self.window().pos() 
                + event.pos() - 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_())

WAYLANDPyQT6/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()

请检查。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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