// 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 时退 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 #include #include 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 ""; 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; }