Files
StandardSence/.cursor/skills/cyclegui-app-development/references/examples.md
T
zhaowei.huangandCursor 158d770c54 docs: 添加 cyclegui-app-development 项目技能
将 CycleGUI 开发技能(含 Duplicated id 防碰撞规范)纳入 .cursor/skills,并调整 gitignore 仅放行 skills 目录可提交。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 22:38:18 +08:00

556 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CycleGUI 实用示例
## 示例 1:最小应用
最简可运行 CycleGUI 应用:
```csharp
using CycleGUI;
using CycleGUI.Terminals;
LocalTerminal.SetTitle("Hello CycleGUI");
LocalTerminal.AddMenuItem("Exit", LocalTerminal.Terminate);
LocalTerminal.Start();
GUI.PromptPanel(pb =>
{
pb.Panel.ShowTitle("Hello");
pb.Label("Hello from CycleGUI!");
if (pb.Button("Exit"))
pb.Panel.Exit();
});
```
## 示例 2:多面板导航(LearnCycleGUI 模式)
主面板按钮打开子面板,子面板停靠在左侧:
```csharp
Panel mainPanel = null;
Panel settingsPanel = null;
List<Panel> activePanels = new();
mainPanel = GUI.PromptPanel(pb =>
{
pb.Panel.ShowTitle("Main");
if (pb.Button("Open Settings"))
{
if (!activePanels.Contains(settingsPanel))
{
settingsPanel = GUI.DeclarePanel()
.ShowTitle("Settings")
.InitPosRelative(mainPanel, 0, 16, 0, 1)
.SetDefaultDocking(Panel.Docking.Left);
settingsPanel.Define(CreateSettingsHandler());
activePanels.Add(settingsPanel);
}
else
settingsPanel.BringToFront();
}
});
PanelBuilder.CycleGUIHandler CreateSettingsHandler()
{
float volume = 0.5f;
bool darkMode = true;
int quality = 1;
return pb =>
{
pb.SliderFloat("Volume", ref volume, 0, 1);
pb.Toggle("Dark Mode", ref darkMode);
pb.RadioButtons("Quality", new[] { "Low", "Medium", "High" }, ref quality);
pb.Separator();
if (pb.Closing())
{
activePanels.Remove(settingsPanel);
pb.Panel.Exit();
}
};
}
```
## 示例 3:模态对话框
阻塞式确认对话框,返回用户选择:
```csharp
bool confirmed = false;
GUI.PromptAndWaitPanel(pb =>
{
pb.Panel.ShowTitle("Confirm").Modal(true).AutoSize(true);
pb.Label("Are you sure you want to delete?");
pb.SameLine();
if (pb.Button("Yes", distinct: "yes"))
{
confirmed = true;
pb.Panel.Exit();
}
pb.SameLine();
if (pb.Button("No", distinct: "no"))
{
pb.Panel.Exit();
}
});
if (confirmed) { /* proceed */ }
```
## 示例 4:实时数据面板
后台线程更新数据,面板实时刷新:
```csharp
float temperature = 20f;
float humidity = 50f;
bool running = true;
var panel = GUI.DeclarePanel()
.ShowTitle("Sensor Monitor")
.InitSize(350, 200)
.SetDefaultDocking(Panel.Docking.Right);
panel.Define(pb =>
{
pb.MiniPlot("Temperature", temperature);
pb.MiniPlot("Humidity", humidity);
pb.RealtimePlot("Temp Chart", temperature);
pb.Separator();
if (pb.Button("Stop"))
running = false;
pb.Panel.Repaint(); // 实时数据:handler 内持续 Repaint
});
Task.Run(() =>
{
var rng = new Random();
while (running)
{
temperature = 20f + (float)rng.NextDouble() * 5;
humidity = 50f + (float)rng.NextDouble() * 10;
Thread.Sleep(100);
// 也可在此 panel.Repaint() 替代 handler 内 Repaint,二选一
}
});
```
## 示例 5:表格展示与交互
```csharp
string[] names = { "Alice", "Bob", "Charlie", "Diana" };
float[] scores = { 95, 87, 73, 91 };
bool[] enabled = { true, true, false, true };
panel.Define(pb =>
{
pb.Table("students", new[] { "Name", "Score", "Enabled", "Action" },
names.Length, (row, i) =>
{
row.Label(names[i]);
row.Label(scores[i].ToString("F1"));
row.Checkbox(ref enabled[i]);
if (row.ButtonGroup("", new[] { "Edit", "Delete" }, out int act))
{
if (act == 0) Console.WriteLine($"Edit {names[i]}");
if (act == 1) Console.WriteLine($"Delete {names[i]}");
}
}, height: 200, enableSearch: true, title: "Student Records");
});
```
## 示例 6:3D 场景 - 加载模型和点云
```csharp
using CycleGUI;
using CycleGUI.API;
using System.Numerics;
// 加载模型类
Workspace.AddProp(new LoadModel
{
name = "car_model",
detail = new Workspace.ModelDetail(File.ReadAllBytes("car.glb"))
{
Scale = 0.001f,
Center = new Vector3(0, 0, 0),
}
});
// 放置两个实例
Workspace.AddProp(new PutModelObject
{
name = "car_a",
clsName = "car_model",
newPosition = new Vector3(0, 0, 0),
newQuaternion = Quaternion.Identity,
});
Workspace.AddProp(new PutModelObject
{
name = "car_b",
clsName = "car_model",
newPosition = new Vector3(5, 0, 0),
newQuaternion = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, MathF.PI / 2),
});
// 添加地面点云
var gridPoints = new List<Vector4>();
var gridColors = new List<uint>();
for (float x = -10; x <= 10; x += 0.5f)
for (float y = -10; y <= 10; y += 0.5f)
{
gridPoints.Add(new Vector4(x, y, 0, 2));
gridColors.Add(0xFF808080);
}
Workspace.AddProp(new PutPointCloud
{
name = "ground_grid",
xyzSzs = gridPoints.ToArray(),
colors = gridColors.ToArray(),
newPosition = Vector3.Zero,
});
// 设置相机
new SetCamera
{
lookAt = new Vector3(2.5f, 0, 0),
altitude = MathF.PI / 4,
azimuth = -MathF.PI / 3,
distance = 15f,
}.IssueToAllTerminals();
```
## 示例 7Painter 实时调试绘制
```csharp
var painter = Painter.GetPainter("debug_overlay");
Task.Run(() =>
{
float t = 0;
while (true)
{
painter.Clear();
// 绘制坐标轴
painter.DrawLine(Color.Red, Vector3.Zero, Vector3.UnitX * 2, 2f,
Painter.ArrowType.End);
painter.DrawLine(Color.Green, Vector3.Zero, Vector3.UnitY * 2, 2f,
Painter.ArrowType.End);
painter.DrawLine(Color.Blue, Vector3.Zero, Vector3.UnitZ * 2, 2f,
Painter.ArrowType.End);
// 绘制旋转点
float x = MathF.Cos(t) * 3;
float y = MathF.Sin(t) * 3;
painter.DrawDot(Color.Yellow, new Vector3(x, y, 0), 5f);
painter.DrawText(Color.White, new Vector3(x, y, 0.3f),
$"({x:F1}, {y:F1})");
// 绘制轨迹线
for (int i = 0; i < 20; i++)
{
float t1 = t - i * 0.1f;
float t2 = t - (i + 1) * 0.1f;
painter.DrawLine(Color.FromArgb(255 - i * 12, 255, 255, 0),
new Vector3(MathF.Cos(t1) * 3, MathF.Sin(t1) * 3, 0),
new Vector3(MathF.Cos(t2) * 3, MathF.Sin(t2) * 3, 0));
}
t += 0.05f;
Thread.Sleep(33);
}
});
```
## 示例 8:对象选择与 Guizmo 操作
```csharp
SelectObject selectOp = null;
GuizmoAction guizmoOp = null;
string selectedObject = null;
void StartSelect()
{
selectOp = new SelectObject();
selectOp.feedback = (results, op) =>
{
if (results.Length > 0)
{
selectedObject = results[0].name;
Console.WriteLine($"Selected: {selectedObject}");
}
};
selectOp.Start();
}
void StartGuizmo()
{
if (selectedObject == null) return;
selectOp?.End();
guizmoOp = new GuizmoAction
{
dof = GuizmoAction.GuizmoDof.PlanarXYYaw,
realtimeResult = true,
};
guizmoOp.feedback = (results, op) =>
{
foreach (var (name, pos, rot) in results)
Console.WriteLine($"Moved {name} to {pos}");
};
guizmoOp.finished = () => StartSelect();
guizmoOp.Start();
}
```
## 示例 9:拾取坐标绘制线段
```csharp
Vector3? lineStart = null;
var getPos = new GetPosition
{
Name = "Draw Line",
method = GetPosition.PickMode.GridPlane,
};
getPos.feedback = (wp, op) =>
{
if (lineStart == null)
{
lineStart = wp.mouse_pos;
}
else
{
Workspace.AddProp(new PutStraightLine
{
name = $"line_{DateTime.Now.Ticks}",
start = lineStart.Value,
end = wp.mouse_pos,
color = Color.Cyan,
width = 2,
arrowType = Painter.ArrowType.End,
});
lineStart = null;
}
};
getPos.Start();
```
## 示例 10Medulla2 风格的 IOObject 面板
Medulla2 中每个设备对象(IOObject)通过 `OpenUI` 打开独立的控制面板:
```csharp
public class Camera3DIOObject : IOObject
{
private Panel uiPanel;
private bool displaying = false;
public void OpenUI()
{
displaying = true;
uiPanel = GUI.DeclarePanel()
.ShowTitle($"Camera: {Name}")
.SetDefaultDocking(Panel.Docking.Right);
uiPanel.Define(pb =>
{
if (pb.Button("Capture")) TakeSnapshot();
pb.Toggle("Live View", ref displaying);
pb.MiniPlot("FPS", currentFps);
if (pb.Closing())
{
displaying = false;
Painter.GetPainter(Name).Clear();
uiPanel.Exit();
}
});
}
public void CloseUI()
{
displaying = false;
Painter.GetPainter(Name).Clear();
uiPanel?.Exit();
}
public void draw()
{
if (!displaying) return;
var pp = Painter.GetPainter(Name);
pp.Clear();
foreach (var pt in cachedPoints)
{
var cc = Color.FromArgb(pt.r, pt.g, pt.b);
pp.DrawDot(cc, new Vector3(pt.X, pt.Y, pt.Z) / 1000f, 1);
}
}
}
```
## 示例 11:工具栏面板
```csharp
var toolbar = GUI.DeclarePanel()
.ShowTitle(null)
.AsToolbarPanel(38, Panel.ToolbarAnchor.RightTop);
toolbar.Define(pb =>
{
var tpb = new ToolbarPanelBuilder(pb);
tpb.Label($"{IconFonts.ForkAwesome.Cube} Scene");
tpb.Separator();
if (tpb.Button($"{IconFonts.ForkAwesome.Play} Run", distinct: "tb-run"))
StartSimulation();
if (tpb.Button($"{IconFonts.ForkAwesome.Stop} Stop", distinct: "tb-stop"))
StopSimulation();
tpb.PopMenuButton($"{IconFonts.ForkAwesome.Camera} View", new[]
{
new MenuItem("Front", () => new SetCamera { azimuth = 0, altitude = MathF.PI/2 }.IssueToDefault()),
new MenuItem("Top", () => new SetCamera { azimuth = 0, altitude = 0 }.IssueToDefault()),
new MenuItem("Reset", () => new SetCamera().IssueToDefault()),
});
});
```
## 示例 12:同时启用 Local + Web 终端
```csharp
static void Main(string[] args)
{
LocalTerminal.SetTitle("Dual Terminal App");
LocalTerminal.SetIcon(LoadIcon(), "DualApp");
LocalTerminal.AddMenuItem("Exit", LocalTerminal.Terminate);
LocalTerminal.Start();
// Web 终端在后台启动
Task.Run(() =>
{
var htdocs = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "htdocs");
if (Directory.Exists(htdocs))
LeastServer.AddServingFiles("/", htdocs);
// 添加自定义 API
LeastServer.AddGetHandler("/api/status", () =>
JsonConvert.SerializeObject(new { status = "running", time = DateTime.Now }));
WebTerminal.Use(port: 8081, ico: LoadIcon());
});
// 远程连接时显示的面板
Terminal.RegisterRemotePanel(terminal =>
CreateMainPanel(terminal));
// 本地面板
GUI.PromptPanel(CreateMainPanel(GUI.defaultTerminal));
}
static PanelBuilder.CycleGUIHandler CreateMainPanel(Terminal t)
{
return pb =>
{
pb.Panel.ShowTitle("Control Panel");
pb.Label($"Terminal: {t.GetType().Name}");
// 同样的 UI 在两个终端上都可见...
};
}
// 图标统一用 .ico 字节(也可 File.ReadAllBytes 从磁盘读)
// 需 csproj: <EmbeddedResource Include="app_icon.ico" />exe 图标另用 <ApplicationIcon>
static byte[] LoadIcon()
{
var asm = Assembly.GetExecutingAssembly();
using var s = asm.GetManifestResourceStream(
asm.GetManifestResourceNames().First(p => p.Contains(".ico")));
return new BinaryReader(s).ReadBytes((int)s.Length);
}
```
## 示例 13Painter 填充多边形
```csharp
var painter = Painter.GetPainter("polygon_demo");
painter.Clear();
// 绘制地面上的三角形
var triangle = new[]
{
new Vector2(0, 0),
new Vector2(1, 0),
new Vector2(0.5f, 0.866f),
};
painter.DrawPolygonFilled(triangle,
trans: new Vector3(0, 0, 0),
quat: Quaternion.Identity,
borderColor: Color.White,
fillColor: Color.FromArgb(128, Color.Green));
// 拉伸为柱体
painter.DrawPolygonFilled(triangle,
trans: new Vector3(3, 0, 0),
quat: Quaternion.Identity,
borderColor: Color.Yellow,
fillColor: Color.FromArgb(100, Color.Blue),
thickness: 1.5f);
```
## 示例 14Medulla2 插件结构
Medulla2 插件 DLL 必须暴露 `MainIOObject` 类:
```csharp
// MyPlugin/MainIOObject.cs
public class MainIOObject : IOObject
{
public object init(string typeName, dynamic[] args)
{
// 根据 typeName 创建设备实例
if (typeName == "sensor_a")
return new SensorA((string)args[0], (int)args[1]);
return null;
}
}
// MyPlugin/SensorA.cs
public class SensorA : IOObject
{
public SensorA(string port, int baudRate) { /* init */ }
[IOObjectUtility]
public void Calibrate() { /* 显示为 UI 按钮 */ }
[IOObjectMonitor]
public float Temperature => ReadTemp();
}
```
csproj 引用 refasmer 生成的参考程序集:
```xml
<Reference Include="RefMedullaCore">
<HintPath>$(ReleaseDir)\RefMedullaCore.dll</HintPath>
</Reference>
```
输出到插件目录:
```xml
<OutputPath>$(DebugDir)\plugins</OutputPath>
```
加载方式(在 .iocmd 脚本中):
```
sensor = io load plugins/MyPlugin.dll
mySensor = sensor init sensor_a COM3 115200
```