使用C++界面框架ImGUI開發一個簡單程式

来源:https://www.cnblogs.com/timefiles/archive/2023/08/15/17632348.html
-Advertisement-
Play Games

[TOC] # 簡介 ImGui 是一個用於C++的用戶界面庫,跨平臺、無依賴,支持OpenGL、DirectX等多種渲染API,是一種即時UI(Immediate Mode User Interface)庫,保留模式與即時模式的區別參考[**保留模式與即時模式**](https://learn.m ...


目錄

簡介

ImGui 是一個用於C++的用戶界面庫,跨平臺、無依賴,支持OpenGL、DirectX等多種渲染API,是一種即時UI(Immediate Mode User Interface)庫,保留模式與即時模式的區別參考保留模式與即時模式。ImGui渲染非常快,但界面上有大量的數據集需要渲染可能會有一些問題,需要使用一些緩存技巧。緩存只是避免數據的更新邏輯耗時太久影響渲染,實際渲染過程不存在瓶頸。

本文最終實現的應用效果如下:
image

使用示例

ImGui有master、docking兩個分支,鏈接如下:

推薦使用docking分支(支持視窗停靠),本文也是使用的docking分支,先把項目下載下來。

下載示例

從github下載示例,打開examples文件夾下的項目,有很多示例可以選:
image

我的電腦只有example_glfw_opengl和example_win32_directx兩個系列的示例能直接運行起來,example_win32_directx的界面不知道為什麼看起來很糊,所以只能選擇example_glfw_opengl3的示例來開始後續的內容。

main文件

example_glfw_opengl3項目的源文件如下:

image

其中,main文件有很多有用的註釋和代碼片段,下麵主要介紹主題、字體部分內容。

設置ImGui風格

ImGui提供Dark、Light、Classic三種風格,示例中預設使用Dark:

// Setup Dear ImGui style
ImGui::StyleColorsDark();
//ImGui::StyleColorsLight();

// Classic在示例註釋裡面沒有提及,但源碼裡面有對應的函數
//ImGui::StyleColorsClassic();

Dark風格:

image

Light風格:

image

Classic 風格:

image

自定義配色方案可參考上面設置風格的函數實現,裡面的顏色種類太多,後面會單獨寫一篇界面美化的文章。

設置字體

ImGui預設字體說實話比較難看,我還是比較喜歡微軟雅黑:

//設置微軟雅黑字體,並指定字體大小
ImFont* font = io.Fonts->AddFontFromFileTTF
(
    "C:/Windows/Fonts/msyh.ttc",
    30,
    nullptr,
    //設置載入中文
    io.Fonts->GetGlyphRangesChineseFull()
);
//必須判斷一下字體有沒有載入成功
IM_ASSERT(font != nullptr);

註意一下,一定要使用GetGlyphRangesChineseFull(),使用GetGlyphRangesChineseSimplifiedCommon()的話會有部分中文載入不出來。
上面載入字體有兩個問題:固定文件路徑、記憶體占用過高,後面會單獨寫一篇關於字體的文章。

主迴圈

main函數的主迴圈類似其它界面框架的消息迴圈,裡面有一些示例代碼需要刪除,簡化後的代碼大致如下:

while (!glfwWindowShouldClose(window))
{
    //一些註釋...
    glfwPollEvents();
    // Start the Dear ImGui frame
    ImGui_ImplOpenGL3_NewFrame();
    ImGui_ImplGlfw_NewFrame();
    ImGui::NewFrame();
    /*
    * 添加自己的代碼,App的實現見下麵的代碼
    */
    App::RenderUI();

    // Rendering
    ImGui::Render();
    //一些代碼和註釋
    glfwSwapBuffers(window);
}

每幀的UI渲染都在App::RenderUI()函數裡面執行,具體實現參考下麵的內容。

添加Application類

實際項目中不可能把所有UI代碼放到Main函數裡面,所以添加命名為Application的頭文件和源文件用來存放UI代碼。
同時,創建一個App的命名空間併在Application.h中引入imgui.h頭文件,在源碼中添加一些示例。
頭文件內容如下:

#pragma once
#include "imgui.h"
namespace App
{
    void RenderUI();
}

源碼文件如下:

#include "Application.h"
#include <iostream>
#include <string>

namespace App
{
    //選中結果
    bool isShowDrag=false;
    //字元串結果
    std::string text="";
    //拖拽值
    float fValue = 0.5f;

