.NET Core上でLINQを使ってMySQLにアクセスする

Windows上では、Visual Studioを使ってデザイナを使いながらMySQLからテーブル構造を引っ張ってこれるのだが、じゃあ、.NET Core の場合はどうするのか?
多分、マイグレーション機能を使って、MySql.Data.EntityFrameworkCore.Design あたりを NuGet でとってきてあれこれやるんだろうだが、面倒なので。

  • あらかじめ、Windows の EF デザイナで取得したクラスを取ってくる。
  • 手作業でテーブルクラスを作る

のどちらかがよい。

Redmine のプロジェクト(projects)とチケット(issues)は、こんな風になっている。これ自体は、Windows 上の .NET でも使えるし、Linux 上の .NET Core でも使える。

public class projects
{
    public int id { get; set; }
    public string name { get; set; }
    public string description { get; set; }
    public string homepage { get; set; }
    public int is_public { get; set; }
    public int? parent_id { get; set; }
    public DateTime? created_on { get; set; }
    public DateTime? updated_on { get; set; }
    public string identifier { get; set; }
    public int status { get; set; }
    public int? lft { get; set; }
    public int? rgt { get; set; }
    public int inherit_members { get; set; }
    public int? default_version_id { get; set; }
}

public class issues
{
    public int id { get; set; }
    public int tracker_id { get; set; }
    public int project_id { get; set; }
    public string subject { get; set; }
    public string description { get; set; }
    public DateTime? due_date { get; set; }
    public int? category_id { get; set; }
    public int status_id { get; set; }
    public int? assigned_to_id { get; set; }
    public int priority_id { get; set; }
    public int? fixed_version_id { get; set; }
    public int author_id { get; set; }
    public int lock_version { get; set; }
    public DateTime? created_on { get; set; }
    public DateTime? updated_on { get; set; }
    public DateTime? start_date { get; set; }
    public int done_ratio { get; set; }
    public double? estimated_hours { get; set; }
    public int? parent_id { get; set; }
    public int? root_id { get; set; }
    public int? lft { get; set; }
    public int? rgt { get; set; }
    public int is_private { get; set; }
    public DateTime? closed_on { get; set; }
}

ちなみに、Visual Studio の EF デザイナだと、こんな projects クラスを吐き出す。アノテーションがついているので、asp.net mvc でスキャフォールディングするときに便利っぽい感じが…するが、まあなくても大丈夫

 [Table("redmine.projects")]
public partial class projects
{
    public int id { get; set; }

    [Required]
    [StringLength(255)]
    public string name { get; set; }

    [Column(TypeName = "text")]
    [StringLength(65535)]
    public string description { get; set; }

    [StringLength(255)]
    public string homepage { get; set; }

    public bool is_public { get; set; }

    public int? parent_id { get; set; }

    public DateTime? created_on { get; set; }

    public DateTime? updated_on { get; set; }

    [StringLength(255)]
    public string identifier { get; set; }

    public int status { get; set; }

    public int? lft { get; set; }

    public int? rgt { get; set; }

    public bool inherit_members { get; set; }

    public int? default_version_id { get; set; }
}

いわゆる、Entities なクラスはどうするかというと、NuGet で「MySql.Data.EntityFrameworkCore」を入れた後に、こんな風に手作業で作る。

public partial class RedmineEntities : DbContext
{
    public RedmineEntities()
    {
            
    }
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        base.OnConfiguring(optionsBuilder);
        optionsBuilder.UseMySQL(@"server=localhost;user id=redmine;password=redmine;database=redmine;port=3306;sslmode=None");
    }

    public DbSet<projects> projects { get; set; }
    public DbSet<issues> issues { get; set; }
}

Windows 上の DbContext と違うのは、データベースに接続するための設定が OnConfiguring メソッドをオーバライドして書くことになる。MySQL の場合は UseMySQL メソッドで指定する。この部分は拡張メソッドになっていて、インターフェースを決めるのではなくて、ダックタイピングなところが興味深いところ。以前は Factory だったような気もするのだが、こんな風にメソッド名でなんとなくルールを決めておくのが実に C# っぽい。

ここまで出来たら、後は .NET Core のコンソールアプリを作る。

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Hello MysQL EF ");

        var ent = new RedmineEntities();
        var items = ent.projects.ToList();
        foreach (var it in items)
        {
            Console.WriteLine($"{it.id} {it.identifier} {it.name}");
        }
    }
}

シンプルに Redmine のプロジェクト一覧を表示しているだけなのだが、LINQ が使えるので SQL 文を書く必要がない。このぐらいの SELECT だと、普通に SQL 文を書いても手間は変わらないのだけど、Update とか Insert とかをやり始めると、ent.SaveChanges() で一発で書き込めるのは結構便利だったりする。特に、カラムの数が多い場合とか。

これを .net core の asp.net mvc で web api 化すると便利だったりするのだが、ターミナルを使ってコマンドラインで打てるのもよいだろう。GUI 絡みは .NET Core ではできないので、別途 Windows から MySQL のあるサーバーに繋げて、WPF で書くのがベターだろう。

カテゴリー: 開発, C# | .NET Core上でLINQを使ってMySQLにアクセスする はコメントを受け付けていません

LINQを使ってMySQLにアクセスする

常々、LINQ を使うために SQL Server を使う必要があって、じゃあ、Linux+MySQLの組み合わせに対して C#+LINQ はどうアクセスすればよいのか?と思っていたのだが、いつの間にか Entity Framework 相当のもの、というか MySql.Data.EntityFramework が出来ていたらしい。

MySQL :: Download Connector/Net
https://dev.mysql.com/downloads/connector/net/

いきなり、Connector/Net 8.0.11 というバージョンが上がっていて(あちこちの記事は 6.x だったりするんだが)、どうやら4月頃にアップデートしている。

何ができるのというと、Redmine や WordPress の内部にある MySQL に LINQ を使って直接アクセスできるということだ。こんな感じに、wordpress の wp_posts テーブルにACCESSできる。

image

NuGet から MySql.Data.EntityFramework をインストールする

実は Oracle のサイトから、Connector/Net をインストールしなくても、NuGet にある。

image

プロジェクトのほうは、MySql.Data.EntityFramework をインストールすれば ok. MySql.Data も一緒にインストールされる。

MySQL のプロバイダをインストールする

EF 自体は、NuGet から取れるのだが、SQL Server のようにデザイナを使いたい場合は、MySQL for Visual Studio をインストールする。

MySQL :: Download MySQL for Visual Studio https://dev.mysql.com/downloads/windows/visualstudio/

これを入れると、EF で「新しい接続」を選択したときに “MySQL Database” ってのが出るようになる

image

MySQL のテーブルを取り込む

準備ができたら、プロジェクトに「ADO NET Entity Data Model」を追加する。

image

モデルのコンテンツの選択では、

  • データベースから EF Designer
  • データベースから Code First

が2つある。Designer は、*.edmx ファイルを作ってグラフィカルなテーブルが表示されるもの。特にグラフィック部分が要らないのであれば、Code First で十分だったりする。

image

「新しい接続」ボタンをクリックして、MySQL Database を選ぶと、接続先のサーバー名とかを入れる画面が出る。ここで「テスト接続」をして接続できることを確認しておく。

image

さて、実は Connector/Net 8.0.11 にバグがあるらしく、ここの「詳細設定」を押して、Ssl Mode の値を「None」にしておかないといけない。

image

マニュアルには SslMode はデフォルトで None なのだけど、初期値が間違っているらしく、次の取得するテーブルを選ぶところで落ちてしまうのだ。

SQL Server に LINQ するときと同じようにクラス化するテーブルを選ぶ。

image

redmine の projects と issues が無事取得できる。

image

これで LINQ が使えるので、

private void clickGetIssues(object sender, RoutedEventArgs e)
 {
     var ent = new RedmineModel.RedmineEntities();
     var q = from t in ent.issues
             where t.project_id == 10
             select t;
     lv.ItemsSource = q.ToList();
 }

とか

private void clickGetWpPosts(object sender, RoutedEventArgs e)
{
     var ent = new WordPressModel.WordpressEntities();
     ent.Database.Log = s => System.Diagnostics.Debug.WriteLine(s);
     var q = from t in ent.posts
             orderby t.post_date descending
             select new { id = t.ID, title = t.post_title, date = t.post_date }
             ;
     q = q.Take(30);
     lv.ItemsSource = q.ToList();
}

な感じで、MySQL へ LINQ を使ってアクセスができる。

ホスティングしている場合は wordpress の MySQL に直接アクセスはできないだろうけど、ローカルで作っているとか高速にページをアップするとか修正するときに便利かもしれない。

カテゴリー: C#, MySQL | LINQを使ってMySQLにアクセスする はコメントを受け付けていません

ラズパイの Readmine 3.4 をインストールする手順

基本は、以下の redmine.jp の手順に従えばいいのだが、自分のメモ用に。

Redmine 3.4をUbuntu Server 16.04.2 LTSにインストールする手順 | Redmine.JP Blog
http://blog.redmine.jp/articles/3_4/install/ubuntu/
Raspberry Pi 3 に Redmine 3.3 をインストール | スモールサーバ開発部 https://blog.smallserver.jp/raspberry-pi-3-%E3%81%AB-redmine-3-3-%E3%82%92%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB/

– Raspberry Pi3 Rasbian Jessie
– Redmine 3.4
– MySQL 5.5
– Apache 2.4.10

pi@raspi3:/usr/local $ uname -a
Linux raspi3 4.9.35-v7+ #1014 SMP Fri Jun 30 14:47:43 BST 2017 armv7l GNU/Linux
pi@raspi3:/usr/local $ mysql --version
mysql  Ver 14.14 Distrib 5.5.60, for debian-linux-gnu (armv8l) using readline 6.3
pi@raspi3:/usr/local $ sudo apachectl -v
Server version: Apache/2.4.10 (Raspbian)
Server built:   Apr  3 2018 16:25:06
pi@raspi3:/usr/local $

パッケージのインストール

sudo locale-gen ja_JP.UTF-8
sudo apt-get update
sudo apt-get install -y build-essential zlib1g-dev libssl-dev libreadline-dev libyaml-dev libcurl4-openssl-dev libffi-dev
sudo apt-get install mysql-server libmysqld-dev

MySQLの準備

外部から直接MySQLに触れるようにIP付きも指定しておく

mysql -u root -p

CREATE DATABASE redmine charset="utf8";
GRANT ALL PRIVILEGES ON redmine.* TO redmine@localhost IDENTIFIED BY 'redmine';
GRANT ALL PRIVILEGES ON redmine.* TO redmine@"172.16.0.%" IDENTIFIED BY  'redmine' WITH GRANT  OPTION;

my.cnf で bind-address をコメントアウト

vi /etc/mysql/my.cnf
↓コメントアウト
# bind-address = 127.0.0.1

opensslをビルド

Rubyがopensslを必要とするのでビルド。ラズパイの場合はパッケージがないので、ソースからビルドする。

wget https://www.openssl.org/source/openssl-1.0.2.tar.gz
tar xvf openssl-1.0.2.tar.gz
cd openssl-1.0.2
./config zlib shared no-ssl2
make && sudo make install

rubyをビルド

ruby 2.4 を使うためにソースからビルドする

curl -O https://cache.ruby-lang.org/pub/ruby/2.4/ruby-2.4.1.tar.gz
tar xvf ruby-2.4.1.tar.gz
cd ruby-2.4.1
./configure --disable-install-doc --with-opt-dir=/usr/local/ssl --enable-shared 
make && sudo make install
cd ..

opensslを入れないと、↓でエラーになる。

sudo gem install bundler --no-rdoc --no-ri

Redmineをインストール

Redmine を svn でとってきて配置。最終的には、http://raspi3.local/redmine でアクセスできるようにするが、/var/lib/redmine にインストールして alias する。

sudo mkdir /var/lib/redmine
sudo chown www-data /var/lib/redmine
sudo -u www-data svn co http://svn.redmine.org/redmine/branches/3.4-stable /var/lib/redmine

database.yml を作成する。データベースは mysql2 を使う。

sudo vi /var/lib/redmine/config/database.yml

production:
  adapter: mysql2
  database: redmine
  host: localhost
  username: redmine
  password: "redmine"
  port: 3306

gem パッケージをインストールした後、初期データを入れる。初期データを戻したい場合は、drop database redmine ; してから、RAILS_ENV の 2行を実行する。

cd /var/lib/redmine
sudo -u www-data bundle install --without development test --path vendor/bundle

sudo -u www-data bundle exec rake generate_secret_token
sudo -u www-data RAILS_ENV=production bundle exec rake db:migrate
sudo -u www-data RAILS_ENV=production REDMINE_LANG=ja bundle exec rake redmine:load_default_data

Apacheの設定

Passenger をインストールする。Apache上のRailsの実行で使われる。

sudo gem install passenger -v 5.1.12 --no-rdoc --no-ri
sudo passenger-install-apache2-module --auto --languages ruby
passenger-install-apache2-module --snippet

redmine.conf を作成する。
最後の Alias /redmine で、ブラウザからアクセスする場所を決める。ルートにしたい場合は、/etc/apache2/sites-enabled/000-default.conf の DocumentRoot を書き替える。

sudo vi /etc/apache2/conf-available/redmine.conf

# Redmineの画像ファイル・CSSファイル等へのアクセスを許可する設定。
# Apache 2.4のデフォルトではサーバ上の全ファイルへのアクセスが禁止されている。
<Directory "/var/lib/redmine/public">
  Require all granted
</Directory>

# Passengerの基本設定。
# passenger-install-apache2-module --snippet で表示された設定を記述。
# 環境によって設定値が異なるため以下の5行はそのまま転記せず、必ず
# passenger-install-apache2-module --snippet で表示されたものを使用すること。
#

LoadModule passenger_module /usr/local/lib/ruby/gems/2.4.0/gems/passenger-5.1.12/buildout/apache2/mod_passenger.so
<IfModule mod_passenger.c>
  PassengerRoot /usr/local/lib/ruby/gems/2.4.0/gems/passenger-5.1.12
  PassengerDefaultRuby /usr/local/bin/ruby
</IfModule>

# 必要に応じてPassengerのチューニングのための設定を追加(任意)。
# 詳しくはPhusion Passenger users guide(https://www.phusionpassenger.com/library/config/apache/reference/)参照。
PassengerMaxPoolSize 20
PassengerMaxInstancesPerApp 4
PassengerPoolIdleTime 864000
PassengerStatThrottleRate 10

Alias /redmine /var/lib/redmine/public
<Location /redmine>
  PassengerBaseURI /redmine
  PassengerAppRoot /var/lib/redmine
</Location>

設定の反映と、apacheの再起動

sudo a2enconf redmine
apache2ctl configtest
sudo service apache2 reload

これで、http://raspi3.local/redmine でアクセス可能になる。
最初は、admin/admin で入れる。

ちなみに、ラズパイ3でも結構遅いので実運用する場合は別途PCに立てるのがベターだと思う。

カテゴリー: 開発, RaspberryPi | ラズパイの Readmine 3.4 をインストールする手順 はコメントを受け付けていません

Redmine Client の作り始め

唐突に Readmine の Windows クライアントがが欲しくなったので作り始め。とはいえ、土曜日から作っていて後から知ったのだけど、Redmine の .NET ライブラリって zapadi/redmine-net-api にあったのであった。NuGet からも落とせる https://www.nuget.org/packages/redmine-api/ ので、それを使えばいいのだが。まあ、ほどよく動くところまで出来てきたので、自前のものを使うことにする。

WPF で作る

最近、VB6 から WPF 版の移植をやっていて、無駄に EF+MVVM+DataGrid の組み合わせのノウハウがついてしまったので早速利用する。

redmine の apikey を取ってきて、RmClient.config に置くと、こんな風にプロジェクト一覧、チケット一覧、チケットの編集ができるようになる。

image

ひとまずは、チケットの新規作成と更新ができればよいので、そこだっけ実装して試している。プロジェクトをあちこち移動すること少ない(サブプロジェクトを組んだ時は別だろうけど)ので、プロジェクト単位に config を用意しておいて専用のアプリ(不具合管理と課題管理とか)を立ち上げる、という風にすると手順が少なくて済むかなと思っている。アプリごとお客に配布して、クライアントから直接いれて貰うとかできればいい。Redmine のブラウザよりも機能を絞ってしまう感じ。

コマンドラインで動かす

手元の wordpress 投稿ツールのように、テキストエディタ+コマンドラインで動くようにしておく。

これは、チケットID を指定して、内容を取ってくる。

image

そして、内容をエディタで編集する。

image

そして、コマンドラインから投稿する。

image

ブラウザからツールで見れば、内容やステータスが更新されていることが分かる。

image

チケットの新規作成とかもできるので、数十件のチケットを同時にアップするとかも可能になる。

RmClient.Lib を .NET Standard 化する

コマンドラインツールが、.NET Core でも動くように、ライブラリ自体は .NET Standard で作っておく。

image

RmClient.Console.Core のほうが .NET Core 版で、Linux 上からも使える

dotnet run Get <ticket_id>

のようにパラメータを指定する訳なんだが、このあたりの解説はまた別途。

コード

ひとまず、現状を github にアップ。

moonmile/RmClient: Redmine Client
https://github.com/moonmile/RmClient

カテゴリー: C#, RmClient | Redmine Client の作り始め はコメントを受け付けていません

Visual Studioで指摘される「名前指定の規則違反」を直す

Visual  Studio を使ってボタンのイベントを作ると button1_Click な感じにキャメルケース(camelCase)になるのだが、「名前指定の規則違反」を指摘されて、こんな感じに大文字で始まらなければいけない、と言われる。

image

毎度、なんじゃこりゃ?と思っている訳で、結構前から C# – イベントハンドラーで構文エラー「~button1_Click~」(100021)|teratail 問題があったりするわけだが。これ、命名規約のルールとして、

  • プライベートなメソッドは小文字で始める
  • パブリックなメソッドは大文字で始める

というパターンが妥当で(実際、以前からそうなっていたし)、イベントの自動生成もプライベートなフィールド名前(button1)から自動でイベントメソッド名を付けている訳で、このキャメルケースでいいような気もする。

というか、Microsoft の初期設定はまあいいから、プライベートなメソッドはキャメルケースにして小文字で始めたいんですけど。

どこで命名規約を決めているのか?

「ツール」→「オプション」で、「テキストエディタ」→「C#」→「コードスタイル」→「名前指定」で命名規約は決めてある。

image

デフォルトでは、3つだけ指定してあって、

  • インターフェースを「I」で始めること
  • 型(クラス)をパスカルケース(大文字始まり)にすること
  • フィールド以外のメンバはパスカールケース(大文字始まり)にすること

というのが決めれれていて、最後の「フィールド以外」ってのに引っ掛かっている。なので、手っ取り早いのは、この規約を消してしまうことだ。右にある「×」ボタンを押して規約から外してしまうと、先の自動生成のイベント名に適用されなくなって警告がでなくなる。

「フィールド以外のメンバー」の適用範囲を public のみにする

もう少し意図通りに動かしたいときは、「仕様の管理」をクリックして「フィールド以外のメンバー」の適用範囲を変えてやる。

image

デフォルトは、こんな感じにアクセシビリティが、public から private まで全てになっている。ここで private とかを外して public だけにする。

image

意図的に、public だけパスカルケースにする

image

C# のエディタを閉じて、開き直すと無事警告がでなくなる。

image

命名規約に return がないのが問題?

これで無事、イベントで生成されるメソッド名に警告がでなくなるのだが、じゃあ、最初に話を戻して

  • プライベートなメソッドは小文字で始める
  • パブリックなメソッドは大文字で始める

という2つの規約を作りたいときはどうするのか?という問題が残る。Visual Studio の命名規則の適用って、上から順番に or で対処していくパターンなので、Outlook のメール振り分けのように途中で return ってのはない。なので、重要度に「なし」をするとうまく合格させることもできるのだが、ここで例外を作るとロジック的に複雑になってしまう。なので、適用範囲を絞りながら少しずつ追加するのがベターじゃないかなと。

でもって、プライベートなメソッドは必ずキャメルケース(小文字始まり)で、ということにするには、こんな風に規則を追加する。

image

最後の2つで、メソッドの命名規則(private と public)を分けることになる。

image

カテゴリー: C# | Visual Studioで指摘される「名前指定の規則違反」を直す はコメントを受け付けていません

EFのクラスをMVVMのINotifyPropertyChangedに対応させる裏技

WPFでEF(Entitiy Framework)を使っていると、リスト表示には楽なのだが、データを反映するためにはINotifyPropertyChangedを継承させなくてはいけなくて、そこが面倒くさい。いちいち、ViewModel クラスで EF のクラスをくるめば良いのだが、それを自動化したい。
特にマスター管理用の画面なんかを作るときは、ASP.NET MVCのように足場を自動化させておきたいというのもある。

EFで出力されるクラスは、下記のようなクラスなので、

public partial class 社員名
{
    public int 社員ID { get; set; }
    public Nullable<int> 部署ID { get; set; }
    public Nullable<int> 営業担当FLG { get; set; }
    public string 社員名1 { get; set; }
}

MVVM対応のINotifyPropertyChangedインターフェースを継承したこんなクラスにしたいわけだ。

