跨平台UI框架的构建与思考

在当今的软件开发领域,跨平台是一个热门话题。随着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); }

Setter和Getter的实现

这部分展示了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); }

OnButtonClick调用

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(); } };

在Emscripten中构建

这部分是为那些希望在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
沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485