20191215のC#に関する記事は13件です。

C#+Excel:選択したオートシェイプの文字列編集能力を奪い取る

オートシェイプのテキスト絡みで
・Excel2016で、矢印キーでIカーソルが見かけ上動かない問題
・Excel2013で、文字列表示とIカーソル位置が横方向に少しずれる問題
が、くそウザかったので、暫定対策すべくツールをつくってみた。1
(最新版のupdateあてれば治るのかもしれないが・・)

画面キャプチャ

image.png

使い方

  1. Get AutoShapeボタンを押す
  2. Excel上で文字列を含むオートシェイプをクリックして選択する
  3. Stop/Fixボタンを押す
  4. テキストを編集すると、Excel上のオートシェイプのテキストも連動して変化する

注意点

  • Excelのオートシェイプ上の文字列が、部分ごとに書式が違う場合、書式情報が抹消されます。
  • 編集対象のオートシェイプをもつExcelのシート閉じたりオートシェイプを消した状態でテキストを編集すると例外で落ちるはず。
  • Excel上でも編集できてしまい、アプリには反映されない。(タイトル詐欺)

コンパイル方法

Windows10 Excel2016環境
(Excelがインストールされてれば、環境違ってもC:\Windows\assembly\GAC_MSIL\以下をdir /s なんちゃら.dllで探せば出てくるはず。)

csc ^
 /r:C:\Windows\assembly\GAC_MSIL\Microsoft.Office.Interop.Excel\15.0.0.0__71e9bce111e9429c\Microsoft.Office.Interop.Excel.dll ^
 /r:C:\Windows\assembly\GAC_MSIL\office\15.0.0.0__71e9bce111e9429c\Office.dll ^
 %*

C#ソースコード

using System;
using System.Drawing;
//using System.Collections.Generic;
//using System.Reflection;
using System.Runtime.CompilerServices; // to use [MethodImpl(MethodImplOptions.NoInlining)]
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Windows.Forms;

using Excel = Microsoft.Office.Interop.Excel;
//using Microsoft.Office.Core;

class ExcelAutoshapeTest : Form
{
    System.Windows.Forms.Timer timer;
    TextBox txtName;
    TextBox txtContent;
    Button btnStartTryGet;
    dynamic _oShape;
    bool _updatedByProgram;

    [MethodImpl(MethodImplOptions.NoInlining)]
    static void DumpTextOfActiveSheet()
    {
        var oExcelApp = (Excel.Application)Marshal.GetActiveObject("Excel.Application");

        var oSheet = (Excel.Worksheet)oExcelApp.ActiveWorkbook.ActiveSheet;

        var oShapes = (Excel.Shapes)oSheet.Shapes;
        foreach ( Excel.Shape oShape in oShapes ) {
            Console.WriteLine(oShape.Type);
            if (oShape.Type == Microsoft.Office.Core.MsoShapeType.msoAutoShape) {
                Console.WriteLine(oShape.AutoShapeType);
                try {
                    dynamic oTextFrame = oShape.TextFrame;
                    Console.WriteLine(oTextFrame.Characters.Text);
                }
                catch(Microsoft.CSharp.RuntimeBinder.RuntimeBinderException) {
                    //Console.WriteLine(e);
                }
                catch(COMException) {
                    //Console.WriteLine(e);
                }
            }
            //oTextFrame.Characters.Text = "bbb";
        }
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    void TryGetActiveAutoShape()
    {
        var oExcelApp = (Excel.Application)Marshal.GetActiveObject("Excel.Application");

        dynamic t = oExcelApp.Selection;

        try {
            _updatedByProgram = true;
            _oShape = null;
            txtContent.Text = Regex.Replace(t.Characters.Text, @"\n", "\r\n", RegexOptions.Multiline);
            txtName.Text = t.Name;
            _oShape = t;
        }
        catch(Exception e) {
            if (e is Microsoft.CSharp.RuntimeBinder.RuntimeBinderException ||
                e is COMException ) {  // Note: C#6以降なら when を使ってもう少しきれいに書ける

                _oShape = null;
                txtName.Text = "";
                txtContent.Text = "";
                // もみ消す
                return;
            }
            throw e;
        }
        finally {
            _updatedByProgram = false;
        }
    }

    void UpdateExcelAutoShape()
    {
        if ( _oShape != null ) {
            string s = Regex.Replace(txtContent.Text, @"\r\n", "\n", RegexOptions.Multiline);
            _oShape.Characters.Text = s;
        }
    }


    ExcelAutoshapeTest()
    {
        _oShape = null;

        Controls.Add(txtName = new TextBox(){
            Location = new Point(0,0),
            Width = 150,
            ReadOnly = true
        });


        Controls.Add(btnStartTryGet = new Button(){
            Text = "Get AutoShape",
            Location = new Point(180, 0),
            Width = 100
        });
        btnStartTryGet.Click += (s,e)=>{
            if (timer.Enabled) {
                ((Button)s).Text="Get AutoShape";
                txtContent.ReadOnly = false;
                timer.Stop();
            }
            else {
                ((Button)s).Text="Stop/Fix";
                txtContent.ReadOnly = true;
                timer.Start();
            }
        };


        Controls.Add(txtContent = new TextBox(){
            Location = new Point(0,30),
            ScrollBars = ScrollBars.Both,
            Multiline = true,
            WordWrap = false
        });
        txtContent.TextChanged += (s,e)=>{if(!_updatedByProgram){UpdateExcelAutoShape();}};


        Load      += (s,e)=>{MyResize();};
        Resize    += (s,e)=>{MyResize();};
        ResizeEnd += (s,e)=>{MyResize();};

        timer = new System.Windows.Forms.Timer();
        timer.Interval = 1000;
        timer.Tick += (s,e)=>{
            TryGetActiveAutoShape();
            GC.Collect();
            GC.WaitForPendingFinalizers();
        };
    }

    void MyResize()
    {
        int w = ClientSize.Width;
        int h = ClientSize.Height - txtContent.Top;
        if(w<50){w=50;}
        if(h<50){h=50;}
        txtContent.Size = new Size(w,h);
    }

    [STAThread]
    static void Main(string[] args)
    {
        //DumpTextOfActiveSheet();
        Application.Run(new ExcelAutoshapeTest());
    }
}

参考サイト


  1. そもそもExcelは表計算ツールなのでオートシェイプ使う事自体がおかしなことかもしれない 

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

C#で変数名を一括で変更する

C# で変な変数名を書いてしまい、後で変更する場合、

例えば、下記コード

実行

exe.gif

一瞬で分かりにくいが、変数 a は変数名がよろしくない

そこで、a を apple に変更したい。

文字列置換だと、ヤバい状態になるのは分かると思う。イマジン!

操作は簡単で、宣言されている場所の 

string a="りんご";

a を

apple に変更して CTRL+. Enter

apple.gif

以上

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

知っててもあまり役に立たない構文私的六選

前口上

これは、C# その2 Advent Calendar 2019の20日目の記事です。

個人的にC#を使っていて、こんな機能有ったんだとか、こんなコトできたんだみたいなあんまり役に立たないけどクスリと笑えそうなモロモロをセレクトしてみました。

なので、既知だよ~って言う突っ込みは無しの方向で一つ、ご笑覧頂ければ幸い

割とどこにでも書けるconst

さて、1つ目はconstに関するお話。constが引き起こすあれやこれやは別として、以下のように書けちゃったりする。

using System;

namespace Advent
{
    class Program
    {
        static void Main(string[] args)
        {
            const string helloWorld = "hello world";
            Console.WriteLine(helloWorld);
        }

        public static int SomeProperty
        {
            get
            {
                const int allAns=42;
                return allAns;
            }
            set
            {
                const string header = "Your input is:";
                Console.WriteLine(header + value);
                //本来はStore処理をすべきだけど省略
            }
        }
    }
}

このように、クラスや構造体のフィールドだけでは無く、メソッドやプロパティの中にも書ける。

ちょっとしたベンチマークテスト書きたいときとか、constでパス指定とかしたり、iteration何かを定数化しておくと、わざわざ書き直す必要も無いし、変数と違って途中で書き換えが出来ないのでわりかし便利かも知れない。

使える場所が超限定の::演算子

C++で完全修飾名にするときとか、std::coutみたいに書くけど、これに近いことが出来る…けどかなり用途先が限定されている。

実例は以下の通り。

using txt=System.Text;
using sys=System;
using io=System.IO;


namespace Advent
{
    class Program
    {
        static void Main(string[] args)
        {
            const string inputText = "Hello C#!";
            var buff = new txt::StringBuilder();

            {
                using var stream = new io::StringWriter(buff);
                stream.WriteLine(inputText);
            }

            sys::Console.WriteLine(buff.ToString());
        }
    }
}

かなり回りくどいけど、usingエイリアスディレクティブを使って作成した名前空間のエイリアスに限り::を使って修飾が可能になる。なので、System::Consoleみたいなことは書けないので注意。ただこれ、当然一般的なメンバアクセス演算子.使って、new txt.StringBuilder()でも全然イケてしまうので通常はそっち使った方が良いと思う。

じゃあなんでこれが必要かというと、名前被っちゃったときの対策とのこと。実例は以下

using sys=System;


namespace Advent
{

    //命名則に反してるけど笑って許して
    public class sys
    {
        public class Console
        {
            public static void WriteLine(string value) =>
                System.Console.WriteLine($"This is \"sys.Console\" class's WriteLine:{value}");
        }

    }

    class Program
    {
        static void Main(string[] args)
        {
            sys::Console.WriteLine("hello C#!");
            sys.Console.WriteLine("hello C#!");

            //output
            //hello C#!
            //This is "sys.Console" class's WriteLine:hello C#!
        }
    }
}

命名則としてどーよというのは激しくあるけど、早い話上記みたいなシナリオでどっちを呼ぶのか弁別する必要があるので、この演算子が必要だったとのこと。また、子細は以下に譲るけど、唯一この演算子でしか解決できないモノにglobal::ってものがある。

ただ、繰り返しになるけどエイリアスはソースコードのファイル単位のスコープしか持ってないので、こんなことしなければならないって言うのは、割と問題があるケースが多いと思うし、書くなら書いたで書いた理由は説明できないとマズいかなとは思う。

using static

C#のver6から導入されたクラスや構造体の静的メソッド/プロパティを修飾無しで呼び出せるdirectiveだけど、実はこれ、それだけじゃ無く、指定されたクラスや構造体の入れ子になっているクラス/構造体も修飾無しで呼び出せる。

下記のようなアセンブリがあったとして

namespace ExternalLib
{
    public static class Envelope
    {
        public class Some
        {
        }

        public static class Nested
        {
            public static int Add(int x, int y) => x + y;
        }

        public static int Calc(int x, int y) => x / y;

    }

}

こいつを、参照してるコンソールアプリあたりでこんな風に書ける。

using System;
using static ExternalLib.Envelope;

namespace Advent
{
    static class MainEntry
    {
        public static void Main()
        {
            Console.WriteLine(Nested.Add(10, 20));
            Console.WriteLine(Calc(100, 20));
            var some = new Some();
        }
    }
}

最初、文字通り、静的な諸々のみが修飾無しで呼び出せるだけだと考えていたので、ちょっとびっくりした。また、当然入れ子になったクラス/構造体に対してもusing staticは使える。

using System;
using static ExternalLib.Envelope;
using static ExternalLib.Envelope.Nested;
namespace Advent
{
    static class MainEntry
    {
        public static void Main()
        {
            Console.WriteLine(Add(10, 20));
            Console.WriteLine(Calc(100, 20));
        }
    }
}

使い道としては、割と局所的だし、こじつけっぽいけどメソッドの階層化とかにはちょっとだけ使えるかもとは思ったり。

using System;
using System.Collections.Generic;
using System.Linq;

namespace ExternalLib
{
    public static class Calculate
    {
        public static class Integer
        {
            public static int Add(int x, int y) => x + y;
        }

        public static class Triangle
        {
            public static double Sin(double a) => Math.Sin(a);
        }

        public static class Statistics
        {
            public static double Average(IEnumerable<double> collection) => collection.Average();
        }
    }
}

こんな風に書いておけば

using System;
using System.Linq;
using static ExternalLib.Calculate;
using static System.Linq.Enumerable;

namespace Advent
{
    static class MainEntry
    {
        public static void Main()
        {
            Console.WriteLine(Integer.Add(10, 20));
            Console.WriteLine(Triangle.Sin(0.733));
            Console.WriteLine(Statistics.Average(Range(0, 200).Select(x => (double) x)));

        }
    }
}

こんな感じ書ける。

ただまぁ、何かそれほどありがたみが無いなぁって感じが強い。

new付きの諸々

C#にはnew修飾子ってのがあり、有り体に言えば継承された諸々を明示的に隠蔽することが出来る。で、これ別にvirtualのついて無いメソッド/プロパティだけじゃ無くありとあらゆるモノが隠蔽できる。

using System;

namespace Advent
{
    public class Base
    {

