【问题标题】:PyQt5: QAction -- QTextEdit.paste not working with QTabWidget?PyQt5:QAction -- QTextEdit.paste 不能与 QTabWidget 一起使用?
【发布时间】:2022-01-10 06:30:45
【问题描述】:

PyQt5:QAction -- QTextEdit.paste 不适用于 QTabWidget?

尝试学习python,我开始了一个项目来编写一个标签式markdown编辑器。在下面的代码中,一切都按预期工作,我可以:

  • 添加和删除标签
  • 使用键盘添加内容
  • 使用RMB上下文菜单添加内容
  • 从文件中加载内容

但是,当我尝试设置 QAction: QTextEdit.paste 时出现以下错误

Traceback (most recent call last):
  File "~/Documents/Python/Mq/bin/Mq-post.py", line 170, in <module>
    M = Main()
  File "~/Documents/Python/Mq/bin/Mq-post.py", line 40, in __init__
    self.initUi()
  File "~/Documents/Python/Mq/bin/Mq-post.py", line 47, in initUi
    configMainMenu(self)
  File "~/Documents/Python/Mq/bin/ConfigMenu.py", line 47, in configMainMenu
    Paste.triggered.connect(self.textArea.paste)
AttributeError: 'Main' object has no attribute 'textArea'

我已尝试将textArea 的定义从addTab 移动到__init__,但是,虽然没有报告错误,并且QTextEdit.paste 操作 按预期工作,但我无法创建新标签。

代码如下:

主要:

""" Mq editor main window """
import sys
from PyQt5.QtWidgets import (QMainWindow, QApplication, QWidget, QTabWidget,
                             QVBoxLayout, QFileDialog, QTextEdit)
from PyQt5.QtGui import (QIcon)

from ConfigMenu import (configMainMenu)
from Functions import (trimFileName, trimHome)


class Main(QMainWindow):
    """ Main app """

    def __init__(self):
        super().__init__()

        # Define initial window geometry
        self.setWindowTitle('Mq Editor')
        self.setWindowIcon(QIcon("Quill.png"))
        self.left = 0
        self.top = 0
        self.width = 1024
        self.height = 768
        self.setGeometry(self.left, self.top, self.width, self.height)

        # Setup a TabBar with movable closable tabs
        self.tabs = QTabWidget()
        self.tabs.tabCloseRequested.connect(self.closeTab)
        self.tabs.setTabsClosable(True)
        self.tabs.setMovable(True)

        # Define the Layout
        widget = QWidget(self)
        self.setCentralWidget(widget)
        self.layout = QVBoxLayout(widget)

        # Defined in add tab, See pylint W0201, (disable to get traceback 2)
        # self.textArea = None

        self.initUi()

    # #####################################

    def initUi(self):
        """ Set up user interface """
        # Create a menu bar
        configMainMenu(self)

        # Adding the tab widget to the layout needs to come after calling
        # 'configMainMenu' and 'createToolBar'
        self.layout.addWidget(self.tabs)

        # Create first tab
        self.addTab()

        # Open window in maximized size
        self.showMaximized()

    # #####################################

    def addTab(self):
        """ Create Tabs """

        # Add tab with new editor instance, (this is only place it works)
        self.textArea = QTextEdit()
        self.textArea.textChanged.connect(self.textChanged)
        self.textArea.createStandardContextMenu()

        self.tabs.addTab(self.textArea, "Untitled")

        # Switch to new tab
        index = self.tabs.count() - 1
        self.tabs.setCurrentIndex(index)
        self.tabs.setTabToolTip(index, "Untitled")

    # #####################################

    def closeTab(self, index):
        """ Close Tab Handler """
        self.tabs.removeTab(index)

    # #####################################

    def getActiveTab(self):
        """ Returns active tab 'index' and 'title' """
        # Get the active tabs index
        index = self.tabs.currentIndex()

        # Get the active tabs title
        title = self.tabs.tabText(index)
        return index, title

    # #####################################

    def setActiveTabTitle(self, title, toolTip):
        """ Sets the active tab 'title' and 'tooltip' """
        # Get the active tabs index
        index = self.tabs.currentIndex()

        # Set the active tabs title
        self.tabs.setTabText(index, title)
        self.tabs.setTabToolTip(index, toolTip)

    # #####################################

    def textChanged(self):
        """ textChanged handler """
        # Get the active tab's details
        index, title = self.getActiveTab()

        # Check if last char of title is marked as 'Text Changed'
        if title[-1] != '*':
            # Not marked as 'Text Changed'
            self.tabs.setTabText(index, title + '*')

    # #####################################

    def closeUnusedTabs(self):
        """ Tab house-keeping """
        tabIndex = self.tabs.count()
        while tabIndex > 0:
            index = tabIndex - 1
            self.tabs.setCurrentIndex(index)
            title = self.tabs.tabText(index)
            if title == "Untitled":
                self.closeTab(index)
            tabIndex = tabIndex - 1

    # #####################################

    def openFile(self):
        """ openFile handler """
        # Open file dialogue
        FileDialogue = QFileDialog.getOpenFileNames
        filePath, _ = FileDialogue(None, "Open File", "",
                                   "Markdown Files (*.md);;All Files (*)")

        # check 'filePath' is not empty
        if filePath:
            # Keep the GUI clean
            self.closeUnusedTabs()

            # Iterate 'filePath' and open files
            for i in filePath:

                # Having removed unused tabs, we just need to add a new tab for
                # each file 'i'
                self.addTab()

                # Read file 'i' and load into new tab instance of editor
                with open(i, "r", encoding="utf8") as f:
                    fileContents = f.read()
                    self.textArea.setPlainText(fileContents)

                # Get a trimmed path as the 'tab tooltip'
                toolTip = trimHome(i)

                # Get a trimmed file name to use as the 'tab title'
                tabTitle = trimFileName(i)

                # Set the new tabs 'title' and 'tootip'
                self.setActiveTabTitle(tabTitle, toolTip)

    def Paste(self):
        " Test  paste click handler "
        print("Hit Paste")


