简单实现http通信

This commit is contained in:
Hector 2026-02-27 11:14:08 +08:00
parent c93ab3edac
commit e917f9019d
4 changed files with 562 additions and 91 deletions

View File

@ -1312,46 +1312,68 @@ class InfoPanelManager(DirectObject):
traceback.print_exc()
QMessageBox.critical(self, "错误", f"创建示例天气信息面板时出错: {str(e)}")
def getRealWeatherData(self):
"""获取真实天气数据"""
try:
import requests
import json
from datetime import datetime
# 请求天气数据
url = "https://wttr.in/Beijing?format=j1"
response = requests.get(url, timeout=10)
response.raise_for_status()
# 解析JSON数据
weather_data = response.json()
# 提取当前天气信息
current_condition = weather_data['current_condition'][0]
weather_desc = current_condition['weatherDesc'][0]['value']
temp_c = current_condition['temp_C']
feels_like = current_condition['FeelsLikeC']
humidity = current_condition['humidity']
pressure = current_condition['pressure']
visibility = current_condition['visibility']
wind_speed = current_condition['windspeedKmph']
wind_dir = current_condition['winddir16Point']
# 提取空气质量(如果可用)
air_quality = "N/A"
if 'air_quality' in weather_data and weather_data['air_quality']:
if 'us-epa-index' in current_condition:
air_quality_index = current_condition['air_quality_index']
air_quality = f"指数: {air_quality_index}"
# 获取更新时间
update_time = datetime.now().strftime("%Y-%m-%d %H:%M")
# 格式化显示内容
content = f"天气状况: {weather_desc}\n温度: {temp_c}°C (体感 {feels_like}°C)\n湿度: {humidity}%\n气压: {pressure} hPa\n能见度: {visibility} km\n风速: {wind_speed} km/h ({wind_dir})\n空气质量: {air_quality}\n更新时间: {update_time}"
return content
def getRealWeatherData(self):
"""获取真实天气数据"""
try:
import requests
import json
from datetime import datetime
# 请求天气数据
url = "https://api.open-meteo.com/v1/forecast?latitude=39.9042&longitude=116.4074&current=temperature_2m,apparent_temperature,relative_humidity_2m,surface_pressure,wind_speed_10m,wind_direction_10m,weather_code&timezone=Asia%2FShanghai"
response = requests.get(url, timeout=10)
response.raise_for_status()
# 解析JSON数据
weather_data = response.json()
current_condition = weather_data.get('current', {})
if not current_condition:
return "错误: 天气数据格式不正确 (缺少 current 字段)"
weather_code = current_condition.get('weather_code')
weather_desc_map = {
0: "晴朗",
1: "大部晴朗",
2: "局部多云",
3: "阴天",
45: "",
48: "冻雾",
51: "毛毛雨",
53: "小雨",
55: "中雨",
61: "小雨",
63: "中雨",
65: "大雨",
71: "小雪",
73: "中雪",
75: "大雪",
80: "阵雨",
81: "较强阵雨",
82: "强阵雨",
95: "雷暴",
96: "雷暴夹小冰雹",
99: "雷暴夹大冰雹",
}
weather_desc = weather_desc_map.get(weather_code, f"天气代码 {weather_code}")
temp_c = current_condition.get('temperature_2m', 'N/A')
feels_like = current_condition.get('apparent_temperature', 'N/A')
humidity = current_condition.get('relative_humidity_2m', 'N/A')
pressure = current_condition.get('surface_pressure', 'N/A')
wind_speed = current_condition.get('wind_speed_10m', 'N/A')
wind_dir = current_condition.get('wind_direction_10m', 'N/A')
visibility = "N/A"
air_quality = "N/A"
update_time = current_condition.get('time')
if not update_time:
update_time = datetime.now().strftime("%Y-%m-%d %H:%M")
# 格式化显示内容
content = f"天气状况: {weather_desc}\n温度: {temp_c}°C (体感 {feels_like}°C)\n湿度: {humidity}%\n气压: {pressure} hPa\n能见度: {visibility}\n风速: {wind_speed} km/h (风向 {wind_dir}°)\n空气质量: {air_quality}\n更新时间: {update_time}"
return content
except requests.exceptions.Timeout:
return "错误: 获取天气数据超时"

View File

@ -25,7 +25,7 @@ Collapsed=0
[Window][工具栏]
Pos=276,20
Size=1413,74
Size=1219,32
Collapsed=0
DockId=0x0000000D,0
@ -36,8 +36,8 @@ Collapsed=0
DockId=0x00000007,0
[Window][属性面板]
Pos=1691,20
Size=229,989
Pos=1497,20
Size=423,377
Collapsed=0
DockId=0x00000003,0
@ -99,8 +99,8 @@ Size=600,500
Collapsed=0
[Window][资源管理器]
Pos=276,723
Size=1413,286
Pos=276,871
Size=1219,138
Collapsed=0
DockId=0x00000006,0
@ -150,8 +150,8 @@ Size=101,226
Collapsed=0
[Window][LUI编辑器]
Pos=1628,412
Size=292,597
Pos=1497,399
Size=423,610
Collapsed=0
DockId=0x00000004,0
@ -207,16 +207,16 @@ Collapsed=0
[Docking][Data]
DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=1920,989 Split=X
DockNode ID=0x00000001 Parent=0x08BD597D SizeRef=1689,989 Split=X
DockNode ID=0x00000001 Parent=0x08BD597D SizeRef=1495,989 Split=X
DockNode ID=0x00000009 Parent=0x00000001 SizeRef=274,989 Split=Y Selected=0xE0015051
DockNode ID=0x00000007 Parent=0x00000009 SizeRef=271,634 Selected=0xE0015051
DockNode ID=0x00000008 Parent=0x00000009 SizeRef=271,353 Selected=0x5428E753
DockNode ID=0x0000000A Parent=0x00000001 SizeRef=1413,989 Split=Y
DockNode ID=0x0000000D Parent=0x0000000A SizeRef=1318,74 HiddenTabBar=1 Selected=0x43A39006
DockNode ID=0x0000000E Parent=0x0000000A SizeRef=1318,913 Split=Y
DockNode ID=0x00000005 Parent=0x0000000E SizeRef=1341,625 CentralNode=1
DockNode ID=0x00000006 Parent=0x0000000E SizeRef=1341,286 Selected=0x3A2E05C3
DockNode ID=0x00000002 Parent=0x08BD597D SizeRef=229,989 Split=Y Selected=0x3188AB8D
DockNode ID=0x00000003 Parent=0x00000002 SizeRef=351,390 Selected=0x5DB6FF37
DockNode ID=0x00000004 Parent=0x00000002 SizeRef=351,597 Selected=0x1EB923B7
DockNode ID=0x0000000A Parent=0x00000001 SizeRef=1219,989 Split=Y
DockNode ID=0x0000000D Parent=0x0000000A SizeRef=1318,32 HiddenTabBar=1 Selected=0x43A39006
DockNode ID=0x0000000E Parent=0x0000000A SizeRef=1318,955 Split=Y
DockNode ID=0x00000005 Parent=0x0000000E SizeRef=1341,815 CentralNode=1
DockNode ID=0x00000006 Parent=0x0000000E SizeRef=1341,138 Selected=0x3A2E05C3
DockNode ID=0x00000002 Parent=0x08BD597D SizeRef=423,989 Split=Y Selected=0x3188AB8D
DockNode ID=0x00000003 Parent=0x00000002 SizeRef=351,377 Selected=0x5DB6FF37
DockNode ID=0x00000004 Parent=0x00000002 SizeRef=351,610 Selected=0x1EB923B7

View File

