Creating an UI Plugin

An OpenSR UI plugin is a dedicated UI DLL loaded by the dashboard or plugin manager.
Unlike Game (IN) or OUT plugins, a UI plugin does not own its own top-level window.

The host provides a child HWND, and the plugin must build and render its entire interface inside that window.

The UI plugin lifecycle is fully managed by the host:

  • Initialize() is called when the UI page becomes active
  • Shutdown() is called when the page is closed or switched
  • Re-entering the page recreates the UI and reloads settings automatically

Because of this lifecycle, no special callback is required to notify settings changes.
If the user edits settings and returns to the UI page, Initialize() runs again and the plugin reloads its configuration naturally.

UI plugins may use:

  • Win32 + GDI
  • GDI+
  • Direct2D
  • DirectWrite
  • Any rendering system compatible with a child HWND

UI Plugin Architecture

A UI plugin implements IPluginUI.

Example export functions:

extern "C" __declspec(dllexport) IPluginUI* CreatePluginUI()
{
    return new MonitorUIPlugin();
}

extern "C" __declspec(dllexport) void DestroyPluginUI(IPluginUI* pUI)
{
    if (pUI)
        delete static_cast<MonitorUIPlugin*>(pUI);
}

Initialize()

The host passes:

BOOL Initialize(
    HWND hWindow,
    OpenSRContext* context,
    void* outBuf,
    const wchar_t* pluginFolderPath,
    const uint64_t sharedBuffer
) override;

Parameters

ParameterDescription
hWindowChild HWND owned by the host
contextOpenSR shared context
outBufOUT telemetry buffers
pluginFolderPathPlugin folder path
sharedBufferOptional shared pointer from OUT plugin

Host-Owned HWND

The most important concept:

The HWND passed to Initialize() belongs to the host application.

The UI plugin:

  • MUST NOT destroy it
  • MUST NOT subclass permanently without restoring
  • MUST render only inside this window
  • MUST cleanup all resources before Shutdown() returns

Typical usage:

m_hWnd = hWindow;

The plugin can then:

  • Create child controls
  • Subclass the window
  • Render using GDI/GDI+/D2D
  • Process messages

Creating Controls

Controls are created as children of the provided host window.

Example:

m_hDumpButton = CreateWindowExW(
    0,
    L"BUTTON",
    L"Dump Lap",
    WS_CHILD | WS_VISIBLE | BS_OWNERDRAW,
    25, 5, 80, 28,
    m_hWnd,
    (HMENU)IDOK,
    hInstance,
    NULL
);

All controls must use m_hWnd as parent.


Window Subclassing

UI plugins typically subclass the host window to intercept:

  • WM_PAINT
  • WM_COMMAND
  • WM_SIZE
  • WM_DRAWITEM
  • Mouse handling
  • Keyboard handling

Example:

SetWindowSubclass(
    m_hWnd,
    MonitorUIPlugin::SubclassWndProc,
    1,
    (DWORD_PTR)this
);

The subclass procedure can then dispatch messages to the plugin instance.


Mandatory Cleanup

Before Shutdown() returns, the plugin MUST restore the original window procedure.

Failure to un-subclass the host window will cause host instability and crashes.

Correct cleanup:

if (m_hWnd) {
    RemoveWindowSubclass(
        m_hWnd,
        MonitorUIPlugin::SubclassWndProc,
        1
    );
}

This is mandatory.


WantsDialogMessages()

BOOL WantsDialogMessages() const override
{
    return TRUE;
}

This tells the host whether the plugin wants to receive dialog-style keyboard messages.

Typical reasons:

  • TAB navigation
  • ENTER handling
  • ESC handling
  • Keyboard shortcuts
  • Input controls

If your UI does not need keyboard/dialog interaction, returning FALSE is acceptable.


Shared Communication With OUT Plugin

UI plugins may optionally communicate with their associated OUT plugin.

The OUT plugin can expose a shared pointer:

virtual uint64_t GetSharedPointer()
{
    return 0;
}

The host forwards this pointer to the UI plugin during Initialize():

const uint64_t sharedBuffer

The UI plugin may reinterpret the pointer:

SharedData* data =
    reinterpret_cast<SharedData*>(sharedBuffer);

This mechanism is entirely plugin-defined.

The host does not inspect or manage the memory.

Typical uses:

  • Shared runtime statistics
  • Device state
  • Internal queues
  • UI interaction state
  • Live monitoring data
  • Shared caches

The developer is fully responsible for:

  • Thread safety
  • Synchronization
  • Lifetime management
  • Memory ownership