public partial class 社員名 : ObservableObject 
{
    private int _社員ID ;
    public int 社員ID {
    	get { return _社員ID; }
    	set { SetProperty( ref _社員ID, value, nameof(社員ID)); }
    }
    private Nullable<int> _部署ID ;
    public Nullable<int> 部署ID {
    	get { return _部署ID; }
    	set { SetProperty( ref _部署ID, value, nameof(部署ID)); }
    }
    private Nullable<int> _営業担当FLG ;
    public Nullable<int> 営業担当FLG {
    	get { return _営業担当FLG; }
    	set { SetProperty( ref _営業担当FLG, value, nameof(営業担当FLG)); }
    }
    private string _社員名1 ;
    public string 社員名1 {
    	get { return _社員名1; }
    	set { SetProperty( ref _社員名1, value, nameof(社員名1)); }
    }
}

こうしておくと、リストビューで一覧表示をさせておいて、選択時にテキストボックスへ表示、そしてテキストボックスでの変更がリストビューに反映される、というところが自動化される。

Model1.ttを直接書き替える

元のクラスにスクリプトを通して、MVVM対応のクラスを作ろうかとも思ったのだが、もっと簡単に EF で出力される Model1.tt を直接書き替えてしまう。T4 で書かれている Model1.tt はファイルに保存した途端にテンプレートに従って各クラスが出力される。

public class CodeStringGenerator
{
	...
    public string Property(EdmProperty edmProperty)
    {
        return string.Format(
            CultureInfo.InvariantCulture,
			// ★ここを書き替え
@"private {1} _{2} ;
	{0} {1} {2} {{
		get {{ return _{2}; }}
		set {{ SetProperty( ref _{2}, value, nameof({2})); }}
	}}",

			// "{0} {1} {2} {{ {3}get; {4}set; }}",
            Accessibility.ForProperty(edmProperty),
            _typeMapper.GetTypeName(edmProperty.TypeUsage),
            _code.Escape(edmProperty),
            _code.SpaceAfter(Accessibility.ForGetter(edmProperty)),
            _code.SpaceAfter(Accessibility.ForSetter(edmProperty)));
    }
	...
    public string EntityClassOpening(EntityType entity)
    {
        return string.Format(
            CultureInfo.InvariantCulture,
			// ★ここを書き替え
            "{0} {1}partial class {2}{3} : ObservableObject ",
            Accessibility.ForType(entity),
            _code.SpaceAfter(_code.AbstractOption(entity)),
            _code.Escape(entity),
            _code.StringBefore(" : ", _typeMapper.GetTypeName(entity.BaseType)));
    }
	...
}

大ざっぱだが、CodeStringGeneratorクラスのPropertyメソッドとEntityClassOpeningメソッドの内容を書き替えてしまう。

EntityClassOpening は、エンティティのクラスを出力するところなので、INotifyPropertyChangedインターフェースを実装した「ObservableObject」クラスを継承するように書き替える。ObservableObjectクラスは別途自前で用意しておく。
Property メソッドは、プロパティを出力するところなので、SetProperty メソッドを呼び出して変更通知が飛ぶようにする。

これを保存すると、各エンティティのクラスが、

public partial class 社員名 : ObservableObject 
{
    private int _社員ID ;
    public int 社員ID {
    	get { return _社員ID; }
    	set { SetProperty( ref _社員ID, value, nameof(社員ID)); }
    }
	...
}

のように書き変わる。

のような画面が、バインドだけで作れるようになる。

カテゴリー: 開発, C# | EFのクラスをMVVMのINotifyPropertyChangedに対応させる裏技 はコメントを受け付けていません

DataGrid の SelectedItem にバインドしていると、ItemsSource 再設定時に落ちる

ちょっとばかし、ハマったのでメモ書きをしておく。

WPFのDataGridにデータバインドする

WPFでリスト表示をするときに、ListViewかDataGridを使うと思うのだが、ここで MVVM を使って ItemsSource にバインドする。

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="50"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    <DataGrid Grid.Row="1" x:Name="lv"
                ItemsSource="{Binding Items}"
                SelectedItem="{Binding Item}"
                SelectedIndex="{Binding ItemIndex}"      >
    </DataGrid>

ViewModel を作って

public class ViewModel : ObservableObject
{
private List<商品> _items;
public List<商品> Items
{
    get { return _items; }
    set { SetProperty(ref _items, value, nameof(Items)); }
}

private 商品 _item;
public 商品 Item
{
    get { return _item; }
    set { SetProperty(ref _item, value, nameof(Item)); }
}
private int _itemIndex;
public int ItemIndex 
{
        get { return _itemIndex; }
        set { SetProperty(ref _itemIndex, value, nameof(ItemIndex)); }
    }
}

ロード時に表示させる。

private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
    _vm = new ViewModel();
    var ent = new Database1Entities();
    _vm.Items = ent.商品.ToList();
    this.DataContext = _vm;
}
ViewModel _vm;

実行すると、こんな感じ。

行を選択するとバインドしている SelectedItem にオブジェクト(ここでは商品オブジェクト)が入るので、詳細データとかを別画面で表示するには便利だったりする。
SelectedIndex でもいいような気もするのだが、実は、DataGrid はヘッダをクリックするとソートする機能が初めから入っていて、結構便利。でもって、ソートしたときはインデックスが変わってしまうので、SelectedIndex では困るので、SelectedItem から選択したオブジェクトを拾うと良いという訳。渡している List<商品> と DataGird 自身が持っている Items の中身が異なる(ソートされている)のでずれがでてくる。

リストをリフレッシュすると落ちる

さて、普段は DataGrid の中身は変わらないので更新することはないのだが、なんらかの理由でリストを再描画させたいとしよう。このときに、ItemsSource にバインドしている ViewModel 側の Items を更新すればよいと思って、次な感じにすると、

private void clickInit(object sender, RoutedEventArgs e)
{
    var ent = new Database1Entities();
    _vm.Items = ent.商品.ToList();
}

~~~
System.NullReferenceException が発生しました
HResult=0x80004003
Message=オブジェクト参照がオブジェクト インスタンスに設定されていません。
~~~

なる例外が発生して落ちる。それも、_vm.Items に代入しようとしているところで落ちるので始末が負えない。
実は、初期表示をしていて、カーソルを DataGird にあてない状態(選択行が無い状態)の場合は落ちなくて、一度選択した後には例外が発生して落ちるのである。
かなり不思議な現象である。ネットでもあちこち困っている人がいるもの、ぴっちりとした解決策は無いように見える、が、

実は、このように Item に null を代入すると落ちなくなる。

private void clickReload(object sender, RoutedEventArgs e)
{
    var ent = new Database1Entities();
    _vm.Item = null;	// ★
    _vm.Items = ent.商品.ToList();
}

そう、ViewModel を見るとわかるのだが、DataGrid の SelectedItem へのバインドが悪さをしている。Items に新しいリストを設定して、ItemsSource プロパティを更新してしまうと、SelectedItem が行先を見失う(?)ことになって NULL例外が発生するらしい。このため、先に SelectedItem に null を設定してやって、カーソルを外した後で ItemsSource に新しいリストを設定することになる。

おまけ

