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>
This commit is contained in:
shenlei
2026-09-23 11:09:05 +09:00
co-authored by Claude Code
parent 369a80fc10
commit b4c9d6cefc
15 changed files with 6255 additions and 0 deletions
+319
View File
@@ -0,0 +1,319 @@
// 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 时退 77(ctest 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 再 SetResolution(UserInterface 的 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)是 CLayer,IsWindow() 为 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(); // 这里才 delete,a/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;
}