Skip to content

NativeControl

Header: winui/ui/hosting/NativeControl.hpp. Spec: winui/ui/hosting/NativeControl.hpp.

An Element that hosts a real platform child window (an HWND on Windows) of any window class, the escape hatch for OS controls the framework does not draw itself.

The window is created lazily on the first sync_native(), moved whenever the arranged bounds change, clipped to what scrolling ancestors leave in view, hidden when it stops being shown, and destroyed with the element. It paints itself, so the theme doesn't apply to it, sits above everything the framework draws, and is outside the Tab-focus ring. A derived class answers the window's parent notifications in handle_native_notification() and configures it once in on_native_created(). The NativeHost it was last synced with must outlive it. Windows only: without a native host (a headless window, another platform) the element keeps its size and draws nothing.

Example:

// Any window class, by name, answering its WM_NOTIFY (Windows only).
class MonthCalendar final : public ui::NativeControl
{
public:
    MonthCalendar()
        : ui::NativeControl(ui::NativeControlDesc{.class_name = MONTHCAL_CLASSA}, ui::Size{230, 160})
    {
    }

    bool handle_native_notification(ui::NativeNotification& n) override
    {
        const auto* header = reinterpret_cast<const NMHDR*>(n.lparam);
        if (n.message != WM_NOTIFY || header->code != MCN_SELECT)
            return false;
        selected = true; // read the date with MCM_GETCURSEL on native_handle()
        return true;
    }

    bool selected = false;

protected:
    void on_native_created(ui::NativeHandle handle) override
    {
        RECT r{};
        MonthCal_GetMinReqRect(handle, &r);
        set_native_size(ui::Size{float(r.right - r.left), float(r.bottom - r.top)});
    }
};

window.set_content(ui::VerticalStack{
    .spacing  = 8,
    .content  = { ui::Text{.text = "Pick a date"}, std::make_unique<MonthCalendar>() },
});

ui::Native

A real Win32 child window hosted in the tree. Builds a ui::NativeControl.

Use .control to reach the built control and, through NativeControl::native_handle(), its HWND. The window is created when the tree is attached to a real window host.

Example:

ui::Control<ui::Native> edit;

window.set_content(ui::Native{
    .desc    = { .class_name = "EDIT", .initial_text = "Hello", .style = 0x0080 /* ES_AUTOHSCROLL */ },
    .native_size = { 200.0f, 24.0f },
    .control = edit,
});

Every field, in the order a designated initializer names them:

ui::Native{
    .desc, .native_size, .style, .margin, .width, .height, .min_width, .min_height, .max_width,
    .max_height, .horizontal_alignment, .vertical_alignment, .visible, .tool_tip,
    .accessibility, .on_pointer_entered, .on_pointer_exited, .on_got_focus, .on_lost_focus,
    .on_key_down, .on_wheel, .control
}

Fields, in declaration order (designators must follow it):

Field Type Default
desc ui::NativeControlDesc The window class, initial text and WS_* / WS_EX_* style bits to create it with.
native_size ui::Binding<ui::Size> The fixed size the control reports (NativeControl::set_native_size()). Native controls take no part in text measurement, so set their size here.
on_pointer_entered ui::Action The pointer moved onto the control.
on_pointer_exited ui::Action The pointer left the control.
on_got_focus ui::Action Keyboard focus arrived on the control, or on anything inside it, from outside it. A control with nothing focusable in it never gets focus, and never raises this.
on_lost_focus ui::Action Keyboard focus left the control and everything inside it.
on_key_down ui::Callback<ui::Handling(const ui::KeyEvent&)> A key pressed while focus is on the control or inside it, before the control's own handling; ui::Handled stops it there (Element::on_key_down()).
on_wheel ui::Callback<ui::Handling(const ui::PointerEvent&)> A wheel turn over the control, before the control scrolls; ui::Handled stops it there (Element::on_wheel()).
control ui::ControlOut<ui::Native> Out: the handle that receives the built control, the way to its HWND.

Plus the common fields style, margin, width, height, min_width, min_height, max_width, max_height, horizontal_alignment, vertical_alignment, visible, tool_tip, accessibility every spec carries (conventions).

build

std::unique_ptr<Element> build()&&

Consumes the spec and returns the built ui::NativeControl; the caller owns it.

ui::NativeControl

Derives from ui::Element.

An Element that hosts a real platform child window (an HWND on Windows) of any window class, the escape hatch for OS controls the framework does not draw itself.

