首页 > 解决方案 > pyqt5 QMdiArea 多个活动子窗口

问题描述

在 QMdiArea 中,我们可以选择(指向)激活子窗口。在我的应用程序中,我想选择多个子窗口(可能使用“Ctrl”按钮)并将它们设置为活动窗口(> = 2 个子窗口)并为它们创建一个列表指针。我正在尝试同时获取多个子窗口的指针。是的,activeSubWindow() 只提供一个窗口。但我想知道是否可以使用键盘上的“Ctrl”按钮来选择两个子窗口并打印指向这些子窗口的指针。这个想法是同时获取每个子窗口(例如TextEditor)内的小部件以执行后续任务,例如比较

from PyQt5.QtWidgets import QApplication, QMainWindow, QMdiArea, QAction, QMdiSubWindow, QTextEdit
import sys
 
 
 
class MDIWindow(QMainWindow):
 
    count = 0
    def __init__(self):
        super().__init__()
 
        self.mdi = QMdiArea()
        self.setCentralWidget(self.mdi)
        bar = self.menuBar()
 
        file = bar.addMenu("File")
        file.addAction("New")
        file.addAction("cascade")
        file.addAction("Tiled")
        file.addAction("selected_subwindows")
        file.triggered[QAction].connect(self.WindowTrig)
        self.setWindowTitle("MDI Application")
 
    def WindowTrig(self, p):
 
 
        if p.text() == "New":
            MDIWindow.count = MDIWindow.count + 1
            sub = QMdiSubWindow()
            sub.setWidget(QTextEdit())
            sub.setWindowTitle("Sub Window" + str(MDIWindow.count))
            self.mdi.addSubWindow(sub)
            sub.show()
 
        if p.text() == "cascade":
            self.mdi.cascadeSubWindows()
 
        if p.text() == "Tiled":
            self.mdi.tileSubWindows()
            
        if p.text()=="selected_subwindows":
            """I want to select multiple subwindows and and set as activate 
                windows with the "Ctrl" button and return a points fot all active windows"""
            print("active windows: ", self.mdi.activeSubWindow())
 
app = QApplication(sys.argv)
mdi  =MDIWindow()
mdi.show()
app.exec_()

标签: pythonpyqtpyqt5

解决方案


就像正常的窗口处理一样,即使在 MDI 区域中也不可能有多个活动的子窗口。

为了实现“多选”系统,您需要跟踪子窗口的激活状态,这可能很棘手。
子窗口可以通过不同的方式激活:

  • 通过单击其标题栏(包括其任何按钮);
  • 通过单击其包含的小部件;
  • 通过以编程方式激活它setActiveSubWindow()(类似于从任务栏中选择普通窗口);

虽然 Qt 提供了aboutToActivate信号,但它并不总是可靠的:即使顶层窗口获得焦点,它也总是会发出,因此没有直接的方法可以知道激活的原因。
信号也是如此(在状态改变windowStateChanged发出)。

