20201127のC#に関する記事は10件です。

Windows GUIプログラミング入門27 ヘルプ画面

■はじめに

今回は画面内にヘルプページを表示するサンプルと、画面パーツを別xamlにするやり方を学びます。
これらの実装例として、正規表現の動作確認ツールを作ります。
000.png

■開発環境

  • Windows 10
  • Visual Studio Community 2019 (Version 16.8.2)
  • .NET Framework 4.6.2 / .NET Core 3.1 / .NET 5.0

■プロジェクト作成

RegexTesterという名前でプロジェクトを作成します。

◇.NET Frameworkの場合

001.png

002.png

◇.NET Core 3.1 / .NET 5.0の場合

003.png

004.png

■プロジェクトの設定

◇.NET Core 3.1の場合

プロジェクトを右クリック、「プロパティ」で「対象のフレームワーク」を.NET Core 3.1に設定します。
005.png

◇.NET 5.0の場合

プロジェクトを右クリック、「プロパティ」で「対象のフレームワーク」を.NET 5.0に設定します。
006.png

プロジェクトをダブルクリックし、
csprojファイルのSdkがMicrosoft.NET.Sdk.WindowsDesktopになっていたらMicrosoft.NET.Sdkに編集します。
007.png

■メイン画面の枠組み作成

Gridを分割し、上にタブ、下に閉じるボタンを配置します。
010.png

MainWindow.xaml
<Window
    x:Class="RegexTester.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:local="clr-namespace:RegexTester"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="Regex Tester .NET"
    Width="800"
    Height="450"
    ResizeMode="CanResizeWithGrip"
    mc:Ignorable="d">
    <Grid>
        <Grid.RowDefinitions>
            <!--  タブ行  -->
            <RowDefinition />
            <!--  ボタン行  -->
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <TabControl Margin="6">
            <TabItem Header="マッチング">
            </TabItem>
            <TabItem Header="ヘルプ">
            </TabItem>
        </TabControl>

        <StackPanel Grid.Row="1">
            <Button
                Width="150"
                Margin="12,6"
                HorizontalAlignment="Right"
                Content="閉じる(_C)" />
        </StackPanel>

    </Grid>
</Window>

■画面パーツ用意

015_.png
プロジェクトを右クリック、「追加」-「新しいフォルダー」でViewsと入力してください。
「Views」フォルダー内にPagesフォルダーを作成してください。

020_.png
「Pages」フォルダーを右クリック、「追加」-「ユーザーコントロール(WPF)」でMatchingContentと入力してください。
同様に「Pages」フォルダにユーザーコントロールHelpContentを追加してください。
025_2.png

■タブのコンテンツ組み込み

追加したコントロールをメイン画面の各タブで参照します。

MainWindow.xamlのWindowに
xmlns:page="clr-namespace:RegexTester.Views.Pages"を追加してください。

MainWindow.xaml
<Window
:
    xmlns:page="clr-namespace:RegexTester.Views.Pages"
:

そして、TabItemの中にpage:の後に先ほど追加したユーザーコントロールの名前を書きます。

MainWindow.xaml
<TabControl Margin="6">
    <TabItem Header="マッチング">
        <page:MatchingContent />
    </TabItem>

    <TabItem Header="ヘルプ">
        <page:HelpContent />
    </TabItem>
</TabControl>

pageの部分は任意の名前を付けられます。
xmlns:abc="clr-namespace:RegexTester.Views.Pages"としていたら
<abc:MatchingContent />という書き方でPagesフォルダのコントロールを参照できます。

ここまでで一度ビルドしておきましょう。

■ヘルプ内容作成

◇画面

「ヘルプ」タブに表示する内容を作ります。
HTMLのテーブル風のリファレンスとURLリンクを作ります。
030.png

Xamlの書き方もHTMLのTableと少し似ています。

HelpContent.xaml
<UserControl
    x:Class="RegexTester.Views.Pages.HelpContent"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:local="clr-namespace:RegexTester.Views.Pages"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    d:DesignHeight="500"
    d:DesignWidth="800"
    mc:Ignorable="d">
    <FlowDocumentScrollViewer>
        <FlowDocument>
            <Section>
                <Paragraph>
                    <Bold>■正規表現クイックリファレンス</Bold>
                </Paragraph>
                <Table FontFamily="JetBrains Mono, Cascadia Code, BIZ UDGothic, Yu Gothic Medium, Meiryo">
                    <Table.Resources>
                        <Style TargetType="TableCell">
                            <Setter Property="BorderBrush" Value="Silver" />
                            <Setter Property="BorderThickness" Value="0.5" />
                        </Style>
                    </Table.Resources>
                    <Table.Columns>
                        <TableColumn Width="*" />
                        <TableColumn Width="4*" />
                    </Table.Columns>
                    <TableRowGroup>
                        <TableRow>
                            <TableCell>
                                <Paragraph>.</Paragraph>
                            </TableCell>
                            <TableCell>
                                <Paragraph>任意の1文字</Paragraph>
                            </TableCell>
                        </TableRow>
                    </TableRowGroup>
                    <TableRowGroup>
                        <TableRow>
                            <TableCell>
                                <Paragraph>*</Paragraph>
                            </TableCell>
                            <TableCell>
                                <Paragraph>直前の要素の0回以上繰り返し</Paragraph>
                            </TableCell>
                        </TableRow>
                    </TableRowGroup>
                    <TableRowGroup>
                        <TableRow>
                            <TableCell>
                                <Paragraph>+</Paragraph>
                            </TableCell>
                            <TableCell>
                                <Paragraph>直前の要素の1回以上繰り返し</Paragraph>
                            </TableCell>
                        </TableRow>
                    </TableRowGroup>
                    <TableRowGroup>
                        <TableRow>
                            <TableCell>
                                <Paragraph>?</Paragraph>
                            </TableCell>
                            <TableCell>
                                <Paragraph>直前の要素の0~1回繰り返し</Paragraph>
                            </TableCell>
                        </TableRow>
                    </TableRowGroup>
                    <TableRowGroup>
                        <TableRow>
                            <TableCell>
                                <Paragraph>^</Paragraph>
                            </TableCell>
                            <TableCell>
                                <Paragraph>行頭</Paragraph>
                            </TableCell>
                        </TableRow>
                    </TableRowGroup>
                    <TableRowGroup>
                        <TableRow>
                            <TableCell>
                                <Paragraph>$</Paragraph>
                            </TableCell>
                            <TableCell>
                                <Paragraph>行末</Paragraph>
                            </TableCell>
                        </TableRow>
                    </TableRowGroup>
                    <TableRowGroup>
                        <TableRow>
                            <TableCell>
                                <Paragraph>{?}</Paragraph>
                            </TableCell>
                            <TableCell>
                                <Paragraph>直前の要素の?回繰り返し</Paragraph>
                            </TableCell>
                        </TableRow>
                    </TableRowGroup>
                    <TableRowGroup>
                        <TableRow>
                            <TableCell>
                                <Paragraph>{?,}</Paragraph>
                            </TableCell>
                            <TableCell>
                                <Paragraph>直前の要素の?回以上繰り返し</Paragraph>
                            </TableCell>
                        </TableRow>
                    </TableRowGroup>
                    <TableRowGroup>
                        <TableRow>
                            <TableCell>
                                <Paragraph>{?,?}</Paragraph>
                            </TableCell>
                            <TableCell>
                                <Paragraph>直前の要素の?~?回繰り返し</Paragraph>
                            </TableCell>
                        </TableRow>
                    </TableRowGroup>
                    <TableRowGroup>
                        <TableRow>
                            <TableCell>
                                <Paragraph>[?]</Paragraph>
                            </TableCell>
                            <TableCell>
                                <Paragraph>?内の任意の1文字</Paragraph>
                            </TableCell>
                        </TableRow>
                    </TableRowGroup>
                    <TableRowGroup>
                        <TableRow>
                            <TableCell>
                                <Paragraph>[^?]</Paragraph>
                            </TableCell>
                            <TableCell>
                                <Paragraph>?内にない任意の1文字</Paragraph>
                            </TableCell>
                        </TableRow>
                    </TableRowGroup>
                    <TableRowGroup>
                        <TableRow>
                            <TableCell>
                                <Paragraph>[?-?]</Paragraph>
                            </TableCell>
                            <TableCell>
                                <Paragraph>文字範囲?から?までの任意の1文字</Paragraph>
                            </TableCell>
                        </TableRow>
                    </TableRowGroup>
                    <TableRowGroup>
                        <TableRow>
                            <TableCell>
                                <Paragraph>()</Paragraph>
                            </TableCell>
                            <TableCell>
                                <Paragraph>グループ</Paragraph>
                            </TableCell>
                        </TableRow>
                    </TableRowGroup>
                    <TableRowGroup>
                        <TableRow>
                            <TableCell>
                                <Paragraph>(?:?)</Paragraph>
                            </TableCell>
                            <TableCell>
                                <Paragraph>非キャプチャ グループ</Paragraph>
                            </TableCell>
                        </TableRow>
                    </TableRowGroup>
                    <TableRowGroup>
                        <TableRow>
                            <TableCell>
                                <Paragraph>
                                    (?&lt;?&gt;?)
                                </Paragraph>
                            </TableCell>
                            <TableCell>
                                <Paragraph>名前付きグループ。?がグループ名</Paragraph>
                            </TableCell>
                        </TableRow>
                    </TableRowGroup>
                </Table>
            </Section>
            <Section>
                <Paragraph>
                    <Bold>■もっと詳しく</Bold>
                </Paragraph>
                <Paragraph>
                    <Hyperlink
                        NavigateUri="https://docs.microsoft.com/ja-jp/dotnet/standard/base-types/regular-expressions"
                        RequestNavigate="Hyperlink_RequestNavigate">
                        .NET の正規表現 | Microsoft Docs
                    </Hyperlink>
                </Paragraph>
            </Section>
        </FlowDocument>
    </FlowDocumentScrollViewer>