じゃあ、ObservableCollection を使って、一度クリアしたあとに1つずつ追加しけばいいじゃないか、と思うかもしれないが、実はそれでも落ちる。

private void clickReload(object sender, RoutedEventArgs e)
{
    var ent = new Database1Entities();
    _vm.Item = null;	// ★
    _vm.Items.Clear();
    foreach (var it in ent.商品) _vm.Items.Add(it);
}

普通の Items の更新と同じように、★部分の null の設定がないと、_vm.Items.Clear() 行で例外が発生して落ちてしまう。これも、SelectedItem が示し先を見失ってしまうためらしい。

カテゴリー: 開発, C#, XAML | DataGrid の SelectedItem にバインドしていると、ItemsSource 再設定時に落ちる はコメントを受け付けていません

DataSetはLINQの夢を半分だけ見られるか?

1か月もブログ記事を書かないとサーバー側のキャッシュがおかしくなる(っぽい)ので、穴埋めに。

Visual Studio 2017には「LINQ to SQL」がないよ

ふと、書籍の改訂版を作っていて、2015から2017に移行するときにテンプレートに「LINQ to SQL」がないことに気付きました。

Visual Studio 2017

Visual Studio 2015

LINQ to SQLが何じゃ?と思うかもしれませんが(実際、何じゃ?という代物なんですが)、DataSetを使っていた後に C#/VB で LINQ が使えるようになって、今の Entity Framework が出る前に SQL Server を LINQ で扱うとという「非常にピンポイント」なコンポーネントだったんですね。確か、同時期に「EF Context」が出てたとおもうのですが、EF Contextのほうはデザイナがなくて、データベースからドラッグ&ドロップするにはこの「LINQ to SQL」のほうが(解説に)便利だったんですよ。

ところで、今「EF 6.x DbContext」を追加しようとすると「置換トークン ‘$edmxInputFile$’ を、生成元になる .edmx ファイルの実際の名前で上書きしてください。」と出るのは、不具合なのかUpdate不足なのか…不明ですが。それはさておき。

「LINQ to SQL」自体は、SQL Serverにしか対応していなくて、MySQLやSQLiteには使えないから当時から汎用性がないのは承知なのですが、逆に言えば SQL Server だけ使うことが決まっていればこれでも良い訳です。まあ、今から作るとなると、EF のほうを使ったほうがいいので、「ADO.NET Entity Data Model」を使ったほうがいいよねということになります。

今後 2017 のテンプレートとして追加されるかどうかは不明なのですが、まあディスコンという奴です。

DataSetってLINQができたっけ?

じゃあ、仕方がない古いところで DataSet って LINQ ってできたっけ?と思ってみたのがこんなところです。

古き良き時代?のDataSetを追加して、SQL Server から2つのテーブルをドラッグ&ドロップします。すると型付きの DataTable ができますよね。

でもって、3つのボタンを付けて動かしてみます。

private void button1_Click(object sender, EventArgs e)
{
    var ad = new DataSet1TableAdapters.ProductTableAdapter();
    var ta = ad.GetData();
    var q = from t in ta select t.Name;
    listBox1.DataSource = q.ToList();
}

private void button2_Click(object sender, EventArgs e)
{
    var ad = new DataSet1TableAdapters.ProductTableAdapter();
    var ta = ad.GetData();
    var q = from t in ta where t.Id == 2 select t.Name;
    listBox1.DataSource = q.ToList();
}

private void button3_Click(object sender, EventArgs e)
{
    var ad1 = new DataSet1TableAdapters.ProductTableAdapter();
    var tproduct = ad1.GetData();
    var ad2 = new DataSet1TableAdapters.StoreTableAdapter();
    var tstore = ad2.GetData();

    var q = from p in tproduct
            join s in tstore on p.Id equals s.ProductId
            select $"{p.Name}({s.Description}) {s.Stored}個";
    listBox1.DataSource = q.ToList();
}

動かしてみると、おお、なんか(一見)LINQが動いているように見えますね。キチンとテーブル結合(join)も動いているし。

なんだ、これって DataSet が進化して LINQ が使えるようになったんじゃん、と喜ぶの早いところで、実は ad.GetData() のところでテーブルの内容をごっそりもってきて単純に List 同士で join しているだけなんですね。なので、button3_Click メソッド内に書いてあるように、2つのテーブルをそれぞれ TableAdapter を使って GetData で全検索。そして、List 化された tproduct と tstore を join しているだけなので、メモリは膨大になります。まあ、今の PC だと 1万件位ならば大丈夫でしょうが、100万件位になると PC は固まるだろうし、そもそもネットワーク負荷が膨大。

素直に EF を使う

なので、素直に「ADO.NET Entity Data Model」を使おうって話です。
書き替えたのがこれ。

private void button1_Click(object sender, EventArgs e)
{
    var ent = new testdbEntities();
    var q = from t in ent.Product select t.Name;
    listBox1.DataSource = q.ToList();
}

private void button2_Click(object sender, EventArgs e)
{
    var ent = new testdbEntities();
    ent.Database.Log = sql => { System.Diagnostics.Debug.WriteLine(sql); };
    var q = from t in ent.Product where t.Id == 2  select t.Name;
    listBox1.DataSource = q.ToList();
}

private void button3_Click(object sender, EventArgs e)
{
    var ent = new testdbEntities();
    ent.Database.Log = sql => { System.Diagnostics.Debug.WriteLine(sql); };
    var q = from p in ent.Product
            join s in ent.Store on p.Id equals s.ProductId
            select $"{p.Name}({s.Description}) {s.Stored}個"; // ★
    listBox1.DataSource = q.ToList();
}

ent.Database.Log なところは、遅延実行される SQL をデバッグ出力しているところです。先の DataSet には、この Log なるところがない(そもそも List の結合なので SQL 文を発行してるわけではない)ところで区別がつきます。

あと、試してみると解りますが、★の部分は動きません。実行エラーになります。DataSet のときは、List の結合で C# の範囲内で動くから ToString 等の文字列関係のものは動くのですが、EF の場合は SQL を発行する(SQL Server側で動く)ので ToString 等の関数がないので「実行エラー」になります。こんなところでも、LINQ がどっちで動いているかどうかわかります。

でもって、DataSetはどうするのか?

よく覚えていないのですが、確か DataSet の型付きDataTableの生成の仕組みが変わって、System.Data.TypedTableBase を継承するようになったからですよね。

DataRowCollection に LINQ 機能を付与する ( ソフトウェア ) – 憂国なプログラマ – Yahoo!ブログ
https://blogs.yahoo.co.jp/hilapon/7600156.html

DataSet / DataTableに対してLINQを使う方法 – @kotyのブログ
http://koty.hatenablog.com/entry/20110524/1306249991

2009年頃の記事を見ると DataRowCollection は IEnumerable を継承していないので LINQ が動きませんとなっていますが、現時点での生成コードを見ると Rows のほうも IEnumerable を継承するように書き変わっています。

つまり「ひそかに DataSet は進化している」訳です。まあ、それでも EF のほうを使う訳ですが。

