C 語言進階:造一個簡單的瀏覽器

前言

本教程將經過一個簡單的仿瀏覽器界面的程序,向你介紹關於構建圖形界面程序的基礎知識,掌握這些知識後,你將會對圖形界面開發有更加深入的理解。css

你能夠提早預覽咱們要寫的程序的最終效果,它的源代碼已經上傳到了 GitHub碼雲上,你能夠試着下載、編譯和運行它。若是你看不懂其中的代碼,或不知道它是被如何設計出來的,別擔憂!接下來的教程會一步一步幫助你理解圖形界面程序的開發方式。node

需求分析

相較於其它開發庫或框架的示例程序而言,咱們開發的程序更加複雜一些,所以,咱們有必要在在開發前花點時間思考這個程序具體須要什麼功能,又該怎麼實現這些功能。git

以 Chrome 瀏覽器爲參考對象,咱們須要實現如下功能:github

  • 路由導航:可以前進、後退、主頁、刷新、輸入地址直接跳轉,若是未找到與地址匹配的頁面則展現 404 頁面
  • 多標籤頁:每一個標籤頁都可以獨立導航,能夠新建和關閉,點擊選項卡可切換到對應標籤頁面
  • 示例頁面:因爲咱們開發的程序側重點在圖形界面開發上,所以須要添加幾個示例頁面做爲瀏覽對象,來模擬實現 Chrome 瀏覽器的頁面瀏覽功能。示例頁面有:npm

    • 新標籤頁:打開新標籤後展現的頁面,提供其它示例頁面的快捷入口
    • 歡迎頁:展現簡單的內容,做爲主頁
    • 關於頁:展現關於這個程序的名稱、圖標、版本號等信息
    • 文件頁:展現當前目錄內的文件列表,包括文件名、修改時間和大小,點擊目錄後展現該目錄內的文件列表
    • 404 頁:提示當前頁面沒法訪問,並引導用戶返回主頁

咱們的程序的圖形界面須要包含如下元素:編程

  • 選項卡欄:每一個選項卡對應一個標籤頁,選項卡包含圖標、頁面標題、關閉按鈕,最右側有個新建標籤頁按鈕
  • 導航欄:包含前進、後退、刷新和主頁這四個導航按鈕,以及地址輸入框、設置菜單按鈕
  • 內區域:展現當前瀏覽的頁面的內容

設計與實現

因爲咱們開發的程序的大部分源代碼都是用於實現圖形界面的,所以,這裏主要講述圖形界面上的設計與實現方法。瀏覽器

組件化

在研究過 Chrome 瀏覽器的界面後,咱們能夠知道它的界面結構以下:sass

  • 主界面bash

    • 選項卡欄
    • 導航欄
    • 內容區

考慮到每一個標籤頁都有本身的選項卡、導航欄和內容區,咱們應該將導航欄和內容區域移入到標籤頁中,結果以下圖所示:網絡

組件化

那麼,咱們須要開發的組件有以下幾個:

  • 主界面 (Browser):負責管理和切換標籤頁,實現選項卡與標籤頁之間的通訊
  • 選項卡 (FrameTab):負責展現對應標籤頁的標題和狀態,在點擊關閉按鈕後通知主界面銷燬標籤頁
  • 標籤頁 (Frame):負責呈現導航欄和頁面內容,實現路由導航功能以及界面交互

按照常規作法,把標籤頁拆分紅導航欄和內容區彷佛會更好一些,但考慮到組件之間的通訊成本和開發成本,組件化到標籤頁已經足夠,由於標籤頁除了導航欄外已經沒有值得組件化的東西了。選項卡欄亦是如此。

佈局

接下來咱們再試着找出界面元素的排列規則:

佈局

如上圖所示,紅色邊框標記的元素是橫向佈局,而綠色邊框標記的元素則是縱向佈局。從中可知,主界面佈局方向爲縱向,選項卡欄和標籤頁從上到下排列,選項卡欄高度固定,標籤頁面高度自動佔滿剩餘空間。標籤頁內的導航欄和內容區也是如此。導航欄中的元素佈局方向爲橫向,按鈕寬度固定,地址欄輸入框的寬度自動佔滿剩餘空間。

數據結構

如何存儲多個標籤頁的數據?

