主頁 > .NET開發 > 使用 MAUI 在 Windows 和 Linux 上繪制 PPT 的圖表

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

2022-09-10 06:27:29 .NET開發

我在做一個圖表工具軟體,這個軟體使用 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 = https://www.cnblogs.com/lindexi/archive/2022/09/09/graphic?.GraphicData;
        var chartReference = graphicData?.GetFirstChild();

        // 獲取到 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 = https://www.cnblogs.com/lindexi/archive/2022/09/09/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 = https://www.cnblogs.com/lindexi/archive/2022/09/09/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 = https://www.cnblogs.com/lindexi/archive/2022/09/09/(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 = https://www.cnblogs.com/lindexi/archive/2022/09/09/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 = https://www.cnblogs.com/lindexi/archive/2022/09/09/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()!.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 = https://www.cnblogs.com/lindexi/archive/2022/09/09/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  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 具體平臺實作,也可以自己基于介面,自己實作一套渲染進行對接

代碼

本文以上的測驗檔案和代碼放在github 和 gitee 歡迎訪問

可以通過如下方式獲取本文的源代碼,先創建一個空檔案夾,接著使用命令列 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]),

轉載請註明出處,本文鏈接:https://www.uj5u.com/net/505992.html

標籤:.NET技术

上一篇:劃分中小型企業網

下一篇:劃分中小型企業網

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • WebAPI簡介

    Web體系結構: 有三個核心:資源(resource),URL(統一資源識別符號)和表示 他們的關系是這樣的:一個資源由一個URL進行標識,HTTP客戶端使用URL定位資源,表示是從資源回傳資料,媒體型別是資源回傳的資料格式。 接下來我們說下HTTP. HTTP協議的系統是一種無狀態的方式,使用請求/ ......

    uj5u.com 2020-09-09 22:07:47 more
  • asp.net core 3.1 入口:Program.cs中的Main函式

    本文分析Program.cs 中Main()函式中代碼的運行順序分析asp.net core程式的啟動,重點不是剖析原始碼,而是理清程式開始時執行的順序。到呼叫了哪些實體,哪些法方。asp.net core 3.1 的程式入口在專案Program.cs檔案里,如下。ususing System; us ......

    uj5u.com 2020-09-09 22:07:49 more
  • asp.net網站作為websocket服務端的應用該如何寫

    最近被websocket的一個問題困擾了很久,有一個需求是在web網站中搭建websocket服務。客戶端通過網頁與服務器建立連接,然后服務器根據ip給客戶端網頁發送資訊。 其實,這個需求并不難,只是剛開始對websocket的內容不太了解。上網搜索了一下,有通過asp.net core 實作的、有 ......

    uj5u.com 2020-09-09 22:08:02 more
  • ASP.NET 開源匯入匯出庫Magicodes.IE Docker中使用

    Magicodes.IE在Docker中使用 更新歷史 2019.02.13 【Nuget】版本更新到2.0.2 【匯入】修復單列匯入的Bug,單元測驗“OneColumnImporter_Test”。問題見(https://github.com/dotnetcore/Magicodes.IE/is ......

    uj5u.com 2020-09-09 22:08:05 more
  • 在webform中使用ajax

    如果你用過Asp.net webform, 說明你也算是.NET 開發的老兵了。WEBform應該是2011 2013左右,當時還用visual studio 2005、 visual studio 2008。后來基本都用的是MVC。 如果是新開發的專案,估計沒人會用webform技術。但是有些舊版 ......

    uj5u.com 2020-09-09 22:08:50 more
  • iis添加asp.net網站,訪問提示:由于擴展配置問題而無法提供您請求的

    今天在iis服務器配置asp.net網站,遇到一個問題,記錄一下: 問題:由于擴展配置問題而無法提供您請求的頁面。如果該頁面是腳本,請添加處理程式。如果應下載檔案,請添加 MIME 映射。 WindowServer2012服務器,添加角色安裝完.netframework和iis之后,運行aspx頁面 ......

    uj5u.com 2020-09-09 22:10:00 more
  • WebAPI-處理架構

    帶著問題去思考,大家好! 問題1:HTTP請求和回傳相應的HTTP回應資訊之間發生了什么? 1:首先是最底層,托管層,位于WebAPI和底層HTTP堆疊之間 2:其次是 訊息處理程式管道層,這里比如日志和快取。OWIN的參考是將訊息處理程式管道的一些功能下移到堆疊下端的OWIN中間件了。 3:控制器處理 ......

    uj5u.com 2020-09-09 22:11:13 more
  • 微信門戶開發框架-使用指導說明書

    微信門戶應用管理系統,采用基于 MVC + Bootstrap + Ajax + Enterprise Library的技術路線,界面層采用Boostrap + Metronic組合的前端框架,資料訪問層支持Oracle、SQLServer、MySQL、PostgreSQL等資料庫。框架以MVC5,... ......

    uj5u.com 2020-09-09 22:15:18 more
  • WebAPI-HTTP編程模型

    帶著問題去思考,大家好!它是什么?它包含什么?它能干什么? 訊息 HTTP編程模型的核心就是訊息抽象,表示為:HttPRequestMessage,HttpResponseMessage.用于客戶端和服務端之間交換請求和回應訊息。 HttpMethod類包含了一組靜態屬性: private stat ......

    uj5u.com 2020-09-09 22:15:23 more
  • 部署WebApi隨筆

    一、跨域 NuGet參考Microsoft.AspNet.WebApi.Cors WebApiConfig.cs中配置: // Web API 配置和服務 config.EnableCors(new EnableCorsAttribute("*", "*", "*")); 二、清除默認回傳XML格式 ......

    uj5u.com 2020-09-09 22:15:48 more
最新发布
  • C#多執行緒學習(二) 如何操縱一個執行緒

    <a href="https://www.cnblogs.com/x-zhi/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2943582/20220801082530.png" alt="" /></...

    uj5u.com 2023-04-19 09:17:20 more
  • C#多執行緒學習(二) 如何操縱一個執行緒

    C#多執行緒學習(二) 如何操縱一個執行緒 執行緒學習第一篇:C#多執行緒學習(一) 多執行緒的相關概念 下面我們就動手來創建一個執行緒,使用Thread類創建執行緒時,只需提供執行緒入口即可。(執行緒入口使程式知道該讓這個執行緒干什么事) 在C#中,執行緒入口是通過ThreadStart代理(delegate)來提供的 ......

    uj5u.com 2023-04-19 09:16:49 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    <a href="https://www.cnblogs.com/huangxincheng/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/214741/20200614104537.png" alt="" /&g...

    uj5u.com 2023-04-18 08:39:04 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    一:背景 1. 講故事 前段時間協助訓練營里的一位朋友分析了一個程式卡死的問題,回過頭來看這個案例比較經典,這篇稍微整理一下供后來者少踩坑吧。 二:WinDbg 分析 1. 為什么會卡死 因為是表單程式,理所當然就是看主執行緒此時正在做什么? 可以用 ~0s ; k 看一下便知。 0:000> k # ......

    uj5u.com 2023-04-18 08:33:10 more
  • SignalR, No Connection with that ID,IIS

    <a href="https://www.cnblogs.com/smartstar/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/u36196.jpg" alt="" /></a>...

    uj5u.com 2023-03-30 17:21:52 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:15:33 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:13:31 more
  • C#遍歷指定檔案夾中所有檔案的3種方法

    <a href="https://www.cnblogs.com/xbhp/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/957602/20230310105611.png" alt="" /></a&...

    uj5u.com 2023-03-27 14:46:55 more
  • C#/VB.NET:如何將PDF轉為PDF/A

    <a href="https://www.cnblogs.com/Carina-baby/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2859233/20220427162558.png" alt="" />...

    uj5u.com 2023-03-27 14:46:35 more
  • 武裝你的WEBAPI-OData聚合查詢

    <a href="https://www.cnblogs.com/podolski/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/616093/20140323000327.png" alt="" /><...

    uj5u.com 2023-03-27 14:46:16 more