feat: window edge drag-to-resize (normal + from maximized)
User asked: '窗口应该可以前后左右拉' Added custom edge drag handling in MainWindow (QMainWindow's default resize support exists but ignored + we want full control for maximized state too): New methods in ui/main_window.py: - _hit_test_edge(pos) -> 'L'/'R'/'T'/'B'/'TL'/'TR'/'BL'/'BR'/'' 6px margin around window. Returns which edge (or corner) the cursor is on. Returns '' when maximized/fullscreen. - _edge_to_cursor(edge): sets Qt.SizeHor/Ver/FDiag/BDiag cursor - _do_resize(global_pos): computes new geometry based on drag delta. Respects self.minimumWidth/Height (QMainWindow's auto min size from menu/toolbar/statusbar) and screen bounds. - _restore_from_max(global_pos): when maximized, click+drag on TOP edge → showNormal() + set geometry to mouse-relative size + continue drag from 'TL' corner (like Windows native behavior) - mousePressEvent/Move/Release: hook into resize state - leaveEvent: clear cursor when leaving window - changeEvent: clear resize state on window state change State fields added in __init__: - self._resize_edge, self._resize_start_geo, self._resize_start_pos - self._resize_margin = 6 - self._dragging_from_max = False Fix: 'QTabWidget.RightSide' was wrong API; used 'QTabBar.RightSide' in the multi-tab commit (unrelated, but I noticed while testing). New test_resize.py (5 tests, all pass): - 8-region hit-test (4 edges + 4 corners) - Right-edge drag → width grows - Left-edge drag → x moves right (width limited by minW) - Top-left corner drag - Maximized + click top edge → restored to normal Existing tests (core 6/6, UI 3/3) still pass. Build: 57MB exe.
This commit is contained in:
@@ -47,6 +47,17 @@ class MainWindow(QMainWindow):
|
||||
self._build_menu()
|
||||
self._build_statusbar()
|
||||
self._apply_theme()
|
||||
|
||||
# 边缘拖动调整大小(含最大化时)—— 状态字段
|
||||
self._resize_edge: str = "" # '' / 'L'/'R'/'T'/'B'/'TL'/'TR'/'BL'/'BR'
|
||||
self._resize_start_geo = None # 拖动开始时窗口 geometry
|
||||
self._resize_start_pos = None # 拖动开始时鼠标全局坐标
|
||||
# 边缘检测阈值(像素)
|
||||
self._resize_margin = 6
|
||||
# 最大化时拖动:是否正在拖(避免和正常拖动冲突)
|
||||
self._dragging_from_max = False
|
||||
# 鼠标光标缓存
|
||||
self.setMouseTracking(True)
|
||||
self._load_hosts_to_list()
|
||||
|
||||
# ============================================================
|
||||
@@ -488,6 +499,193 @@ class MainWindow(QMainWindow):
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "导入失败", str(e))
|
||||
|
||||
# ============================================================
|
||||
# 窗口边缘拖动调整大小(普通 + 最大化时)
|
||||
# ============================================================
|
||||
def _hit_test_edge(self, pos) -> str:
|
||||
"""根据 pos(窗口内坐标)返回哪条边被命中
|
||||
返回 '' / 'L'/'R'/'T'/'B'/'TL'/'TR'/'BL'/'BR'
|
||||
|
||||
最大化/全屏时不返回(避免和系统最大化手势冲突)。
|
||||
"""
|
||||
if self.isMaximized() or self.isFullScreen():
|
||||
return ""
|
||||
w, h = self.width(), self.height()
|
||||
m = self._resize_margin
|
||||
x, y = pos.x(), pos.y()
|
||||
# 角
|
||||
if x <= m and y <= m:
|
||||
return "TL"
|
||||
if x >= w - m and y <= m:
|
||||
return "TR"
|
||||
if x <= m and y >= h - m:
|
||||
return "BL"
|
||||
if x >= w - m and y >= h - m:
|
||||
return "BR"
|
||||
# 边
|
||||
if x <= m:
|
||||
return "L"
|
||||
if x >= w - m:
|
||||
return "R"
|
||||
if y <= m:
|
||||
return "T"
|
||||
if y >= h - m:
|
||||
return "B"
|
||||
return ""
|
||||
|
||||
def _edge_to_cursor(self, edge: str):
|
||||
from PyQt5.QtGui import QCursor
|
||||
from PyQt5.QtCore import Qt as _Qt
|
||||
cursors = {
|
||||
"L": _Qt.SizeHorCursor, "R": _Qt.SizeHorCursor,
|
||||
"T": _Qt.SizeVerCursor, "B": _Qt.SizeVerCursor,
|
||||
"TL": _Qt.SizeFDiagCursor, "BR": _Qt.SizeFDiagCursor,
|
||||
"TR": _Qt.SizeBDiagCursor, "BL": _Qt.SizeBDiagCursor,
|
||||
}
|
||||
c = cursors.get(edge)
|
||||
if c is not None:
|
||||
self.setCursor(QCursor(c))
|
||||
|
||||
def _restore_cursor(self):
|
||||
self.unsetCursor()
|
||||
|
||||
def mouseMoveEvent(self, e):
|
||||
# 拖动中
|
||||
if self._resize_edge and self._resize_start_geo and self._resize_start_pos:
|
||||
self._do_resize(e.globalPos())
|
||||
return
|
||||
# 否则只更新光标
|
||||
if not self.isMaximized() and not self.isFullScreen():
|
||||
edge = self._hit_test_edge(e.pos())
|
||||
if edge:
|
||||
self._edge_to_cursor(edge)
|
||||
else:
|
||||
self._restore_cursor()
|
||||
else:
|
||||
self._restore_cursor()
|
||||
super().mouseMoveEvent(e)
|
||||
|
||||
def mousePressEvent(self, e):
|
||||
if e.button() == Qt.LeftButton:
|
||||
# 特殊情况:窗口最大化时,从顶部边缘按下 → 还原并跟随鼠标调整
|
||||
if self.isMaximized() and e.pos().y() <= self._resize_margin:
|
||||
self._restore_from_max(e.globalPos())
|
||||
e.accept()
|
||||
return
|
||||
edge = self._hit_test_edge(e.pos())
|
||||
if edge:
|
||||
self._resize_edge = edge
|
||||
self._resize_start_geo = self.geometry()
|
||||
self._resize_start_pos = e.globalPos()
|
||||
e.accept()
|
||||
return
|
||||
super().mousePressEvent(e)
|
||||
|
||||
def _restore_from_max(self, global_pos):
|
||||
"""从最大化状态恢复窗口,并将宽度按鼠标位置调整"""
|
||||
# 记录当前最大化的位置
|
||||
self._dragging_from_max = True
|
||||
# 屏幕可用区域
|
||||
screen = QApplication.primaryScreen().availableGeometry()
|
||||
# 还原(先恢复原始 geometry)
|
||||
self.showNormal()
|
||||
# 设定宽度:按鼠标 X 位置占屏幕的比例
|
||||
ratio = max(0.2, min(0.8, (global_pos.x() - screen.x()) / screen.width()))
|
||||
new_w = int(screen.width() * ratio)
|
||||
new_w = max(self.minimumWidth(), new_w)
|
||||
new_h = int(screen.height() * 0.85)
|
||||
new_h = max(self.minimumHeight(), new_h)
|
||||
new_x = max(screen.x(), global_pos.x() - new_w // 2)
|
||||
new_y = screen.y() + (screen.height() - new_h) // 2
|
||||
self.setGeometry(new_x, new_y, new_w, new_h)
|
||||
# 让后续 mouseMove 继续调整
|
||||
self._resize_edge = "TL" # 模拟从左上角拖
|
||||
self._resize_start_geo = self.geometry()
|
||||
self._resize_start_pos = global_pos
|
||||
|
||||
def mouseReleaseEvent(self, e):
|
||||
if self._resize_edge:
|
||||
self._resize_edge = ""
|
||||
self._resize_start_geo = None
|
||||
self._resize_start_pos = None
|
||||
e.accept()
|
||||
return
|
||||
super().mouseReleaseEvent(e)
|
||||
|
||||
def _do_resize(self, global_pos):
|
||||
"""根据当前鼠标位置和拖动方向调整窗口 geometry"""
|
||||
geo = self._resize_start_geo
|
||||
if geo is None:
|
||||
return
|
||||
dx = global_pos.x() - self._resize_start_pos.x()
|
||||
dy = global_pos.y() - self._resize_start_pos.y()
|
||||
# 屏幕可用区域(用于限制)
|
||||
try:
|
||||
screen = QApplication.primaryScreen().availableGeometry()
|
||||
except Exception:
|
||||
from PyQt5.QtCore import QRect
|
||||
screen = QRect(0, 0, 10000, 10000)
|
||||
# 最小尺寸:尊重 QMainWindow 自己的 minimumWidth/minimumHeight
|
||||
min_w = self.minimumWidth() if self.minimumWidth() > 0 else 400
|
||||
min_h = self.minimumHeight() if self.minimumHeight() > 0 else 300
|
||||
new_x, new_y, new_w, new_h = geo.x(), geo.y(), geo.width(), geo.height()
|
||||
|
||||
edge = self._resize_edge
|
||||
# 左边
|
||||
if "L" in edge:
|
||||
new_x = geo.x() + dx
|
||||
new_w = geo.width() - dx
|
||||
if new_w < min_w:
|
||||
new_w = min_w
|
||||
new_x = geo.x() + geo.width() - min_w
|
||||
# 右边
|
||||
if "R" in edge:
|
||||
new_w = geo.width() + dx
|
||||
if new_w < min_w:
|
||||
new_w = min_w
|
||||
# 上边
|
||||
if "T" in edge:
|
||||
new_y = geo.y() + dy
|
||||
new_h = geo.height() - dy
|
||||
if new_h < min_h:
|
||||
new_h = min_h
|
||||
new_y = geo.y() + geo.height() - min_h
|
||||
# 下边
|
||||
if "B" in edge:
|
||||
new_h = geo.height() + dy
|
||||
if new_h < min_h:
|
||||
new_h = min_h
|
||||
# 屏幕限制
|
||||
if new_x < screen.x():
|
||||
new_x = screen.x()
|
||||
if new_y < screen.y():
|
||||
new_y = screen.y()
|
||||
if new_x + new_w > screen.x() + screen.width():
|
||||
new_w = screen.x() + screen.width() - new_x
|
||||
if new_y + new_h > screen.y() + screen.height():
|
||||
new_h = screen.y() + screen.height() - new_y
|
||||
# 拆成 move + resize + setGeometry 兜底(部分平台 setGeometry 会失败)
|
||||
self.resize(new_w, new_h)
|
||||
self.move(new_x, new_y)
|
||||
if (self.width(), self.height()) != (new_w, new_h):
|
||||
self.setGeometry(new_x, new_y, new_w, new_h)
|
||||
|
||||
def leaveEvent(self, e):
|
||||
if not self._resize_edge:
|
||||
self._restore_cursor()
|
||||
super().leaveEvent(e)
|
||||
|
||||
def changeEvent(self, e):
|
||||
"""窗口状态变化:清除残留的拖动状态"""
|
||||
from PyQt5.QtCore import QEvent as _QEvent
|
||||
if e.type() == _QEvent.WindowStateChange:
|
||||
# 状态变化(最大化/还原)时清掉拖动状态
|
||||
self._resize_edge = ""
|
||||
self._resize_start_geo = None
|
||||
self._resize_start_pos = None
|
||||
self._restore_cursor()
|
||||
super().changeEvent(e)
|
||||
|
||||
def closeEvent(self, e):
|
||||
try:
|
||||
self.monitor._stop_worker()
|
||||
|
||||
Reference in New Issue
Block a user