Writing Your First App
Load and display HTML in a native OS window.
On this page
This tutorial walks you through the 02-basic-app sample which uses Ultralight and the AppCore module to display HTML in a native desktop window.
1. Build the Sample
First, build and run the sample so you have an idea of the end-result.
👍 Build the samples
If you haven't built the SDK samples yet, see Trying the Samples.
Then, open the source code for the 02-basic-app sample from the SDK's samples folder in your favorite text editor or IDE so you can follow along.
2. Program Structure
The sample defines a single class, MyApp, to manage the application and its window.
#include <AppCore/App.h>
#include <AppCore/Window.h>
using namespace ultralight;
class MyApp : public WindowListener {
RefPtr<App> app_;
RefPtr<Window> window_;
RefPtr<Panel> panel_;
public:
MyApp() {
// The code from the next steps goes here.
}
void Run() { app_->Run(); }
};
MyApp inherits from WindowListener and stores the App, Window, and Panel in RefPtr smart pointers. The constructor configures the window and loads the page, while Run() starts the event loop.
3. Create the App and Window
At the beginning, notice that the sample code calls App::Create().
app_ = App::Create();
// 900 by 600 logical pixels (the monitor's DPI sets the real size).
window_ = Window::Create(app_->main_monitor(), 900, 600, false,
WindowFlags::Titled | WindowFlags::Hidden);
window_->SetTitle("Ultralight Sample 2 - Basic App");
#include <AppCore/CAPI.h>
static ULApp app = NULL;
static ULWindow window = NULL;
void CreateAppAndWindow(void) {
///
/// Create our main App instance (it manages the lifetime of the
/// application and is required to create any windows). NULL settings
/// and config mean the defaults.
///
app = ulCreateApp(NULL, NULL);
///
/// Create our Window. The size (900 by 600) is in DPI-independent
/// logical pixels-- the actual pixel size follows the monitor's DPI.
///
window = ulCreateWindow(ulAppGetMainMonitor(app), 900, 600, false,
kWindowFlags_Titled | kWindowFlags_Hidden);
///
/// Set the title of our window.
///
ulWindowSetTitle(window, "Ultralight Sample 2 - Basic App");
}
App::Create() sets up the renderer and platform integration. You need this App instance to create windows and run the main event loop.
Window::Create() creates a native OS window on the main monitor, sized in DPI-independent logical pixels. Passing WindowFlags::Hidden keeps the window off screen while the page loads— step 6 displays it once the content is ready.
🚧 Only create one App
You should only create one
Appinstance per application lifetime.
👍 Combining window flags
You can combine window flags with the bitwise OR operator (
|). For example,WindowFlags::Titled | WindowFlags::Resizablecreates a resizable window. To see all available flags and display options, see Windows, Monitors, and DPI.
4. Add a Panel and Load a Page
The sample code calls Window::AddPanel() without arguments to create a panel that fills the entire window.
panel_ = window_->AddPanel(); // fills the window and tracks its size
panel_->view()->LoadURL("file:///page.html");
static ULPanel panel = NULL;
static ULView view = NULL;
void AddPanelAndLoadPage(void) {
///
/// Add a web-content panel that spans the entire window and tracks
/// its size automatically (NULL options and view config mean the defaults).
///
panel = ulWindowAddPanel(window, NULL, NULL);
///
/// Get the panel's View. This is a new reference we own, so we destroy
/// it at teardown (that releases only our reference, never the panel's).
///
view = ulPanelGetView(panel);
///
/// Load a local HTML file into our panel's View. Strings are created
/// for the call and destroyed once it returns.
///
ULString url = ulCreateString("file:///page.html");
ulViewLoadURL(view, url);
ulDestroyString(url);
}
Every window organizes its contents using a layout tree of panels. The window manages the panel's size, paints it, and routes input to it.
Call View::LoadURL() on the panel's underlying View to load the HTML page.
📘 Resolving file URLs
file:///URLs resolve relative toSettings::file_system_path, which defaults to./assets/. This folder sits beside the executable on Windows and Linux, and insideYourApp.app/Contents/Resources/on macOS. Files placed inassets/ship directly with the app.
5. Handle Window Close
To handle window events, register MyApp as the window's listener in the constructor.
// In the constructor:
window_->set_listener(this);
// Inherited from WindowListener:
void OnClose(ultralight::Window* window) override {
app_->Quit();
}
///
/// Exit the application when the window is closed.
///
static void OnClose(void* user_data, ULWindow closed_window) {
ulAppQuit(app);
}
Window::set_listener() registers the listener without taking ownership. When the user closes the window, WindowListener::OnClose() fires and calls App::Quit().
Calling App::Quit() stops the event loop so that App::Run() returns— it does not terminate the process itself.
6. Show the Window
Call Window::ShowWhenReady() at the end of the constructor to reveal the window.
// At the end of the constructor:
window_->ShowWhenReady();
///
/// Show the window once its page is ready (wait at most 0.5 seconds).
///
ulWindowShowWhenReady(window, 0.5);
Because the window was created hidden in step 3, Window::ShowWhenReady() keeps it off-screen until the page loads and settles. This ensures the first frame on screen is the finished page rather than a blank window.
By default, the call waits up to 0.5 seconds for the page to settle— if loading takes longer, the window shows anyway.
7. Run the Application
The main() function instantiates MyApp and starts the event loop.
int main() {
MyApp app;
app.Run();
return 0;
}
int main(void) {
CreateAppAndWindow();
AddPanelAndLoadPage();
///
/// Register our close callback. The last two arguments are the
/// callback's user_data and the function that frees it-- we need neither.
///
ulWindowSetCloseCallback(window, OnClose, NULL, NULL);
///
/// Show the window once its page is ready.
///
ulWindowShowWhenReady(window, 0.5);
///
/// Run the app until the window is closed.
///
ulAppRun(app);
///
/// Destroy what we created, in reverse order: the View, the panel,
/// the window, then the App.
///
ulDestroyView(view);
ulDestroyPanel(panel);
ulDestroyWindow(window);
ulDestroyApp(app);
return 0;
}
App::Run() blocks until App::Quit() is called. On each pass through the loop, the application handles OS events, forwards input to panels, and updates the renderer.
Update Native App Logic
To run custom app logic on every pass through the loop, implement AppListener::OnUpdate() and attach the listener using App::set_listener().
8. Modify the Page
You can now edit assets/page.html to customize the interface.
🚧 Rebuild to see asset changes
The build system copies files from
assets/into the output folder. Editassets/page.htmland rebuild to see changes in the window.
What's Next
From here, Building a Desktop App walks through expanding this window into a full desktop application.