""" 窗口边缘拖动测试: - _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()