正常狀況下,標籤頁數量是比較少的,對讀寫性能要求很低,所以能夠選擇用鏈表來存放。

標籤頁的數據應該包含什麼?

爲了便於管理標籤頁數據、展現頁面標題和狀態、實現組件間的通訊,標籤頁的數據應該包含編號、標題、狀態、標籤頁和主界面的 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 類,以及圖標、按鈕、下拉菜單等組件。

開發工具:

  • LCPkg —— 包管理工具,能夠用來下載安裝 LCUI 及相關庫的二進制包。
  • LCUI CLI —— LCUI 的命令行開發工具,用於簡化 LCUI 應用程序項目的開發。
  • CMake —— 跨平臺構建工具,用於構建咱們的程序。

開發

準備開發環境

注:這裏假設你使用的是 Windows 系統,若是不是,請自行處理環境搭建和編譯上的問題

在開發前,你須要在你的計算機上安裝如下軟件:

以後,打開命令行窗口,輸入如下命令安裝 lcui-cli 和 lcpkg:

npm install -g @lcui/cli lcpkg

設置 lcpkg,讓它安裝相關依賴工具:

lcpkg setup

建立項目

使用 lcui 命令建立一個名爲 lcui-router-app 的項目:

lcui create lcui-router-app

這個命令會替你完成如下工做:

  • 初始化 git 代碼庫
  • 安裝必要的依賴工具
  • 建立適合 LCUI 應用程序項目的目錄結構
  • 添加最小 LCUI 應用程序的源文件
  • 添加 CMake 和 XMake 的配置文件

注意,受限於國內的網絡環境,部分資源下載比較慢,建議先使用如下命令添加國內源:

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

考慮到大量的代碼片斷會影響文章的閱讀體驗,從這裏開始,咱們將盡可能只關注功能的大體實現原理,具體實現代碼請查看相關文件。

主界面有如下功能:

  • 初始化:構造標籤頁鏈表、選項卡欄、標籤頁的新建按鈕
  • 銷燬:釋放標籤頁鏈表佔用的資源
  • 加載:建立一個標籤頁示例,併爲之綁定相關事件處理器
  • 激活:將指定標籤頁設爲當前展現的標籤頁面
  • 關閉:關閉指定標籤頁,並釋放它佔用的資源

爲了實現與用戶的交互,咱們須要添加如下事件處理器:

  • PageTabClose: 在點擊頁面選項卡的關閉按鈕時,關閉對應的標籤頁
  • PageLoad: 在頁面加載時,將選項卡的狀態切換爲加載中
  • PageLoaded: 在頁面加載完畢時,將頁面的標題更新到選項卡上,並取消選項卡的加載中狀態
  • CommandQuit: 關閉主程序
  • CommandOpenNewTab: 建立新的標籤頁面

主界面的功能實現後,咱們再設計它的佈局和樣式:

  • 主界面採用 flex 佈局,方向爲縱向 (column),高度等於根部件的高度
  • 選項卡欄採用 flex 部件,方向爲默認的橫向 (row)
  • 標籤頁新建按鈕的採用固定的尺寸,禁止拉伸和收縮

相關文件:

實現選項卡組件

選項卡組件由圖標、文字和關閉按鈕組成,在標籤頁處於加載中狀態的時候,咱們能夠用 LC Design 組件庫提供的 Spinner 組件來表達。

該組件提供如下方法:

  • SetActive: 設置是否爲激活狀態,本質上是增長/移除 active 類
  • SetLoading: 設置是否爲加載中狀態,本質上是增長/移除 loading 類
  • SetText: 設置文本內容

佈局與樣式:

  • 最大寬度爲容器內容區寬度的 25%,最小寬度 60px
  • 採用 flex 佈局,文字寬度自動佔滿剩餘空間,其它元素寬度固定
  • 若是處於加載中狀態,顯示 Spinner 組件並隱藏圖標,不然顯示圖標並隱藏 Spinner 組件

相關文件:

實現標籤頁

LCUI Router 提供了 router-view 組件用於渲染與當前路由匹配的組件,咱們能夠將它做爲標籤頁的內容區域,這樣咱們只須要爲導航欄添加相應的事件處理器,而後調用 LCUI Router 的接口來實現路由導航功能。

