在当今的软件开发领域,跨平台是一个热门话题。随着Web技术的快速发展,越来越多的开发者希望能够将桌面应用程序迁移到Web上。本文将介绍如何使用C++构建一个跨平台的UI框架,该框架能够在MFC和HTML之间进行切换,以实现代码的重用。
构建一个完整的框架是一项艰巨的任务。本文介绍的UI控制包装器抽象并隐藏了MFC和HTML的实现细节,使得开发者可以使用标准的C++代码。尽管底层使用了MFC,但作者努力使实现尽可能接近.NET Winform。原因很简单:在易用性和优雅性方面,MFC与Winform相去甚远。这种方法的用例不是将现有的Win32/MFC代码迁移到这种方法上,而是将一个简单的OpenGL演示迁移到Web上。Emscripten对OpenGL和WebGL的桥接支持非常好,但对OpenGL窗口之外的UI控件没有任何支持。在任何时候,作者都编写了C++ UI代码,并重写了相同的UI逻辑以JavaScript控制OpenGL动画。在开始这项工作之前,开发者需要了解一些注意事项。
这种方法只适用于同时存在于Windows和HTML上的简单控件。不适用于复合控件如CCheckListBox。不适用于复杂的控件如CListCtrl。也不适用于子类化自定义绘制控件。此外,这种方法不适用于单文档界面(SDI)和多文档界面(MDI),因为这些层次结构很难迁移到HTML。作者不认为这是一个问题,因为Winform没有SDI/MDI架构,它在桌面应用程序上做得很好,并且非常受.NET开发者的欢迎。
这种方法不适用于UI布局:MFC有自己的UI设计器,而HTML5依赖于HTML/CSS来布局和样式化其控件。这两种技术在布局UI控件方面非常不同,作者无法调和。为了本文的目的,作者手写了HTML控件。Qt没有这些问题,因为它自己处理UI绘制,而不需要使用等效的HTML控件。对于作者的方法,作者不得不在Win32和HTML之间选择最低的共同点。一个优势是HTML代码下载可能更小,因为作者的UI工作委托给了HTML控件。
在编写对话框头文件之前,必须根据__EMSCRIPTEN__宏的存在包含UI控件的头文件。
#ifdef __EMSCRIPTEN__
#include "JsUI.h"
#else
#include "CppUI.h"
#endif
Dialog类对Emscripten和MFC有不同的构造函数。对于Emscripten版本,它接收HTML控件名称,而MFC版本则接收MFC控件的指针。有一个OnButtonClick来处理按钮点击。这个Dialog类中有三种控件:文本框、复选框和按钮。
class Dialog {
public:
#ifdef __EMSCRIPTEN__
Dialog(const char* TextBoxName, const char* CheckBoxName, const char* ButtonName);
#else
Dialog(CEdit* pTextBox, CButton* pCheckBox, CButton* pButton);
#endif
void OnButtonClick();
public:
WATextBox m_TextBox;
WACheckBox m_CheckBox;
WAButton m_Button;
};
这是Dialog构造函数的实现。m_TextBox的标题设置为"Mike"。为m_Button.Clicked事件分配了一个事件处理程序。注意它与C# Winform 1.0事件的相似性。
#include "Dialog.h"
#ifdef __EMSCRIPTEN__
Dialog::Dialog(const char* TextBoxName, const char* CheckBoxName, const char* ButtonName)
: m_TextBox(TextBoxName), m_CheckBox(CheckBoxName), m_Button(ButtonName) {
#else
Dialog::Dialog(CEdit* pTextBox, CButton* pCheckBox, CButton* pButton)
: m_TextBox(pTextBox), m_CheckBox(pCheckBox), m_Button(pButton) {
#endif
m_TextBox.SetText("Mike");
m_Button.Clicked += EventHandler(&Dialog::OnButtonClick, this);
}
在OnButtonClick()中,根据复选框是否被选中,"Hello"被添加到文本前面。然后显示一个包含文本的消息框。
void Dialog::OnButtonClick() {
std::string text = m_TextBox.GetText();
if (m_CheckBox.GetCheck())
text = std::string("Hello ") + text;
WAMessageBox(text);
}
这部分展示了CppUI.h中复选框的setter和getter的一般实现方式。
bool GetCheck() const {
return (m_pCheckBox->GetCheck() == BST_CHECKED);
}
void SetCheck(bool check) {
int v = check ? BST_CHECKED : BST_UNCHECKED;
m_pCheckBox->SetCheck(check);
}
JsUI.h中复选框的setter和getter是通过调用EM_ASM系列函数实现的,这些函数允许在C++源代码中嵌入JavaScript代码。访问其他类型的控件的想法是相同的。
bool GetCheck() const {
int check = EM_ASM_INT({
var ctrl = document.getElementById(UTF8ToString($0));
return ctrl.checked;
}, m_CheckBoxID.c_str());
return (check > 0);
}
void SetCheck(bool check) {
EM_ASM({
var ctrl = document.getElementById(UTF8ToString($0));
ctrl.checked = $1;
}, m_CheckBoxID.c_str(), check);
}
C++中的Dialog::OnButtonClick()由MFC的OnBnClickedBtnSayHello()调用:
void CTestMFCWebAssemblyDlg::OnBnClickedBtnSayHello() {
m_pDialog->m_Button.CallClickHandler();
}
JavaScript中的Dialog::OnButtonClick()由同名的C函数调用。需要注意的是,C++不会混淆这个名称,所以这个函数被包围在extern "C"中。目前,作者手动连接HTML5按钮来调用这个OnButtonClick(),但作者希望将来能够通过向导或IDE或助手透明地完成。
extern "C" {
void EMSCRIPTEN_KEEPALIVE OnButtonClick() {
g_pDialog->m_Button.CallClickHandler();
}
};
这部分是为那些希望在Windows子系统Linux 2(WSL2)上使用Emscripten构建演示代码的读者准备的。关于如何在Ubuntu WSL2上安装Emscripten,请参考这篇文章。假设源文件和Makefile位于/mnt/c/temp2/文件夹中,这是调用make命令的方式。
(cd /mnt/c/temp2/ && make)
这是删除构建的二进制文件wa_dialog.js的命令:
(cd /mnt/c/temp2/ && make clean)
在Makefile中,请记得根据需要更改SRCFOLDER和OUTPUT,如果文件夹不是/mnt/c/temp2/,输出文件不是wa_dialog.js。
SRCFOLDER=/mnt/c/temp2
...
OUTPUT=/mnt/c/temp2/wa_dialog.js