C语言图形界面编程

C 语言进阶:造一个简单的浏览器

2020-03-26  本文已影响0人  司徒永超

前言

本教程将通过一个简单的仿浏览器界面的程序,向你介绍关于构建图形界面程序的基础知识,掌握这些知识后,你将会对图形界面开发有更加深刻的理解。

你可以提前预览我们要写的程序的最终效果,它的源代码已经上传到了 GitHub码云上,你可以试着下载、编译和运行它。如果你看不懂其中的代码,或不知道它是被如何设计出来的,别担心!接下来的教程会一步一步帮助你理解图形界面程序的开发方式。

需求分析

相较于其它开发库或框架的示例程序而言,我们开发的程序更加复杂一些,因此,我们有必要在开发前花点时间思考这个程序具体需要什么功能,又该怎么实现这些功能。

以 Chrome 浏览器为参考对象,我们需要实现以下功能:

我们的程序的图形界面需要包含以下元素:

设计与实现

由于我们开发的程序的大部分源代码都是用于实现图形界面的,因此,这里主要讲述图形界面上的设计与实现方法。

组件化

在研究过 Chrome 浏览器的界面后,我们可以知道它的界面结构如下:

考虑到每个标签页都有自己的选项卡、导航栏和内容区,我们应该将导航栏和内容区域移入到标签页中,结果如下图所示:

组件化

那么,我们需要开发的组件有如下几个:

按照常规做法,把标签页拆分成导航栏和内容区似乎会更好一些,但考虑到组件之间的通信成本和开发成本,组件化到标签页已经足够,因为标签页除了导航栏外已经没有值得组件化的东西了。选项卡栏亦是如此。

布局

接下来我们再试着找出界面元素的排列规则:

布局

如上图所示,红色边框标记的元素是横向布局,而绿色边框标记的元素则是纵向布局。从中可知,主界面布局方向为纵向,选项卡栏和标签页从上到下排列,选项卡栏高度固定,标签页面高度自动占满剩余空间。标签页内的导航栏和内容区也是如此。导航栏中的元素布局方向为横向,按钮宽度固定,地址栏输入框的宽度自动占满剩余空间。

数据结构

如何存储多个标签页的数据?

正常情况下,标签页数量是比较少的,对读写性能要求很低,因此可以选择用链表来存放。

标签页的数据应该包含什么?

为了便于管理标签页数据、展示页面标题和状态、实现组件间的通信,标签页的数据应该包含编号、标题、状态、标签页和主界面的 UI 元素对象的引用。伪代码如下:

struct Page {
    number id;
    string title;
    boolean loading;
    UIElement tab;
    UIElement frame;
    UIElement browser;
};

选型

在准备开发前,我们可以花一些时间调查和研究开源社区上是否有其他开发者开发的库可供使用,选择合适的开发库有助于提升开发效率和降低开发成本。

图形界面开发库:

LCUI,C 语言编写的图形界面开发库,支持 CSS 样式和 Flex 布局。以它现有的功能足以实现我们的程序的图形界面。

路由管理器:

LCUI Router,LCUI 的官方路由管理器,用于解决 LCUI 应用内多视图的切换和状态管理问题。我们可以用它实现浏览器的路由导航功能。

组件库:

LC Design,专为 LCUI 设计的组件库,提供基础排版样式、布局系统和实用工具 CSS 类,以及图标、按钮、下拉菜单等组件。

开发工具:

开发

准备开发环境

注:这里假设你使用的是 Windows 系统,如果不是,请自行处理环境搭建和编译上的问题

在开发前,你需要在你的计算机上安装以下软件:

之后,打开命令行窗口,输入以下命令安装 lcui-cli 和 lcpkg:

npm install -g @lcui/cli lcpkg

设置 lcpkg,让它安装相关依赖工具:

lcpkg setup

创建项目

使用 lcui 命令创建一个名为 lcui-router-app 的项目:

lcui create lcui-router-app

这个命令会替你完成以下工作:

注意,受限于国内的网络环境,部分资源下载比较慢,建议先使用以下命令添加国内源:

npm config set registry https://registry.npm.taobao.org
set SASS_BINARY_SITE=https://npm.taobao.org/mirrors/node-sass/