m = QApplication(sys.argv)
M = Main()
M.show()
sys.exit(m.exec_())

配置菜单:

""" Module to create a menu bar """

from PyQt5.QtWidgets import (QAction)


def configMainMenu(self):
    """ Creating a menu bar"""

    # mainMenu Definition
    mainMenu = self.menuBar()
    fileMenu = mainMenu.addMenu('File')
    editMenu = mainMenu.addMenu('Edit')

    # 'New'
    New = QAction('New', self)
    New.setShortcut('Ctrl+N')
    New.setStatusTip('New File')
    New.triggered.connect(self.addTab)
    fileMenu.addAction(New)

    # 'Close'
    Close = QAction('Close', self)
    Close.setShortcut('Ctrl+W')
    Close.setStatusTip('Close File')
    Close.triggered.connect(self.closeTab)
    fileMenu.addAction(Close)

    # 'Open'
    Open = QAction('Open', self)
    Open.setShortcut('Ctrl+O')
    Open.setStatusTip('Open')
    Open.triggered.connect(self.openFile)
    fileMenu.addAction(Open)

    # 'Paste'
    Paste = QAction('Paste', self)
    Paste.setShortcut('Ctrl+V')
    Paste.setStatusTip('Paste text')
    # ### The traceback leads here ###
    Paste.triggered.connect(self.textArea.paste)  # Fails
    # Paste.triggered.connect(self.Paste)         # For testing
    editMenu.addAction(Paste)

功能:

""" Useful functions """

from pathlib import Path
from os.path import expanduser


def trimFileName(filePath):
    """ Returns size limited 'tab title' based on 'file name'  """

    # FRST: Get the file name without extension
    # Convert 'file path' to type 'path' without '.ext'
    filePath = Path(filePath).with_suffix('')

    # Convert the 'Path' back to 'string' and 'split' into a 'list'
    pathStr = str(filePath).split("/")

    # The file name is last element of the 'list'
    tabTitle = pathStr[-1]

    # SCOND: trim length to 12 characters.
    # Nte: we use the tab 'tooltip' to show 'path' and 'file name'
    length = len(tabTitle)
    if length > 12:
        tabTitle1 = tabTitle[0:3]
        tabTitle2 = tabTitle[length - 6:length]
        tabTitle = tabTitle1 + '..' + tabTitle2

    return tabTitle


def trimHome(filePath):
    """ Replace ${HOME} with '~' in 'filePath' """
    # Note: This also works on Windows :)

    home = expanduser("~")
    trimmedPath = filePath.replace(home, "~")

    return trimmedPath

欢迎任何关于如何在保持 Tab 功能的同时实现 Edit Menu 的建议或帮助。

【问题讨论】:

  • 移动configMainMenu() 之后 self.addTab().

标签: python python-3.x pyqt5 qtextedit qtabwidget


【解决方案1】:

建议的解决方案:

简介:

