Winform的介面也可以變好看?

Winform的介面也可以變好看?

前幾天跟大家介紹了在winform中使用blazor hybrid,而且還說配上blazor的UI可以讓我們的winform程式設計的更加好看,接下來我想以一個在winform blazor hybrid中繪圖的範例來進行說明,希望對你有所幫助。

最後更新 2024/2/29 上午5:29
DotNet学习交流
預計閱讀 12 分鐘
分類
Winform Blazor
標籤
.NET C# Blazor Winform 混合應用

在 WinForms Blazor Hybrid 中繪圖

前幾天跟大家介紹了在 WinForms 中使用 Blazor Hybrid,而且還說配上 Blazor 的 UI 可以讓我們 WinForms 程式設計得更加好看。接下來我想以一個在 WinForms Blazor Hybrid 中繪圖的例子來說明,希望對你有所幫助。

效果

在開始之前,先給大家演示一下效果,如下所示:

繪圖效果

圖片

具體實作

如果你對具體實作感興趣,可以繼續往下閱讀。

1、引入 Ant Design Blazor

該應用中用到的所有元件都來源於 Ant Design Blazor。

在本文中我只介紹繪圖部分的實作,首先需要在專案中引入 Ant Design Blazor。

安裝 NuGet 套件引用,如下所示:

圖片

如果需要畫圖的話,還需要引用 AntDesign.Charts 套件引用。

在專案的 Form1.cs 中註冊相關服務:

services.AddAntDesign();

如下所示:

圖片

引入靜態樣式和指令碼檔案:

<link href="_content/AntDesign/css/ant-design-blazor.css" rel="stylesheet" />
<script src="_content/AntDesign/js/ant-design-blazor.js"></script>

WinForms Blazor Hybrid 專案在 wwwroot/index.html 中引入,如下所示:

圖片

這裡我也把 AntDesign.Charts 引入了。

_Imports.razor 中加入命名空間:

@using AntDesign

為了動態地顯示彈出元件,需要在 App.razor 中添加一個 <AntContainer /> 元件。

這是官網的說法,在 WinForms Blazor Hybrid 中可以在當做主頁面的 Razor 中添加,我這裡是 Index.razor 如下所示:

圖片

現在就可以使用 Ant Design Blazor 的元件了。

2、頁面設計

繪圖頁面的設計如下所示:

圖片

第一步選擇喜歡的佈局,我選的是官網中的這一款,如下所示:

圖片

自己修改一下圖示與名稱,那麼現在擺在面前的第一個問題就是,如何實現點擊切換頁面呢?

每一個 MenuItem 都有一個 Key 屬性,如下所示:

圖片

在這裡每一個 Key 都是唯一的。點擊不同的 MenuItem 都會觸發點擊事件,而點擊事件使用了 Lambda 表達式呼叫了同一個方法,但是參數不同。

現在來看看這個方法:

 int selectedMenuItem = 1;
 private void NavigateToContent(int menuItemNumber)
 {
     selectedMenuItem = menuItemNumber;
 }

很簡單,只是將參數傳給 selectedMenuItem。

然後在內容這個地方,使用 switch case:

<Content Class="site-layout-background" Style="margin: 24px 16px;padding: 24px;min-height: 450px;">
@switch(selectedMenuItem)
 {
   case 1:
    <GetData></GetData>
           break;
   case 2:
     <QueryData></QueryData>
           break;
   case 3:
      <Painting></Painting>
           break;
   case 4:
      <Export></Export>
           break;
 }
 </Content>

然後就可以根據不同的 selectedMenuItem 值顯示不同的元件了。

現在來看看<Painting></Painting>元件的設計。

<Painting></Painting>元件的頁面程式碼如下:

<div>
  <GridRow>
    <GridCol Span="8">
      <Space Direction="DirectionVHType.Vertical">
        <SpaceItem>
          <Text Strong>開始日期:</Text>
        </SpaceItem>
        <SpaceItem>
          <DatePicker TValue="DateTime?" Format="yyyy/MM/dd" Mask="yyyy/dd/MM"
          Placeholder="@("yyyy/dd/MM")" @bind-Value = "Date1"/>
        </SpaceItem>
        <SpaceItem>
          <Text Strong>結束日期:</Text>
        </SpaceItem>
        <SpaceItem>
          <DatePicker TValue="DateTime?" Format="yyyy/MM/dd" Mask="yyyy/dd/MM"
          Placeholder="@("yyyy/dd/MM")" @bind-Value = "Date2"/>
        </SpaceItem>
        <SpaceItem>
          <Text Strong>站名:</Text>
        </SpaceItem>
        <SpaceItem>
          <AutoComplete
            @bind-Value="@value"
            Options="@options"
            OnSelectionChange="OnSelectionChange"
            OnActiveChange="OnActiveChange"
            Placeholder="input here"
            Style="width:150px"
          />
        </SpaceItem>
        <SpaceItem>
          <Text Strong>繪圖指標:</Text>
        </SpaceItem>
        <SpaceItem>
          <div>
            <AntDesign.CheckboxGroup
              Options="@ckeckAllOptions"
              @bind-Value="selectedValues"
            />
          </div>
        </SpaceItem>
        <SpaceItem>
          <button type="@ButtonType.Primary" OnClick="Painting_Clicked">
            繪圖
          </button>
        </SpaceItem>
      </Space>
    </GridCol>
    <GridCol Span="12">
      <AntDesign.Charts.Line
        Data="@Data1"
        Config="Config1"
        @ref="lineChartRef"
      />
    </GridCol>
  </GridRow>
</div>

3、填充站名

當我們一打開這個元件,就有不同的站名了,如下所示:

圖片

這是怎麼實現的呢?

首先使用<AutoComplete>自動完成這個元件,如下所示:

<AutoComplete
  @bind-Value="@value"
  Options="@options"
  OnSelectionChange="OnSelectionChange"
  OnActiveChange="OnActiveChange"
  Placeholder="input here"
  Style="width:150px"
/>
List<string> options = new List<string>();
protected override void OnInitialized()
{
    options = weatherServer.GetDifferentStations();
}

在 Blazor 中,OnInitialized 是一個生命週期方法,用於在元件初始化時執行一些邏輯。具體而言,OnInitialized 方法是 Microsoft.AspNetCore.Components.ComponentBase 類別中定義的一個虛擬方法,你可以在衍生的元件中覆蓋它,以在元件初始化的時候執行一些自訂的操作。

這裡採用了三層架構的方式,分為 UI 層、業務邏輯層、資料庫存取層。

其中的weatherServer是我自訂的服務,使用這個服務,需要在開頭添加陳述式:

@inject IWeatherServer weatherServer;

在 Blazor 中,@inject 是用於在 Razor 頁面或元件中注入服務的指令。透過 @inject,你可以將相依性注入服務引入到 Blazor 頁面或元件中,以便在其中使用這些服務。

當然要使用服務,必須先註冊服務:

services.AddSingleton<IWeatherServer,WeatherServer>();
services.AddSingleton<DataServer>();

這裡一個是業務邏輯的服務,一個是資料存取的服務。

其中IWeatherServer是業務邏輯層的介面,使用介面的好處,大家可以參考一下:

實作多繼承:

C# 中的類別只支援單一繼承,但一個類別可以實作多個介面。介面提供了一種方式,允許一個類別在不同的維度上取得和實作功能。一個類別可以實作多個介面,從而擁有每個介面定義的一組成員。

實作規範:

介面定義了一組規範,要求實作類別提供特定的成員。這有助於強制實作類別遵循一定的程式設計規範和標準,從而提高程式碼的一致性和可讀性。

提供抽象和彈性:

介面本身不提供具體的實作,只是定義了成員的合約。這使得介面成為一種強大的抽象工具,讓你可以在不暴露具體實作的情況下描述類別的能力。

介面還提供了一種擴展和修改類別行為的方法,而無需更改類別本身的實作。

實作相依性注入:

介面和相依性注入相結合,使得在應用程式中實現可替代性和可測試性變得更加容易。透過相依性注入框架,你可以在執行時期注入不同的實作,從而實現模組之間的低耦合性。

定義公共合約:

介面提供了一種定義公共合約的方式,使得多個實作可以在系統中一起工作,而不管它們的具體型別如何。這對於外掛系統、擴展性和模組化設計非常有用。

允許多型:

透過介面,你可以利用 C# 中的多型機制。當你引用一個物件的介面型別時,你可以在執行時期實際上引用該物件的衍生型別,從而實現多型行為。

定義事件合約: 介面可以包含事件宣告,用於定義類別應該提供的事件合約。這有助於規範事件的使用和處理。

我這裡使用介面,主要是為了明晰服務到底實現了哪些功能,因為具體實作類別中會有很多程式碼,不好看清楚。

比如跟繪圖相關的介面如下所示:

public List<string> GetDifferentStations();
 public List<WeatherData> GetDataByCondition(Condition condition);

然後在實作類別中進行具體實作:

 public List<string> GetDifferentStations()
 {
     return dataService.GetDifferentStations();
 }


 public List<WeatherData> GetDataByCondition(Condition condition)
 {
     return dataService.GetDataByCondition(condition);
 }

業務邏輯層中不與資料庫直接相互作用,使用了資料庫存取服務:

public List<string> GetDifferentStations()
 {
    return db.Queryable<WeatherData>().Select(x => x.StationName ?? "").Distinct().ToList();
 }
 public List<WeatherData> GetDataByCondition(Condition condition)
 {
     return db.Queryable<WeatherData>()
              .Where(x => x.Date >= condition.StartDate &&
                          x.Date < condition.EndDate.AddDays(1) &&
                          x.StationName == condition.StationName).ToList();
 }

這裡資料庫使用的是 SQLite,ORM 使用的是 SQLSugar,具體怎麼設定,在這裡我就不詳細說明了,可以查看官網也可以查看歷史文章。

4、繪圖的實作

程式碼如下:

async void Painting_Clicked()
{
    if (Date1 != null && Date2 != null && value != null && selectedValues != null)
    {
        if(Data1?.Length > 0)
        {
            Data1 = new object[0];
        }
        if (plotDatas.Count > 0)
        {
            plotDatas.Clear();
        }
        var cofig = new MessageConfig()
            {
                Content = "正在繪圖中...",
                Duration = 0
            };
        var task = _message.Loading(cofig);
        var condition = new Condition();
        condition.StartDate = (DateTime)Date1;
        condition.EndDate = (DateTime)Date2;
        condition.StationName = value;
        for(int i = 0;i < selectedValues.Length;i ++)
        {
            switch (selectedValues[i])
            {
                case "Tem_Low":
                    var result1 = weatherServer.GetDataByCondition(condition).Select(x => new PlotData
                    {
                        Date = x.Date,
                        Type = "Tem_Low",
                        Value = Convert.ToDouble(x.Tem_Low)
                    }).ToList();
                    plotDatas.AddRange(result1);
                    break;
                case "Tem_High":
                    var result2 = weatherServer.GetDataByCondition(condition).Select(x => new PlotData
                        {
                            Date = x.Date,
                            Type = "Tem_High",
                            Value = Convert.ToDouble(x.Tem_High)
                        }).ToList();
                    plotDatas.AddRange(result2);
                    break;
                case "Visibility_Low":
                    var result3 = weatherServer.GetDataByCondition(condition).Select(x => new PlotData
                        {
                            Date = x.Date,
                            Type = "Visibility_Low",
                            Value = Convert.ToDouble(x.Visibility_Low)
                        }).ToList();
                    plotDatas.AddRange(result3);
                    break;
                case "Visibility_High":
                    var result4 = weatherServer.GetDataByCondition(condition).Select(x => new PlotData
                        {
                            Date = x.Date,
                            Type = "Visibility_High",
                            Value = Convert.ToDouble(x.Visibility_High)
                        }).ToList();
                    plotDatas.AddRange(result4);
                    break;

            }
        }

        // 將自訂型別的陣列投影為 object[] 型別的陣列
        Data1 = plotDatas.Select(p => new { date = p.Date, type = p.Type, value = p.Value }).ToArray();

        // 更新圖表資料
        await lineChartRef.ChangeData(Data1);
        task.Start();
    }
    else
    {
        await _message.Error("請檢查開始日期、結束日期、站名與繪圖指標是否都已選擇!!!");
    }
}

在 AntDesign.Charts 中畫多條折線圖,官網位置如下所示:

圖片

建立一個自訂的繪圖資料類別:

public class PlotData
 {
     public DateTime? Date {  get; set; }
     public string? Type { get; set; }
     public double Value { get; set; }
 }

然後建一個繪圖資料類別的清單:

List<PlotData> plotDatas = new List<PlotData>();

建立一個自訂的條件類別:

 public class Condition
  {
      public DateTime StartDate{ get; set; }
      public DateTime EndDate { get; set; }
      public string? StationName { get; set; }
  }

然後在我點擊的時候,如果各項不為空,那麼建立一個條件物件:

var condition = new Condition();
condition.StartDate = (DateTime)Date1;
condition.EndDate = (DateTime)Date2;
condition.StationName = value;

該物件包含了我們選擇的開始時間、結束時間與站名。

然後遍歷 selectedValues:

for(int i = 0;i < selectedValues.Length;i ++)

selectedValues 是 string[]? 型別。

string[]? selectedValues;

表示的是複選框中選中的值。

static CheckboxOption[] ckeckAllOptions = new CheckboxOption[]{
     new CheckboxOption{ Label="最低溫度(℃)",Value="Tem_Low" },
     new CheckboxOption{ Label="最高溫度(℃)", Value="Tem_High" },
     new CheckboxOption{ Label="最低可見度(km)", Value="Visibility_Low"},
     new CheckboxOption{ Label="最高可見度(km)", Value="Visibility_High" },
 };

選擇的 Label 都有對應的 value。

 switch (selectedValues[i])
 {
     case "Tem_Low":
         var result1 = weatherServer.GetDataByCondition(condition).Select(x => new PlotData
         {
             Date = x.Date,
             Type = "Tem_Low",
             Value = Convert.ToDouble(x.Tem_Low)
         }).ToList();
         plotDatas.AddRange(result1);
         break;
     case "Tem_High":
         var result2 = weatherServer.GetDataByCondition(condition).Select(x => new PlotData
             {
                 Date = x.Date,
                 Type = "Tem_High",
                 Value = Convert.ToDouble(x.Tem_High)
             }).ToList();
         plotDatas.AddRange(result2);
         break;
     case "Visibility_Low":
         var result3 = weatherServer.GetDataByCondition(condition).Select(x => new PlotData
             {
                 Date = x.Date,
                 Type = "Visibility_Low",
                 Value = Convert.ToDouble(x.Visibility_Low)
             }).ToList();
         plotDatas.AddRange(result3);
         break;
     case "Visibility_High":
         var result4 = weatherServer.GetDataByCondition(condition).Select(x => new PlotData
             {
                 Date = x.Date,
                 Type = "Visibility_High",
                 Value = Convert.ToDouble(x.Visibility_High)
             }).ToList();
         plotDatas.AddRange(result4);
         break;
 }

如果值為Tem_Low,那麼我們的繪圖資料就是:

var result2 = weatherServer.GetDataByCondition(condition).Select(x => new PlotData
             {
                 Date = x.Date,
                 Type = "Tem_High",
                 Value = Convert.ToDouble(x.Tem_High)
             }).ToList();

這裡首先 weatherServer.GetDataByCondition(condition) 的實作如下:

 public List<WeatherData> GetDataByCondition(Condition condition)
 {
     return dataService.GetDataByCondition(condition);
 }

而 dataService.GetDataByCondition(condition) 的實作如下:

 public List<WeatherData> GetDataByCondition(Condition condition)
 {
     return db.Queryable<WeatherData>()
              .Where(x => x.Date >= condition.StartDate &&
                          x.Date < condition.EndDate.AddDays(1) &&
                          x.StationName ==                condition.StationName).ToList();
 }

最終獲得了滿足日期與站名要求的 List<WeatherData>,然後再使用 Select 方法構造 PlotData 物件:

Select(x => new PlotData
             {
                 Date = x.Date,
                 Type = "Tem_High",
                 Value = Convert.ToDouble(x.Tem_High)
             }).ToList();

然後加入到 plotDatas 中:

plotDatas.AddRange(result1);

這樣遍歷完 selectedValues 之後就得到了我們所有需要的繪圖資料,選中了幾項就有幾項,然後需要對應到 object[] 型別的陣列:

object[]? Data1;
 // 將自訂型別的陣列投影為 object[] 型別的陣列
 Data1 = plotDatas.Select(p => new { date = p.Date, type = p.Type, value = p.Value }).ToArray();

這裡我也很困惑,Ant Design Charts Blazor 的 AntLineChart 等元件通常使用 object[] 型別的陣列作為圖表的資料來源。這是因為 JavaScript 本身是一種弱型別語言,而 Blazor 透過 JavaScript Interop 進行與 JavaScript 的通訊,這是 ChatGPT 的解釋,大家可以參考一下。

然後更新圖表:

// 更新圖表資料
await lineChartRef.ChangeData(Data1);

繪圖的設定:

LineConfig Config1 = new LineConfig
{
    Padding = "auto",
    XField = "date",
    YField = "value",
    SeriesField = "type",
    Smooth = true
};

然後就可以實現繪圖了。

小結

這是我第一次嘗試使用 WinForms Blazor Hybrid 寫一個小案例,Blazor Hybrid 也才剛開始了解,不足之處,請各位多多包涵,最後希望對你有所幫助。

繼續探索

延伸閱讀

更多文章
同分類 / 同標籤 2024/2/29

Winform中也可以這樣做資料展示

在做winform開發的過程中,經常需要做資料展示的功能,之前一直使用的是gridcontrol控制項,今天想透過一個範例,跟大家介紹一下如何在winform blazor hybrid中使用ant design blazor中的table元件做資料展示。

繼續閱讀