"""Раскладка панелей RViz: строка «QMainWindow State» для конфига без Qt. Где и какой ширины стоят панели, RViz хранит в конфиге сериализацией Qt — шестнадцатеричной строкой `QMainWindow::saveState()`. Руками её не набрать, а Qt с привязками для Python в образе нет (и из WSL нет выхода к apt). Формат при этом простой и стабильный, он собран здесь по исходникам Qt 5.15 (`QDockAreaLayout::saveState`, `QDockAreaLayoutInfo::saveState`): маркер версии, маркер доков, по каждой зоне — размер и список панелей с именем, флагами, положением и размером. Панель находится по имени объекта, а у RViz это имя панели или дисплея («Displays», «Мозг мухи»). python tools/rviz_layout.py печатает строки для обоих конфигов: `config/flyguard.rviz` (панель Displays слева) и `config/flyguard_brain.rviz` (плюс схема мозга справа во всю высоту). """ from __future__ import annotations import argparse import struct QWIDGETSIZE_MAX = 16777215 LEFT, RIGHT = 0, 1 # QInternal::DockPosition VERTICAL = 2 # Qt::Vertical — панели в зоне идут столбиком CORNERS = (4, 4, 8, 8) # углы окна: верхняя, верхняя, нижняя, нижняя зона def _i32(v: int) -> bytes: return struct.pack(">i", v) def _qstring(s: str) -> bytes: b = s.encode("utf-16-be") return struct.pack(">I", len(b)) + b def state(areas, central: int, height: int) -> str: """areas — [(зона, ширина, [имя панели, ...])], central — ширина трёхмерного вида.""" out = _i32(0xff) + _i32(0) + b"\xfd" + _i32(len(areas)) for pos, width, docks in areas: out += _i32(pos) + _i32(width) + _i32(height) out += b"\xfc" + bytes([VERTICAL]) + _i32(len(docks)) step = height // len(docks) for k, name in enumerate(docks): out += b"\xfb" + _qstring(name) + b"\x01" # видима, не плавает out += _i32(k * step) + _i32(step) + _i32(100) + _i32(QWIDGETSIZE_MAX) out += _i32(central) + _i32(height) for c in CORNERS: out += _i32(c) return out.hex() def parse(hexs: str): """Обратный разбор — проверка, что строка собрана без хвостов.""" b = bytes.fromhex(hexs) p = 0 def rd(fmt): nonlocal p v = struct.unpack_from(fmt, b, p) p += struct.calcsize(fmt) return v assert rd(">ii") == (0xff, 0) and rd(">B") == (0xfd,) n, = rd(">i") areas = [] for _ in range(n): pos, w, h = rd(">iii") marker, _, cnt = rd(">BBi") assert marker == 0xfc docks = [] for _ in range(cnt): assert rd(">B") == (0xfb,) ln, = rd(">I") docks.append(b[p:p + ln].decode("utf-16-be")) p += ln rd(">Biiii") areas.append((pos, w, h, docks)) central = rd(">ii") rd(">iiii") assert p == len(b), "строка не разобрана до конца" return areas, central def main() -> None: ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument("--width", type=int, default=1880, help="ширина окна, как Width в конфиге") ap.add_argument("--height", type=int, default=900, help="высота зоны панелей") ap.add_argument("--left", type=int, default=300, help="ширина панели Displays") ap.add_argument("--brain", type=int, default=820, help="ширина схемы мозга") a = ap.parse_args() plain = state([(LEFT, a.left, ["Displays"])], a.width - a.left - 4, a.height) left = a.left - 20 brain = state([(LEFT, left, ["Displays"]), (RIGHT, a.brain, ["Мозг мухи"])], a.width - left - a.brain - 8, a.height) for name, s in (("flyguard.rviz", plain), ("flyguard_brain.rviz", brain)): parse(s) print(f"{name}:\n QMainWindow State: {s}") if __name__ == "__main__": main()