なので、データベースに直接アクセスしてデータを引っ張ってくる場合は EF を使った遅延実行が必須なのですが、アプリケーション内で扱うマスターデータとか定義値を羅列した enum タイプのデータを join する場合は、DataTable を使うという方法もアリですね。
よくやる、初回だけデータベースから拾ってきて combobox とか listbox とかデータバインドするというアレです。まあ、EF でも同じことはできるので、新規の場合は意味はないのですが、既存の DataTable コードを EF に直さなくても、自前キャッシュを作り直して LINQ を導入してみるのもありかな、と思った次第です。

カテゴリー: 開発, C# | DataSetはLINQの夢を半分だけ見られるか? はコメントを受け付けていません

年末なので ROS を調べてちらほらと

ざっと、調べていったことを感想をメモしておきます。

参考文献

読んでいる書籍は以下の通り。

実はチュートリアルも含めて、公式の ros.org にあったりするのですが、書籍/Kindle でブーストしています。邦訳オライリー本は、英語オライリー本が元ネタになるので、実は中身はちょっと古めです。ですが、実はそれで十分だったりします。訳は後述します。

ROS.org | Powering the world’s robots

OpenRTM と ROS

実は、ロボット制御の方法として一方で OpenRTM-aist | The power to connect があります。OpenRTM のほうは国産でなので、日本でロボットをやっていく分には OpenRTM のほうが良いのでは?と思っていた(個人的に)時期もあるのですが、自分はいまのところ ROS な方向で勉強中。そういえば、OpenRTM は Open と名はついているけどオープンではなかったものですが、今ざっとみるとオープンソースな感じになったようです。

Ubuntu と ROS

なんで、ROS は Linux 上で(特に Ubuntu 上で)を想定しいるんだろうか?と思っていたのですが理由がわかりました。

  • ROS の安定パッケージを作りやすくする
  • Ubuntu の LTS (Long Time Support) と時期を合わせている。

2年ほど前 PLAN2 を購入しようと決めたときに、ROS 対応の Intel Edison 版というのがありました。そのときは漠然と ROS + Linux のイメージしかなかったのですが、Edison をつかうことにより ROS パッケージをそのまま使うことができるというメリットがあったのです。実は ROS には Arduino にシリアル接続するパッケージもあるのですが、プログラムの作りやすさ(配布しやすさ?)としては Linux 上のプロセスとして統一したほうが開発効率がよい、というところでしょう。

ただし、現実には Edison 自体がディスコンになってしまったので、PLEN2 + ROS 対応というのはサポート的に難しくなってしまったということろです。

ROS はひとつのロボットを扱う

複数のプロセスが協調動作をする(実際は master がいるのですが)ROS では、ひとつのプロセスがひとつの制御機器を扱う(複数扱えるけど、ひとつに対応させたほうがよさそう)ので、複数のプロセスで「ひとつのロボットを動かす」というのが前提になっています。じゃあ、協調動作するロボット(双腕のロボットとか、複数台が協調するロボットアームとか)はどうするのかというと、まあ、そのまま ROS で作ってもよいし、次のバージョンである ROS2 で作るのがよいというところなんでしょう。ROS2 自体はまで調べていないので言及はしませんが、

  • 組み込み機器に対応している(現状のROSでは、Linux機器に限られる)
  • 各プロセスが直接相互に通信する(現状は master を通している)

というスタイルになるそうです。私としては、まずひとつのロボットを動かせる位に ROS をやってからと思っているので、もうちょっとスタートは前から

ROS Indigo で十分かも

Distributions – ROS Wiki を見ると、ROS は毎年1回定期的にバージョンアップされています。おそらく、Ubuntu のバージョンアップと合わせる形でもあるのでしょうし、ロボットのようなハードウェアを扱う場合、長期的に安定して制御装置が動く必要があるので「あまり頻繁なミドルウェアの更新は望ましくない」としたのでしょう。

そんな中で、現在の最新の ROS は、Lunar というものなのですが、実は 2014年にリリースされた Indigo とサポート期間が同じになっています。

ROS の場合、Ubuntu の apt-get で様々なパッケージを入れることになりますが、それぞれの ROS ライブラリが Linux のライブラリに依存していいます。それらの依存状況によって、毎年の ROS の更新に対して ROS パッケージが更新されていたりされていなかったりするんですよね。このあたりは、Linux の各種ライブラリのバージョンに ROS パッケージ + ROS そのものが依存するという制限のためです。

この理由から、一番 ROS が盛んだった(っぽい)ROS Indigo のパッケージ群 ROS packages が一番充実しています。おそらく、この時期から ROS2 の開発が進みだしたしたので、それ以降のパッケージの更新に手が回っていないのではないか?と想像しています。

最初、最新版がいいだろうと思って ROS Lunar でやろうとしていたのですが、あとで ROS Indigo を入れて試しています。ただし、Indigo を使おうとすると、当時にリリースしていた Ubuntu のバージョンに合わせないと動かないところが多いので、そのあたりは痛し痒し。

ロボットを使わない ROS の動作環境

ロボットや各種センサーを扱うための ROS ですが、ROS 自体は主にプロセス間通信を扱うのでセンサー無しでも利用できます。

なので、ROS を試すだけなれば、

  • VMWare 上に Ubuntu を入れて ROS を導入する
  • Ubuntu on Windows を入れて ROS を導入する
  • ラズパイの Ubuntu を使い ROS を導入する

ということが可能です。これは、Windows 10 の Ubuntu on Windows(WSL/ Windows Subsystem for Linux)で ROS を入れた例ですが、きちんと ROS の roscore/talker/listener のプログラムが動いてます。

image

ROS Lunar のインストール手順は、ひたすら lunar/Installation/Ubuntu – ROS Wiki を追っていくだけです。ペタペタとコピペしていけば、時間はかかかりますが確実に ROS がインストールできます。

超簡単な ROS の仕組み

要は、出版-購読型モデル です。Publish/Subscribe パターンが分かっていれば、ROS の各プロセスの動きに納得がいくし、新しくパッケージを作るのもやりやすいでしょう。

image

要は、プロセス間通信をするときに、Publisher(送り手)とSubscriber(受け手)が直接会話をしません。いったん、トピックという形式にに直して中央の master(ROS では roscore )を通します。これ、いちいち master を通すのが面倒という話もありますが、利点として、

  • publisher は、受け手の生き死に関わらず topic を送れる(master に送るので)
  • subscriber は、送り手の生き死ににかかわらず topic を受け取れる(master がキープしているので)

という利点があります。いわゆる amazon の在庫や流通の倉庫みたいな役割を持たせるのが pub/sub パターンです。これで、何が便利/嬉しいかというと(これ、英語では Are you happy ? … これで良いか?と聞かれるので、Happy = 嬉しい/幸せの直訳ですよ)、

  • 高速なセンサーの類は、ひとまず高速に master にデータをアップロードできる
  • 定期的なデータ抽出は、センサーの状態にかかわらず Subscriber 役の PC などから取り出せる

ということです。いわゆる、クラウドの IoT Hub の仕組みです。というかクラウドの IoT Hub がこれですよね。

そんな訳で、センサーやカメラ機器、場合によっては Android のようなものが Publisher として作ることができます。それを抽出する側の PC だったり Web サーバーだったりスマホだったりするのが Subscriber として作ればよいのです。もちろん、両方の機能を持たせてコンバーターとして働くプロセスを作る事も可能です(実際、そういうパッケージがある)。

