pyqt5 QMdiArea 多个活动子窗口

pyqt5 QMdiArea multiple active subwindows

在QMdiArea中,我们可以select(指向)激活子window。在我的应用程序中,我想 select 多个 subwindows (可能使用“Ctrl”按钮)并将它们设置为活动 windows (>=2 subwindows) 和为他们创建一个列表指针。我正在尝试同时获取多个 subwindow 的指针。是的,activeSubWindow() 只给出一个window。但我想知道我是否可以使用键盘中的“Ctrl”按钮之类的东西来 select 两个子windows 并打印指向这些子windows 的指针。这个想法是同时获取每个子window(例如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_()

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

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

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

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

对于你的情况,最好的办法主要是根据mousePressEvent实现subwindow,还要考虑window状态变化,因为你需要保持跟踪每当以任何其他方式更改激活时(通过单击小部件或使用 setActiveSubWindow().

,当前激活的 windows

由于鼠标事件是在 window 激活更改后处理的,正确的解决方案是创建一个发射将被延迟(预定)的信号,为了知道激活是否实际上是通过在 subwindow 上按下鼠标按钮实现的(而不是在 child 小部件上),最后检查 Ctrl同时按下了键。

请注意以下代码非常基础,您可能需要做一些调整。例如,它不考虑最小化 windows 的激活(与普通 windows 不同,subwindow 即使被最小化也可能处于活动状态),也不考虑单击任何window 个按钮。

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

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

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