How to Write a Simple Native GUI Application on Haiku
Build a warning-clean Haiku GUI with BApplication, BWindow, layout-managed controls and BMessage handling, then test its real lifecycle.
A native Haiku GUI is organized around loopers and messages. One BApplication object represents the running application, each BWindow owns a window thread/message loop, controls are handlers in that hierarchy, and BMessage objects carry actions to targets. The minimal example should demonstrate that lifecycle correctly, not merely create a rectangle that happens to appear.
Prepare and verify the native toolchain
Develop on a current Haiku installation or VM so the headers, runtime, and behavior being tested match the target. Install the development headers through package management rather than copying headers into the system:
pkgman search -a haiku_devel
pkgman install haiku_devel
which c++
Create a clean project directory containing source, resources, and a separate build directory. Record architecture and compiler version. The example links libbe, which supplies the main Application and Interface Kit classes.
Use one complete, compilable example
Create main.cpp:
#include <Application.h>
#include <Button.h>
#include <LayoutBuilder.h>
#include <Message.h>
#include <Rect.h>
#include <StringView.h>
#include <Window.h>
enum {
kSayHello = 'helo'
};
class MainWindow : public BWindow {
public:
MainWindow()
: BWindow(BRect(100, 100, 480, 260), "Native Hello",
B_TITLED_WINDOW,
B_AUTO_UPDATE_SIZE_LIMITS | B_QUIT_ON_WINDOW_CLOSE)
{
fStatus = new BStringView("status", "Ready");
BButton* button = new BButton("hello", "Say hello",
new BMessage(kSayHello));
button->SetTarget(this);
BLayoutBuilder::Group<>(this, B_VERTICAL, 8.0f)
.SetInsets(12.0f)
.Add(fStatus)
.Add(button);
}
void MessageReceived(BMessage* message) override
{
switch (message->what) {
case kSayHello:
fStatus->SetText("Hello from Haiku");
break;
default:
BWindow::MessageReceived(message);
break;
}
}
private:
BStringView* fStatus;
};
class HelloApplication : public BApplication {
public:
HelloApplication()
: BApplication("application/x-vnd.example-NativeHello")
{
}
void ReadyToRun() override
{
MainWindow* window = new MainWindow();
window->Show();
}
};
int main()
{
HelloApplication app;
app.Run();
return 0;
}
Compile it with warnings and debug information:
mkdir -p build
c++ -g -Wall -Wextra -std=c++17 main.cpp -o build/NativeHello -lbe
./build/NativeHello
Do not ignore warnings just because the program launches. Keep the exact command in a build file once the experiment grows beyond one source file.
Understand the application lifecycle
BApplication must be constructed before windows and before Run(). Its signature is a MIME-style reverse-domain identifier; use a stable, unique vendor/application value because Haiku uses signatures to identify and route to applications. It is not a display name and should not be copied from another program.
Subclassing BApplication is conventional when the program needs lifecycle hooks, but the requirement is exactly one BApplication object—not that every program must override every method. ReadyToRun() is a clean place to create the first window after the application is ready to receive messages.
Run() starts the application message loop and normally returns only when the application quits. Avoid doing long blocking initialization on that looper; move lengthy work to a worker thread and report completion through messages.
Keep window ownership and threading correct
Windows start hidden. Calling Show() starts the window’s looper and makes it visible. Keeping Show() outside the constructor means the object is fully initialized before another thread can dispatch messages to it.
B_QUIT_ON_WINDOW_CLOSE asks the application to quit when this window closes, which is appropriate for this single-window sample. A multi-window application may instead override QuitRequested() or track document windows so closing one does not exit all of them.
The window and its child views are owned by the window hierarchy. Do not manually delete fStatus or the button after adding them. Do not directly modify views from an unrelated worker thread without locking the window; send a BMessage to the appropriate handler instead.
Use layouts instead of frozen coordinates
The window frame sets an initial position and size, while BLayoutBuilder installs a vertical group layout. Insets and spacing keep controls separate, and B_AUTO_UPDATE_SIZE_LIMITS lets the window update constraints from its layout.
Layout-managed interfaces adapt better to fonts, translated labels, theme metrics, and content changes than hard-coded child rectangles. Test longer strings and larger font settings. A control fitting the English label at one DPI does not prove a usable layout.
For a production interface, use Haiku’s standard spacing/inset constants and nested group/grid layouts consistently with nearby system applications. This numeric sample keeps the first build self-contained, not a style policy.
Route control actions through BMessage
The button owns the message passed to its constructor. SetTarget(this) routes it to the window handler. When clicked, the window’s looper invokes MessageReceived() on its own thread.
Four-character what codes are an established Haiku convention for local actions. Choose values unique within the receiving context, define them once, and add typed fields when an action needs data. For communication that crosses application or version boundaries, document field names/types and validate every incoming value.
The default branch is essential. Passing unknown messages to BWindow::MessageReceived() preserves inherited handling. Swallowing every unrecognized message can break framework behavior and future extensions.
Test the complete user path
Launch from Terminal and Tracker. Confirm one team appears, the window activates, the button changes the label, keyboard focus reaches the control, and closing the window removes the team. Repeat across workspaces and after changing system font size.
Use Debugger if a crash occurs and keep the symbolized executable. Exercise rapid clicking, close during an action, repeated launch/quit, low-resource errors where practical, and any worker cancellation. Watch for a process remaining after the last window disappears.
Also test application signature collisions by ensuring no copied sample uses the same signature. Add file handling only after defining ownership and error behavior; a successful open dialog does not prove data was saved atomically.
Add resources and packaging deliberately
A distributable application needs a vector icon, version information, signature resources, localization, license, and a HaikuPorts recipe or other supported package workflow. Compile .rdef resources with rc, attach them with xres, and inspect the final executable with listres rather than assuming the resource step succeeded.
Keep settings under the user settings hierarchy and cache under the cache hierarchy. Never write runtime data into package-managed application directories. Package installation/removal must leave user documents intact and remove only package-owned files.
This small architecture scales because responsibilities remain explicit: the application owns global lifecycle, each window owns UI handlers and thread affinity, layouts own geometry, and messages own asynchronous intent. More complex native programs add models, workers, documents, and services without abandoning those boundaries.
Related:
- How Haiku’s Interface Kit and app_server Render Native Windows
- How to Set Up a Haiku Development Environment and Build From Source
Sources: