Custom Components GUI - easier in GH2?

My programming experience is pretty limited- I’m a self-taught coder - but I managed to create a few basic Grasshopper plugins in Visual Studio. However, building custom GUI components turned out to be a huge challenge. The process was extremely tedious, and most of the time I was working almost blindly. Did I do something wrong, or is this really such an ungrateful task in GH1?

But more importantly… will the switch to ETO change anything in this regard in GH 2? Will it finally be possible, at least for prototyping, to create more sophisticated GUI components in Grasshopper 2 without having to use Visual Studio and recompile the plugin every time?

It would be pity if the fun of creating small custom interfaces were reserved only for those who are fully immersed in the system — basically, David himself.

In Grasshopper 1 I’ve seen a few examples of more advanced components, like the ones used in Karamba, or those mentioned here:

当然,相信我,在GH2制作UI控件相当简单

1 Like

Interesting, are those screenshots from some video? Can you share a link to it? You had to recompile and restart Rhino to see changes or is it possible to have live preview of the GUI changes while coding?

编译对gui的更新 对于节点的图标,节点名称,节点的ui并不一样,但对于GHI更新只需要在vs里面热重载(不需要再次编译rhino)

因为一些原因,我不太确定你能否打开这个网页(我昨天清空了youtub的视频发布,所以youtub上不能看了)门无的个人空间-门无个人主页-哔哩哔哩视频

1 Like

Wow, the depths of the Chinese internet. Thanks! Would you say that it’s easier for you to create custom components in GH2 than in GH1?

在GH2实现“控件”相当容易(你可以用eto很方便的更改节点gui,严格来说与在wpf,imgui上没有任何区别),using Eto.Drawing;
using Eto.Forms;
using Grasshopper2.Components;
using Grasshopper2.Doc;
using Grasshopper2.Doc.Attributes;
using Grasshopper2.Extensions;
using Grasshopper2.UI;
using Grasshopper2.UI.Canvas;
using Grasshopper2.UI.ContentBrowser;
using Grasshopper2.UI.Flex;
using Grasshopper2.UI.Primitives;
using Grasshopper2.UI.Skinning;
using GrasshopperIO;
using jianjuchanshuhua;
using ku;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Rhino.Geometry;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Brushes = Eto.Drawing.Brushes;
using Color = Eto.Drawing.Color;
using FontStyle = Eto.Drawing.FontStyle;
using Path = System.IO.Path;
using Pens = Eto.Drawing.Pens;
using PointF = Eto.Drawing.PointF;
using RectangleF = Eto.Drawing.RectangleF;
using Size = Eto.Drawing.Size;
using SystemFonts = Eto.Drawing.SystemFonts;
namespace ku
{
public class GitHubBrowser
{
public void ShowGitHubFileListWindow2(string path = “”)
{
var form = new Form
{
Title = “GitHub 仓库文件列表”,
ClientSize = new Size(600, 400)
};

        var grid = new GridView();
        grid.ShowHeader = true;

        // 文件名列
        grid.Columns.Add(new GridColumn
        {
            HeaderText = "文件名",
            DataCell = new TextBoxCell { Binding = Binding.Property<FileItem, string>(r => r.Name) },
            Expand = true
        });

        // 类型列
        grid.Columns.Add(new GridColumn
        {
            HeaderText = "类型",
            DataCell = new TextBoxCell { Binding = Binding.Property<FileItem, string>(r => r.Type) }
        });

        // 顶部返回按钮
        var backButton = new Button { Text = "← 返回上一级" };
        backButton.Enabled = false;

        var layout = new DynamicLayout();
        layout.AddRow(backButton);
        layout.AddRow(grid);
        form.Content = layout;

        string currentPath = path;

        async System.Threading.Tasks.Task LoadFiles(string relativePath)
        {
            string repoApiUrl = $"https://api.github.com/repos/wuq1/gh2_jzbf/contents/{relativePath}";
            try
            {
                using (var client = new HttpClient())
                {
                    client.DefaultRequestHeaders.Add("User-Agent", "request");
                    string json = await client.GetStringAsync(repoApiUrl);
                    var files = JArray.Parse(json);

                    var items = new List<FileItem>();
                    foreach (var file in files)
                    {
                        items.Add(new FileItem
                        {
                            Name = (string)file["name"],
                            Type = (string)file["type"],
                            DownloadUrl = (string)file["download_url"]
                        });
                    }

                    grid.DataStore = items;
                }

                backButton.Enabled = !string.IsNullOrEmpty(relativePath);
            }
            catch (Exception ex)
            {
                grid.DataStore = new List<FileItem> { new FileItem { Name = $"请求失败: {ex.Message}", Type = "" } };
                backButton.Enabled = false;
            }
        }

        // 首次加载
        _ = LoadFiles(currentPath);

        // 返回上一级按钮
        backButton.Click += async (sender, e) =>
        {
            if (!string.IsNullOrEmpty(currentPath))
            {
                int lastSlash = currentPath.LastIndexOf('/');
                currentPath = lastSlash > 0 ? currentPath.Substring(0, lastSlash) : "";
                await LoadFiles(currentPath);
            }
        };

        // 双击进入子目录 或 下载文件
        grid.CellDoubleClick += async (sender, e) =>
        {
            if (e.Item is FileItem file)
            {
                if (file.Type == "dir")
                {
                    currentPath = string.IsNullOrEmpty(currentPath) ? file.Name : $"{currentPath}/{file.Name}";
                    await LoadFiles(currentPath);
                }
                else if (file.Type == "file")
                {
                    await DownloadFile(file, form);
                  
                }
            }
        };

        // 右键菜单下载
        var menu = new ContextMenu();
        var downloadItem = new ButtonMenuItem { Text = "下载到桌面并且在参考窗口打开" };
        downloadItem.Click += async (sender, e) =>
        {

            if (grid.SelectedItem is FileItem file && file.Type == "file") 
            { await DownloadFile(file, form); }
        };
        menu.Items.Add(downloadItem);
        grid.ContextMenu = menu;

        form.Show();
    }

