就像正常的窗口处理一样,即使在一个 MDI 区域中也不可能有多个活动的子窗口。
为了实现“多选”系统,需要跟踪子窗口的激活状态,这可能很棘手。
子窗口可以通过不同的方式激活:
虽然 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 必须是 instance 属性,而不是类属性:如果出于任何原因,您必须创建主窗口的多个实例,您将获得不一致的计数;您还应该考虑当前存在的窗口;
- 如果根本没有重载(
QMenu.triggered 的情况),则不应指定信号重载,如果仅使用一次(并且它们的名称不是那么长,如 self.menuBar()),则不应创建局部变量);