使用 HTML5 构建 C++ 应用程序






4.88/5 (44投票s)
使用 HTML5 构建 C++ 应用程序
精美的图片
作为独立的 Qt/Webkit 应用程序运行的示例
在 FireFox 中作为 Apache 模块运行的相同应用程序
下载次数
引言
HTML5 是创建丰富、平台无关的用户界面的绝佳方式。HTML5 为 Web 开发人员提供的自由度和灵活性是大多数原生 UI 工具所无法比拟的。使用 HTML5 可以相对快速轻松地构建的 UI 界面,对于其他 C++ GUI 工具来说,可能是一项漫长且效率低下的体验。
在本文中,我将尝试帮助弥合 HTML5 和 C++ 之间的差距,以便我们可以使用 HTML5 来创建独立 C++ 应用程序的用户界面部分。为了实现这一目标,我将使用两个关键元素:WebKit 和 Qt。
WebKit
一个基于 KHTML 分叉的浏览器引擎,它非常稳定,易于编译和使用。它是 Chrome、Safari、Android 和 iPhone 浏览器的引擎。
Qt
由 Trolltech 开发、最近被诺基亚收购的跨平台原生应用程序框架。它非常可靠、完整且免费使用。
Qt 拥有可能是最易于使用的 WebKit 集成,而我们围绕它的框架将非常简单。事实上,我希望在研究完之后,您会觉得可以将框架从 Qt+WebKit 迁移到 wxWidgets+Mozilla 或 MFC+IEActiveX,而不会遇到太多麻烦。当然,只有当您尝试嵌入 Mozilla 时,您才会这么想;)
动机
您可能希望使用 HTML5 进行 UI 开发的一些原因
- Web 工具可能是目前可用的最快、成本效益最高且最易于理解的 UI 创建方法。有大量的免费库、示例代码和支持。
- 重用网站资源、设计(和设计师)、HTML/JavaScript 代码、布局、图像等。
- 基于 Web 的 UI 具有高度的平台无关性。此应用程序还可以构建为 Apache 模块或 IIS CGI 模块。我计划很快添加一个 Android 示例。
- 嵌入式 Web 服务器可用于允许远程访问服务器,或为无法直接访问用户桌面的服务提供接口。
- 也许您的应用程序需要与网站或在线数据库紧密耦合。
- 这不像您想的那么遥不可及,像 Netflix 这样的其他公司也在使用这种 UI 开发方法。
如果您在网上搜索基于 WebKit 的应用程序的示例和教程,您会找到很多。然而,大多数会演示直接从 C++ 执行 JavaScript 函数以及大量调用引擎本身的技巧。
在开发了许多使用嵌入式 Web 引擎的应用程序之后,我坚信这不是正确的方法。随着引擎内部越来越暴露给上层应用程序逻辑,它最终会成为可扩展性的障碍。C++ 代码将专门针对浏览器引擎的实现,HTML/JavaScript 代码也是如此。使用 Web 引擎的许多潜在好处将很快丢失。
要获得全部好处,我们希望能够按原样使用 C++ 代码和我们找到的库。我们也希望能够按原样使用 HTML/JavaScript 代码和我们找到的库,例如 jQuery 或 Prototype。因此,我们需要在我们的 HTML/JavaScript/C++ 和浏览器引擎之间建立清晰的分界。这还将是与其他平台(非 Webkit/Qt)保持兼容性的关键。
事实证明,事情已经按照这种方式设计了,我们所要做的就是确保在集成时不违反规则。我们将只使用普通的页面请求与 UI 进行 C++ 接口。这将有助于避免许多常见陷阱,我们不必深入研究 WebKit 引擎。
目标
我将描述一种方法,以高效且可扩展的方式将嵌入式 Web 浏览器(特别是 WebKit)与 C++ 代码集成。我什邡将更进一步,提供一个基础且灵活的代码库供您入门。我将假设您具有 Web 开发和 C++ 的一些经验。我不会详细介绍如何使用 C++ 或 HTML5 - 我将专注于集成这两者。
所以,让我们设定一些目标。
- 创建一个跨平台的 HTML5 应用程序开发框架,利用 Qt 和 WebKit。
- 由于我们希望跨平台,请确保所有内容都能在 GCC 和 Cygwin 中构建。但别担心,Visual Studio 也能正常工作。
- 提供一个示例,使 HTML/JavaScript 和 C++ 尽可能保持独立和纯粹。
驯服 C++
为了简化 C++ 接口,我借鉴了脚本语言的经验,将 C++ 代码直接嵌入到 HTML 文档中。这为 HTML/JavaScript 代码提供了原生表达,并方便 C++ 输出数据。只要嵌入的 C++ 代码保持简洁,调试就很容易,并且优点会大于缺点。我们将能够编写如下代码:
<html>
<?global
int add( int a, int b )
{
return a + b;
}
?>
<body>
5 + 7 = <?c out << add( 5, 7 ) ?>
<ul>
<?c
for ( int i = 0; i < 10; i++ )
{
?>
<li><?c out << i ?></li>
<?c
}
?>
</body>
</html>
这意味着没有 C++ 知识的 Web 开发人员和设计人员将能够很大程度上理解这些文档,并使用他们自己偏好的方法和工具来修改 JavaScript 代码、应用主题和调整 CSS 值,即使它们在编译之前不会完全起作用。
我在文档中引入了两个特殊标签:<?global ?> 和 <?c ?>。
<?global ?>
出现在 <?global ?> 标签中的任何内容都将被插入到最终 CPP 文件(全局空间)的顶部。此标签可以包含 #include 语句和函数定义,这些语句和函数将作用于 CPP 文件。<?global ?> 标签可以出现在任何地方,它们都会被提取并按出现的顺序插入到生成的 CPP 文档的顶部。
<?c ?>
<?c ?> 标签的内容将按出现的顺序放入一个内部函数中。每次要渲染页面时都会调用该函数。在我实现的这个内部函数的原型如下:
int _internal_run( TPropertyBag< str::t_string8 > &in, TPropertyBag< str::t_string8 > &out );
当然,您可以根据自己的喜好更改它。在我的实现中,两个 TPropertyBag 参数提供了所有数据输入。**in** 变量包含 GET、POST 和其他请求数据。**out** 变量定义了返回给客户端的响应。在大多数情况下,它的行为类似于 std::basic_string<>。事实上,它实际上是该字符串对象的包装器,但也允许以任何方式进行多维响应。最终它会变成一个纯粹的 C++ 文件,您可以在这里做任何您在其他 C++ 文件中可以做的事情。
为了让这些文档栩栩如生,我们必须将代码“内向外”处理,以便可以使用标准的 C++ 编译器来构建它。我通过创建一个预编译器来实现这一点,该预编译器在 C++ 编译器之前处理代码。事实证明,预编译器很容易编写,并且预编译器的额外复杂性通过使 C++/Web 接口整洁无缝而得到了回报。
这样的代码
<html>
<?global
#include <string>
?>
<b> <?c out << std::basic_string< char >( "Hello" ) + " World" ?> </b>
</html>
被转换成这样并编译
#include <string>
static int _internal_run( TPropertyBag< str::t_string8 > &in,
TPropertyBag< str::t_string8 > &out )
{
out << "<html>\n\n";
out << "\n\t<b>\n\t\t";
out << std::basic_string< char >( "Hello" ) + " World" ;
out << "\n\t</b>\n</html>";
return 0;
}
稍后调用该函数时,它会将此写入 **out** 变量
<html>
<b> Hello World </b>
</html>
然后我们将此输出传递给 WebKit 进行处理。HTML/JavaScript 将像处理普通 HTML 一样链接到页面,并对嵌入的 C++ 代码一无所知。如果您已经是 PHP 开发人员,您应该对此感到宾至如归,除了关于框架如何知道如何将文件名映射到函数指针的奇怪感觉。您问得好……
所有编译函数的链接都存储在一个 C++ 数组中,并由文件名引用。然后,我们可以根据 WebKit 引擎收到的文件名请求轻松查找要调用的适当函数。我甚至将站点固定在主机名 embedded 下,这样我们仍然可以允许 WebKit 引擎在选择允许的情况下透明地访问 Internet 数据。
另外,预编译器还可以以跨平台的方式将我们的其他资源(如图像、JavaScript 以及我们想要的任何其他文件)嵌入到应用程序中。所以,我们最终得到一个包含我们整个用户界面的单一 exe 或 dll!
有一点需要注意。当出现 C++ 错误时,编译器将引用预编译的文件而不是原始源文件。虽然对您来说应该仍然很清楚发生了什么,但出于这个原因,您可能希望将大部分繁重的工作保留在标准的 C++ 文件中,而仅将嵌入用于接口。
Qt
至于 Qt 的工作,那将是最小的。正如我之前提到的,我们不需要深入了解 WebKit 的细节。我可以轻松地概述集成的主要部分。
创建主窗口
禁用滚动条和上下文菜单等功能(如果愿意,也可以不禁用),使其更具“应用程序感”。
CMainWindow::CMainWindow()
{
// Create web view
m_pView = new QWebView( this );
// Create custom network manager
m_pNet = new CNetworkMgr( this, m_pView->page()
->networkAccessManager() );
// Set our custom network manager
m_pView->page()->setNetworkAccessManager( m_pNet );
// No scrollbars
m_pView->page()
->mainFrame()
->setScrollBarPolicy( Qt::Vertical,Qt::ScrollBarAlwaysOff );
m_pView->page()
->mainFrame()
->setScrollBarPolicy( Qt::Horizontal,Qt::ScrollBarAlwaysOff );
// No context menu
m_pView->setContextMenuPolicy( Qt::PreventContextMenu );
// Enable cross scripting
m_pView->settings()
->setAttribute( QWebSettings::XSSAuditingEnabled, 0 );
m_pView->settings()
->setAttribute( QWebSettings::LocalContentCanAccessRemoteUrls,
1 );
// Load the home page
m_pView->load( QUrl( "http://embedded/index.htm" ) );
}
重载 QNetworkMgr 和 QNetworkReply 来代理页面请求
在这里,我们将拦截页面请求并重定向到我们嵌入的资源和 C++ 嵌入式代码。
QNetworkReply* CNetworkMgr::createRequest( QNetworkAccessManager::Operation op,
const QNetworkRequest &req, QIODevice *device )
{
// Anything with embedded as the host referes to our embedded resources
if ( req.url().host() == "embedded" )
return new CNetworkReply( this, req, op );
// Disable to disallow real network access
return QNetworkAccessManager::createRequest( op, req, device );
}
CNetworkReply::CNetworkReply( QObject *parent,
const QNetworkRequest &req,
const QNetworkAccessManager::Operation op )
: QNetworkReply( parent )
{
// Get the path to the file
QByteArray path = req.url().path().toUtf8();
// See if there is such a resource
HMRES hRes = res.FindResource( 0, full.c_str() );
str::t_string mime = res.GetMimeType( hRes );
// If it's a binary embedded resource
if ( isBinaryResource( hRes ) )
{
const void *ptr = res.Ptr( hRes );
unsigned long sz = res.Size( hRes );
m_content.append( QByteArray::fromRawData( (const char* )ptr, sz ) );
} // end if
// Page with embedded C++, so we must call a function
else
{
mime = "text/html";
// Get function pointer
CHmResources::t_fn pFn = res.Fn( hRes );
if ( pFn )
{
// in / out
TPropertyBag< str::t_string8 > in;
TPropertyBag< str::t_string8 > out;
// Execute the page
pFn( in, out );
// Set the output
m_content.append( out.data(), out.length() );
} // end if
} // end else
}
这就是大致的想法——其余的是实现细节。您可以看到框架的侵入性很小,主要是 **in** 和 **out** TPropertyBag 变量。这本可以被 STDOUT 捕获(更像标准的 CGI)所取代。在此实现中,我选择不这样做,因为我喜欢使用 STDOUT 进行调试。
我为 bash/gcc 提供了 make 文件,这些文件应该可以在 Linux 或 Windows(使用 Cygwin)上直接使用。有关在 Windows 上安装 Cygwin 的说明,请参阅 htmapp wiki。
总结
我提供了一个示例应用程序,希望您认为它看起来不错,这是另一个磁盘分析器。我能够利用 Quicksand 和 d3 等开源包,在更少的时间内完成。
为了使开发更容易,您可以在开发过程中考虑将应用程序构建为 CGI 或 Apache 模块,这样您就可以使用标准的浏览器工具来调试“实时”构建,尽管缺乏持久性可能仍然使其略有不完整。例如,在我提供的示例中,后台线程没有作为 CGI 模块运行。使用嵌入式 Web 服务器运行应用程序将解决此问题。我希望很快添加此功能。
如果您想进一步了解,我还有另一个库,它具有嵌入式 Web 服务器,并支持 Visual Studio 和 Android,以及许多其他功能。如果您有兴趣,可以 在此处 查看。
感谢阅读,希望这篇文章能为您带来一些启发。
鸣谢
我非常感谢以下项目