feat: 完善一级关系编辑 UI 与视图体验
This commit is contained in:
@@ -0,0 +1,275 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ctypes
|
||||
import ctypes.wintypes
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from PySide6.QtCore import QEvent, QObject, QPoint, Qt, qInstallMessageHandler
|
||||
from PySide6.QtTest import QTest
|
||||
from PySide6.QtWidgets import QApplication, QMessageBox, QWidget
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from step_editor.app import DEFAULT_MODEL_PATH, StepEditorWindow, _suppress_vtk_output_window
|
||||
|
||||
|
||||
def _rect_tuple(hwnd: int) -> tuple[int, int, int, int]:
|
||||
rect = ctypes.wintypes.RECT()
|
||||
if not ctypes.windll.user32.GetWindowRect(hwnd, ctypes.byref(rect)):
|
||||
return (0, 0, 0, 0)
|
||||
return (int(rect.left), int(rect.top), int(rect.right), int(rect.bottom))
|
||||
|
||||
|
||||
def _window_text(hwnd: int) -> str:
|
||||
length = ctypes.windll.user32.GetWindowTextLengthW(hwnd)
|
||||
buffer = ctypes.create_unicode_buffer(max(length + 1, 1))
|
||||
ctypes.windll.user32.GetWindowTextW(hwnd, buffer, len(buffer))
|
||||
return buffer.value
|
||||
|
||||
|
||||
def _class_name(hwnd: int) -> str:
|
||||
buffer = ctypes.create_unicode_buffer(256)
|
||||
ctypes.windll.user32.GetClassNameW(hwnd, buffer, len(buffer))
|
||||
return buffer.value
|
||||
|
||||
|
||||
def _visible_native_windows_for_process(pid: int) -> list[dict[str, object]]:
|
||||
windows: list[dict[str, object]] = []
|
||||
|
||||
@ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.c_void_p, ctypes.c_void_p)
|
||||
def callback(hwnd, _lparam):
|
||||
window_pid = ctypes.wintypes.DWORD()
|
||||
ctypes.windll.user32.GetWindowThreadProcessId(hwnd, ctypes.byref(window_pid))
|
||||
if int(window_pid.value) != pid:
|
||||
return True
|
||||
if not ctypes.windll.user32.IsWindowVisible(hwnd):
|
||||
return True
|
||||
left, top, right, bottom = _rect_tuple(int(hwnd))
|
||||
width = max(0, right - left)
|
||||
height = max(0, bottom - top)
|
||||
if width <= 0 or height <= 0:
|
||||
return True
|
||||
windows.append(
|
||||
{
|
||||
"hwnd": int(hwnd),
|
||||
"class": _class_name(int(hwnd)),
|
||||
"title": _window_text(int(hwnd)),
|
||||
"rect": [left, top, right, bottom],
|
||||
"size": [width, height],
|
||||
}
|
||||
)
|
||||
return True
|
||||
|
||||
ctypes.windll.user32.EnumWindows(callback, 0)
|
||||
return windows
|
||||
|
||||
|
||||
def _widget_text(widget: QWidget) -> str:
|
||||
if isinstance(widget, QMessageBox):
|
||||
return str(widget.text() or "")
|
||||
getter = getattr(widget, "text", None)
|
||||
if callable(getter):
|
||||
try:
|
||||
value = getter()
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
except Exception:
|
||||
return ""
|
||||
return ""
|
||||
|
||||
|
||||
class WindowProbe(QObject):
|
||||
def __init__(self, output_dir: Path) -> None:
|
||||
super().__init__()
|
||||
self.output_dir = output_dir
|
||||
self.events: list[dict[str, object]] = []
|
||||
self.native_samples: list[dict[str, object]] = []
|
||||
self._native_seen: set[tuple[object, ...]] = set()
|
||||
|
||||
def eventFilter(self, watched, event):
|
||||
if isinstance(watched, QWidget) and event.type() in {
|
||||
QEvent.Type.Show,
|
||||
QEvent.Type.Hide,
|
||||
QEvent.Type.WindowActivate,
|
||||
QEvent.Type.ToolTip,
|
||||
}:
|
||||
if watched.isWindow() or event.type() == QEvent.Type.ToolTip:
|
||||
self.events.append(
|
||||
{
|
||||
"event": event.type().name,
|
||||
"qt_class": type(watched).__name__,
|
||||
"object_name": watched.objectName(),
|
||||
"title": watched.windowTitle(),
|
||||
"text": _widget_text(watched),
|
||||
"is_window": watched.isWindow(),
|
||||
"visible": watched.isVisible(),
|
||||
"size": [watched.width(), watched.height()],
|
||||
"flags": int(watched.windowFlags()),
|
||||
}
|
||||
)
|
||||
return False
|
||||
|
||||
def sample_native(self, label: str) -> None:
|
||||
for item in _visible_native_windows_for_process(os.getpid()):
|
||||
key = (item["hwnd"], item["class"], item["title"], tuple(item["rect"]))
|
||||
if key in self._native_seen:
|
||||
continue
|
||||
self._native_seen.add(key)
|
||||
sample = dict(item)
|
||||
sample["label"] = label
|
||||
self.native_samples.append(sample)
|
||||
|
||||
def grab_qt_windows(self, label: str) -> None:
|
||||
app = QApplication.instance()
|
||||
if app is None:
|
||||
return
|
||||
for index, widget in enumerate(app.topLevelWidgets()):
|
||||
if not widget.isVisible():
|
||||
continue
|
||||
name = widget.objectName() or type(widget).__name__
|
||||
safe_name = "".join(ch if ch.isalnum() or ch in "-_" else "_" for ch in name)
|
||||
target = self.output_dir / f"{label}_qt_{index}_{safe_name}.png"
|
||||
widget.grab().save(str(target))
|
||||
|
||||
def grab_native_windows(self, label: str) -> None:
|
||||
app = QApplication.instance()
|
||||
if app is None:
|
||||
return
|
||||
screen = app.primaryScreen()
|
||||
if screen is None:
|
||||
return
|
||||
for index, item in enumerate(_visible_native_windows_for_process(os.getpid())):
|
||||
hwnd = int(item["hwnd"])
|
||||
safe_class = "".join(ch if ch.isalnum() or ch in "-_" else "_" for ch in str(item["class"]))
|
||||
target = self.output_dir / f"{label}_native_{index}_{safe_class}_{hwnd}.png"
|
||||
screen.grabWindow(hwnd).save(str(target))
|
||||
|
||||
def write_log(self) -> None:
|
||||
payload = {
|
||||
"qt_events": self.events,
|
||||
"native_samples": self.native_samples,
|
||||
}
|
||||
(self.output_dir / "transient_windows_log.json").write_text(
|
||||
json.dumps(payload, ensure_ascii=False, indent=2),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def _pump(app: QApplication, probe: WindowProbe, label: str, ms: int) -> None:
|
||||
deadline = time.monotonic() + ms / 1000.0
|
||||
while time.monotonic() < deadline:
|
||||
app.processEvents()
|
||||
probe.sample_native(label)
|
||||
time.sleep(0.005)
|
||||
|
||||
|
||||
def _find_face_click_point(window: StepEditorWindow) -> QPoint:
|
||||
width = max(1, window.vtk_widget.width())
|
||||
height = max(1, window.vtk_widget.height())
|
||||
fractions = [0.5, 0.42, 0.58, 0.35, 0.65, 0.28, 0.72]
|
||||
for fy in fractions:
|
||||
for fx in fractions:
|
||||
x = int(width * fx)
|
||||
y_qt = int(height * fy)
|
||||
y_vtk = int(height - y_qt)
|
||||
target = window._pick_selection_target("Face", x, y_vtk)
|
||||
if target is not None:
|
||||
return QPoint(x, y_qt)
|
||||
return QPoint(width // 2, height // 2)
|
||||
|
||||
|
||||
def _click_first_property_card(window: StepEditorWindow) -> bool:
|
||||
rows = getattr(window, "property_card_rows", {})
|
||||
if not isinstance(rows, dict):
|
||||
return False
|
||||
for widgets in rows.values():
|
||||
card = widgets.get("card") if isinstance(widgets, dict) else None
|
||||
if isinstance(card, QWidget) and card.isVisible():
|
||||
QTest.mouseClick(card, Qt.MouseButton.LeftButton, Qt.KeyboardModifier.NoModifier, QPoint(20, max(2, card.height() // 2)))
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--output-dir", default="assets/screenshots/transient_probe")
|
||||
parser.add_argument("--model", default=str(DEFAULT_MODEL_PATH))
|
||||
parser.add_argument("--offscreen", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.offscreen:
|
||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||
|
||||
output_dir = Path(args.output_dir)
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
qt_messages: list[str] = []
|
||||
|
||||
def message_handler(_mode, _context, message):
|
||||
text = str(message)
|
||||
qt_messages.append(text)
|
||||
print(text, file=sys.stderr)
|
||||
|
||||
qInstallMessageHandler(message_handler)
|
||||
_suppress_vtk_output_window()
|
||||
app = QApplication(["transient-window-probe"])
|
||||
probe = WindowProbe(output_dir)
|
||||
app.installEventFilter(probe)
|
||||
|
||||
window = StepEditorWindow(Path(args.model), background_load=False)
|
||||
window.show()
|
||||
window.resize(1280, 820)
|
||||
_pump(app, probe, "after_show", 300)
|
||||
|
||||
window.load_step(Path(args.model), background=False)
|
||||
_pump(app, probe, "after_load", 800)
|
||||
window.render_window.Render()
|
||||
_pump(app, probe, "after_render", 300)
|
||||
probe.grab_qt_windows("after_render")
|
||||
probe.grab_native_windows("after_render")
|
||||
|
||||
window._set_selection_mode("Face")
|
||||
point = _find_face_click_point(window)
|
||||
QTest.mouseClick(window.vtk_widget, Qt.MouseButton.LeftButton, Qt.KeyboardModifier.NoModifier, point)
|
||||
_pump(app, probe, "after_face_click", 900)
|
||||
probe.grab_qt_windows("after_face_click")
|
||||
probe.grab_native_windows("after_face_click")
|
||||
|
||||
clicked_card = _click_first_property_card(window)
|
||||
_pump(app, probe, "after_property_click", 900)
|
||||
probe.grab_qt_windows("after_property_click")
|
||||
probe.grab_native_windows("after_property_click")
|
||||
|
||||
(output_dir / "probe_summary.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"model": str(Path(args.model)),
|
||||
"face_click_point": [point.x(), point.y()],
|
||||
"selected_kind": getattr(window, "selected_kind", None),
|
||||
"selected_face_id": getattr(window, "selected_face_id", None),
|
||||
"clicked_property_card": clicked_card,
|
||||
"qt_message_count": len(qt_messages),
|
||||
"qt_messages": qt_messages[-50:],
|
||||
},
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
probe.write_log()
|
||||
window.close()
|
||||
_pump(app, probe, "after_close", 100)
|
||||
app.quit()
|
||||
print(f"transient window probe wrote {output_dir}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user