niumasoftware/ui/flow_layout.py
2026-04-03 21:50:36 +08:00

192 lines
6.2 KiB
Python

from PyQt6.QtWidgets import QWidget
from PyQt6.QtCore import QPoint, QTimer, Qt, QRect, QSize
from PyQt6.QtGui import QPainter, QColor, QPen
ITEM_W = 72
ITEM_H = 80
GAP = 4
RUBBER_MIN = 4 # 小于此尺寸的框选视为误触
class FlowContainer(QWidget):
"""手动绝对定位的流式容器,完全绕开 QLayout。"""
def __init__(self, parent=None):
super().__init__(parent)
self._widgets: list[QWidget] = []
self._pending = False
self._drag_highlight = False
self.setMouseTracking(True)
self._rubber_active = False
self._rubber_origin = QPoint()
self._rubber_rect: QRect | None = None
def add_widget(self, w: QWidget):
w.setParent(self)
self._widgets.append(w)
if self.width() > 0:
self._relayout()
else:
self._schedule_relayout()
def clear_widgets(self):
for w in self._widgets:
w.hide()
w.setParent(None)
self._widgets.clear()
self.setFixedHeight(60)
def get_widgets(self) -> list:
return list(self._widgets)
def set_drag_highlight(self, active: bool):
if self._drag_highlight != active:
self._drag_highlight = active
self.update()
def showEvent(self, event):
super().showEvent(event)
self._schedule_relayout()
def resizeEvent(self, event):
self._relayout()
super().resizeEvent(event)
def _schedule_relayout(self):
if not self._pending:
self._pending = True
QTimer.singleShot(0, self._delayed_relayout)
def _delayed_relayout(self):
self._pending = False
self._relayout()
def _relayout(self):
avail_w = self.width()
if avail_w <= 0:
p = self.parent()
while p:
if p.width() > 0:
avail_w = p.width() - 16
break
p = p.parent()
if avail_w <= 0:
self._schedule_relayout()
return
x, y = GAP, GAP
row_h = 0
for w in self._widgets:
if not w.isVisible():
w.move(-9999, -9999)
continue
iw = w.width() if w.width() > 0 else ITEM_W
ih = w.height() if w.height() > 0 else ITEM_H
if row_h > 0 and x + iw + GAP > avail_w:
x = GAP
y += row_h + GAP
row_h = 0
w.move(x, y)
x += iw + GAP
row_h = max(row_h, ih)
visible = [w for w in self._widgets if w.isVisible()]
total_h = (y + row_h + GAP) if visible else 60
self.setFixedHeight(max(total_h, 0))
def index_at(self, pos: QPoint) -> int:
"""拖拽插入位置:与流式布局顺序一致,同行每个图标单独判断,不再只认行内第一个。"""
visible = [w for w in self._widgets if w.isVisible()]
if not visible:
return 0
if pos.y() < visible[0].geometry().top():
return 0
for w in visible:
g = w.geometry()
if pos.y() < g.top():
return self._widgets.index(w)
if g.top() <= pos.y() <= g.bottom():
if pos.x() < g.left():
return self._widgets.index(w)
if pos.x() <= g.right():
mid = (g.left() + g.right()) // 2
if pos.x() < mid:
return self._widgets.index(w)
return self._widgets.index(w) + 1
# 落在该行、但在本图标右侧,继续看下一个(同行或下一行)
return len(self._widgets)
def _clear_selection_except(self, keep):
for w in self._widgets:
if w is not keep and hasattr(w, "_set_selected"):
w._set_selected(False)
def clear_selection(self):
self._clear_selection_except(None)
def _begin_rubber(self, pos: QPoint):
self.clear_selection()
self._rubber_active = True
self._rubber_origin = pos
self._rubber_rect = QRect(pos, QSize())
self.grabMouse()
self.update()
def mousePressEvent(self, event):
if event.button() == Qt.MouseButton.LeftButton:
ch = self.childAt(event.pos())
if ch is None:
self._begin_rubber(event.pos())
return
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
if self._rubber_active:
self._rubber_rect = QRect(self._rubber_origin, event.pos()).normalized()
self.update()
return
super().mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
if self._rubber_active and event.button() == Qt.MouseButton.LeftButton:
self._rubber_active = False
self.releaseMouse()
r = self._rubber_rect
self._rubber_rect = None
self.update()
if r is not None and r.width() >= RUBBER_MIN and r.height() >= RUBBER_MIN:
self.clear_selection()
for w in self._widgets:
if not w.isVisible():
continue
if hasattr(w, "_set_selected") and r.intersects(w.geometry()):
w._set_selected(True)
return
super().mouseReleaseEvent(event)
def paintEvent(self, event):
super().paintEvent(event)
need_drag = self._drag_highlight
need_rubber = self._rubber_rect is not None and self._rubber_active
if not need_drag and not need_rubber:
return
p = QPainter(self)
p.setRenderHint(QPainter.RenderHint.Antialiasing)
if need_drag:
pen = QPen(QColor("#4a9eff"), 2, Qt.PenStyle.DashLine)
pen.setDashPattern([6, 4])
p.setPen(pen)
p.setBrush(QColor(74, 158, 255, 25))
p.drawRoundedRect(2, 2, self.width() - 4, self.height() - 4, 6, 6)
if need_rubber:
pen = QPen(QColor("#4a9eff"), 1, Qt.PenStyle.DashLine)
p.setPen(pen)
p.setBrush(QColor(74, 158, 255, 40))
p.drawRoundedRect(self._rubber_rect, 2, 2)
p.end()