    private async System.Threading.Tasks.Task DownloadFile(FileItem file, Form parent)
    {
       
        try
        {
            using (var client = new HttpClient())
            {
                client.DefaultRequestHeaders.Add("User-Agent", "request");

                string savePath = Path.Combine(
                    Environment.GetFolderPath(Environment.SpecialFolder.Desktop),
                    file.Name
                );
                byte[] bytes = await client.GetByteArrayAsync(file.DownloadUrl);
                File.WriteAllBytes(savePath, bytes);

                MessageBox.Show(parent, $"已下载: {file.Name}\n保存到桌面");
                if (file.Name.EndsWith(".ghz", StringComparison.OrdinalIgnoreCase))
                {
                    // 下载完成后,直接打开 GH2 文档
                    var use = new Use();
                    use.ShowFileListWindow2(savePath);   // ✅ 注意这里传完整路径
                }
            }
        }
        catch (Exception ex)
        {
            MessageBox.Show(parent, $"下载失败: {ex.Message}");
        }
    }

    // 数据结构
    class FileItem
    {
        public string Name { get; set; }
        public string Type { get; set; }
        public string DownloadUrl { get; set; }
    }
}
public class MyComponentAttributes2 : ComponentAttributes
{
    private RectangleF buttonRect;   // 按钮矩形
    private bool pressed = false;    // 按钮状态

    public MyComponentAttributes2(Component owner) : base(owner) { }

    protected override void LayoutBounds(Shape shape)
    {
        base.LayoutBounds(shape);

        // 在电池底部增加 25 高的矩形区域作为按钮
        float extraHeight = 25f;
        buttonRect = RectangleF.FromSides(
            Bounds.Left,
            Bounds.Bottom,
            Bounds.Right,
            Bounds.Bottom + extraHeight
        );

        // 扩展电池整体 Bounds,包含按钮
        Bounds = RectangleF.Union(Bounds, buttonRect);
    }