    void RenderUI()
    {
        //創建一個設置視窗
        ImGui::Begin("設置拖拽按鈕");
        //按鈕在單擊時返回true(大多數小部件在編輯/激活時返回true)
        if (ImGui::Button("按鈕"))
        {
            //單擊事件處理程式
        }
        //顯示一些文本(也可以使用字元串格式)
        ImGui::Text("這是一個中文字元串");

        // 緩衝區用於存儲文本輸入值
        char buffer[256] = ""; 
        ImGui::InputText("輸入框", buffer, sizeof(buffer));
        //編碼轉換
        std::string textU8 = buffer;

        ImGui::Checkbox("顯示拖拽", &isShowDrag);
        if (isShowDrag)
        {
            float value = 10.0f;
            ImGui::DragFloat(u8"值",&value);
        }
        //使用從0.0f到1.0f的滑塊編輯1個浮動
        ImGui::SliderFloat("float", &fValue, 0.0f, 1.0f);
        ImGui::SameLine();
        ImGui::Text("Value %f", fValue);
        ImGui::End();
    }
}

運行結果如下:
image

中文編碼問題

在上述例子中,有一個關於中文字元串的問題需要註意。預設情況下,VS使用ANSI編碼格式的字元串,因此ImGui會顯示為????。

關於這個問題,網上提供了幾種解決方案:

  • 在字元串前面添加u8首碼,例如:ImGui::Begin(u8"顯示中文");
  • 將字元串轉換為UTF-8編碼格式後再傳入,例如:ImGui::Begin(string_to_utf8("顯示中文").c_str());
  • 使用記事本將源碼文件另存為UTF-8格式(推薦使用),參考B站視頻:imgui 幫助 19

推薦使用第三種方法,它簡單快速。第一種方法實際上沒有作用,依然會顯示亂碼。第二種方法可能會導致部分代碼轉換為換行符,從而導致編譯錯誤。第三種方法只需修改文件的編碼格式,之後可以直接使用中文,無需添加u8首碼。

需要註意的是,將源碼文件另存為UTF-8格式只解決了顯示問題,並沒有解決中文輸入問題。文本框控制項中的中文字元串值採用的是UTF-8格式。如果需要在代碼中列印或保存文本框的輸入值,需要將其轉換為ANSI編碼。可以參考C++字元串編碼轉換進行轉換。需要註意的是,使用標準庫版本的編碼轉換可能存在多線程bug。

界面設計

界面整體使用"左導航右內容"佈局,視窗標題下麵最多加一個菜單欄用來設置一下業務無關的邏輯(如界面主題、停靠選項等),界面效果見文章開頭。下麵會介紹怎麼創建這樣一個界面,包括一些需要註意的問題。

關於imgui_demo.cpp

imgui_demo.cpp有8000多行代碼,裡面有各種有用的示例代碼,下麵需要樹控制項和停靠空間的代碼都是從這裡拷貝改造的。
可以先調用ImGui::ShowDemoWindow()查看示例顯示效果,並根據渲染的字元串到代碼裡面進行搜索,如搜索Basic treesDockSpace等。

創建停靠空間

ImGui的docking分支雖然支持停靠,但需要使用者主動去創建才能使用。把ShowExampleAppDockSpace函數從imgui_demo.cpp複製到Application.cpp的RenderUI函數裡面,稍微改造刪除一些代碼、註釋。
代碼如下:

void RenderUI()
{
    //p_open不需要,改成nullptr
    bool* p_open = nullptr;
    static bool opt_fullscreen = true;
    static bool opt_padding = false;
    static ImGuiDockNodeFlags dockspace_flags = ImGuiDockNodeFlags_None;    ImGuiWindowFlags window_flags = ImGuiWindowFlags_MenuBar | ImGuiWindowFlags_NoDocking;
    if (opt_fullscreen)
    {
        const ImGuiViewport* viewport = ImGui::GetMainViewport();
        ImGui::SetNextWindowPos(viewport->WorkPos);
        ImGui::SetNextWindowSize(viewport->WorkSize);
        ImGui::SetNextWindowViewport(viewport->ID);
        ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
        ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
        window_flags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove;
        window_flags |= ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoNavFocus;
    }
    else
    {
        dockspace_flags &= ~ImGuiDockNodeFlags_PassthruCentralNode;
    }    if (dockspace_flags & ImGuiDockNodeFlags_PassthruCentralNode)
        window_flags |= ImGuiWindowFlags_NoBackground;    if (!opt_padding)
        ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
    ImGui::Begin("DockSpace Demo", p_open, window_flags);
    if (!opt_padding)
        ImGui::PopStyleVar();    if (opt_fullscreen)
        ImGui::PopStyleVar(2);    // Submit the DockSpace
    ImGuiIO& io = ImGui::GetIO();
    if (io.ConfigFlags & ImGuiConfigFlags_DockingEnable)
    {
        ImGuiID dockspace_id = ImGui::GetID("MyDockSpace");
        ImGui::DockSpace(dockspace_id, ImVec2(0.0f, 0.0f), dockspace_flags);
    }
    else
    {
        //不關閉Docking
        //ShowDockingDisabledMessage();
    }    if (ImGui::BeginMenuBar())
    {
        //菜單做一些漢化
        if (ImGui::BeginMenu("選項(Options)"))
        {
            ImGui::MenuItem("全屏(Fullscreen)", NULL, &opt_fullscreen);
            ImGui::MenuItem("填充(Padding)", NULL, &opt_padding);
            ImGui::Separator();            if (ImGui::MenuItem("標誌:不分割(Flag: NoSplit)", "", (dockspace_flags & ImGuiDockNodeFlags_NoSplit) != 0)) { dockspace_flags ^=mGuiDockNodeFlags_NoSplit; }
            if (ImGui::MenuItem("標誌:不調整大小(Flag: NoResize)", "", (dockspace_flags & ImGuiDockNodeFlags_NoResize) != 0)) { dockspace_flags ^=mGuiDockNodeFlags_NoResize; }
            if (ImGui::MenuItem("標誌:不停靠在中心節點(Flag: NoDockingInCentralNode)", "", (dockspace_flags & ImGuiDockNodeFlags_NoDockingInCentralNode) != 0)) {ockspace_flags ^= ImGuiDockNodeFlags_NoDockingInCentralNode; }
            if (ImGui::MenuItem("標誌:自動隱藏選項卡欄(Flag: AutoHideTabBar)", "", (dockspace_flags & ImGuiDockNodeFlags_AutoHideTabBar) != 0)) { dockspace_flags= ImGuiDockNodeFlags_AutoHideTabBar; }
            if (ImGui::MenuItem("標誌:中心節點篩選器(Flag: PassthruCentralNode)", "", (dockspace_flags & ImGuiDockNodeFlags_PassthruCentralNode) != 0,pt_fullscreen)) { dockspace_flags ^= ImGuiDockNodeFlags_PassthruCentralNode; }
            ImGui::Separator();
            //不關閉菜單
           /* if (ImGui::MenuItem("Close", NULL, false, p_open != NULL))
                *p_open = false;*/
            ImGui::EndMenu();
        }
        //增加主題切換
        if (ImGui::BeginMenu("主題(Other)"))
        {
            if (ImGui::MenuItem("暗黑(Dark)")) { ImGui::StyleColorsDark(); }
            if (ImGui::MenuItem("明亮(Light)")) { ImGui::StyleColorsLight(); }
            if (ImGui::MenuItem("經典(Classic)")) { ImGui::StyleColorsClassic(); }
            ImGui::EndMenu();
        }
        //HelpMarker 不需要
        ImGui::EndMenuBar();
    }

    /**添加自己的視窗**/
    ShowTreeView();
    ShowMainView();

    ImGui::End();
}

創建頁面

先在Application.h文件中定義需要用到的一些函數:

#pragma once
#include "imgui.h"
namespace App
{
    //主UI函數,放停靠空間的代碼
    void RenderUI();

    //隱藏視窗的TabBar
    void HideTabBar();
    //導航頁面
    void ShowTreeView();
    //內容頁面
    void ShowMainView();
    /*
    * 內容頁面0-5
    */
    void ShowPageView0();
    void ShowPageView1();
    void ShowPageView2();
    void ShowPageView3();
    void ShowPageView4();
}

隱藏視窗標簽欄

正常情況下視窗創建後會有一個標簽欄,這個很影響界面外觀需要去掉,參考github上面的issues:以編程方式完成停靠和隱藏選項卡欄
沒次創建視窗時,需要在ImGui::Begin前面調用,代碼如下:

#include <imgui_internal.h>

void HideTabBar()
{
    ImGuiWindowClass window_class;
    window_class.DockNodeFlagsOverrideSet = ImGuiDockNodeFlags_NoTabBar;
    ImGui::SetNextWindowClass(&window_class);
}

創建導航頁面

導航頁面本質上就是一個樹控制項,使用ImGui自帶的控制項樣式即可,代碼也是從demo裡面拷貝到Application.cpp的,如下所示:

//一級索引
int FirstIdx = 0;
//二級索引
int SecondIdx = 0;
//導航頁面
void ShowTreeView()
{
    HideTabBar();
    ImGui::Begin("導航視窗");
    if (ImGui::TreeNode("功能選項"))
    {
        for (int i = 0; i < 5; i++)
        {
            if (i == 0)
                ImGui::SetNextItemOpen(true, ImGuiCond_Once);
            if (ImGui::TreeNode((void*)(intptr_t)i, "功能 %d", i))
            {
                ImGui::Text("圖標");
                ImGui::SameLine();
                if (ImGui::SmallButton("按鈕1")) { FirstIdx = i; SecondIdx = 0; }
                ImGui::Text("圖標");
                ImGui::SameLine();
                if (ImGui::SmallButton("按鈕2")) { FirstIdx = i; SecondIdx = 1; }
                ImGui::TreePop();
            }
        }
        ImGui::TreePop();
    }
    if (ImGui::TreeNode("其它選項"))
    {
        ImGui::Text("圖標");
        ImGui::SameLine();
        if (ImGui::SmallButton("按鈕")) { }
        ImGui::TreePop();
    }
    ImGui::End();
}

註:上面的“圖標”使用圖標字體就可以顯示真正的圖標,圖標字體的載入留到後續的文章再說。

創建內容頁面

內容頁面根據導航的索引確定需要渲染的內容,為了避免頁面關於單調,裡面加了一個表格控制項和選項卡控制項的示例,代碼如下:

//內容頁面
void ShowMainView()
{
    HideTabBar();
    // 清除之前的內容
    ImGui::Begin("頁面視窗");
    switch (FirstIdx)
    {
    case 0:
        ShowPageView0();
        break;
    case 1:
        ShowPageView1();
        break;
    case 2:
        ShowPageView2();
        break;
    case 3:
        ShowPageView3();
        break;
    case 4:
        ShowPageView4();
        break;
    default:
        break;
    }
    ImGui::End();
}
void ShowPageView0()
{
    ImGui::Text("功能%d -> 按鈕%d -> 頁面0", FirstIdx,SecondIdx);
    //一個表格示例
    static ImGuiTableFlags flags =
        ImGuiTableFlags_SizingFixedFit |
        ImGuiTableFlags_RowBg |
        ImGuiTableFlags_Borders |
        ImGuiTableFlags_Resizable |
        ImGuiTableFlags_Reorderable |
        ImGuiTableFlags_Hideable;
    if (ImGui::BeginTable("table0", 3, flags))
    {
        ImGui::TableSetupColumn("AAA", ImGuiTableColumnFlags_WidthFixed);
        ImGui::TableSetupColumn("BBB", ImGuiTableColumnFlags_WidthFixed);
        ImGui::TableSetupColumn("CCC", ImGuiTableColumnFlags_WidthStretch);
        ImGui::TableHeadersRow();
        for (int row = 0; row < 5; row++)
        {
            ImGui::TableNextRow();
            for (int column = 0; column < 3; column++)
            {
                ImGui::TableSetColumnIndex(column);
                ImGui::Text("%s %d,%d", (column == 2) ? "Stretch" : "Fixed", column, row);
            }
        }
        ImGui::EndTable();
    }
}
void ShowPageView1()
{
    ImGui::Text("功能%d -> 按鈕%d -> 頁面1", FirstIdx, SecondIdx);
    //選項卡示例
    ImGuiTabBarFlags tab_bar_flags = ImGuiTabBarFlags_None;
    if (ImGui::BeginTabBar("MyTabBar", tab_bar_flags))
    {
        if (ImGui::BeginTabItem("Avocado"))
        {
            ImGui::Text("This is the Avocado tab!\nblah blah blah blah blah");
            ImGui::EndTabItem();
        }
        if (ImGui::BeginTabItem("Broccoli"))
        {
            ImGui::Text("This is the Broccoli tab!\nblah blah blah blah blah");
            ImGui::EndTabItem();
        }
        if (ImGui::BeginTabItem("Cucumber"))
        {
            ImGui::Text("This is the Cucumber tab!\nblah blah blah blah blah");
            ImGui::EndTabItem();
        }
        ImGui::EndTabBar();
    }
}
void ShowPageView2() { ImGui::Text("功能%d -> 按鈕%d -> 頁面2", FirstIdx, SecondIdx); }
void ShowPageView3() { ImGui::Text("功能%d -> 按鈕%d -> 頁面3", FirstIdx, SecondIdx); }
void ShowPageView4() { ImGui::Text("功能%d -> 按鈕%d -> 頁面4", FirstIdx, SecondIdx); }