</UserControl>

◇ロジック

◎.NET Frameworkの場合

HelpContent.xaml.cs
using System.Windows.Controls;
using System.Windows.Navigation;

namespace RegexTester.Views.Pages
{
    /// <summary>
    /// HelpContent.xaml の相互作用ロジック
    /// </summary>
    public partial class HelpContent : UserControl
    {
        public HelpContent()
        {
            InitializeComponent();
        }

        private void Hyperlink_RequestNavigate(object sender, RequestNavigateEventArgs e)
        {
            // URLを開く
            System.Diagnostics.Process.Start(e.Uri.AbsoluteUri);
        }
    }
}

◎.NET Core 3.1 / .NET 5.0の場合

HelpContent.xaml.cs
using System.Windows.Controls;
using System.Windows.Navigation;

namespace RegexTester.Views.Pages
{
    /// <summary>
    /// HelpContent.xaml の相互作用ロジック
    /// </summary>
    public partial class HelpContent : UserControl
    {
        public HelpContent()
        {
            InitializeComponent();
        }

        private void Hyperlink_RequestNavigate(object sender, RequestNavigateEventArgs e)
        {
            var p = new System.Diagnostics.Process();
            p.StartInfo.FileName = e.Uri.AbsoluteUri;
            p.StartInfo.UseShellExecute = true;
            // URLを開く
            p.Start();
        }
    }
}

作り終わったらビルドし、MainWindowで「ヘルプ」タブを選択してみましょう。
ちゃんと「ヘルプ」タブ内に表示されました。
035.png

■メインコンテンツ作成

「マッチング」タブに表示する内容を作ります。
040.png

MatchingContent.xaml
<UserControl
    x:Class="RegexTester.Views.Pages.MatchingContent"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:local="clr-namespace:RegexTester.Views.Pages"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    d:DesignHeight="450"
    d:DesignWidth="800"
    mc:Ignorable="d">
    <UserControl.Resources>
        <Style TargetType="Label">
            <Setter Property="Margin" Value="6,6,6,0" />
        </Style>
        <Style TargetType="TextBox">
            <Setter Property="Margin" Value="6,0,6,6" />
            <Setter Property="FontFamily" Value="JetBrains Mono, Cascadia Code, BIZ UDGothic, Yu Gothic Medium, Meiryo" />
        </Style>
        <Style TargetType="CheckBox">
            <Setter Property="Margin" Value="6" />
        </Style>
    </UserControl.Resources>
    <Grid FocusManager.FocusedElement="{Binding ElementName=InTxt}">
        <Grid.RowDefinitions>
            <!--  上部(検索文字列)  -->
            <RowDefinition Height="Auto" MinHeight="60" />
            <!--  スプリッター  -->
            <RowDefinition Height="Auto" />
            <!--  下部(正規表現パターン~結果)  -->
            <RowDefinition />
        </Grid.RowDefinitions>

        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <!--  文字列テキスト行  -->
                <RowDefinition />
            </Grid.RowDefinitions>

            <Label Content="文字列(_I)" Target="{Binding ElementName=InTxt}" />
            <TextBox
                x:Name="InTxt"
                Grid.Row="1"
                AcceptsReturn="True"
                AcceptsTab="False"
                Text=""
                TextWrapping="Wrap"
                VerticalScrollBarVisibility="Visible" />
        </Grid>

        <GridSplitter
            Grid.Row="1"
            Height="6"
            Margin="0,1"
            HorizontalAlignment="Stretch" />

        <Grid Grid.Row="2">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <!--  結果のテキスト行  -->
                <RowDefinition />
            </Grid.RowDefinitions>

            <StackPanel>
                <Label Content="正規表現パターン(_P)" Target="{Binding ElementName=RegPtn}" />
                <TextBox x:Name="RegPtn" Text="" />

                <StackPanel Orientation="Horizontal">
                    <CheckBox x:Name="ROptIgnoreCase" Content="大文字小文字の区別をしない(_O)" />
                    <CheckBox x:Name="ROptMultiLine" Content="複数行モード(_M)" />
                    <CheckBox x:Name="ROptSingleLine" Content="単一行モード(_S)" />

                    <Button
                        Margin="12,0"
                        Padding="18,0"
                        Click="RunButton_Click"
                        Content="実行(_E)"
                        ToolTip="マッチングを実行します" />
                </StackPanel>

                <Label Content="結果(_R)" Target="{Binding ElementName=OutTxt}" />
            </StackPanel>

            <TextBox
                x:Name="OutTxt"
                Grid.Row="1"
                IsReadOnly="True"
                Text=""
                VerticalScrollBarVisibility="Visible" />
        </Grid>
    </Grid>
</UserControl>
MatchingContent.xaml.cs
using System;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Controls;

namespace RegexTester.Views.Pages
{
    /// <summary>
    /// MatchingContent.xaml の相互作用ロジック
    /// </summary>
    public partial class MatchingContent : UserControl
    {
        public MatchingContent()
        {
            InitializeComponent();
        }

        /// <summary>
        /// 実行ボタンクリック
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void RunButton_Click(object sender, RoutedEventArgs e)
        {
            this.IsEnabled = false;
            ExecuteMatching(InTxt.Text, RegPtn.Text, GetRegexOptions());
            this.IsEnabled = true;
        }

        /// <summary>
        /// マッチング実行
        /// </summary>
        /// <param name="input">文字列</param>
        /// <param name="regPattern">正規表現パターン</param>
        /// <param name="regOpt">検索オプション</param>
        public void ExecuteMatching(string input, string regPattern, RegexOptions regOpt)
        {
            OutTxt.Clear();

            // 未入力がないかチェック
            if (IsAnyNullOrEmpties(input, regPattern))
            {
                MessageBox.Show("未入力の項目があります。");
                return;
            }

            try
            {
                var reg = new Regex(regPattern, regOpt);
                // マッチング実行
                MatchCollection matches = reg.Matches(input);
                if (matches.Count == 0)
                {
                    OutTxt.Text = "★マッチなし★";
                    return;
                }

                var sb = new StringBuilder();
                sb.Append($"★{matches.Count}件マッチ★\n");
                foreach (Match m in matches)
                {
                    sb.Append($"Value ⇒ {m.Value}\n");

                    sb.Append("【グループ名】\n");
                    foreach (string gName in reg.GetGroupNames())
                    {
                        sb.Append($"{gName} => {m.Groups[gName].Value}\n");
                    }
                    sb.Append("--------------------\n");
                }

                // 結果を設定
                OutTxt.Text = sb.ToString();
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.ToString());
            }
        }