@ -1,5 +1,10 @@
import os
import sys
import json
import time
import threading
import urllib.request
import urllib.error
from pathlib import Path
import panda3d.core as p3d
from panda3d.core import NodePath, CardMaker
@ -787,6 +792,331 @@ class luiFunction:
manager.components.append(comp_data)
return obj
def create_http_text(manager, x=100, y=100, width=320, height=120, url="https://api.open-meteo.com/v1/forecast?latitude=39.9042&longitude=116.4074&current=temperature_2m,apparent_temperature,relative_humidity_2m,wind_speed_10m&timezone=Asia%2FShanghai", parent=None):
"""Create a HTTP text component that fetches data from URL and renders it."""
if not manager.lui_enabled:
return None
if parent is None and manager.current_canvas_index >= 0:
parent = manager.canvases[manager.current_canvas_index]['panel']
elif parent is None:
parent = manager.overlay_root
obj = LUIObject(parent=parent, x=x, y=y)
obj.width = width
obj.height = height
obj.solid = True
if hasattr(obj, 'set_sort'):
obj.set_sort(1)
# Panel background
spr = LUISprite(obj, "blank", "skin")
spr.width = width
spr.height = height
spr.left = 0
spr.top = 0
spr.color = (0.08, 0.14, 0.20, 0.92)
# Text content
text_label = LUILabel(
parent=obj,
text="HTTP组件\n点击“立即请求”获取数据",
x=8,
y=8,
font_size=14,
wordwrap=True
)
text_label.color = (0.92, 0.95, 1.0, 1.0)
comp_data = {
'type': 'HttpText',
'object': obj,
'sprite': spr,
'text_label': text_label,
'text': "HTTP组件\n点击“立即请求”获取数据",
'left': x,
'top': y,
'width': width,
'height': height,
'canvas_index': manager.current_canvas_index,
'name': f"HttpText_{len(manager.components)}",
'sort': 1,
'color': (0.08, 0.14, 0.20, 0.92),
'text_color': (0.92, 0.95, 1.0, 1.0),
'error_color': (1.0, 0.40, 0.40, 1.0),
'text_padding': 8.0,
# HTTP settings
'http_url': url,
'http_method': 'GET',
'http_headers': '{}',
'http_body': '',
'http_timeout': 8.0,
'http_json_path': 'current',
'auto_refresh': True,
'refresh_interval': 60.0,
'max_chars': 300,
'http_status': '未请求',
'last_error': '',
'last_response_raw': '',
'last_refresh_time': 0.0,
# Runtime flags
'_http_inflight': False,
'_http_pending_result': None,
}
manager.luiFunction.sync_http_text_layout(manager, comp_data)
comp_index = len(manager.components)
manager._setup_component_drag(comp_data, comp_index)
manager._add_to_scene_tree(comp_data)
manager.components.append(comp_data)
# Trigger first request in background
manager.luiFunction.trigger_http_request(manager, comp_data, force=True)
return obj
def sync_http_text_layout(manager, comp_data):
"""Keep HttpText internal label in sync with panel size and padding."""
if comp_data.get('type') != 'HttpText':
return
label = comp_data.get('text_label')
if label is None:
return
pad = float(comp_data.get('text_padding', 8.0))
width = float(comp_data.get('width', 100.0))
height = float(comp_data.get('height', 40.0))
inner_w = max(1.0, width - pad * 2.0)
inner_h = max(1.0, height - pad * 2.0)
try:
label.left = pad
label.top = pad
if hasattr(label, 'width'):
label.width = inner_w
if hasattr(label, 'height'):
label.height = inner_h
except Exception:
pass
def _extract_json_by_path(path_text, payload):
"""Extract value from JSON payload by a simple path: a.b[0].c"""
if not path_text:
return payload
path = str(path_text).strip()
if not path:
return payload
tokens = []
current = ""
i = 0
while i < len(path):
ch = path[i]
if ch == '.':
if current:
tokens.append(('key', current))
current = ""
i += 1
continue
if ch == '[':
if current:
tokens.append(('key', current))
current = ""
j = path.find(']', i + 1)
if j < 0:
raise ValueError(f"JSON路径非法: {path_text}")
idx_text = path[i + 1:j].strip()
if not idx_text:
raise ValueError(f"JSON路径非法: {path_text}")
tokens.append(('index', int(idx_text)))
i = j + 1
continue
current += ch
i += 1
if current:
tokens.append(('key', current))
node = payload
for token_type, token_val in tokens:
if token_type == 'key':
if not isinstance(node, dict):
raise KeyError(f"JSON路径错误期望对象: {token_val}")
node = node[token_val]
else:
if not isinstance(node, (list, tuple)):
raise KeyError(f"JSON路径错误期望数组: [{token_val}]")
node = node[token_val]
return node
def _set_http_text_display(manager, comp_data, text, is_error=False):
"""Update HttpText display text and color."""
msg = text if text is not None else ""
comp_data['text'] = msg
label = comp_data.get('text_label')
if label is None:
return
try:
label.text = msg
if is_error:
color = comp_data.get('error_color', (1.0, 0.4, 0.4, 1.0))
else:
color = comp_data.get('text_color', (0.92, 0.95, 1.0, 1.0))
if hasattr(label, 'set_color'):
label.set_color(color)
else:
label.color = color
except Exception:
pass
def trigger_http_request(manager, comp_data, force=False):
"""Start one async HTTP request for HttpText component."""
if comp_data.get('type') != 'HttpText':
return False
if comp_data.get('_http_inflight'):
return False
now_ts = time.time()
if not force:
interval = max(1.0, float(comp_data.get('refresh_interval', 60.0)))
last_ts = float(comp_data.get('last_refresh_time', 0.0))
if now_ts - last_ts < interval:
return False
url = str(comp_data.get('http_url', '')).strip()
if not url:
comp_data['http_status'] = "URL为空"
comp_data['last_error'] = "URL为空"
manager.luiFunction._set_http_text_display(manager, comp_data, "HTTP请求失败: URL为空", is_error=True)
return False
method = str(comp_data.get('http_method', 'GET')).upper().strip()
if method not in ('GET', 'POST'):
method = 'GET'
comp_data['http_method'] = 'GET'
timeout_sec = max(1.0, float(comp_data.get('http_timeout', 8.0)))
headers_raw = str(comp_data.get('http_headers', '{}'))
body_raw = str(comp_data.get('http_body', ''))
json_path = str(comp_data.get('http_json_path', '')).strip()
max_chars = max(32, int(comp_data.get('max_chars', 300)))
comp_data['_http_inflight'] = True
comp_data['http_status'] = "请求中..."
def _worker():
result = {
'ok': False,
'message': '',
'status_code': None,
'error': '',
'time': time.time(),
'raw': '',
}
try:
headers = {}
if headers_raw.strip():
parsed_headers = json.loads(headers_raw)
if not isinstance(parsed_headers, dict):
raise ValueError("Headers 必须是 JSON 对象")
headers = {str(k): str(v) for k, v in parsed_headers.items()}
data_bytes = None
if body_raw:
data_bytes = body_raw.encode('utf-8')
request = urllib.request.Request(
url=url,
data=data_bytes,
headers=headers,
method=method
)
with urllib.request.urlopen(request, timeout=timeout_sec) as response:
status_code = getattr(response, 'status', 200)
raw_bytes = response.read()
charset = None
response_headers = getattr(response, 'headers', None)
if response_headers is not None and hasattr(response_headers, 'get_content_charset'):
charset = response_headers.get_content_charset()
if not charset:
charset = "utf-8"
raw_text = raw_bytes.decode(charset, errors='replace')
display_text = raw_text.strip()
if json_path:
json_payload = json.loads(raw_text)
extracted = luiFunction._extract_json_by_path(json_path, json_payload)
if isinstance(extracted, (dict, list)):
display_text = json.dumps(extracted, ensure_ascii=False)
else:
display_text = str(extracted)
if len(display_text) > max_chars:
display_text = display_text[:max_chars] + "..."
result['ok'] = True
result['message'] = display_text
result['status_code'] = status_code
result['raw'] = raw_text
except Exception as err:
result['error'] = str(err)
comp_data['_http_pending_result'] = result
thread = threading.Thread(target=_worker, daemon=True)
thread.start()
return True
def update_http_components(manager):
"""Consume async HTTP results and run auto-refresh."""
now_ts = time.time()
for comp_data in manager.components:
if comp_data.get('type') != 'HttpText':
continue
pending = comp_data.get('_http_pending_result')
if pending is not None:
comp_data['_http_pending_result'] = None
comp_data['_http_inflight'] = False
comp_data['last_refresh_time'] = float(pending.get('time', now_ts))
comp_data['last_response_raw'] = pending.get('raw', '')
if pending.get('ok'):
status_code = pending.get('status_code')
stamp = time.strftime("%H:%M:%S", time.localtime(comp_data['last_refresh_time']))
comp_data['http_status'] = f"HTTP {status_code} @ {stamp}"
comp_data['last_error'] = ''
manager.luiFunction._set_http_text_display(
manager,
comp_data,
pending.get('message', ''),
is_error=False
)
else:
error_msg = pending.get('error', '未知错误')
comp_data['last_error'] = error_msg
comp_data['http_status'] = "请求失败"
manager.luiFunction._set_http_text_display(
manager,
comp_data,
f"HTTP请求失败: {error_msg}",
is_error=True
)
# Auto refresh
if comp_data.get('auto_refresh', False) and not comp_data.get('_http_inflight'):
interval = max(1.0, float(comp_data.get('refresh_interval', 60.0)))
last_ts = float(comp_data.get('last_refresh_time', 0.0))
if now_ts - last_ts >= interval:
manager.luiFunction.trigger_http_request(manager, comp_data, force=True)
def create_progressbar(manager, value=50, x=100, y=100, width=200, parent=None):
"""Create a LUI Progressbar"""
if not manager.lui_enabled: return None
@ -1200,6 +1530,13 @@ class luiFunction:
"""绘制组件属性编辑面板"""
if index < 0 or index >= len(manager.components):
return
# Keep async HTTP results synchronized even when not in dedicated task loop.
if hasattr(manager.luiFunction, 'update_http_components'):
try:
manager.luiFunction.update_http_components(manager)
except Exception:
pass
comp_data = manager.components[index]
comp_obj = comp_data['object']
@ -1501,9 +1838,9 @@ class luiFunction:
if comp_type == 'Button' and hasattr(comp_obj, '_apply_stretch_sizes'):
comp_obj._apply_stretch_sizes()
if comp_type == 'Slider' and hasattr(comp_obj, 'set_height'):
comp_obj.set_height(new_height)
comp_obj.set_height(float(comp_data.get('height', 16)))
if comp_type == 'InputField' and hasattr(comp_obj, 'set_height'):
comp_obj.set_height(new_height)
comp_obj.set_height(float(comp_data.get('height', 24)))
if comp_type == 'Slider' and hasattr(comp_obj, 'set_width'):
comp_obj.set_width(new_width)
if comp_type == 'InputField' and hasattr(comp_obj, 'set_width'):
@ -1522,12 +1859,15 @@ class luiFunction:
if comp_type == 'Button' and hasattr(comp_obj, '_apply_stretch_sizes'):
comp_obj._apply_stretch_sizes()
if comp_type == 'Slider' and hasattr(comp_obj, 'set_width'):
comp_obj.set_width(new_width)
comp_obj.set_width(float(comp_data.get('width', 200)))
if comp_type == 'InputField' and hasattr(comp_obj, 'set_width'):
comp_obj.set_width(new_width)
comp_obj.set_width(float(comp_data.get('width', 200)))
if (width_changed or height_changed) and hasattr(manager, '_update_anchored_children'):
manager._update_anchored_children(index)
if width_changed or height_changed:
if comp_type == 'HttpText' and hasattr(manager.luiFunction, 'sync_http_text_layout'):
manager.luiFunction.sync_http_text_layout(manager, comp_data)
if hasattr(manager, '_update_anchored_children'):
manager._update_anchored_children(index)
if comp_type in ['VerticalLayout', 'HorizontalLayout'] and hasattr(manager, '_update_layout_inner'):
@ -2001,6 +2341,79 @@ class luiFunction:
except Exception as e:
print(f"Selectbox options update failed: {e}")
elif comp_type == 'HttpText':
imgui.spacing()
imgui.text("HTTP通信")
imgui.separator()
http_url = str(comp_data.get('http_url', ''))
changed_url, new_url = imgui.input_text("URL", http_url, 512)
if changed_url:
comp_data['http_url'] = new_url
method_options = ["GET", "POST"]
curr_method = str(comp_data.get('http_method', 'GET')).upper()
curr_method_idx = 1 if curr_method == "POST" else 0
changed_method, new_method_idx = imgui.combo("Method", curr_method_idx, method_options)
if changed_method:
comp_data['http_method'] = method_options[new_method_idx]
timeout_val = float(comp_data.get('http_timeout', 8.0))
changed_timeout, new_timeout = imgui.input_float("Timeout(s)", timeout_val, 1.0, 5.0, "%.1f")
if changed_timeout:
comp_data['http_timeout'] = max(1.0, new_timeout)
auto_refresh = bool(comp_data.get('auto_refresh', True))
changed_auto, new_auto = imgui.checkbox("自动刷新", auto_refresh)
if changed_auto:
comp_data['auto_refresh'] = new_auto
interval_val = float(comp_data.get('refresh_interval', 60.0))
changed_interval, new_interval = imgui.input_float("刷新间隔(s)", interval_val, 1.0, 10.0, "%.1f")
if changed_interval:
comp_data['refresh_interval'] = max(1.0, new_interval)
json_path = str(comp_data.get('http_json_path', ''))
changed_path, new_path = imgui.input_text("JSON路径(可选)", json_path, 256)
if changed_path:
comp_data['http_json_path'] = new_path
headers_text = str(comp_data.get('http_headers', '{}'))
changed_headers, new_headers = imgui.input_text("Headers(JSON)", headers_text, 512)
if changed_headers:
comp_data['http_headers'] = new_headers
body_text = str(comp_data.get('http_body', ''))
changed_body, new_body = imgui.input_text("Body", body_text, 512)
if changed_body:
comp_data['http_body'] = new_body
max_chars = int(comp_data.get('max_chars', 300))
changed_max, new_max = imgui.input_int("最大显示字符", max_chars)
if changed_max:
comp_data['max_chars'] = max(32, int(new_max))
imgui.text(f"状态: {comp_data.get('http_status', '未请求')}")
last_error = comp_data.get('last_error', '')
if last_error:
imgui.text_colored((1.0, 0.4, 0.4, 1.0), f"错误: {last_error}")
if comp_data.get('_http_inflight'):
imgui.text("请求中...")
if imgui.button("北京天气示例", (120, 24)):
comp_data['http_url'] = "https://api.open-meteo.com/v1/forecast?latitude=39.9042&longitude=116.4074&current=temperature_2m,apparent_temperature,relative_humidity_2m,wind_speed_10m&timezone=Asia%2FShanghai"
comp_data['http_method'] = "GET"
comp_data['http_json_path'] = "current"
comp_data['http_headers'] = "{}"
comp_data['http_body'] = ""
comp_data['refresh_interval'] = 60.0
comp_data['auto_refresh'] = True
imgui.same_line()
if imgui.button("立即请求", (100, 24)):
manager.luiFunction.trigger_http_request(manager, comp_data, force=True)
elif comp_type == 'Video':
imgui.spacing()
imgui.text("视频属性")

