Aplicações Desktop Ultraleves com C++ e WebView: Interfaces HTML5 Nativas Sem Electron

Published on: 2026-01-06
Post image
pt c-desktop webview-c aplicativos-desktop-leves c-html-css-javascript alternativa-ao-electron webview2-windows webkit-macos-linux cmake-c vite-frontend-desktop interface-web-em-c aplicacoes-multiplataforma-c ui-nativa-com-webview

Aplicações desktop com interface web costumam ser associadas a empacotadores grandes, que trazem um mecanismo de navegador completo junto do aplicativo. Uma alternativa mais enxuta é usar um WebView, isto é, um componente nativo do sistema operacional que renderiza HTML, CSS e JavaScript dentro de uma janela de aplicativo tradicional.

Com C++ e a biblioteca webview, a interface pode ser construída como uma página web, enquanto a lógica pesada, integrações com o sistema e acesso a arquivos ficam no código nativo. Essa abordagem usa WebView2 no Windows, WebKit no macOS e WebKitGTK no Linux, reduzindo drasticamente o tamanho do binário e mantendo inicialização rápida, especialmente quando comparada a soluções que empacotam um runtime de navegador.

Por que WebView tende a ser menor e mais rápido do que runtimes empacotados

Um runtime empacotado normalmente inclui um navegador inteiro e uma camada adicional de execução, o que aumenta o tamanho do instalador e do executável final. Já um WebView reaproveita o motor de renderização que já existe no sistema operacional, reduzindo o que precisa ser distribuído junto do aplicativo. Em cenários simples, isso pode levar a binários na ordem de dezenas de kilobytes até menos de 1 MB, dependendo de como as dependências são ligadas. A inicialização também tende a ser mais rápida por não precisar levantar um processo separado com runtime completo. O trade-off central é depender do componente web do sistema, com variações entre plataformas.

O que é a biblioteca webview (C++) e quais componentes nativos ela usa

A biblioteca webview é um wrapper pequeno que cria uma janela e hospeda um WebView nativo por plataforma. No Windows, o backend mais comum é o WebView2, baseado no Edge, normalmente instalado como runtime separado no sistema. No macOS, o componente é o WKWebView (WebKit), parte do sistema. No Linux, é comum usar WebKitGTK, integrado via GTK. A página oficial do projeto fica em:

Arquitetura recomendada: UI web como “thread” de interface e C++ como backend nativo

Uma divisão prática é tratar a camada web como responsável por estados visuais, navegação e interação. A camada C++ fica responsável por operações que exigem desempenho, bibliotecas nativas ou acesso ao sistema, como arquivos, processos e integração com serviços locais. A comunicação acontece por uma ponte entre JavaScript e C++, geralmente com chamadas assíncronas e troca de dados em JSON. Essa separação facilita manter o frontend moderno e iterável, sem abrir mão da robustez do backend em C++. Também reduz o acoplamento entre UI e regras de negócio.

Estrutura de projeto com CMake e frontend Vite

Uma estrutura comum separa o código nativo em uma pasta como src e o frontend em ui. O frontend é compilado por um bundler (como o Vite) para gerar arquivos estáticos otimizados. Em seguida, o HTML final pode ser incorporado ao binário como um header C/C++ (um array de bytes ou string), evitando depender de arquivos externos em tempo de execução. Isso simplifica distribuição e ajuda a manter o pacote pequeno. O CMake coordena dependências C++ (webview e JSON), compila o executável e inclui o header gerado pelo build do frontend.

Dependências C++: webview e nlohmann/json

Para hospedar a UI e permitir bindings, a dependência principal é a webview. Para troca de mensagens estruturadas, é comum usar JSON, e a biblioteca nlohmann/json é popular por ser header-only e simples de integrar. O repositório do nlohmann/json fica em:

Com CMake, essas dependências podem ser buscadas por FetchContent, que baixa o código na configuração e o integra ao build. Isso reduz fricção inicial e torna o projeto mais reprodutível. Em ambientes corporativos, também é comum substituir por gerenciadores como vcpkg ou Conan, mas o FetchContent é suficiente para um exemplo completo e minimalista.

CMake completo: buscando dependências e construindo o executável

O arquivo abaixo mostra um CMakeLists.txt funcional que busca a webview e o nlohmann/json diretamente do GitHub e gera um executável. Ele também ajusta diretórios de saída e define a versão da API do WebKitGTK no Linux. A opção WIN32 evita abrir um console no Windows quando se usa WinMain. A inclusão do header da UI é feita adicionando a pasta ui/dist como include directory.

cmake_minimum_required(VERSION 3.16)
project(app LANGUAGES CXX)

set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin")
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib")
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib")
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

# Necessário em algumas distros para escolher a API do WebKitGTK
set(WEBVIEW_WEBKITGTK_API 6.0)

include(FetchContent)

FetchContent_Declare(
  webview
  GIT_REPOSITORY https://github.com/webview/webview
  GIT_TAG 0.12.0
)
FetchContent_MakeAvailable(webview)