The window is created lazily on the first sync_native(), moved whenever the arranged bounds change, clipped to what scrolling ancestors leave in view, hidden when it stops being shown, and destroyed with the element. It paints itself, so the theme doesn't apply to it, sits above everything the framework draws, and is outside the Tab-focus ring. A derived class answers the window's parent notifications in handle_native_notification() and configures it once in on_native_created(). The NativeHost it was last synced with must outlive it. Windows only: without a native host (a headless window, another platform) the element keeps its size and draws nothing.

Example:

// Any window class, by name, answering its WM_NOTIFY (Windows only).
class MonthCalendar final : public ui::NativeControl
{
public:
    MonthCalendar()
        : ui::NativeControl(ui::NativeControlDesc{.class_name = MONTHCAL_CLASSA}, ui::Size{230, 160})
    {
    }

    bool handle_native_notification(ui::NativeNotification& n) override
    {
        const auto* header = reinterpret_cast<const NMHDR*>(n.lparam);
        if (n.message != WM_NOTIFY || header->code != MCN_SELECT)
            return false;
        selected = true; // read the date with MCM_GETCURSEL on native_handle()
        return true;
    }

    bool selected = false;

protected:
    void on_native_created(ui::NativeHandle handle) override
    {
        RECT r{};
        MonthCal_GetMinReqRect(handle, &r);
        set_native_size(ui::Size{float(r.right - r.left), float(r.bottom - r.top)});
    }
};

window.set_content(ui::VerticalStack{
    .spacing  = 8,
    .content  = { ui::Text{.text = "Pick a date"}, std::make_unique<MonthCalendar>() },
});

NativeControl

NativeControl(NativeControlDesc desc, Size size)

desc describes the window class/style to create; size is the fixed desired size measure() reports. Native controls take no part in text measurement, so a caller sizes them explicitly.

~NativeControl

~NativeControl() override

Destroys the native window through the host it was last synced with, if it was created.

NativeControl

NativeControl(const NativeControl&) = delete

operator=

NativeControl& operator=(const NativeControl&) = delete

sync_native

void sync_native(NativeHost& host) override

Called by Window after layout and input. Creates the native window on the first call and moves it whenever bounds() changed since the last sync.

on_native_content_changed

virtual void on_native_content_changed()

Called when the hosted control reports that its content changed. On Windows that is an EN_CHANGE notification, as a RichEdit sends. The framework can't see such an edit otherwise, since the text lives inside the native control and the keystrokes go straight to the child HWND.

handle_native_notification

virtual bool handle_native_notification(NativeNotification&)

A message the hosted window sent its parent about itself (render/NativeHost.hpp's NativeNotification): WM_COMMAND, WM_NOTIFY, WM_CTLCOLOR* and a trackbar's or scroll bar's WM_HSCROLL/WM_VSCROLL on Windows. Return true, with notification.result set, to answer it; false lets the host's window procedure handle it. This is the parent half of hosting any window class: a month calendar's MCN_SELECT, a date picker's DTN_DATETIMECHANGE or the colors an EDIT asks for, none of which the element could see otherwise. EN_CHANGE still reaches on_native_content_changed() when this declines it.

native_size

Size native_size() const

The fixed size measure() reports. Changing it measures the control again, for a control that learns the size it wants from the native window once it exists (a month calendar's MCM_GETMINREQRECT, read in on_native_created()).

set_native_size

void set_native_size(Size size)

Sets the desired size (DIPs) and marks layout dirty when it changed.

visible_rect

Rect visible_rect() const

The part of the control its scrolling ancestors leave in view, in arranged coordinates: empty when it is Collapsed, under a Collapsed ancestor or scrolled wholly out of view. What sync_native() shows and clips the native window to.

automation_is_native_host

bool automation_is_native_host() const override

True: the framework's automation tree leaves the control out, because the child HWND provides its own accessibility once created.

native_handle

NativeHandle native_handle() const

The raw platform handle (HWND on Windows), for a derived control to send it control-specific messages. nullptr before the first sync_native() call, and after the window that held it was closed.

on_native_created

virtual void on_native_created(NativeHandle)

Called once, from the sync_native() that created the real window, after it has been registered with the host and moved to its first bounds. handle is native_handle()'s value and is never null here. Not called if creation failed.

This is where a derived control sends the one-time messages its window class needs to be configured with. A NativeControl's window is created lazily on the first sync, so the handle does not exist yet in the constructor.

It is also the place to read a size the native control wants (a month calendar's MCM_GETMINREQRECT) and hand it to set_native_size().

measure_override

Size measure_override(Size available, const MeasureContext& context) const override

Always native_size(), whatever space is available.

render_override

void render_override(Renderer& renderer, const RenderContext& context) const override

Emits nothing: the native window paints itself on top of the framework's surface.

Tests

  • tests/test_native_control.cpp