简单实现http通信
This commit is contained in:
parent
c93ab3edac
commit
e917f9019d
@ -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¤t=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 "错误: 获取天气数据超时"
|
||||
|
||||
32
imgui.ini
32
imgui.ini
@ -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
|
||||
|
||||
|
||||
@ -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¤t=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¤t=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("视频属性")
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user