FetchContent_Declare(
  json
  GIT_REPOSITORY https://github.com/nlohmann/json
  GIT_TAG v3.12.0
)
FetchContent_MakeAvailable(json)

add_executable(app WIN32)

target_sources(app PRIVATE
  src/main.cpp
  ui/dist/index_html.h
)

target_include_directories(app PRIVATE
  ui/dist
)

target_link_libraries(app PRIVATE
  webview::core
  nlohmann_json::nlohmann_json
)

C++: criando a janela WebView, definindo título e tamanho

O código C++ cria uma instância de webview::webview, define título e tamanho da janela e carrega o HTML embutido. No Windows, o ponto de entrada é WinMain para não abrir console, enquanto em outras plataformas usa-se main. O método set_html injeta o conteúdo HTML diretamente, dispensando servidor local e arquivos externos. O método run inicia o loop de mensagens da janela, mantendo o aplicativo ativo até o fechamento. O bloco try/catch captura exceções específicas da biblioteca.

#include <iostream>
#include <nlohmann/json.hpp>
#include "webview/webview.h"
#include "index_html.h"

using json = nlohmann::json;

#ifdef _WIN32
#include <windows.h>
int WINAPI WinMain(HINSTANCE /*hInst*/, HINSTANCE /*hPrevInst*/,
                   LPSTR /*lpCmdLine*/, int /*nCmdShow*/) {
#else
int main() {
#endif
    try {
        webview::webview main_window(false, nullptr);
        main_window.set_title("Prompt Workbench");
        main_window.set_size(1280, 720, WEBVIEW_HINT_NONE);

        main_window.bind("ping", [&](const std::string& args_str) -> std::string {
            json args = json::parse(args_str);

            std::cout << "Ping from UI: " << args[0] << std::endl;

            json result = {{"code", 200}};
            return result.dump();
        });

        main_window.set_html(INDEX_HTML);
        main_window.run();
    } catch (const webview::exception& e) {
        std::cerr << e.what() << '\n';
        return 1;
    }

    return 0;
}

Bindings: expondo funções C++ ao JavaScript com webview.bind

Um binding é uma função C++ registrada com um nome que passa a existir no JavaScript como uma chamada disponível na página. No exemplo, a função ping recebe uma string JSON com argumentos, faz parse e retorna um JSON com o resultado. Essa estratégia padroniza a comunicação, evitando formatos ad-hoc e facilitando adicionar campos como códigos de status, mensagens e dados. É comum tratar o primeiro argumento como payload principal e evoluir para objetos com múltiplos campos. A função retorna uma string porque a ponte costuma transportar texto, então JSON vira o formato natural.

Troca de dados com JSON: formato de entrada, saída e versionamento simples

JSON é um formato de texto leve para representar objetos e listas, muito usado no JavaScript e fácil de serializar no C++. Ao receber args_str, o código chama json::parse e acessa args[0], assumindo que o JavaScript enviou uma lista de argumentos. No retorno, result.dump() converte o objeto JSON em string para atravessar a ponte. Em projetos maiores, um padrão comum é incluir um campo como type (tipo de mensagem) e payload (conteúdo), facilitando evolução sem quebrar compatibilidade. Também é comum validar campos antes de usar para evitar exceções por formato inesperado.

Frontend com Vite: HTML, JavaScript e CSS para uma UI simples e rápida

O frontend pode ser desenvolvido como uma aplicação web normal, com HTML para estrutura, CSS para estilo e JavaScript para eventos. O Vite atua como ferramenta de desenvolvimento e build, gerando uma versão otimizada para distribuição. No HTML, o script aponta para o módulo principal, que registra eventos e chama o binding exposto pelo C++. No JavaScript, a função window.ping existe por causa do bind no lado C++. No CSS, estilos mínimos tornam o exemplo consistente sem adicionar peso.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>View</title>
  </head>
  <body>
    <h1>Hello!</h1>
    <label for="text-field">String to Send to the C++ Process:</label>
    <input type="text" name="text-field" id="text-field" />
    <button id="send-btn">Send</button>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>
import './style.css';

function main() {
  const campoTexto = document.querySelector("#text-field");
  const botaoEnviar = document.querySelector("#send-btn");

  botaoEnviar.addEventListener("click", () => {
    window.ping(campoTexto.value);
  });
}

window.addEventListener('DOMContentLoaded', main);
:root {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  font-size: 14px;
  font-weight: 400;
}

*, *:before, *:after {
  box-sizing: border-box;
}

html, body {
  padding: 0;
  margin: 0;
}

h1, h2, h3, h4, h5, h6 {
  font-size: 1rem;
  font-weight: 400;
  margin: 0;
}

label, input {
  display: block;
}

package.json: scripts de build e geração do header embutido

Para incorporar a UI no binário, o build do frontend precisa produzir um HTML final e depois convertê-lo em um arquivo header. O postbuild é um script executado após o build, normalmente responsável por transformar dist/index.html em dist/index_html.h. Essa conversão costuma criar uma constante C/C++ com o conteúdo do HTML, escapando caracteres e preservando bytes. O exemplo abaixo organiza scripts de desenvolvimento, build e preview, mantendo o fluxo direto. A dependência de um plugin de singlefile indica a intenção de empacotar tudo em um único HTML, reduzindo arquivos e simplificando embed.

{
  "name": "ui",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "postbuild": "node ./postbuild.js ./dist/index.html ./dist/index_html.h",
    "preview": "vite preview"
  },
  "devDependencies": {
    "vite": "npm:rolldown-vite@7.1.14",
    "vite-plugin-singlefile": "^2.3.0"
  },
  "overrides": {
    "vite": "npm:rolldown-vite@7.1.14"
  }
}

