在 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;
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
};
然後就可以實現繪圖了。
小結
這是我第一次嘗試使用 winform blazor hybrid 寫一個小案例,blazor hybrid 也才剛開始了解,不足之處,請各位多多包涵,最後希望對你有所幫助。