データは、Topic で引き渡します。ROS の場合は、あらかじめ msg ファイルを作ることになり、大方のデータ形式は用意されています。ここの Topic で扱う構造体は、オライリーの ROS 本では「標準のものを推奨」していますが、自前で構築する場合は、BLE の GATT よろしくバンバン作ってしまったほうがメッセージのやり取りをするのが便利ではないか、と思っています。まあ、それだと既存の機器につながらなくなるパターンに陥るのですが、要所要所にデータコンバートをする Pub/Sub なプロセスを挟み込めばいいのです(多少、通信効率は落ちるけど)。

 

プログラム言語は Python と C++

ROS の各種サンプルは Python で書かれはいますが、C++ も使えます。というか C++ を使ったほうが便利ですよ、きっとたぶん。

ちょっとした操作ならば Python でもいいんですが、あれこれと各種ライブラリを使ってコンバートしたり既存の科学計算を利用しようとすると、そのままライブラリを組み込める C++ のほうが便利です。とは言いつつ、OpenCV も Python から扱えるし(ROS 自体にも OpenCV を扱うパッケージもあるし)、Python で突き進んでもいいかもしれませんね。

まあ、私の場合、C++ のほうがやりやすいので。

本格的な ROS の Python コードは長かったりするのですが、基本は Pub/Sub パターンで作られた ROS ライブラリを使うことになるので、ひとつのプログラムは 100 行程度で書ける感じです(まだ本格的に作ってはいないので)。たとえば、画像を加工する部分のコードは、ROS であってもなくても関係ないわけで、自前ライブラリとして TDD を使って書いておきます。それを Pub/Sub のパターンに乗せるために、100 行位追加すればよいので、センサーの類を使って通信するプログラムの場合は ROS を使うと開発効率がいいのでは?と思っています。

 

そんな訳で、まだ本格的に ROS を使っている訳ではないのですが、あれこれの情報を見てざっとインストールしたところまでの感想です。どうやら、ラズパイの Ubuntu を入れれば ROS のインストールもさっくりと済むらしいので、

  • Ubuntu on Raspberry Pi
  • タミヤのギアボックス
  • Arduino でモーターと距離センサーの制御
  • USB カメラ から入力して PC で閲覧

までを作ってみようかなと。そうそう、同時にデルタ式の 3D プリンタを組み立ててロボットアームに仕立て上げるのも(1年越しになってるし)。

カテゴリー: ROS | 年末なので ROS を調べてちらほらと はコメントを受け付けていません

LattePanda と Siv3D(仮)

このエントリーは Siv3D Advent Calendar 2017 – Qiita の 12 日目のエントリーです。前のエントリーは、para7 さんの OpenSiv3Dのトランプ描画機能(PlayingCard) です。

LattePanda とは

唐突ですが、LattePanda という組み込みボードがあります。組み込みボードというとRaspberry Pi が有名だったり、もっと小さくやろうと思うと Arduino が良かったりというのもあるのですが、Raspberry Pi は OS が Rasbian という Linux であったり、Arduino の場合はそもそも OS が無かったりという特徴があります。じゃあ、LattePanda はなんじゃ?というと、

  • Arduino のような GPIO ピンを持っていて
  • Windows 10 が動く

という特徴を持つボードです。

image

まあ、さっそく種明かしをしてしまえば、Windows 10 が動くボードと Arduino が I2C でくっついているだけのボードなので、普通のノート PC と Arduino を USB ケーブルで接続してしまえば同じことができます。中古なノートPCを買って、格安な Arduino を買えばモニタ付きで(当然 LattePanda にはモニタがありません)組み込みな環境が整えられるので、じゃあ LattePanda を使う意味があるのか?という問題は多々あるのですが、

  • 大きさがラズパイ並みに小さい(ラズパイよりも一回り大きい程度)
  • 電源が USB micro で 5V2A 程度で動く
  • モニタが HDMI で繋げる
  • フルな Windows 10 が動く

大きさがラズパイ並みということはモニタを必要としない場合には、展示品の脇において制御するときなど場所が小さくて済みます。電源もノートPCのような特殊なアダプタを必要とせずスマートフォンの充電器で十分です。ちなみに、スマホのモバイルバッテリで動かすこともできます(もちろん、バッテリー切れになると OS ごと落ちるので、かなり難点ですが)。

また、HDMI コネクタがついているので、大抵のモニタならば行けます。秋ごろに学研ワールドアイという球型モニタで試してみましたが十分いけました。

そして、なんといってもフルで Windows 10 が動くということでしょう。ラズパイ+Windows IoT Core という組み合わせもあるのですが(私としては、それはそれで使いようがあるのだけれど)、この小ささでフルの Windows 10 が動くのであれば動作するアプリを UWP アプリにしなくても、Windows アプリのままいけるということです。

価格としては秋月 http://akizukidenshi.com/catalog/g/gM-12549/ で 14k 位、直で DFROBOT https://www.dfrobot.com/product-1405.html から買うと $120 位なのであまり変わりません。私が買ったときは、直で買うしかなかったので郵送料が掛かりましたがあまり値段は変わっていないハズです。

ということは Siv3D が動くということだな

フルで Windows 10 が動くということは DirectX とかなんか適当な GPU 絡みのなんとかも動くはずです。ためしに、OpenSiv3D のサンプルコードをダウンロードを動かしてみたのが、これです。

image

球型モニタを使っているので走馬燈のような不思議なものができますね。まあ、走馬燈のほうが相当安いんですが。それは言わない約束で。

という訳で、こういう小型のボードと OpenSiv3D あるいは Siv3D の組み合わせると、ユーザーインターフェース記述が手軽な Siv3D とボードの小ささ(裏に隠れるという意味で)の相乗効果で何か新しい道が開けるのではないか?(新しくなくてもいいけど)と思った次第なのです。

 

じゃあ、具体的に何か?ってのをプログラマならばコードを書かねば、と思ってたところなのですがね、ちょっと先週の土曜日に子供からインフルエンザを移されたらしく沈没しておりまして、現在も沈没中。でもって、寝ながら考えていたネタが、hota1024 さんの Siv3Dで15パズルを極限までハイクオリティーにしてみる にネタ被りしてしまったという。要はアズレンの16パズルを16パズルにするというネタだったのですが…どうしたものか。

そんな訳で、形だけでもバトン的に AkiraKoizumi さんの「Siv3Dでグラデーション文字を作る」まで繋げておきます。完治したら、ここの記事を増やしておきますので、しばしお待ちを、では。

追記 2017/12/30

帰省中に、えいっと25駒のスライドパズルを作りました。LattePanda を持ってくるのを忘れてしまったので、LattePanda で動かす場合には、ってのは後から書きますが。ひとまず動作するコードだけをざっと。

# include <Siv3D.hpp> // OpenSiv3D v0.1.7

// 画像を指定位置でクリップする
s3d::Image &clip(const s3d::Image &src, s3d::Image &dest, int l, int t, int w, int h)
{
	for (int y = 0; y < h; y++ ) {
		for (int x = 0; x < w; x++) {
			dest[y][x] = src[t + y][l + x];
		}
	}
	return dest;
}

/// スライドパズルのボードクラス
class Board {