对于您的情况,最好的方法主要是基于mousePressEvent子窗口的实现,同时还要考虑窗口状态的变化,因为每当激活以任何其他方式发生变化时,您都需要跟踪当前的活动窗口(通过单击小部件或使用setActiveSubWindow().

由于鼠标事件是在窗口激活更改处理的,因此正确的解决方案是创建一个将延迟(计划)发射的信号,以便了解激活是否实际上是通过在子窗口上按下鼠标按钮实现的(不在子小部件上),最后检查是否Ctrl同时按下了键。

请注意,以下代码非常基础,您可能需要进行一些调整。例如,它不考虑最小化窗口的激活(与普通窗口不同,子窗口即使被最小化也可能是活动的),也不考虑单击任何窗口按钮时的激活。

from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import sys

class SubWindow(QMdiSubWindow):
    activated = pyqtSignal(object, bool)
    ctrlPressed = False
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setAttribute(Qt.WA_DeleteOnClose)
        self.windowStateChanged.connect(self.delayActivated)
        self.activatedTimer = QTimer(
            singleShot=True, interval=1, timeout=self.emitActivated)

    def delayActivated(self, oldState, newState):
        # Activation could also be triggered for a previously inactive top
        # level window, but the Ctrl key might still be handled by the child
        # widget, so we should always assume that the key was not pressed; if
        # the activation is done through a mouse press event on the subwindow
        # then the variable will be properly set there.
        # Also, if the window becomes inactive due to programmatic calls but
        # *after* a mouse press event, the variable has to be reset anyway.
        self.ctrlPressed = False
        if newState & Qt.WindowActive:
            self.activatedTimer.start()
        elif not newState and self.activatedTimer.isActive():
            self.activatedTimer.stop()

    def emitActivated(self):
        self.activated.emit(self, self.ctrlPressed)
        self.ctrlPressed = False

    def mousePressEvent(self, event):
        if event.button() == Qt.LeftButton:
            self.ctrlPressed = event.modifiers() & Qt.ControlModifier
            self.activatedTimer.start()
        super().mousePressEvent(event)


class MDIWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("MDI Application")

        self.activeWindows = []
        activeContainer = QWidget()
        activeLayout = QVBoxLayout(activeContainer)
        activeLayout.setContentsMargins(0, 0, 0, 0)
        self.activeList = QListWidget()
        # Note: the following "monkey patch" is only for educational purposes
        # and done in order to keep the code short, you should *not* normally
        # do this unless you really know what you're doing.
        self.activeList.sizeHint = lambda: QSize(150, 256)
        activeLayout.addWidget(self.activeList)

        self.compareBtn = QPushButton('Compare', enabled=False)
        activeLayout.addWidget(self.compareBtn)

        self.activeDock = QDockWidget('Selected windows')
        self.activeDock.setWidget(activeContainer)
        self.addDockWidget(Qt.LeftDockWidgetArea, self.activeDock)
        self.activeDock.setFeatures(self.activeDock.NoDockWidgetFeatures)

        self.mdi = QMdiArea()
        self.setCentralWidget(self.mdi)
        bar = self.menuBar()

        fileMenu = bar.addMenu("File")
        self.newAction = fileMenu.addAction("New")
        self.cascadeAction = fileMenu.addAction("Cascade")
        self.tileAction = fileMenu.addAction("Tiled")
        self.compareAction = fileMenu.addAction("Compare subwindows")

        fileMenu.triggered.connect(self.menuTrigger)
        self.compareBtn.clicked.connect(self.compare)

    def menuTrigger(self, action):
        if action == self.newAction:
            windowList = self.mdi.subWindowList()
            if windowList:
                count = windowList[-1].index + 1
            else:
                count = 1
            sub = SubWindow()
            sub.index = count
            sub.setWidget(QTextEdit())
            sub.setWindowTitle("Sub Window " + str(count))
            self.mdi.addSubWindow(sub)
            sub.show()
            sub.activated.connect(self.windowActivated)
 
        elif action == self.cascadeAction:
            self.mdi.cascadeSubWindows()
 
        elif action == self.tileAction:
            self.mdi.tileSubWindows()
            
        elif action == self.compareAction:
            self.compare()

    def windowActivated(self, win, ctrlPressed):
        if not ctrlPressed:
            self.activeWindows.clear()
        if win in self.activeWindows:
            self.activeWindows.remove(win)
        self.activeWindows.append(win)
        self.activeList.clear()
        self.activeList.addItems([w.windowTitle() for w in self.activeWindows])
        valid = len(self.activeWindows) >= 2
        self.compareBtn.setEnabled(valid)
        self.compareAction.setEnabled(valid)

    def compare(self):
        editors = [w.widget() for w in self.activeWindows]
        if len(editors) < 2:
            return
        it = iter(editors)
        oldEditor = next(it)
        while True:
            try:
                editor = next(it)
            except:
                msg = 'Documents are equal!'
                break
            if oldEditor.toPlainText() != editor.toPlainText():
                msg = 'Documents do not match!'
                break
            oldEditor = editor
        QMessageBox.information(self, 'Comparison result', msg, QMessageBox.Ok)


app = QApplication(sys.argv)
mdi = MDIWindow()
mdi.show()
app.exec_()

请注意,我必须对您的代码进行一些进一步的更改:

  • 动作检查不应该通过字符串比较来完成:样式或本地化可能会向动作文本添加助记符或文本变体,并且您永远不会触发动作:创建适当的实例属性并通过对象比较来验证动作。
  • count必须是实例属性,而不是类之一:如果出于任何原因必须创建主窗口的多个实例,则会得到不一致的计数;您还应该考虑当前存在的窗口;
  • 如果根本没有重载(这是 的情况),则不应指定信号重载,如果QMenu.triggered仅使用一次(并且它们的名称不是那么长,例如self.menuBar()),则不应创建局部变量;

推荐阅读