今回のテーマは「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キー等を設定してクライアントを初期化する - チャット履歴を管理しながら対話を行う基本的な仕組みを実装した
詳しくは、以下の記事を参考にしてください。

この基礎知識を踏まえて、今回はより洗練された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アプリ)のコードをベースとして作ります。

それでは、実際に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の応答性を維持します。
非同期処理の基本については以下の記事も参考にしてください。

インターフェイスを使うと、あとから別のAIチャット実装(例:ClaudeAIChatService)などを作って、具体的なAIチャット実装を入れ替えることも容易になります。
C#におけるインターフェイスの使い方の基本については、以下の記事も参考にしてください。

将来実装を交換する可能性がない場合には、インターフェイスを使わず直接「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の通知領域(タスクトレイ)活用は以下でも紹介しています。

まとめ
本記事では、WinForms+Blazorハイブリッドを使ったAIチャットアプリの開発について学びました。
Blazorハイブリッドにより、WinFormsだけでは作成が難しいモダンなWebベースのUIを実現できます。
演習を通じて、以下のポイントを学びました。
- Azure OpenAIをサービスクラスとしてモジュール化する方法
- BlazorベースのチャットUIをWinFormsアプリ内に統合する方法
- 依存性注入(DI)を使った柔軟なサービス登録と利用方法
このようなハイブリッド設計は、将来的にローカルファイルアクセスによるRAG(検索拡張生成)実装や、通知領域機能との連携など、Windowsアプリならではの拡張も可能です。
次回からさらにこのAIチャットを改良したり、何か面白い機能を追加したりと拡張していきます。
引き続き、一緒にC# WinFormsアプリ開発を学んでいきましょう!