        /// <summary>
        /// 未入力チェック
        /// </summary>
        /// <param name="values"></param>
        /// <returns>全部入力されていればfalse</returns>
        private bool IsAnyNullOrEmpties(params string[] values)
        {
            foreach (string value in values)
            {
                if (string.IsNullOrEmpty(value))
                {
                    // 未入力
                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// 画面から正規表現オプション取得
        /// </summary>
        /// <returns></returns>
        private RegexOptions GetRegexOptions()
        {
            var opt = RegexOptions.None;

            // 大文字小文字の区別をしない
            if (ROptIgnoreCase.IsChecked == true)
            {
                opt |= RegexOptions.IgnoreCase;
            }
            // 複数行モード
            if (ROptMultiLine.IsChecked == true)
            {
                opt |= RegexOptions.Multiline;
            }
            // 単一行モード
            if (ROptSingleLine.IsChecked == true)
            {
                opt |= RegexOptions.Singleline;
            }

            return opt;
        }

    }
}

作り終わったらビルドし、MainWindowで「マッチング」タブを見てみましょう。

■メイン画面外枠部分の実装

「閉じる」ボタンの処理を書きます。

MainWindow.xaml
:
<StackPanel Grid.Row="1">
    <Button
        Width="150"
        Margin="12,6"
        HorizontalAlignment="Right"
        Click="CloseButton_Click"
        Content="閉じる(_C)" />
</StackPanel>
MainWindow.xaml.cs
using System.Windows;

namespace RegexTester
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void CloseButton_Click(object sender, RoutedEventArgs e)
        {
            Close();
        }
    }
}

■実行

「文字列」のテキストボックスに検索対象の文字列を入力し、
「正規表現パターン」のテキストボックスに検索する正規表現を入力し、実行ボタンを押します。
045.png
050_2.png
055_2.png
060_2.png

おしまい


<< 最初の記事   < 前の記事   次の記事 >

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

.NET 5 を使いたい理由6選

速いので使いたい

私の場合、ここ数か月で一番素晴らしいニュースだと感じたブログがこれでした。
https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-5/
.NET5 がどれだけパフォーマンス向上のために努力してきたかという内容です。
ものすごいボリュームで読むのが大変でしたが、満足感のある記事でした。
この記事を読んだだけでも、早く.NET 5 を使いたいという気持ちになりました。
パフォーマンスが良くなったという知らせはいつでもエンジニアの気持ちを高揚させるのだと思います。

使いたい理由1 : GCが高速化した

いくつものアプローチを重ねたことが記されていました。

  • GCが到達可能オブジェクトをマークする処理の情報を他のスレッドでも流用できるようにして、各スレッド内の同処理の作業量を一部省略可能にした
  • GCのGen0,1が使用する近傍のメモリのデコミット(OSにメモリ返却する処理)を最適化した。メモリが返却されてもGen0,1の領域はすぐに再確保が必要になる可能性があり、その頻度を減らすことを目的とする
  • GCの統計手法の変更により、GCのスキャン競合によるロックを減らすことに成功した。これによりマルチコア時のスケーラビリティが改善する
  • Intel AVX512 に対応することでGC内のソートを高速化した(並び替えをベクター化することに合わせて導入)
  • バックグラウンドGCの進行中にフォアグラウンドGCのサスペンド処理自体を高速化することで、ロック時間を短縮した

要約してはみたものの、ソースコードや解説を見ても分からないものも結構ありました。