进入项目目录,安装 lcui-router 和 lc-design 依赖包:

cd lcui-router-app
lcpkg install github.com/lc-soft/lcui-router github.com/lc-ui/lc-design  --arch x64

修改 CMakeLists.txt.in 文件,添加依赖项:

  ...
  if(WIN32)
      set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /SUBSYSTEM:WINDOWS")
      target_link_libraries(
          app
          AppUI
          AppUIViews
          AppUIComponents
          AppLib
          optimized LCUI
          optimized LCUIMain
+         optimized LCUIRouter
+         optimized LCDesign
          debug "${LCPKG_DIR}/debug/lib/LCUI.lib"
          debug "${LCPKG_DIR}/debug/lib/LCUIMain.lib"
+         debug "${LCPKG_DIR}/debug/lib/LCUIRouter.lib"
+         debug "${LCPKG_DIR}/debug/lib/LCDesign.lib"
      )
  else()
      target_link_libraries(
          app
          AppUI
          AppUIViews
          AppUIComponents
          AppLib
          debug "${LCPKG_DIR}/debug/lib/LCUI"
+         debug "${LCPKG_DIR}/debug/lib/LCUIRouter"
+         debug "${LCPKG_DIR}/debug/lib/LCDesign"
          optimized LCUI
+         optimized LCUIRouter
+         optimized LCDesign
      )
  endif(WIN32)
  ...

修改 app/assets/views/app.xml 文件,引入 LC Design 组件库的 CSS 样式文件:

  <?xml version="1.0" encoding="UTF-8" ?>
  <lcui-app>
+   <resource type="text/css" src="assets/stylesheets/lc-design.css"/>
    <resource type="text/css" src="assets/stylesheets/app.css"/>
    <ui>
      <resource type="text/xml" src="assets/views/home.xml"/>
    </ui>
  </lcui-app>

编辑 src/ui.c 文件,添加 LC Design 组件库的初始化代码:

  #include <LCUI.h>
