使用c++和XAML开发UWP程序

Windows 桌面程序开发一些方案

开发 Windows GUI 程序的方案有很多,接触过比较流行的大概有三种,一种是 C++和 Qt,一种是 HTML5+浏览器内核,最后一种是 C#+WPF。另外古老的 WinForms 和更古老的 MFC 也不多说了。

Qt 的跨平台特性得以开发的项目可以跨平台,而且各种 C++的组件非常丰富。但是但是 Qt 本身库的并不小,我也不是很喜欢 QML 那种 JSON 的书写方式,而且 Qt Creator 用起来也不太顺手,所以一般没怎么用过这种开发模式进行桌面应用的开发。

H5 和浏览器内核是一个不错的方式,可以轻松跨平台,而且 H5+JS 可以有很快的开发速度。主流的方案有 Electron、nwjs、cef 和 wke(其中弹幕派所用的方案就是 wke),但是 Electron 同样体积巨大,不利于应用的分发。wke 虽小但也很久没有更新,内核很老,bug 比较多。最近志鹏同学正在研究之前 wke 开发者新开发的 miniblink 内核,相信这个方案会比较优秀。

WPF 必须依赖于.NetFramework,所以无法跨平台,而且 XP 也不自带.Net,需要用户安装。另外,XAML 虽然写起来麻烦一些,但是开发漂亮的 GUI 还是比较方便。而最新的 UWP 技术也利用了 WPF 的 XAML 进行 UI 的设计,转型起来并不困难。

起因

上周受到卤蛋同学的启发,如果需要使用 C++开发 GUI 程序难道只能用 Qt(或者北邮老师专用的 ege)?能不能使用熟悉的就技术来开发呢?于是在 Windows Dev Center 里找到了使用 C++和 XAML 开发 UWP 程序的方式。粗略看了一下,大概是使用经过微软扩展的 C++,名字叫 c++/cx,基于 Windows RT,但是不受.Net 的托管,也就是要自己处理垃圾回收(这方面理解不够深入,感觉大概是这个意思吧)。

但不管如何,可以使用 C++和 XAML 开发 Windows 应用还是让人眼前一亮,忍不住去尝试一下。所以,下面开工吧!

环境配置

开发环境必须使用 Visual Studio 2017,在新建项目中选择 Visua C++语言的 Windows 通用选项,建立一个空白应用即可。如果没有下载 SDK,上方会提示下载,如果没有显示该选项,说明需要在 VS 安装程序中安装一下 VS 对 C++ UWP 的支持。

在几秒种后,Visual Studio 就会打开一个新建的应用。假设你对 WPF 很熟悉,你可以看见熟悉的 XAML 设计器,在左侧的解决方案管理器中,也可以看见 C++的文件以代码隐藏文件的形式被嵌在 xaml 文件之后,包含一个.h 文件和一个.cpp 文件。整个工程的组织形式和 WPF 极其相似,这足以让人很激动了。

开始 Coding

进一步熟悉一下项目组织形式,可以发现,除了 C++的语法和 C#有区别,开发思路和 WPF 中的 C#是基本一致的,于是对应卤蛋的大作业题目,我建立了一些类的头文件和相关实现,需要注意的是字符串的处理问题,如果要支持中文需要使用 wstring(其实这个问题也并不仅仅是这种时候才会遇到),但关键在于 XAML 中的字符串使用了另外一种并不是 C++标准库中的字符串类型 Platform::String^,所以需要一些函数来在这些字符串中进行转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Platform::String^ Runtime::stops(std::string s)
{
return ref new Platform::String(stows(s).c_str());
}
std::wstring Runtime::stows(std::string s)
{
std::wstring ws;
ws.assign(s.begin(), s.end());
return ws;
}
std::string Runtime::pstos(Platform::String^ ps)
{
return wstos(std::wstring(ps->Data()));
}
std::string Runtime::wstos(std::wstring ws)
{
std::string s;
s.assign(ws.begin(), ws.end());
return s;
}
std::wstring Runtime::pstows(Platform::String^ ps) {
return std::wstring(ps->Data());
}
Platform::String^ Runtime::wstops(std::wstring ws) {
return ref new Platform::String(ws.c_str());
}

具体转换关系可以参考下图,图片来自http://www.cnblogs.com/nio-nio/p/3511843.html

