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 activeShutdown()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
| Parameter | Description |
|---|---|
hWindow | Child HWND owned by the host |
context | OpenSR shared context |
outBuf | OUT telemetry buffers |
pluginFolderPath | Plugin folder path |
sharedBuffer | Optional 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_PAINTWM_COMMANDWM_SIZEWM_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:
- User opens UI page
Initialize()called- Plugin loads settings
- User switches to Settings page
- UI plugin is destroyed
- User edits settings
- User returns to UI page
Initialize()called again- 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.