+ #include <LCDesign.h>
  #include <LCUI/gui/widget.h>
  #include <LCUI/gui/builder.h>
  #include "ui/views/browser.h"
  #include "version.h"
  #include "ui.h"

  int UI_Init(void)
  {
          LCUI_Widget root;
          LCUI_Widget wrapper;
          LCUI_Widget browser;

          LCUI_Init();
+         LCDesign_Init();
          UI_InitComponents();
          UI_InitViews();
          wrapper = LCUIBuilder_LoadFile("assets/views/app.xml");
          if (!wrapper) {
                return -1;
         }
  ...

更新构建配置:

npm run configure

运行这个项目,看看效果:

npm start

配置路由

新建 config 目录,在里面新建一个 LCUI Router 的路由配置文件,命名为 router.js,内容如下:

module.exports = [
  {
    name: 'home',
    path: '/',
    component: 'home'
  },
  {
    name: 'welcome',
    path: '/welcome',
    component: 'welcome'
  },
  {
    name: 'help',
    path: '/help',
    component: 'help'
  },
  {
    path: '/file',
    component: 'file'
  },
  {
    name: 'file',
    path: '/file/*',
    component: 'file'
  },
  {
    path: '*',
    component: 'notfound'
  }
]

配置文件采用 JavaScript 语言来描述是为了便于编写和维护,免去用 C 代码来配置 LCUI Router 的麻烦。该文件内容描述了路径 (Path) 与组件 (Component) 的映射关系,LCUI Router 会在路由变化时渲染与路径匹配的组件。要让它能够在我们的程序中生效,还需要使用以下命令将它转译为 C 代码:

lcui compile router

该命令会在 src/lib 目录中生成 router.hrouter.h 文件,并在 src/ui/components.c 文件中追加 router-linkrouter-view 组件的注册代码。

添加界面组件

lcui-cli 提供了视图 (View) 和组件 (Widget) 两种生成器,视图与组件的区别主要在于复杂度和可复用性,你可以根据实际情况来选择。

首先,我们使用以下命令添加视图:

# 浏览器主界面
lcui generate view browser

# 标签页
lcui generate view frame

# 新标签页的引导页面
lcui generate view home

# 欢迎页面
lcui generate view welcome

# 文件页面
lcui generate view file

# 关于页面
lcui generate view help

# 404 页面
lcui generate view notfound

然后,添加组件:

# 选项卡
lcui generate widget frame-tab

实现主界面

在开始前,我们需要先删除 app/assets/views/browser.xml 文件,因为主界面的内容都是在运行时动态生成的,并不需要借助 xml 文件来描述。

然后,编辑 app/assets/views/app.xml 文件,添加浏览器视图:

  <?xml version="1.0" encoding="UTF-8" ?>
  <lcui-app>
    <resource type="text/css" src="assets/stylesheets/lc-design.css"/>
    <resource type="text/css" src="assets/stylesheets/app.css"/>
    <ui>
-     <resource type="text/xml" src="assets/views/home.xml"/>
+     <browser id="browser" />
    </ui>
  </lcui-app>

编辑 src/ui.c 文件,添加主界面的操作代码,让我们的程序在启动时打开一个新标签页。

  ...
  int UI_Init(void)
  {
          LCUI_Widget root;
          LCUI_Widget wrapper;
+         LCUI_Widget browser;

          LCUI_Init();
          LCDesign_Init();
          UI_InitComponents();
          UI_InitViews();
          wrapper = LCUIBuilder_LoadFile("assets/views/app.xml");
          if (!wrapper) {
                  return -1;
          }
          root = LCUIWidget_GetRoot();
+         browser = LCUIWidget_GetById("browser");
+         BrowserView_Active(browser, BrowserView_Load(browser, "/"));
+         Widget_SetTitleW(root, L"Browser demo");
          Widget_Append(root, wrapper);
          Widget_Unwrap(wrapper);
          return 0;
  }

考虑到大量的代码片段会影响文章的阅读体验,从这里开始,我们将尽量只关注功能的大致实现原理,具体实现代码请查看相关文件。

主界面有以下功能:

为了实现与用户的交互,我们需要添加以下事件处理器:

主界面的功能实现后,我们再设计它的布局和样式:

相关文件:

实现选项卡组件

选项卡组件由图标、文字和关闭按钮组成,在标签页处于加载中状态的时候,我们可以用 LC Design 组件库提供的 Spinner 组件来表达。

该组件提供以下方法:

布局与样式:

相关文件:

实现标签页

LCUI Router 提供了 router-view 组件用于渲染与当前路由匹配的组件,我们可以将它作为标签页的内容区域,这样我们只需要为导航栏添加相应的事件处理器,然后调用 LCUI Router 的接口来实现路由导航功能。

标签页面的功能有:

事件处理器:

样式与布局:

相关文件:

实现新标签页面

新标签页面提供欢迎页、关于页、文件浏览页的快捷入口,效果与 Chrome 浏览器一致:

新标签页

编辑 app/assets/views/home.xml 文件,添加以下内容:

<?xml version="1.0" encoding="UTF-8" ?>
<lcui-app>
  <ui>
    <w class=" container">
      <w class="v-home__cards d-flex justify-content-center align-items-center">
        <router-link class="v-home__card" to="/welcome">
          <icon class="v-home__card-icon text-pink" name="emoticon-cool-outline" />
          <text class="v-home__card-title">Welcome!</text>
        </router-link>
        <router-link class="v-home__card" to="/file">
          <icon class="v-home__card-icon text-orange" name="folder-search" />
          <text class="v-home__card-title">File</text>
        </router-link>
        <router-link class="v-home__card" to="/help">
          <icon class="v-home__card-icon text-blue" name="help-box" />
          <text class="v-home__card-title">About</text>
        </router-link>
      </w>
      </w>
  </ui>
</lcui-app>

编辑 src/ui/views/home.c 文件,用以下内容覆盖:

#include <LCUI.h>
#include <LCUI/gui/widget.h>
#include <LCUI/gui/builder.h>
#include <LCUI/timer.h>
#include "home.h"

static LCUI_WidgetPrototype home_proto;

static void HomeView_OnTimer(void *arg)
{
        LCUI_WidgetEventRec e;

        LCUI_InitWidgetEvent(&e, "PageLoaded");
        e.cancel_bubble = FALSE;
        Widget_TriggerEvent(arg, &e, NULL);
}

static void HomeView_OnInit(LCUI_Widget w)
{
        LCUI_Widget wrapper;

        wrapper = LCUIBuilder_LoadFile("assets/views/home.xml");
        if (wrapper) {
                Widget_Append(w, wrapper);
                Widget_Unwrap(wrapper);
        }
        Widget_AddData(w, home_proto, 0);
        Widget_AddClass(w, "v-home");
        Widget_SetTitleW(w, L"New tab");
        LCUI_SetTimeout(0, HomeView_OnTimer, w);
}

void UI_InitHomeView(void)
{
        home_proto = LCUIWidget_NewPrototype("home", NULL);
        home_proto->init = HomeView_OnInit;
}

注:该页面的代码同样适用于关于页面、欢迎页面、404 页面,在下面的教程中将不再重复说明,你只需要复制粘贴这段代码到对应的文件然后做点修改即可。

新标签页面由 home 组件呈现,与该组件绑定的路径是 /,当导航到该路径时 LCUI Router 会将新标签页面挂载到 router-view 组件内。

为了通知主界面当前页面已经加载完成,我们在 home 组件初始化时创建一个定时器,等下一帧更新开始时触发 PageLoaded 事件。如果你想让选项卡中的 Spinner 组件的转圈动画多转一段时间,可以将定时器的超时时间设长一点。

样式与布局:

相关文件:

实现文件浏览页面

其它示例页面的内容都是静态的,如果我们开发的程序的功能只是在这些页面之间切换的话,未免有些过于简单,为了进一步展示图形界面编程的例子,并充分利用 LCUI Router 的特性,我们有必要开发一个更为复杂的页面,而文件浏览页面恰好是最为合适的选择。

以 Chrome 浏览器的文件浏览页面作为参考对象:

文件浏览

页面标题包含了当前目录的路径,文件列表项由图标、文件名、大小和日期组成,在点击文件名后会跳转到该文件路径,如果是目录,则会渲染该目录的文件列表。

对于文件列表的呈现,我们可以在视图初始化后遍历当前路径下的文件列表,然后获取它们的名称、大小和修改日期。由于文件操作比较耗时且容易阻塞 UI 线程而导致界面卡顿,我们应该将这些操作放在工作线程上执行,等文件列表生成后再转到主线程上渲染。

文件名点击跳转功能可以用 LCUI Router 提供的 router-link 组件来实现。在构造文件列表项时,构造 router-link 组件来呈现文件名,然后将它的 to 属性设为目录路径,这样在点击文件名后 router-link 组件会自动导航到该路径,从而使得位于标签页内容区内的 router-view 组件响应路由更新并重新渲染文件浏览页面。

文件浏览页面的功能有:

布局与样式:

相关文件:

实现关于页面

Chrome 浏览器的关于页面展示了它的名称、版本号、帮助链接、版权声明:

关于页

lcui-cli 为我们创建的项目已经预置了用于展示程序信息的 about 组件,我们只需要在 help.xml 文件中引入它即可,代码如下:

<?xml version="1.0" encoding="UTF-8" ?>
<lcui-app>
  <ui>
    <w class="container">
      <text class="v-help__title">About this app</text>
      <about />
    </w>
  </ui>
</lcui-app>

相关文件:

实现欢迎页面和 404 页面

欢迎页面和 404 页面是纯信息展示页面,与其它页面类似,内容可由你自由发挥,这里就不再详细说明了。

完成

至此,你已经完成了第一个基于 LCUI 的图形界面程序!你已经了解了 LCUI 的基础知识:组件、SCSS 和 XML 语法。你还学习了多个视图如何切换,以及组件如何相互通信。

接下来我们运行以下命令来更新构建配置:

npm run configure

构建并运行程序,体验一下最终效果。

npm start

如果一切正常的话,程序的运行效果会是这样:

LCUI Router App

结语

如果你发现这篇文章有错别字、病句,或者某些内容令人费解,可以在 GitHub 上改进此文章。

上一篇 下一篇

猜你喜欢

热点阅读