The host only forwards the raw pointer.


Settings Lifecycle

UI plugins are recreated whenever the user reopens the UI page.

Example flow:

  1. User opens UI page
  2. Initialize() called
  3. Plugin loads settings
  4. User switches to Settings page
  5. UI plugin is destroyed
  6. User edits settings
  7. User returns to UI page
  8. Initialize() called again
  9. Settings reloaded automatically

Because of this lifecycle:

  • No settings callback is needed
  • No hot reload mechanism is required
  • Reinitialization naturally reloads configuration

Typical loading:

std::wstring spath =
    StringUtils::createWString(
        m_pContext->osrDocFolder,
        L"\\",
        m_pluginPath,
        L"\\settings.xml"
    );

LoadPluginSettings(spath, m_pluginSettings);

Rendering

The example implementation uses:

  • GDI
  • GDI+
  • Double buffering
  • Owner-drawn controls

However Direct2D is fully supported as well.

The only requirement is rendering into the provided child HWND.


Double Buffered Rendering

The sample uses a memory DC to avoid flickering:

HDC memDC = CreateCompatibleDC(hdc);
HBITMAP memBitmap =
    CreateCompatibleBitmap(hdc, width, height);

SelectObject(memDC, memBitmap);

After rendering:

BitBlt(
    hdc,
    0, 0,
    width, height,
    memDC,
    0, 0,
    SRCCOPY
);

This is strongly recommended for smooth UI rendering.


Render Thread

The example uses a dedicated render/update thread.

The thread periodically invalidates the window:

InvalidateRect(m_hWnd, &rc, FALSE);

This allows:

  • Controlled refresh rate
  • Background telemetry updates
  • Smooth rendering
  • Reduced CPU usage

The plugin itself owns and manages the thread lifecycle.


Thread Ownership

UI plugins are responsible for their own worker threads.

Before shutdown:

  • Signal stop
  • Wake sleeping threads
  • Join all threads
  • Release synchronization objects

Example:

m_stopRequested = true;

m_pauseCv.notify_all();
m_sleepCv.notify_all();

if (renderThread_.joinable())
    renderThread_.join();

Never leave worker threads running after Shutdown().


WM_PAINT Handling

Typical implementation:

case WM_PAINT:
{
    PAINTSTRUCT ps;
    HDC hdc = BeginPaint(hWnd, &ps);

    pThis->RenderTelemetry(hdc);

    EndPaint(hWnd, &ps);
    return 0;
}

The UI plugin completely owns rendering inside the provided client area.


WM_COMMAND Handling

Child controls send commands through the host HWND.

Example:

case WM_COMMAND:
{
    const int id = LOWORD(wParam);

    if (id == IDOK)
    {
        pThis->OnDump((HWND)lParam);
    }

    return 0;
}

Owner Draw Controls

The sample uses:

BS_OWNERDRAW

Combined with:

WM_DRAWITEM

This allows:

  • Custom styling
  • Hover states
  • Modern buttons
  • Full GDI+ rendering

GDI+ Initialization

The plugin initializes GDI+ in the constructor:

GdiplusStartupInput gdiplusStartupInput;

GdiplusStartup(
    &gdiplusToken_,
    &gdiplusStartupInput,
    NULL
);

And shuts it down in the destructor:

GdiplusShutdown(gdiplusToken_);

Important Rules

DO

  • Render only inside the provided HWND
  • Cleanup everything during shutdown
  • Remove all subclasses
  • Join all threads
  • Destroy all GDI objects
  • Handle repainting properly
  • Use double buffering

DO NOT

  • Destroy the host HWND
  • Leak threads
  • Keep dangling subclass procedures
  • Assume the UI lives forever
  • Store invalid pointers after shutdown

Minimal Lifecycle Summary

Creation

Host creates child HWND
        ↓
Load UI DLL
        ↓
CreatePluginUI()
        ↓
Initialize()
        ↓
Plugin creates controls/rendering

Destruction

Shutdown()
        ↓
Stop threads
        ↓
Remove subclasses
        ↓
Destroy resources
        ↓
DestroyPluginUI()

Recommended Structure

A typical UI plugin contains:

PluginUI/
 ├── PluginUI.cpp
 ├── PluginUI.h
 ├── Renderer.cpp
 ├── Renderer.h
 ├── Controls.cpp
 ├── Controls.h
 ├── SharedData.h
 └── settings.xml (shared)

Separating rendering, controls, and shared communication logic is strongly recommended for maintainability.