WinForms

【C#/WinForms実践入門編(11)】AIアシスタントアプリ ~WinForms+BlazorハイブリッドでAIチャットを作る!~【Azure OpenAI活用】

今回のテーマは「WinForms+BlazorハイブリッドによるAIチャットアプリの開発」です。

前回学んだAzure OpenAIの基盤と、以前学んだWinForms+Blazorハイブリッドの知識を組み合わせて、モダンなUIを持つAIチャットアプリ(Windowsアプリ)を作成します。

この記事では以下の内容を説明します。

  • 「Azure OpenAI」と「WinForms+Blazorハイブリッド」の概要復習
  • BlazorベースのチャットUIの実装方法
  • Azure OpenAIをサービスクラスとしてモジュール化する方法

次のような方に役立つ内容となっています。

  • モダンなUIを持つWindows(WinForms)アプリを開発したい方
  • BlazorとWinFormsを組み合わせたWebハイブリッド開発に興味がある方
  • Azure OpenAIでAIチャットアプリを作りたい方

前回はコンソールアプリでAzure OpenAIの基本的な使い方を学びました。

今回はそれを発展させ、WinForms+Blazorハイブリッドアプローチでよりリッチなユーザーインターフェースを持つチャットアプリを作成します。

プロ太

Windowsアプリなので、将来的にはローカルファイルへのアクセスを行ってRAG(検索拡張生成)を実現するなどの応用も可能な土台になります。

演習のコード一式はGitHubに置いてあります。

以下に動画もあります。

講義:Azure OpenAIとBlazorハイブリッドの復習

Azure OpenAIの基本

前回はAzure OpenAI Serviceの基本概念と、C#コンソールアプリからの利用方法について学びました

前回、以下のようなことを学びました。

  • Azure OpenAI Serviceは、Microsoft Azureが提供するクラウドベースのAIサービスで、OpenAIの言語モデルをAzure上で利用できる
  • C#からAzure OpenAIを使うには、Azure.AI.OpenAIパッケージを利用し、APIキー等を設定してクライアントを初期化する
  • チャット履歴を管理しながら対話を行う基本的な仕組みを実装した

詳しくは、以下の記事を参考にしてください。

【C#/WinForms実践入門編(10)】AIアシスタントアプリ ~Azure OpenAI Serviceを使う!~ 今回のテーマは「AIアシスタントアプリの開発」です。Azure OpenAIを使って、テキスト生成やデータ分析を行うWinFormsア...

この基礎知識を踏まえて、今回はより洗練されたUIを持つアプリへと発展させます。

WinFormsにおけるBlazorハイブリッド

今回、WinForms+Blazorハイブリッドを活用して、モダンなWebベースのUIをWindows(WinForms)アプリ内に統合します。

プロ美

おお、WinFormsアプリだけど、モダンな雰囲気のUI!

プロ太

WinForms単独でこのようなUIを作るのはかなり手間がかかるのですが、Blazorハイブリッドを使うと比較的簡単につくれます!

Blazorハイブリッドには以下の特長があります。

  • Webとデスクトップのいいとこ取り:Webの美しいUIとWinFormsのデスクトップアプリの使いやすさを組み合わせられる
  • 既存のWeb知識を活用:HTML、CSS、Bootstrapなどの既存のWeb技術をデスクトップアプリ開発に活用できる
  • モダンな開発体験:依存性注入(DI)やコンポーネント指向開発など、最新の開発アプローチをWinFormsアプリに導入できる

詳しくは以下の記事を参考にしてください。今回は、この記事の演習2(BlazorハイブリッドWinFormsアプリ)のコードをベースとして作ります。

【C#/WinForms実践入門編(8)】WebハイブリッドWindowsアプリを作る!~WebView2・Blazorハイブリッドとは?~ 今回は、Windows Forms(WinForms)で「WebハイブリッドWindowsアプリ」を作成する方法を紹介します。 ...
プロ太

それでは、実際にWinForms+BlazorハイブリッドでAIチャットアプリを作ってみましょう!

演習:WinForms+BlazorハイブリッドでAIチャットアプリを作る

今回作るアプリと作成手順

以下の手順で進めます。Azure環境やモデルについては前回構築したものをそのまま使います。

  • 手順1:WinForms+Blazorハイブリッドプロジェクトの作成
  • 手順2:Azure OpenAI用のサービスクラスの作成
  • 手順3:BlazorでのチャットUIの実装
  • 手順4:メインフォームの実装
  • 手順5:環境変数の設定

手順1:WinForms+Blazorハイブリッドプロジェクトの作成

今回は演習8-2のコードをベースとします。プロジェクトの作り方やBlazorハイブリッドアプリ構築方法は「【C#/WinForms実践入門編(8)】の演習2」を参考にしてください。

以下のように、Form1.csをリネームしてFormsフォルダ配下に移動し、少しフォルダを整理しています。

手順2~4で以下のようにコードを作成します。

手順2:Azure OpenAI用のサービスクラスの作成