View File

@ -69,7 +69,7 @@ class LUIManager:
print("LUI is not available, LUIManager will be disabled.")
return
# 1. 优先注册中文字体 (必须在皮肤加载前)
# 1. 优先注册中文字体
self._register_chinese_font()
# 2. 初始化核心 LUI 区域
@ -78,6 +78,10 @@ class LUIManager:
# 3. 加载皮肤资源
self.lui_skin = LUIDefaultSkin()
self.lui_skin.load()
# 3.1 皮肤会覆盖 default/label/header 字体,这里再次注册中文字体
# 以避免 LUIText 输出中文时出现 “Font does not support character ...”
self._register_chinese_font()
# 4. 状态管理 - 与engine保持一致
self.canvases = []
@ -133,6 +137,7 @@ class LUIManager:
self.world.taskMgr.add(self._update_drag, "update_lui_drag")
self.world.taskMgr.add(self._update_resize_handles, "update_resize_handles")
self.world.taskMgr.add(self._update_component_outlines, "update_lui_outlines")
self.world.taskMgr.add(self._update_http_components_task, "update_http_components")
print("✓ LUIManager initialized with complete functionality")
@ -168,7 +173,10 @@ class LUIManager:
# 尝试常见的系统路径,确保使用完整的 OS 路径
candidate_fonts = [
"C:/Windows/Fonts/msyh.ttc",
"C:/Windows/Fonts/msyhbd.ttc",
"C:/Windows/Fonts/simhei.ttf",
"C:/Windows/Fonts/simsun.ttc",
"C:/Windows/Fonts/arialuni.ttf",
"C:/Windows/Fonts/arial.ttf"
]
@ -176,13 +184,21 @@ class LUIManager:
for fpath in candidate_fonts:
if os.path.exists(fpath):
try:
# 在 LUI 中,建议直接用 loader.loadFont 载入后获取动态库指针
# 或者直接传递路径字符串
font = self.world.loader.loadFont(fpath)
if font:
LUIFontPool.get_global_ptr().register_font("default", font)
LUIFontPool.get_global_ptr().register_font("label", font)
print(f"✓ LUI 成功注册字体: {fpath}")
font_default = self.world.loader.loadFont(fpath)
font_label = self.world.loader.loadFont(fpath)
font_header = self.world.loader.loadFont(fpath)
if font_default and font_label and font_header:
if hasattr(font_label, "setPixelsPerUnit"):
font_label.setPixelsPerUnit(32)
if hasattr(font_header, "setPixelsPerUnit"):
font_header.setPixelsPerUnit(80)
font_pool = LUIFontPool.get_global_ptr()
font_pool.register_font("default", font_default)
font_pool.register_font("label", font_label)
font_pool.register_font("header", font_header)
print(f"✓ LUI 成功注册中文字体: {fpath}")
font_registered = True
break
except Exception as e:
@ -1025,7 +1041,7 @@ class LUIManager:
comp_type = comp_data['type']
# 只对Frame、Button、Slider、InputField、Plane、Image显示resize handles
if comp_type not in ['Frame', 'Button', 'Slider', 'InputField', 'Plane', 'Image', 'Checkbox', 'Text', 'Label', 'Video', 'Progressbar', 'Selectbox', 'ScrollableRegion', 'TabbedFrame', 'VerticalLayout', 'HorizontalLayout']:
if comp_type not in ['Frame', 'Button', 'Slider', 'InputField', 'Plane', 'Image', 'Checkbox', 'Text', 'Label', 'Video', 'Progressbar', 'Selectbox', 'ScrollableRegion', 'TabbedFrame', 'VerticalLayout', 'HorizontalLayout', 'HttpText']:
self._hide_resize_handles()
return task.cont
@ -1187,6 +1203,16 @@ class LUIManager:
return task.cont
def _update_http_components_task(self, task):
"""Drive async updates for HttpText components."""
if not self.lui_enabled:
return task.cont
try:
self.luiFunction.update_http_components(self)
except Exception:
pass
return task.cont
def _show_and_update_handles(self, comp_data):
"""显示并更新handles位置 - 修复坐标系不一致问题"""
# 每次显示时重新创建手柄,确保它们在最前面
@ -1599,11 +1625,13 @@ class LUIManager:
if comp_type == 'Frame':
pass
elif comp_type in ['Plane', 'Image', 'Video']:
elif comp_type in ['Plane', 'Image', 'Video', 'HttpText']:
sprite = comp_data.get('sprite')
if sprite is not None:
sprite.width = new_width
sprite.height = new_height
if comp_type == 'HttpText':
self.luiFunction.sync_http_text_layout(self, comp_data)
elif comp_type == 'Button':
if hasattr(comp_obj, 'set_width'):
comp_obj.set_width(new_width)
@ -1729,7 +1757,7 @@ class LUIManager:
parent_obj = parent_data['object']
parent_type = parent_data.get('type')
visual_container_types = [
'Frame', 'Plane', 'Video',
'Frame', 'Plane', 'Video', 'HttpText',
'ScrollableRegion', 'TabbedFrame',
'VerticalLayout', 'HorizontalLayout'
]
@ -1840,7 +1868,7 @@ class LUIManager:
print(f"[_set_parent_child_relationship] Error ensuring Frame visibility: {e}")
# Special handling for Plane, Image, Video: ensure sprite visibility
elif child_data.get('type') in ['Plane', 'Image', 'Video']:
elif child_data.get('type') in ['Plane', 'Image', 'Video', 'HttpText']:
try:
spr = child_data.get('sprite')
if spr is not None:
@ -2017,7 +2045,7 @@ class LUIManager:
print(f"[_set_parent_root] Error ensuring button visibility: {e}")
# 对于 Plane, Image, Video - 确保内部 sprite 可见
elif comp_type in ['Plane', 'Image', 'Video'] and child_obj is not None:
elif comp_type in ['Plane', 'Image', 'Video', 'HttpText'] and child_obj is not None:
try:
spr = child_data.get('sprite')
if spr is not None:
@ -2090,6 +2118,7 @@ class LUIManager:
'Plane': (100, 100),
'Image': (100, 100),
'Video': (320, 240),
'HttpText': (320, 120),
'Progressbar': (200, 30),
'Selectbox': (200, 30),
'ScrollableRegion': (200, 200),
@ -2126,6 +2155,8 @@ class LUIManager:
child_obj = self.luiFunction.create_image(self, x=child_x, y=child_y, parent=parent_obj)
elif comp_type == 'Video':
child_obj = self.luiFunction.create_video(self, x=child_x, y=child_y, parent=parent_obj)
elif comp_type == 'HttpText':
child_obj = self.luiFunction.create_http_text(self, x=child_x, y=child_y, parent=parent_obj)
elif comp_type == 'Progressbar':
child_obj = self.luiFunction.create_progressbar(self, x=child_x, y=child_y, parent=parent_obj)
elif comp_type == 'Selectbox':
@ -2445,7 +2476,7 @@ class LUIManager:
# Component Type Selection
component_types = ["按钮 (Button)", "文本 (Text)", "复选框 (CheckBox)", "输入框 (InputField)",
"滑块 (Slider)", "框架 (Frame)","面板 (Plane)","图片 (Image)","视频 (Video)",
"滑块 (Slider)", "框架 (Frame)","面板 (Plane)","图片 (Image)","视频 (Video)", "HTTP文本 (HttpText)",
"进度条 (Progressbar)", "下拉框 (Selectbox)", "滚动区域 (ScrollableRegion)", "标签页 (TabbedFrame)",
"垂直布局组 (VerticalLayout)", "水平布局组 (HorizontalLayout)"]
@ -2554,8 +2585,8 @@ class LUIManager:
imgui.separator()
if imgui.begin_menu("创建子组件"):
child_types = ['Button', 'Text', 'CheckBox', 'InputField', 'Slider', 'Frame', 'Plane','Image', 'Video','Progressbar','Selectbox','ScrollableRegion','TabbedFrame', 'VerticalLayout', 'HorizontalLayout']
cn_names = ['按钮', '标签', '复选框', '输入框', '滑块', '框架', '面板', '图片', '视频','进度条','下拉框','滚动区域','标签页', '垂直布局组', '水平布局组']
child_types = ['Button', 'Text', 'CheckBox', 'InputField', 'Slider', 'Frame', 'Plane','Image', 'Video', 'HttpText', 'Progressbar','Selectbox','ScrollableRegion','TabbedFrame', 'VerticalLayout', 'HorizontalLayout']
cn_names = ['按钮', '标签', '复选框', '输入框', '滑块', '框架', '面板', '图片', '视频', 'HTTP文本', '进度条','下拉框','滚动区域','标签页', '垂直布局组', '水平布局组']
for ct_idx, ct in enumerate(child_types):
if imgui.menu_item(f"{cn_names[ct_idx]}", "", False)[0]:
self.create_child_component(idx, ct)
@ -2712,12 +2743,13 @@ class LUIManager:
6: (100, 100), # Plane
7: (100, 100), # Image
8: (320, 240), # Video
9: (200, 30), # Progressbar
10: (200, 30), # Selectbox
11: (200, 200), # ScrollableRegion
12: (300, 200), # TabbedFrame
13: (300, 200), # VerticalLayout
14: (300, 200) # HorizontalLayout
9: (320, 120), # HttpText
10: (200, 30), # Progressbar
11: (200, 30), # Selectbox
12: (200, 200), # ScrollableRegion
13: (300, 200), # TabbedFrame
14: (300, 200), # VerticalLayout
15: (300, 200) # HorizontalLayout
}
@ -2750,17 +2782,19 @@ class LUIManager:
self.luiFunction.create_image(self, x=canvas_relative_x, y=canvas_relative_y)
elif type_index == 8: # Video
self.luiFunction.create_video(self, x=canvas_relative_x, y=canvas_relative_y)
elif type_index == 9: # Progressbar
elif type_index == 9: # HttpText
self.luiFunction.create_http_text(self, x=canvas_relative_x, y=canvas_relative_y)
elif type_index == 10: # Progressbar
self.luiFunction.create_progressbar(self, x=canvas_relative_x, y=canvas_relative_y)
elif type_index == 10: # Selectbox
elif type_index == 11: # Selectbox
self.luiFunction.create_selectbox(self, x=canvas_relative_x, y=canvas_relative_y)
elif type_index == 11: # ScrollableRegion
elif type_index == 12: # ScrollableRegion
self.luiFunction.create_scrollable_region(self, x=canvas_relative_x, y=canvas_relative_y)
elif type_index == 12: # TabbedFrame
elif type_index == 13: # TabbedFrame
self.luiFunction.create_tabbed_frame(self, x=canvas_relative_x, y=canvas_relative_y)
elif type_index == 13: # VerticalLayout
elif type_index == 14: # VerticalLayout
self.luiFunction.create_vertical_layout(self, x=canvas_relative_x, y=canvas_relative_y)
elif type_index == 14: # HorizontalLayout
elif type_index == 15: # HorizontalLayout
self.luiFunction.create_horizontal_layout(self, x=canvas_relative_x, y=canvas_relative_y)
print(f"✓ 创建组件成功,已重定向至 Canvas 中心: ({canvas_relative_x:.1f}, {canvas_relative_y:.1f})")
@ -2887,9 +2921,11 @@ class LUIManager:
if hasattr(comp_obj, 'height'):
comp_obj.height = new_height
if comp_type in ['Plane', 'Image', 'Video'] and 'sprite' in comp_data:
if comp_type in ['Plane', 'Image', 'Video', 'HttpText'] and 'sprite' in comp_data:
comp_data['sprite'].width = new_width
comp_data['sprite'].height = new_height
if comp_type == 'HttpText':
self.luiFunction.sync_http_text_layout(self, comp_data)
elif comp_type == 'Button':
if hasattr(comp_obj, 'set_width'):
comp_obj.set_width(new_width)