使用 MAUI 在 Windows 和 Linux 上繪製 PPT 的圖表

来源:https://www.cnblogs.com/lindexi/archive/2022/09/09/16671495.html
-Advertisement-
Play Games

題目 中文 實現一個以 T 作為泛型參數的 IsNever類型. 如果 T 是never, 返回 true, 否則返回 false. 示例: type A = IsNever<never>; // expected to be true type B = IsNever<undefined>; // ...


我在做一個圖表工具軟體,這個軟體使用 MAUI 開發。我的需求是圖表的內容需要和 PPT 的圖表對接,需要用到 OpenXML 解析 PPT 內容,讀取到 PPT 圖表元素的內容,接著使用 MAUI 渲染層繪製圖表元素。圖表工具軟體需要在 Windows 平臺和 Linux 平臺上運行。在 Windows 下,我採用 WPF 應用,用來闢謠說 MAUI 不支持 WPF 應用。 在 Linux 選用 Ubuntu 系統,採用 GTKSharp 應用加上 Skia 渲染對接 MAUI 框架

圖表工具軟體的開發架構如下,可以看到只有和具體平臺對接的一層不相同

本文將包含兩個部分,一個是解析渲染面積圖圖表,另一個是使用 MAUI 開發跨平臺應用。解析面積圖圖表是用到 OpenXML 解析 PPT 的知識,本文只包含很少量的 OpenXML 的知識,我將詳細的使用 OpenXML 解析 PPT 的面積圖的方法放在了 dotnet OpenXML 解析 PPT 圖表 面積圖入門 博客里。本文的用到的解析 PPT 的代碼也是從此博客裡面抄的,這部分代碼將不會在本文上貼出。 如對 OpenXML 解析 PPT 毫無概念的伙伴,閱讀本文也不會存在問題,只需要假定本文的解析 PPT 的代碼是通過某個方式獲取到了圖表的相關信息即可,請將重點放在圖表的繪製渲染,以及如何做跨平臺對接上

本文使用的代碼只能用來做例子,本文的解析 PPT 圖表的代碼只能支持本文例子里的測試文件,本文的測試文件和代碼可以從本文最後獲取

在開始之前,先看一下本文實現的效果

效果

這是在 PPT 的圖表:

在 Windows 下,使用 Skia 繪製為圖片文件,然後使用 Image 控制項顯示圖片,界面效果如下:

以上只是將 MAUI 接入 WPF 的一個方法。不代表只能通過圖片文件的方式接入,其他繪製方法請看 WPF 使用 MAUI 的自繪製邏輯

在 Linux 下,使用 Skia 對接 Gtk 框架,界面效果如下:

動態運行效果如下

接下來將告訴大家如何實現

解析繪製面積圖圖表

開始實現繪製 PPT 的圖表之前,需要先解析圖表的內容

圖表的解析部分需要用到 OpenXML 知識,這部分解析的內容,在 dotnet OpenXML 解析 PPT 圖表 面積圖入門 博客裡面有詳細說明。使用 dotnet OpenXML 解析 PPT 圖表 面積圖入門 的方法解析出圖表的內容將獲取到的內容放入到 AreaChartRenderContext 類型,此類型用來提供渲染繪製使用的上下文,包括以下屬性

/// <summary>
///     用來提供圖表 面積圖 渲染的上下文信息
/// </summary>
public class AreaChartRenderContext
{
    // 忽略代碼

    public ChartSpace ChartSpace { get; }
    public SlideContext SlideContext { get; }
    public Pixel Width { get; }
    public Pixel Height { get; }

    /// <summary>
    ///     類別軸上的數據 橫坐標軸上的數據
    /// </summary>
    public ChartValueList CategoryAxisValueList { get; } = null!;

    /// <summary>
    ///     面積圖的系列信息集合
    /// </summary>
    public AreaChartSeriesInfoList AreaChartSeriesInfoList { get; } = new();
}

上面代碼的 ChartSpace 屬性是圖表元素,通過 dotnet OpenXML 解析 PPT 圖表 面積圖入門 博客可以瞭解到裡面包含圖表的信息。上面代碼的 SlideContext 屬性是我所在的團隊開源的 OpenXml 解析輔助庫提供的包含元素所在頁面的類型,詳細請看: https://github.com/dotnet-campus/DocumentFormat.OpenXml.Extensions

圖表關鍵的信息包含類別軸上的數據,也稱為橫坐標軸上的數據,放在 CategoryAxisValueList 屬性。系列信息集合,放在 AreaChartSeriesInfoList 屬性。這兩個屬性是從 ChartSpace 讀取,讀取的方法請看 dotnet OpenXML 解析 PPT 圖表 面積圖入門 博客或者閱讀本文用到的代碼

在獲取到了圖表的各個信息之後,即可進行繪製圖表。開始進行繪製之前,還請先瞭解圖表的各個組成部分

  • 橫坐標軸 類別坐標軸數據:

  • 縱坐標軸:

  • 數據系列:

在圖表裡面有數據系列的概念,每個系列的數據組成一個個的數據系列。對於大部分圖表來說,數據層都是由一個個數據系列組成的

每個數據系列可以有自己的系列名稱

系列名稱大部分時候都放在圖例裡面,也就是圖例裡面的內容就是由系列名稱提供的

在圖表裡面,核心就是對數據的處理,系列的數據內容就是核心的

如圖,面積圖有兩個數據系列,通過上面的 Excel 內容可以瞭解到兩個系列的數據分別如下

系列 1:32,32,28,12,15
系列 2:12,12,12,21,28

為了讓繪製邏輯更方便閱讀,定義 AreaChartRender 類用來繪製圖表

圖表繪製 AreaChartRender 需要兩個參數,一個是 AreaChartRenderContext 用來提供信息,一個是 Microsoft.Maui.Graphics.ICanvas 用來提供渲染繪製方法。在各個平臺上,可以使用不同的實現對接 MAUI 的渲染,也就是 Microsoft.Maui.Graphics.ICanvas 介面可以對應不同的實現。在解析渲染模塊里不耦合具體的平臺渲染實現,只使用抽象的介面,定義的類型如下

public class AreaChartRender
{
    public AreaChartRender(AreaChartRenderContext context)
    {
        Context = context;
    }

    public AreaChartRenderContext Context { get; }

    public void Render(ICanvas canvas)
    {
        // 忽略代碼
    }

圖表繪製 AreaChartRender 基礎的使用方法是在和 OpenXML 解析 PPT 的圖表這一層對接,通過 AreaChartRenderContext 類型拿到圖表的內容,創建出 AreaChartRender 對象,傳遞給具體的渲染層。在渲染層里,將區分平臺進行渲染,各個平臺定義 Microsoft.Maui.Graphics.ICanvas 的實現,傳入到 AreaChartRender 的 Render 方法。在 Render 方法將繪製圖表內容,即可通過抽象的 Microsoft.Maui.Graphics.ICanvas 介面,調用各個平臺具體的繪製實現

使用以下代碼即可使用 OpenXML 解析 PPT 的圖表,獲取圖表內容,關於以下代碼的細節邏輯,請看 dotnet OpenXML 解析 PPT 圖表 面積圖入門

public class ModelReader
{
    /// <summary>
    ///     構建出面積圖上下文
    /// </summary>
    /// <param name="file">這裡是例子,要求只能傳入 Test.pptx 文件。其他文件沒有支持</param>
    /// <returns></returns>
    public AreaChartRender BuildAreaChartRender(FileInfo file)
    {
        using var presentationDocument = PresentationDocument.Open(file.FullName, false);
        var slide = presentationDocument.PresentationPart!.SlideParts.First().Slide;

        /*
          <p:cSld>
            <p:spTree>
              <p:graphicFrame>
                ...
              </p:graphicFrame>
            </p:spTree>
          </p:cSld>
        */
        // 獲取圖表元素,在這份課件里,有一個面積圖。以下使用 First 忽略細節,獲取圖表
        var graphicFrame = slide.Descendants<GraphicFrame>().First();

        /*
              <p:graphicFrame>
                <a:graphic>
                  <a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/chart">
                    <c:chart xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" r:id="rId2" />
                  </a:graphicData>
                </a:graphic>
              </p:graphicFrame>
         */
        // 獲取到對應的圖表信息,圖表是引用的,內容不是放在 Slide 頁面裡面,而是放在獨立的圖表 xml 文件里
        var graphic = graphicFrame.Graphic;
        var graphicData = graphic?.GraphicData;
        var chartReference = graphicData?.GetFirstChild<ChartReference>();

        // 獲取到 id 也就是 `r:id="rId2"` 根據 Relationship 的描述,可以知道去 rels 文件裡面獲取關聯的內容。在 OpenXml SDK 里,封裝好了獲取方法,獲取時需要有兩個參數,一個是 id 另一個是去哪裡獲取的 Part 內容
        var id = chartReference?.Id?.Value;

        // 這裡需要告訴 OpenXml SDK 去哪裡獲取資源。詳細請看 https://blog.lindexi.com/post/dotnet-OpenXML-%E4%B8%BA%E4%BB%80%E4%B9%88%E8%B5%84%E6%BA%90%E4%BD%BF%E7%94%A8-Relationship-%E5%BC%95%E7%94%A8.html 
        // 如果是放在模版裡面,記得要用模版的 Part 去獲取
        var currentPart = slide.SlidePart!;

        if (!currentPart.TryGetPartById(id!, out var openXmlPart))
        {
            // 在這份課件里,一定不會進入此分支
            // 一定能從頁面找到對應的資源內容也就是圖表
        }

        var chartPart = (ChartPart) openXmlPart!;

        // 這裡的 ChartPart 對應的就是 charts\chartN.xml 文件。這裡的 chartN.xml 表示的是 chart1.xml 或 chart2.xml 等文件
        var chartSpace = chartPart.ChartSpace;

        var slideContext = new SlideContext(slide, presentationDocument);

        var transformData = graphicFrame.GetOrCreateTransformData(slideContext);

        return new AreaChartRender(new AreaChartRenderContext(chartSpace, slideContext, transformData.Width.ToPixel(),
            transformData.Height.ToPixel()));
    }
}

具體的平臺渲染實現部分,放在下一章。下麵先在 Render 方法對接 MAUI 的抽象的 Microsoft.Maui.Graphics.ICanvas 介面,進行繪製圖表。繪製圖表的工作量包括繪製坐標軸信息,計算刻度線,對各個系列的繪製

本文這裡採用的是絕對佈局方式,相對來說用到的知識簡單。缺點是很多計算都會放在下麵代碼,看起來比較複雜,好在計算只是小學數學的加減

下麵的繪製代碼只能作為本文的例子使用,很多原本需要進行排版計算的值,為了方便理解,我都使用常量,如下麵代碼,還請忽略這部分的細節

    public void Render(ICanvas canvas)
    {
        // 圖表標題
        float chartTitleHeight = 52;
        // 圖例高度,圖例是放在最下方
        float chartLegendHeight = 42;
        // 類別信息的高度
        float axisValueHeight = 20;

        float yAxisLeftMargin = 42;
        float yAxisRightMargin = 42;
        float xAxisBottomMargin = 25;
    }

從 OpenXML 解析的 PPT 圖表獲取到的 AreaChartRenderContext 拿到圖表的元素尺寸,用來作為圖表繪製畫布的限制尺寸

        var chartWidth = (float) Context.Width.Value;
        var chartHeight = (float) Context.Height.Value;

以上的數值定義全部採用 float 類型,其原因是 MAUI 為了更好的適配更多的平臺,選用了 float 作為渲染繪製的參數的通用類型。這一點和 WPF 的不相同,在 WPF 或 UWP 或 WinFroms 等,通用的繪製計算都採用 double 類型。對於渲染繪製,大部分情況,使用 float 也是夠用的。如果一個 double 值的範圍是在 float 內,那進行 double 轉 float 也是安全的。至於性能的損耗,如果不是熱點代碼,也可以忽略

通過以上的信息即可計算出圖表的繪製範圍,包括坐標和尺寸

        var plotAreaOffsetX = yAxisLeftMargin;
        var plotAreaOffsetY = chartTitleHeight;
        var plotAreaWidth = chartWidth - yAxisLeftMargin - yAxisRightMargin;
        var plotAreaHeight = chartHeight - chartTitleHeight - chartLegendHeight - xAxisBottomMargin;

這些信息屬於佈局信息,本文這裡只是使用簡單的固定數值計算,而不是跟隨具體的圖表數據進行計算,以上的代碼比較“塑料”還請不要抄到實際項目代碼。完成佈局計算之後,開始繪製坐標軸信息。坐標軸信息包含了刻度信息,也就是 Y 軸的刻度。刻度信息包括了每個刻度之間的數值間隔是多少,最大值和最小值是多少的信息。我採用了玄學的計算方法 GetRatio 獲取到了刻度的間隔的值,以及和這份 PPT 的圖表一樣固定了只有 8 條線

        var rowLineCount = 8; // 這份 PPT 測試文件里只有 8 條線
        // 獲取數據最大值
        var maxData = GetMaxValue();
        // 獲取刻度的值
        var ratio = GetRatio(maxData, rowLineCount); // 這是一個玄學的方法。才不告訴你方法裡面直接返回了一個常量
        var maxValue = ratio * (rowLineCount - 1);

完成了基礎計算,接下來可以開始繪製坐標軸。繪製坐標軸就需要用到 MAUI 的繪製知識,對這些繪製知識感興趣還請參閱官方文檔: Graphics - .NET MAUI Microsoft Docs

繪製坐標軸,本質上是繪製網格線,步驟是先繪製 Y 軸,再繪製 X 軸。如 PPT 的圖表效果,這份文檔的 Y 軸只有刻度,也就是需要繪製 Y 軸的刻度和 x 行的線。在 MAUI 里,繪製線條只需要使用 DrawLine 方法,傳入兩個點即可。控制線條的粗細和顏色等,是通過在 DrawLine 方法之前,先設置好參數屬性。如下麵代碼繪製 X 行的線

        for (var i = 0; i < rowLineCount; i++)
        {
            canvas.StrokeSize = 2;
            canvas.StrokeColor = Colors.Gray;

            var offsetX = plotAreaOffsetX;
            var offsetY = plotAreaOffsetY + plotAreaHeight - plotAreaHeight / (rowLineCount - 1) * i;

            canvas.DrawLine(offsetX, offsetY, offsetX + plotAreaWidth, offsetY);
        }

以上代碼通過 StrokeSize 設置繪製的線條的粗細是 2 的值,這裡的值是沒有一個單位的,具體的單位是具體的渲染平臺自己賦予的,可以認為是像素。使用 StrokeColor 設置線條的顏色,再使用 DrawLine 傳入兩個點,繪製出線條

接下來繼續繪製 Y 軸的刻度。繪製刻度需要用到文本繪製的方法,文本繪製中存在一個小問題,那就是中文字體設置的問題,好在此問題被我修複了,詳細請看 Fix set the Font to Microsoft.Maui.Graphics.Skia by lindexi · Pull Request #9124 · dotnet/maui

以下代碼只是繪製數字而已,不需要設置中文字體,也就不會踩到上文說到的坑。為了讓繪製文本對齊到刻度,需要給定繪製文本的範圍,這裡稍微有一些知識需要瞭解,詳細請看 Microsoft.Maui.Graphics.Skia 使用 DrawString 繪製文本的坐標問題

            // 獲取刻度的值
            var fontSize = 16f;
            canvas.FontSize = fontSize;
            var textRightMargin = 5;
            var textX = 0;
            var textY = offsetY - fontSize / 2f;
            var textWidth = plotAreaOffsetX - textX - textRightMargin;
            var textHeight = 25;
            // 獲取刻度的文本
            var value = (ratio * i).ToString(CultureInfo.CurrentCulture);
            canvas.DrawString(value, textX, textY, textWidth, textHeight, HorizontalAlignment.Right, VerticalAlignment.Top);

和繪製線條相同的是,在繪製文本之前,通過參數屬性設置文本的屬性,例如上面代碼設置了文本的字體大小。同樣,這裡的字體大小也是沒有具體單位的,由具體的平臺實現決定,大部分情況可以認為是像素單位

完成了繪製 Y 軸的刻度和 x 行的線,繼續繪製放在 X 軸底部的類別信息,也就是對應本文的圖表的日期信息。好在日期的表示的字元串也沒有用到中文,依然不會踩到上文描述的中文字體的坑

        // 繪製 X 軸,繪製類別信息
        var categoryAxisValueList = Context.CategoryAxisValueList.ValueList;
        for (var i = 0; i < categoryAxisValueList.Count; i++)
        {
            var offsetX = plotAreaOffsetX + plotAreaWidth * i / (categoryAxisValueList.Count - 1);
            var offsetY = plotAreaOffsetY + plotAreaHeight;

            var textX = offsetX - 20;
            var textY = offsetY + xAxisBottomMargin;
            if (i < categoryAxisValueList.Count)
            {
                var text = categoryAxisValueList[i].GetViewText();
                canvas.DrawString(text, textX, textY, HorizontalAlignment.Left);
            }
        }

繪製類別信息的工作量就是計算出文本的坐標,和使用 GetViewText 方法,獲取到具體類別里的用戶可見的文本的字元串,然後調用 DrawString 方法即可

完成坐標軸的繪製之後,就進入關鍵的 DrawArea 方法,在此方法裡面,將會繪製圖表的數據信息。將圖表的各個系列的數據作為面積圖繪製

繪製面積圖圖表的方法是獲取到圖表的各個系列的數值信息,根據這些數值創建出一段 Path Geometry 路徑幾何用於填充面積圖。創建路徑幾何可使用 PathF 類型創建一個基於 float 存儲信息的路徑幾何。這裡的 PathF 就是 Path + Float 的意思,如以下代碼進行創建

  using var path = new PathF();

在 MAUI 里,這個 PathF 是推薦做釋放的,在各個平臺的 PathF 的底層實現有所不同,不代表著一定需要釋放。好在多調用釋放是安全的,這裡就加上 using 用來在方法執行結束釋放。開始繪製之前,先準備一點點路徑幾何創建的知識。按照 Path 的創建慣例,開始點採用 Move 方法設置,如以下代碼

  path.Move(startX, startY);

在 MAUI 的設計里,可以使用連續的方法,輸入繪製參數,如畫兩條線,然後設置幾何關閉,可以採用如下代碼

            path.LineTo(x1, y1)
                .LineTo(x2, y2)
                .Close();

如上面代碼即可畫出一段路徑集合出來,本文會用到的也僅僅只是以上幾個方法,這也就是本文用到的核心繪製路徑的知識。當然,路徑幾何 PathF 是一個複雜的類型,擁有的方法和功能可遠不止本文介紹的這一點,更多繪製知識,還請參閱官方文檔。在瞭解了基礎用法,接下來開始繪製面積圖

繪製面積圖只是一些計算邏輯,通過給定的數據計算出 PathF 的內容,代碼如下

        for (var chartDataIndex = 0; chartDataIndex < Context.AreaChartSeriesInfoList.Count; chartDataIndex++)
        {
            var chartSeriesInfo = Context.AreaChartSeriesInfoList[chartDataIndex];
            if (chartSeriesInfo.ChartValueList is null)
            {
                continue;
            }

            using var path = new PathF();
            var startX = plotAreaOffsetX;
            var startY = plotAreaOffsetY + plotAreaHeight;
            path.Move(startX, startY);

            for (var i = 0; i < chartSeriesInfo.ChartValueList.ValueList.Count; i++)
            {
                var value = chartSeriesInfo.ChartValueList.ValueList[i];

                if (value is NumericChartValue numericChartValue)
                {
                    var offsetX = plotAreaOffsetX + plotAreaWidth * i / (categoryAxisValueList.Count - 1);
                    var offsetY = plotAreaOffsetY + plotAreaHeight -
                                  numericChartValue.GetValue() / maxValue * plotAreaHeight;

                    path.LineTo(offsetX, (float) offsetY);
                }
            }

            path.LineTo(plotAreaOffsetX + plotAreaWidth, plotAreaOffsetY + plotAreaHeight)
                .LineTo(startX, startY)
                .Close();

        }

創建 path 路徑完成,即可繪製到畫布。按照慣例,繪製需要先設置填充顏色,再繪製

                // 在這份課件里,一定是純色
                var (success, a, r, g, b) =
                    BrushCreator.ConvertToColor(chartSeriesInfo.FillBrush!.GetFill<SolidFill>()!.RgbColorModelHex!.Val!);

                var color = new Color(r, g, b, a); // 獲取到各個系列的填充顏色
                canvas.FillColor = color;

                canvas.FillPath(path);

以上簡單的代碼即可完成圖表的繪製。我將上面代碼放在一個方法,方便大家閱讀

    public void Render(ICanvas canvas)
    {
        var chartWidth = (float) Context.Width.Value;
        var chartHeight = (float) Context.Height.Value;

        // 圖表標題
        float chartTitleHeight = 52;
        // 圖例高度,圖例是放在最下方
        float chartLegendHeight = 42;
        // 類別信息的高度
        float axisValueHeight = 20;

        float yAxisLeftMargin = 42;
        float yAxisRightMargin = 42;
        float xAxisBottomMargin = 25;

        var plotAreaOffsetX = yAxisLeftMargin;
        var plotAreaOffsetY = chartTitleHeight;
        var plotAreaWidth = chartWidth - yAxisLeftMargin - yAxisRightMargin;
        var plotAreaHeight = chartHeight - chartTitleHeight - chartLegendHeight - xAxisBottomMargin;

        // void CreateCoordinate()
        // 繪製坐標系

        // 先找到 Y 軸的刻度,找到最大值

        // 有多少條行的線,保持和 PPT 相同
        var rowLineCount = 8;
        // 獲取數據最大值
        var maxData = GetMaxValue();
        // 獲取刻度的值
        var ratio = GetRatio(maxData, rowLineCount);
        var maxValue = ratio * (rowLineCount - 1);

        // 繪製網格線,先繪製 Y 軸,再繪製 X 軸
        // 繪製 Y 軸的刻度和 x 行線
        for (var i = 0; i < rowLineCount; i++)
        {
            canvas.StrokeSize = 2;
            canvas.StrokeColor = Colors.Gray;

            var offsetX = plotAreaOffsetX;
            var offsetY = plotAreaOffsetY + plotAreaHeight - plotAreaHeight / (rowLineCount - 1) * i;

            canvas.DrawLine(offsetX, offsetY, offsetX + plotAreaWidth, offsetY);

            // 獲取刻度的值
            var fontSize = 16f;
            canvas.FontSize = fontSize;
            var textRightMargin = 5;
            var textX = 0;
            var textY = offsetY - fontSize / 2f;
            var textWidth = plotAreaOffsetX - textX - textRightMargin;
            var textHeight = 25;
            var value = (ratio * i).ToString(CultureInfo.CurrentCulture);
            canvas.DrawString(value, textX, textY, textWidth, textHeight, HorizontalAlignment.Right,
                VerticalAlignment.Top);
        }

        // 繪製 X 軸,繪製類別信息
        var categoryAxisValueList = Context.CategoryAxisValueList.ValueList;
        for (var i = 0; i < categoryAxisValueList.Count; i++)
        {
            var offsetX = plotAreaOffsetX + plotAreaWidth * i / (categoryAxisValueList.Count - 1);
            var offsetY = plotAreaOffsetY + plotAreaHeight;

            var textX = offsetX - 20;
            var textY = offsetY + xAxisBottomMargin;
            if (i < categoryAxisValueList.Count)
            {
                var text = categoryAxisValueList[i].GetViewText();
                canvas.DrawString(text, textX, textY, HorizontalAlignment.Left);
            }
        }

        // void DrawArea()
        // 繪製內容
        for (var chartDataIndex = 0; chartDataIndex < Context.AreaChartSeriesInfoList.Count; chartDataIndex++)
        {
            var chartSeriesInfo = Context.AreaChartSeriesInfoList[chartDataIndex];
            if (chartSeriesInfo.ChartValueList is null)
            {
                continue;
            }

            using var path = new PathF();
            var startX = plotAreaOffsetX;
            var startY = plotAreaOffsetY + plotAreaHeight;
            path.Move(startX, startY);

            for (var i = 0; i < chartSeriesInfo.ChartValueList.ValueList.Count; i++)
            {
                var value = chartSeriesInfo.ChartValueList.ValueList[i];

                if (value is NumericChartValue numericChartValue)
                {
                    var offsetX = plotAreaOffsetX + plotAreaWidth * i / (categoryAxisValueList.Count - 1);
                    var offsetY = plotAreaOffsetY + plotAreaHeight -
                                  numericChartValue.GetValue() / maxValue * plotAreaHeight;

                    path.LineTo(offsetX, (float) offsetY);
                }
            }

            path.LineTo(plotAreaOffsetX + plotAreaWidth, plotAreaOffsetY + plotAreaHeight)
                .LineTo(startX, startY)
                .Close();

            if (chartDataIndex < Context.AreaChartSeriesInfoList.Count)
            {
                // 在這份課件里,一定是純色
                var (success, a, r, g, b) =
                    BrushCreator.ConvertToColor(chartSeriesInfo.FillBrush!.GetFill<SolidFill>()!.RgbColorModelHex!
                        .Val!);

                var color = new Color(r, g, b, a);
                canvas.FillColor = color;
            }

            canvas.FillPath(path);
        }
    }

原本是將上面代碼拆開作為多個函數,為了方便調試,還是放在一個函數里。在實際項目上,不要讓一個方法的代碼如此多

開發跨平臺應用

完成圖表的繪製邏輯,接下來需要各個平臺進行對接。與 MAUI 的對接是十分簡單的,按照慣例,是先安裝 NuGet 庫,然後調用庫提供的方法即可完成對接。先對接 Windows 平臺的 WPF 應用

在 WPF 應用里,這次採用的是對接圖片文件渲染方法。如本文開始的開發架構圖所述,在 Windows 上通過 Microsoft.Maui.Graphics.Skia 將 Skia 和 MAUI 對接,使用 Skia 作為 MAUI 的畫布,在繪製完成之後使用 Skia 保存本地圖片文件,再使用 WPF 渲染保存的圖片

這不代表著在 WPF 裡面,只能通過 Skia 才能和 MAUI 對接,也不代表著 WPF 對接 Skia 只能通過本地圖片的顯示。關於在 WPF 裡面,直接對接 MAUI 的方法請看 WPF 使用 MAUI 的自繪製邏輯

關於在 WPF 裡面,使用 WriteableBitmap 控制項作為 Skia 的輸出的方式,讓 WPF 對接 Skia 的方法請看 WPF 使用 Skia 繪製 WriteableBitmap 圖片

回到對接的邏輯,由於本文的 WPF 應用只負責將 Skia 保存的圖片進行渲染,也就是說 WPF 層是可以不知道任何 MAUI 和 Skia 的邏輯,只需要知道保存的圖片文件在哪即可。既然沒有什麼 WPF 的邏輯,那就先來關註一下 Skia 的對接邏輯

這裡的 Skia 邏輯包括兩個部分,一個是 Skia 輸出到本地圖片文件,另一個是 Skia 對接 MAUI 的邏輯。關於 Skia 對接 MAUI 的邏輯,細節可參閱 dotnet 控制台 使用 Microsoft.Maui.Graphics 配合 Skia 進行繪圖入門 文檔,本文將不包含細節邏輯

開始之前,按照慣例先安裝 NuGet 庫。在 dotnet 6 應用里,通過編輯 csproj 項目文件的方式可以快速安裝 NuGet 庫,在 csproj 文件上加上以下代碼用來安裝 NuGet 庫。安裝的 NuGet 庫包括用來解析 PPT 的 dotnetCampus.DocumentFormat.OpenXml.FlattendotnetCampus.OpenXmlUnitConverterDocumentFormat.OpenXml 庫,和 MAUI 的 Microsoft.Maui.GraphicsMicrosoft.Maui.Graphics.Skia

    <ItemGroup>
        <PackageReference Include="dotnetCampus.DocumentFormat.OpenXml.Flatten" Version="2.1.0" />
        <PackageReference Include="dotnetCampus.OpenXmlUnitConverter" Version="2.1.0" />
        <PackageReference Include="DocumentFormat.OpenXml" Version="2.17.1" />

        <PackageReference Include="Microsoft.Maui.Graphics" Version="6.0.403" />
        <PackageReference Include="Microsoft.Maui.Graphics.Skia" Version="6.0.403" />
    </ItemGroup>

為了方便開發,我將 Skia 對接 MAUI 的邏輯,封裝到 SkiaPngImageRenderCanvas 類型。此類型繼承 IRenderCanvas 介面,介面定義如下

/// <summary>
///     渲染畫板
/// </summary>
public interface IRenderCanvas
{
    /// <summary>
    ///     開始進行渲染
    /// </summary>
    /// <param name="action"></param>
    void Render(Action<ICanvas> action);
}

通過調用 Render 方法,傳入委托,委托的參數就是 Microsoft.Maui.Graphics.ICanvas 介面,在此委托裡面完成實際的繪製邏輯

創建 SkiaPngImageRenderCanvas 需要三個參數,分別是寬度高度的畫布尺寸,也就是保存的圖片的尺寸,這裡的單位是像素,和保存的文件。上層業務調用 Render 完成,將輸出文件


/// <summary>
///     提供使用 png 作為輸出的 Skia 畫板
/// </summary>
public class SkiaPngImageRenderCanvas : IRenderCanvas
{
    private readonly int _width;
    private readonly int _height;
    private readonly FileInfo _outputPngFile;

    public SkiaPngImageRenderCanvas(int width, int height, FileInfo outputPngFile)
    {
        _width = width;
        _height = height;
        _outputPngFile = outputPngFile;
    }

    public void Render(Action<ICanvas> action)
    {
    	// 忽略代碼
    }

在 Render 方法里,將先創建 Skia 的畫布,接著使用 Skia 的畫布創建 MAUI 的畫布,將 MAUI 的畫布傳入到委托作為參數,繪製完成保存本地文件

在 Skia 裡面,最重要的概念是畫布 SKCanvas 類型,基本的繪製邏輯都是調用此類型的方法完成。通過此類型即可在上面繪製內容。而 Skia 與 MAUI 的對接里,也需要用到此類型,對接的方法是創建 Microsoft.Maui.Graphics.Skia.SkiaCanvas 對象,此 SkiaCanvas 對象繼承了 Microsoft.Maui.Graphics.ICanvas 介面,即可用來傳入圖表的繪製層作為繪製的畫布

初始化 SkiaCanvas 對象就需要用到 SKCanvas 對象,以下代碼包含了創建 SKCanvas 對象和使用 SKCanvas 對象創建出 SkiaCanvas 對象

    public void Render(Action<ICanvas> action)
    {
        var skImageInfo = new SKImageInfo(_width, _height, SKColorType.Bgra8888, SKAlphaType.Unpremul, SKColorSpace.CreateSrgb());

        using (var skImage = SKImage.Create(skImageInfo))
        {
            using (var skBitmap = SKBitmap.FromImage(skImage))
            {
                using (var skCanvas = new SKCanvas(skBitmap))
                {
                    skCanvas.Clear(SKColors.Transparent);

                    var skiaCanvas = new SkiaCanvas();
                    skiaCanvas.Canvas = skCanvas;

                    ICanvas canvas = skiaCanvas;

                    action(canvas);

                    // 忽略代碼
                }
            }
        }
    }

接著在執行 action 委托完成之後,保存為本地圖片,代碼如下

                    skCanvas.Clear(SKColors.Transparent);

                    var skiaCanvas = new SkiaCanvas();
                    skiaCanvas.Canvas = skCanvas;

                    ICanvas canvas = skiaCanvas;

                    action(canvas);

                    skCanvas.Flush();

                    using (var skData = skBitmap.Encode(SKEncodedImageFormat.Png, 100))
                    {
                        var file = _outputPngFile;
                        using (var fileStream = file.OpenWrite())
                        {
                            fileStream.SetLength(0);
                            skData.SaveTo(fileStream);
                        }
                    }

以上代碼忽略細節邏輯,更多對接細節請看 dotnet 控制台 使用 Microsoft.Maui.Graphics 配合 Skia 進行繪圖入門

以上就完成了 Skia 的對接,接下來就交給 WPF 層,將 OpenXML 解析和 Skia 和 MAUI 對接一起

先對接 OpenXML 解析 PPT 圖表的邏輯。獲取測試文件,將測試文件傳入 ModelReader 構建出 AreaChartRender 用來繪製,如此即可完成 OpenXML 的對接

            var file = new FileInfo("Test.pptx");

            var modelReader = new ModelReader();
            var areaChartRender = modelReader.BuildAreaChartRender(file);

接著定義輸出的本地圖片,創建 SkiaPngImageRenderCanvas 用來做畫布。這裡是隨便找一個文件用來輸出

            var tempFile = Path.GetTempFileName();
            var outputFile = new FileInfo(tempFile);

            var skiaPngImageRenderCanvas = new SkiaPngImageRenderCanvas((int) Math.Ceiling(areaChartRender.Context.Width.Value), (int) Math.Ceiling(areaChartRender.Context.Height.Value), outputFile);

讓 AreaChartRender 使用 Skia 提供的畫布進行渲染,這就是關鍵的對接代碼

 skiaPngImageRenderCanvas.Render(areaChartRender.Render);

如此即可將讓圖表繪製到 SkiaPngImageRenderCanvas 提供的 SkiaCanvas 對象上,最終使用 SKCanvas 保存到本地文件

最後一步就是在 WPF 裡面將保存的文件在界面顯示

            var image = new Image()
            {
                Width = areaChartRender.Context.Width.Value,
                Height = areaChartRender.Context.Height.Value,
                Margin = new Thickness(10, 10, 10, 10),
                Source = new BitmapImage(new Uri(outputFile.FullName))
            };

            Root.Children.Add(image);

以上的 Root 是一個放在 XAML 的 Grid 元素

  <Grid x:Name="Root"></Grid>

這就是在 WPF 上對接的方法,所有的代碼如下

    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            Loaded += MainWindow_Loaded;
        }

        private void MainWindow_Loaded(object sender, RoutedEventArgs e)
        {
            var file = new FileInfo("Test.pptx");

            var modelReader = new ModelReader();
            var areaChartRender = modelReader.BuildAreaChartRender(file);

            var tempFile = Path.GetTempFileName();
            var outputFile = new FileInfo(tempFile);

            var skiaPngImageRenderCanvas = new SkiaPngImageRenderCanvas((int) Math.Ceiling(areaChartRender.Context.Width.Value), (int) Math.Ceiling(areaChartRender.Context.Height.Value), outputFile);

            skiaPngImageRenderCanvas.Render(areaChartRender.Render);

            var image = new Image()
            {
                Width = areaChartRender.Context.Width.Value,
                Height = areaChartRender.Context.Height.Value,
                Margin = new Thickness(10, 10, 10, 10),
                Source = new BitmapImage(new Uri(outputFile.FullName))
            };

            Root.Children.Add(image);
        }
    }

運行效果如下

可以看到在 Windows 下,通過 WPF 對接 MAUI 是十分簡單的

下麵開始對接 Linux 平臺的應用,在 Linux 平臺上使用 GtkSharp 框架做應用,依然使用 Skia 做 MAUI 的渲染層

在 Linux 平臺上的對接分為多個任務:

  • 創建 GtkSharp 應用
  • 將 Skia 與 GtkSharp 對接
  • 將 Skia 與 MAUI 的對接

上文已經有了 Skia 和 MAUI 的對接邏輯的細節,接下來將跳過 Skia 與 MAUI 的對接部分的細節邏輯。本文接下來將重點放在如何創建 GtkSharp 應用以及將 Skia 與 GtkSharp 對接上

在開始 GtkSharp 應用的創建之前,需要先聊一點歷史。嗯,本考古學家要聊的不是上古的歷史了,只是聊聊現代的歷史。關於上古的 Gtk 的故事,還請自行查詢。回到歷史故事上,很久之前 mono 組織就創建了 https://github.com/mono/gtk-sharp 倉庫,此倉庫在 2020 之前還能勉力支持,但漸漸就跟不上 gtk 的發展了,只能支持到 gtk2 的版本。後來大佬們專門給 GtkSharp 創建了組織和倉庫,在 mono 組織的 gtk-sharp 的基礎上繼續維護,現在支持到了 gtk3 的版本,請看 https://github.com/GtkSharp/GtkSharp

本文創建的 GtkSharp 應用,就是使用 https://github.com/GtkSharp/GtkSharp 提供的支持

手動創建的方法是先創建一個 dotnet 6 的控制台應用,接著編輯 csproj 文件,修改為以下代碼,安裝 GtkSharp 和 SkiaSharp.Views.Gtk3 庫。如以下代碼可以瞭解到創建一個 GtkSharp 項目十分簡單,只需要安裝上支持 .NET Standard 2.0 及以上框架的 GtkSharp 庫即可

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <None Remove="**\*.glade" />
    <EmbeddedResource Include="**\*.glade">
      <LogicalName>%(Filename)%(Extension)</LogicalName>
    </EmbeddedResource>
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="GtkSharp" Version="3.24.24.*" />
    <PackageReference Include="SkiaSharp.Views.Gtk3" Version="2.88.0" />

    <PackageReference Include="dotnetCampus.DocumentFormat.OpenXml.Flatten" Version="2.1.0" />
    <PackageReference Include="dotnetCampus.OpenXmlUnitConverter" Version="2.1.0" />
    <PackageReference Include="DocumentFormat.OpenXml" Version="2.17.1" />
    <PackageReference Include="Microsoft.Maui.Graphics" Version="6.0.403" />
    <PackageReference Include="Microsoft.Maui.Graphics.Skia" Version="6.0.403" />
    <PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="2.88.0" />
  </ItemGroup>

</Project>

其實 https://github.com/GtkSharp/GtkSharp 倉庫的細節還是做的很好的,除了以上手工創建的方法外,還可以通過 dotnet new 命令創建項目。以下是使用 dotnet new 命令創建項目的方法

第一步是安裝 dotnet new 模版,在控制台命令行輸入以下代碼即可進行安裝

dotnet new --install GtkSharp.Template.CSharp

安裝完成之後,即可使用如下命令創建項目,請將下麵命令的 MyApplication 替換為你的項目名

dotnet new gtkapp -o MyApplication

創建好了 GtkSharp 項目和安裝完成了必要的 NuGet 包之後,接下來是讓 Skia 和 GtkSharp 進行對接。在開始對接之前,需要說明的是,我推薦是在 Ubuntu 上構建和運行此項目,而不是在 Windows 上運行。儘管 GtkSharp 聲稱是支持 Windows 平臺的,而且 https://github.com/GtkSharp/GtkSharp 倉庫也做了很多輔助構建工作,但是實際在 Windows 平臺上的構建體驗還是比較鬧心的。為什麼這麼說?構建的第一步是需要將依賴下載了,依賴放在 https://github.com/GtkSharp/Dependencies 倉庫里,將依賴下載到 %LocalAppData%\Gtk\3.24.24\gtk.zip 文件。然而這是一個 50MB 左右的文件,在國內的垃圾網速下……

如果想要在 Windows 下構建,同時嫌棄拉 gtk-3.24.24.zip 的速度太慢,可以試試我上傳到 CSDN 下載的資源 https://download.csdn.net/download/lindexi_gd/86362889

如果構建成功,但是運行提示 System.DllNotFoundException: Gtk: libgtk-3-0.dll 失敗,請參閱 https://github.com/GtkSharp/GtkSharp/issues/337

回到讓 Skia 和 GtkSharp 進行對接的邏輯,編輯 MainWindow.glade 文件,替換為以下代碼

<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1 -->
<interface>
    <requires lib="gtk+" version="3.18"/>
    <object class="GtkWindow" id="MainWindow">
        <property name="can_focus">False</property>
        <property name="title" translatable="yes">SkiaSharp</property>
        <property name="default_width">600</property>
        <property name="default_height">600</property>
    </object>
</interface>

這個文件就是 GTK 的界面描述,更多關於這個文件的知識,還請自行瞭解,這不是本文的重點。如果對 GtkSharp 不熟悉,不知道如何配置,推薦到本文最後獲取所有的代碼

編輯 MainWindow.cs 修改構造函數為以下代碼,以下代碼的含義是將一個 SKDrawingArea 對象作為視窗顯示的內容,這裡的 SKDrawingArea 對象里提供了 PaintSurface 事件,通過此事件即可獲取到 Skia 的畫布。在構造函數里,對接了 GtkSharp 和 Skia 的邏輯

        public MainWindow()
            : this(new Builder("MainWindow.glade"))
        {
        }

        private MainWindow(Builder builder)
            : base(builder.GetObject("MainWindow").Handle)
        {
            builder.Autoconnect(this);
            DeleteEvent += OnWindowDeleteEvent;

            var skiaView = new SKDrawingArea();
            skiaView.PaintSurface += OnPaintSurface;
            skiaView.Show();
            Child = skiaView;
        }

        private void OnWindowDeleteEvent(object sender, DeleteEventArgs a)
        {
            Application.Quit();
        }

        private void OnPaintSurface(object sender, SKPaintSurfaceEventArgs e)
        {
            // 忽略代碼
        }

OnPaintSurface 方法裡面就是 Skia 的渲染回調,有點和 WPF 的 OnRender 方法類似,在此函數里,通過 e.Surface.Canvas 繪製的內容,將會輸出到 GtkSharp 的視窗

根據上文的 WPF 對接 Skia 和 MAUI 的邏輯,可以瞭解到對接的方式是使用 Skia 的畫布創建 MAUI 的 SkiaCanvas 畫布,如以下代碼

// the the canvas and properties
var canvas = e.Surface.Canvas;

var skiaCanvas = new SkiaCanvas()
{
    Canvas = canvas,
};

儘管推薦 OnPaintSurface 方法只處理繪製邏輯,不要在這個方法裡面寫業務邏輯,但為了方便理解,在本文的例子就在 OnPaintSurface 方法處理了 PPT 解析和圖表繪製邏輯。請不要在實際的項目上,在 PaintSurface 事件里,處理業務邏輯

解析 PPT 文件需要先獲取到測試文件,再使用上文的 ModelReader 創建出 AreaChartRender 對象,這些邏輯在各個平臺都是相同的

            var file = new FileInfo(System.IO.Path.Combine(System.IO.Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "Test.pptx"));

            var modelReader = new ModelReader();
            AreaChartRender areaChartRender = modelReader.BuildAreaChartRender(file);

再使用和上文一樣的對接 Skia 和 MAUI 的邏輯進行對接。對接方法依然是獲取到 skiaCanvas 對象,傳入到 AreaChartRender 繪製,這就是最關鍵的代碼

areaChartRender.Render(skiaCanvas);

可以看到,關鍵的代碼也只需要一句即可完成

這就是在 GtkSharp 上對接的方法,核心的代碼如下

        private void OnPaintSurface(object sender, SKPaintSurfaceEventArgs e)
        {
            var file = new FileInfo(System.IO.Path.Combine(System.IO.Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "Test.pptx"));

            var modelReader = new ModelReader();
            AreaChartRender areaChartRender = modelReader.BuildAreaChartRender(file);

            // the the canvas and properties
            var canvas = e.Surface.Canvas;

            // make sure the canvas is blank
            canvas.Clear(SKColors.White);

            var skiaCanvas = new SkiaCanvas()
            {
                Canvas = canvas,
            };
            areaChartRender.Render(skiaCanvas);
        }

運行的效果如下

這就是使用 MAUI 在 Windows 和 Linux 上解析和繪製 PPT 的圖表的例子,本文忽略了很多細節,更多細節請閱讀本文使用的代碼

整個 MAUI 是一個非常龐大和強大的框架,如此龐大的框架想要完全完成還是需要一些時間的。本文所用到的僅僅只是 MAUI 的渲染層,我將 MAUI 的渲染層拆開,即可放入到現有的應用裡面,也可以輸出到本地圖片文件。既支持 Windows 平臺,又支持 Linux 平臺。可以使用預設自帶的 MAUI 具體平臺實現,也可以自己基於介面,自己實現一套渲染進行對接

代碼

本文以上的測試文件和代碼放在githubgitee 歡迎訪問

可以通過如下方式獲取本文的源代碼,先創建一個空文件夾,接著使用命令行 cd 命令進入此空文件夾,在命令行裡面輸入以下代碼,即可獲取到本文的代碼

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin c5477e93289a71c05787af4b1ab1dbb23f18b0e6

以上使用的是 gitee 的源,如果 gitee 不能訪問,請替換為 github 的源

git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git

獲取代碼之後,打開 Pptx.sln 文件,裡面的包含三個項目:

  • PptxCore 是 PPT 解析和圖表繪製的項目,此項目可以在 Windows 和 Linux 平臺使用
  • Pptx 是一個 WPF 項目
  • PptxGtk 是一個 GtkSharp 項目

更多

更多關於 OpenXML 解析請看 Office 使用 OpenXML SDK 解析文檔博客目錄

更多關於 MAUI 請看 博客導航

博客園博客只做備份,博客發佈就不再更新,如果想看最新博客,請到 https://blog.lindexi.com/

知識共用許可協議
本作品採用知識共用署名-非商業性使用-相同方式共用 4.0 國際許可協議進行許可。歡迎轉載、使用、重新發佈,但務必保留文章署名[林德熙](http://blog.csdn.net/lindexi_gd)(包含鏈接:http://blog.csdn.net/lindexi_gd ),不得用於商業目的,基於本文修改後的作品務必以相同的許可發佈。如有任何疑問,請與我[聯繫](mailto:[email protected])。
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、問題 在用freemarker生成word文檔的時候,在本地可以成功獲取到類路徑下的資源文件。但是打了jar包放在linux系統下啟動,無法獲取到該文件,導致生成的word文檔是個空文檔。 二、解決 1、文件存放路徑 2、原先代碼 第一種 File docxFile = ResourceUtil ...
  • ApplicationContextAware是一個介面,它提供一個方法setApplicationContext,當spring註冊完成之後,會把ApplicationContext對象以參數的方式傳遞到方法里,在方法里我們可以實現自己的邏輯,去獲取自己的bean,當前對接的斷言等;一般用在被封裝 ...
  • 來源:cnblogs.com/jae-tech/p/15409340.html 寫在前面 此異常非彼異常,標題所說的異常是業務上的異常。 最近做了一個需求,消防的設備巡檢,如果巡檢發現異常,通過手機端提交,後臺的實時監控頁面實時獲取到該設備的信息及位置,然後安排員工去處理。 因為需要服務端主動向客戶 ...
  • 一、為什麼要進行類型別名優化 首先我們來看一下前面寫的UserMapper.xml配置文件: <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "h ...
  • 在實際業務中,當後臺數據發生變化,客戶端能夠實時的收到通知,而不是由用戶主動的進行頁面刷新才能查看,這將是一個非常人性化的設計。有沒有那麼一種場景,後臺數據明明已經發生變化了,前臺卻因為沒有及時刷新,而導致頁面顯示的數據與實際存在差異,從而造成錯誤的判斷。那麼如何才能在後臺數據變更時及時通知客戶端呢... ...
  • 一、實驗目的 1.熟悉和掌握小型區域網的配置方法 2.掌握子網劃分中IP地址的分配方法 3.掌握DHCP的配置方法 4.掌握VLAN的配置方法 5.掌握路由的配置方法 6.掌握交換機、路由器的配置方法 二、設備與環境 微型電腦、Windows 系列操作系統 、ensp軟體 三、實驗內容 某公司的小 ...
  • 1.前言 往往一些剛接觸C#編程的初學者,對於泛型的認識就是直接跳到對泛型集合的使用上,雖然微軟為我們提供了很多內置的泛型類型,但是如果我們只是片面的瞭解調用方式,這會導致我們對泛型盲目的使用。至於為什麼要使用泛型,什麼情況下定義屬於自己的泛型,定義泛型又能為程式帶來哪些好處。要理清這些問題,我們就 ...
  • 使用asp.net core 開發應用系統過程中,基本上都會涉及到用戶身份的認證,及授權訪問控制,因此瞭解認證和授權流程也相當重要,下麵通過分析asp.net core 框架中的認證和授權的源碼來分析認證、授權的原理及認證和授權的關係。 認證是什麼? 認證是應用系統識別當前訪問者的身份的一個過程,當 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...