前回の演習10-1のコードを元にチャットサービスクラスを作成します。ICharService.csで以下のようにチャットサービスクラスのインターフェイスを定義します。

namespace WinFormsBlazorApp.Services
{
    internal interface IChatService
    {
        Task<string> SendMessageAsync(string userInput);
        void ClearHistory();
    }
}

そして、IChatServiceの具体的な実装をAzureOpenAIChatService.csで以下のように実装します。

using Azure.AI.OpenAI;
using Azure;
using OpenAI.Chat;

namespace WinFormsBlazorApp.Services
{
    internal class AzureOpenAIChatService : IChatService
    {
        private readonly ChatClient _chatClient;
        private readonly List<ChatMessage> _conversationHistory;

        public AzureOpenAIChatService(string endpoint, string apiKey, string deploymentName)
        {
            // Azure OpenAI クライアントの初期化
            var azureClient = new AzureOpenAIClient(
                new Uri(endpoint),
                new AzureKeyCredential(apiKey));

            // チャットクライアントの取得
            _chatClient = azureClient.GetChatClient(deploymentName);
            _conversationHistory = new List<ChatMessage>();
        }

        public async Task<string> SendMessageAsync(string userInput)
        {
            if (string.IsNullOrEmpty(userInput))
                throw new ArgumentNullException(nameof(userInput), "メッセージが空です。");

            // ユーザーメッセージを履歴に追加
            _conversationHistory.Add(new UserChatMessage(userInput));

            // チャット完了リクエスト
            ChatCompletion completion = await _chatClient.CompleteChatAsync(_conversationHistory);

            // アシスタントの応答を履歴に追加
            _conversationHistory.Add(new AssistantChatMessage(completion));

            // レスポンステキストを返す
            return completion.Content[0].Text;
        }

        public void ClearHistory()
        {
            _conversationHistory.Clear();
        }
    }
}

内部でCompleteChatAsyncという非同期メソッドを使うことで、SendMessageAsyncを非同期メソッドとし、Azure OpenAIの返答待ちの間でもUIの応答性を維持します。

非同期処理の基本については以下の記事も参考にしてください。

C#入門編(17)非同期処理(async, await, Task) ~複数の処理を並行して実行~ 今回は「非同期処理」について解説します。 非同期処理は、複数の処理を同時並行で効率的に実行するための仕組みです。 実は、こ...

インターフェイスを使うと、あとから別のAIチャット実装(例:ClaudeAIChatService)などを作って、具体的なAIチャット実装を入れ替えることも容易になります。

C#におけるインターフェイスの使い方の基本については、以下の記事も参考にしてください。

C#入門編(12)オブジェクト指向とは?「インターフェイス」 ~さまざまなクラスを一貫した方法でJSON出力する~ 今回は「インターフェイス」について解説します。 インターフェイスとは、関連性のないクラス間で共通の振る舞いを定義し、違うクラスを...
プロ太

将来実装を交換する可能性がない場合には、インターフェイスを使わず直接「AzureOpenAIChatService」を使うのもありです。

手順3:BlazorでのチャットUIの実装

Char.razorを以下のように実装します。

@using WinFormsBlazorApp.Services
@inject IChatService ChatService

<div class="container-fluid d-flex flex-column vh-100 p-3">
    <h3>シンプルAIチャット</h3>
    <div class="border rounded p-3 mb-3 flex-grow-1" style="overflow-y: auto;">
        @foreach (var message in chatHistory)
        {
            <div class="@(message.IsUser ? "text-end" : "text-start") mb-2">
                <span class="badge @(message.IsUser ? "bg-primary" : "bg-secondary") p-2 text-wrap"
                      style="max-width: 80%; display: inline-block; text-align: left;">
                    @if (message.IsUser)
                    {
                        @message.Text
                    }
                    else
                    {
                        @((MarkupString)message.Text.Replace("\n", "<br>"))
                    }
                </span>
            </div>
        }
    </div>
    <div class="input-group">
        <input type="text" class="form-control" placeholder="メッセージを入力..."
               @bind="currentMessage" @bind:event="oninput" @onkeypress="@(async e => { if (e.Key == "Enter") await SendMessageAsync(); })" />
        <button class="btn btn-primary" @onclick="SendMessageAsync">送信</button>
    </div>
</div>
@code {
    private List<ChatMessage> chatHistory = new List<ChatMessage>();
    private string currentMessage = string.Empty;
    private async Task SendMessageAsync()
    {
        if (string.IsNullOrWhiteSpace(currentMessage))
            return;

        string userMessage = currentMessage;
        currentMessage = string.Empty;

        // ユーザーメッセージをチャット履歴に追加
        chatHistory.Add(new ChatMessage { Text = userMessage, IsUser = true });

        // ChatServiceを使用して応答を取得
        var response = await ChatService.SendMessageAsync(userMessage);

        // ボットの応答をチャット履歴に追加
        chatHistory.Add(new ChatMessage { Text = response, IsUser = false });
    }

    // シンプルなチャットメッセージのモデルクラス
    private class ChatMessage
    {
        public string Text { get; set; } = string.Empty;
        public bool IsUser { get; set; }
    }
}