標籤頁面的功能有:

  • 初始化:建立路由管理器實例,添加路由監聽器,構造導航按鈕、地址輸入框和設置菜單
  • 銷燬:銷燬路由管理器實例
  • 加載:導航至指定的路徑
  • 更新導航欄:根據路由管理器實例中記錄路由位置,更新前進和後退按鈕的禁用狀態

事件處理器:

  • BtnBackClick: 在後退按鈕被點擊時,導航至上一個頁面
  • BtnForwardClick: 在前進按鈕被點擊時,導航至下一個頁面
  • BtnRefreshClick: 在刷新按鈕被點擊時,刷新當前頁面
  • BtnHomeClick: 在主頁按鈕被點擊時,導航至主頁
  • InputKeyDown: 在地址輸入框接受按鍵輸入時,若是按的是回車鍵,則導航至該路徑
  • RouteUpdate:在當前路由更新時,將路由的路徑更新到地址輸入框,並觸發 PageLoad 事件讓主界面更新選項卡
  • CommandOpenNewTab: 通知主界面打開新標籤頁
  • CommandQuit: 通知主界面退出程序
  • OpenHelp: 導航相當於頁面

樣式與佈局:

  • 標籤頁採用 flex 佈局,方向爲縱向 (column),自動佔滿剩餘空間
  • 導航欄採用 flex 佈局,高度固定,導航按鈕尺寸固定,地址輸入框寬度自動佔滿剩餘空間

相關文件:

實現新標籤頁面

新標籤頁面提供歡迎頁、關於頁、文件瀏覽頁的快捷入口,效果與 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 組件的轉圈動畫多轉一段時間,能夠將定時器的超時時間設長一點。

樣式與佈局:

  • 頁面採用 flex 佈局,高度與內容區域相同,內部元素垂直居中
  • 容器採用 flex 佈局,最大寬度爲四個卡片的寬度,內部元素水平居中
  • 卡片尺寸固定,從上到下分別爲頁面的圖標和標題

相關文件:

實現文件瀏覽頁面

其它示例頁面的內容都是靜態的,若是咱們開發的程序的功能只是在這些頁面之間切換的話,未免有些過於簡單,爲了進一步展現圖形界面編程的例子,並充分利用 LCUI Router 的特性,咱們有必要開發一個更爲複雜的頁面,而文件瀏覽頁面剛好是最爲合適的選擇。

以 Chrome 瀏覽器的文件瀏覽頁面做爲參考對象:

文件瀏覽

頁面標題包含了當前目錄的路徑,文件列表項由圖標、文件名、大小和日期組成,在點擊文件名後會跳轉到該文件路徑,若是是目錄,則會渲染該目錄的文件列表。

對於文件列表的呈現,咱們能夠在視圖初始化後遍歷當前路徑下的文件列表,而後獲取它們的名稱、大小和修改日期。因爲文件操做比較耗時且容易阻塞 UI 線程而致使界面卡頓,咱們應該將這些操做放在工做線程上執行,等文件列表生成後再轉到主線程上渲染。

文件名點擊跳轉功能能夠用 LCUI Router 提供的 router-link 組件來實現。在構造文件列表項時,構造 router-link 組件來呈現文件名,而後將它的 to 屬性設爲目錄路徑,這樣在點擊文件名後 router-link 組件會自動導航到該路徑,從而使得位於標籤頁內容區內的 router-view 組件響應路由更新並從新渲染文件瀏覽頁面。

文件瀏覽頁面的功能有:

  • 初始化:綁定 ready 事件,等視圖準備完畢後再繼續下一步
  • 準備完畢:讀取當前路由中通配符匹配的路徑,將它做爲當前瀏覽的目錄路徑,而後將文件列表的加載任務發送到工做線程
  • 加載文件列表:遍歷當前目錄下的文件,獲取文件信息,而後構造文件列表項,在遍歷完後,發送渲染任務到 UI 線程
  • 渲染文件列表:將生成的文件列表追加到視圖中,並觸發 PageLoaded 事件通知主界面當前標籤頁已加載完畢

佈局與樣式:

  • 文件列表項採用 flex 佈局
  • 文件名寬度佔滿剩餘空間,文件大小和修改時間的爲固定寬度
  • 若是文件列表項是目錄,則在 hover 時會高亮文件名

相關文件:

實現關於頁面

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 上改進此文章。

相關文章
相關標籤/搜索