WinFormと聞くと、多くのプログラマーが懐かしさを感じることでしょう。デスクトップアプリケーション開発で初めて触れる技術の一つであり、まさに「旧友」と呼べる存在です。現在、主流の技術コミュニティではあまり話題にならないものの、一部のエンタープライズプロジェクトや社内ツールなど、いまだに重要な役割を担っています。特にレガシーシステムでは、その安定性、シンプルさ、メンテナンス性の高さから重宝されています。
一方、AIは今やテクノロジー業界の「主役」となりました。ここ2~3年、AIへの関心は爆発的に高まりました。ChatGPTや画像生成ツール、スマートアシスタントなど、さまざまな“AI神ツール”が次々と登場しています。エンジニアだけでなく、一般の方々まで「AIってすごいね」と話題にするほどです。
「ベテラン」と「新鋭」──WinFormとAI、一見ミスマッチに思えるこの組み合わせも、実は非常に面白い取り組みです。最近、私もWinFormでシンプルなAIツールを作ってみたのですが、旧技術と新技術をうまく組み合わせることで、想像以上に便利なものが出来上がりました。
今後何回かの記事で、「古参」WinFormがAIの“急行列車”に乗る方法をご紹介していきます。安定した基盤を生かしつつ、AIによる“ひらめき”を体験しましょう。
ChatはAIが最も得意とする分野なので、まずはChat機能から取り上げます。
AI技術スタック:Ollama+Phi4-mini
-
Ollamaのインストール
Windows版Ollamaを https://ollama.com/download からダウンロードしてインストールします。 -
モデルの検索
https://ollama.com/search でphi4-miniモデルを検索します。 -
モデルの取得
Windowsの「PowerShell」や「コマンドプロンプト」で、下記コマンドを実行してphi4-miniモデルを取得します。ollama pull phi4-mini
Ollamaには他にも多くのコマンドがありますので、必要に応じて学んでください。
WinFormへの組み込み
次に、WinForm内でカスタムユーザーコントロールを作成します。完成イメージは以下の通りです。
SemanticKernelを導入し、AI機能を利用します。SemanticKernelについては過去の記事もご参照ください。
まずは.csprojファイルの内容を見てみましょう。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
<PackageReference Include="Microsoft.SemanticKernel" Version="1.58.0" />
<PackageReference Include="Microsoft.SemanticKernel.Connectors.Ollama" Version="1.58.0-alpha" />
</ItemGroup>
</Project>
続いて、ユーザーコントロールのレイアウト(AIChat.Designer.cs)に対応するC#コードです。
namespace SmartWinForms
{
partial class AIChat
{
/// <summary>
/// 必要なデザイナ変数。
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// リソースの破棄処理
/// </summary>
/// <param name="disposing">マネージドリソースを解放する場合はtrue、それ以外はfalse。</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region コンポーネント デザイナで生成されたコード
/// <summary>
/// デザイナ サポートに必要なメソッド
/// コードエディタでこのメソッドの内容を変更しないでください。
/// </summary>
private void InitializeComponent()
{
splitContainer1 = new SplitContainer();
historyTB = new RichTextBox();
chatTB = new RichTextBox();
bottomPan = new Panel();
sendBut = new Button();
((System.ComponentModel.ISupportInitialize)splitContainer1).BeginInit();
splitContainer1.Panel1.SuspendLayout();
splitContainer1.Panel2.SuspendLayout();
splitContainer1.SuspendLayout();
bottomPan.SuspendLayout();
SuspendLayout();
//
// splitContainer1
//
splitContainer1.Dock = DockStyle.Fill;
splitContainer1.Location = new Point(0, 0);
splitContainer1.Name = "splitContainer1";
splitContainer1.Orientation = Orientation.Horizontal;
//
// splitContainer1.Panel1
//
splitContainer1.Panel1.Controls.Add(historyTB);
//
// splitContainer1.Panel2
//
splitContainer1.Panel2.Controls.Add(chatTB);
splitContainer1.Panel2.Controls.Add(bottomPan);
splitContainer1.Size = new Size(1180, 870);
splitContainer1.SplitterDistance = 510;
splitContainer1.TabIndex = 0;
//
// historyTB
//
historyTB.Dock = DockStyle.Fill;
historyTB.Location = new Point(0, 0);
historyTB.Name = "historyTB";
historyTB.ReadOnly = true;
historyTB.Size = new Size(1180, 510);
historyTB.TabIndex = 0;
historyTB.Text = "";
//
// chatTB
//
chatTB.Dock = DockStyle.Fill;
chatTB.Location = new Point(0, 0);
chatTB.Name = "chatTB";
chatTB.Size = new Size(1180, 281);
chatTB.TabIndex = 1;
chatTB.Text = "";
chatTB.KeyPress += chatTB_KeyPress;
//
// bottomPan
//
bottomPan.Controls.Add(sendBut);
bottomPan.Dock = DockStyle.Bottom;
bottomPan.Location = new Point(0, 281);
bottomPan.Name = "bottomPan";
bottomPan.Padding = new Padding(5);
bottomPan.Size = new Size(1180, 75);
bottomPan.TabIndex = 0;
//
// sendBut
//
sendBut.Dock = DockStyle.Right;
sendBut.Location = new Point(1011, 5);
sendBut.Name = "sendBut";
sendBut.Size = new Size(164, 65);
sendBut.TabIndex = 0;
sendBut.Text = "送信";
sendBut.UseVisualStyleBackColor = true;
sendBut.Click += sendBut_Click;
//
// AIChat
//
AutoScaleDimensions = new SizeF(11F, 24F);
AutoScaleMode = AutoScaleMode.Font;
Controls.Add(splitContainer1);
Name = "AIChat";
Size = new Size(1180, 870);
splitContainer1.Panel1.ResumeLayout(false);
splitContainer1.Panel2.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)splitContainer1).EndInit();
splitContainer1.ResumeLayout(false);
bottomPan.ResumeLayout(false);
ResumeLayout(false);
}
#endregion
private SplitContainer splitContainer1;
private RichTextBox historyTB;
private RichTextBox chatTB;
private Panel bottomPan;
private Button sendBut;
}
}
次に、AIの機能を呼び出す部分(AIChat.cs)のソースです。
using Microsoft.Extensions.DependencyInjection;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.Ollama;
using OllamaSharp;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
#pragma warning disable
namespace SmartWinForms
{
public partial class AIChat : UserControl
{
private readonly IChatCompletionService _chatService;
private readonly ChatHistory _history;
public AIChat()
{
InitializeComponent();
var ollamaApiClient = new OllamaApiClient(new Uri("http://localhost:11434"), DefaultModelId);
var builder = Kernel.CreateBuilder();
builder.Services.AddScoped<IChatCompletionService>(_ => ollamaApiClient.AsChatCompletionService());
var kernel = builder.Build();
_chatService = kernel.GetRequiredService<IChatCompletionService>();
_history = new ChatHistory();
_history.AddSystemMessage(SystemPrompt);
}
[Category("AIプロパティ")]
[Description("システムプロンプト")]
public string SystemPrompt
{
get;
set;
} = "あなたはAIアシスタントです。簡潔に回答してください。";
[Category("AIプロパティ")]
[Description("モデルID")]
public string DefaultModelId
{
get;
set;
} = "phi4-mini";
private async void sendBut_Click(object sender, EventArgs e)
{
if (string.IsNullOrWhiteSpace(chatTB.Text))
{
MessageBox.Show("内容を入力してください。");
return;
}
var input = chatTB.Text;
var response = _chatService.GetStreamingChatMessageContentsAsync(input);
var content = "";
var role = AuthorRole.Assistant;
_history.AddUserMessage(input);
historyTB.Text += $"ユーザー:\n{_history.Last().Content}";
historyTB.Text += $"AIアシスタント:\n";
historyTB.Text = historyTB.Text.Trim();
Task.Run(async () =>
{
await foreach (var message in response)
{
this.Invoke(() =>
{
historyTB.Text += $"{message.Content}";
});
content += message.Content;
role = message.Role.Value;
}
});
historyTB.Text += $"\n";
chatTB.Text = string.Empty;
_history.AddMessage(role, content);
}
private void chatTB_KeyPress(object sender, KeyPressEventArgs e)
{
if (e.KeyChar == (char)Keys.Enter)
{
e.Handled = true;
sendBut.PerformClick();
}
}
}
}
ユーザーコントロールを利用するには、新しいフォームを作成し、AIChatをドラッグ&ドロップで追加するだけです。
なお、このユーザーコントロールはollamaとの連携をシンプルにラップしたものなので、現時点では公開されているプロパティは少なめです。もし他のLLMやAIモデルと連携したい場合は、コントロールのプロパティを拡張してカスタマイズ可能です。
最後に、実際の動作結果をお見せします。
(※動画プレイヤー部分は省略)
(Translated by GPT)
元のリンク:https://mp.weixin.qq.com/s/kjPiohaSqfl-Gtw48RlUGw?token=1840160119&lang=zh_CN&wt.mc_id=MVP_325642