        public class Nested
        {
            public Nested() => Console.WriteLine("Base.Nested");
        }

        public int IntField = 100;
        public int Hoge() => 42;
    }

    public class Derived : Base
    {
        public new class Nested
        {
            public Nested() => Console.WriteLine("DerivedA.Nested");
        }

        public new int IntField = 200;
        public new int Hoge() => 114514;
    }





    static class MainEntry
    {
        public static void Main()
        {
            Base up(Derived d) => d;

            var a = new Derived();

            Console.WriteLine(a.IntField);
            //200

            Console.WriteLine(up(a).IntField);
            //100

            a.IntField = 42;

            Console.WriteLine(a.IntField);
            //42

            Console.WriteLine(up(a).IntField);
            //100

            up(a).IntField = 114514;

            Console.WriteLine(a.IntField);
            //42

            Console.WriteLine(up(a).IntField);
            //114514


            //当たり前だけど違う型なので変数も別々
            var b = new Base.Nested();
            //Base.Nested

            var c = new Derived.Nested();
            //Derived.Nested
        }
    }
}

メソッドやらプロパティでも割と混乱するけど、フィールドになるとそいつに拍車がかかる。名前は一緒だけど別々に割りあてられてるメンバフィールドって言う扱いになるので、どー修飾されたかで結果が変わるという地獄絵図にw

同様に、Nestedクラスに関してもBase.Nestedと、Derived.Nestedが別個の実体として存在するので、管理が面倒なことになる。

この隠蔽に関しては、利用側が隠蔽されているのか否かをすぐにわかりずらく、また一般とは違うメンバールックアップが行われるので余程強い理由があるときに限り使うべきだと思う。

特にフィールドに対する隠蔽は同名のフィールドが別々に存在するというどー見ても地雷踏みそうな状況を作るので、余程のことがあったとしても使うべきじゃないし、こんなことするなら設計が歪んでいる方をまず疑うべきじゃ無いかと思う。

構造体の部分初期化

ローカル変数を初期化しないまま参照しようとした場合、コンパイルエラーになるけど、フィールドに限っては部分初期化に対応している。以下のように書いた場合

using System;

namespace Advent
{

    static class MainEntry
    {
        public static void Main()
        {
            ValueTuple<int, int> point;

            point.Item1 = 100;
            point.Item2 = point.Item1 + 200;

            Console.WriteLine(point);
        }
    }
}

13行目でコンパイルエラーになることは無い。

但し、これはフィールドに限った話で、部分初期化しかされていない状態で、直接関係の無いメソッドやプロパティの呼び出しを行うと,いつも通りCS1065が発生する。

namespace Advent
{
    public struct IntegerPoint
    {
        public  int X;

        public void Some() { }


        public int Y { get; set; }
    }


    static class MainEntry
    {
        public static void Main()
        {
            IntegerPoint point;

            //これは出来る。
            point.X = 100;

            //property setterの呼び出しはNG(CS0165発生)
            point.Y = 100;

            //当然、何も関係なくてもメソッドの呼び出しもNG(CS0165発生)
            point.Some();
        }
    }
}

用途としては、まさにさっきのタプルの例のように、一部を初期化して、初期化済みの値を使いつつ他のフィールドの初期化を行う時には少し使い道があるかな?と思える程度。

ただ、とち狂ったことしても問題が局所化されやすいしコンパイルエラーになるので、危険性はそこまで無いかなとは思ったり。

namespaceにくるまれていないモノ達

VisualStudioであれ、Riderであれ新たにソースコードファイルを作成すると、規定の名前空間に従った名前空間が作成されてその空間の中に各種実装を書いていくのが一般的じゃ無いかなと思う。

それじゃ、名前空間にくるまず以下のようなモノを書いたとして

public class NoEnveloped
{ }

こいつを、同一プロジェクトの名前空間にくるまっている先から呼び出したり、参照している外部アセンブリからどのように見えるかというと、実はusingすら不要で可視になる

using System;


public class OutOfNameSpace
{

}


namespace Advent
{
    static class MainEntry
    {
        public static void Main()
        {
            //こいつはExternalLibなる別のアセンブリにある
            NoEnveloped hoge = new NoEnveloped();

            //こいつは上のやつ
            OutOfNameSpace piyo = new OutOfNameSpace();
        }
    }
}

と、このように、名前空間の修飾無しで呼べてしまう。このように、名前空間に含まれていない実装はグローバル名前空間という名前空間に存在することになる。

じゃあ、以下のようなシナリオはどうなるだろうか?

using System;


public class OutOfNamespace
{

}


namespace Advent
{
    public class OutOfNamespace
    {

    }

    public class NoEnveloped
    {

    }



    static class MainEntry
    {
        public static void Main()
        {
            //この場合、Advent.NoEnvelopedと解釈される
            NoEnveloped hoge = new NoEnveloped();

            //これも同様に、Advent.OutOfNameSpaceと解釈される
            OutOfNamespace piyo = new OutOfNamespace();

            //これがグローバル名前空間にあるOutOfNamespace
            global::OutOfNamespace foo=new global::OutOfNamespace();

            //こいつは、ExternalLibにあるグローバル名前空間にいたNoEnveloped
            global::NoEnveloped bar=new global::NoEnveloped();
        }
    }
}

名前が衝突してコンパイルエラーに成るのでは無く、同一名前空間へのルックアップが優先されて、修飾無しの場合はAdvent.NoEnvelopedAdvent.OutOfNameSpaceと解釈される。

それでは、名前空間の外にいる諸々を呼び出したい場合、先の::のくだりで出ていた、global::を使って修飾することで指定可能となっている。

で、こんなことは現時点における自分の思慮では絶対にやっちゃいけないと思う。名前空間は多数のアセンブリを参照する上で、名前の衝突を防止する強力な手段なのにそいつをバイパスしちゃうのは極めてマズいし、global::で弁別可能とは言え、混乱の原因になるのでその観点からもマズいと思う。

切口上

Advent Calendarにあわせて、C#で遊んでいておや?と思ったことをつらつらと書き連ねてみました。

タイポしてたことに、コンパイル実行後に気づいてアレ通っちゃった?!となるパターンと、.NET Coreのコードリードしていて、あらまこんな書き方出来たんだってコトが今年結構有ったので、サクッと読んでクスリと笑ってもらえるかなって感じで書いてみました。

とは言え、グローバル名前空間や、new隠蔽に関しちゃ結構危険な匂いがするので積極的に使うもんでも無いと思うし、逆にローカルconstあたりは知っていればもっと早くから使いたかったなんてこともあったりなかったり。

来年も読んで頂いた皆様が素晴らしいコーディングライフを送れることを祈念しつつ、

Merry Christmas!&Happy New Year!

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

UIテスト自動化の話

いきなりですが

  最近Windows GUIアプリケーションの開発に関わるようになりました。自動テストに関して、一応ないわけではないですが、メンテ追い付かず動いたり、動かなかったりする、まぁよくある話ですね。私自身もそこまでテスト熱心なわけではないですが、何せデグレに恐れて夜眠れなくなる弱い人間なわけで、そんな心の声に従い長く付き合える自動テストを求める旅にでました。

Friendlyとの出会い

  UIの自動テスト、マウス操作をレコードし、繰り返す実行するいわゆるキャプチャリプレイーはまず思いつくでしょう。RPAという呼び方も最近のはやりらしいが、これはダメですね(RPAがだめなわけではない)、断言しましょう。アプリの反応が早かったり、遅かったりするので、確実に捕まえる保証はないからです。まぁ、実際一回見ればすぐわかる話です。というわけで最初段階で候補から外しました。
  ご存じのとおり、Windows GUIアプリ(Win32,WinForm,WPF)にはUI AutomationというUIの各種操作をシミュレートする機能を持つFWがあり、UIツリー構造を表示するVSの機能にも使用されているもので、一応自動テストに応用した記事をご紹介しますが、正直使いこなせる自信がないですね。
https://www.atmarkit.co.jp/fdotnet/special/uiautomation/uiautomation_01.html
  そんなわけでいろいろ調べて、これ以上手がかりなければもうそろそろ諦めるという自分の勘所に来てこころが曇ってきたある日、Friendlyというものが目に入りました、最初に飛び込んだのはかずきさんの記事でした、まだFriendlyそのものの存在はしならい。
https://blog.okazuki.jp/archive/category/Friendly

初見の感想

  かずきさんは足し算アプリの自動テストを紹介しました。UIを捕まえるにはByBindingという見慣れないもので、全部のUIが都合よくBinding使っているわけではないし、文字列なのでインテリセンス効かないし、変わってもビルド時気づかないし、どういうものかいまいちよさ理解できない自分がいました(ByTypeがあるのは知らないだけでした)。とはいえいままでにない期待感がわいてきましたので、早速入れてみることに

実際使ってみる

 同時にFriendlyというキーワードを意識し、ネット記事の探しまくりです。そこでQiitaの2014年のAdvent Canlenderにたどり着き、作者は日本の方で、制作するまでのいきさつも語られてて、Frienldyを詳しくしりたいならこの方ブログがおすすめです。
https://qiita.com/advent-calendar/2014/friendly
http://ishikawa-tatsuya.hatenablog.com/
 
 前置き長くなりました、本題に入ります。
 まずここのソースを実際動かして感覚つかめることにしました。主要API使い方のを中心に、かなり洗練されている感じです、最初の人はまず見ておいたほうがいいと思います。
https://github.com/Ishikawa-Tatsuya/WPFFriendlySampleDotNetConf2016

 いくつかポイントをご紹介します。詳しい説明ではなく、メモ程度のものなので、ご容赦ください。

//このXamlを前提に話します()
<Window x:Class="WpfApp1.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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <TextBox x:Name="_text1" Text="{Binding Text1}"/>
        <TextBox x:Name="_text2" Text="{Binding Text2}"/>
        <Button Content="Command1" Command="{Binding Command1}"/>
        <Button Content="Command2" Command="{Binding Command2}"/>
    </Grid>
</Window>
      //Friendlyの足場を作る作業
       var dir = Path.GetFullPath("../../../WpfApp1/bin/Release");
       var pathExe = dir + "/WpfApp1.exe";
       var info = new ProcessStartInfo(pathExe) { WorkingDirectory = dir };
       Process = Process.Start(pathExe);   //対象アプリケーションの起動
       app = new WindowsAppFriend(Process);  //魔法の時間、これで相手のプロセスに潜り込んでやりたい放題

       //UIインスタンスをつかんでみる
       //おなじみのApplication.Current.MainWindowですね、
       //MainWindowここにきてるように見えますが、実はきてません(相手プロセスにいます)
       AppVar mainWindow = app.Type<Application>().Current.MainWindow;

       // 明らかに一個しかない場合はこれで特定できるが、上のXamlではエラーになります。
       var onlyOne = mainWindow.LogicalTree().ByType<TextBox>().Single();

       // 普通はもう一段ByBinding書いて対象を絞る。
    // ByType,ByBidingはDependencyObjectのコレクションを返すのでメソッドチェンできる
       var textbox1 = mainWindow.LogicalTree().ByType<TextBox>().ByBinding("Text1").Single();

       // 型参照できない場合は文字列のインターフェスを使う
       var unkwownType = mainWindow.LogicalTree().ByType("ThirdPartyTextBox").Single();

       // ちなみにあんまりおすすめできないがインデックスアクセスできます
    var command1Button = mainWindow.LogicalTree().ByType<Button>()[0];
    //x:Nameでの捕まえ方、実はリフレクションを使ったフィールドアクセス(と思います)
    var textbox2 = mainWindow.Dynamic()._text2;

       // リフレクションなのでVisablity関係ないですから、DataContextもとれる
       // MVVMを採用しれいれば、通常DataContextが内部API詰まってるので、ユニットテストに活用する手もありです
       // ちなみに、これは結合した状態の生きたインスタンスなので、普段ユニットテストで足場を作る作業は不要ですよ。
    var dataContext = mainWindow.Dynamic().DataContext;

       // staticメンバーへのアクセス
    //インスタンス前提で話してきたが、staticメンバーの場合はこれでアクセスできます。(結構はまりました)
    var staticMember = app.Type<MainWindowVM>().StaticMember
       //UIのふるまいをシミュレートする
       WPFTextBox wpfTextBox = new WPFTextBox(textbox1);
     wpfTextBox.EmulateChangeText("NewValue");

       WPFButtonBase wpfButtonBase = new WPFButtonBase(command1Button);
       wpfButtonBase.EmulateClick();

UIテストの難所

  いかがでしょうか、初歩的なこと一通り書きましたが、まぁ、実際のUIの操作は複雑でこんな一筋にはいきません。たとえばモーダルダイアログはどうでしょう。スレッドが止まるから相手プロセスに行ったきりですよね。そこでWindowControlとAsyncという仕組みが用意されていました。

http://ishikawa-tatsuya.hatenablog.com/entry/2015/01/05/230614
詳しくはここに書かれていますので、要点だけ

    var mainWindow = new WindowControl(app);
    var modalDialogButton = app.LogicalTree().ByType<ModalDialogButton>().Single;

    //Asyncでクリックする、スレッドは止まらない
    var async = new Async();
    buttonModal.EmulateClick(async);

    //モーダルダイアログが表示されるのを確実に待ち合わせる
    var dlg = mainWindow.WaitForNextModal();

    //ダイアログ上のボタンを押す、dlgからしか取れません。
    var buttonOK = new WPFButtonBase(dlg.Dynamic()._buttonOK);
    buttonOK .EmulateClick();

    //非同期で実行したモーダルボタン押下の処理が完全に終了するのを待つ
    async.WaitForCompletion();

  これで一般的なモーダルダイアログパターンを突破できたわけですが、実はダイアログが表示されるか不確定な場合は結構厄介で悩みところです。表示されない場合のWaitForNextModal()は現在のTopのダイアログが返されて。dynamic型なので確実に型を判定できないのでそのあとの処理できず結構焦りました。

最後

 そんなところですが、UIテスト自動化の戦いがまだまだ続きそうです。これからもFriendlyと末永く付き合いたいので、また新たなネタがありましたら書いていきたいと思います。

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

(C#)赤色のみ、除外する画像処理 (特定色だけ操作する処理)

(C#)
赤色のみ、除外する画像処理
(特定色だけ操作する処理)

ーーー画像加工ソフトに同等の機能はあるが、色を操作するソースコードを自作すれば、自身の意図に合わせて、自由度の高い処理が可能になる。

iro2iro.cs
//c:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe /target:winexe iro2iro.cs

using System.Drawing;

public class iro2iro
{
 public static void Main(string[] args)
 {
    if(args.Length==2){
    string path1;//in file name
    string path2;//out file name

    path1 = @".\" + args[0];
    path2 = @".\" + args[1];

    Bitmap image1;
    image1 = new Bitmap(path1, true);

    Bitmap canvas = new Bitmap((int)image1.Width, (int)image1.Height);
    Graphics g = Graphics.FromImage(canvas);
    Color pClr;
    SolidBrush b;
    int x;
    int y;

    int r1;
    int g1;
    int b1;

    for(x=1;x<(int)image1.Width;x+=1){
        for(y=1;y<(int)image1.Height;y+=1){
        r1=0;g1=0;b1=0;
        pClr = image1.GetPixel(x , y);

        r1 = (int)pClr.R;
        g1 = (int)pClr.G;
        b1 = (int)pClr.B;

        //ここで抽出する色の条件を決める
        //一例。赤要素が160以上でかつ、
        //青緑の平均より、2割上回る部分(これがないと単に白色が抽出される)
        //if (r1 > 160 )
        if (r1 > 160 && r1 > ((g1+b1)/2*1.2))
        {
            //pClr = Color.FromArgb(255,255,255);
            pClr = Color.FromArgb(200,200,200);
            //単に白色に置き換えると返って浮く場合は色を調整
        }

        b = new SolidBrush(pClr);
        g.FillRectangle(b, x, y, 1, 1);

        }
    }

    canvas.Save(path2, System.Drawing.Imaging.ImageFormat.Jpeg);
    }
 }

}

・変換後
3q3.jpg

・変換前
3sample.jpeg

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

【C#, log4net】シングルトンクラスのコンストラクタでロガーを使ったらNullReferenceExceptionが発生して苦闘した話

追記 2019/12/15

コメント欄にてalbireoさんにシングルトンの実装の仕方などについてご指摘いただきましたので、
そちらも一緒にお読みいただければと思います。

はじめに

シングルトンクラスを作り、そのインスタンスを外部のクラスから取得しようとしたら、下記の画像のような例外が発生した。
原因はシングルトンクラスに書いていたlog4netのロガーインスタンスを取得するコード(GetLoggerメソッド)にあった。
色々すっ飛ばしてこの話の結論を先に述べると、「ロガーインスタンス取得コードはフィールドの一番最初に書きましょう」。
もし同じ問題ではまっている方がいればとの思いで記事を書いた(いるかな・・・)。
※シングルトンパターンについて正しくない説明があるかもしれませんので、その点はご了承いただいたうえでお読みください。
image.png

環境

・IDEはVisual Studio 2017を使用

問題の発生まで

このサイト(.NET TIPS:シングルトンパターンを実現するには?[C#/VB])を参考にして、シングルトンクラスを作成し、ログ出力するためロガーインスタンス取得コードを書いた。

下記がそのシングルトンクラス。

SingletonClass.cs
namespace Hoge
{
    class SingletonClass
    {
        // 自身のクラスのインスタンスを初期化(アプリ実行時にインスタンスはこの1つしか存在しない)
        private static SingletonClass _instance = new SingletonClass();

        // ロガーインスタンス取得コード
        private static readonly log4net.ILog _logger = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

        // 自身のインスタンスを外部から使うための手段を用意してあげる
        // プロパティを使った方法
        public static SingletonClass Instance => _instance;
        // メソッドを使った方法
        public static SingletonClass GetInstance() => _instance;


        // 外部から直接呼べないコンストラクタ
        private SingletonClass()
        {
            _logger.Debug("SingletonClassのインスタンスが生成されました");
        }
    }
}

 
そして、シングルトンクラスを利用する側のクラスで、シングルトンインスタンス取得コードを書いた。

UseSingleton.cs
namespace Hoge
{
    class UseSingleton
    {
        // シングルトンインスタンスを保持するフィールド
        private SingletonClass _singletonInstance;

        public void SomeMethod()
        {
            // シングルトンインスタンスを取得
            _singletonInstance = SingletonClass.GetInstance();         
        }
    }
} 

 

実行したら、落ちた。
image.png

問題の原因

問題のコードは下図の矢印の順序で処理が行われている。
まずGetInstanceメソッドを呼び出すと、シングルトンクラス内部ではシングルトンインスタンスを取得しようとする。
その際にシングルトンインスタンスがnewされ、したがってシングルトンクラスのコンストラクタに処理が移る。
ここからが問題。
今回はコンストラクタでロガーを使ったログ出力を行おうとした。
しかしこの時点でロガーインスタンスの取得は実行されていないため_loggerはNullの状態だったのだ。
まだ取得してないはずの_loggerを使おうとしたから、「オブジェクト参照がオブジェクトインスタンスに設定されていません」と怒られたのであった・・・。

image.png

解決

シングルトンクラスにおいて、ロガーインスタンス取得コードを自身のインスタンス初期化コードより先に書いてあげ、無事に動いた。

SingletonClass.cs
    class SingletonClass
    {
        // ロガーインスタンス取得コード
        private static readonly log4net.ILog _logger = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

        // 自身のクラスのインスタンスを初期化
        private static SingletonClass _instance = new SingletonClass();


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

[WPF]ComboBoxのControlTemplateを使ってシンプルかつMouseOrver時に色が変わるComboBoxを作ってみた

ComboBoxの外観を変更したい

シンプルなComboBox&MouseOverした時に色を変えることをやりたかったが思ったより時間がかかったのでまとめておきます。

ComboBox MSDNを見ると

Customizing the ComboBox Control
To apply the same property settings to multiple ComboBox controls, use the Style property. You can modify the default ControlTemplate to give the control a unique appearance. For more information about creating a ControlTemplate, see Customizing the Appearance of an Existing Control by Creating a ControlTemplate. To see the parts and states that are specific to the ComboBox, see ComboBox Styles and Templates.

とあります。
複数のComboBoxの外観を変更したい場合はControlTemplateを使うようです。
ComboBox Styles and Templates MSDNを見てみると、ComboBoxのControlTemplate例があります。

とりあえず、ComboBoxのControlTemplate例を使ってみた結果、
image.png
ToggleButtonがグラデーションがかかっていたり丸角になっていたり、シンプルとは言いづらいですね。

シンプルなComboBox

ComboBoxのControlTemplate例を編集して、以下の項目を削除変更してシンプルにしてみました。

  • グラデーションをかけているLinearGradientBrush MSDNを削除
  • 要素の丸みを削除
  • ToggleButtonとPopupの間のMarginを削除
  • ToggleButtonの▼をよりシンプルなものに変更

また、WPF Flat Combo Box Style GitHubを参考にさせていただきました。

image.png

どこを編集したか分かりやすいように色は以下に設定しました。

  • ToggleButton: Blue
  • ToggleButton横のTextBox: Green
  • ToggleButtonのBoorder: Aqua
  • ToggleButton横のTextBoxのBoorder: GreenYellow
  • Popupの選択した項目: Orange

ToggleButton横のTextBoxのBoorderは設定しましたが、見た目としては表れないようですね。

Dictonary.xaml
        <ControlTemplate x:Key="ComboBoxToggleButton"
                 TargetType="{x:Type ToggleButton}">
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition />
                    <ColumnDefinition Width="20" />
                </Grid.ColumnDefinitions>
                <Border x:Name="Border"
                        Grid.ColumnSpan="2"
                        BorderThickness="1">
                    <Border.BorderBrush>
                        <SolidColorBrush Color="Aqua"/>
                    </Border.BorderBrush>
                    <Border.Background>
                        <SolidColorBrush Color="Blue"/>
                    </Border.Background>
                </Border>
                <Border Grid.Column="0"
                        Margin="1" >
                    <Border.BorderBrush>
                        <SolidColorBrush Color="GreenYellow"/>
                    </Border.BorderBrush>
                    <Border.Background>
                        <SolidColorBrush Color="Green"/>
                    </Border.Background>
                </Border>
                <Path x:Name="Arrow"
                      Grid.Column="1"
                      HorizontalAlignment="Center"
                      VerticalAlignment="Center"
                      Data="M0,0 L0,2 L4,6 L8,2 L8,0 L4,4 z" >
                    <Path.Fill>
                        <SolidColorBrush Color="{DynamicResource GlyphColor}"/>
                    </Path.Fill>
                </Path>
            </Grid>
        </ControlTemplate>

        <Style x:Key="SimpleComboBoxStyle"
       TargetType="{x:Type ComboBox}">
            <Setter Property="SnapsToDevicePixels"
                    Value="true" />
            <Setter Property="OverridesDefaultStyle"
                    Value="true" />
            <Setter Property="ScrollViewer.HorizontalScrollBarVisibility"
                    Value="Auto" />
            <Setter Property="ScrollViewer.VerticalScrollBarVisibility"
                    Value="Auto" />
            <Setter Property="ScrollViewer.CanContentScroll"
                    Value="true" />
            <Setter Property="MinWidth"
                    Value="120" />
            <Setter Property="MinHeight"
                    Value="20" />
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type ComboBox}">
                        <Grid>
                            <ToggleButton x:Name="ToggleButton"
                                            Template="{StaticResource ComboBoxToggleButton}"
                                            Grid.Column="2"
                                            Focusable="false"
                                            ClickMode="Press"
                                            IsChecked="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"/>
                            <ContentPresenter x:Name="ContentSite"
                                                IsHitTestVisible="False"
                                                Content="{TemplateBinding SelectionBoxItem}"
                                                ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}"
                                                ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}"
                                                Margin="3,3,23,3"
                                                VerticalAlignment="Stretch"
                                                HorizontalAlignment="Left">
                            </ContentPresenter>
                            <TextBox x:Name="PART_EditableTextBox"
                                       Style="{x:Null}"
                                       HorizontalAlignment="Left"
                                       VerticalAlignment="Bottom"
                                       Margin="3,3,23,3"
                                       Focusable="True"
                                       Background="Transparent"
                                       Visibility="Hidden"
                                       IsReadOnly="{TemplateBinding IsReadOnly}" >
                                <TextBox.Template>
                                    <ControlTemplate TargetType="TextBox" >
                                        <Border Name="PART_ContentHost" Focusable="False" />
                                    </ControlTemplate>
                                </TextBox.Template>
                            </TextBox>
                            <Popup x:Name="Popup"
                                     Placement="Bottom"
                                     IsOpen="{TemplateBinding IsDropDownOpen}"
                                     AllowsTransparency="True"
                                     Focusable="False"
                                     PopupAnimation="Slide">
                                <Grid x:Name="DropDown"
                                      SnapsToDevicePixels="True"
                                      MinWidth="{TemplateBinding ActualWidth}"
                                      MaxHeight="{TemplateBinding MaxDropDownHeight}">
                                    <Border x:Name="DropDownBorder"
                                            BorderThickness="1">
                                        <Border.BorderBrush>
                                            <SolidColorBrush Color="{DynamicResource BorderMediumColor}" />
                                        </Border.BorderBrush>
                                        <Border.Background>
                                            <SolidColorBrush Color="{DynamicResource ControlLightColor}" />
                                        </Border.Background>
                                    </Border>
                                    <ScrollViewer Margin="4,6,4,6"
                                                  SnapsToDevicePixels="True">
                                        <StackPanel IsItemsHost="True"
                                                    KeyboardNavigation.DirectionalNavigation="Contained" />
                                    </ScrollViewer>
                                </Grid>
                            </Popup>
                        </Grid>
                        <ControlTemplate.Triggers>
                            <Trigger Property="HasItems"
                                     Value="false">
                                <Setter TargetName="DropDownBorder"
                                        Property="MinHeight"
                                        Value="95" />
                            </Trigger>
                            <Trigger Property="IsGrouping"
                                     Value="true">
                                <Setter Property="ScrollViewer.CanContentScroll"
                                        Value="false" />
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

MouseOrverで色が変わるComboBox

MouseOrverで色が変わったことが分かりやすいように以下のように色を設定しました。

TextBox + ToggleButtonの色

  • 要素を選んでいる時: Orange
  • MouseOrver時: Red
  • 上記以外: Blue

image.png
image.png

Popupの色

  • 選択した項目: Orange
  • MouseOrver時: Red
  • 上記以外: White

image.png

Dictonary.xaml
        <Style x:Key="ChangingColorComboBoxBorder" TargetType="{x:Type Border}">
            <Setter Property="Background" Value="Blue"/>
            <Setter Property="BorderBrush" Value="Aqua"/>
            <Style.Triggers>
                <DataTrigger Binding="{Binding IsMouseOver, ElementName=Border}" Value="True">
                    <Setter Property="Background" Value="Red"/>
                </DataTrigger>
                <DataTrigger Binding="{Binding IsMouseOver, ElementName=TextBoxBorder}" Value="True">
                    <Setter Property="Background" Value="Red"/>
                </DataTrigger>
                <DataTrigger Binding="{Binding IsChecked, ElementName=ToggleButton}" Value="True">
                    <Setter Property="Background" Value="Orange"/>
                </DataTrigger>
                <DataTrigger Binding="{Binding IsChecked, ElementName=ToggleButton}" Value="True">
                    <Setter Property="Background" Value="Orange"/>
                </DataTrigger>
            </Style.Triggers>
        </Style>

        <ControlTemplate x:Key="ComboBoxToggleButton"
                 TargetType="{x:Type ToggleButton}">
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition />
                    <ColumnDefinition Width="20" />
                </Grid.ColumnDefinitions>
                <Border x:Name="Border"
                        Grid.ColumnSpan="2"
                        BorderThickness="1"
                        Style="{StaticResource ChangingColorComboBoxBorder}"/>
                <Border x:Name="TextBoxBorder"
                        Grid.Column="0"
                        Margin="1" 
                        Style="{StaticResource ChangingColorComboBoxBorder}"/>
                <Path x:Name="Arrow"
                      Grid.Column="1"
                      HorizontalAlignment="Center"
                      VerticalAlignment="Center"
                      Data="M0,0 L0,2 L4,6 L8,2 L8,0 L4,4 z" >
                    <Path.Fill>
                        <SolidColorBrush Color="{DynamicResource GlyphColor}"/>
                    </Path.Fill>
                </Path>
            </Grid>
        </ControlTemplate>

        <Style x:Key="ChangingColorComboBoxItem"
                TargetType="{x:Type ComboBoxItem}">
            <Setter Property="SnapsToDevicePixels"
                    Value="true" />
            <Setter Property="OverridesDefaultStyle"
                    Value="true" />
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type ComboBoxItem}">
                        <Border x:Name="Border"
                                Padding="2"
                                SnapsToDevicePixels="true"
                                Background="Transparent">
                            <ContentPresenter />
                        </Border>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsSelected" 
                                    Value="true">
                                <Setter Property="Background" TargetName="Border"
                                        Value="Orange" />
                                <Setter Property="Foreground"
                                        Value="{DynamicResource {x:Static SystemColors.HighlightTextBrushKey}}" />
                            </Trigger>
                            <Trigger Property="IsMouseOver" 
                                    Value="true">
                                <Setter Property="Background" TargetName="Border"
                                        Value="Red" />
                                <Setter Property="Foreground"
                                        Value="{DynamicResource {x:Static SystemColors.HighlightTextBrushKey}}" />
                            </Trigger>
                            <Trigger Property="IsEnabled"
                                    Value="false">
                                <Setter Property="Foreground"
                                        Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}" />
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