    protected override void DrawForeground(Context context, Skin skin, Capsule capsule, Shade shade)
    {
        base.DrawForeground(context, skin, capsule, shade);

        // 按钮颜色
        var fill = pressed ? Colors.DarkGray : Colors.DarkGray;
        var border = Colors.Black;
        // 原始按钮区域(比如在电池下面)
        RectangleF buttonRect = new RectangleF(Bounds.X, Bounds.Y + 25, Bounds.Width, Bounds.Height - 26);

        // 在四周都收缩 2 像素
        buttonRect.Inflate(-2, -2);
        // 绘制按钮
        //context.Graphics.FillRectangle(fill, buttonRect);
        // context.Graphics.DrawRectangle(border, buttonRect);
        // 在你的电池 UI 绘制里用:

        float cornerRadius = 4f;
        using (var path = CreateRoundedRect(buttonRect, cornerRadius))
        {
            context.Graphics.FillPath(fill, path);
            context.Graphics.DrawPath(Pens.Gray, path);
        }

        // 绘制按钮文字
        var text = "一个按键";
        //var font = SystemFonts.Default(10);
        var font = new Eto.Drawing.Font(Eto.Drawing.SystemFont.Default, 10);

        var size = context.Graphics.MeasureString(font, text);
        var center = new PointF(
            buttonRect.Left + (buttonRect.Width - size.Width) / 2,
            buttonRect.Top + (buttonRect.Height - size.Height) / 2
        );
        context.Graphics.DrawText(font, Colors.Black, center, text);
    }

    protected override Response HandleMouseDown(MouseEventArgs e)
    {
        RectangleF clickRect = buttonRect;
        clickRect.Inflate(-2, -2);  // 同步缩小
        if (buttonRect.Contains(e.Location))
        {
            pressed = !pressed; // 切换按钮状态

            // 弹出窗口
            /*
            var form = new Eto.Forms.Form
            {
                Title = "按钮窗口",
                ClientSize = new Eto.Drawing.Size(200, 100)
            };
            form.Content = new Eto.Forms.Label
            {
                Text = "你点击了按钮!",
                VerticalAlignment = Eto.Forms.VerticalAlignment.Center,
                 TextAlignment = Eto.Forms.TextAlignment.Center  // 水平居中
            };
            form.Show();*/
            //
            // ShowFileListWindow();

           // GitHubBrowser b = new GitHubBrowser();
           // b.ShowGitHubFileListWindow2();
            Use use = new Use();
            use.ShowListWindow();
            Owner.Document?.Solution.DelayedExpire(Owner); // 刷新电池
            return Response.Handled;
        }

        return base.HandleMouseDown(e);
    }
    //绘制窗口
   


    GraphicsPath CreateRoundedRect(RectangleF rect, float radius)
    {
        var path = new GraphicsPath();

        float x = rect.X;
        float y = rect.Y;
        float w = rect.Width;
        float h = rect.Height;
        float r = radius;

        // 左上角
        path.AddArc(x, y, r * 2, r * 2, 180, 90);
        // 右上角
        path.AddArc(x + w - 2 * r, y, r * 2, r * 2, 270, 90);
        // 右下角
        path.AddArc(x + w - 2 * r, y + h - 2 * r, r * 2, r * 2, 0, 90);
        // 左下角
        path.AddArc(x, y + h - 2 * r, r * 2, r * 2, 90, 90);

        path.CloseFigure();
        return path;
    }