隱藏控制台視窗

隱藏控制台視窗一般都是調用HWND hWnd = GetConsoleWindow()獲取控制台視窗,然後使用ShowWindow(hWnd, SW_HIDE)隱藏視窗,這裡使用另一種更合理的方法。

右擊項目“屬性->鏈接器->系統->子系統”,將控制台改為視窗,如下圖所示:
image
然後將main函數改成WinMain函數,代碼如下:

// Main code
//int main(int, char**)
int WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)

如果使用的不是win32,還需要引用以下頭文件:

#include <windows.h>

順便把程式標題也改一下,代碼如下:

// Create window with graphics context
GLFWwindow* window = glfwCreateWindow(1280, 720, "演示程式", nullptr, nullptr);

打包程式

將生成的exe文件重命名為App.exe單獨放到一個文件夾,初次打開需要手動調整佈局,程式退出時佈局自動保存在ImGui.ini文件中。最後打包的文件如下所示:

image

可以看到exe文件很小,只有500多KB。

總結

待解決問題

本文記錄初次使用ImGui的整個過程,解決了使用過程中的大部分問題,但還有一些細節問題需要解決:

  • 字體記憶體載入方式時軟體退出報錯
  • 字體文件過大時載入耗時導致白屏
  • 程式圖標自定義,預設窗體標題欄的自定義
  • 控制項外觀自定義,載入圖標字體作為控制項圖標

這些問題後面會每個單獨寫一篇文章處理,目前的內容已經可以開發一些簡單的應用。

開發優勢

ImGui界面框架是我使用過開發體驗的最好的界面框架,有種在C++中使用Winform開發的感覺,甚至比Winform開發更簡單。這裡要點名吐槽MFC,如果不是工作所迫我是絕對不會用它的。個人感覺ImGui良好的開發體驗主要來自兩方面:

  • 即時UI模式降低了界面和數據之間的交互難度
  • 框架預設的佈局、樣式降低了開發的心智負擔

ImGui目前主要應用在游戲開發中,但我感覺在其它領域也能很好的發揮作用,在C#中也有ImGui.Net可以使用。

附件

源代碼:提取碼: 59nz


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 索引簽名是 TypeScript 中一個強大的特性,它允許我們在對象和類中使用動態的屬性名稱。通常情況下,我們會在對象或類中定義固定的屬性,但有時我們需要處理具有動態屬性名稱的情況。這時,索引簽名就派上了用場。 在這篇技術博文中,我們將介紹索引簽名的使用方法和用例,將展示如何定義帶有索引簽名的介面... ...
  • CQRS也叫命令查詢職責分離,是近年來非常流行的應用程式架構模式。本文將重點介紹如何通過MediatR的管道功能將FluentValidation集成到CQRS項目中實現驗證功能。 ...
  • 獲取byte中每一位的值 byte byData = 0x36; int n0, n1, n2, n3, n4, n5, n6, n7; n0 = (byData & 0x01) == 0x01 ? 1 : 0; n1 = (byData & 0x02) == 0x02 ? 1 : 0; n2 = ...
  • 背景:在linux環境下,很多服務我們都使用docker來跑,很是方便,容器服務獨立,配置獨立,數據獨立等等,但是有個問題,就是如果某個服務異常了,暫停了,停止了,一直重啟中,我們要怎麼及時的知道是哪個服務,併進行處理,保證業務正常運行。 本文主要介紹使用docker服務自帶的一些命令來實現一個基本 ...
  • Lua程式設計第四版第二部分編程實操自做練習題答案,帶:star:為重點。 ## 14.1 :star: > 該函數用於兩個稀疏矩陣相加 ```lua function martixAdd(a, b) local c = {} for i = 1, #a, 1 do c[i] = {} for k, ...
  • [TOC] ## 類型的基本歸類 **整形家族:** ```c char unsigned char signed char short unsigned short [int] signed short [int] int unsigned int signed int long unsigned ...
  • 在Python中,列表(list)是一種有序、可變的數據結構,用於存儲多個元素。列表可以包含不同類型的元素,包括整數、浮點數、字元串等。實際上列表有點類似C++語言中的數組,但僅僅只是類似,和數組還是有點不一樣的。列表非常適合利用順序和位置定位某一元素,尤其是當元素的順序或內容經常發生改變時。 在P ...
  • # 1. 回顧 > 1. java實現多線程: [1]繼承Thread類並重寫run方法 [2]實現Runnable介面 > > 2. 線程Thread中常用的方法: setName(): Thread.currentThread().getName(): > > ​ static void sle ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...