2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Winform+AIシリーズ:Chatユーザーコントロール

Posted at

WinFormと聞くと、多くのプログラマーが懐かしさを感じることでしょう。デスクトップアプリケーション開発で初めて触れる技術の一つであり、まさに「旧友」と呼べる存在です。現在、主流の技術コミュニティではあまり話題にならないものの、一部のエンタープライズプロジェクトや社内ツールなど、いまだに重要な役割を担っています。特にレガシーシステムでは、その安定性、シンプルさ、メンテナンス性の高さから重宝されています。

一方、AIは今やテクノロジー業界の「主役」となりました。ここ2~3年、AIへの関心は爆発的に高まりました。ChatGPTや画像生成ツール、スマートアシスタントなど、さまざまな“AI神ツール”が次々と登場しています。エンジニアだけでなく、一般の方々まで「AIってすごいね」と話題にするほどです。

「ベテラン」と「新鋭」──WinFormとAI、一見ミスマッチに思えるこの組み合わせも、実は非常に面白い取り組みです。最近、私もWinFormでシンプルなAIツールを作ってみたのですが、旧技術と新技術をうまく組み合わせることで、想像以上に便利なものが出来上がりました。

今後何回かの記事で、「古参」WinFormがAIの“急行列車”に乗る方法をご紹介していきます。安定した基盤を生かしつつ、AIによる“ひらめき”を体験しましょう。


ChatはAIが最も得意とする分野なので、まずはChat機能から取り上げます。

AI技術スタック:Ollama+Phi4-mini

  1. Ollamaのインストール
    Windows版Ollamaを https://ollama.com/download からダウンロードしてインストールします。

  2. モデルの検索
    https://ollama.com/search でphi4-miniモデルを検索します。

  3. モデルの取得
    Windowsの「PowerShell」や「コマンドプロンプト」で、下記コマンドを実行してphi4-miniモデルを取得します。

    ollama pull phi4-mini
    

    Ollamaには他にも多くのコマンドがありますので、必要に応じて学んでください。

Ollamaモデル取得画面

WinFormへの組み込み

次に、WinForm内でカスタムユーザーコントロールを作成します。完成イメージは以下の通りです。

WinFormカスタムコントロールUI

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

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?