好的,终于,经过四天的研究和测试,我有一个似乎可行的解决方案

虽然 @musicamante 的建议很有趣,但它只允许粘贴到创建的第一个标签中。事实上,这是我在研究期间发现的许多“解决方案”和“示例”中经常出现的问题:它们只是 粘贴文本 进入创建的第一个或最后一个选项卡。

但是,它确实突出了基本问题。

问题:

  1. textArea 需要在 addTab 函数中定义,但这意味着它不能通过 editMenu 操作 直接寻址mainMenuToolBar
  2. 正如最初所写,每个选项卡的 textArea 不是唯一可识别的。

解决办法:

第 1 部分:

为了解决第一部分的问题,我创建了一个函数来处理所有 editMenu triggered.connect 信号:

def editFunctions(self, Fnct):
        """ edit Menu/ToolBar clickHandlers"""
        print(self.tabs.currentIndex())
        match Fnct:
            case "selectall": self.currentEditor.selectAll()
            case "paste": self.currentEditor.paste()
            case "copy": self.currentEditor.copy()
            case "undo": self.currentEditor.undo()
            case "redo": self.currentEditor.redo()
            case "cut": self.currentEditor.cut()

并调用我使用的上述函数,triggered.connect 信号形式如下:

Paste.triggered.connect(partial(self.editFunctions, "paste"))

第 2 部分:

这仍然存在每个选项卡的 textArea 无法唯一识别的问题。

首先,我向 *tabwidget" 添加了一个新信号,并创建了一个 list 可用的文本区域,称为 editors

    def __init__(self):
        super().__init__()

        # Define initial window geometry
        self.setWindowTitle('Mq Editor')
        self.setWindowIcon(QIcon(c.Quill))
        self.left = 0
        self.top = 0
        self.width = 1024
        self.height = 768
        self.setGeometry(self.left, self.top, self.width, self.height)

        # Setup a TabBar with movable closable tabs
        self.tabs = QTabWidget()
        self.tabs.tabCloseRequested.connect(self.closeTab)
        self.tabs.currentChanged.connect(self.changeTextEditor)
        self.tabs.setTabsClosable(True)
        self.tabs.setMovable(True)
        self.editors = []

        # Define the Layout
        widget = QWidget(self)
        self.setCentralWidget(widget)
        self.layout = QVBoxLayout(widget)

        self.initUi()

这需要一个新函数来处理信号和一个新的属性实例

    def changeTextEditor(self, index):
        """ Set currentEditor to index of current tab """
        self.currentEditor = self.editors[index]

这也意味着编辑 addTabcloseTab 函数来更新新的属性

    def addTab(self):
        """ Create Tabs """

        # Add tab with new editor instance
        textArea = QTextEdit()
        textArea.textChanged.connect(self.textChanged)
        textArea.createStandardContextMenu()

        # Update list of editors
        self.currentEditor = textArea
        self.editors.append(self.currentEditor)

        self.tabs.addTab(textArea, "Untitled")

        # Switch to new tab
        index = self.tabs.count() - 1
        self.tabs.setCurrentIndex(index)
        self.tabs.setTabToolTip(index, "Untitled")
    def closeTab(self, index):
        """ Close Tab Handler """
        self.tabs.removeTab(index)

        # Update editors list
        del self.editors[index]

最后,我需要将self.textArea 的所有实例替换为self.currentEditor

总结:

我的解决方案可能需要进一步完善,但就目前的情况而言,它已经通过了我所有的初始测试,而且似乎也能正常工作且没有错误。

欧文

编辑:

正如我所说,它可以做一些改进,特别是:使用QTabWidget.currentWidget() 方法。

    def editFunctions(self, Fnct):
        """ 'edit' Menu/ToolBar clickHandlers"""
        
        currentEditor = self.tabs.currentWidget()
        
        match Fnct:
            case "selectall": currentEditor.selectAll()
            case "paste": currentEditor.paste()
            case "copy": currentEditor.copy()
            case "undo": currentEditor.undo()
            case "redo": currentEditor.redo()
            case "cut": currentEditor.cut()

它是:

  • 更灵活
  • 处理可移动标签
  • 只需将新函数添加到原始代码中,无需进一步编辑

【讨论】:

    猜你喜欢
    • 2021-11-22
    • 2016-08-13
    • 1970-01-01
    • 1970-01-01
    • 2014-10-17
    • 1970-01-01
    • 1970-01-01
    • 2017-04-29
    • 2020-09-01
    相关资源
    最近更新 更多