        <Style x:Key="ChangingColorComboxStyle"
       TargetType="{x:Type ComboBox}">
            <Setter Property="SnapsToDevicePixels"
                    Value="true" />
            <Setter Property="OverridesDefaultStyle"
                    Value="true" />
            <Setter Property="ScrollViewer.HorizontalScrollBarVisibility"
                    Value="Auto" />
            <Setter Property="ScrollViewer.VerticalScrollBarVisibility"
                    Value="Auto" />
            <Setter Property="ScrollViewer.CanContentScroll"
                    Value="true" />
            <Setter Property="MinWidth"
                    Value="120" />
            <Setter Property="MinHeight"
                    Value="20" />
            <Setter Property="ItemContainerStyle" 
                    Value="{StaticResource ChangingColorComboBoxItem}"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type ComboBox}">
                        <Grid>
                            <ToggleButton x:Name="ToggleButton"
                                            Template="{StaticResource ComboBoxToggleButton}"
                                            Grid.Column="2"
                                            Focusable="false"
                                            ClickMode="Press"
                                            IsChecked="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"/>
                            <ContentPresenter x:Name="ContentSite"
                                                IsHitTestVisible="False"
                                                Content="{TemplateBinding SelectionBoxItem}"
                                                ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}"
                                                ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}"
                                                Margin="3,3,23,3"
                                                VerticalAlignment="Stretch"
                                                HorizontalAlignment="Left">
                            </ContentPresenter>
                            <TextBox x:Name="PART_EditableTextBox"
                                       Style="{x:Null}"
                                       HorizontalAlignment="Left"
                                       VerticalAlignment="Bottom"
                                       Margin="3,3,23,3"
                                       Focusable="True"
                                       Background="Transparent"
                                       Visibility="Hidden"
                                       IsReadOnly="{TemplateBinding IsReadOnly}" >
                                <TextBox.Template>
                                    <ControlTemplate TargetType="TextBox" >
                                        <Border Name="PART_ContentHost" Focusable="False" />
                                    </ControlTemplate>
                                </TextBox.Template>
                            </TextBox>
                            <Popup x:Name="Popup"
                                     Placement="Bottom"
                                     IsOpen="{TemplateBinding IsDropDownOpen}"
                                     AllowsTransparency="True"
                                     Focusable="False"
                                     PopupAnimation="Slide">
                                <Grid x:Name="DropDown"
                                      SnapsToDevicePixels="True"
                                      MinWidth="{TemplateBinding ActualWidth}"
                                      MaxHeight="{TemplateBinding MaxDropDownHeight}">
                                    <Border x:Name="DropDownBorder"
                                            BorderThickness="1">
                                        <Border.BorderBrush>
                                            <SolidColorBrush Color="{DynamicResource BorderMediumColor}" />
                                        </Border.BorderBrush>
                                        <Border.Background>
                                            <SolidColorBrush Color="{DynamicResource ControlLightColor}" />
                                        </Border.Background>
                                    </Border>
                                    <ScrollViewer Margin="4,6,4,6"
                                                  SnapsToDevicePixels="True">
                                        <StackPanel IsItemsHost="True"
                                                    KeyboardNavigation.DirectionalNavigation="Contained" />
                                    </ScrollViewer>
                                </Grid>
                            </Popup>
                        </Grid>
                        <ControlTemplate.Triggers>
                            <Trigger Property="HasItems"
                                     Value="false">
                                <Setter TargetName="DropDownBorder"
                                        Property="MinHeight"
                                        Value="95" />
                            </Trigger>
                            <Trigger Property="IsGrouping"
                                     Value="true">
                                <Setter Property="ScrollViewer.CanContentScroll"
                                        Value="false" />
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

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

C# - Win32APIでマイク入力を拾う(32bit)

インストールレス・ライブラリレスでいける音声入力処理を作ってみた。※終了処理は作ってません。

感想

マーシャリングがつらすぎた。「ここ」見つけられなかったら多分動くところまで行けてない。

・32bitアプリとしてしかつかえなさそう。
・バッファが枯渇するとアプリごと落ちる現象が発生した。
ので、
DirectX(←使うにはインストールが必要そう)なりWASAPIを勉強したほうがよいかもしれない。

画面キャプチャ

image.png

ソースコード

コマンドラインから実行する場合は、chcp 65001でUTF-8を表示できるようにしておいてください。

コンパイルはcsc /platform:x86 ファイル名.csとしてください。
(WinMMのAPI関連の構造体が64bitを考慮していないようなので32bit指定でコンパイルする。)

using System;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Windows.Forms;

class WaveInTest : Form
{
    class NativeMethods
    {
        const int MAXPNAMELEN = 32;

        public const int MMSYSERR_NOERROR = 0; // MMRESULT

        public const int WAVE_FORMAT_1M08 = 0x00001;
        public const int WAVE_FORMAT_1S08 = 0x00002;
        public const int WAVE_FORMAT_1M16 = 0x00004;
        public const int WAVE_FORMAT_1S16 = 0x00008;
        public const int WAVE_FORMAT_2M08 = 0x00010;
        public const int WAVE_FORMAT_2S08 = 0x00020; // 22.05 kHz, stereo, 8-bit
        public const int WAVE_FORMAT_2M16 = 0x00040;
        public const int WAVE_FORMAT_2S16 = 0x00080;
        public const int WAVE_FORMAT_4M08 = 0x00100;
        public const int WAVE_FORMAT_4S08 = 0x00200;
        public const int WAVE_FORMAT_4M16 = 0x00400;
        public const int WAVE_FORMAT_4S16 = 0x00800;

        public const int CALLBACK_WINDOW   = 0x10000;

        public const int WIM_OPEN  = 0x3BE;
        public const int WIM_CLOSE = 0x3BF;
        public const int WIM_DATA  = 0x3C0;

        public const int WAVE_FORMAT_PCM = 1;

        //const int WAVE_MAPPER = -1;

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] // CharSet.Unicodeを指定しないとstringのサイズが不正になる
        public struct WaveInCaps{
            public Int16 wMid;
            public Int16 wPid;
            public Int32 vDriverVersion;
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = MAXPNAMELEN)]
            public string szPname;
            public Int32 dwFormats;
            public Int16 wChannels;
            Int16 wReserved1;
        } ;

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
        public struct WaveFormatEx
        {
            public Int16 wFormatTag;
            public Int16 nChannels;
            public Int32 nSamplesPerSec;
            public Int32 nAvgBytesPerSec;
            public Int16 nBlockAlign;
            public Int16 wBitsPerSample;
            public Int16 cbSize;
        }

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
        public struct WaveHdr
        {
            public IntPtr lpData;
            public Int32 dwBufferLength;
            public Int32 dwBytesRecorded;
            public IntPtr dwUser;
            public Int32 dwFlags;
            public Int32 dwLoops;
            public IntPtr lpNext;
            public Int32 reserved;
        }

        [DllImport("winmm.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        public static extern Int32 waveInGetNumDevs();

        [DllImport("winmm.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        public static extern Int32 waveInGetDevCaps(
            Int32 uDeviceID,
            ref WaveInCaps wic,
            Int32 cbwic
        );

        // for 32bit only
        [DllImport("winmm.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        public static extern Int32 waveInOpen(
            ref IntPtr hwi,
            Int32 uDeviceID,
            ref WaveFormatEx _wfx,
            IntPtr dwCallback,  // Int32
            Int32 dwCallbackInstance,
            Int32 fdwOpen
        );

        [DllImport("winmm.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        public static extern Int32 waveInClose(IntPtr hwi);

        [DllImport("winmm.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        public static extern Int32 waveInPrepareHeader(IntPtr hwi, ref WaveHdr wh, Int32 cbwh);

        [DllImport("winmm.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        public static extern Int32 waveInUnprepareHeader(IntPtr hwi, ref WaveHdr wh, Int32 cbwh);

        [DllImport("winmm.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        public static extern Int32 waveInAddBuffer(IntPtr hwi, ref WaveHdr wh, Int32 cbwh);

        [DllImport("winmm.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        public static extern Int32 waveInStart(IntPtr hwi);

        [DllImport("winmm.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        public static extern Int32 waveInStop(IntPtr hwi);
    }

    class MyWaveBuffer
    {
        public GCHandle selfHandle;
        public GCHandle dataHandle;
        public byte[] data;
        public NativeMethods.WaveHdr hdr;
        public static readonly int cbwh = 32;// Marshal.SizeOf(hdr);

        public MyWaveBuffer(int length)
        {
            data = new byte[length];
            dataHandle = GCHandle.Alloc(data, GCHandleType.Pinned);
            selfHandle = GCHandle.Alloc(this);

            hdr = new NativeMethods.WaveHdr();
            hdr.lpData = dataHandle.AddrOfPinnedObject();
            hdr.dwBufferLength = length;
            hdr.dwFlags = 0;
            hdr.reserved = 0;
            hdr.dwUser = GCHandle.ToIntPtr(selfHandle);
        }
    }

    static readonly int BUFFER_SIZE = 4800;
    static readonly int N_BUFFERS = 30;
    NativeMethods.WaveFormatEx _wfx;
    MyWaveBuffer[] _wbuf;
    byte[] _waveData;
    IntPtr _hwi;
    bool _redrawRequest;
    bool _rec;
    int _counter;

    Button btn;
    PictureBox pct;
    System.Windows.Forms.Timer timer;

    static readonly int WIDTH  = 400;
    static readonly int HEIGHT = 300;

    WaveInTest()
    {
        Text = "WaveIn Test";
        ClientSize = new Size(WIDTH, HEIGHT+30);

        _rec = false;
        _waveData = new byte[BUFFER_SIZE];
        _redrawRequest = false;


        Load += Form_Load;

        Closing += (sender,e)=>{
            if ( _hwi != IntPtr.Zero ) {
                _rec = false;
                NativeMethods.waveInClose(_hwi);
                _hwi = IntPtr.Zero;
            }
        };

        btn = new Button(){Text="Rec"};
        btn.Enabled = false;
        btn.Click += (sender,e)=>{
            if ( !_rec && _hwi != IntPtr.Zero) {
                btn.Enabled = false;
                _rec = true;
                int retCode = NativeMethods.waveInStart(_hwi);
                if ( retCode != NativeMethods.MMSYSERR_NOERROR ) {
                    Console.WriteLine("waveInStart ErrorCode: 0x"+retCode.ToString("X"));
                }
            }
        };
        Controls.Add(btn);

        pct = new PictureBox();
        pct.Top = 30;
        pct.Size = new Size(WIDTH, HEIGHT);
        pct.Image = new Bitmap(WIDTH, HEIGHT);
        Controls.Add(pct);

        timer = new System.Windows.Forms.Timer();
        timer.Interval = 250;
        timer.Tick += (sender,e)=>{MyRedraw();};
        timer.Start();
    }


    void Form_Load(object sender, EventArgs e)
    {
        ShowDeviceInfo();

        _wfx = new NativeMethods.WaveFormatEx();
        _wfx.cbSize = 0;//(Int16)Marshal.SizeOf(_wfx);
        _wfx.wFormatTag = NativeMethods.WAVE_FORMAT_PCM;
        _wfx.nChannels = 1;//wic.wChannels;
        _wfx.nSamplesPerSec = 22050;
        _wfx.wBitsPerSample = 8;
        _wfx.nBlockAlign = (Int16)(_wfx.wBitsPerSample / 8 * _wfx.nChannels);
        _wfx.nAvgBytesPerSec = _wfx.nSamplesPerSec * _wfx.nBlockAlign;

        {
            int retCode = NativeMethods.waveInOpen(ref _hwi, 0, ref _wfx, this.Handle, 0, NativeMethods.CALLBACK_WINDOW);
            if ( retCode != NativeMethods.MMSYSERR_NOERROR ) {
                Console.WriteLine("waveInOpen ErrorCode: 0x"+retCode.ToString("X"));
                return;
            }
            Console.WriteLine("waveInOpen success");
        }
    }


    protected override void WndProc(ref Message m)
    {
        switch (m.Msg) {
            case NativeMethods.WIM_OPEN:
                Console.WriteLine("open");
                if (PrepareBuffer()) {
                    btn.Enabled = true;
                }
                base.WndProc(ref m);
                break;
            case NativeMethods.WIM_CLOSE:
                base.WndProc(ref m);
                break;
            case NativeMethods.WIM_DATA:
                if ( _rec ) {
                    WaveInCallbackProc(m.WParam, m.LParam);
                }
                base.WndProc(ref m);
                break;
            default:
                base.WndProc(ref m);
                break;
        }
    }


    bool ShowDeviceInfo()
    {
        int n = NativeMethods.waveInGetNumDevs();
        if ( n == 0 ) {
            Console.WriteLine("No waveIn devices.");
            return false;
        }

        var wic = new NativeMethods.WaveInCaps();
        for ( int i=0 ; i<n ; i++ ) {
            int retCode = NativeMethods.waveInGetDevCaps(i, ref wic, Marshal.SizeOf(wic)); 
            if ( retCode == NativeMethods.MMSYSERR_NOERROR ) {
                Console.WriteLine("Name: "        + wic.szPname);
                Console.WriteLine("  Formats: 0x" + wic.dwFormats.ToString("X"));
                Console.WriteLine("  Channels: "  + wic.wChannels.ToString());
            }
            else {
                Console.WriteLine("waveInGetDevCaps ErrorCode: 0x"+retCode.ToString("X"));
            }
        }

        return true;
    }

    bool PrepareBuffer()
    {
        if(_hwi==IntPtr.Zero){return false;}

        int retCode;

        _wbuf = new MyWaveBuffer[N_BUFFERS];

        for ( int i=0 ; i<N_BUFFERS ; i++ ) {
            _wbuf[i] = new MyWaveBuffer(BUFFER_SIZE);

            retCode = NativeMethods.waveInPrepareHeader(_hwi, ref _wbuf[i].hdr, MyWaveBuffer.cbwh);
            if ( retCode != NativeMethods.MMSYSERR_NOERROR ) {
                Console.WriteLine("waveInPrepareHeader ErrorCode: 0x"+retCode.ToString("X"));
                return false;
            }

            retCode = NativeMethods.waveInAddBuffer(_hwi, ref _wbuf[i].hdr, MyWaveBuffer.cbwh);
            if ( retCode != NativeMethods.MMSYSERR_NOERROR ) {
                Console.WriteLine("waveInAddBuffer ErrorCode: 0x"+retCode.ToString("X"));
                return false;
            }
        }

        return true;
    }

    public bool WaveInCallbackProc(IntPtr hwi, IntPtr hwvhdr)
    {
        NativeMethods.WaveHdr wvhdr;

        if ( !_rec ) { return false; }
        if ( hwi != _hwi ) { return false; }

        MyWaveBuffer t;
        wvhdr = (NativeMethods.WaveHdr)Marshal.PtrToStructure(hwvhdr, typeof(NativeMethods.WaveHdr));
        t = (MyWaveBuffer)GCHandle.FromIntPtr(wvhdr.dwUser).Target;

        if ( t.hdr.dwBytesRecorded == BUFFER_SIZE ) {
            Array.Copy(t.data, 0, _waveData, 0, BUFFER_SIZE);
            _redrawRequest = true;
        }

        _counter++;
        Text = _counter.ToString();

        int retCode;
        retCode = NativeMethods.waveInUnprepareHeader(hwi, ref t.hdr, Marshal.SizeOf(wvhdr));
        if(retCode!=NativeMethods.MMSYSERR_NOERROR){Console.WriteLine("waveInUnprepareHeader failed");}
        retCode = NativeMethods.waveInPrepareHeader(hwi, ref t.hdr, Marshal.SizeOf(wvhdr));
        if(retCode!=NativeMethods.MMSYSERR_NOERROR){Console.WriteLine("waveInPrepareHeader failed");}
        retCode = NativeMethods.waveInAddBuffer(hwi, ref t.hdr, Marshal.SizeOf(wvhdr));
        if(retCode!=NativeMethods.MMSYSERR_NOERROR){Console.WriteLine("waveInAddBuffer failed");}

//        Text = _counter.ToString() + "+";

        return true; 
    }

    void MyRedraw()
    {
        if ( _redrawRequest ) {
            int step = (BUFFER_SIZE+WIDTH-1)/WIDTH;
            Graphics g = Graphics.FromImage(pct.Image);
            g.Clear(Color.White);

            int xPrev = (0*WIDTH)/BUFFER_SIZE;
            int yPrev = (_waveData[0] * HEIGHT) / 256;

            for(int i=step;i<BUFFER_SIZE;i+=step) {
                int x = (i*WIDTH)/BUFFER_SIZE;
                int y = (_waveData[i] * HEIGHT) / 256;

                g.DrawLine(Pens.Black, xPrev, yPrev, x, y);

                xPrev = x;
                yPrev = y;
            }

            g.Dispose();
            pct.Refresh();
        }
    }

    [STAThread]
    static void Main(string[] args)
    {
        if ( Marshal.SizeOf(IntPtr.Zero) != 4 ) {
            throw new Exception("must compile as 32bit application. (csc /platform:x86)");
        }
        else {
            Application.Run(new WaveInTest());
        }
    }
}

参考サイト

WinMM関連の定義値はmmsystem.hをググると何かしら見つかるはず。

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

C# - Win32API(WinMM)でマイク入力を拾う(32bit)

インストールレス・ライブラリレスでいける音声入力処理を作ってみた。※終了処理は作ってません。

感想

マーシャリングがつらすぎた。「ここ」見つけられなかったら多分動くところまで行けてない。

WinMMを使う場合、下記の問題
・32bitアプリとしてしかつかえなさそう。
・バッファが枯渇するとアプリごと落ちる現象が発生した(自分のアプリのバグかもですが)。
が出たので、
DirectX(←使うにはインストールが必要そう)なりWASAPIを勉強したほうがよいかもしれない。
ほかの手段は下記が参考になりそう。
http://mikeo410.minim.ne.jp/cms/~programingaudiorecording

画面キャプチャ

image.png

コンパイル方法

WinMMのAPI関連の構造体が64bitを考慮していないようなので、32bitアプリとしてコンパイルする必要があります。
32bit指定はcsc /platform:x86 ファイル名.csでコンパイルすればよいです。

Windows10 64bit環境で下記警告がでましたが、動作しました。
(ご利用は自己責任で・・)

warning CS1607: アセンブリの生成 -- 参照アセンブリ 'System.Data.dll' は異なるプロセッサを対象にしています。
warning CS1607: アセンブリの生成 -- 参照アセンブリ 'System.Data.OracleClient.dll' は異なるプロセッサを対象にしています。
warning CS1607: アセンブリの生成 -- 参照アセンブリ 'System.EnterpriseServices.dll' は異なるプロセッサを対象にしています。
warning CS1607: アセンブリの生成 -- 参照アセンブリ 'System.Transactions.dll' は異なるプロセッサを対象にしています。
warning CS1607: アセンブリの生成 -- 参照アセンブリ 'System.Web.dll' は異なるプロセッサを対象にしています。
warning CS1607: アセンブリの生成 -- 参照アセンブリ 'mscorlib.dll' は異なるプロセッサを対象にしています。

ソースコード

コマンドラインから実行する場合は、chcp 65001でUTF-8を表示できるようにしておいてください。

using System;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Windows.Forms;

class WaveInTest : Form
{
    class NativeMethods
    {
        const int MAXPNAMELEN = 32;

        public const int MMSYSERR_NOERROR = 0; // MMRESULT

        public const int WAVE_FORMAT_1M08 = 0x00001;
        public const int WAVE_FORMAT_1S08 = 0x00002;
        public const int WAVE_FORMAT_1M16 = 0x00004;
        public const int WAVE_FORMAT_1S16 = 0x00008;
        public const int WAVE_FORMAT_2M08 = 0x00010;
        public const int WAVE_FORMAT_2S08 = 0x00020; // 22.05 kHz, stereo, 8-bit
        public const int WAVE_FORMAT_2M16 = 0x00040;
        public const int WAVE_FORMAT_2S16 = 0x00080;
        public const int WAVE_FORMAT_4M08 = 0x00100;
        public const int WAVE_FORMAT_4S08 = 0x00200;
        public const int WAVE_FORMAT_4M16 = 0x00400;
        public const int WAVE_FORMAT_4S16 = 0x00800;

        public const int CALLBACK_WINDOW   = 0x10000;

        public const int WIM_OPEN  = 0x3BE;
        public const int WIM_CLOSE = 0x3BF;
        public const int WIM_DATA  = 0x3C0;

        public const int WAVE_FORMAT_PCM = 1;

        //const int WAVE_MAPPER = -1;

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] // CharSet.Unicodeを指定しないとstringのサイズが不正になる
        public struct WaveInCaps{
            public Int16 wMid;
            public Int16 wPid;
            public Int32 vDriverVersion;
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = MAXPNAMELEN)]
            public string szPname;
            public Int32 dwFormats;
            public Int16 wChannels;
            Int16 wReserved1;
        } ;

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
        public struct WaveFormatEx
        {
            public Int16 wFormatTag;
            public Int16 nChannels;
            public Int32 nSamplesPerSec;
            public Int32 nAvgBytesPerSec;
            public Int16 nBlockAlign;
            public Int16 wBitsPerSample;
            public Int16 cbSize;
        }

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
        public struct WaveHdr
        {
            public IntPtr lpData;
            public Int32 dwBufferLength;
            public Int32 dwBytesRecorded;
            public IntPtr dwUser;
            public Int32 dwFlags;
            public Int32 dwLoops;
            public IntPtr lpNext;
            public Int32 reserved;
        }

        [DllImport("winmm.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        public static extern Int32 waveInGetNumDevs();

        [DllImport("winmm.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        public static extern Int32 waveInGetDevCaps(
            Int32 uDeviceID,
            ref WaveInCaps wic,
            Int32 cbwic
        );

        // for 32bit only
        [DllImport("winmm.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        public static extern Int32 waveInOpen(
            ref IntPtr hwi,
            Int32 uDeviceID,
            ref WaveFormatEx _wfx,
            IntPtr dwCallback,  // Int32
            Int32 dwCallbackInstance,
            Int32 fdwOpen
        );

        [DllImport("winmm.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        public static extern Int32 waveInClose(IntPtr hwi);

        [DllImport("winmm.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        public static extern Int32 waveInPrepareHeader(IntPtr hwi, ref WaveHdr wh, Int32 cbwh);

        [DllImport("winmm.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        public static extern Int32 waveInUnprepareHeader(IntPtr hwi, ref WaveHdr wh, Int32 cbwh);

        [DllImport("winmm.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        public static extern Int32 waveInAddBuffer(IntPtr hwi, ref WaveHdr wh, Int32 cbwh);

        [DllImport("winmm.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        public static extern Int32 waveInStart(IntPtr hwi);

        [DllImport("winmm.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        public static extern Int32 waveInStop(IntPtr hwi);
    }

    class MyWaveBuffer
    {
        public GCHandle selfHandle;
        public GCHandle dataHandle;
        public byte[] data;
        public NativeMethods.WaveHdr hdr;
        public static readonly int cbwh = 32;// Marshal.SizeOf(hdr);

        public MyWaveBuffer(int length)
        {
            data = new byte[length];
            dataHandle = GCHandle.Alloc(data, GCHandleType.Pinned);
            selfHandle = GCHandle.Alloc(this);

            hdr = new NativeMethods.WaveHdr();
            hdr.lpData = dataHandle.AddrOfPinnedObject();
            hdr.dwBufferLength = length;
            hdr.dwFlags = 0;
            hdr.reserved = 0;
            hdr.dwUser = GCHandle.ToIntPtr(selfHandle);
        }
    }

    static readonly int BUFFER_SIZE = 4800;
    static readonly int N_BUFFERS = 30;
    NativeMethods.WaveFormatEx _wfx;
    MyWaveBuffer[] _wbuf;
    byte[] _waveData;
    IntPtr _hwi;
    bool _redrawRequest;
    bool _rec;
    int _counter;

    Button btn;
    PictureBox pct;
    System.Windows.Forms.Timer timer;

    static readonly int WIDTH  = 400;
    static readonly int HEIGHT = 300;

    WaveInTest()
    {
        Text = "WaveIn Test";
        ClientSize = new Size(WIDTH, HEIGHT+30);

        _rec = false;
        _waveData = new byte[BUFFER_SIZE];
        _redrawRequest = false;


        Load += Form_Load;

        Closing += (sender,e)=>{
            if ( _hwi != IntPtr.Zero ) {
                _rec = false;
                NativeMethods.waveInClose(_hwi);
                _hwi = IntPtr.Zero;
            }
        };

        btn = new Button(){Text="Rec"};
        btn.Enabled = false;
        btn.Click += (sender,e)=>{
            if ( !_rec && _hwi != IntPtr.Zero) {
                btn.Enabled = false;
                _rec = true;
                int retCode = NativeMethods.waveInStart(_hwi);
                if ( retCode != NativeMethods.MMSYSERR_NOERROR ) {
                    Console.WriteLine("waveInStart ErrorCode: 0x"+retCode.ToString("X"));
                }
            }
        };
        Controls.Add(btn);

        pct = new PictureBox();
        pct.Top = 30;
        pct.Size = new Size(WIDTH, HEIGHT);
        pct.Image = new Bitmap(WIDTH, HEIGHT);
        Controls.Add(pct);

        timer = new System.Windows.Forms.Timer();
        timer.Interval = 250;
        timer.Tick += (sender,e)=>{MyRedraw();};
        timer.Start();
    }


    void Form_Load(object sender, EventArgs e)
    {
        ShowDeviceInfo();

        _wfx = new NativeMethods.WaveFormatEx();
        _wfx.cbSize = 0;//(Int16)Marshal.SizeOf(_wfx);
        _wfx.wFormatTag = NativeMethods.WAVE_FORMAT_PCM;
        _wfx.nChannels = 1;//wic.wChannels;
        _wfx.nSamplesPerSec = 22050;
        _wfx.wBitsPerSample = 8;
        _wfx.nBlockAlign = (Int16)(_wfx.wBitsPerSample / 8 * _wfx.nChannels);
        _wfx.nAvgBytesPerSec = _wfx.nSamplesPerSec * _wfx.nBlockAlign;

        {
            int retCode = NativeMethods.waveInOpen(ref _hwi, 0, ref _wfx, this.Handle, 0, NativeMethods.CALLBACK_WINDOW);
            if ( retCode != NativeMethods.MMSYSERR_NOERROR ) {
                Console.WriteLine("waveInOpen ErrorCode: 0x"+retCode.ToString("X"));
                return;
            }
            Console.WriteLine("waveInOpen success");
        }
    }


    protected override void WndProc(ref Message m)
    {
        switch (m.Msg) {
            case NativeMethods.WIM_OPEN:
                Console.WriteLine("open");
                if (PrepareBuffer()) {
                    btn.Enabled = true;
                }
                base.WndProc(ref m);
                break;
            case NativeMethods.WIM_CLOSE:
                base.WndProc(ref m);
                break;
            case NativeMethods.WIM_DATA:
                if ( _rec ) {
                    WaveInCallbackProc(m.WParam, m.LParam);
                }
                base.WndProc(ref m);
                break;
            default:
                base.WndProc(ref m);
                break;
        }
    }


    bool ShowDeviceInfo()
    {
        int n = NativeMethods.waveInGetNumDevs();
        if ( n == 0 ) {
            Console.WriteLine("No waveIn devices.");
            return false;
        }

        var wic = new NativeMethods.WaveInCaps();
        for ( int i=0 ; i<n ; i++ ) {
            int retCode = NativeMethods.waveInGetDevCaps(i, ref wic, Marshal.SizeOf(wic)); 
            if ( retCode == NativeMethods.MMSYSERR_NOERROR ) {
                Console.WriteLine("Name: "        + wic.szPname);
                Console.WriteLine("  Formats: 0x" + wic.dwFormats.ToString("X"));
                Console.WriteLine("  Channels: "  + wic.wChannels.ToString());
            }
            else {
                Console.WriteLine("waveInGetDevCaps ErrorCode: 0x"+retCode.ToString("X"));
            }
        }

        return true;
    }

    bool PrepareBuffer()
    {
        if(_hwi==IntPtr.Zero){return false;}

        int retCode;

        _wbuf = new MyWaveBuffer[N_BUFFERS];

        for ( int i=0 ; i<N_BUFFERS ; i++ ) {
            _wbuf[i] = new MyWaveBuffer(BUFFER_SIZE);

            retCode = NativeMethods.waveInPrepareHeader(_hwi, ref _wbuf[i].hdr, MyWaveBuffer.cbwh);
            if ( retCode != NativeMethods.MMSYSERR_NOERROR ) {
                Console.WriteLine("waveInPrepareHeader ErrorCode: 0x"+retCode.ToString("X"));
                return false;
            }

            retCode = NativeMethods.waveInAddBuffer(_hwi, ref _wbuf[i].hdr, MyWaveBuffer.cbwh);
            if ( retCode != NativeMethods.MMSYSERR_NOERROR ) {
                Console.WriteLine("waveInAddBuffer ErrorCode: 0x"+retCode.ToString("X"));
                return false;
            }
        }

        return true;
    }

    public bool WaveInCallbackProc(IntPtr hwi, IntPtr hwvhdr)
    {
        NativeMethods.WaveHdr wvhdr;

        if ( !_rec ) { return false; }
        if ( hwi != _hwi ) { return false; }

        MyWaveBuffer t;
        wvhdr = (NativeMethods.WaveHdr)Marshal.PtrToStructure(hwvhdr, typeof(NativeMethods.WaveHdr));
        t = (MyWaveBuffer)GCHandle.FromIntPtr(wvhdr.dwUser).Target;

        if ( t.hdr.dwBytesRecorded == BUFFER_SIZE ) {
            Array.Copy(t.data, 0, _waveData, 0, BUFFER_SIZE);
            _redrawRequest = true;
        }

        _counter++;
        Text = _counter.ToString();

        int retCode;
        retCode = NativeMethods.waveInUnprepareHeader(hwi, ref t.hdr, Marshal.SizeOf(wvhdr));
        if(retCode!=NativeMethods.MMSYSERR_NOERROR){Console.WriteLine("waveInUnprepareHeader failed");}
        retCode = NativeMethods.waveInPrepareHeader(hwi, ref t.hdr, Marshal.SizeOf(wvhdr));
        if(retCode!=NativeMethods.MMSYSERR_NOERROR){Console.WriteLine("waveInPrepareHeader failed");}
        retCode = NativeMethods.waveInAddBuffer(hwi, ref t.hdr, Marshal.SizeOf(wvhdr));
        if(retCode!=NativeMethods.MMSYSERR_NOERROR){Console.WriteLine("waveInAddBuffer failed");}

//        Text = _counter.ToString() + "+";

        return true; 
    }

    void MyRedraw()
    {
        if ( _redrawRequest ) {
            int step = (BUFFER_SIZE+WIDTH-1)/WIDTH;
            Graphics g = Graphics.FromImage(pct.Image);
            g.Clear(Color.White);

            int xPrev = (0*WIDTH)/BUFFER_SIZE;
            int yPrev = (_waveData[0] * HEIGHT) / 256;

            for(int i=step;i<BUFFER_SIZE;i+=step) {
                int x = (i*WIDTH)/BUFFER_SIZE;
                int y = (_waveData[i] * HEIGHT) / 256;

                g.DrawLine(Pens.Black, xPrev, yPrev, x, y);

                xPrev = x;
                yPrev = y;
            }

            g.Dispose();
            pct.Refresh();
        }
    }

    [STAThread]
    static void Main(string[] args)
    {
        if ( Marshal.SizeOf(IntPtr.Zero) != 4 ) {
            throw new Exception("must compile as 32bit application. (csc /platform:x86)");
        }
        else {
            Application.Run(new WaveInTest());
        }
    }
}

参考サイト - WinMM

WinMM関連の定義値はmmsystem.hをググると何かしら見つかるはず。

参考サイト - WASAPI

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

VisualStudioCodeを使い始めて変わったこと

VisualStudioCodeを使い始めて変わったことを書いていきたいと思います。

実は今回書いたのはVisualStudioCodeが出たての頃からのヘビーユーザです。

戸倉さん(@ayatokura)ごめんなさい。
自分はVisualStudioCodeが出たときからのヘビーユーザです。
なぜかと言うとEclipseが◯んこだったから。
1ヶ月に一回再インストールに飽き飽きしていた頃に出てきたのがAtomとVisualStudioCodeでした。
Atomはプラグインが少なかったのと重くて使いようになりませんでした。
そこで選んだのがVisualStudioCodeでした。
一番の特徴はTerminalが入っていてコマンドスクリプトも使えるんですよ。
実はLinux(Ubuntu・CentOS・RHESなどで使用可能)やMac・Windowsでも喜んでVisualStudioCodeを使っちゃうユーザでした。

依然ある職場でLinuxVimerにかなり妬まれました。
Vimが嫌になっておりしこたま困りました。
それはVisualStudioCodeという秘密兵器を知っていたからなんです。
目の前でUbuntu版のVisualStudioCode使ってガリガリやってやろうかと思ったことが思ったことがありました。
ですがさすがにやりませんでした。
それはDensoとBoschのJVのADITにいたからです。
大手に社の前でやってしまうと地雷を踏むことになります。
しかも職場はDenso刈谷本社が当時の職場だったので流石にまずかったので行動しませんでした。

今は別の職場でWebの基盤設計と開発をやっております。
今の職場はVisualStudioCodeなので嬉しくてたまりません。
だって楽ちんなんだもん。
もちろんハッカソンでもバリバリ使ってますよ。
個人で開発するときはもちろんVisualStudioCodeです。
ですがこの使いやすさを知っちゃうと離れられません。

どうしてVisualStuioCodeを使い倒すのか。

それは各プログラム言語・クラウド・Git・Subversionなど網羅してくれています。
Github・Bitbucket・AWS・Azureなどの基盤製品につなぐのが簡単です。
残念ながらちょっとSubversionはいけてません(戸倉さんごめんなさい)。
実は職場でSubversion使っていますので困っています。いいプラグインあれば教えて下さい。
Gitはバンバン使っています。
各言語・スクリプト言語のIntelliSense非常に素晴らしいです。
コーディングスピードが約二倍上がりました。
最近はJavaってEclipseやIntelliJじゃないのと言っている人いると思いますが。
コマンドラインが使えるのでVisualStudioCodeかけちゃいます。
実際自分もVisualStudioCodeで書いてること多いです。
パスさえ通してしまえばビルドまで出来ちゃいます。

C#書いてる人もVisualStudioじゃないとC#かけないんじゃないっと思っている人いると思います。
実はWindowsFormやUnityなどの画面付きじゃない場合はVisualStudioCodeで全て事が足ります。
C・C++はVimやEclipse・VisualStudioと思っている人いませんか?
画面系がなければすべてVisualStudioCodeで事が足りてしまいます。
実はLinuxのbashやzshのスクリプトも書けちゃいます。

これ覚えておくとLinuxユーザはかなり助かると思います。
あと色んなスクリプトのプラグインがあるので入れておくと便利です。

不都合ことも少しはあるかも

WindowsアプリやUnityやJavaの画面アプリを作っている人はかなり困るかも。
それはヘビーユーザなら誰もが知ってますがVisualStudioCodeにはHTMLの画面デバック機能は存在しますが。
WindowsアプリやJavaアプリの場合は作りが特殊です。
VisualStudioCodeで対応できないことがあります。
WindowsアプリやUnityの場合はVisualStudioを使ってください。
商用で使わない限りはこちらも無償のVisualStudioComminityEditionがあるのでそちらをお使いください。
Javaアプリの場合はこれは画像の裏のコードが特殊で複雑なことが多いので今の自分ならIntelliJのComminityをおすすめします。
AndroidStudioという手もありますがAndroidに特化しているのであまりおすすめしません。
実はIntelliJでAndroidプラグインを組み込めばAndroidの開発ができることがわかっています。

皆さん気をつけてもらいたいことがあります。
プラグインいっぱいあるから全部入れちゃえ。
これやっちゃだめです。
VisualStudioCodeはElectronでできてますのでプラグインを入れすぎると重くなります。
必要に迫られたときなどに入れる程度に留めておきましょう。

不都合なことがない場合はなるべく軽い開発環境がいいです。

正直言うと重い開発環境を使っていると非常に時間がもったいないことがじつは多いんです。
軽い環境だとやる気がどんどん出てきます。
大学でもViualStudioCode広めてください。
HTML・Python・PHP・C++(C)・JavaScvript・NodeJS・HTML5実はSwiftもKotlinも色んなプログラム言語がガリガリかけます。
おまけにGithubやBitbucketも簡単につなげるのでいいと思います。

VisualStudioCodeでEnjoyHackingしましょう。

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

C#で作るBINGO抽選エンジン

はじめに

気が付けば師走も半ば、世の中は忘年会シーズンですね。
車輪の再々々...発明な本記事ですが、備忘録も兼ねて投稿します。

作ったもの

余興などでBINGOを行うための抽選プログラムを作りました。
また、特定の番号を強制当選させたいことがあったため、初期化関数は抽選不可番号をオプションで指定できるようにしました。(BINGOとしては良くない気がする。)
なお、対象のフレームワークは.NET Framework 4.7.2で行いました。LINQが使えれば動く(はず)のでもっと古いバージョンでも大丈夫だと思います。

使い方

 以下のように使うことができます。

Program.cs
BingoEngine engine = new BingoEngine();

engine.Initialize(1, 20, 45);           // 1, 20, 45は当選させないように初期化

while (!engine.IsEmpty)                 // 抽選番号がなくなるまでループ
{
    int number = engine.DrawLottery();  // 抽選

    Console.WriteLine($"Get Number:{number}");
}

// Get Number:47
// Get Number:39
// Get Number:30
// ...

抽選エンジンの実装

 BINGOの抽選方法については、抽選番号リストにランダムアクセスするという方法もあるのですが、この方法では、当選した番号が再当選しないような仕組みが必要となります。
 そこで、今回は抽選番号リストをランダムに並び替えてしまい、後(あるいは前)から順番に値を読むという方法をとりました。
 実装したクラスBingoEngineを以下に示します。

BingoEngine.cs
using System;
using System.Collections.Generic;
using System.Linq;

public class BingoEngine
    {
        Queue<int> _numbers;    // 未抽選の番号を保持するキュー
        const int NMAX = 75;    // BINGOの最大番号は75
        const int NMIN = 1;     // BINGOの最小番号は1

        public BingoEngine()
        {
            Initialize();
        }

        /// <summary>
        /// ビンゴエンジンの初期化
        /// </summary>
        /// <param name="avoidNumbers">抽選させない番号</param>
        public void Initialize(params int[] avoidNumbers)
        {
            // キューの初期化
            _numbers = new Queue<int>(NMAX - avoidNumbers.Length);

            // 抽選対象番号をランダムに並び替える
            var init = Enumerable.Range(NMIN, NMAX).Except(avoidNumbers).OrderBy(v => Guid.NewGuid());

            foreach (var number in init)
            {
                _numbers.Enqueue(number);   // キューに番号を順に登録
            }
        }

        /// <summary>
        /// 抽選する
        /// </summary>
        /// <returns>当選番号</returns>
        public int DrawLottery()
        {
            // 例外処理:引くくじが残っていない
            if (_numbers.Count < 1) throw new Exception("Remaining numbers are already Empty.");

            return _numbers.Dequeue();
        }

        /// <summary>
        /// 玉が残っているか
        /// </summary>
        public bool IsEmpty => _numbers.Count < 1;
    }

以上です!読んでいただきありがとうございました。

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

C# xUnit

こういうのって実際のやつで試してみた方がいいんだろうけどとりあえず簡単なまとめ。
今回macでやっているのでxUnitを使っていきます。

まずはコンソールアプリのプロジェクトを作る。
スクリーンショット 2019-12-14 0.20.35.png

次にソリューションから追加で新しいプロジェクトを追加でxUnitを追加する。

スクリーンショット 2019-12-14 0.25.26.png

テストコード

UnitTest1.cs
using Xunit;

namespace unit_sample
{
    public class UnitTest1
    {
        [Fact]
        public void Test1()
        {
            Assert.True(true);
        }
    }
}

実行は右にある単体テストのところから行う。

スクリーンショット 2019-12-15 0.01.49.png

DisplayName

[Fact]にDisplayNameを加えると下の実行の所にDisplayNameが出てくる。

UnitTest1.cs
using Xunit;

namespace unit_sample
{
    public class UnitTest1
    {
        [Fact(DisplayName = "サンプルテスト")]
        public void Test1()
        {
            Assert.True(true);
        }
    }
}

実行結果 左下のところにDisplayNameで設定したものが出ている。

スクリーンショット 2019-12-15 1.07.00.png

Skip

[Fact]にSkipを加えるとそのテストはSkipされる。

UnitTest1.cs
using Xunit;

namespace unit_sample
{
    public class UnitTest1
    {
        [Fact(DisplayName = "サンプルテスト")]
        public void Test1()
        {
            Assert.True(true);
        }

        [Fact(Skip = "無視する")]
        public void Test2()
        {
            Assert.True(true);
        }
    }
}

Timeout

[Fact]にTimeoutを設定してあげると時間の上限を定めることができる。

UnitTest1.cs
using System.Threading.Tasks;
using Xunit;

namespace unit_sample
{
    public class UnitTest1
    {
        [Fact(DisplayName = "サンプルテスト")]
        public void Test1()
        {
            Assert.True(true);
        }

        [Fact(Timeout = 3000)]
        public async Task Test2()
        {
            await Task.Delay(4000);
            Assert.True(true);
        }
    }
}

Theory

メソッド一つで複数回の実行も可能
下記例であれば、aに1,0,-1がそれぞれ代入され実行される。

UnitTest1.cs
using Xunit;

namespace unit_sample
{
    public class UnitTest1
    {
        [Theory]
        [InlineData(1)]
        [InlineData(0)]
        [InlineData(-1)]
        public void Test1(int a)
        {
            Assert.InRange(a, -2, 2);
        }
    }
}

以下Assertの使い方。

分からないものが結構あったので学んでいきたいところ...
最初のTrueだとか以外はABCの順番でやっていったので
後で同系統のものをまとめたい。

tureであるかどうか

UnitTest1.cs
using Xunit;

namespace unit_sample
{
    public class UnitTest1
    {
        [Fact]
        public void Test1()
        {
            Assert.True(true);
        }
    }
}

falseかどうか

UnitTest1.cs
using Xunit;

namespace unit_sample
{
    public class UnitTest1
    {
        [Fact]
        public void Test1()
        {
            Assert.False(false);
        }
    }
}

Listが空かどうか

UnitTest1.cs
using System.Collections.Generic;
using Xunit;

namespace unit_sample
{
    public class UnitTest1
    {
        [Fact]
        public void Test1()
        {
            Assert.Empty(new List<string>());
        }
    }
}

文字列の後ろにxxxが含まれているかどうか

引数1がxxxで引数2が対象の文字列

UnitTest1.cs
using Xunit;

namespace unit_sample
{
    public class UnitTest1
    {
        [Fact]
        public void Test1()
        {
            Assert.EndsWith("es", "studies");
        }
    }
}

AとBが同じかどうか

UnitTest1.cs
using Xunit;

namespace unit_sample
{
    public class UnitTest1
    {
        [Fact]
        public void Test1()
        {
            Assert.Equal("aaa", "aaa");
        }
    }
}

範囲に入っているかどうか

第一引数が実際のやつ
第二引数が下限
第三引数が上限

UnitTest1.cs
using Xunit;

namespace unit_sample
{
    public class UnitTest1
    {
        [Fact]
        public void Test1()
        {
            Assert.InRange(3, 1, 4);
        }
    }
}

文字列もできるが使い道はないか。
下の例だとb,c,d,eが範囲となる

UnitTest1.cs
using Xunit;

namespace unit_sample
{
    public class UnitTest1
    {
        [Fact]
        public void Test1()
        {
            Assert.InRange("c", "b", "e");
        }
    }
}

型の一致確認

UnitTest1.cs
using Xunit;

namespace unit_sample
{
    public class UnitTest1
    {
        [Fact]
        public void Test1()
        {
            int a = 22;
            Assert.IsType(a.GetType(), 32);
        }
    }
}

型の不一致の確認

UnitTest1.cs
using Xunit;

namespace unit_sample
{
    public class UnitTest1
    {
        [Fact]
        public void Test1()
        {
            string a = "a";
            Assert.IsNotType(a.GetType(), 32);
        }
    }
}

正規表現とマッチしているかどうか

UnitTest1.cs
using Xunit;

namespace unit_sample
{
    public class UnitTest1
    {
        [Fact]
        public void Test1()
        {
            Assert.Matches("[a-z]", "a");
        }
    }
}

リストが空ではないかどうか

UnitTest1.cs
using System.Collections.Generic;
using Xunit;

namespace unit_sample
{
    public class UnitTest1
    {
        [Fact]
        public void Test1()
        {
            Assert.NotEmpty(new List<string>{ "apple"});
        }
    }
}

AとBが同じではないかどうか

UnitTest1.cs
using Xunit;

namespace unit_sample
{
    public class UnitTest1
    {
        [Fact]
        public void Test1()
        {
            Assert.NotEqual("aaa", "bbb");
        }
    }
}

範囲に入っていないかどうか

UnitTest1.cs
using Xunit;

namespace unit_sample
{
    public class UnitTest1
    {
        [Fact]
        public void Test1()
        {
            Assert.NotInRange(5, 1, 4);
        }
    }
}

Nullではないかどうか

UnitTest1.cs
using Xunit;

namespace unit_sample
{
    public class UnitTest1
    {
        [Fact]
        public void Test1()
        {
            int a = 1;
            Assert.NotNull(a);
        }
    }
}

同じではないかどうか

UnitTest1.cs
using Xunit;

namespace unit_sample
{
    public class UnitTest1
    {
        [Fact]
        public void Test1()
        {
            Assert.NotSame("a", "b");
        }
    }
}

Nullかどうか

UnitTest1.cs
using Xunit;

namespace unit_sample
{
    public class UnitTest1
    {
        [Fact]
        public void Test1()
        {
            Assert.Null(null);
        }
    }
}

同じかどうか

UnitTest1.cs
using Xunit;

namespace unit_sample
{
    public class UnitTest1
    {
        [Fact]
        public void Test1()
        {
            Assert.Same("aaa", "aaa");
        }
    }
}

要素が一つかどうか

UnitTest1.cs
using System.Collections.Generic;
using Xunit;

namespace unit_sample
{
    public class UnitTest1
    {
        [Fact]
        public void Test1()
        {
            Assert.Single(new List<int> {1});
        }
    }
}

最初の文字がxxxで始まるかどうか

UnitTest1.cs
using Xunit;

namespace unit_sample
{
    public class UnitTest1
    {
        [Fact]
        public void Test1()
        {
            Assert.StartsWith("red", "redmine");
        }
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Web開発でJavaScriptの代わりにC#でロジックを書くBlazorがめちゃ便利!

はじめに

UL Systems Advent Calendar 2019 の17日目です。

Blazorとは、.NET の技術を使ってクライアント側のWeb UIを構築するためのフレームワークです。このBlazorを使えば、今まで主に業務アプリケーションの開発で C#を使用してきたような開発者であっても、極力JavaScriptを使わず、追加の学習コスト少なくWebアプリケーション(SPA)を開発することができるようになります。

Blazor には、サーバ側で処理するBlazorサーバーと、クライアント側で処理するBlazor WebAssemblyがあります。Blazorの開発を今まさに進めており、2019年9月にリリースされた.Net Core 3.0にてBlazor サーバーが正式にサポートされるようになりました。Blazor WebAssemblyも2020年5月に正式リリースされる見通しです。
Blazor WebAssemblyは、webフロントエンドの高速化などを目的として、C、C++でコンパイルされた機械語をブラウザ上で実行できるWebAssemblyを使用していますが、先日WebAssemblyがW3C勧告に到達したことも発表され、事実上Webの標準になり、注目されています。

今回は、BlazorのメリットとBlazorの作成方法の触りをチュートリアルを通じてご紹介します。

Blazor のメリット

今までの.Netノウハウが活用できる

Blazor のメリットは、.NET ライブラリなど既存の .NETのノウハウを活用できることです。いつものVisual Studio、C#構文、NuGet、MSTestなどをそのままに開発することができます。

サーバーとクライアント全体でアプリケーションのロジックを共有できる

従来のサーバーと通信するアプリの場合、クライアントとサーバ間のやり取りをAPI経由で別々に実装していることが多いです。しかし、Blazorではクライアントとサーバーを一つのソリューションで作成できます。
例えば、型情報をDTOごと共有できるため、サーバ・クライアント間で型を合わせたり重複したコードを書くこともなくなります。

デバイスを選ばないクロスプラットフォーム

当たり前ですが、SPAなのでブラウザ上でブラウザが動く環境であれば基本的には同じように動作させることができます。現在、WebAssemblyは一般的に使用されるブラウザには実装済みです。
なお、プラグイン方式ではないので、その点ではSilverlightのような形で主要ブラウザで使えなくなってしまうようなことはなさそうです。

(Blazor WebAssemblyの場合、)常時接続環境を必須としない

Blazor WebAssemblyではオフラインのみでも実行可能であり、常時接続を保証できないような条件でも大丈夫です。
この場合、PWA化も選択肢に入るかと思います。

Blazor の始め方

Visual Studio 2019 最新版を持っていれば、Blazor拡張を別途インストールすることなくBlazorのプロジェクトを作成することができます。
なお、Blazorプロジェクトのプレビュー版のテンプレートを選択するためには下記のコマンドを打つ必要があります。
dotnet new -i Microsoft.AspNetCore.Blazor.Templates::3.1.0-preview4.19579.2

上記のコマンドにより、下記のテンプレートが選択できるようになります。
blazor_template.png

これを選択すると下記のように、数字をカウントアップするアプリと、取得したデータを表示する機能を持つアプリが出来上がっています。下記は取得したデータを表示するものです。
image.png

なお、以降では、Blazor WebAssemblyについて記載します。

Blazorプロジェクトの構造

Blazor のプロジェクトの中で作成が必要なファイルには主に以下があります。

  • Program.cs :ASP.NET Coreホストを設定するアプリのエントリポイント。定型で変更されることは殆どありません。
  • Startup.cs :アプリのスタート時に実行されるロジックです。下記のようにアプリ全体で使用する設定が記述されます。
    • アプリの依存関係挿入 (DI)サービス
    • アプリの要求処理パイプライン
  • wwwroot/index.html: Blazorそのものを起動するための情報が書かれています。META要素などのヘッダー情報を変更する必要が発生したら書き換えることになります。
  • [ページ] フォルダー(Razorファイル):アプリ上のページをコーディングします。
    • 各ページの名前は @page を使用して指定します。
    • HTML構文の中に @code で括ってC#のコードを埋め込むことができます。

チュートリアルをもとに実践する

Github上でピザの注文ウェブアプリが題材のワークショップが公開されています。
チュートリアルとしては、それなりのボリュームです。

https://github.com/dotnet-presentations/blazor-workshop

今回はこの中から、Section1, Section2を題材に、主要なコードを確認してみたいと思います。
この2つのセクションでは、ピザの注文のうち、クライアント側でピザを表示するところから注文を入力するところまでが実装されます。

なお、Blazorのプロジェクト作成の指針としては、下記のようにしていくのが望ましそうです。このワークショップで作成されるコードもこのようなプロジェクト構成になっています。

  • サーバー側の処理はServer側のプロジェクトに実装。基本的には、表示に必要なデータを構成することのみをサーバー側の責務とする。
  • レンダリング(HTML自体の構成)は主にClient側のプロジェクトに機能を実装。
  • jsonで通信するクラスやサーバーとクライアントの両方で使用するユーティリティをSharedのプロジェクトに実装。

Section1 Components and Layout

このSectionでは、「クライアント側の表示」処理の実装を作成しています。このSectionが完成すると以下のような画面が表示されます。

image.png

以下に、このSectionで作成されるコードの一部を示します。表示ロジックは.razor の拡張子のファイルにrazor構文という形で記述します。

razor構文の中で主に実装している内容は以下です。HTMLとC#の構文を知っている人であれば、それほど迷いなく理解できるのではないかと思います。

  • @code の中で画面に表示するためのオブジェクトの中身(specials)をセットして、HTMLの中に埋め込んだ変数で表示するようにしています。
  • @code のなかで使用しているHTTPClient は、@inject をページで宣言することで使えるようになります。HTTPClient はDIの宣言をせずにデフォルトで使用することができます。
  • サーバ側で宣言された"specials" APIからピザのデータを取得し、そのデータをビューに表示するようにしています。サーバ側に取得用の実装がありますがその説明は省略します(c#でDBからデータを取得するロジックが書かれています)
Pages/Index.razor
@page "/"
@inject HttpClient HttpClient
<div class="main">
    <ul class="pizza-cards">
        @if (specials != null)
        {
            @foreach (var special in specials)
            {
                <li style="background-image: url('@special.ImageUrl')">
                    <div class="pizza-info">
                        <span class="title">@special.Name</span>
                        @special.Description
                        <span class="price">@special.GetFormattedBasePrice()</span>
                    </div>
                </li>
            }
        }
    </ul>
</div>

@code {
    List<PizzaSpecial> specials;

    protected async override Task OnInitializedAsync()
    {
        specials = await HttpClient.GetJsonAsync<List<PizzaSpecial>>("specials");
    }
}

Section2 Customize a pizza

ここでは、クライアントでピザを選択し、ダイアログでトッピング等を選択したものの送信を作成します。
このSectionが完成すると、ピザをクリックして以下のようなダイアログが表示され、注文を送信することができます。

image.png

以下の説明ではセクションの内容全てではなく一部の説明を省いています。

Implement the pizza customization dialog

以下はピザが選択された後に、ダイアログを表示するイベントの実装です。
ダイアログが閉じられた後のイベントでダイアログが閉じられるよう、showingConfigureDialogのフラグでダイアログ起動を制御しています。

Pages/index.razor
<li @onclick="@(() => ShowConfigurePizzaDialog(special))" style="background-image: url('@special.ImageUrl')">  // ピザをクリックしたらShowConfigurePizzaDialogを呼び出す。
~~
@code {
    void ShowConfigurePizzaDialog (PizzaSpecial special)
    {
        configuringPizza = new Pizza()
        {
            Special = special,
            SpecialId = special.Id,
            Size = Pizza.DefaultSize,
            Toppings = new List<PizzaTopping>(),
        };

        showingConfigureDialog = true;
    }
}
@if (showingConfigureDialog)  // showingConfigureDialog がtrueになったらダイアログを表示、falseで非表示にする
{
    <ConfigurePizzaDialog Pizza="configuringPizza" ~~>
}

Data binding・Add additional toppings

ダイアログ側の処理です。以下の処理では、レンジ入力から数を設定するものと、リストボックスから値を選択する実装の一部です。レンジ入力のようにレンジバーが変わった都度値がデータに反映されるようにするために @bind をつけています。リストボックスの選択時は、メソッドを呼び出して値を入れています。

Shared/ConfigurePizzaDialog.razor
        <input type="range" min="@Pizza.MinimumSize" max="@Pizza.MaximumSize" step="1" @bind="Pizza.Size" @bind:event="oninput" />
~~
        <select class="custom-select" @onchange="@ToppingSelected">
            <option value="-1" disabled selected>(select)</option>
            ~~
        </select>
~~
    void ToppingSelected(ChangeEventArgs e)
    {
        if (int.TryParse((string)e.Value, out var index) && index >= 0)
        {
            AddTopping(toppings[index]);
        }
    }

    void AddTopping(Topping topping)
    {
        if (Pizza.Toppings.Find(pt => pt.Topping == topping) == null)
        {
            Pizza.Toppings.Add(new PizzaTopping() { Topping = topping });
        }
    }

Display the current order

最後に、選択された注文をサーバ側に送信する部分です。(orderという変数に注文内容が格納されていた後の処理です。

Pages/index.razor
async Task PlaceOrder()
{
    await HttpClient.PostJsonAsync("orders", order);
    order = new Order();
}

さいごに

c# でSPAが書けるBlazorを紹介しました。現段階でまだWebAssemblyはプレビュー版でまだ発展途上であることや、.Net技術者でwebアプリを書くことのニーズがどれほどあるか、またどの程度流行るのか未知数のところもあり、本番で使うことはまだ早いかもしれません。
しかし、このようなC#の技術を生かして、複雑なSPAを比較的学習コストの負担が少なく簡単に作れるBlazorは可能性のある技術だと思います。今後の動向にも注目です。

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