Incorporação do HTML no binário: o papel do index_html.h

O arquivo index_html.h normalmente contém uma constante como INDEX_HTML com o conteúdo do HTML final. Ao chamar set_html(INDEX_HTML), o aplicativo carrega a interface diretamente da memória, evitando caminhos relativos, problemas de instalação e dependência de diretórios. Essa técnica também favorece a portabilidade do executável, já que a UI viaja junto do binário. Em builds de produção, o HTML final costuma incluir CSS e JS embutidos para reduzir chamadas e simplificar carregamento. Quando a UI cresce, ainda é possível embutir múltiplos recursos, mas um HTML único costuma ser o formato mais simples para começar.

Desempenho: tempo de inicialização, uso de memória e custo de renderização

O custo inicial de um app com WebView depende principalmente do carregamento do componente nativo e do conteúdo da página. Como não há um runtime web completo empacotado, a inicialização tende a ser mais rápida e o binário, menor. O uso de memória é influenciado pela complexidade da página e pelas bibliotecas JavaScript escolhidas, além do comportamento do WebView do sistema. A renderização segue o modelo do motor nativo, com aceleração por GPU quando disponível. Em comparação com frameworks imediatos de UI nativa, o perfil de custo pode variar, mas o ganho de produtividade do ecossistema web frequentemente compensa em interfaces ricas.

Quando usar WebView em vez de Dear ImGui ou Egui

Dear ImGui e Egui são bibliotecas de UI “imediata”, focadas em ferramentas, depuração e interfaces funcionais com baixo esforço de layout tradicional. Uma UI web com WebView favorece design mais próximo de produtos finais, com tipografia, layout responsivo e componentes modernos. Em interfaces que exigem alto nível de personalização visual, temas, animações e reutilização de bibliotecas web, WebView costuma ser mais adequado. Em contrapartida, ImGui/Egui podem ser melhores quando a prioridade é simplicidade, baixa dependência do ambiente web e controle total do renderizador. A decisão também depende da necessidade de empacotamento mínimo versus previsibilidade de UI em todas as máquinas.

Boas práticas para manter o pacote pequeno

O tamanho final depende do binário C++ e do que a UI embute, então reduzir dependências no frontend tem impacto direto. Evitar frameworks pesados quando não são necessários e preferir CSS e JS simples ajuda a manter o HTML final compacto. No C++, compilar com otimizações de tamanho e remover símbolos em builds de release reduz o executável, embora as flags exatas variem por compilador. Incorporar a UI como um único arquivo diminui complexidade e reduz erros de empacotamento. Também é importante evitar incluir recursos grandes (fontes, imagens) sem necessidade, já que eles entram no binário quando embutidos.

Cenários comuns e cuidados multiplataforma

No Windows, o WebView2 depende do runtime, que pode já estar presente ou precisar ser instalado, e isso influencia a estratégia de distribuição. No Linux, a disponibilidade e a versão do WebKitGTK variam entre distribuições, exigindo atenção a pacotes do sistema e à configuração de API definida no CMake. No macOS, o WebKit tende a ser mais uniforme por vir com o sistema, mas permissões e sandboxing podem influenciar acesso a arquivos e recursos. O comportamento de certas APIs web também pode variar entre motores, então é útil manter a UI compatível com padrões amplamente suportados. A ponte JS/C++ deve tratar erros e entradas inválidas para evitar travamentos por parsing ou chamadas inesperadas.

Encerramento: um desktop moderno com binário pequeno e integração nativa

O uso de C++ com a biblioteca webview permite criar aplicações desktop com UI em HTML5/CSS3/JS reaproveitando o motor de navegador do próprio sistema operacional. Essa decisão reduz significativamente o tamanho do pacote quando comparada a soluções que distribuem um navegador inteiro junto do aplicativo. Com CMake, o projeto pode manter dependências claras, build reproduzível e integração simples do frontend empacotado via Vite. A comunicação por bindings e JSON organiza responsabilidades e facilita crescimento do aplicativo mantendo a interface ágil e o backend nativo eficiente. O resultado é um modelo de aplicação com início rápido, boa ergonomia de desenvolvimento e distribuição enxuta.

Baixe o exemplo completo neste repositório: