Winformの画面も綺麗にできる?

Winformの画面も綺麗にできる?

先日、winformでblazor hybridを使用することを紹介しました。また、blazorのUIを組み合わせることでwinformプログラムのデザインをより美しくできると言いました。今回はwinform blazor hybridで描画する例を挙げて説明します。参考になれば幸いです。

最終更新 2024/02/29 5:29
DotNet学习交流
読了目安 9 分
カテゴリ
Winform Blazor
タグ
.NET C# Blazor Winform 混合アプリケーション

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 をクリックすると、クリックイベントが発生します。クリックイベントでは、ラムダ式を使って同じメソッドを呼び出していますが、引数が異なります。

では、このメソッドを見てみましょう。

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 はコンポーネントの初期化時に何らかのロジックを実行するためのライフサイクルメソッドです。具体的には、Microsoft.AspNetCore.Components.ComponentBase クラスで定義された仮想メソッドであり、派生コンポーネントでオーバーライドしてコンポーネント初期化時にカスタム操作を実行できます。

ここでは、UI 層、ビジネスロジック層、データベースアクセス層という3層アーキテクチャを採用しています。

weatherServer は私がカスタムしたサービスです。このサービスを使用するには、先頭に次のステートメントを追加する必要があります。

@inject IWeatherServer weatherServer;

Blazor では、@inject は Razor ページまたはコンポーネントにサービスを注入するためのディレクティブです。@inject を使用すると、依存性注入サービスを Blazor ページやコンポーネントに導入し、それらのサービスを使用できるようになります。

もちろん、サービスを使用するには事前にサービスを登録する必要があります。

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

ここでは、ビジネスロジック層のサービスとデータアクセス層のサービスがあります。

IWeatherServer はビジネスロジック層のインターフェースです。インターフェースを使用する利点については、以下の点を参考にしてください。

多重継承の実現:

C# のクラスは単一継承しかサポートしていませんが、1つのクラスで複数のインターフェースを実装できます。インターフェースは、クラスが異なる次元で機能を取得および実装する方法を提供します。1つのクラスは複数のインターフェースを実装でき、各インターフェースで定義された一連のメンバーを持つことができます。

仕様の実現:

インターフェースは一連の仕様を定義し、実装クラスに特定のメンバーを提供することを要求します。これにより、実装クラスが特定のプログラミング仕様と標準に従うことが強制され、コードの一貫性と可読性が向上します。

抽象化と柔軟性の提供:

インターフェース自体は具体的な実装を提供せず、メンバーの契約のみを定義します。これにより、インターフェースは強力な抽象化ツールとなり、具体的な実装を公開せずにクラスの機能を記述できます。

また、インターフェースはクラスの動作を拡張および変更する方法を提供し、クラス自体の実装を変更する必要はありません。

依存性注入の実現:

インターフェースと依存性注入を組み合わせることで、アプリケーションでの代替可能性とテスト容易性を実現しやすくなります。依存性注入フレームワークを使用すると、実行時に異なる実装を注入でき、モジュール間の低結合性を実現できます。

共通契約の定義:

インターフェースは共通契約を定義する方法を提供し、複数の実装がシステム内で連携して動作できるようにします。これは、プラグインシステム、拡張性、モジュラー設計に非常に役立ちます。

ポリモーフィズムの許可:

インターフェースを使用すると、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 ++)

selectedValuesstring[]? 型です。

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 についてもまだ学び始めたばかりで、至らない点があればご容赦ください。最後に、この記事が皆様のお役に立てれば幸いです。

さらに探索

関連読書

その他の記事
同じカテゴリ / 同じタグ 2024/02/29

Winformでもこんなデータ表示ができる

winform開発の過程で、データ表示機能が必要になることがよくあります。これまではgridcontrolコントロールを使用していましたが、今日は例を通して、winform blazor hybridでant design blazorのtableコンポーネントを使ってデータ表示を行う方法を紹介します。

続きを読む