	int xmax;
	int ymax;
	int cellWidth, cellHeight;
	std::vector<Texture*> *_cells;
	std::vector<Texture*> *_goal;
	Image *_image;
	Texture *_textrueOriginal;
	Texture *_blank;

public:
	/// ボードの初期化
	Board(int width, int height) {
		xmax = width;
		ymax = height;
		_cells = new std::vector<Texture*>(xmax * ymax);
	}
public:
	/// 画像を設定する
	void setImage(const wchar_t *path) {
		_image = new Image(FilePath(String(path)));
		_textrueOriginal = new Texture(*_image);

		cellWidth = _image->width() / xmax;
		cellHeight = _image->height() / ymax;
		Image imageTemp(cellWidth, cellHeight);
		for (int y = 0; y < ymax; y++) {
			for (int x = 0; x < xmax; x++) {
				clip(*_image, imageTemp, x * cellWidth, y * cellHeight, cellWidth, cellHeight);
				_cells->at(x + y * xmax) = new Texture(imageTemp);
			}
		}
		// 25枚目だけ黒で塗りつぶし
		const Image imageBlank(cellWidth, cellHeight, Color(100, 100, 100));
		_blank = _cells->at(xmax*ymax -1) = new Texture(imageBlank);

		// 最初の配置を覚えておく
		_goal = new std::vector<Texture*>();
		for (auto it : *_cells) {
			_goal->push_back(it);
		}
	}

	/// シャッフル
	void shuffle()
	{
		std::random_device seed_gen;
		std::mt19937 engine(seed_gen());
		std::shuffle(_cells->begin(), _cells->end(), engine);
	}

	/// 完成かどうかをチェックする
	bool IsGoal() {
		bool same = true;
		for (int i = 0; _cells->size(); i++) {
			if (_cells->at(i) != _goal->at(i)) {
				same = false;
				break;
			}
		}
		return same;
	}

	// ブランクセルの位置を取得
	s3d::Point GetBlank() {
		for (int y = 0; y < ymax; y++) {
			for (int x = 0; x < xmax; x++) {
				if (_cells->at(x + y * xmax) == _blank) {
					return s3d::Point(x, y);
				}
			}
		}
		// 本来はここには来ない
		return s3d::Point(0, 0);
	}

	// ブランクのセルを動かす
	void MoveTo(int mx, int my)
	{
		s3d::Point pt = GetBlank();
		s3d::Point pt2 = s3d::Point(pt.x + mx, pt.y + my);
		// 移動先がはみ出ていないこと
		if (pt2.x < 0 || pt2.x >= xmax) return;
		if (pt2.y < 0 || pt2.y >= ymax) return;

		// セルを移動させる
		_cells->at(pt.x + pt.y * xmax) = _cells->at(pt2.x + pt2.y * xmax);
		_cells->at(pt2.x + pt2.y * xmax ) = _blank;
	}

	/// キー操作を処理する
	void InputKey() {
		if (s3d::KeyUp.down()) this->MoveTo(    0,-1);
		if (s3d::KeyDown.down()) this->MoveTo(  0,+1);
		if (s3d::KeyLeft.down()) this->MoveTo( -1, 0);
		if (s3d::KeyRight.down()) this->MoveTo(+1, 0);
	}
	/// マウス操作を処理する
	void InputMouse() {
		if (s3d::MouseL.down()) {
			auto m = Cursor::Pos();
			auto b = s3d::Point( GetBlank().x * cellWidth, GetBlank().y * cellHeight );
			if (b.y > m.y  && ( b.x <= m.x && m.x <= b.x + cellWidth ))			 
				this->MoveTo(0, -1);
			if (b.y + cellHeight < m.y && (b.x <= m.x && m.x <= b.x + cellWidth))
				this->MoveTo(0, +1);
			if (b.x > m.x && (b.y <= m.y && m.y <= b.y + cellHeight)) 
				this->MoveTo(-1, 0);
			if (b.x + cellWidth < m.x && (b.y <= m.y && m.y <= b.y + cellHeight)) 
				this->MoveTo(1, 0);
		}
	}
	/// 現在のボードを描画する
	void Draw() {
		for (int y = 0; y < ymax; y++) {
			for (int x = 0; x < xmax; x++) {
				_cells->at(x + y * xmax)->draw(x * cellWidth, y * cellHeight);
			}
		}
	}
	/// 現在のボードを描画する
	void GoalDraw() {
		_textrueOriginal->draw();
	}
};

void Main()
{
	Graphics::SetBackground(ColorF(0.8, 0.9, 1.0));
	const Font font(50);

	// ボードの作成
	Board board(5, 5);
	// ボードに画像を読み込む
#define IMAGE L"C:\\Users\\masuda\\Pictures\\IMG_0139.jpg"
#define IMAGE L"C:\\Users\\masuda\\Pictures\\azurlane001.jpg"
	board.setImage(IMAGE);
	// 配列をシャッフル
	board.shuffle();

	while (System::Update())
	{
		// すべて揃っているか?
		if ( board.IsGoal() ) {
			//  揃っていれば、ゲーム終了
			board.GoalDraw();
			font(L"GOAL, Siv3D!").drawAt(Window::Center(), Palette::Black);
			continue;
		} 

		// キー入力
		board.InputKey();
		// マウス入力
		board.InputMouse();
		// 配列に従って、画像を表示
		board.Draw();

		font(Cursor::Pos()).draw(20, 400, ColorF(0.6));
		Circle(Cursor::Pos(), 30).draw(ColorF(1, 0, 0, 0.5));
	}
}

コード自体は、200行程度です。あまりにも雑に書きすぎてしまったので、クラスはボードクラス(Board)しかありませんが、まあ、ほどよく動作します。中で、std::shuffle してしまっているので本当にスライドゲームが完成するのかは不明…というひどいゲームですが。

実は、OpenSiv3D をきちんと書くのは(たった200行ですが)、これが初めてで、12/30の午前中に初めて、途中で買い物に行って夕方にクラス化するというパターンで、調べ調べしながら4時間ほどで作っています。思うに、この手のゲームのようなものは自分の思ったように作ることが重要で、あれこれなプログラミングなテクニックは後から追加していったほうが良いのでは?と思っています。仕事自体はシステム屋さんなところが多いので、設計してからコーディング(とはいえ、紙に設計するのではなく疑似コードレベルですが)というパターンが多く、数時間で思ったようなものを作るという機会は仕事ではなかなかありません。OpenSiv3D はそういうところで、手軽に使える「道具立て」ではないかな、と思っています。

プログラムは実行するとこんな感じです。

完成すれば、こんな感じになるはず。

C++ なのにメモリを解放していないという悪さ(苦笑)はありますが、そのあたりきちんとデストラクタを作って処理するか、const 使いつつスコープ内での自動解放な機能を使えばもう少し安全なコードになるはずです。できるだけ表にポインタが出ないのが Siv3D の良いところではないか、というのにポインタを使いまくっているのがアレなのですが、main 関数だけ見れば、なんかすっきりしているという具合でしょうか。そのあたりは、C++ なオブジェクト指向のよいところ。

 

カテゴリー: 開発, C++ | LattePanda と Siv3D(仮) はコメントを受け付けていません