當前位置: 妍妍網 > 碼農

winform的界面也可以變好看?

2024-02-29碼農

在winform blazor hybrid中繪圖

前幾天跟大家介紹了在winform中使用blazor hybrid,而且還說配上blazor的ui可以讓我們的winform程式設計的更加好看,接下來我想以一個在winform 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>

winform blazor hybrid計畫在wwwroot/index.html中引入,如下所示:

這裏我也把AntDesign.Charts的引入了。

_Imports.razor 中加入名稱空間:

@using AntDesign

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

這是官網的說法,在winform blazor hybrid中可以在當做主頁面的razor中添加,我這裏是Index.razor如下所示:

現在就可以使用ant design blazor的元件了。

2、頁面設計

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

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

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

每一個MenuItem都有一個Key內容,如下所示:

在這裏每一個Key都是唯一的。點選不同的MenuItem都會觸發點選事件,而點選事件使用了lambda運算式呼叫了同一個方法,但是參數不同。

現在來看看這個方法:

int selectedMenuItem = 1;
privatevoidNavigateToContent(int menuItemNumber)
 {
selectedMenuItem = menuItemNumber;
 }

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

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

<Content class="site-layout-background" >"margin: 24px 16px;padding: 24px;min-height: 450px;">
@switch(selectedMenuItem)
 {
case1:
<GetData></GetData>
break;
case2:
<QueryData></QueryData>
break;
case3:
<Painting></Painting>
break;
case4:
<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"
>"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"
>"width:150px"/>

List<string> options = new List<string>();
protectedoverridevoidOnInitialized()
{
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<stringGetDifferentStations();
public List<WeatherData> GetDataByCondition(Condition condition);

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

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

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

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

public List<stringGetDifferentStations()
 {
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、繪圖的實作

程式碼如下:

asyncvoidPainting_Clicked()
{
if (Date1 != null && Date2 != null && value != null && selectedValues != null)
{
if(Data1?.Length > 0)
{
Data1 = newobject[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 classPlotData
 {
public DateTime? Date { getset; }
publicstring? Type { getset; }
publicdouble Value { getset; }
 }

然後建一個畫圖數據類的列表:

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

建立一個自訂的條件類:

public classCondition
{
public DateTime StartDate{ getset; }
public DateTime EndDate { getset; }
publicstring? StationName { getset; }
}

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

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

然後就可以實作繪圖了。

小結

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