Building Your Own Rendering Canvas
This guide provides detailed implementation information for creating your own RenderingCanvas
using the DispEngine. By understanding these implementation details, you can customize the rendering component for specific application needs or create implementations for other UI frameworks.
Note
For Windows Applications: If you are developing for Windows systems, it is recommended to directly use the existing RenderingCanvas
implementations in the Hi.WinForm
or Hi.Wpf
packages, rather than creating your own. These implementations are fully tested, optimized, and maintained.
For Webapi Applications: Using Hi.Webapi
.
The implementation details provided in this document are primarily for educational purposes or for developers who need to port RenderingCanvas to other platforms/frameworks.
Basic DispEngine Usage
The DispEngine is designed to display objects that implement the IDisplayee interface. This is the fundamental purpose of DispEngine - to render displayable objects. Assign IDisplayee to DispEngine.Displayee.
Core Implementation Pattern
When implementing a custom RenderingCanvas
for a UI platform, follow these key steps:
- Initialize UI Component - Set up the UI control properties and event handling
- Configure DispEngine - Create and properly initialize the DispEngine instance
- Set Up Rendering Pipeline - Implement buffer swapping mechanism for visualization
- Handle User Input - Map platform-specific input events to DispEngine methods
- Manage Component Lifecycle - Ensure proper resource management and cleanup
Let's examine the actual implementations in WinForm and WPF frameworks to understand these patterns in practice.
WinForm Implementation Details
The WinForm implementation in Hi.WinForm
combines Windows Forms controls with the DispEngine rendering system.
Core Properties and Fields
Here are the essential properties and fields defined in the WinForm implementation:
/// <summary>
/// <see cref="DispEngine"/>.
/// </summary>
public DispEngine DispEngine { get; }
// Constants and structures for WM_TOUCH
private const int WM_TOUCH = 0x0240;
private const int TOUCHEVENTF_MOVE = 0x0001;
private const int TOUCHEVENTF_DOWN = 0x0002;
private const int TOUCHEVENTF_UP = 0x0004;
[StructLayout(LayoutKind.Sequential)]
private struct TOUCHINPUT
{
public int x;
public int y;
public IntPtr hSource;
public int dwID;
public int dwFlags;
public int dwMask;
public int dwTime;
public IntPtr dwExtraInfo;
public int cxContact;
public int cyContact;
}
[DllImport("user32.dll")]
private static extern bool RegisterTouchWindow(IntPtr hWnd, uint ulFlags);
[DllImport("user32.dll")]
private static extern bool GetTouchInputInfo(IntPtr hTouchInput, int cInputs, [In, Out] TOUCHINPUT[] pInputs, int cbSize);
[DllImport("user32.dll")]
private static extern void CloseTouchInputHandle(IntPtr lParam);
Initialization
The initialization code sets up event handlers and creates the DispEngine:
/// <summary>
/// Ctor.
/// </summary>
/// <param name="displayees">displayees</param>
public unsafe RenderingCanvas(params IDisplayee[] displayees)
{
// Configure the control's visual styles
SetStyle(ControlStyles.Selectable, true);
SetStyle(ControlStyles.OptimizedDoubleBuffer, false);
SetStyle(ControlStyles.ContainerControl, false);
SetStyle(ControlStyles.ResizeRedraw, false);
DoubleBuffered = true;
InitializeComponent();
Dock = DockStyle.Fill;
// Connect event handlers for user input and window events
this.Resize += RenderingCanvas_Resize;
this.VisibleChanged += RenderingCanvas_VisibleChanged;
this.MouseMove += RenderingCanvas_MouseMove;
this.MouseDown += RenderingCanvas_MouseDown;
this.MouseUp += RenderingCanvas_MouseUp;
this.MouseWheel += RenderingCanvas_MouseWheel;
this.KeyDown += RenderingCanvas_KeyDown;
this.KeyUp += RenderingCanvas_KeyUp;
// Add focus event handler
this.GotFocus += RenderingCanvas_GotFocus;
this.HandleCreated += OnHandleCreated;
// Enable touch input and click events for the control
this.SetStyle(ControlStyles.StandardClick, true);
this.SetStyle(ControlStyles.StandardDoubleClick, true);
this.TabStop = true;
// Initialize the DispEngine with provided displayees
DispEngine = new DispEngine(displayees);
DispEngine.BackgroundColor = new Vec3d(0.1, 0.1, 0.5);
DispEngine.BackgroundOpacity = 0.1;
DispEngine.SetViewToHomeView();
DispEngine.ImageRequestAfterBufferSwapped += DispEngine_ImageRequestAfterBufferSwapped;
// Set initial size and start the rendering engine
this.Size = new System.Drawing.Size(500, 300);
DispEngine.Start(this.ClientSize.Width, this.ClientSize.Height);
}
Rendering Pipeline
The rendering pipeline processes images from DispEngine and displays them:
private unsafe void DispEngine_ImageRequestAfterBufferSwapped(byte* bgra_unsignedbyte_pixels, int w, int h)
{
// Create a bitmap from the raw pixel data provided by DispEngine
Bitmap bitmap;
bitmap = new Bitmap(new Bitmap(w, h, w * 4,
PixelFormat.Format32bppArgb, new IntPtr(bgra_unsignedbyte_pixels)));
// Update the background image and dispose the previous one
Image pre = this.BackgroundImage;
this.BackgroundImage = bitmap;
pre?.Dispose();
}
Input Handling
Windows Message Handling for Touch
WinForm implementation intercepts Windows touch messages and forwards them to DispEngine:
protected override void WndProc(ref Message m)
{
if (m.Msg == WM_TOUCH)
{
HandleTouchInput(m.WParam, m.LParam);
return;
}
base.WndProc(ref m);
}
private void OnHandleCreated(object sender, EventArgs e)
{
// Register window to receive touch messages
RegisterTouchWindow(this.Handle, 0);
}
private void HandleTouchInput(IntPtr wParam, IntPtr lParam)
{
int inputCount = wParam.ToInt32();
TOUCHINPUT[] inputs = new TOUCHINPUT[inputCount];
if (!GetTouchInputInfo(lParam, inputCount, inputs, Marshal.SizeOf(typeof(TOUCHINPUT))))
return;
try
{
for (int i = 0; i < inputCount; i++)
{
TOUCHINPUT ti = inputs[i];
int touchId = ti.dwID;
// Convert touch coordinates to client coordinates
Point touchPoint = PointToClient(new Point(ti.x / 100, ti.y / 100));
if ((ti.dwFlags & TOUCHEVENTF_DOWN) != 0)
{
// Touch down event
DispEngine.TouchDown(touchId, touchPoint.X, touchPoint.Y);
this.Focus();
}
else if ((ti.dwFlags & TOUCHEVENTF_MOVE) != 0)
{
// Touch move event
DispEngine.TouchMove(touchId, touchPoint.X, touchPoint.Y);
}
else if ((ti.dwFlags & TOUCHEVENTF_UP) != 0)
{
// Touch up event
DispEngine.TouchUp(touchId);
}
}
}
finally
{
CloseTouchInputHandle(lParam);
}
}
The key aspect is mapping Windows touch events to DispEngine's touch API:
// Inside HandleTouchInput method
if ((ti.dwFlags & TOUCHEVENTF_DOWN) != 0)
{
// Touch down event - delegate to DispEngine
DispEngine.TouchDown(touchId, touchPoint.X, touchPoint.Y);
this.Focus();
}
else if ((ti.dwFlags & TOUCHEVENTF_MOVE) != 0)
{
// Touch move event - delegate to DispEngine
DispEngine.TouchMove(touchId, touchPoint.X, touchPoint.Y);
}
else if ((ti.dwFlags & TOUCHEVENTF_UP) != 0)
{
// Touch up event - delegate to DispEngine
DispEngine.TouchUp(touchId);
}
Mouse Events
private void RenderingCanvas_MouseMove(object sender, MouseEventArgs e)
{
// Update mouse position and handle drag transforms
DispEngine.MouseMove(e.Location.X, e.Location.Y);
DispEngine.MouseDragTransform(e.Location.X, e.Location.Y,
new mouse_button_table__transform_view_by_mouse_drag_t()
{
LEFT_BUTTON = (long)MouseButtons.Left,
RIGHT_BUTTON = (long)MouseButtons.Right
});
}
private void RenderingCanvas_MouseDown(object sender, MouseEventArgs e)
{
// Handle mouse button press
DispEngine.MouseButtonDown((long)e.Button);
this.Focus();
}
private void RenderingCanvas_MouseUp(object sender, MouseEventArgs e)
{
// Handle mouse button release
DispEngine.MouseButtonUp((long)e.Button);
}
private void RenderingCanvas_MouseWheel(object sender, MouseEventArgs e)
{
// Handle mouse wheel for zoom operations
DispEngine.MouseWheel(0, e.Delta / 120);
DispEngine.MouseWheelTransform(0, e.Delta / 120);
}
Keyboard Events
/// <inheritdoc/>
protected override bool IsInputKey(Keys keyData)
{
//since in default, arrow does not trigger key event(keyDown and keyUp).
return true;
}
private void RenderingCanvas_KeyDown(object sender, KeyEventArgs e)
{
Focus();
DispEngine.KeyDown((long)e.KeyData);
// Map specific keys for view transformation
long key = (long)e.KeyData;
if (key == (long)Keys.LShiftKey || key == (long)Keys.RShiftKey || key == (long)Keys.ShiftKey)
key = (long)Keys.Shift;
DispEngine.KeyDownTransform(key, new key_table__transform_view_by_key_pressing_t()
{
HOME = (long)Keys.Home,
PAGE_UP = (long)Keys.PageUp,
PAGE_DOWN = (long)Keys.PageDown,
F1 = (long)Keys.F1,
F2 = (long)Keys.F2,
F3 = (long)Keys.F3,
F4 = (long)Keys.F4,
SHIFT = (long)Keys.Shift,
ARROW_LEFT = (long)Keys.Left,
ARROW_RIGHT = (long)Keys.Right,
ARROW_DOWN = (long)Keys.Down,
ARROW_UP = (long)Keys.Up
});
}
private void RenderingCanvas_KeyUp(object sender, KeyEventArgs e)
{
DispEngine.KeyUp((long)e.KeyData);
}
Lifecycle Management
Window event handling ensures proper state management:
private void RenderingCanvas_Resize(object sender, EventArgs e)
{
// Notify DispEngine of size changes
DispEngine.Resize(this.ClientSize.Width, this.ClientSize.Height);
}
private void RenderingCanvas_VisibleChanged(object sender, EventArgs e)
{
// Update visibility state in DispEngine
DispEngine.IsVisible = this.Visible;
}
Resource Cleanup
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
// Dispose the DispEngine to free resources
DispEngine.Dispose();
components.Dispose();
}
base.Dispose(disposing);
}
WPF Implementation Details
The WPF implementation uses WPF-specific controls and mechanisms but follows the same core pattern.
Core Properties
/// <summary>
/// The DispEngine instance that handles rendering and user interactions
/// </summary>
public DispEngine DispEngine { get; } = new DispEngine();
/// <summary>
/// Internal container for rendering content
/// </summary>
private UserControl DisplayerPane { get; }
/// <summary>
/// Dictionary to store touch point information
/// </summary>
private Dictionary<int, Point> TouchingPointsMap { get; } = new Dictionary<int, Point>();
/// <summary>
/// Dictionary to store previous positions of touch points
/// </summary>
private Dictionary<int, Point> PreviousTouchingPointsMap { get; } = new Dictionary<int, Point>();
Initialization
/// <summary>
/// Initializes a new instance of the RenderingCanvas
/// </summary>
public RenderingCanvas()
{
DispEngine.BackgroundColor = new Vec3d(0.1, 0.1, 0.5);
DispEngine.BackgroundOpacity = 0.1;
// Configure the main control properties
HorizontalAlignment = HorizontalAlignment.Stretch;
VerticalAlignment = VerticalAlignment.Stretch;
Focusable = true;
KeyboardNavigation.SetDirectionalNavigation(this, KeyboardNavigationMode.Cycle);
DataContextChanged += CanvasDataContextChanged;
// Create and configure the display pane
DisplayerPane = new UserControl();
DisplayerPane.HorizontalAlignment = HorizontalAlignment.Stretch;
DisplayerPane.VerticalAlignment = VerticalAlignment.Stretch;
DisplayerPane.Focusable = true;
DisplayerPane.IsTabStop = true;
// Connect event handlers for user input and window events
DisplayerPane.SizeChanged += RenderingCanvas_SizeChanged;
DisplayerPane.MouseMove += RenderingCanvas_MouseMove;
DisplayerPane.MouseDown += RenderingCanvas_MouseDown;
DisplayerPane.MouseUp += RenderingCanvas_MouseUp;
DisplayerPane.MouseWheel += RenderingCanvas_MouseWheel;
DisplayerPane.KeyDown += RenderingCanvas_KeyDown;
DisplayerPane.KeyUp += RenderingCanvas_KeyUp;
DisplayerPane.Loaded += RenderingCanvas_Loaded;
DisplayerPane.Unloaded += RenderingCanvas_Unloaded;
DisplayerPane.IsVisibleChanged += DisplayerPane_IsVisibleChanged;
// Add touch event handlers
DisplayerPane.TouchDown += RenderingCanvas_TouchDown;
DisplayerPane.TouchMove += RenderingCanvas_TouchMove;
DisplayerPane.TouchUp += RenderingCanvas_TouchUp;
// Enable touch support
this.IsManipulationEnabled = true;
// Initialize power management
InitializePowerManagement();
// Add the display pane to this control's content
Content = DisplayerPane;
}
Rendering Pipeline
private byte[] PreImageBgra { get; set; }
/// <summary>
/// Handles the buffer swapped event from DispEngine
/// </summary>
private unsafe void RenderingCanvas_BufferSwapped(byte* data, int w, int h)
{
if (data == null)
return;
//since the treatment is different from Razor version,
//the image checking mechanism cannot be set in DispEngine.
Span<byte> bgra = new Span<byte>(data, w * h * 4);
if (PreImageBgra != null && bgra.SequenceEqual(PreImageBgra))
return;
PreImageBgra = bgra.ToArray();
// Copy pixel data from DispEngine
int n = w * h * 4;
byte[] arr = new byte[n];
for (int i = 0; i < n; i++)
arr[i] = data[i];
// Update UI on the UI thread
DisplayerPane.Dispatcher.InvokeAsync(() =>
{
BitmapSource bitmap = BitmapSource.Create(w, h, 1, 1, PixelFormats.Bgra32, null, arr, w * 4);
DisplayerPane.Background = new ImageBrush(bitmap);
});
}
/// <summary>
/// Handles the size changed event
/// </summary>
private void RenderingCanvas_SizeChanged(object sender, SizeChangedEventArgs e)
{
// Notify DispEngine of size changes
DispEngine.Resize((int)DisplayerPane.RenderSize.Width, (int)DisplayerPane.RenderSize.Height);
}
/// <summary>
/// Handles visibility changes
/// </summary>
private unsafe void DisplayerPane_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
// Update visibility state in DispEngine
DispEngine.IsVisible = IsVisible;
}
Mouse and Keyboard Handling
/// <summary>
/// Helper method to get mouse button mask
/// </summary>
internal static HiMouseButtonMask GetMouseButtonMask(MouseDevice device)
{
HiMouseButtonMask mouseButtonMask = 0;
mouseButtonMask.SetLeftPressed(device.LeftButton == MouseButtonState.Pressed);
mouseButtonMask.SetMiddlePressed(device.MiddleButton == MouseButtonState.Pressed);
mouseButtonMask.SetRightPressed(device.RightButton == MouseButtonState.Pressed);
mouseButtonMask.SetXButton1Pressed(device.XButton1 == MouseButtonState.Pressed);
mouseButtonMask.SetXButton2Pressed(device.XButton2 == MouseButtonState.Pressed);
return mouseButtonMask;
}
/// <summary>
/// Handles the mouse wheel event
/// </summary>
private void RenderingCanvas_MouseWheel(object sender, MouseWheelEventArgs e)
{
// Handle mouse wheel for zoom operations
DispEngine.MouseWheel(0, e.Delta / 120);
DispEngine.MouseWheelTransform(0, e.Delta / 120);
}
/// <summary>
/// Handles the mouse up event
/// </summary>
private void RenderingCanvas_MouseUp(object sender, MouseButtonEventArgs e)
{
// Handle mouse button release
DispEngine.MouseButtonUp((long)e.ChangedButton);
(sender as UIElement)?.ReleaseMouseCapture();
}
/// <summary>
/// Handles the mouse down event
/// </summary>
private void RenderingCanvas_MouseDown(object sender, MouseButtonEventArgs e)
{
// Handle mouse button press
DispEngine.MouseButtonDown((long)e.ChangedButton);
DisplayerPane.Focus();
(sender as UIElement)?.CaptureMouse();
}
/// <summary>
/// Handles the mouse move event
/// </summary>
private void RenderingCanvas_MouseMove(object sender, MouseEventArgs e)
{
// Update mouse position and handle drag transforms
Point p = e.GetPosition(DisplayerPane);
DispEngine.MouseMove((int)p.X, (int)p.Y);
DispEngine.MouseDragTransform((int)p.X, (int)p.Y,
new mouse_button_table__transform_view_by_mouse_drag_t()
{
LEFT_BUTTON = (long)MouseButton.Left,
RIGHT_BUTTON = (long)MouseButton.Right
});
}
/// <summary>
/// Handles the key up event
/// </summary>
private void RenderingCanvas_KeyUp(object sender, KeyEventArgs e)
{
DispEngine.KeyUp((long)e.Key);
}
/// <summary>
/// Handles the key down event
/// </summary>
private void RenderingCanvas_KeyDown(object sender, KeyEventArgs e)
{
DispEngine.KeyDown((long)e.Key);
// Map specific keys for view transformation
long key = (long)e.Key;
if (key == (long)Key.RightShift)
key = (long)Key.LeftShift;
DispEngine.KeyDownTransform(key, new key_table__transform_view_by_key_pressing_t()
{
HOME = (long)Key.Home,
PAGE_UP = (long)Key.PageUp,
PAGE_DOWN = (long)Key.PageDown,
F1 = (long)Key.F1,
F2 = (long)Key.F2,
F3 = (long)Key.F3,
F4 = (long)Key.F4,
SHIFT = (long)Key.LeftShift,
ARROW_LEFT = (long)Key.Left,
ARROW_RIGHT = (long)Key.Right,
ARROW_DOWN = (long)Key.Down,
ARROW_UP = (long)Key.Up
});
}
Lifecycle Management
/// <summary>
/// Handles window state changes (maximize, minimize, etc.)
/// </summary>
private unsafe void RenderingCanvas_StateChanged(object sender, EventArgs e)
{
switch ((sender as Window).WindowState)
{
case WindowState.Maximized:
DispEngine.IsVisible = true;
break;
case WindowState.Minimized:
DispEngine.IsVisible = false;
break;
case WindowState.Normal:
DispEngine.IsVisible = true;
break;
}
}
/// <summary>
/// Handles data context changes
/// </summary>
private unsafe void CanvasDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
DispEngine pre = e.OldValue as DispEngine;
DispEngine cur = e.NewValue as DispEngine;
//child's binding event is triggered after IsVisible event and Load event.
if (pre != null) //this section will never occur if the datacontext not set twice.
{
pre.Terminate();
pre.ImageRequestAfterBufferSwapped -= RenderingCanvas_BufferSwapped;
}
if (cur != null)
{
cur.ImageRequestAfterBufferSwapped += RenderingCanvas_BufferSwapped;
cur.Start((int)DisplayerPane.RenderSize.Width, (int)DisplayerPane.RenderSize.Height);
cur.IsVisible = IsVisible;
}
}
/// <summary>
/// Reference to the current window containing this control
/// </summary>
private Window currentWindow;
/// <summary>
/// Gets or sets the current window, connecting or disconnecting state change events
/// </summary>
Window CurrentWindow
{
get => currentWindow; set
{
if (currentWindow != null)
currentWindow.StateChanged -= RenderingCanvas_StateChanged;
currentWindow = value;
if (currentWindow != null)
currentWindow.StateChanged += RenderingCanvas_StateChanged;
}
}
/// <summary>
/// Handles the loaded event
/// </summary>
private unsafe void RenderingCanvas_Loaded(object sender, RoutedEventArgs e)
{
// Get the window containing this control
CurrentWindow = Window.GetWindow(this);
// Set up DispEngine rendering
DispEngine.ImageRequestAfterBufferSwapped -= RenderingCanvas_BufferSwapped;
DispEngine.ImageRequestAfterBufferSwapped += RenderingCanvas_BufferSwapped;
DispEngine.Start((int)DisplayerPane.RenderSize.Width, (int)DisplayerPane.RenderSize.Height);
DispEngine.IsVisible = IsVisible;
}
/// <summary>
/// Handles the unloaded event
/// </summary>
private unsafe void RenderingCanvas_Unloaded(object sender, RoutedEventArgs e)
{
DispEngine.IsVisible = IsVisible;
DispEngine.ImageRequestAfterBufferSwapped -= RenderingCanvas_BufferSwapped;
CurrentWindow = null;
}
Resource Cleanup
/// <summary>
/// Flag to track disposed state
/// </summary>
private bool disposedValue;
/// <summary>
/// Disposes managed resources
/// </summary>
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
// Unsubscribe from power events
SystemEvents.PowerModeChanged -= SystemEvents_PowerModeChanged;
// Dispose the DispEngine to free resources
DispEngine.Dispose();
}
disposedValue = true;
}
}
/// <summary>
/// Public dispose method to free resources
/// </summary>
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
Core DispEngine Integration Patterns
1. Initialization Sequence
// Create DispEngine (optionally with displayees)
var engine = new DispEngine(displayees);
// Set up image buffer callback
engine.ImageRequestAfterBufferSwapped += OnBufferSwapped;
// Initialize with canvas size
engine.Start(width, height);
// Set initial view (optional)
engine.SetViewToHomeView();
2. Render Loop
The rendering process follows this pattern:
- DispEngine processes IDisplayee objects
- Buffer is swapped and callback is triggered
- UI framework renders the buffer to screen
- User input triggers view updates
- Process repeats
3. Complete User Input Mapping
All user interactions must be mapped to DispEngine methods:
User Action | DispEngine Method |
---|---|
Mouse move | MouseMove(int, int) |
Mouse drag | MouseDragTransform(int, int, mouse_button_table__transform_view_by_mouse_drag_t) |
Mouse button | MouseButtonDown(long) / MouseButtonUp(long) |
Mouse wheel | MouseWheel(int, int) and MouseWheelTransform(int, int, double) |
Key press | KeyDown(long) / KeyUp(long) and KeyDownTransform(long, key_table__transform_view_by_key_pressing_t) |
Touch events | TouchDown(int, int, int) / TouchMove(int, int, int) / TouchUp(int) |
4. Proper Resource Cleanup
Resource management is critical for proper operation:
// In dispose method
DispEngine.ImageRequestAfterBufferSwapped -= OnBufferSwapped;
DispEngine.Terminate();
DispEngine.Dispose();
Advanced Implementation Considerations
When creating custom implementations, consider these aspects:
View Manipulation
Use SketchView to directly access or modify the view matrix:
// Get current view matrix
Mat4d currentView = engine.SketchView;
// Apply custom rotation
Mat4d rotation = Mat4d.RotateX(Math.PI/4);
engine.SketchView = currentView * rotation;