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:
+106
-7
@@ -1,8 +1,11 @@
|
||||
"""
|
||||
窗口边缘拖动测试:
|
||||
- _hit_test_edge 返回正确的边
|
||||
- mouseMove + mousePress 模拟拖动
|
||||
- mouseMove + mousePress 模拟拖动(直接投递到 QMainWindow)
|
||||
- 普通模式 + 最大化模式都覆盖
|
||||
- 真实事件投递(childAt → 子控件,依赖传播/事件过滤器):4 边 + 3 角
|
||||
- 最大化时真实按压菜单栏顶缘 → 还原
|
||||
- 最小尺寸合理性(回归:MinimumSizeHint 曾把窗口最小高度撑到 1121px,上下无法缩小)
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
@@ -12,7 +15,7 @@ sys.path.insert(0, os.path.dirname(__file__))
|
||||
|
||||
from PyQt5.QtCore import Qt, QPoint, QEvent
|
||||
from PyQt5.QtGui import QMouseEvent
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PyQt5.QtWidgets import QApplication, QSizeGrip
|
||||
|
||||
from ui.main_window import MainWindow
|
||||
|
||||
@@ -23,6 +26,35 @@ def make_mouse_event(type_, pos, button=Qt.LeftButton):
|
||||
return QMouseEvent(type_, pos, glb, button, button, Qt.NoModifier)
|
||||
|
||||
|
||||
def deliver(target, type_, local_pos, button=Qt.LeftButton, buttons=Qt.NoButton):
|
||||
"""把事件投给 target(模拟 Qt 投给光标下子控件的真实路径)"""
|
||||
ev = QMouseEvent(type_, local_pos, QPoint(local_pos.x(), local_pos.y()),
|
||||
button, buttons, Qt.NoModifier)
|
||||
QApplication.sendEvent(target, ev)
|
||||
return ev
|
||||
|
||||
|
||||
def try_real_drag(w, press_pos, delta, expected_edge):
|
||||
"""真实投递:press 到 childAt 命中的控件 → move → release。
|
||||
返回 (命中控件, geo_before, geo_after)"""
|
||||
child = w.childAt(press_pos)
|
||||
target = child if child else w
|
||||
local = target.mapFrom(w, press_pos) if child else press_pos
|
||||
geo0 = w.geometry()
|
||||
deliver(target, QEvent.MouseButtonPress, local,
|
||||
button=Qt.LeftButton, buttons=Qt.LeftButton)
|
||||
assert w._resize_edge == expected_edge, \
|
||||
f"{expected_edge}: press 后 _resize_edge={w._resize_edge!r}(命中 {type(target).__name__})"
|
||||
moved = QPoint(local.x() + delta[0], local.y() + delta[1])
|
||||
deliver(target, QEvent.MouseMove, moved, button=Qt.NoButton, buttons=Qt.LeftButton)
|
||||
deliver(target, QEvent.MouseMove, moved, button=Qt.NoButton, buttons=Qt.LeftButton)
|
||||
geo1 = w.geometry()
|
||||
deliver(target, QEvent.MouseButtonRelease, moved,
|
||||
button=Qt.LeftButton, buttons=Qt.NoButton)
|
||||
assert w._resize_edge == "", "release 后 _resize_edge 未清空"
|
||||
return type(target).__name__, geo0, geo1
|
||||
|
||||
|
||||
def main():
|
||||
app = QApplication(sys.argv)
|
||||
# 在 offscreen 平台,默认屏幕可能很小(800x600),把窗口放大会被限制
|
||||
@@ -43,7 +75,7 @@ def main():
|
||||
orig_primary = QApplication.primaryScreen
|
||||
QApplication.primaryScreen = staticmethod(lambda: FakeScreen())
|
||||
|
||||
print("[1/5] 创建 MainWindow + 边缘 hit-test")
|
||||
print("[1/8] 创建 MainWindow + 边缘 hit-test")
|
||||
w = MainWindow()
|
||||
w.resize(800, 600)
|
||||
w.show()
|
||||
@@ -66,7 +98,7 @@ def main():
|
||||
assert w._hit_test_edge(QPoint(w_real_w - 2, w_real_h - 2)) == "BR"
|
||||
print(" ✓ 8 个区域(4 边 + 4 角)全部正确命中")
|
||||
|
||||
print("[2/5] 普通模式:拖右边缘放大窗口")
|
||||
print("[2/8] 普通模式:拖右边缘放大窗口")
|
||||
w.resize(800, 600)
|
||||
w.show()
|
||||
rw, rh = w.width(), w.height()
|
||||
@@ -87,7 +119,7 @@ def main():
|
||||
assert w._resize_edge == ""
|
||||
print(f" ✓ 右拖: 宽 {geo0.width()} → {geo1.width()} (+{geo1.width() - geo0.width()})")
|
||||
|
||||
print("[3/5] 普通模式:拖左边缘(鼠标右移 → 窗口变宽)")
|
||||
print("[3/8] 普通模式:拖左边缘(鼠标右移 → 窗口变宽)")
|
||||
w.resize(800, 600)
|
||||
w.show()
|
||||
rw, rh = w.width(), w.height()
|
||||
@@ -111,7 +143,7 @@ def main():
|
||||
assert geo1.x() > geo0.x(), f"x 没变: {geo0.x()} -> {geo1.x()}"
|
||||
QApplication.sendEvent(w, make_mouse_event(QEvent.MouseButtonRelease, new_pos))
|
||||
|
||||
print("[4/5] 普通模式:拖角(左上)")
|
||||
print("[4/8] 普通模式:拖角(左上)")
|
||||
w.resize(800, 600)
|
||||
w.show()
|
||||
rw, rh = w.width(), w.height()
|
||||
@@ -131,7 +163,7 @@ def main():
|
||||
print(f" ✓ TL 拖: {geo0.width()}x{geo0.height()} → {geo1.width()}x{geo1.height()} (受 minSize 限制)")
|
||||
QApplication.sendEvent(w, make_mouse_event(QEvent.MouseButtonRelease, new_pos))
|
||||
|
||||
print("[5/5] 最大化时:拖顶部边缘应还原窗口")
|
||||
print("[5/8] 最大化时:拖顶部边缘应还原窗口(直接投递路径)")
|
||||
w.resize(800, 600)
|
||||
w.showMaximized()
|
||||
app.processEvents()
|
||||
@@ -149,6 +181,73 @@ def main():
|
||||
QApplication.sendEvent(w, make_mouse_event(QEvent.MouseButtonRelease, QPoint(400, 3)))
|
||||
print(f" ✓ 最大化时点顶部 → 已还原 (geometry={w.geometry().width()}x{w.geometry().height()})")
|
||||
|
||||
print("[6/8] 真实事件投递:4 边 + 3 角都能拖动(经子控件传播/事件过滤器)")
|
||||
cases = [
|
||||
# (名称, 按压点函数, 拖动增量, 期望 edge, 断言函数)
|
||||
("L", lambda W, H: QPoint(3, H // 2), (60, 0), "L",
|
||||
lambda g0, g1: g1.x() > g0.x()),
|
||||
("R", lambda W, H: QPoint(W - 4, H // 2), (60, 0), "R",
|
||||
lambda g0, g1: g1.width() > g0.width()),
|
||||
("T", lambda W, H: QPoint(W // 2, 3), (0, 60), "T",
|
||||
lambda g0, g1: g1.y() > g0.y()),
|
||||
("B", lambda W, H: QPoint(W // 2, H - 4), (0, 60), "B",
|
||||
lambda g0, g1: g1.height() > g0.height()),
|
||||
("TL", lambda W, H: QPoint(3, 3), (60, 60), "TL",
|
||||
lambda g0, g1: g1.x() > g0.x() and g1.y() > g0.y()),
|
||||
("TR", lambda W, H: QPoint(W - 4, 3), (-60, 60), "TR",
|
||||
lambda g0, g1: g1.y() > g0.y()),
|
||||
("BL", lambda W, H: QPoint(3, H - 4), (60, -60), "BL",
|
||||
lambda g0, g1: g1.x() > g0.x()),
|
||||
]
|
||||
for name, pos_fn, delta, edge, check in cases:
|
||||
w.resize(800, 600)
|
||||
w.show()
|
||||
app.processEvents()
|
||||
W, H = w.width(), w.height()
|
||||
hit, geo0, geo1 = try_real_drag(w, pos_fn(W, H), delta, edge)
|
||||
assert check(geo0, geo1), \
|
||||
f"{name}: 几何未生效 {geo0.getRect()} → {geo1.getRect()} (命中 {hit})"
|
||||
print(f" ✓ {name:2s} 命中 {hit:10s} → {geo0.width()}x{geo0.height()} "
|
||||
f"→ {geo1.width()}x{geo1.height()}")
|
||||
# BR:原生 QSizeGrip 负责(真实桌面可拖;offscreen 下只验证 grip 存在且不被过滤器拦截)
|
||||
w.resize(800, 600)
|
||||
w.show()
|
||||
app.processEvents()
|
||||
br_child = w.childAt(QPoint(w.width() - 4, w.height() - 4))
|
||||
assert isinstance(br_child, QSizeGrip), f"右下角应为 QSizeGrip, 实际 {type(br_child).__name__}"
|
||||
print(f" ✓ BR 命中 QSizeGrip(原生角拖保留)")
|
||||
|
||||
print("[7/8] 最大化时:真实按压菜单栏顶缘 → 还原(事件过滤器路径)")
|
||||
w.resize(800, 600)
|
||||
w.showMaximized()
|
||||
app.processEvents()
|
||||
assert w.isMaximized()
|
||||
mb = w.menuBar()
|
||||
local = QPoint(mb.width() // 2, 2) # 菜单栏局部坐标,顶缘 6px 带内
|
||||
deliver(mb, QEvent.MouseButtonPress, local,
|
||||
button=Qt.LeftButton, buttons=Qt.LeftButton)
|
||||
app.processEvents()
|
||||
assert not w.isMaximized(), "事件过滤器路径:最大化时按菜单栏顶缘应还原"
|
||||
deliver(mb, QEvent.MouseButtonRelease, local,
|
||||
button=Qt.LeftButton, buttons=Qt.NoButton)
|
||||
assert w._resize_edge == ""
|
||||
print(f" ✓ 菜单栏顶缘按压 → 已还原 ({w.width()}x{w.height()})")
|
||||
|
||||
print("[8/8] 最小尺寸合理性(回归:曾被 MinimumSizeHint 撑到 1121px 高)")
|
||||
assert w.minimumWidth() <= 800, f"最小宽度异常: {w.minimumWidth()}"
|
||||
assert w.minimumHeight() <= 600, f"最小高度异常: {w.minimumHeight()}"
|
||||
w.resize(1280, 800)
|
||||
app.processEvents()
|
||||
assert w.width() == 1280 and w.height() == 800, \
|
||||
f"resize(1280,800) 被 clamp 成 {w.width()}x{w.height()}"
|
||||
# 能缩小到最小尺寸
|
||||
w.resize(760, 480)
|
||||
app.processEvents()
|
||||
assert w.width() == 760 and w.height() == 480, \
|
||||
f"无法缩到下限: {w.width()}x{w.height()}"
|
||||
print(f" ✓ minimumSize={w.minimumWidth()}x{w.minimumHeight()}, "
|
||||
f"resize(1280,800)/resize(760,480) 均精确生效")
|
||||
|
||||
print("\n窗口边缘拖动测试通过 ✓")
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user