Hi Everyone,
I am developing a RhinoCommon plugin where I need to embed a fully interactive native Rhino Viewport into a WPF window (using HwndHost).
I cannot use Rhino.UI.Controls.ViewportControl because I need full native navigation behavior, context menus, and standard object interaction, which the lightweight control lacks.
My Approach:
-
Create a new view using
RhinoDoc.ActiveDoc.Views.Add(...). -
Use P/Invoke
SetParentto re-parent the native Rhino View handle into a WPFHwndHost. -
Strip the window styles (
WS_CAPTION,WS_THICKFRAME, etc.) to make it look like a control.
The Problem:
The embedding works perfectly (rendering and interaction are fine). However, destroying the WPF window causes severe stability issues:
-
Black Screen on New File: After closing the WPF window (and the embedded view), if I open a new file or create a new document, all Rhino viewports turn completely black. It seems the OpenGL/Display Pipeline context is corrupted or lost.
-
Process Deadlock: Often, Rhino cannot be closed normally after this operation and must be terminated via Task Manager.
What I have tried (but failed):
I implemented a “Safe Cleanup” logic in the DestroyWindowCore of the HwndHost:
-
Deactivate View: Switched
ActiveViewto a different safe viewport and forcedRedraw(). -
Reparenting: Before calling
Close(), I usedSetParentto move the view handle back to its original parent (or the main Rhino window) to detach it from WPF. -
Deferred Close: I used
RhinoApp.Idleto delay theRhinoView.Close()call to avoid conflict with the WPF destruction cycle.
Despite these efforts, the display pipeline still breaks (Black Screen) upon loading a new document.
using System;
using System.Runtime.InteropServices;
using Rhino;
using Rhino.Commands;
using Rhino.Display;
using Rhino.DocObjects;
using Rhino.Geometry;
using Rhino.Input;
using Rhino.Input.Custom;
using Rhino.UI;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Controls;
using System.Windows.Media;
using Label = System.Windows.Controls.Label;
namespace Test
{
public class TestWpfEmbeddedView : Command
{
public override string EnglishName => "TestWpfEmbeddedView";
protected override Result RunCommand(RhinoDoc doc, RunMode mode)
{
var wpfWindow = new MyWpfWindow();
new System.Windows.Interop.WindowInteropHelper(wpfWindow).Owner = RhinoApp.MainWindowHandle();
wpfWindow.Show();
return Result.Success;
}
}
public class MyWpfWindow : System.Windows.Window
{
private RhinoViewportHost _rhinoHost;
private SimplePreviewConduit _conduit;
private ViewportInteraction _mouseHandler;
private Brep _sphere;
private void InitLayout()
{
var grid = new Grid();
grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(30) });
grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
var label = new Label { Content = "WPF Window", HorizontalAlignment = System.Windows.HorizontalAlignment.Center };
Grid.SetRow(label, 0);
grid.Children.Add(label);
_rhinoHost = new RhinoViewportHost();
Grid.SetRow(_rhinoHost, 1);
grid.Children.Add(_rhinoHost);
this.Content = grid;
}
private void OnWindowLoaded(object sender, RoutedEventArgs e)
{
if (_rhinoHost.RhinoView != null)
{
var viewId = _rhinoHost.RhinoView.ActiveViewportID;
_conduit.TargetViewportId = viewId;
_rhinoHost.RhinoView.ActiveViewport.ZoomBoundingBox(_conduit.ContentBoundingBox);
_rhinoHost.RhinoView.Redraw();
_mouseHandler = new ViewportInteraction(viewId, _sphere, _conduit);
_mouseHandler.Enabled = true;
}
}
private void OnWindowClosed(object sender, EventArgs e)
{
_conduit.Enabled = false;
if (_mouseHandler != null) _mouseHandler.Enabled = false;
}
public MyWpfWindow()
{
Title = "WPF Embed Fixed";
Width = 800;
Height = 600;
WindowStartupLocation = WindowStartupLocation.CenterOwner;
_sphere = new Sphere(Point3d.Origin, 15).ToBrep();
_conduit = new SimplePreviewConduit(_sphere, System.Drawing.Color.Red);
_conduit.Enabled = true;
InitLayout();
this.Loaded += OnWindowLoaded;
this.Closed += OnWindowClosed;
}
}
public class RhinoViewportHost : HwndHost
{
private const int GWL_STYLE = -16;
private const int WS_CHILD = 0x40000000;
private const int WS_VISIBLE = 0x10000000;
private const int WS_CAPTION = 0xC00000;
private const int WS_THICKFRAME = 0x40000;
private const int WS_POPUP = unchecked((int)0x80000000);
private const int SW_HIDE = 0;
private const uint SWP_FRAMECHANGED = 0x0020;
private const uint SWP_NOMOVE = 0x0002;
private const uint SWP_NOSIZE = 0x0001;
private const uint SWP_NOZORDER = 0x0004;
private const uint SWP_SHOWWINDOW = 0x0040;
private IntPtr _originalParent = IntPtr.Zero;
private bool _isClosing = false;
public RhinoView RhinoView { get; private set; }
protected override HandleRef BuildWindowCore(HandleRef hwndParent)
{
string viewName = "Embedded_" + Guid.NewGuid().ToString();
var bounds = new System.Drawing.Rectangle(0, 0, 100, 100);
RhinoView = RhinoDoc.ActiveDoc.Views.Add(viewName, DefinedViewportProjection.Perspective, bounds, true);
if (RhinoView == null) throw new Exception("Create View Failed");
IntPtr viewHandle = RhinoView.Handle;
_originalParent = GetParent(viewHandle);
if (_originalParent != IntPtr.Zero) ShowWindow(_originalParent, SW_HIDE);
int style = GetWindowLong(viewHandle, GWL_STYLE);
style = (style & ~WS_POPUP & ~WS_CAPTION & ~WS_THICKFRAME) | WS_CHILD | WS_VISIBLE;
SetWindowLong(viewHandle, GWL_STYLE, style);
SetParent(viewHandle, hwndParent.Handle);
SetWindowPos(viewHandle, IntPtr.Zero, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED | SWP_SHOWWINDOW);
RhinoView.ActiveViewport.DisplayMode = DisplayModeDescription.FindByName("Shaded");
return new HandleRef(this, viewHandle);
}
protected override void DestroyWindowCore(HandleRef hwnd)
{
SafeDestroy();
}
[DllImport("user32.dll", SetLastError = true)]
private static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent);
[DllImport("user32.dll")]
private static extern IntPtr GetParent(IntPtr hWnd);
[DllImport("user32.dll")]
private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
[DllImport("user32.dll")]
private static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
[DllImport("user32.dll")]
private static extern int GetWindowLong(IntPtr hWnd, int nIndex);
[DllImport("user32.dll")]
private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
private void SafeDestroy()
{
if (RhinoView == null || _isClosing) return;
_isClosing = true;
try
{
var doc = RhinoDoc.ActiveDoc;
var viewToClose = RhinoView;
if (doc.Views.ActiveView.ActiveViewportID == viewToClose.ActiveViewportID)
{
var safeView = System.Linq.Enumerable.FirstOrDefault(doc.Views, v => v.ActiveViewportID != viewToClose.ActiveViewportID);
if (safeView != null)
{
doc.Views.ActiveView = safeView;
}
doc.Views.Redraw();
RhinoApp.Wait();
}
if (_originalParent != IntPtr.Zero)
{
SetParent(viewToClose.Handle, _originalParent);
}
else
{
SetParent(viewToClose.Handle, RhinoApp.MainWindowHandle());
}
RhinoApp.Idle += OnIdleCloseView;
}
catch (Exception ex)
{
RhinoApp.WriteLine($"SafeDestroy Error: {ex.Message}");
}
}
private void OnIdleCloseView(object sender, EventArgs e)
{
RhinoApp.Idle -= OnIdleCloseView;
try
{
if (RhinoView != null)
{
RhinoView.Close();
}
RhinoDoc.ActiveDoc?.Views.Redraw();
}
catch
{
}
finally
{
RhinoView = null;
_originalParent = IntPtr.Zero;
}
}
}
public class ViewportInteraction : Rhino.UI.MouseCallback
{
private readonly Guid _targetViewportId;
private readonly Brep _targetGeometry;
private readonly SimplePreviewConduit _conduit;
protected override void OnMouseDown(Rhino.UI.MouseCallbackEventArgs e)
{
if (e.View.ActiveViewportID != _targetViewportId) return;
if (e.Button != System.Windows.Forms.MouseButtons.Left) return;
var line = e.View.ActiveViewport.ClientToWorld(e.ViewportPoint);
var intersection = Rhino.Geometry.Intersect.Intersection.RayShoot(
new Ray3d(line.From, line.Direction), new[] { _targetGeometry }, 1
);
if (intersection != null && intersection.Length > 0)
{
_conduit.Highlight = !_conduit.Highlight;
e.View.Redraw();
}
}
public ViewportInteraction(Guid viewId, Brep geo, SimplePreviewConduit conduit)
{
_targetViewportId = viewId;
_targetGeometry = geo;
_conduit = conduit;
}
}
public class SimplePreviewConduit : Rhino.Display.DisplayConduit
{
private readonly Brep _geometry;
private readonly DisplayMaterial _normalMat;
private readonly DisplayMaterial _highlightMat;
public bool Highlight { get; set; } = false;
public Guid TargetViewportId { get; set; } = Guid.Empty;
public BoundingBox ContentBoundingBox => _geometry.GetBoundingBox(true);
protected override void ObjectCulling(CullObjectEventArgs e)
{
if (TargetViewportId != Guid.Empty && e.Viewport.Id != TargetViewportId) { base.ObjectCulling(e); return; }
if (e.RhinoObject != null) e.CullObject = true;
}
protected override void PostDrawObjects(DrawEventArgs e)
{
if (TargetViewportId != Guid.Empty && e.Viewport.Id != TargetViewportId) return;
var mat = Highlight ? _highlightMat : _normalMat;
e.Display.DrawBrepShaded(_geometry, mat);
e.Display.DrawBrepWires(_geometry, System.Drawing.Color.Black, 1);
}
protected override void CalculateBoundingBox(CalculateBoundingBoxEventArgs e)
{
if (TargetViewportId != Guid.Empty && e.Viewport.Id != TargetViewportId) return;
e.IncludeBoundingBox(ContentBoundingBox);
}
public SimplePreviewConduit(Brep geometry, System.Drawing.Color color)
{
_geometry = geometry;
_normalMat = new DisplayMaterial(color);
_highlightMat = new DisplayMaterial(System.Drawing.Color.Yellow);
}
}
}
My Question:
Is there a supported or “correct” way to cleanly destroy a RhinoView that has been re-parented via SetParent without corrupting the global Display Pipeline? Or is there an alternative way to host a fully native RhinoView inside a WPF container?
Any insights would be greatly appreciated.