このコードの要点は以下になります。

  • このコードはBlazorで作られたチャット画面で、ユーザメッセージ(右側・青色)とAI応答(左側・灰色)を表示します。
  • テキスト入力とボタン(または Enter キー)でメッセージを送信し、IChatServiceがAI応答を生成します。
  • 全てのメッセージはchatHistoryリストに保存され、会話の履歴として画面に表示されます。
  • ChatMessageクラスでメッセージのテキストと送信者情報(ユーザかAIか)を管理しています。

BlazorやRazorコンポーネントの基礎については、Webアプリ開発編(Blazorアプリ開発)を参考にしてください。

手順4:メインフォームの実装

以下のようにMainForm.csへ追記します。

using Microsoft.AspNetCore.Components.WebView.WindowsForms;
using Microsoft.Extensions.DependencyInjection;
using WinFormsBlazorApp.Pages;
using WinFormsBlazorApp.Services;

namespace WinFormsBlazorApp.Forms
{
    public partial class MainForm : Form
    {
        public MainForm()
        {
            InitializeComponent();
            var services = new ServiceCollection();
            services.AddWindowsFormsBlazorWebView();
            services.AddScoped<IChatService, AzureOpenAIChatService>(
                serviceProvider =>
                {
                    var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT");
                    var apiKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY");
                    var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME");
                    if (string.IsNullOrEmpty(endpoint) || string.IsNullOrEmpty(apiKey) || string.IsNullOrEmpty(deploymentName))
                    {
                        throw new InvalidOperationException("Azure OpenAI configuration is missing.");
                    }
                    return new AzureOpenAIChatService(endpoint, apiKey, deploymentName);
                });
            blazorWebView1.HostPage = "wwwroot\\index.html";
            blazorWebView1.Services = services.BuildServiceProvider();
            blazorWebView1.RootComponents.Add<Chat>("#app");
        }
    }
}

今回追加したコードの要点は以下です。

  • 環境変数からAzure OpenAIの設定(エンドポイント、APIキー、デプロイメント名)を取得しています
  • IChatServiceインターフェースの実装としてAzureOpenAIChatServiceを依存性注入(DI)コンテナに登録しています
  • blazorWebViewコントロールを設定し、Blazorの「Chat」コンポーネントをアプリケーションのルートとしてマウントしています

手順5:環境変数の設定

最後に環境変数を設定しましょう。これは前回の演習と同様です。「デバッグ>【プロジェクト名】」のプロパティで、以下のように設定します。

プロ太

これでできました!

アプリを実行

アプリをデバッグ実行してみましょう。

プロ美

モダンなUIのAIチャットアプリ(Windowsアプリ)ができた!

プロ太

Windowsアプリなので、ローカルファイルアクセス、通知領域(タスクトレイ)に表示、通知機能を使うなどもこれに組み合わせられますよ!

WinFormsでWindowsの機能を最大限使いつつ、WebのモダンなUIを使えるというのが嬉しい点ですね。

WinFormsの通知領域(タスクトレイ)活用は以下でも紹介しています。

【C#/WinForms実践入門編(9)】通知領域(タスクトレイ) ~バックグラウンド実行可能なタイマーアプリへ~ 今回のテーマは「通知領域(タスクトレイ)」です。アプリをバックグラウンドで実行させ、ユーザに通知を送る機能を実装します。 この記...

まとめ

本記事では、WinForms+Blazorハイブリッドを使ったAIチャットアプリの開発について学びました。

Blazorハイブリッドにより、WinFormsだけでは作成が難しいモダンなWebベースのUIを実現できます。

演習を通じて、以下のポイントを学びました。

  • Azure OpenAIをサービスクラスとしてモジュール化する方法
  • BlazorベースのチャットUIをWinFormsアプリ内に統合する方法
  • 依存性注入(DI)を使った柔軟なサービス登録と利用方法

このようなハイブリッド設計は、将来的にローカルファイルアクセスによるRAG(検索拡張生成)実装や、通知領域機能との連携など、Windowsアプリならではの拡張も可能です。

次回からさらにこのAIチャットを改良したり、何か面白い機能を追加したりと拡張していきます。

プロ太

引き続き、一緒にC# WinFormsアプリ開発を学んでいきましょう!

ABOUT ME
プロ太
●仕事:現在は個人事業主(メンター・情報発信等)、大手IT企業で技術者・マネージャ(15年以上)、大学の外部講師、学生時代は学習塾で非常勤講師(約4年間) ●博士(工学)の学位取得 ●高校生の頃に独学で始め、プログラミング歴20年以上 ●言語:C# 、Java、C/C++、Python、JavaScript/TypeScript等 ●分野:Webアプリ、テスト自動化、生成AI、デバッガ、コード解析、ドメイン特化言語

ご依頼・ご相談について

プログラミング学習のご相談、お仕事のご依頼については、
こちらのお問い合わせページをご確認ください。