- 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>
320 lines
12 KiB
C++
320 lines
12 KiB
C++
// 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;
|
||
}
|