fix: top/bottom edge resize broken — menubar ate events + minimumSize clamped to 1121px
Two independent root causes for '左右能拉,上下不能拉': 1. Event delivery: the top 6px edge band belongs to QMenuBar, which actively consumes mouse events (menu hover) — nothing ever propagated to QMainWindow, so T/TL/TR (and maximize-restore from top edge) were dead on every platform. Install an event filter on menuBar()/statusBar(): within the edge band it handles hover cursor, press (grabMouse), drag and release; outside the band everything passes through (menus open normally, native QSizeGrip corner drag preserved). 2. Layout minimum: MonitorPanel's minimumSizeHint (~1015px of stacked tables/group boxes) propagated through QTabWidget → central widget → QMainWindow, making the window minimum size 898x1121. Any vertical shrink was clamped dead. Set explicit per-tab-page minimumSize (320x200) + window minimumSize (760x480). Tests: test_resize.py now drives edges through real event delivery (childAt → child widget, propagation/filter path) for L/R/T/B/TL/TR/BL, verifies BR stays on QSizeGrip, maximize-restore via menubar filter, and minimum-size regression. All 8 suites pass.
This commit is contained in:
+70
-5
@@ -7,7 +7,7 @@ import sys
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
from PyQt5.QtCore import Qt, QSize
|
||||
from PyQt5.QtCore import Qt, QSize, QEvent
|
||||
from PyQt5.QtGui import QFont, QIcon
|
||||
from PyQt5.QtWidgets import (
|
||||
QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QListWidget, QListWidgetItem,
|
||||
@@ -47,6 +47,13 @@ class MainWindow(QMainWindow):
|
||||
self._build_menu()
|
||||
self._build_statusbar()
|
||||
|
||||
# 菜单栏/状态栏事件过滤器:
|
||||
# 窗口上下边缘分别被 QMenuBar / QStatusBar 覆盖,QMenuBar 会主动消费
|
||||
# 鼠标事件(悬停高亮菜单),事件不会传播到 QMainWindow,导致上边缘
|
||||
# (及最大化时从顶部拖还原)永远失效。装过滤器在边缘带内截获处理。
|
||||
self.menuBar().installEventFilter(self)
|
||||
self.statusBar().installEventFilter(self)
|
||||
|
||||
# 左侧主机栏折叠状态
|
||||
# 注意:Ctrl+B 快捷键只由「视图」菜单项(act_toggle_sidebar)承载,
|
||||
# 不再额外建 QShortcut —— 否则一次按键触发两个绑定,会 toggle 两次等于没反应
|
||||
@@ -172,6 +179,16 @@ class MainWindow(QMainWindow):
|
||||
splitter.addWidget(self.tabs)
|
||||
splitter.setSizes([280, 1000])
|
||||
|
||||
# 各 Tab 页显式设小最小尺寸:否则 QTabWidget 取所有页 minimumSizeHint
|
||||
# 的最大值(MonitorPanel 内部表格/分组堆出 ~1015px 高),一路传到
|
||||
# QMainWindow,导致窗口最小高度 > 1100px —— 上下边缘拖动被 clamp 死,
|
||||
# 表现为"上下不能拉"。QSplitter 子件默认 minimumSize=0,压缩无碍。
|
||||
for _page in (self.terminal_panel, self.file_browser,
|
||||
self.monitor, self.ai_panel):
|
||||
_page.setMinimumSize(320, 200)
|
||||
# 窗口级合理下限(显式 minimumSize 覆盖布局算出的巨大值)
|
||||
self.setMinimumSize(760, 480)
|
||||
|
||||
def _toggle_sidebar(self):
|
||||
"""折叠/展开左侧主机栏"""
|
||||
self._sidebar_collapsed = not self._sidebar_collapsed
|
||||
@@ -620,7 +637,7 @@ class MainWindow(QMainWindow):
|
||||
return "B"
|
||||
return ""
|
||||
|
||||
def _edge_to_cursor(self, edge: str):
|
||||
def _edge_to_cursor(self, edge: str, widget=None):
|
||||
from PyQt5.QtGui import QCursor
|
||||
from PyQt5.QtCore import Qt as _Qt
|
||||
cursors = {
|
||||
@@ -631,10 +648,58 @@ class MainWindow(QMainWindow):
|
||||
}
|
||||
c = cursors.get(edge)
|
||||
if c is not None:
|
||||
self.setCursor(QCursor(c))
|
||||
(widget or self).setCursor(QCursor(c))
|
||||
|
||||
def _restore_cursor(self):
|
||||
self.unsetCursor()
|
||||
def _restore_cursor(self, widget=None):
|
||||
(widget or self).unsetCursor()
|
||||
|
||||
def eventFilter(self, obj, e):
|
||||
"""拦截菜单栏/状态栏上的鼠标事件,实现上/下边缘拖动调整窗口。
|
||||
|
||||
QMenuBar 主动消费鼠标事件(悬停高亮菜单项),事件不会向上传播到
|
||||
QMainWindow,上边缘 hit-test 永远收不到事件;状态栏在部分平台/样式
|
||||
下同理。过滤器只在 6px 边缘带内接管,其余区域原样放行
|
||||
(菜单照常点开、QSizeGrip 原生右下角拖动不受影响)。
|
||||
"""
|
||||
mb, sb = self.menuBar(), self.statusBar()
|
||||
if obj is mb or obj is sb:
|
||||
et = e.type()
|
||||
if et == QEvent.MouseMove:
|
||||
# 拖动中:press 时已 grabMouse,move 持续走这里
|
||||
if self._resize_edge and (e.buttons() & Qt.LeftButton):
|
||||
self._do_resize(e.globalPos())
|
||||
return True
|
||||
# 悬停:边缘带内显示调整光标并屏蔽菜单高亮,带外放行
|
||||
if not e.buttons():
|
||||
edge = self._hit_test_edge(obj.mapTo(self, e.pos()))
|
||||
if edge:
|
||||
self._edge_to_cursor(edge, obj)
|
||||
return True
|
||||
self._restore_cursor(obj)
|
||||
return False
|
||||
if et == QEvent.MouseButtonPress and e.button() == Qt.LeftButton:
|
||||
# 最大化时:菜单栏顶部边缘按下 → 还原并跟随鼠标
|
||||
if obj is mb and self.isMaximized() and e.pos().y() <= self._resize_margin:
|
||||
self._restore_from_max(e.globalPos())
|
||||
return True
|
||||
edge = self._hit_test_edge(obj.mapTo(self, e.pos()))
|
||||
if edge:
|
||||
self._resize_edge = edge
|
||||
self._resize_start_geo = self.geometry()
|
||||
self._resize_start_pos = e.globalPos()
|
||||
obj.grabMouse() # 后续 move/release 全部经过本过滤器
|
||||
return True
|
||||
return False
|
||||
if et == QEvent.MouseButtonRelease and self._resize_edge:
|
||||
self._resize_edge = ""
|
||||
self._resize_start_geo = None
|
||||
self._resize_start_pos = None
|
||||
obj.releaseMouse()
|
||||
return True
|
||||
if et == QEvent.Leave:
|
||||
self._restore_cursor(obj)
|
||||
return False
|
||||
return super().eventFilter(obj, e)
|
||||
|
||||
def mouseMoveEvent(self, e):
|
||||
# 拖动中
|
||||
|
||||
Reference in New Issue
Block a user