在完成对底层数据对象的抽象以后,就可以着手设计界面和应用逻辑了。UI 的设计因为使用了 XAML,所以非常简单,所有的 XAML 特性都可以使用。而 UI 的事件绑定也和 C#非常类似,如果让 Vs 自动生成事件响应程序的话,它会在代码隐藏文件的头文件和实现文件中分别生成函数的声明和函数体,然后就可以直接在函数中写代码来控制它了。

简单的尝试

页面导航

首先说页面的导航,这里和 UWP 的概念非常吻合,只需要一行代码就可以实现。比如我现在需要导航到 UserPage.xaml,我只需要:

1
Frame->Navigate(UserPage::typeid);

就可以完成,有一点需要提醒的就是必须在.h 文件中引入 UserPage.xaml.h 才可以这样调用。

获取事件触发者

获取事件触发者也很容易,和 C#中思路相当,事件委托的 Handler 中触发者和参数会被以 sender 和 e 的形式传入,只需要做一些类型转换就可以获得到,下面是一个例子:

1
2
3
4
5
6
7
void App1::QuestionListPage::OnPointerPressed(Platform::Object ^sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs ^e)
{
StackPanel^ s = safe_cast<StackPanel^>(sender);
int questionId =_wtoi(QA::Runtime::pstows( s->Name).c_str());
QA::Runtime::currentQuestion = QA::Runtime::questionList[questionId];
Frame->Navigate(QuestionPage::typeid);
}

生成 XAML 元素

不多说,直接贴上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
QuestionListPage::QuestionListPage()
{
InitializeComponent();
vector<QA::Question*>::iterator iter;
for (iter = QA::Runtime::questionList.begin(); iter != QA::Runtime::questionList.end(); iter++) {
auto questionStack = ref new StackPanel();
QA::Question *question = *iter;
auto info = question->getInfo();
auto questionTitle = ref new TextBlock();
questionTitle->Text = L"问题:" + QA::Runtime::wstops(info[0]);
questionTitle->FontSize = +30;
auto userName = ref new TextBlock();
userName->Text = L"用户:" + QA::Runtime::wstops(QA::Runtime::currentUser->getUserName());
questionStack->Children->Append(questionTitle);
questionStack->Children->Append(userName);
questionStack->Name = question->getQuestionId().ToString();
questionStack->PointerPressed += ref new Windows::UI::Xaml::Input::PointerEventHandler(this, &App1::QuestionListPage::OnPointerPressed);
questionList->Items->Append(questionStack);
QA::Runtime::currentQuestion = question;
}

}

以上代码在界面初始化以后动态加载了一个问题列表,其中包含了 Title 和 UserName,并且给 StackPanel 绑定了一个叫做 OnPointerPressed 的事件。(就是之前获取触发者中的代码) 一切都非常简单,不得不说微软已经把 c++/cx 改造得很像 C#了,甚至连委托都被加入了进去。

全局变量

最后想说说的就是应用中全局变量的实现。 本程序采用了静态类的方式实现了全局变量和一些基础方法的封装(比如各种字符串转换函数)。只需要把需要全局的变量设置成 static,并且赋予初始值,在需要的时候调用即可。以下是代码中全局的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//classes.h
class Runtime {
public:
static int userNum;
static int questionNum;
static int answerNum;
static User *currentUser;
static Question *currentQuestion;
static vector<User*> userList;
static vector<Question*> questionList;
};

//classes.cpp
int Runtime::userNum = 0;
int Runtime::questionNum = 0;
int Runtime::answerNum = 0;
Question *Runtime::currentQuestion = nullptr;
User *Runtime::currentUser = nullptr;
vector<User*> Runtime::userList = vector<User*>();
vector<Question_> Runtime::questionList = vector<Question_>();

对于全局方法,用法一样,不再赘述。

可用的资源

后记

说应用跑不起来是假的,自己写的应用,含着泪都要让它跑起来

不过仔细思考一下,如果不是必须要用 C++,这种开发方案作用并不很大。首先资料太少,连 MSDN 上面都只能搜到 API,几乎没有任何示例,而社区中也几乎一丁点都见不到对这种方案的讨论,所以如果遇到坑基本只能自己靠经验解决(但如果太大呢),比如研究事件委托怎么写我就研究了半个晚上,没有 API 也没有文档。另外这种方案开发出的应用也只能跑在 Windows10 中,对于老版本的 Windows 和其他系统(感觉 Xarmain 也不会支持这种模式),所以应用范围也很受限。