駐車連絡用QRコード生成ツールの開発実践

駐車連絡用QRコード生成ツールの開発実践

本記事では、C#とAvaloniaを使用したデスクトップ版、およびBlazorフロントエンドと.NET Web APIを使用したオンライン版の駐車連絡用QRコード生成ツールの開発方法について、要件分析、コアコード実装、UI設計、MVVMパターンの適用を含めて紹介します。

最終更新 2025/03/10 19:54
沙漠尽头的狼
読了目安 14 分
カテゴリ
.NET
タグ
.NET C# Blazor Avalonia UI MVVM

一、需要分析と設計案

背景問題

車が行き交う現代都市では、駐車問題は常につきまとう悩みであり、車の移動(ノーチェ)も多くのドライバーが日常的に直面する問題の一つです。以前は、他人が連絡を取りやすくするために、車内の見える場所に電話番号を書いた紙を置いたり、電話番号を刻んだノーチェ置物を置いたりしていました。この伝統的な方法は一見簡単で直接的ですが、多くのリスクをはらんでいます。

まず、プライバシー漏洩の問題です。現在、個人情報の安全性は極めて重要であり、公共の場所に電話番号をむやみにさらすことは、プライバシーの扉を開け放つようなものです。悪意のある者はこれらの情報を悪用して嫌がらせ、勧誘電話、詐欺SMSを仕掛け、ユーザーを悩ませます。過去には、駐車場で「番号収集者」が巡回し、ノーチェ用電話番号を収集してマーケティング会社や詐欺グループに一括販売するというニュースもありました。その結果、車の所有者は様々な迷惑情報で生活が溢れてしまいました。

プライバシー漏洩に加えて、ノーチェ番号の悪用も頻繁に発生しています。例えば、ほんの一時的な駐車であるにもかかわらず、頻繁にノーチェ電話がかかってきたり、緊急でない場合でも無関係に電話をかけられ、時間を無駄にすることがあります。さらに悪質な場合、悪意のある者がノーチェ電話を利用して、交通違反や事故などと偽った虚偽の理由で、被害者にリンクをクリックさせたり送金させたりして、財産的損害を与える詐欺を行うケースもあります。

解決策

上記の問題を解決するため、私たちはシンプルで実用的なノーチェQRコード生成ツールを開発し、以下の機能を実現しました:

  1. 車の所有者の連絡先を含むQRコードを迅速に生成
  2. QRコードをスキャンするだけでワンタップで電話をかけられる
  3. カスタムノーチェメッセージ文をサポート
  4. 電話番号は暗号化処理され、プライバシーを保護
  5. オフラインデスクトップ版とオンラインWeb版の2種類の利用方法を提供

電話番号を直接表示する従来の方法とは異なり、当ノーチェQRコードツールは暗号化技術を採用しており、車の所有者の携帯番号が直接露出することを防ぎます。また、QRコードをスキャンして専用のノーチェページに遷移することで、使用体験が向上し、番号が悪用されるリスクも低減されます。本記事では、この2つの実装方法(C#とAvaloniaを使用したデスクトップアプリ版、Blazorフロントエンドと.NET Web APIを使用したオンラインWeb版)を詳しく紹介します。

効果は以下の通りで、WeChatでスキャンすると詳細ページが表示されます:

詳細ページでは、「車の所有者に電話する」をクリックするか、緑色のハイパーリンク「ノーチェコードを作成する」をクリックします:

二、コアQRコード生成コード

まず、コアとなるQRコード生成ロジックを見てみましょう。この部分のコードはCodeWF.ToolsプロジェクトのQrCodeGeneratorクラスにカプセル化されています:

using ImageMagick;
using ImageMagick.Drawing;
using ZXing;
using ZXing.QrCode;

namespace CodeWF.Tools.Image;

public static class QrCodeGenerator
{
    public static void GenerateQrCode(string title, string content, string imagePath, string? subTitle = "")
    {
        var qrCodeWriter = new BarcodeWriterPixelData
        {
            Format = BarcodeFormat.QR_CODE,
            Options = new QrCodeEncodingOptions
            {
                Width = 400,
                Height = 400,
                Margin = 1,
                ErrorCorrection = ZXing.QrCode.Internal.ErrorCorrectionLevel.H,
                CharacterSet = "UTF-8",
                DisableECI = true
            }
        };

        var pixelData = qrCodeWriter.Write(content);

        using var qrCodeImage = new MagickImage();
        var settings = new PixelReadSettings((uint)pixelData.Width, (uint)pixelData.Height, StorageType.Char, PixelMapping.RGBA);
        qrCodeImage.ReadPixels(pixelData.Pixels, settings);

        var backgroundHeight = string.IsNullOrWhiteSpace(subTitle) ? 600u : 630u;
        using var background = new MagickImage(MagickColors.White, 500, backgroundHeight);

        background.BorderColor = new MagickColor("#2888E2");
        background.Border(8);

        var titleText = new Drawables()
            .Font("SimHei")
            .FontPointSize(95)
            .FillColor(new MagickColor("#FF5722"))
            .TextAlignment(TextAlignment.Center)
            .Text(250, 120, title);
        background.Draw(titleText);

        background.Composite(qrCodeImage, 50, 170, CompositeOperator.Over);

        if (!string.IsNullOrWhiteSpace(subTitle))
        {
            var subTitleText = new Drawables()
                .Font("SimHei")
                .FontPointSize(20)
                .FillColor(new MagickColor("#333333"))
                .TextAlignment(TextAlignment.Center)
                .Text(250, 600, subTitle);
            background.Draw(subTitleText);
        }

        //using var logo = new MagickImage("logo.png");
        //logo.Resize(100, 100);
        //background.Composite(logo, 250, 250, CompositeOperator.Over);

        background.Quality = 100;
        background.Write(imagePath);
    }
}

上記のコードではNuGetパッケージ:ZXing.Net.Bindings.Magickを使用しています。ZXing.NETはQRコードのマトリックスデータを生成し、Magick.NETは画像処理と合成を担当し、以下の機能を実現しています:

  1. 高品質なQRコードを作成。高い誤り訂正レベル(Hレベル)を設定しているため、QRコードの一部が隠れても正常に認識可能
  2. 白地に青い枠線の背景画像を生成
  3. 上部にカスタムタイトルテキストを追加
  4. QRコード画像を背景に合成
  5. QRコード下部にカスタムサブタイトルテキストを追加可能
  6. 高品質PNG画像として保存

この設計により、最終的に生成されるノーチェQRコードは美しく実用的です。

ノーチェQRコード

三、オフラインデスクトップ版の実装

1. ユーザーインターフェースデザイン

Avaloniaフレームワークを使用してユーザーインターフェースを設計し、NuoCheView.axamlファイルで定義されています:

<UserControl xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:vm="clr-namespace:CodeWF.Modules.Converter.ViewModels"
             xmlns:prism="http://prismlibrary.com/"
             xmlns:u="https://irihi.tech/ursa"
             xmlns:i18n="https://codewf.com"
             xmlns:language="clr-namespace:Localization"
             prism:ViewModelLocator.AutoWireViewModel="True"
             x:CompileBindings="True"
             x:DataType="vm:NuoCheViewModel"
             d:DesignHeight="450"
             d:DesignWidth="800"
             x:Class="CodeWF.Modules.Converter.Views.NuoCheView"
             mc:Ignorable="d">
    <StackPanel Margin="10" HorizontalAlignment="Center">
        <u:Form LabelPosition="Left" LabelWidth="*">
            <TextBox Width="300" u:FormItem.Label="{i18n:I18n {x:Static language:NuoCheView.InputPhoneNumber}}"
                     u:FormItem.IsRequired="True" Text="{Binding PhoneNumber}" />
            <TextBox Width="300" u:FormItem.Label="{i18n:I18n {x:Static language:NuoCheView.InputTitle}}"
                     u:FormItem.IsRequired="True" Text="{Binding InputTitle}" />
            <StackPanel Orientation="Horizontal" u:FormItem.Label="{i18n:I18n {x:Static language:NuoCheView.EnableSubTitle}}">
                <CheckBox IsChecked="{Binding EnableSubTitle}" Command="{Binding EnableSubTitleHandler}" VerticalAlignment="Center" Content="表示" Margin="5 0" />
                <TextBox Width="300" Text="{Binding SubTitle}" IsEnabled="{Binding EnableSubTitle}" VerticalAlignment="Center" />
            </StackPanel>
        </u:Form>

        <StackPanel Orientation="Horizontal" Spacing="10" Margin="0,0,0,10">
            <Button Content="{i18n:I18n {x:Static language:NuoCheView.CreateButtonContent}}"
                    Command="{Binding GenerateQrCode}" />
            <Button Content="{i18n:I18n {x:Static language:NuoCheView.SaveQrCodeFileTitle}}"
                    Command="{Binding SaveQrCode}" />
            <HyperlinkButton Content="{i18n:I18n {x:Static language:NuoCheView.PreviewNuoCheUrl}}"
                             NavigateUri="{Binding GeneratedUrl}" Classes="WithIcon Underline"
                             VerticalAlignment="Center" />
        </StackPanel>

        <Image Source="{Binding QrCodeImage}" Width="340" Height="340" DragDrop.AllowDrop="True">
            <Interaction.Behaviors>
                <EventTriggerBehavior EventName="PointerPressed">
                    <InvokeCommandAction Command="{Binding RaisePointerPressed}" PassEventArgsToCommand="True" />
                </EventTriggerBehavior>
            </Interaction.Behaviors>
        </Image>
    </StackPanel>
</UserControl>

インターフェースのレイアウトはシンプルで明確で、主に以下を含みます:

  1. 3つの入力ボックス:電話番号、QRコードタイトル、オプションのQRコードサブタイトル
  2. 3つの操作ボタン:QRコード生成、保存、ノーチェページのプレビュー
  3. 画像表示エリア:生成されたQRコードを表示し、ドラッグ&ドロップ操作をサポート

実装効果は以下の通りです:

オフライン版インターフェーススクリーンショット

2. ビューモデルの実装

NuoCheViewModel.csでビジネスロジックを実装します:

using Avalonia.Input;
using Avalonia.Media.Imaging;
using Avalonia.Platform.Storage;
using AvaloniaXmlTranslator;
using CodeWF.Core.IServices;
using CodeWF.LogViewer.Avalonia;
using CodeWF.Tools.Image;
using HashidsNet;
using ReactiveUI;

namespace CodeWF.Modules.Converter.ViewModels;

public class NuoCheViewModel : ReactiveObject
{
    private readonly INotificationService _notificationService;
    private readonly IFileChooserService _fileChooserService;
    private string _inputTitle;
    private long _phoneNumber;
    private Bitmap? _qrCodeImage;
    private string _qrCodeImagePath;
    private string _generatedUrl;
    private string _subTitle;
    private bool _enableSubTitle;

    public NuoCheViewModel(INotificationService notificationService, IFileChooserService fileChooserService)
    {
        _notificationService = notificationService;
        _fileChooserService = fileChooserService;

        InputTitle = I18nManager.Instance.GetResource(Localization.NuoCheView.DefaultInputTitle);
        PhoneNumber = 16899999999;
        SubTitle = $"{I18nManager.Instance.GetResource(Localization.NuoCheView.DefaultSubTitlePrefix)}: {PhoneNumber}";
        EnableSubTitle = false;
    }

    public string InputTitle
    {
        get => _inputTitle;
        set => this.RaiseAndSetIfChanged(ref _inputTitle, value);
    }

    public long PhoneNumber
    {
        get => _phoneNumber;
        set => this.RaiseAndSetIfChanged(ref _phoneNumber, value);
    }

    public Bitmap? QrCodeImage
    {
        get => _qrCodeImage;
        private set => this.RaiseAndSetIfChanged(ref _qrCodeImage, value);
    }

    public string QrCodeImagePath
    {
        get => _qrCodeImagePath;
        private set => this.RaiseAndSetIfChanged(ref _qrCodeImagePath, value);
    }

    public string GeneratedUrl
    {
        get => _generatedUrl;
        private set => this.RaiseAndSetIfChanged(ref _generatedUrl, value);
    }

    public string SubTitle
    {
        get => _subTitle;
        set => this.RaiseAndSetIfChanged(ref _subTitle, value);
    }

    public bool EnableSubTitle
    {
        get => _enableSubTitle;
        set => this.RaiseAndSetIfChanged(ref _enableSubTitle, value);
    }

    public void EnableSubTitleHandler()
    {
        SubTitle = $"{I18nManager.Instance.GetResource(Localization.NuoCheView.DefaultSubTitlePrefix)}: {PhoneNumber}";
    }

    public void GenerateQrCode()
    {
        if (string.IsNullOrWhiteSpace(InputTitle))
        {
            _notificationService.Show(I18nManager.Instance.GetResource(Localization.NuoCheView.Title),
                I18nManager.Instance.GetResource(Localization.NuoCheView.NeedInputTip));
            return;
        }

        try
        {
            var encodedPhone = new Hashids("codewf").EncodeLong(PhoneNumber);

            GeneratedUrl =
                $"https://codewf.com/nuoche?p={encodedPhone}";
            QrCodeImagePath = Path.Combine(Path.GetTempPath(), "nuoche.png");

            QrCodeGenerator.GenerateQrCode(InputTitle, GeneratedUrl, QrCodeImagePath, 
                EnableSubTitle ? SubTitle : null);

            QrCodeImage = new Bitmap(QrCodeImagePath);
        }
        catch (Exception ex)
        {
            Logger.Error(I18nManager.Instance.GetResource(Localization.NuoCheView.CreateErrorMessage), ex);
            _notificationService.Show(I18nManager.Instance.GetResource(Localization.NuoCheView.Title),
                $"{I18nManager.Instance.GetResource(Localization.NuoCheView.CreateErrorMessage)}: {ex}");
        }
    }


    public async Task SaveQrCode()
    {
        if (QrCodeImage == null || string.IsNullOrEmpty(QrCodeImagePath))
        {
            _notificationService.Show(I18nManager.Instance.GetResource(Localization.NuoCheView.SaveNotificationTitle),
                I18nManager.Instance.GetResource(Localization.NuoCheView.SaveNoQrCodeMessage));
            return;
        }

        try
        {
            var file = await _fileChooserService.SaveFileAsync(
                I18nManager.Instance.GetResource(Localization.NuoCheView.SaveQrCodeFileTitle),
                new[]
                {
                    new FilePickerFileType(
                        I18nManager.Instance.GetResource(Localization.NuoCheView.SaveQrCodeFileFormat))
                    {
                        Patterns = new[] { "*.png" }
                    }
                }
            );

            if (file != null)
            {
                File.Copy(QrCodeImagePath, file, true);
                _notificationService.Show(
                    I18nManager.Instance.GetResource(Localization.NuoCheView.SaveQrCodeSuccessTitle),
                    I18nManager.Instance.GetResource(Localization.NuoCheView.SaveQrCodeSuccessMessage));
            }
        }
        catch (Exception ex)
        {
            _notificationService.Show(I18nManager.Instance.GetResource(Localization.NuoCheView.SaveNotificationTitle),
                $"{I18nManager.Instance.GetResource(Localization.NuoCheView.SaveQrCodeErrorMessage)}: {ex.Message}");
        }
    }

    public async Task RaisePointerPressed(PointerPressedEventArgs e)
    {
        if (string.IsNullOrWhiteSpace(QrCodeImagePath) || !File.Exists(QrCodeImagePath))
        {
            return;
        }

        var dragData = new DataObject();
        if (await _fileChooserService.StorageProvider.TryGetFileFromPathAsync(QrCodeImagePath) is { } storageFile)
        {
            dragData.Set(DataFormats.Files, new[] { storageFile });
        }

        await DragDrop.DoDragDrop(e, dragData, DragDropEffects.Copy);
    }
}

ビューモデルは以下の機能を実装しています:

  1. データバインディング:ユーザー入力とUI状態を管理
  2. QRコード生成:コア生成ロジックを呼び出して結果を表示
  3. ファイル保存:生成されたQRコードをユーザー指定の場所に保存
  4. ドラッグ&ドロップ対応:ユーザーがQRコード画像を他のアプリケーションに直接ドラッグ可能
  5. エラーハンドリング:親切なエラーメッセージを表示

特筆すべき点として、Hashidsライブラリを使用して電話番号をエンコードし、プライバシー保護を強化しています。これにより、QRコードが公開されても、他人が実際の携帯番号を直接取得することはできません(ただし、最終ステップでダイヤル画面に遷移する際には実際の番号が表示されるため、仮想番号を使用して実際の番号を隠すことも検討可能です)。

ドラッグ&ドロップによるノーチェQRコード保存の効果:

ノーチェQRコードのドラッグ保存

四、オンラインWeb版の実装

デスクトップアプリ版に加えて、BlazorベースのオンラインノーチェQRコード生成ツールも開発しました。これにより、ユーザーはソフトウェアをインストールすることなく利用できます。

オンラインアクセスURL:https://dotnet9.com/nuoche

PC端でノーチェコードを作成:

オンラインPC版でノーチェコードを作成

モバイル端でノーチェコードを作成:

オンラインモバイル版でノーチェコードを作成

1. オンライン版ノーチェコード生成の特長

デスクトップ版と比較して、オンライン版には以下の特長があります:

  1. インストール不要:ブラウザから直接アクセスして使用可能
  2. 双方向機能:QRコードの生成と、解析後のノーチェページへの直接アクセスが可能
  3. モバイルフレンドリー:モバイルデバイスでの表示と操作性を最適化
  4. 洗練されたUIデザイン:モダンなUIコンポーネントとインタラクション効果を採用

2. 実装方法

オンライン版はBlazorを使用して実装されており、コアコード構造は以下の通りです:

@page "/NuoChe"
@using HashidsNet
@inject IOptions<SiteOption> SiteOption
@layout EmptyLayout

@code {
    [SupplyParameterFromQuery]
    public string? P { get; set; }

    public const string Slug = "nuoche";

    private long? _decodePhone;

    protected override void OnInitialized()
    {
        base.OnInitialized();
        if (!string.IsNullOrWhiteSpace(P))
        {
            _decodePhone = new Hashids("codewf").DecodeLong(P)[0];
        }
    }
}

<PageTitle>無料ノーチェコード</PageTitle>

<div class="nuoche-container">
    @if (string.IsNullOrWhiteSpace(P))
    {
        <div class="generator-container">
            <h1 class="generator-title">ノーチェコードオンラインジェネレーター</h1>
            <div class="input-group">
                <label for="phoneNumber">電話番号</label>
                <input type="tel" id="phoneNumber" placeholder="電話番号を入力" />
            </div>
            <div class="input-group">
                <label for="title">タイトル</label>
                <input type="text" id="title" placeholder="スキャンしてノーチェ" value="スキャンしてノーチェ" />
            </div>
            <div class="input-group">
                <div class="checkbox-container">
                    <input type="checkbox" id="enableSubtitle" />
                    <label for="enableSubtitle">サブタイトルを追加</label>
                </div>
                <input type="text" id="subtitle" placeholder="スキャンして車の所有者に連絡、または電話: 16800000000" disabled />
            </div>
            <div class="button-group">
                <button class="primary-button" onclick="generateQrCode()">
                    <i class="fas fa-qrcode"></i> QRコードを生成
                </button>
            </div>
            <div class="qr-code-container" style="display: none">
                <img alt="ノーチェコード" />
                <div class="action-buttons">
                    <a target="_blank" class="preview-link">
                        <i class="fas fa-external-link-alt"></i> プレビュー
                    </a>
                    <a class="download-link">
                        <i class="fas fa-download"></i> ダウンロード
                    </a>
                </div>
                <div class="alert alert-warning mt-2">
                    <i class="fas fa-clock"></i> 注意:生成されたファイルは2分間のみ保持されます。すぐにダウンロードしてください。
                </div>
            </div>
        </div>
    }
    else
    {
        <div class="card">
            <div class="card-header">
                <i class="fas fa-car"></i>
                <h1 class="title">一時駐車中、ご了承ください</h1>
            </div>
            <div class="card-body">
                <p class="description">私の車があなたの車の通行を妨げている場合、下のボタンをクリックして通知してください。ご不便をおかけして申し訳ございません。</p>
                <a class="phone-button" href="tel:@_decodePhone">
                    <i class="fas fa-phone-alt"></i> 車の所有者に電話する
                </a>
                <a class="generate-link" href="/nuoche">ノーチェコードを作成する</a>
            </div>
        </div>
    }
</div>

この実装には2つの主要機能ページがあります:

  1. 生成ページ:ユーザーが/nuocheに直接アクセスした場合、QRコード生成インターフェースが表示され、電話番号とタイトルの入力が可能
  2. ノーチェページ:ユーザーがQRコードスキャン経由でアクセスした場合(pパラメータ付き)、ワンタップで車の所有者に電話をかけるインターフェースが表示される

この設計により、QRコードの完全なライフサイクル(生成、スキャン、連絡)を実現し、ユーザーに便利なノーチェソリューションを提供しています。

前述のスクリーンショットでスキャン後に表示される詳細情報ページ:

スキャン後のスクリーンショット

3. オンライン版のコアJavaScriptインタラクション

async function generateQrCode() {
    const title = document.getElementById('title').value;
    const phoneNumber = document.getElementById('phoneNumber').value;
    const enableSubtitle = document.getElementById('enableSubtitle').checked;
    let subtitle = null;
    
    if (enableSubtitle) {
        subtitle = document.getElementById('subtitle').value.trim();
        if (!subtitle) {
            subtitle = `スキャンして車の所有者に連絡、または電話: ${phoneNumber || "16800000000"}`;
            document.getElementById('subtitle').value = subtitle;
        }
    }

    if (!title || !phoneNumber) {
        alert('タイトルと電話番号を入力してください');
        return;
    }

    try {
        const response = await fetch('/api/Image/nuoche', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({
                title: title,
                phoneNumber: phoneNumber,
                subtitle: subtitle
            })
        });

        const data = await response.json();
        if (data.success) {
            const qrCodeContainer = document.querySelector('.qr-code-container');
            const img = qrCodeContainer.querySelector('img');
            img.src = data.qrCodeUrl;
            qrCodeContainer.querySelector('.preview-link').href = data.generatedUrl;

            // ダウンロード機能を追加
            const downloadLink = qrCodeContainer.querySelector('.download-link');
            downloadLink.onclick = () => {
                const a = document.createElement('a');
                a.href = data.qrCodeUrl;
                a.download = 'ノーチェコード.png';
                document.body.appendChild(a);
                a.click();
                document.body.removeChild(a);
            };

            qrCodeContainer.style.display = 'block';
        } else {
            throw new Error(data.message);
        }
    } catch (error) {
        console.error('QRコード生成に失敗:', error);
        alert('QRコード生成に失敗しました。後でもう一度お試しください');
    }
}

// サブタイトルチェックボックスのイベント処理
document.addEventListener('DOMContentLoaded', function() {
    const enableSubtitleCheckbox = document.getElementById('enableSubtitle');
    const subtitleInput = document.getElementById('subtitle');
    const phoneInput = document.getElementById('phoneNumber');
    
    if (enableSubtitleCheckbox && subtitleInput) {
        enableSubtitleCheckbox.addEventListener('change', function() {
            subtitleInput.disabled = !this.checked;
            if (this.checked) {
                if (!subtitleInput.value.trim()) {
                    const phoneNumber = phoneInput.value.trim() || "16800000000";
                    subtitleInput.value = `スキャンして車の所有者に連絡、または電話: ${phoneNumber}`;
                }
                subtitleInput.focus();
            }
        });
        
        // 電話番号が変更されたら、サブタイトルが有効でデフォルト形式を使用している場合、更新する
        phoneInput.addEventListener('input', function() {
            if (enableSubtitleCheckbox.checked && subtitleInput.value.startsWith('スキャンして車の所有者に連絡、または電話:')) {
                const phoneNumber = this.value.trim() || "16800000000";
                subtitleInput.value = `スキャンして車の所有者に連絡、または電話: ${phoneNumber}`;
            }
        });
    }
});

オンライン版のバックエンドAPI実装はオフライン版と同様に、同一のコアQRコード生成ロジックを使用していますが、ファイルアップロード処理、一時保存、クリーンアップ機能などが追加されています。詳細は割愛します。オンライン版ソースコードはこちら。ノーチェコードAPIインターフェースは以下のように定義されています:

[HttpPost("nuoche")]
[AllowAnonymous]
public async Task<IActionResult> NuoCheAsync([FromBody] NuoCheRequest request,
    [FromServices] IWebHostEnvironment env,
    [FromServices] IOptions<SiteOption> siteOption)
{
    try
    {
        if (string.IsNullOrWhiteSpace(request.Title) || string.IsNullOrWhiteSpace(request.PhoneNumber))
        {
            return BadRequest(new { success = false, message = "タイトルと電話番号は必須です" });
        }

        if (!long.TryParse(request.PhoneNumber, out long phoneNumberLong))
        {
            return BadRequest(new { success = false, message = "無効な電話番号です" });
        }

        var encodedPhone = new Hashids("codewf").EncodeLong(phoneNumberLong);
        var generatedUrl = $"{siteOption.Value.Domain}/nuoche?p={encodedPhone}";

        var fileName = $"qrcode_{Guid.NewGuid():N}.png";
        var qrCodePath = Path.Combine(env.WebRootPath, IconFolder, fileName);

        Directory.CreateDirectory(Path.Combine(env.WebRootPath, IconFolder));

        QrCodeGenerator.GenerateQrCode(request.Title, generatedUrl, qrCodePath, request.SubTitle);

        var qrCodeUrl = $"/{IconFolder}/{fileName}";
        return Ok(new
        {
            success = true,
            qrCodeUrl,
            generatedUrl
        });
    }
    catch (Exception ex)
    {
        return BadRequest(new { success = false, message = ex.Message });
    }
}

五、機能比較と適用シーン

オフラインデスクトップ版とオンラインWeb版はそれぞれに利点があり、以下のような特徴の比較があります:

機能 オフライン版 オンライン版
インストール要件 ダウンロードとインストールが必要 インストール不要、ブラウザでアクセス
プラットフォーム対応 Windows、macOS、Linux 最新のブラウザ
ネットワーク依存 初回URL生成のみネットが必要 完全にネットワークに依存
ファイル保存 ローカルに永続保存 サーバーに一時保存
UI体験 デスクトップアプリ風 レスポンシブWebデザイン
ドラッグ対応 QRコードのドラッグをサポート 直接ドラッグは非対応
プライバシー保護 高(ローカル処理) 中(サーバー処理)
サブタイトル機能 カスタムサブタイトル対応 カスタムサブタイトル対応

サブタイトル機能の追加により、QRコードの情報量が増え、QRコードの下に「スキャンして車の所有者に連絡」などの追加情報を表示できるため、識別性と使用の利便性が向上します。

六、まとめと展望

本記事では、ノーチェQRコード生成ツールの開発プロセスについて、要件分析、コアコード実装、UI設計、マルチプラットフォーム展開まで詳しく紹介しました。このツールは、従来のノーチェ連絡方法が抱えるプライバシー漏洩や番号悪用の問題を解決するだけでなく、最新技術を通じてより安全で便利なノーチェ体験を提供します。

このツールは以下のシナリオで特に有効です:

  1. 一時駐車:マンションやショッピングモールなどでの一時駐車時に連絡先を提供
  2. 展示会イベント:車両展示時に連絡用QRコードを配置
  3. 共有駐車スペース:個人の駐車スペースで一時的な連絡先を提供
  4. 緊急時:特別な事情で車両の緊急連絡が必要な場合

今後は、このツールをさらに充実させる予定です。考えられる改良方向は以下の通りです:

  1. 多言語対応:英語、日本語などの多言語インターフェースを追加し、国際ユーザーが使いやすいように
  2. カスタマイズオプションの拡充:QRコードの色、スタイル、レイアウトのカスタマイズをサポート
  3. 統計機能:スキャン回数の統計を追加し、ユーザーがQRコードの使用状況を把握できるように
  4. エンタープライズ版機能:フリート管理や駐車場などの企業ユーザー向けに一括生成機能を提供

実用的なツールとして、ノーチェQRコードジェネレーターは現実の問題を解決するとともに、最新の.NET技術スタックを使用してクロスプラットフォームアプリケーションを構築する方法を示しています。

本記事がお役に立てば幸いです。ご質問があれば、コメント欄でお気軽にご議論ください。

ソースコード参照:

さらに探索

関連読書

その他の記事
同じカテゴリ / 同じタグ 2026/01/11

AvaloniaのクリップボードとDataGridの問題

最近のAvaloniaデスクトップソフトウェア開発で解決した2つの問題を記録:クリップボードコピーのクラッシュ、タブ切り替え時のDataGridの遅延。根本原因を分析し、解決策を提供する

続きを読む
同じカテゴリ / 同じタグ 2025/02/25

.NET 10 Preview 1 リリース

本日.NET 10 Preview 1がリリースされました。私はすぐにダウンロードして、Avalonia UIプロジェクトとブログサイトをアップグレードしました。前者は機能テストとAOT公開が正常に動作し、後者はデバッグが正常に行えます。Dockerは今のところ成功していません。

続きを読む