簡體   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