使いたい理由2 : coreclr(C/C++)からcorelib(C#)への移植が進んだ

GCの最適化と並行してC/C++ネイティブ実装のcoreclrをC#実装のcorelibに移植する作業も進められたそうです。
基本的にC#は安全なものなので、ネイティブ実装が減れば減った分、安全性が増します。
しかし恩恵はそれだけにとどまりません。coreclr内のネイティブコードが動作している間、GCですらもその終了を待つことになります。つまりネイティブ実装が減りC#実装が増えたことにより、GC自体が待つことが減ります。
結果として、GC.Collect()による遅延時間は非常に短縮されます。

使いたい理由3 : Span<T>.SortによるArray.Sort

ソートは大変重い処理でありながら、私たちはこれを避けることができません。しかし、Span<T>.Sortが追加されたことで状況は変わります。
https://docs.microsoft.com/ja-jp/dotnet/api/system.memoryextensions.sort?view=net-5.0
前述のcorelib化の恩恵でソート自体が高速化しました。C#のソートは一般に配列に対するものですが、これをSpanで実行することで配列の境界チェックの処理などをかなり節約することができるようです。
またArray.Sortが内部的にSpan<T>.Sortで動作するようになり、.NET Framework 4.8と比べて約2倍、.NET Core 3.1 と比べても数十%の優位があります。

使いたい理由4 : 配列やSpan<T>に対するC#コンパイラやJITの最適化が素晴らしい

.NET Core 3.1 でもかなりの最適化が施されていましたが、さらにさらに強化されました。
最適化の多くは、何度も同じ処理を繰り返さないことと、不要と確定している判定を除去することが主体となります。

  • メソッドローカルのReadOnlySpan<T>はスタックではなくstatic領域に固定的に保持されるようになった
  • 16バイト未満の配列からの積極的な境界判定の削除
  • 初期化の必要がないSpan<T>を初期化しない

などなど。

これらについては種類が多すぎて語りつくせません。
ただ言えることは、配列の境界判定のような処理が減るだけで、コードサイズは減り、処理時間も減り、良いことづくめであるということです。
配列とSpan<T>関連についてはともかく強くなったということです。

使いたい理由5 : 型の扱いも速くなった

C#キャストは内部的にはCastHelperというクラスによってキャッシュされています。この実装が改善されたため(ref を多用した形に変わっていました)キャストの速度は格段に向上しました。
キャスト以外にも、ジェネリックメソッドのスロットの汎用化が進み、ジェネリックメソッドの呼び出しコストが小さくなりました。
このようにRuntimeHelper群の最適化が進みました。

使いたい理由6 : ライブラリ群のパフォーマンスが改善されている

  • 文字列処理
  • 正規表現
  • async/await
  • コレクション
  • LINQ
  • ネットワーキング

まとめ

このどれもが、既存のコードを書き換えずとも恩恵を受けることができるということがポイントとなります。
この記事にはソースコードやベンチマーク結果を掲載しません。なぜなら、冒頭のリンク先にすべてあるからです。
.NET 5 は素晴らしいものです。このパフォーマンス改善だけでも、.NET 5 に移行する動機づけとなる可能性があります。

謝辞

冒頭でリンクしたブログの内容は本当に素晴らしいものでした。Stephen Toub氏に感謝いたします。
また、.NET と C# の進歩と高速化に日々貢献してくださっているすべてのエンジニアに感謝いたします。

ご案内

「.NET 5 を使いたい理由」シリーズをいくつか予定しています。
次の記事は私の所属先のお客様である 弥生株式会社様の Advent Calendar 2020 で 12/3 に公開予定です。
https://qiita.com/advent-calendar/2020/yayoi
ご拝読いただければ嬉しく思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

C#とBlazorでホロライブファン向けの動画ビューワー『ホロビューワー』を開発しました

この記事について

自己紹介

  • kawa0x0Aと申します。 Twitterプロフィール
  • ゲーム系プログラマーでC#やUnityやC++が得意です。 (ゲーム業界経験5年弱です)
  • 2020年11月現在転職活動中です。
    • もしよかったらTwitterなどでお仕事のお声などをかけて頂けると助かります。

作ったもの

Windows版アプリケーション

  • WindowsとMacで動作する動画ビューワーです。
    • アプリケーションに表示されているページ内のリンクからホロライブメンバーのYoutubeページに移動できます。
    • また、画面分割ボタンを押すと指定した形でページを表示して動画を視聴できます。
      • 動画を同時視聴したいときは画面分割をお使いいただくと便利です。

開発のきっかけとかフレームワークの採用理由

  • 最近VTuberさんの動画をYoutubeで見る機会が多くなってきた
    • コラボなどで複数人が同時刻にライブ配信するケースがあり、同時に動画を見たくなる
    • いちいちブラウザを複数起動させて画面サイズを調整するのも面倒なので、なんとか解決したかった。
    • ブラウザの拡張機能とかで作ったほうがよかったんじゃない?
      • たぶんそっちのほうがよかったけど、それは言わないで
  • Blazorの勉強もしてみたかった
    • 普通はWindowsやMacなどのマルチプラットフォームで動作するアプリケーションを作ろうと思ったらElectron辺りを使うのが一般的だと思います。
    • ただ、自分としては javascriptよりC#を使いたかったので今回のようにBlazorを採用してみました。

Blazorコードについての解説

Blazor自体の解説は省略させていただきます。
アプリケーションのコードの一部ですが、Blazorは以下のようにC#とHTMLのコードを組み合わせて処理を記述できます。
HTML内では@から始まる箇所がC#の処理になります。
また、@codeブロックからは通常のC#の処理になります。

Index.razor
@page "/"

@inject ApplicationScreenMode ApplicationScreenMode

<div>
    <div class="group">
        @foreach (var group in vTuberGroups)
        {
            <ul>
                <li>
                    @group.groupName
                    // 一部処理を省略
                </li>
            </ul>
        }
    </div>
</div>

@code
{
    public class VTuberGroup
    {
        public readonly string groupName;
        public readonly VTuberProfile[] groupMembers;

        public VTuberGroup (string groupName, VTuberProfile[] groupMembers)
        {
            this.groupName = groupName;
            this.groupMembers = groupMembers;
        }
    }

    public class VTuberProfile
    {
        public readonly string fullName;
        public readonly string markIcon;
        public readonly string youtubePageUrl;

        public VTuberProfile (string fullName, string markIcon, string youtubePageUrl)
        {
            this.fullName = fullName;
            this.markIcon = markIcon;
            this.youtubePageUrl = youtubePageUrl;
        }
    }

    public static readonly VTuberGroup[] vTuberGroups =
    {
        new VTuberGroup("0期生", new VTuberProfile[]
        {
            new VTuberProfile("ときのそら", "??", "https://www.youtube.com/channel/UCp6993wxpyDPHUpavwDFqgg"),
            new VTuberProfile("ロボ子さん", "?", "https://www.youtube.com/channel/UCDqI2jOz0weumE8s7paEk6g"),
            new VTuberProfile("さくらみこ", "?", "https://www.youtube.com/channel/UC-hM6YJuNYVAmUWxeIr9FeA"),
            new VTuberProfile("星街すいせい", "☄️", "https://www.youtube.com/channel/UC5CwaMl1eIgY8h02uZw7u8A"),
        }),
        // 以下省略
    };
}

まとめ

  • BlazorはWebページの作成でもC#の経験を活かせるので便利だったので、もっと広まっていってほしい。
  • 開発したアプリケーションはリリースできたが、バグが残ってしまっているのでバグを潰しながら開発を進めていきたい。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

画面を文字認識(OCR)してみよう(C#.NET on Windows10)

はじめに

Windows10でOCRをする方法は

  1. 自分で実装する
  2. クラウドサービスを使う(AWS,GCP,Azureなど)
  3. Tesseractを使う
  4. Windows10搭載のMicrosoftOCRを使う

大体以上の4つがあります。
それぞれ利点と短所がありますが、
今回は主に④のMicrosoftOCRを使いながら、
C#.NETで画面の文字を認識を試みてみます。

なお、この記事は"公開型開発プロジェクト"PRODUCTICAの過程で作られた記事です。
ご興味がある方はTwitterの公式アカウントを是非ご覧になってみてください!
https://twitter.com/Productica_lis

使用するもの

  • Microsoft VisualStudio 2019
  • Microsoft Windows10 Software Development Kit

MicrosoftOCRについて

MicrosoftOCRは、デスクトップ版Windows10に実装されている
Optical Character Recognition(OCR)・文字認識用APIです。

様々なOCRの中でも結構精度が高いらしいです。
https://rpa.bigtreetc.com/column/microsoftocr/

使い方についてですが、
プロジェクトをUWP形式かWPF形式で作るかによって使い方が違います。

・UWPの場合
using Windows.Media.Ocr;を書くことでそのままで使うことが出来る。

・UWP以外の場合(WPFやFormsなど)
NuGetでMicrosoft.Windows.SDK.Contractsプラグインを導入する必要がある。
また、それに伴った複数の設定が必要。

以下、WPF形式でプラグインを導入して使う方法を説明していきます。

Windos.SDK.Contractsプラグインの導入

Microsoft.Windows.SDK.ContractsはUniversalWindowsのAPIをWPFなどのプロジェクトで使うことが出来るようにするプラグインです。導入することでUWP形式で使えるWindows10の標準APIを使うことができます。

導入するためには、VisualStudio上で以下の手順を行ってください。

1. [ツール]>[NuGetパッケージマネージャー]>[パッケージマネージャー設定]を開く。

2. [パッケージの管理]にある[既定のパッケージ管理形式]を
「PackageReference」に変更する(※1)

3. プロジェクトを作る(※2)

4. [NuGetパッケージの管理] を開く。

5. 検索ボックスの右側にある [プレリリースを含める] にチェックを入れる。

6. 検索ボックスに「Microsoft.Windows.SDK.Contracts」と入力して検索する。

7. 「Microsoft.Windows.SDK.Contracts」を選択し、インストールする(※3)。

8. 下記のURLよりContractsのバージョンに合うWindows10SDKをインストールする。
 過去バージョンのSDK
  https://developer.microsoft.com/ja-jp/windows/downloads/sdk-archive/
 最新バージョンのSDK
  https://developer.microsoft.com/ja-jp/windows/downloads/windows-10-sdk/

13. [参照の追加] を開く。

14. [参照(B)] ボタンを選択し、ファイル選択ダイアログを表示する。

15. 下記のファイルをそれぞれ設定(sdk-versionは任意のバージョン)(※4)
 ・System.Runtime.WindowsRuntime
  C:\Windows\Microsoft.NET\Framework\v4.0.30319
 ・System.Runtime.WindowsRuntime.UI.Xaml
  C:\Windows\Microsoft.NET\Framework\v4.0.30319
 ・System.Runtime.InteropServices.WindowsRuntime
  C:\Windows\Microsoft.NET\Framework\v4.0.30319
 ・windows.winmd
  C:\Program Files (x86)\Windows Kits\10\UnionMetadata\<sdk version>\Facade
 ・Windows.Foundation.UniversalApiContract.winmd
  C:\Program Files (x86)\Windows Kits\10\References\<sdk version>\Windows.Foundation.UniversalApiContract
 ・Windows.Foundation.FoundationContract.winmd
  C:\Program Files (x86)\Windows Kits\10\References\<sdk version>\Windows.Foundation.FoundationContract

※1.パッケージ管理システムがPackage.configだと導入時にエラーが出る場合がある。
※2.既存のプロジェクトを開く場合、パッケージ管理がPackage.configのままの場合があるので、ソリューションエクスプローラー内の参照ツリーを右クリックし、[Package.configをPackageReferenceに移行する]があればそれを押す。その後、プラグインなどの確認ウインドウが出るので問題がなければOKを押す。
※3.バージョンは幾つかあるので、使用したい・インストール済みのWindowsSDKのバージョンと合わせる。
※4.参照の追加によって競合が発生する場合があります。その場合はエラーに基づいて一部追加した参照を消してください。

画面をキャプチャする

以下のコードを使い画面全体のキャプチャを取得する。

using System.Drawing;
using System.Windows.Forms;
private Bitmap CaptureScreen(){
    //Bitmapの作成
    Bitmap bitmap = new Bitmap(Screen.PrimaryScreen.Bounds.Width,
    Screen.PrimaryScreen.Bounds.Height);
    //Graphicsの作成
    Graphics g = Graphics.FromImage(bitmap);
    //画面全体をコピーする
    g.CopyFromScreen(new Point(0, 0), new Point(0, 0), bmp.Size);
    //解放
    g.Dispose();
    return bitmap;
}

BitmapをSoftwareBitmapに変換

上記で画面の画像は取得できるが、このままではMicrosoftOCRでは処理できない。
何故なら、MicrosoftOCRのインプットはSoftwareBitmapという形式になっているからです。
そのため、取得したBitmapを以下のコードでSoftwareBitmapに変換しま。

using System.IO;
//using System.Drawing;ダブリ、単体で使う場合は有効に
using System.Drawing.Imaging;
using Windows.Storage;
using Windows.Storage.Streams;
using Windows.Graphics.Imaging;
public async Task<SoftwareBitmap> GetSoftwareSnapShot(Bitmap snap)
{
    // 取得したキャプチャ画像をファイルとして保存
    var folder = Directory.GetCurrentDirectory();
    var imageName = "ScreenCapture.bmp";
    StorageFolder appFolder = await StorageFolder.GetFolderFromPathAsync(@folder);
    snap.Save(folder + "\\" + image_name, ImageFormat.Bitmap);
    SoftwareBitmap softwareBitmap;
    var bmpFile = await appFolder.GetFileAsync(image_name);

    // 保存した画像をSoftwareBitmap形式で読み込み
    using (IRandomAccessStream stream = await bmpFile.OpenAsync(FileAccessMode.Read))
    {
        BitmapDecoder decoder = await BitmapDecoder.CreateAsync(stream);
        softwareBitmap = await decoder.GetSoftwareBitmapAsync();
    }

    // 保存した画像ファイルの削除
    File.Delete(folder + "\\" + image_name);

    // SoftwareBitmap形式の画像を返す
    return softwareBitmap;
}

一旦画像を保存して、それをsoftwareBitmap形式に読み込み直すという若干力技じみた実装ではあります。
他にスマートな方法もありそうなので、詳しい方は教えていただけると幸いです。

画像を文字認識する

ここはとっても簡単、OcrEngineを作ってSoftwareBitmapを渡すだけ。

using Windows.Media.Ocr;
//using Windows.Graphics.Imaging;ダブリ
private async Task<OcrResult> RecognizeText(SoftwareBitmap snap)
{
    OcrEngine ocrEngine = OcrEngine.TryCreateFromUserProfileLanguages();
    // OCR実行
    var ocrResult = await ocrEngine.RecognizeAsync(snap);
    return ocrResult.text;
}

上記の出力結果ocrResult.textで認識結果(String)を得ることが出来ます。

おわりに

今回はWindows10 OCR APIを使いましたが、 自分で1から実装することも良いかもしれません。そういう場合、機械学習を用いた文字認識モデルを作る手法などを使うことになります。Pythonではそれらの機械学習の実装が頻繁に行われているので、それらリソースを参照しながら各言語で実装を進めるのが良いかもしれません。

以下の記事が参考になりそうなので、貼っておきます。
【日本語OCRを作ったので解説してみる】
https://qiita.com/tanreinama/items/8fc1c8af6554654aae00
【文字認識アルゴリズムのFOTSを実装した】
https://qiita.com/jjjkkkjjj/items/bfa03d89eaf6ab0c0487#recognition

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

画面を文字認識(OCR)してみよう!(C#.NET on Windows10)

はじめに

Windows10でOCRをする方法は

  1. 自分で実装する
  2. クラウドサービスを使う(AWS,GCP,Azureなど)
  3. Tesseractを使う
  4. Windows10搭載のMicrosoftOCRを使う

大体以上の4つがあります。
今回は主に④のMicrosoftOCRを使いながら、
C#.NETで画面の文字を認識を試みてみます。

なお、この記事は"公開型開発プロジェクト"PRODUCTICAの過程で作られた記事です。
ご興味がある方はTwitterの公式アカウントを是非ご覧になってみてください!
https://twitter.com/Productica_lis

使用するもの

  • Microsoft VisualStudio 2019
  • Microsoft Windows10 Software Development Kit

MicrosoftOCRについて

MicrosoftOCRは、デスクトップ版Windows10に実装されている
Optical Character Recognition(OCR)・文字認識用APIです。

様々なOCRの中でも結構精度が高いらしいです。
https://rpa.bigtreetc.com/column/microsoftocr/

使い方についてですが、
プロジェクトをUWP形式かWPF形式で作るかによって使い方が違います。

・UWPの場合
using Windows.Media.Ocr;を書くことでそのままで使うことが出来る。

・UWP以外の場合(WPFやFormsなど)
NuGetでMicrosoft.Windows.SDK.Contractsプラグインを導入する必要がある。
また、それに伴った複数の設定が必要。

以下、WPF形式でプラグインを導入して使う方法を説明していきます。

Windos.SDK.Contractsプラグインの導入

Microsoft.Windows.SDK.ContractsはUniversalWindowsのAPIをWPFなどのプロジェクトで使うことが出来るようにするプラグインです。導入することでUWP形式で使えるWindows10の標準APIを使うことができます。

導入するためには、VisualStudio上で以下の手順を行ってください。

1. [ツール]>[NuGetパッケージマネージャー]>[パッケージマネージャー設定]を開く。

2. [パッケージの管理]にある[既定のパッケージ管理形式]を
「PackageReference」に変更する(※1)

3. プロジェクトを作る(※2)

4. [NuGetパッケージの管理] を開く。

5. 検索ボックスの右側にある [プレリリースを含める] にチェックを入れる。

6. 検索ボックスに「Microsoft.Windows.SDK.Contracts」と入力して検索する。

7. 「Microsoft.Windows.SDK.Contracts」を選択し、インストールする(※3)。

8. 下記のURLよりContractsのバージョンに合うWindows10SDKをインストールする。
 過去バージョンのSDK
  https://developer.microsoft.com/ja-jp/windows/downloads/sdk-archive/
 最新バージョンのSDK
  https://developer.microsoft.com/ja-jp/windows/downloads/windows-10-sdk/

13. [参照の追加] を開く。

14. [参照(B)] ボタンを選択し、ファイル選択ダイアログを表示する。

15. 下記のファイルをそれぞれ設定(sdk-versionは任意のバージョン)(※4)
 ・System.Runtime.WindowsRuntime
  C:\Windows\Microsoft.NET\Framework\v4.0.30319
 ・System.Runtime.WindowsRuntime.UI.Xaml
  C:\Windows\Microsoft.NET\Framework\v4.0.30319
 ・System.Runtime.InteropServices.WindowsRuntime
  C:\Windows\Microsoft.NET\Framework\v4.0.30319
 ・windows.winmd
  C:\Program Files (x86)\Windows Kits\10\UnionMetadata\<sdk version>\Facade
 ・Windows.Foundation.UniversalApiContract.winmd
  C:\Program Files (x86)\Windows Kits\10\References\<sdk version>\Windows.Foundation.UniversalApiContract
 ・Windows.Foundation.FoundationContract.winmd
  C:\Program Files (x86)\Windows Kits\10\References\<sdk version>\Windows.Foundation.FoundationContract

※1.パッケージ管理システムがPackage.configだと導入時にエラーが出る場合がある。
※2.既存のプロジェクトを開く場合、パッケージ管理がPackage.configのままの場合があるので、ソリューションエクスプローラー内の参照ツリーを右クリックし、[Package.configをPackageReferenceに移行する]があればそれを押す。その後、プラグインなどの確認ウインドウが出るので問題がなければOKを押す。
※3.バージョンは幾つかあるので、使用したい・インストール済みのWindowsSDKのバージョンと合わせる。
※4.参照の追加によって競合が発生する場合があります。その場合はエラーに基づいて一部追加した参照を消してください。

画面をキャプチャする

以下のコードを使い画面全体のキャプチャを取得する。

using System.Drawing;
using System.Windows.Forms;
private Bitmap CaptureScreen(){
    //Bitmapの作成
    Bitmap bitmap = new Bitmap(Screen.PrimaryScreen.Bounds.Width,
    Screen.PrimaryScreen.Bounds.Height);
    //Graphicsの作成
    Graphics g = Graphics.FromImage(bitmap);
    //画面全体をコピーする
    g.CopyFromScreen(new Point(0, 0), new Point(0, 0), bmp.Size);
    //解放
    g.Dispose();
    return bitmap;
}

BitmapをSoftwareBitmapに変換

上記で画面の画像は取得できるが、このままではMicrosoftOCRでは処理できない。
何故なら、MicrosoftOCRのインプットはSoftwareBitmapという形式になっているからです。
そのため、取得したBitmapを以下のコードでSoftwareBitmapに変換しま。

using System.IO;
//using System.Drawing;ダブリ、単体で使う場合は有効に
using System.Drawing.Imaging;
using Windows.Storage;
using Windows.Storage.Streams;
using Windows.Graphics.Imaging;
public async Task<SoftwareBitmap> GetSoftwareSnapShot(Bitmap snap)
{
    // 取得したキャプチャ画像をファイルとして保存
    var folder = Directory.GetCurrentDirectory();
    var imageName = "ScreenCapture.bmp";
    StorageFolder appFolder = await StorageFolder.GetFolderFromPathAsync(@folder);
    snap.Save(folder + "\\" + imageName, ImageFormat.Bitmap);
    SoftwareBitmap softwareBitmap;
    var bmpFile = await appFolder.GetFileAsync(imageName);

    // 保存した画像をSoftwareBitmap形式で読み込み
    using (IRandomAccessStream stream = await bmpFile.OpenAsync(FileAccessMode.Read))
    {
        BitmapDecoder decoder = await BitmapDecoder.CreateAsync(stream);
        softwareBitmap = await decoder.GetSoftwareBitmapAsync();
    }

    // 保存した画像ファイルの削除
    File.Delete(folder + "\\" + imageName);

    // SoftwareBitmap形式の画像を返す
    return softwareBitmap;
}

一旦画像を保存して、それをsoftwareBitmap形式に読み込み直すという若干力技じみた実装ではあります。
他にスマートな方法もありそうなので、詳しい方は教えていただけると幸いです。

画像を文字認識する

ここはとっても簡単、OcrEngineを作ってSoftwareBitmapを渡すだけ。

using Windows.Media.Ocr;
//using Windows.Graphics.Imaging;ダブリ
private async Task<OcrResult> RecognizeText(SoftwareBitmap snap)
{
    OcrEngine ocrEngine = OcrEngine.TryCreateFromUserProfileLanguages();
    // OCR実行
    var ocrResult = await ocrEngine.RecognizeAsync(snap);
    return ocrResult;
}

上記の出力結果ocrResult.textで認識結果(String)を得ることが出来ます。

おわりに

今回はWindows10 OCR APIを使いましたが、実装と検証に多くの時間が割けられるのならば、自分で1から実装することも良いと思います。そういう場合、機械学習を用いた文字認識モデルを作る手法などを考えたり実装することになります。Pythonではそれらの機械学習の実装が頻繁に行われているので、それらリソースを参照しながら各言語で実装を進めるのが良いかもしれません。

以下の記事が参考になりそうなので、貼っておきます。
【日本語OCRを作ったので解説してみる】
https://qiita.com/tanreinama/items/8fc1c8af6554654aae00
【文字認識アルゴリズムのFOTSを実装した】
https://qiita.com/jjjkkkjjj/items/bfa03d89eaf6ab0c0487#recognition

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

jsonを入れるだけで、動的にRDBのテーブル・カラムを作成してくれるライブラリ「DynamicRDB」を作ってみた

きっかけ

・ログや一時的な大量のデータ(機械学習のためのスクレイピングしたデータなど)を置いておきたい!テキストファイルで保管は嫌だ!でも、テーブルをいちいち作るのもめんどくさい!
・DocumentDBだと出来ないことがある、そしてお手軽なものが無い!
・Interfaceを使ってみたかった(個人的な理由)

というわけで、jsonを入れると動的にRDBのテーブル・カラムを作成してくれるライブラリを作ってみました

現在対応してるDBは
・SQLite
・PostgreSQL
です

実際に使ってみる

使用するJson

test.json
{
    "category": "human",
    "first_name": "Jirou",
    "last_name": "Tanaka",
    "birthday": "1999-04-23T19:38:02.929Z",
    "live": true
}

プログラム

C#
var executer = new DynamicRDBService(new SQLiteCreator(), new SqliteRepository(new SqliteDBConfig().OpendSQLiteConnection()));
var startupPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);

//json読み込み
JObject jObject = ReadJsonFile(Path.Combine(startupPath, "test.json"));

//category要素をテーブル名とする
string tableName = jObject["category"].ToString();
jObject.Remove("category");
var dbobjects = new DBobjectConverter().JsonToDBObject(jObject);

//Insertするデータと、テーブル名を指定する
executer.DynamicInsert(jObject, tableName);

private static JObject ReadJsonFile(string path)
{
    var jsonStr = ReadFile(path);
    return JObject.Parse(jsonStr);
}

結果

jsonのcategoryの値であった、「human」がテーブル名として、そのほかの要素がカラム名となってデータがInsertされました!

3.PNG

また、カラムの型もjsonと対応する型になっています
5.PNG

動的にカラムも作れる!

同じく使用するJsonと、今度はxmlも使用してみます

test2.json
{
    "category": "animal",
    "classification": "dog",
    "name": "poti",
    "age": 5,
    "weight": 6.5
}
test3.json
{
    "category": "animal",
    "classification": "cat",
    "name": "mary",
    "beard_length": 18,
    "cute":true
}
test4.xml
<xml>
    <category>animal</category>
    <classification>Tiger</classification>
    <name>tora</name>
    <age>14</age>
    <weight>59</weight>
    <cute>false</cute>
</xml>

プログラム

C#
var executer = new DynamicRDBService(new SQLiteCreator(), new SqliteRepository(new SqliteDBConfig().OpendSQLiteConnection()));
var startupPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);

jObject = ReadJsonFile(Path.Combine(startupPath, "test2.json"));
info = CreateInfo(jObject);
executer.DynamicInsert(info.Item1, info.Item2);

//AddColumn
jObject = ReadJsonFile(Path.Combine(startupPath, "test3.json"));
info = CreateInfo(jObject);
executer.DynamicInsert(info.Item1, info.Item2);

//xml(XMLを読込んで、Jsonに変換)
XmlDocument doc = new XmlDocument();
var str = ReadFile(Path.Combine(startupPath, "test4.xml"));
doc.LoadXml(str);
string jsonText = JsonConvert.SerializeXmlNode(doc);
var jObjectTemp = JObject.Parse(jsonText);
jObject = new JObject();

foreach (var j in jObjectTemp["xml"].Children())
    jObject.Add(j);

info = CreateInfo(jObject);
executer.DynamicInsert(info.Item1, info.Item2);


private static JObject ReadJsonFile(string path)
{
    var jsonStr = ReadFile(path);
    return JObject.Parse(jsonStr);
}

private static string ReadFile(string path)
{
    string str = string.Empty;
    using (StreamReader sr = new StreamReader(path))
    {
        str = sr.ReadToEnd();
    }
    return str;
}

private static (IEnumerable<DBObject>, string) CreateInfo(JObject json)
{
    string tableName = json["category"].ToString();
    json.Remove("category");

    var dbobjects = new DBobjectConverter().JsonToDBObject(json);

    return (dbobjects, tableName);
}

結果

test2.jsonがDynamicInsertされた時点でのDB
6.PNG

それに続き、test3.json、test4.xmlがDynamicInsertされた時点でのDB
test2.jsonには無く、test3.json、test4.xmlに存在した要素「beard_length」、「cute」がカラムとして追加されています
7.PNG

マルチインサートも対応しています

何故かというと、DynamicInsertはInsertのたびにテーブル存在チェックとカラム存在チェックをしているので、動作が遅いからです
DynamicMultiInsertでは、配列の1番目を元にテーブル、カラムを作成し、それ以降のテーブル変更はありません
(なので、バラバラの構造の配列の場合エラーになる)

使用するJson

test5.json
{
    "array": [
        {
            "category": "fruits",
            "name": "apple",
            "price": 150,
            "sweetness": 5.5
        },
        {
            "category": "fruits",
            "name": "orange",
            "price": 120,
            "sweetness": 3.5
        },
        {
            "category": "fruits",
            "name": "ruby-roman",
            "price": 15000,
            "sweetness": 24
        }
    ]
}

プログラム

C#
jObject = ReadJsonFile(Path.Combine(startupPath, "test5.json"));
List<IEnumerable<DBObject>> dBObjects = new List<IEnumerable<DBObject>>();

string dbName = string.Empty;
foreach (JObject j in jObject["array"].Children()){
    var dbinfo = CreateInfo(j);
    dBObjects.Add(dbinfo.Item1);
    dbName = dbinfo.Item2;
}
executer.DynamicMultiInsert(dBObjects, dbName);

課題

一通りの機能はできましたが、まだ課題がたくさんあります
・SQLは大文字小文字区別されないため、存在チェックでエラーになる場合がある
・SQLインジェクションはめちゃくちゃ出来る状態
・Jsonの入れ子は対応していない。再帰処理使えば出来ると思うがあまりやる気が起きない
・内部にTableDefinitionのリストを持って、DBに接続せずカラム存在チェックをしよう
・TableDefinitionの際、テーブル名が大文字だと取得できなかったりする?
 →インサートの時にテーブル名が勝手に小文字にってる

最後に

課題はたくさんありますが、意外とサクッと出来ました
データベースを作成するDDLやら、interfaceやら(複数DB対応したので使った)中々書いたり使ったりする機会がないので
それらを経験できたのは良かったなーっておもいます

ここで紹介したライブラリのgitリポジトリは下記になります(DynamicRDBExampleにここで書かれてるコードがあります)
https://github.com/HawkClaws/DynamicRDB

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

dotnet-5.0のシングルファイルアプリについて

はじめに

dotnetはその仕組み上、普通に構築すると多数のアセンブリを伴う実行ファイルとなる場合が多い。
これはウェブアプリとして展開する場合等は問題にならないが、コンソールアプリケーションとして展開したい場合、不便になることが多い。

そこで、dotnetではシングルファイルのバイナリとして生成物をまとめる機能がついているが、dotnet-5.0でどうなっているか、使う上でポイントとなる部分を書いていこうと思う。

事前知識: msbuildのプロパティについて

記事中msbuildのプロパティについて言及するが、設定方法がいくつかあるので、簡単に書く。
msbuildプロパティについては他にもいくつかルールがあったりするが、詳細は公式ドキュメントを見てほしい

プロジェクト単位のプロパティ

プロジェクト単位でプロパティを設定する場合、ビルド時にコマンドラインから指定する方法と、予めprojファイルに入れておく方法がある。
コマンドライン指定の場合、msbuild /p:[プロパティ名]=[プロパティの値]のように設定する。
dotnet publishコマンドでビルドする場合、"dotnet publish -p:[プロパティ名]=[プロパティ値]`のように指定する

projファイルに書き込む場合、下記のようにする

app.csproj
<PropertyGroup>
  <プロパティ名>[プロパティ値]</プロパティ名>
</PropertyGroup>

同じプロパティ名が複数指定された場合、最後に読まれたものが採用される

アイテム単位のプロパティ

例えば個別のファイルごとにプロパティを設定する場合は、以下のようにする。

app.csproj
<ItemGroup>
  <!-- 新しく指定する場合 -->
  <アイテム種別 Include="[globパターン]" プロパティ名1="プロパティ値1">
    <プロパティ名2>プロパティ値2</プロパティ名2>
  </アイテム種別>
  <!-- 既存のアイテムに追加したい場合 -->
  <アイテム種別 Update="[globパターン]" プロパティ名1="プロパティ値1>
    <プロパティ名2>プロパティ値2</プロパティ名2>
  </アイテム種別>
</ItemGroup>

PublishSingleFile

この件について、まず挙げられるのがPublishSingleFileフラグだろう。
dotnet publishの実行時、PublishSingleFile=trueプロパティを追加すれば、付随するアセンブリファイルが全て一つの実行可能ファイルにまとめられる。
また、必ずネイティブバイナリを含むため、publish時のRuntimeIdentifierプロパティ指定(またはdotnet publishコマンドの--runtimeオプション)は必須となる
ユーザーの見た目的にはあまり変わった所は無いが、その実dotnet core-3.1とdotnet 5ではその中身が異なっている。

フレームワーク依存型と自己完結型について

dotnetのpublishには二つのまとめ方があり、フレームワーク依存と自己完結がある。PublishSingleFileは両方に対応している。
前者は、ベースクラスライブラリとランタイムをバイナリに含まないことで、サイズを小さくできるのに対し、
後者はランタイムを含めることで、ランタイムがシステムに無くても動作できるようになるが、サイズは飛躍的に増大する。

サイズについて具体的に言うと、HelloWorldレベルでは前者が精々150kb弱程度に対し、後者は何もしないと60MB超まで大きくなる。

なお、PublishSingleFileを設定した場合、デフォルトでは自己完結型になるので、フレームワーク依存型にしたい場合は、
dotnet publish--self-contained=falseオプションを追加するか、msbuildプロパティでSelfContained=falseを指定する。

また、PublishSingleFileを設定しない場合、デフォルトはRuntimeIdentifierを指定しない場合はフレームワーク依存型、指定する場合は自己完結型になるので、この辺りの違いにも注意すること。

デフォルト値の関係をまとめると、以下のようになる。

puglishsinglefile-default.png

除外

例えばプラグインとして利用しているdll等、まとめたくないようなファイルがある場合、個別のアイテムにExcludeFromSingleFile=trueプロパティを設定することで、統合されるのを回避することができる。

csproj
<ItemGroup>
  <None Update="hoge.txt" ExcludeFromSingleFile="true"/>
</ItemGroup>

dotnet core-3.1の方式(ディスク展開方式)

この方式の場合、publish時に実行可能ファイルにアセンブリ群が圧縮されて格納される。
この実行可能ファイルを実行すると、以下のような挙動をする

  1. テンポラリフォルダに圧縮されたファイル群を展開する
    • `[ベースパス]/[アプリケーション名]/[ID]"に展開される。ベースパスは、"DOTNET_BUNDLE_EXTRACT_BASE_DIR"環境変数が設定されていればそこを起点にするが、未設定の場合は以下のようになる
      • Windows: %TEMP%\.net
      • Linux,macOS: $TMPDIR/.net/$UID,/var/tmp/.net/$UID,/tmp/.net/$UIDのどれか
  2. 圧縮されたファイル群の中に含まれる起点となる実行可能ファイルを実行する
  3. 後は通常の実行と同じ

この方式では、最初に展開の手間はかかるものの、既存の仕組みから大きな変更をすることなくアプリを実行することが可能になる。
ただし、いくら一時フォルダとはいえ実行時にファイルを展開するのは、ディスク圧迫、あるいは余計なIO負荷につながるという欠点がある。

また、dotnet 5でも IncludeAllContentForSelfExtract=true をmsbuildのプロパティに追加することで、self extractedにすることができる。

dotnet 5.0の方式(埋め込み方式)

依存アセンブリ等の付随するファイルを生成された実行可能ファイルに格納するのは3.1と同じだが、テンポラリフォルダに展開せずに、ファイルから直接アセンブリを読みだして実行できるようにした。(実際は
詳細は dotnetのドキュメントに記載されている が、通常のものと比べて若干動作が違ってくる機能がある。

埋め込み方式に必要なランタイムについて

dotnet core-3.1(netcoreapp3.1)の時は、特に何も設定せずとも単一の実行可能ファイルができるだけだったが、net5.0では、以下のネイティブバイナリが依存バイナリとして同じフォルダに存在する必要がある。

  • coreclr.dll: ランタイム本体
  • clrjit.dll: JITコンパイラ
  • clrcompression.dll: 解凍/圧縮用ライブラリ

基本的に必要なものは上記で、他にデバッグ用バイナリが付随することがある。

更にこれらのファイルもまとめたい場合は、msbuildのビルドプロパティに IncludeNativeLibrariesForSelfExtract=true を追加する(+5~10MB程度)

サイズの削減について

特に追加設定せずにPublishSingleFileすると、HelloWorldレベルのアプリでも60MB以上になる。
このほとんどは実際は使われていないようなライブラリのファイルで占められている。
コンパイル時にPublishTrimmed=trueをビルド時のプロパティに追加すれば、使っていないと判断されたDLLを生成物から除外してくれる。
これにより、HelloWorldレベルならば20MB弱まで削減できる。まだ大きいと言われればまあそれはそうなんだけど。

ただし、リフレクション等を多用するようなライブラリを使用する場合は、誤判断が発生して除外される可能性もあるため、必ず生成物をテストするように推奨されている。

また、dotnet 5.0からは、"TrimMode"プロパティに"Link"を設定することによって、従来のアセンブリ単位の削減から更に踏み込んで、フィールドやメソッド等のメンバー情報も削減するという動作をするようになる。
実際これを設定してHelloWorldレベルのアプリをpublishしたところ、10MB超程度になった。
ただし、こちらは更に積極的に削減を行うオプションのため、従来からは起きなかったトラブルが起きる可能性もあるため、使用する際は要注意。

その他、明示的に削減されるのを回避したい場合の設定などは、MSの公式ドキュメントを参照のこと

Publishの違いによるAPIの動作変化

シングルファイルの方式により、一部のAPIが以下のような影響を受ける

方式 Assembly.Location AppContext.BaseDirectory
PublishSingleFileしない 該当アセンブリ(dllまたはexe)のアセンブリパス 実行可能ファイルがあるフォルダ
ディスク展開方式 展開されたフォルダにあるアセンブリパス 展開されたフォルダ
埋め込み方式 常に空文字列 実行可能ファイルがあるフォルダ

特にAssembly.Locationが空になるのは注意したい。
その他の影響としては、 MSの公式ドキュメントを参考にしてほしい

終わりに

一通りなぞってみたが、実際内部の仕組み等に目を向けると色々と面白い所もあるので、気が向いたらその辺りの話も書けたらいいなと思っている。
また、HellOWorldで10MBという数字はコンソールアプリとしてはまだ大きい方というのは確かで、更に不満がある人はNative AOT(Ahead Of Time)コンパイルという手法もあるので、興味のある人は調べてみると良いと思う。

参考リンクまとめ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

LineRendererで割ときれいな線を描いてみる

はじめに

この記事は以下のサイトの記事を参考にさせていただいてます
https://qiita.com/7of9/items/3750d30590e3efcfd389

注意

最適化までは行っていないのでその点はご注意ください

仕様的なもの

  • 受け渡されたor設定された座標をつなぎ合わせて1本のきれいな線を描く
  • Z軸は使用しない

コード部分

DrawLine.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DrawLineUI : MonoBehaviour
{
    [SerializeField] private Material lineMaterial;  // 線に使うマテリアル
    [SerializeField] private Color lineColor;   // 線の色
    [SerializeField] private float lineWidth;    // 線の太さ
    [SerializeField] private List<GameObject> objList;    
    [SerializeField] private List<Vector2> positionObjList;

    // 追加されたゲームオブジェクトのリスト
    private List<GameObject> addGameObjectList = new List<GameObject>();

    void Start () 
    {
        DrawLine();
    }

    private void DrawLine()
    {
        List<Vector3> positionList = new List<Vector3>();
        positionList = StoringVector2ToVector3(positionObjList);
        for(int index = 0;index < positionObjList.Count - 1;++index)
        {
            CreateLine(positionObjList,index);
        }
    }

    // Vector2のリストを持ってきて、SetActiveを確認しながらCreateLineに使うListに格納していく
    private List<Vector3> StoringVector2ToVector3(List<Vector2> positionObjList)
    {
        List<Vector3> positionList = new List<Vector3>();
        for(int index=0;index<positionObjList.Count;++index)
        {
             if(objList[index].active == false)
             {   //gameObjectが設定されていない場合に飛ばす
                 continue;
             }
         }
        positionObjList.Add(positionList[index]);

        return positionObjList;
    }

    // 新しいオブジェクトを作ってLineRenderともろもろの設定を突っ込むとこ
    private void CreateLine(List<Vector3> vec3List, int indexNum)
    {
        List<Vector3> myPoint = new List<Vector3>();    // LineRendererのPositionを設定するのに使用
        for(int idx = 0; idx < 2; idx++) 
        {
            myPoint.Add(new Vector3(vec3List[indexNum + idx].x, vec3List[indexNum + idx].y, 0.0f));
        }
        // 生成するオブジェクト名
        GameObject newLine = new GameObject ("Line"+addGameObjectList.Count);   
        addGameObjectList.Add(newLine);
        LineRenderer lineRenderer = newLine.AddComponent<LineRenderer> ();
        lineRenderer.useWorldSpace = false;
        lineRenderer.SetVertexCount(2);
        lineRenderer.SetWidth (lineWidth, lineWidth);
        lineRenderer.SetColors(lineColor,lineColor);    // 色の設定
        if(lineMaterial != null)
        {
            lineRenderer.material = lineMaterial;
        }
        Vector3 startVec = myPoint[0];
        Vector3 endVec   = myPoint[1];
        lineRenderer.numCapVertices = 90;
        lineRenderer.SetPosition (0, startVec);
        lineRenderer.SetPosition (1, endVec);
    } 
}

最後に

LineRenderer複雑ですね~
もうちょっと簡単に線を実装できたらいいんですけど・・・・

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Azure Functions で長めの処理時間をかける関数(C#)

はじめに

Azure Functions では、SKU によって処理上限時間が決まっています (参考)。
これを試験とかで試してみる用に関数を作成したので、とりあえず公開しておこうかなと思います。

環境

  • .NET Core 3.1

コード

Visual Studio 2019 のテンプレートから HTTP Trigger で作成し、ちょちょっと変えただけですが。

using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;

namespace LongRunFunctions01
{
    public static class Function1
    {
        [FunctionName("LongRunFunction01")]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            log.LogInformation("C# HTTP trigger function processed a request.");

            // デフォルトでは5秒実行
            int sec = 5;

            // sec というパラメータに入れた数字を秒数として、その時間だけ実行する
            if (req.Query.ContainsKey("sec"))
            {
                sec = int.Parse(req.Query["sec"]);
            }

            for (int i = 0; i < sec; i++)
            {
                await Task.Delay(1000);
                if(i % 10 == 0)
                {
                    log.LogInformation($"{i} sec");
                }
            }

            string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
            dynamic data = JsonConvert.DeserializeObject(requestBody);

            string responseMessage = "This HTTP triggered function executed successfully. ";

            return new OkObjectResult(responseMessage);
        }
    }
}

Usage

下記のようにアクセスすると、100秒間実行しようとします。

<URL>/LongRunFunction01?sec=100

さいごに

インフラエンジニアにとっては、こういうインフラの動きを試すためにコードを書く必要が出てきたりしていますね。良い勉強になるので、今後も色々と試してみようと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ハッシュ値を取得・検証するWindows Formアプリ(.NET Core, C#)を作成・GitHubで公開してみた

タイトル通りです。
実は以前にこういう記事を書きました。

何でまたハッシュとか思われそうですが、C#とアプリ開発の勉強もあるのですが、ときどき使用することがあるので、いちいちコマンドラインで出さなくても良いようにするためです。

公開してる場所

↓のGitHubに置いています。
まぁ、フリーソフトというかオープンソースです。

screenshot.png

Git(GitHubも)の使い方、最初は全然分かりませんでしたが、徐々に使い慣れてきました。

git initで新規作成して、
git config user.nameでユーザー名を登録して(メールアドレスも)、
git remote add origin ...して、
git addでファイル追加して、
git commit -m コメントでコミットして、
git push ...という流れでしょうか…。
(事前にGitHubのリポジトリ作成する必要あるけど)

除外したいファイルを事前に指定する方法が知りたいのですが、まだそこには至っていません。

機能

  • 入力ソースは文字列・ファイル両方に対応
  • 比較ができる(ダウンロードしたファイルのハッシュ値突合で使える)
  • .NETで利用できる主要なハッシュ値には対応した(MD5, SHA1, SHA256, SHA386, SHA512)。
  • 引数にファイルパスを指定可能(起動時にハッシュ値は自動で出力)。
  • TextChangedイベントで、リアルタイムで出力する(いちいちボタンを押すのが面倒だから)
  • ファイルはドラッグアンドドロップでも指定可能

基本的には.NET Coreの機能を使用してるだけのGUIラッパーだと思います。それを使いやすくしたものです(使いやすくできてるといいのだけど)。

開発にあたっての所感とか

イベントドリブンやっぱり面倒

コンソールアプリと違って、イベントドリブンだと処理のパターンが単純ではないので、中身のロジックをどうすればすっきりするか、結構悩みます。

まぁ、前職でもVB.NETで開発はしていましたが、どうしてもごちゃごちゃしてしまうのが難点(スキル不足かもしれませんが)。

ドラッグアンドドロップの実装も初めてしてみました

2つのイベントを使用して書かないといけないんですね…。何気にコードがごちゃごちゃします。

        private void txtCharas_DragEnter(object sender, DragEventArgs e)
        {
            if (e.Data.GetDataPresent(DataFormats.FileDrop))
            {
                e.Effect = DragDropEffects.All;
            }
            else
            {
                e.Effect = DragDropEffects.None;
            }
        }

        private void txtCharas_DragDrop(object sender, DragEventArgs e)
        {
            string[] dropdata =  (string[])e.Data.GetData(DataFormats.FileDrop);

            if (dropdata.Length > 0)
                this.txtCharas.Text = dropdata[0];
        }

あと、PBKDF2も入れようか迷いましたが、特にパスワード保管用のハッシュ値をわざわざ手作業で作る必要も今のところないので、これは省きました。あと、MD5やSHA256は継承元のクラスが同じですが、PBKDF2については違いますので、追加したら分岐がさらに増えます。

Windowアプリケーションの開発ってもっと楽にならないかなぁ……。WPFとかUWPも使ってはみたいのだけど、前者どうも普及しているかよく分からないし、後者もまだ様子見の段階。

すごく走り書きの記事になりましたが、どうぞ、温かい目で見て頂けると嬉しいです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む