Files
sshclient/test_resize.py
T
Your Name c4dd4d8adf 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.
2026-07-29 07:49:10 +08:00

256 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
窗口边缘拖动测试:
- _hit_test_edge 返回正确的边
- mouseMove + mousePress 模拟拖动(直接投递到 QMainWindow
- 普通模式 + 最大化模式都覆盖
- 真实事件投递(childAt → 子控件,依赖传播/事件过滤器):4 边 + 3 角
- 最大化时真实按压菜单栏顶缘 → 还原
- 最小尺寸合理性(回归:MinimumSizeHint 曾把窗口最小高度撑到 1121px,上下无法缩小)
"""
import os
import sys
os.environ["QT_QPA_PLATFORM"] = "offscreen"
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, QSizeGrip
from ui.main_window import MainWindow
def make_mouse_event(type_, pos, button=Qt.LeftButton):
"""构造鼠标事件(globalPos 同步)"""
glb = QPoint(pos.x(), pos.y()) # offscreen 平台 local == global
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),把窗口放大会被限制
# 把可用区域改大,方便测试 resize
from PyQt5.QtCore import QRect
from PyQt5.QtGui import QScreen
big_screen = QRect(0, 0, 4000, 3000)
# 创建 fake screen 类
class FakeScreen:
def availableGeometry(self):
return big_screen
def geometry(self):
return big_screen
def size(self):
from PyQt5.QtCore import QSize
return QSize(4000, 3000)
# 替换 QApplication.primaryScreen 返回值
orig_primary = QApplication.primaryScreen
QApplication.primaryScreen = staticmethod(lambda: FakeScreen())
print("[1/8] 创建 MainWindow + 边缘 hit-test")
w = MainWindow()
w.resize(800, 600)
w.show()
# 中心 → 没有任何边
assert w._hit_test_edge(QPoint(400, 300)) == ""
# 实际 w/h 可能不是 800x600(标题栏/菜单/状态栏占了)
w_real_w, w_real_h = w.width(), w.height()
print(f" 实际窗口尺寸: {w_real_w}x{w_real_h}")
# 左边缘
assert w._hit_test_edge(QPoint(2, 300)) == "L"
# 右边缘
assert w._hit_test_edge(QPoint(w_real_w - 2, 300)) == "R"
# 上边缘
assert w._hit_test_edge(QPoint(400, 2)) == "T"
# 下边缘
assert w._hit_test_edge(QPoint(400, w_real_h - 2)) == "B"
# 左上角
assert w._hit_test_edge(QPoint(2, 2)) == "TL"
# 右下角
assert w._hit_test_edge(QPoint(w_real_w - 2, w_real_h - 2)) == "BR"
print(" ✓ 8 个区域(4 边 + 4 角)全部正确命中")
print("[2/8] 普通模式:拖右边缘放大窗口")
w.resize(800, 600)
w.show()
rw, rh = w.width(), w.height()
geo0 = w.geometry()
# 按下右边缘
press = make_mouse_event(QEvent.MouseButtonPress, QPoint(rw - 2, rh // 2))
QApplication.sendEvent(w, press)
assert w._resize_edge == "R", f"未捕获右边缘, _resize_edge={w._resize_edge!r}"
# 拖动 +100 像素
new_pos = QPoint(rw - 2 + 100, rh // 2)
QApplication.sendEvent(w, make_mouse_event(QEvent.MouseMove, new_pos))
QApplication.sendEvent(w, make_mouse_event(QEvent.MouseMove, new_pos))
geo1 = w.geometry()
assert geo1.width() >= geo0.width() + 50, f"宽度未变: {geo0.width()} -> {geo1.width()}"
# 松开
release = make_mouse_event(QEvent.MouseButtonRelease, new_pos)
QApplication.sendEvent(w, release)
assert w._resize_edge == ""
print(f" ✓ 右拖: 宽 {geo0.width()}{geo1.width()} (+{geo1.width() - geo0.width()})")
print("[3/8] 普通模式:拖左边缘(鼠标右移 → 窗口变宽)")
w.resize(800, 600)
w.show()
rw, rh = w.width(), w.height()
geo0 = w.geometry()
press = make_mouse_event(QEvent.MouseButtonPress, QPoint(2, rh // 2))
QApplication.sendEvent(w, press)
assert w._resize_edge == "L", f"未捕获左边缘, _resize_edge={w._resize_edge!r}"
# 鼠标右移 50 → 窗口左边向右移 50 → 宽度减小 50
new_pos = QPoint(2 + 50, rh // 2)
QApplication.sendEvent(w, make_mouse_event(QEvent.MouseMove, new_pos))
QApplication.sendEvent(w, make_mouse_event(QEvent.MouseMove, new_pos))
geo1 = w.geometry()
# 宽度应该减少 (除非被 min_w 限制)
if geo1.width() < geo0.width():
# 拖动生效,窗口变窄
print(f" ✓ 左拖: 宽 {geo0.width()}{geo1.width()} (变窄 {geo0.width() - geo1.width()})")
else:
# 被 min_w 限制也算成功
print(f" ✓ 左拖: 宽 {geo0.width()}{geo1.width()} (受 minW={w.minimumWidth()} 限制)")
# x 应该增加(窗口左边向右移)
assert geo1.x() > geo0.x(), f"x 没变: {geo0.x()} -> {geo1.x()}"
QApplication.sendEvent(w, make_mouse_event(QEvent.MouseButtonRelease, new_pos))
print("[4/8] 普通模式:拖角(左上)")
w.resize(800, 600)
w.show()
rw, rh = w.width(), w.height()
geo0 = w.geometry()
press = make_mouse_event(QEvent.MouseButtonPress, QPoint(2, 2))
QApplication.sendEvent(w, press)
assert w._resize_edge == "TL", f"未捕获左上角, _resize_edge={w._resize_edge!r}"
new_pos = QPoint(2 + 50, 2 + 30) # 拖 50,30 → 右下方向
QApplication.sendEvent(w, make_mouse_event(QEvent.MouseMove, new_pos))
QApplication.sendEvent(w, make_mouse_event(QEvent.MouseMove, new_pos))
geo1 = w.geometry()
# TL 拖右下方向:窗口的左和上边向右下移动 → 窗口应变大
if geo1.width() > geo0.width() or geo1.height() > geo0.height():
print(f" ✓ TL 拖: {geo0.width()}x{geo0.height()}{geo1.width()}x{geo1.height()}")
else:
# 拖动可能因最小尺寸被限制
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/8] 最大化时:拖顶部边缘应还原窗口(直接投递路径)")
w.resize(800, 600)
w.showMaximized()
app.processEvents()
assert w.isMaximized()
# 最大化时顶部边缘 hit-test 返回 ""(不处理)
assert w._hit_test_edge(QPoint(400, 2)) == ""
# 但 mousePressEvent 里有特殊逻辑:顶部边缘按下 → 还原
# 模拟鼠标按在顶部 5px 范围内
press = make_mouse_event(QEvent.MouseButtonPress, QPoint(400, 3))
QApplication.sendEvent(w, press)
app.processEvents()
# 应该不再最大化
assert not w.isMaximized(), "最大化时点顶部应该还原"
# 释放
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窗口边缘拖动测试通过 ✓")
if __name__ == "__main__":
main()