    private Color Lerp(Color a, Color b, float t)
    {
        float r = a.R + (b.R - a.R) * t;
        float g = a.G + (b.G - a.G) * t;
        float bl = a.B + (b.B - a.B) * t;
        float al = a.A + (b.A - a.A) * t;
        return new Color(r, g, bl, al);
    }
}
public class Use
{
    public void ShowFileListWindow2(string path)
    {
        var form = new Form
        {
            Title = "GH2 FileUtility 测试",
            ClientSize = new Size(800, 600)
        };

        // 创建 Canvas
        var canvas = new Canvas();

        // 打开 GH2 文档
        //path = @"C:\Users\32035\Desktop\001.ghz";
        bool ok = canvas.TryOpenDocument(path, OpenDocumentOptions.Activate);

        // 获取当前文档(Canvas 内部会创建一个 Document)
        Document doc = canvas.Document;
        if (doc != null)
        {
            doc.Notes = "这是一个测试文档";
            doc.Modify();
        }

        // 把 Canvas 加入窗口
        form.Content = canvas;

        // 在窗口关闭时清理
        form.Closed += (sender, e) =>
        {
            if (canvas.Document != null)
            {
                // 关闭文档释放资源
                canvas.Document.Close();
                Rhino.RhinoDoc.ActiveDoc.Views.Redraw();
            }

            // 清空 Canvas
            //canvas.Document = null;
        };

        // 显示窗口
        form.Show();
    }
    public void ShowListWindow()
    {
        string desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
        string filePath = Path.Combine(desktop, "information.json");

        Information info;

        if (File.Exists(filePath))
        {
            try
            {
                string json = File.ReadAllText(filePath);
                info = JsonConvert.DeserializeObject<Information>(json) ?? new Information();
            }
            catch
            {
                info = new Information();
            }
        }
        else
        {
            info = new Information();
        }

        var form = new Form
        {
            Title = "编辑信息",
            ClientSize = new Size(400, 350)
        };

        // 文本框
        var nameBox = new TextBox { Text = info.name ?? "" };
        var pathBox = new TextBox { Text = info.path ?? "" };
        var layersBox = new TextBox { Text = info.NumberOfLayers.ToString() };
        var heightBox = new TextBox { Text = info.CeilingHeight.ToString() };
        var wallBox = new TextBox { Text = info.ThickWall.ToString() };

        Action syncToInfo = () =>
        {
            info.name = nameBox.Text;
            info.path = pathBox.Text;
            if (int.TryParse(layersBox.Text, out int layers)) info.NumberOfLayers = layers;
            if (double.TryParse(heightBox.Text, out double height)) info.CeilingHeight = height;
            if (double.TryParse(wallBox.Text, out double wall)) info.ThickWall = wall;
        };

        var saveBtn = new Button { Text = "保存", Height = 25 };
        var importBtn = new Button { Text = "导入", Height = 25 };
        var saveAsBtn = new Button { Text = "另存为", Height = 25 };

        saveBtn.Click += (s, e) =>
        {
            syncToInfo();
            string json = JsonConvert.SerializeObject(info, Formatting.Indented);
            File.WriteAllText(filePath, json);
            MessageBox.Show($"信息已保存到:\n{filePath}");
            form.Close(); // 关闭窗口
        };

        importBtn.Click += (s, e) =>
        {
            var openDialog = new OpenFileDialog
            {
                Title = "选择信息文件",
                Filters = { new FileFilter("JSON 文件", ".json") }
            };
            if (openDialog.ShowDialog(form) == DialogResult.Ok)
            {
                try
                {
                    string json = File.ReadAllText(openDialog.FileName);
                    var loaded = JsonConvert.DeserializeObject<Information>(json);
                    if (loaded != null)
                    {
                        info = loaded;
                        nameBox.Text = info.name ?? "";
                        pathBox.Text = info.path ?? "";
                        layersBox.Text = info.NumberOfLayers.ToString();
                        heightBox.Text = info.CeilingHeight.ToString();
                        wallBox.Text = info.ThickWall.ToString();
                        MessageBox.Show("导入成功!");
                        form.Close(); // 关闭窗口
                    }
                }
                catch (Exception ex)
                {
                    MessageBox.Show("导入失败: " + ex.Message);
                }
            }
        };

        saveAsBtn.Click += (s, e) =>
        {
            syncToInfo();
            var saveDialog = new SaveFileDialog
            {
                Title = "另存为",
                Filters = { new FileFilter("JSON 文件", ".json") },
                FileName = "information.json"
            };
            if (saveDialog.ShowDialog(form) == DialogResult.Ok)
            {
                string json = JsonConvert.SerializeObject(info, Formatting.Indented);
                File.WriteAllText(saveDialog.FileName, json);
                MessageBox.Show($"文件已保存到:\n{saveDialog.FileName}");
                form.Close(); // 关闭窗口
            }
        };

        // 布局
        var layout = new TableLayout
        {
            Padding = 10,
            Spacing = new Size(5, 5)
        };

        layout.Rows.Add(new TableRow(new Label { Text = "名称:" }, nameBox));
        layout.Rows.Add(new TableRow(new Label { Text = "地址:" }, pathBox));
        layout.Rows.Add(new TableRow(new Label { Text = "层数:" }, layersBox));
        layout.Rows.Add(new TableRow(new Label { Text = "层高:" }, heightBox));
        layout.Rows.Add(new TableRow(new Label { Text = "墙厚:" }, wallBox));

        // 按钮横向居中,单独一行
        layout.Rows.Add(new TableRow(
            null,
            new StackLayout
            {
                Orientation = Orientation.Horizontal,
                HorizontalContentAlignment = HorizontalAlignment.Center,
                Spacing = 10,
                Items = { saveBtn, importBtn, saveAsBtn }
            }
        ));

        form.Content = new Scrollable { Content = layout };
        form.Show();
    }



}

}一个简单的例子.只需要在节点cs中添加protected override IAttributes CreateAttributes()
{
return new MyComponentAttributes2(this);
}