Table of Contents

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:

  1. Initialize UI Component - Set up the UI control properties and event handling
  2. Configure DispEngine - Create and properly initialize the DispEngine instance
  3. Set Up Rendering Pipeline - Implement buffer swapping mechanism for visualization
  4. Handle User Input - Map platform-specific input events to DispEngine methods
  5. 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:

  1. DispEngine processes IDisplayee objects
  2. Buffer is swapped and callback is triggered
  3. UI framework renders the buffer to screen
  4. User input triggers view updates
  5. 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;

See Also