Files
mtgodot-poc/extension/tests/port_window_tree_test.cpp
T
shenleiandClaude Code b4c9d6cefc port(2V0-b): EterPythonLib 窗口树实现 + EterLib 资源层
- port: PythonWindow/PythonSlotWindow/PythonGridSlotWindow/PythonWindowManager(布局/命中/事件/插槽渲染)
- port: EterLib ReferenceObject/Resource/ResourceManager(资源引用与查找)
- platform: EterLib Util、EterPythonLib PythonGraphic
- tests: port.window_tree(需要解释器,不需要 40250 Client/pack)
- 顺带补上 4 个缺失的 Godot .uid

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-23 11:09:05 +09:00

320 lines
12 KiB
C++
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.
// port/EterPythonLib 的窗口树(批次 2V0-b):UI::CWindow / UI::CWindowManager 在真解释器上跑一遍。
//
// 这一层是 40250 整个 UI 的骨架:Python 侧每个窗口对象背后都是一个 CWindow,布局(UpdateRect 的
// 对齐规则)、命中(PickWindow 的 flag 语义)、拖动、焦点、按键捕获全在 C++ 里,Python 只收事件。
// wndMgr 模块(2V0-c)只是把这些函数包一层,所以这里直接按 C++ API 驱动,验的是同一份逻辑。
//
// 渲染不碰:Render()/OnRender() 需要 CPythonGraphic 和图形设备,那是 2V0-f 的事。解释器是必须的 ——
// CWindowManager 的构造函数就 Py_BuildValue("()"),事件也确实要发到 Python 处理器上。
//
// port_window_tree_test <python27.zip>
//
// 缺 python27.zip 时退 77ctest SKIP),MT_ASSETS_STRICT=1 时改成失败。
#include "EterPythonLib/StdAfx.h"
#include "EterPythonLib/PythonWindow.h"
#include "EterPythonLib/PythonWindowManager.h"
#include "ScriptLib/PythonLauncher.h"
#include "../src/platform/ScriptLib/PythonHost.h"
#include <cstdio>
#include <cstdlib>
#include <string>
static int g_failures = 0;
#define CHECK(cond) \
do { \
if (!(cond)) { \
std::fprintf(stderr, "%s:%d: CHECK(%s)\n", __FILE__, __LINE__, #cond); \
++g_failures; \
} \
} while (0)
// Python 侧的处理器:每个事件往 LOG 里记一条,C++ 这边取出来比对。这正是 40250 的 ui.py 做的事情
// Window 类的 OnMouseOverIn / OnKeyDown ... 都是普通的 Python 方法)。
static const char* const kHandlerScript =
"LOG = []\n"
"class Handler:\n"
" def __init__(self, name, eat = 0):\n"
" self.name = name\n"
" self.eat = eat\n"
" def OnMouseOverIn(self): LOG.append(self.name + ':in')\n"
" def OnMouseOverOut(self): LOG.append(self.name + ':out')\n"
" def OnMouseLeftButtonDown(self): LOG.append(self.name + ':down')\n"
" def OnMouseLeftButtonUp(self): LOG.append(self.name + ':up')\n"
" def OnSetFocus(self): LOG.append(self.name + ':focus')\n"
" def OnKillFocus(self): LOG.append(self.name + ':blur')\n"
" def OnUpdate(self): LOG.append(self.name + ':update')\n"
" def OnKeyDown(self, key):\n"
" LOG.append('%s:kd%d' % (self.name, key))\n"
" return self.eat\n"
" def OnKeyUp(self, key):\n"
" LOG.append('%s:ku%d' % (self.name, key))\n"
" return 1\n"
"A = Handler('A')\n"
"B = Handler('B')\n"
"C = Handler('C', 1)\n";
static PyObject* main_dict()
{
return PyModule_GetDict(PyImport_AddModule((char*) "__main__"));
}
static PyObject* handler(const char* name)
{
PyObject* object = PyDict_GetItemString(main_dict(), name); // Borrowed; __main__ 一直持有
if (!object)
std::fprintf(stderr, "handler('%s') missing\n", name);
return object;
}
// LOG 的内容,取完清空 —— 每个检查点只看自己那一段。
static std::string take_log()
{
PyObject* log = PyDict_GetItemString(main_dict(), "LOG");
if (!log || !PyList_Check(log))
return "<no LOG>";
std::string text;
for (Py_ssize_t i = 0; i < PyList_Size(log); ++i)
{
PyObject* item = PyList_GetItem(log, i);
if (!text.empty())
text += ' ';
text += PyString_Check(item) ? PyString_AS_STRING(item) : "?";
}
PyList_SetSlice(log, 0, PyList_Size(log), NULL);
return text;
}
static bool rect_is(UI::CWindow* window, long left, long top, long right, long bottom)
{
const RECT& rect = window->GetRect();
if (rect.left == left && rect.top == top && rect.right == right && rect.bottom == bottom)
return true;
std::fprintf(stderr, "rect(%s) = %ld,%ld,%ld,%ld, expected %ld,%ld,%ld,%ld\n", window->GetName(),
(long) rect.left, (long) rect.top, (long) rect.right, (long) rect.bottom,
left, top, right, bottom);
return false;
}
static void run(UI::CWindowManager& wm)
{
// 40250 是先 SetScreenSize 再 SetResolutionUserInterface 的 OnSizeChange)。分辨率为 0 时
// SetMousePosition 直接 return,鼠标坐标永远是 0 —— 这条顺序是有意义的。
wm.SetScreenSize(800, 600);
wm.SetResolution(800, 600);
CHECK(wm.GetScreenWidth() == 800 && wm.GetScreenHeight() == 600);
long hres = 0, vres = 0;
wm.GetResolution(hres, vres);
CHECK(hres == 800 && vres == 600);
CHECK(wm.GetAspect() == 800.0f / 600.0f);
// ---- 层与树 ----
UI::CWindow* a = wm.RegisterWindow(handler("A"), "UI");
a->SetName("A");
CHECK(std::string(a->GetParent()->GetName()) == "UI");
// 五个层(GAME/UI_BOTTOM/UI/TOP_MOST/CURTAIN)是 CLayerIsWindow() 为 FALSE,所以注册到层上的
// 窗口自己就是 root —— Python 侧的 GetRoot() 从来拿不到层。
CHECK(a->GetRoot() == a);
CHECK(!a->IsShow());
a->SetSize(100, 50);
a->SetPosition(10, 20);
a->Show();
CHECK(rect_is(a, 10, 20, 110, 70));
UI::CWindow* b = wm.RegisterWindow(handler("B"), "UI");
b->SetName("B");
wm.SetParent(b, a);
CHECK(b->GetParent() == a && a->GetChildCount() == 1);
CHECK(b->GetRoot() == a);
b->SetSize(20, 10);
b->SetPosition(5, 5);
b->Show();
CHECK(rect_is(b, 15, 25, 35, 35));
// ---- UpdateRect 的对齐规则(父窗口 100x50 @ 10,20,子窗口 20x10 @ 5,5----
b->SetHorizontalAlign(UI::CWindow::HORIZONTAL_ALIGN_RIGHT); // left = |100| - 5 + 10
CHECK(rect_is(b, 105, 25, 125, 35));
b->SetHorizontalAlign(UI::CWindow::HORIZONTAL_ALIGN_CENTER); // left = 100/2 - 20/2 + 5 + 10
CHECK(rect_is(b, 55, 25, 75, 35));
b->SetVerticalAlign(UI::CWindow::VERTICAL_ALIGN_BOTTOM); // top = 50 - 5 + 20
CHECK(rect_is(b, 55, 65, 75, 75));
b->SetVerticalAlign(UI::CWindow::VERTICAL_ALIGN_CENTER); // top = (50 - 10)/2 + 5 + 20
CHECK(rect_is(b, 55, 45, 75, 55));
b->SetHorizontalAlign(UI::CWindow::HORIZONTAL_ALIGN_LEFT);
b->SetVerticalAlign(UI::CWindow::VERTICAL_ALIGN_TOP);
CHECK(rect_is(b, 15, 25, 35, 35));
// 父窗口移动,子窗口跟着走(UpdateRect 会 for_each 到所有子窗口)。
a->SetPosition(20, 30);
CHECK(rect_is(a, 20, 30, 120, 80) && rect_is(b, 25, 35, 45, 45));
a->SetPosition(10, 20);
// IsRendering:自己 Show 不够,链路上每一级都要 Show。
CHECK(a->IsRendering() && b->IsRendering());
a->Hide();
CHECK(b->IsShow() && !b->IsRendering());
a->Show();
// ---- 命中 ----
UI::CWindow* c = wm.RegisterWindow(handler("C"), "UI");
c->SetName("C");
c->SetSize(100, 50);
c->SetPosition(10, 20);
c->Show();
take_log();
// 同层后注册的在上面(PickWindow 反向遍历子链表)。
wm.RunMouseMove(50, 40);
CHECK(wm.GetPointWindow() == c);
CHECK(take_log() == "C:in");
// 鼠标坐标按 屏幕尺寸/分辨率 换算,这里 1:1。
long mx = 0, my = 0;
wm.GetMousePosition(mx, my);
CHECK(mx == 50 && my == 40);
// C 藏起来,命中落到 A 的子树上。
c->Hide();
wm.RunMouseMove(20, 28);
CHECK(wm.GetPointWindow() == b);
CHECK(take_log() == "C:out B:in");
// FLAG_NOT_PICK:这个窗口自己不被选中,但仍然往下找子窗口 —— 结果落回父窗口。
b->AddFlag(UI::CWindow::FLAG_NOT_PICK);
wm.RunMouseMove(20, 28);
CHECK(wm.GetPointWindow() == a);
CHECK(take_log() == "B:out A:in");
b->RemoveFlag(UI::CWindow::FLAG_NOT_PICK);
// FLAG_IGNORE_SIZE:不做 IsIn 判断,只要父窗口被命中就轮到它。
b->AddFlag(UI::CWindow::FLAG_IGNORE_SIZE);
wm.RunMouseMove(100, 65); // 在 A 里,不在 B 的矩形里
CHECK(wm.GetPointWindow() == b);
b->RemoveFlag(UI::CWindow::FLAG_IGNORE_SIZE);
wm.RunMouseMove(100, 65);
CHECK(wm.GetPointWindow() == a);
take_log();
// 层外面不命中任何窗口。
wm.RunMouseMove(700, 500);
CHECK(wm.GetPointWindow() == NULL);
take_log();
// SetPickAlways:不管挡在前面的是谁,只要在矩形里就先拿到(工具提示、拖动图标用的)。
c->Show();
wm.SetPickAlways(b);
wm.RunMouseMove(20, 28);
CHECK(wm.GetPointWindow() == b);
take_log();
// ---- SetTop:只对 FLAG_FLOAT 的窗口生效 ----
CHECK(!a->IsFlag(UI::CWindow::FLAG_FLOAT));
wm.SetTop(a); // 没有 FLAG_FLOAT,什么也不做
a->AddFlag(UI::CWindow::FLAG_FLOAT);
wm.SetTop(a); // A 挪到层的链表末尾,压过 C
wm.RunMouseMove(90, 65); // A 和 C 都覆盖这里
CHECK(wm.GetPointWindow() == a);
take_log();
// ---- 拖动:FLAG_MOVABLE 时窗口管理器直接改坐标 ----
a->AddFlag(UI::CWindow::FLAG_MOVABLE);
wm.RunMouseLeftButtonDown(90, 65);
CHECK(take_log() == "A:down");
wm.RunMouseMove(100, 75); // 位移 +10,+10
CHECK(a->GetPositionX() == 20 && a->GetPositionY() == 30);
// FLAG_LIMIT:窗口不许离开屏幕,LimitBias 是四边的留白。
a->AddFlag(UI::CWindow::FLAG_LIMIT);
a->SetLimitBias(0, 0, 0, 0);
wm.RunMouseMove(2000, 2000);
CHECK(a->GetPositionX() == 800 - a->GetWidth() && a->GetPositionY() == 600 - a->GetHeight());
wm.RunMouseMove(-2000, -2000);
CHECK(a->GetPositionX() == 0 && a->GetPositionY() == 0);
wm.RunMouseLeftButtonUp(-2000, -2000);
a->RemoveFlag(UI::CWindow::FLAG_LIMIT);
a->RemoveFlag(UI::CWindow::FLAG_MOVABLE);
a->SetPosition(10, 20);
take_log();
// ---- 焦点 ----
CHECK(!wm.IsFocus());
wm.ActivateWindow(a);
CHECK(wm.IsFocus() && wm.IsFocusWindow(a) && wm.GetActivateWindow() == a);
CHECK(take_log() == "A:focus");
wm.ActivateWindow(c);
CHECK(wm.IsFocusWindow(c) && !wm.IsFocusWindow(a));
CHECK(take_log() == "A:blur C:focus");
wm.DeactivateWindow();
CHECK(!wm.IsFocus());
take_log();
// ---- 按键:RunKeyDownEvent 从 root 往下走,第一个 OnKeyDown 返回真的窗口吃掉它并被记成捕获者 ----
a->Hide(); // A 的处理器返回 0,藏起来让这段日志只剩 C
wm.RunKeyDown(65);
CHECK(take_log() == "C:kd65");
// KeyUp 只发给捕获者,不再重新查找。
wm.RunKeyUp(65);
CHECK(take_log() == "C:ku65");
// 没人捕获的键:RunKeyUpEvent 从 root 往下发一轮(C 的 OnKeyUp 返回 1,到此为止)。
wm.RunKeyUp(66);
CHECK(take_log() == "C:ku66");
a->Show();
// ---- Update 沿着树走,Hide 的分支整支跳过 ----
take_log();
wm.Update();
CHECK(take_log() == "A:update B:update C:update");
b->Hide();
wm.Update();
CHECK(take_log() == "A:update C:update");
b->Show();
// ---- 销毁:先从管理器的各个指针里摘干净,再从父窗口摘掉,真正的 delete 留到下一次 Update ----
wm.RunMouseMove(90, 65);
CHECK(wm.GetPointWindow() == a);
wm.ActivateWindow(a);
wm.RunKeyDown(65); // A 的处理器返回 0,捕获者还是 C
take_log();
wm.DestroyWindow(c);
CHECK(wm.GetActivateWindow() != c);
wm.DestroyWindow(a); // B 是 A 的子窗口,Clear() 只断链不删
CHECK(wm.GetPointWindow() == NULL);
CHECK(!b->HasParent() && !b->IsShow());
wm.Update(); // 这里才 deletea/c 从此不可用
CHECK(take_log() == "");
delete b; // 40250 也不管子窗口,Python 那边各自 Destroy
}
int main(int argc, char** argv)
{
const char* strict = std::getenv("MT_ASSETS_STRICT");
const bool is_strict = strict && std::string(strict) == "1";
const std::string stdlib = argc > 1 ? argv[1] : PythonHost::DefaultStdLibPath();
if (stdlib.empty() || std::fopen(stdlib.c_str(), "rb") == NULL)
{
std::fprintf(stderr, "port_window_tree_test: no python27.zip at '%s'\n", stdlib.c_str());
return is_strict ? 1 : 77;
}
PythonHost::Configure();
{
CPythonLauncher launcher; // 和 40250 Main() 一样在栈上,构造即 Py_Initialize
CHECK(launcher.Create());
std::string error;
CHECK(PythonHost::InstallStdLib(stdlib.c_str(), &error));
if (!error.empty())
std::fprintf(stderr, "InstallStdLib: %s\n", error.c_str());
CHECK(launcher.RunLine(kHandlerScript));
{
UI::CWindowManager wm; // 构造函数建 root + 五个层,析构函数 Py_DECREF 空元组
run(wm);
}
CHECK(PyErr_Occurred() == NULL);
launcher.Clear();
}
CHECK(!Py_IsInitialized());
std::printf("%s: %d failure(s)\n", argv[0], g_failures);
return g_failures == 0 ? 0 : 1;
}