20200218のC#に関する記事は8件です。

VB6マイグレーション でわかったこと

①Trimとかの関数が何故、関数名だけかくことが上手く行かないのか

https://www.atmarkit.co.jp/ait/articles/1606/15/news028.html

②.net バージョンは違いが起きる

https://docs.microsoft.com/ja-jp/archive/blogs/jpvsblog/net-framework-3

③イベント記述の違い

https://www.chuken-engineer.com/entry/2019/08/15/121828

④My 用法

http://rucio.a.la9.jp/main/dotnet/shokyu/standard37.htm

⑤ 諸々を書いてくれて、ありがとう

https://t-ashula.hateblo.jp/entry/2011/12/06/210804

⑥コントロール配列の解決方法
https://www.itlab51.com/?p=2236

⑦ icsharpcode は SharpDevelopより進化している可能性もある。
https://github.com/icsharpcode/CodeConverter
VSへのアドインもあるようだ。

⑧ VB C# 仕様変化

https://elleneast.com/?p=10679

⑨VB レガシーを封じる
https://earlgreytea.hatenablog.com/entry/2016/05/10/073046

⑩vs2008 と sharpdevelop のエラーの原因は
Option Stirct On
https://docs.microsoft.com/ja-jp/dotnet/visual-basic/language-reference/statements/option-strict-statement

型変換が実行時行われるのは、C#erには当然ありえないのだが、VB6などは存在した。
それが両者の変換エラーにつながったのではないか。

VBにはこういう弱点があるので、やはりC#かと思うけど、喰わず嫌いみたいにあまり知らないのにケチつけたくない。
https://www.atmarkit.co.jp/fdotnet/vb6tonet2/vbnet2_13/vbnet2_13_01.html

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

ASP.NET Coreに NLogを組み込む

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

NuGet使って以下の最新パッケージをインストールします。

NLog.Web.AspNetCore 
NLog 

csprojファイルには、以下のような記述が追加されているはずです。

<PackageReference Include="NLog.Web.AspNetCore" Version="4.9.0" />
<PackageReference Include="NLog" Version="4.6.8" />

nlog.config ファイルの作成

プロジェクトの直下に、nlog.config ファイルを新規追加します。

nlog.config ファイルの例を示します。

<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      autoReload="true"
      internalLogLevel="Info"
      internalLogFile="internal-nlog.txt">
  <extensions>
    <add assembly="NLog.Web.AspNetCore" />
  </extensions>
  <targets async="true">
    <target xsi:type="File" name="debuglog" 
            fileName="${aspnet-appbasepath}/logs/debug-${shortdate}.log" 
            layout="${longdate}|${event-properties:item=EventId_Id}|${uppercase:${level}}|${logger}|${message}|url: ${aspnet-request-url:IncludeQueryString=true}" />
    <target xsi:type="File" name="infolog"
            fileName="${aspnet-appbasepath}/logs/info-${shortdate}.log"
            layout="${longdate}|${event-properties:item=EventId_Id}|${uppercase:${level}}|${logger}|${message} ${exception:format=tostring}|url: ${aspnet-request-url:IncludeQueryString=true}" />
    <target xsi:type="File" name="errorlog"
            fileName="${aspnet-appbasepath}/logs/error-${shortdate}.log"
            layout="${longdate}|${event-properties:item=EventId_Id}|${uppercase:${level}}|${logger}|${message} ${exception:format=tostring}|user: ${aspnet-user-identity} |url: ${aspnet-request-url:IncludeQueryString=true}|action: ${aspnet-request-form}" />
  </targets>
  <rules>
    <logger name="*" maxlevel="Debug" writeTo="debuglog" />
    <logger name="Microsoft.*" maxLevel="Info" final="true" />
    <logger name="*" levels="Error,Fatal,Warn" writeTo="errorlog" />
    <logger name="*" minlevel="Info" writeTo="infolog" />
  </rules>
</nlog>

nlob.config のプロパティ設定

Visual Studioで、nlog.config のプロパティを開き、

  • 「ビルドアクション」を "コンテンツ"、
  • 「出力ディレクトリにコピー」を "新しい場合はコピーする"

に設定します。

Program.cs の編集

Program.cs を以下のように変更します。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.DependencyInjection;
using NLog.Web;

namespace MyApp
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var logger = NLogBuilder.ConfigureNLog("nlog.config").GetCurrentClassLogger();
            try {
                logger.Debug("init main");
                var host = CreateHostBuilder(args).Build();
                using (var scope = host.Services.CreateScope()) {
                    ... // 必要ならここになにかを記述
                }
                host.Run();
            }
            catch (Exception ex) {
                logger.Error(ex, "Stopped program because of exception");
                throw;
            } finally {
                NLog.LogManager.Shutdown();
            }
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>  {
                    webBuilder.UseStartup<Startup>();
                })
                .ConfigureLogging(logging => {
                    logging.ClearProviders();
                    logging.SetMinimumLevel(LogLevel.Trace);
                })
                .UseNLog();
    }
}

appsettings.json を編集

"Logging" に対する設定を以下のように変更します。

  "Logging": {
    "IncludeScopes": false,
    "LogLevel": {
      "Default": "Trace",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  ...

LogLevelは、必要に応じて変更します。

LogLevelは、

Trace、Debug、Information、Warning、Error、Critical

の6種類があります。

ログを出力する

例えば、HomeController の Index Action メソッドの中で、ログ出力する場合は、以下のようなコードを記述します。

using Microsoft.Extensions.Logging;

public class HomeController : Controller
{
    private readonly ILogger<HomeController> _logger;

    public HomeController(ILogger<HomeController> logger)
    {
        _logger = logger;
    }

    public IActionResult Index()
    {
        _logger.LogInformation("Enter HomeController.Index Method");
        return View();
    }

実行例

プロジェクトのあるフォルダに logs フォルダが作成され、2つのファイルが作成されます。

info-2020-02-16.log

2020-02-16 15:29:30.3013||INFO|RazorPagesMovie.Pages.IndexModel|HomeController.Index method called!!! |url: https://localhost/

debug-2020-02-16.log

2020-02-16 15:29:26.2826||DEBUG|RazorPagesMovie.Program|init main|url: 
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Azure Web AppsからDBへの接続情報の設定

はじめに

WebアプリケーションでDBへの接続を行うとき、アプリケーションはどこかにDBへの接続情報を持つ必要がある。

一番シンプルな解決方法は、アプリケーション内に接続文字列として情報を埋め込むパターンだが、この方法だと接続先を変更するためにアプリケーションの再ビルドが必要となってしまい、保守の手間が大きくなる問題点がある。

二つ目の方法は、アプリケーションの構成ファイル(web.configなど)に接続情報を記述する方法で、この方法だと構成ファイルの文字列を変更してあげれば接続先の変更は可能で、アプリケーションの再ビルドは不要である。ただしこの場合別の問題が発生する。SQL ServerなどのDBでは接続文字列にIDやパスワードを記述するため、ファイルにアクセスされると機密情報が洩れてしまうセキュリティ上の問題が発生する。

アプリケーションの配置先が社内の専用サーバーであり、管理者のみがログインしてアプリのデプロイ・構成ファイルの修正を行うのであればアクセスできるユーザーが限られているという点で安全性を確保できているといえそうだが、Azure上にアプリケーションをGitデプロイする場合などは設定が書かれた構成ファイル自身もGit上に配置しないといけないため、問題となる場合が多いことが予想される。

Gitは通常開発者にもアクセス権限が付与されるが、一般に管理者でないユーザー(体制によっては部外者)に接続情報が見える環境というのは危険である。

では、接続情報はGit管理しないという方針が考えられるが、Git上デプロイでCI/CDの環境を構築しているのに構成ファイルだけは別途修正が必要というのも受け入れがたいのではないだろうか。

Azureではこの問題を解決するため、従来web.configに記述可能であったconnectionStringsセクションの情報をApp Serviceの構成情報としてAzure上で管理できる機能を持っている。

以下では、2020年2月時点でのApp Serviceでの接続文字列情報の設定方法と、アプリケーション側での値の取得について記載する。

App Serviceにおける接続文字列の設定

設定に用いる画面は、下記のメニューから行けるApp Serviceの構成画面で行う。
image.png
image.png
構成画面の下の方にある「接続文字列」の設定で、新しい接続文字列リンクを押して、設定画面を開く。
ここで「名前」はアプリケーションから接続文字列を取得する際のキーとなるので、App Service内で一意とする必要がある。

値に関しては、接続文字列をそのまま記述すればよいのだが、SQL Databaseの接続文字列画面からコピペしてくると当然パスワードの部分が書かれていないため、自分で設定してあげる必要がある。

種類のところは接続対象となるDBの種類を指定する。

接続文字列はApp Serviceに対して設定しているが、App ServiceはIISにおけるアプリケーションプールに対応していると考えられるため、App Serviceに登録した複数のアプリケーションからこの接続情報は利用することが可能である。

この点はアプリケーションごとに配置されるweb.conifgに接続情報を記述するのとは若干性質が異なるが、再起動のタイミングなどが一致する複数のアプリケーションが接続文字列を共有することはそこまで不自然な設定ではないだろう。

なお、ここで設定した値はweb.configに反映されることはなくあくまでアプリケーションから利用可能となっただけである。ファイルに接続情報が書かれないという点がこのアプローチがセキュアな理由である。

アプリケーションからの接続文字列の利用

アプリケーションから接続文字列を利用するのは、web.configにconnectionStringsセクションを記述した場合と全く同じである。以下に簡単なサンプルコードを載せる。

VS2019でASP.NET Webアプリケーションプロジェクトを新規作成すると、自動でサンプルのアプリケーションが作成される。このアプリケーションのdefault.aspxに設定した接続文字列を表示するコードを追加する。

default.aspx
<%@ Page Title="Home Page" Language="C#" MasterPageFile="~/Site.Master" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="GetConnectionStringSample._Default" %>

<asp:Content ID="BodyContent" ContentPlaceHolderID="MainContent" runat="server">

    <div class="jumbotron">
        <h1>ASP.NET</h1>
        <p class="lead">ASP.NET is a free web framework for building great Web sites and Web applications using HTML, CSS, and JavaScript.</p>
        <p><a href="http://www.asp.net" class="btn btn-primary btn-lg">Learn more &raquo;</a></p>
    </div>

    <div class="row">
        <div class="col-md-4">
            <h2>Getting started</h2>
            <p>
                ASP.NET Web Forms lets you build dynamic websites using a familiar drag-and-drop, event-driven model.
            A design surface and hundreds of controls and components let you rapidly build sophisticated, powerful UI-driven sites with data access.
            </p>
            <p>
                <a class="btn btn-default" href="https://go.microsoft.com/fwlink/?LinkId=301948">Learn more &raquo;</a>
            </p>
        </div>
        <div class="col-md-4">
            <h2>Get more libraries</h2>
            <p>
                NuGet is a free Visual Studio extension that makes it easy to add, remove, and update libraries and tools in Visual Studio projects.
            </p>
            <p>
                <a class="btn btn-default" href="https://go.microsoft.com/fwlink/?LinkId=301949">Learn more &raquo;</a>
            </p>
        </div>
        <div class="col-md-4">
            <h2>Web Hosting</h2>
            <p>
                You can easily find a web hosting company that offers the right mix of features and price for your applications.
            </p>
            <p>
                <a class="btn btn-default" href="https://go.microsoft.com/fwlink/?LinkId=301950">Learn more &raquo;</a>
            </p>
        </div>
        <!-- ここから -->
        <div class="col-md-4">
            <h2>ConnectionString</h2>
            <p>
                接続文字列
            </p>
            <p>
                <asp:Label ID="Label1" runat="server" Text="Label"></asp:Label>
            </p>
        </div>
        <!-- ここまで追記 -->
    </div>
</asp:Content>

コードビハインドには、Labelに対して接続文字列をセットするコードを記述する。

default.aspx.cs
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace GetConnectionStringSample
{
    public partial class _Default : Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            var csSection = ConfigurationManager.ConnectionStrings["SqlDbConnection"];
            if(csSection != null)
            {
                Label1.Text = csSection.ConnectionString;
            }
            else
            {
                Label1.Text = "接続文字列が定義されていません";
            }
        }
    }
}

ここで、ConfigurationManager.ConnectionStrings["SqlDbConnection"]の「SqlDbConnection」の部分がApp Serviceの接続文字列で定義した「名前」の部分である。

このアプリケーションをAzure上にデプロイし、App Serviceで「SqlDbConnection」という接続文字列を定義すれば、設定した接続文字列が画面上に表示されるはずである。
image.png

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

[Shopify] の Multipass APIを理解する

本日はShopifyが提供するMultipass APIを理解するため情報をまとめました。

すでに会員管理システムをもつオーナーさんからするとShopoify用(自社EC用)のIDとパスワードを別にもつことはユーザー体験的にもいけてないし管理も煩雑になってしまいます。

これを解決してくれるのがMultipass APIです。 概念図はこんな感じです。

Multipass 前

image.png

Multipass 後

image.png

では、具体的にどのようによう活用するか見ていきましょう。

初期設定

チェックアウトページへ移動します
/admin/settings/checkout

image.png

ここで、「アカウントを任意とする」または「アカウントを必要とする」を選択します。

Shopify Plusを契約しているアカウント限定ですが、下記のように Multipassを有効かするボタンがあります。ここでシークレットコードを取得します。

ユーザー情報(JSON)の準備

既存会員DBより下記のようなデータを用意します。

  {
    email: "bob@shopify.com",
    created_at: "2013-04-11T15:16:23-04:00",
  }

created_at にはこのjsonデータを生成した時間をいれます。

ここで生成できるデータタイプはこちらを参照ください。
https://shopify.dev/docs/admin-api/rest/reference/plus/user

メールアドレスをキーとしない場合はidentifierは必須となりますので指定してください。

JSONの暗号化、署名、Base64化

本家資料ではRubyとPHPでのサンプルがありますが、C#だとすでにライブラリーがあり、このあたりの処理をすべて自動で行ってくれます。

利用したライブラリーは下記です。
https://github.com/uoc1691/ShopifyMultipassTokenGenerator

下記が実際に使ったコードです

Controller

using System;
using System.IO;
using System.Security.Cryptography;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;

namespace WarehouseApplication.Controllers
{
    public class MpController : Controller
    {
        public IActionResult Index()
        {
            return View();
        }

        /// <summary>
        /// The customer information is represented as a hash which must contain at least the email address of the customer 
        /// and a current timestamp (in ISO8601 encoding). 
        /// </summary>
        public class UserInformation
        {
            public string email { get; set; }
            public string first_name { get; set; }
            public string last_name { get; set; }
            public string created_at { get; set; }

            public UserInformation()
            {
                this.created_at = DateTime.Now.ToString("yyyy-MM-ddTHH\\:mm\\:sszzz");
            }
        }
        public IActionResult CreateToken(string email, string firstName, string LastName)
        {
            if (string.IsNullOrEmpty(email))
            {
                return View();
            }

            var user = new UserInformation()
            {
                email = email,
                first_name = firstName,
                last_name = LastName
            };

            // Serialize the customer data to JSON and encrypt it
            ViewData["Token"] = Hash256It(JsonConvert.SerializeObject(user));

            return View();
        }

        public string Hash256It(string userInformationJsonString)
        {
            // To generate a valid multipass login token, you need the secret given to you in your Shopify admin. 
            string shopifyMpKey = "YOUR SECRET KEY HERE";

            var sp = new ShopifyMultipassTokenGenerator.ShopifyMultipass(shopifyMpKey, "gyrocanopy-delivery-motor.myshopify.com");

            var token = sp.Process(userInformationJsonString);

            return token;
        }
    }
}

View

@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>CreateToken</title>
</head>
<body>

    <form action="/Mp/CreateToken" method="post">
        @Html.TextBox("email")
        @Html.TextBox("firstName")
        @Html.TextBox("lastName")
        <button class="" type="submit">
            Generate Token
        </button>
    </form>
    <div>
        @ViewData["Token"]
    </div>
</body>
</html>

Shopへリダイレクト

上記のコードを生成するとShopへのURLが表示されます。そのURLをブラウザー上にコピペすると動作を確認することができます。

下記のURLが生成されました

https://gyrocanopy-delivery-motor.myshopify.com/account/login/multipass/Iy9IuNpCaUBng3ZEYdKAEV7o4Q2zd6zrdl60zW9Xmizkbz6cshZ6aQVxPRJFRrXp76uY4C7WRPWpPjNq0K8BNQwRYajcxbjbY1svgqSN9nLdETsFqUJM6wVnwB30oJk-gKIrIOAtuev5Vqq0FfAUfi0hpSGRhJyyP3AX7ihQUTuGpeSbTWQvXAIRIkFHOnxjAtdN9Cgv1r3dgReMFpy1cIOVKuJ08nn4qFw_KZRnqQg=

このURLをブラザーにそのままコピペしてみます。そうすると、同じメールアドレスをもったユーザーのアカウントに遷移することができました。

image.png

参照

本家資料はこちらを参照ください。
https://shopify.dev/docs/admin-api/rest/reference/plus/multipass

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

Androidの戻るキー対応をなるべく簡単にする提案

意外と面倒くさいAndroid戻るキー対応

ね。面倒くさいですよね。(iOSには無いわけですし・・・)
さしあたり一番簡単なのは「画面上の『ネガティブ的』なボタンと同等の機能をつける」事です。

ダイアログを開いたのであれば、戻るキーが「閉じるボタン」や「戻るボタン」相当になればよいですし。
image.png

画面遷移したのであれば、戻るキーが「Backボタン」(前の画面に戻る)相当になればよいわけです。
image.png

そう書くと簡単のように聞こえますが、
画面遷移した次の画面で、ダイアログを表示した場合は?
image.png

戻るキーを押したら、ダイアログも閉じてしまう+画面も前の画面に戻ってしまう

では困るわけです。

これをまともに対応しようとすると、

  • 優先順位スタックマネージャ的なクラスを作成
  • 画面遷移したら、「戻るボタン」の処理(Actionとか?)を(上記)スタックマネージャにPush
  • ダイアログを開いたら「ダイアログ閉じるボタン」の処理(Actionとか?)をスタックマネージャにPush
  • 戻るキーを押したら、スタックマネージャにスタックされている処理の一番上(Peek)を処理
  • ダイアログを閉じたら「ダイアログ閉じるボタン」の処理をスタックマネージャからRemove
  • もう一度戻るキーを押したら・・・・

といった、管理が必要になります。 はい面倒臭いですね!

もっとシンプルに考える

そもそも、上記例の「画面遷移した次の画面で、ダイアログを表示した場合」って、普通はダイアログがモーダル的に表示されていて、後ろの「戻るボタン」は押せないようにしているのがほとんどのはず。(わざわざ後ろのボタンのintaractiveをfalseにしているのか、「タッチガード」的な全画面Panelを一枚噛ませてタッチイベントを遮断しているかのどちらかがほとんどでしょう)

問題なのは、

単純に戻るキーとボタンが押されたときの処理を関連付けてしまうと、uguiのイベントとは関係無しに処理が呼ばれてしまう

ことです。

なので、徹底的にuguiのイベントを倣い、戻るキーの押下を指定ボタンへのマウスクリックへとすり替えてあげれば解決です。

作ってみた

KeyBind.cs
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

[RequireComponent(typeof(Button))]
public class KeyBind : MonoBehaviour
{
    [SerializeField]
    private Button _targetButton;

    public KeyCode _bindKey;

    private void Reset()
    {
        _targetButton = GetComponent<Button>();
    }

    private static List<RaycastResult> raycastResultList = new List<RaycastResult>();
    private PointerEventData _pointerEventData;

    private void Update()
    {
        //指定したキーの押下
        if (Input.GetKeyDown(_bindKey))
        {
            _pointerEventData = new PointerEventData(EventSystem.current)
            {
                button = PointerEventData.InputButton.Left,
                position = _targetButton.transform.position //指定したボタンの位置にマウスがある体
            };
            EventSystem.current.RaycastAll(_pointerEventData , raycastResultList);
            var validGameObject = raycastResultList.Select(result => result.gameObject).FirstOrDefault(gameObject => gameObject != null);//一番最初にぶつかっている有効なGameObject取得
            raycastResultList.Clear();
            if (validGameObject == null)
            {
                return;
            }
            var currentPointerDownHandlerObject = ExecuteEvents.GetEventHandler<IPointerDownHandler>(validGameObject); //ボタン位置にあるGameObjectからIPointerDownHandlerを保持しているGameObjectを取得
            if (currentPointerDownHandlerObject != _targetButton.gameObject){
                return;    //ボタン位置から得られたGameObjectとボタンのGameObjectが異なる=別のもので遮られている ので処理しない
            }

            _pointerEventData.pointerPress = currentPointerDownHandlerObject;
            ExecuteEvents.Execute(currentPointerDownHandlerObject, _pointerEventData, ExecuteEvents.pointerDownHandler);
        }

        //指定したキーの押上
        if (_pointerEventData != null && _pointerEventData.pointerPress != null && Input.GetKeyUp(_bindKey))
        {
            ExecuteEvents.Execute(_pointerEventData.pointerPress, _pointerEventData, ExecuteEvents.pointerUpHandler);
            ExecuteEvents.Execute(_pointerEventData.pointerPress, _pointerEventData, ExecuteEvents.pointerClickHandler);
            _pointerEventData = null;
        }
    }
}

(よくわからんなりに調べて作ったので、大分力業ですが・・・)

使い方

このScriptをButtonコンポーネントが乗っているGameObjectに追加します。
image.png

Target Button は勝手に同GameObjectButtonがセットされます。
そして
Bind Key には割り当てたいハードキー を指定します(KeyCode の一覧が候補で出ます)
Androidの戻るキーは KeyCode.Escape で割り当たります。
image.png

なんと、これだけで、ボタンのタップとAndroidの戻るキーが同等になります! シンプル!!

注意

ボタンを疑似的にクリックした相当なので、(利点でも欠点でもあるんですが)ボタンのTransitionがそのまま効きます。
↑の動画をよく見ると分かるんですが、戻るキーを押した時でもボタンの色が変化しています(戻るキーを押しっぱなしにすると、ボタンも押されっぱなしになる)
それが嫌! という場合には使えないです。 悪しからず・・・。

補足

今回、 Androidの戻るキー対応 と銘打っては居ますが、既に書いた通りボタンには KeyCodeで割り当てるキーを指定することができます。
もう一つの使い道として、入力処理の一元化があります。

こちらの動画のゲームで今回のKeyBind.csが実際に使われており、前半はマウスでボタンをクリックして操作ですが、後半はそれぞれのボタンに割り当てられたキーボードで操作しています。

このように「複数方法の入力処理を制御」するには

  • ベタでボタンがクリックされた場合の処理とキーボード入力の処理の場合を分けて書いてしまうスタイル
  • IInput のような入力処理を抽象化したインタフェースを切り、IInput を実装した KeyboardInputButtonInput のようなクラスをそれぞれ実装するスタイル
  • 神Inputクラスに想定される全インプット処理分の条件分岐をぶち込んでいくGODスタイル

などなど。方法はありますがそれなりに面倒で。

対して、↑の動画のゲームでは入力制御は「ボタン処理」のみ対象に記述しています。
そして、KeyBind.csはあくまでもボタンの疑似クリック処理なので、キーボード操作を増やしても入力制御処理は何も手を入れずに済んでいます。 つまり、ボタンによる入力処理で「処理の一元化」がされている状態です。

もちろん、これは画面上にバーチャルパッド的なものをuguiで置いているから出来るだけなので適用範囲はそう広くは無いですが、使える人も少なくないのではないでしょうか。

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

UnityでAndroidの戻るキー対応をなるべく簡単にする提案

意外と面倒くさいAndroid戻るキー対応

ね。面倒くさいですよね。(iOSには無いわけですし・・・)
さしあたり一番簡単なのは「画面上の『ネガティブ的』なボタンと同等の機能をつける」事です。

ダイアログを開いたのであれば、戻るキーが「閉じるボタン」や「戻るボタン」相当になればよいですし。
image.png

画面遷移したのであれば、戻るキーが「Backボタン」(前の画面に戻る)相当になればよいわけです。
image.png

そう書くと簡単のように聞こえますが、
画面遷移した次の画面で、ダイアログを表示した場合は?
image.png

戻るキーを押したら、ダイアログも閉じてしまう+画面も前の画面に戻ってしまう

では困るわけです。

これをまともに対応しようとすると、

  • 優先順位スタックマネージャ的なクラスを作成
  • 画面遷移したら、「戻るボタン」の処理(Actionとか?)を(上記)スタックマネージャにPush
  • ダイアログを開いたら「ダイアログ閉じるボタン」の処理(Actionとか?)をスタックマネージャにPush
  • 戻るキーを押したら、スタックマネージャにスタックされている処理の一番上(Peek)を処理
  • ダイアログを閉じたら「ダイアログ閉じるボタン」の処理をスタックマネージャからRemove
  • もう一度戻るキーを押したら・・・・

といった、管理が必要になります。 はい面倒臭いですね!

もっとシンプルに考える

そもそも、上記例の「画面遷移した次の画面で、ダイアログを表示した場合」って、普通はダイアログがモーダル的に表示されていて、後ろの「戻るボタン」は押せないようにしているのがほとんどのはず。(わざわざ後ろのボタンのintaractiveをfalseにしているのか、「タッチガード」的な全画面Panelを一枚噛ませてタッチイベントを遮断しているかのどちらかがほとんどでしょう)

問題なのは、

単純に戻るキーとボタンが押されたときの処理を関連付けてしまうと、uguiのイベントとは関係無しに処理が呼ばれてしまう

ことです。

なので、徹底的にuguiのイベントを倣い、戻るキーの押下を指定ボタンへのマウスクリックへとすり替えてあげれば解決です。

作ってみた

KeyBind.cs
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

[RequireComponent(typeof(Button))]
public class KeyBind : MonoBehaviour
{
    [SerializeField]
    private Button _targetButton;

    public KeyCode _bindKey;

    private void Reset()
    {
        _targetButton = GetComponent<Button>();
    }

    private static List<RaycastResult> raycastResultList = new List<RaycastResult>();
    private PointerEventData _pointerEventData;

    private void Update()
    {
        //指定したキーの押下
        if (Input.GetKeyDown(_bindKey))
        {
            _pointerEventData = new PointerEventData(EventSystem.current)
            {
                button = PointerEventData.InputButton.Left,
                position = _targetButton.transform.position //指定したボタンの位置にマウスがある体
            };
            EventSystem.current.RaycastAll(_pointerEventData , raycastResultList);
            var validGameObject = raycastResultList.Select(result => result.gameObject).FirstOrDefault(gameObject => gameObject != null);//一番最初にぶつかっている有効なGameObject取得
            raycastResultList.Clear();
            if (validGameObject == null)
            {
                return;
            }
            var currentPointerDownHandlerObject = ExecuteEvents.GetEventHandler<IPointerDownHandler>(validGameObject); //ボタン位置にあるGameObjectからIPointerDownHandlerを保持しているGameObjectを取得
            if (currentPointerDownHandlerObject != _targetButton.gameObject){
                return;    //ボタン位置から得られたGameObjectとボタンのGameObjectが異なる=別のもので遮られている ので処理しない
            }

            _pointerEventData.pointerPress = currentPointerDownHandlerObject;
            ExecuteEvents.Execute(currentPointerDownHandlerObject, _pointerEventData, ExecuteEvents.pointerDownHandler);
        }

        //指定したキーの押上
        if (_pointerEventData != null && _pointerEventData.pointerPress != null && Input.GetKeyUp(_bindKey))
        {
            ExecuteEvents.Execute(_pointerEventData.pointerPress, _pointerEventData, ExecuteEvents.pointerUpHandler);
            ExecuteEvents.Execute(_pointerEventData.pointerPress, _pointerEventData, ExecuteEvents.pointerClickHandler);
            _pointerEventData = null;
        }
    }
}

(よくわからんなりに調べて作ったので、大分力業ですが・・・)

使い方

このScriptをButtonコンポーネントが乗っているGameObjectに追加します。
image.png

Target Button は勝手に同GameObjectButtonがセットされます。
そして
Bind Key には割り当てたいハードキー を指定します(KeyCode の一覧が候補で出ます)
Androidの戻るキーは KeyCode.Escape で割り当たります。
image.png

なんと、これだけで、ボタンのタップとAndroidの戻るキーが同等になります! シンプル!!

注意

ボタンを疑似的にクリックした相当なので、(利点でも欠点でもあるんですが)ボタンのTransitionがそのまま効きます。
↑の動画をよく見ると分かるんですが、戻るキーを押した時でもボタンの色が変化しています(戻るキーを押しっぱなしにすると、ボタンも押されっぱなしになる)
それが嫌! という場合には使えないです。 悪しからず・・・。

補足

今回、 Androidの戻るキー対応 と銘打っては居ますが、既に書いた通りボタンには KeyCodeで割り当てるキーを指定することができます。
もう一つの使い道として、入力処理の一元化があります。

こちらの動画のゲームで今回のKeyBind.csが実際に使われており、前半はマウスでボタンをクリックして操作ですが、後半はそれぞれのボタンに割り当てられたキーボードで操作しています。

このように「複数方法の入力処理を制御」するには

  • ベタでボタンがクリックされた場合の処理とキーボード入力の処理の場合を分けて書いてしまうスタイル
  • IInput のような入力処理を抽象化したインタフェースを切り、IInput を実装した KeyboardInputButtonInput のようなクラスをそれぞれ実装するスタイル
  • 神Inputクラスに想定される全インプット処理分の条件分岐をぶち込んでいくGODスタイル

などなど。方法はありますがそれなりに面倒で。

対して、↑の動画のゲームでは入力制御は「ボタン処理」のみ対象に記述しています。
そして、KeyBind.csはあくまでもボタンの疑似クリック処理なので、キーボード操作を増やしても入力制御処理は何も手を入れずに済んでいます。 つまり、ボタンによる入力処理で「処理の一元化」がされている状態です。

もちろん、これは画面上にバーチャルパッド的なものをuguiで置いているから出来るだけなので適用範囲はそう広くは無いですが、使える人も少なくないのではないでしょうか。

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

XmlElementのラッパークラス

ノード作成時のXmlDocument.CreateElementが面倒

XMLファイルをXmlDocumentで作成する場合、testAノードの下にtestBノードを作成したい時に、testA.AppendChild(testB)としたいのに、毎回XmlDocument.CreateElement(新しいノード)をする必要があるため、より直感的にAppendできように簡単なラッパークラスを考えてみました。

環境

IDE:VisualStudio2019
アプリケーション:コンソールアプリ
フレームワーク:.NET Core 3.1

XmlDocumentを持ちまわる

XmlElementをラップし、さらにXmlDocumentを持ちまわって、Append時にそのXmlDocumentでCreateElementさせます。

using System.Xml;

namespace TestProject
{
    /// <summary>
    /// XmlDocument拡張クラス
    /// </summary>
    public class XmlDocumentExtension : XmlDocument
    {
        /// <summary>
        /// ルート要素を作成
        /// ※既存の子ノードはすべて削除します
        /// </summary>
        /// <param name="el">要素</param>
        /// <param name="version">バージョン</param>
        /// <param name="encoding">エンコーディング</param>
        /// <param name="standalone">外部依存</param>
        /// <returns>XmlElementWrapper</returns>
        public XmlElementWrapper CreateRootOfElementWrapper(string el, string version = "1.0", string encoding = "utf-8", string standalone = null)
        {
            this.RemoveAll();
            this.AppendChild(this.CreateXmlDeclaration(version, encoding, standalone));
            var createEl = this.CreateElement(el);
            return new XmlElementWrapper(this.AppendChild(createEl));
        }
    }

    /// <summary>
    /// XmlElementラッパークラス
    /// </summary>
    public class XmlElementWrapper
    {
        /// <summary>
        /// XmlElement
        /// </summary>
        private XmlElement _el;

        /// <summary>
        /// XmlDocument
        /// </summary>
        private XmlDocument _doc;

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="node">XmlNode</param>
        public XmlElementWrapper(XmlNode node)
        {
            // 自身のXmlElementを保持
            this._el = node as XmlElement;

            // XmlDocumentを保持
            this._doc = this._el.OwnerDocument;
        }

        /// <summary>
        /// 子要素を追加
        /// </summary>
        /// <param name="el">要素</param>
        /// <returns>追加した子要素</returns>
        public XmlElementWrapper AppendChild(string el)
        {
            var addEl = this._el.AppendChild(this._doc.CreateElement(el));
            return new XmlElementWrapper(addEl);
        }
    }
}

AppendChildで追加した子要素を新たにXmlElementWrapperでラップすることで、後続のエレメントでもAppendが楽になります。
※AppendChildの引数はstringですがXmlNode版もあるといいのかもしれません。

また、最初のrootエレメントを起点とすることにしたため、XmlDocumentの拡張クラスを用意しています。

このクラスの使用イメージは以下です。

using System;
using System.IO;
using TestProject.Extension;

namespace TestProject
{
    /// <summary>
    /// メインクラス
    /// </summary>
    public class Program
    {
        /// <summary>
        /// メインエントリ
        /// </summary>
        /// <param name="args">実行時引数</param>
        public static void Main(string[] args)
        {
            // XmlDocument拡張クラス
            var doc = new XmlDocumentExtension();

            // root要素作成
            var root = doc.CreateRootOfElementWrapper("root");

            // rootの直下にtestAを作成
            var testA = root.AppendChild("testA");

            // testAの下にtestBを作成
            var testB = testA.AppendChild("testB");

            // xml保存
            doc.Save(Path.Combine(Environment.CurrentDirectory, "test.xml"));
        }
    }
}

以下のxmlが作成されます。

<?xml version="1.0" encoding="utf-8"?>
<root>
  <testA>
    <testB />
  </testA>
</root>

必要なメソッドを実装していく

値設定/値取得や、XPath、属性設定等 必要に応じてメソッドを用意し、ラップしたXElementの各処理を実装します。

        /// <summary>
        /// 値を設定
        /// </summary>
        /// <param name="value">値</param>
        public void SetValue(string value)
        {
            this._el.InnerText = value;
        }

        /// <summary>
        /// 値を取得
        /// </summary>
        /// <returns>値</returns>
        public string GetValue()
        {
            return this._el.InnerText;
        }

        /// <summary>
        /// 要素検索
        /// </summary>
        /// <param name="xPath">XPath</param>
        /// <returns>XmlElementWrapper</returns>
        public XmlElementWrapper SelectSingleNode(string xPath)
        {
            var node = this._el.SelectSingleNode(xPath);
            return node == null ? null : new XmlElementWrapper(node);
        }

サンプル全文

using System.Xml;

namespace TestProject.Extension
{
    /// <summary>
    /// XmlDocument拡張クラス
    /// </summary>
    public class XmlDocumentExtension : XmlDocument
    {
        /// <summary>
        /// ルート要素を作成
        /// ※既存の子ノードはすべて削除します
        /// </summary>
        /// <param name="el">要素</param>
        /// <param name="version">バージョン</param>
        /// <param name="encoding">エンコーディング</param>
        /// <param name="standalone">外部依存</param>
        /// <returns>XmlElementWrapper</returns>
        public XmlElementWrapper CreateRootOfElementWrapper(string el, string version = "1.0", string encoding = "utf-8", string standalone = null)
        {
            this.RemoveAll();
            this.AppendChild(this.CreateXmlDeclaration(version, encoding, standalone));
            var createEl = this.CreateElement(el);
            return new XmlElementWrapper(this.AppendChild(createEl));
        }
    }

    /// <summary>
    /// XmlElementラッパークラス
    /// </summary>
    public class XmlElementWrapper
    {
        /// <summary>
        /// XmlElement
        /// </summary>
        private XmlElement _el;

        /// <summary>
        /// XmlDocument
        /// </summary>
        private XmlDocument _doc;

        /// <summary>
        /// タグ名
        /// </summary>
        public string Name => this._el.Name;

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="node">XmlNode</param>
        public XmlElementWrapper(XmlNode node)
        {
            this._el = node as XmlElement;
            this._doc = this._el.OwnerDocument;
        }

        /// <summary>
        /// 子要素を追加
        /// </summary>
        /// <param name="el">要素</param>
        /// <returns>追加した子要素</returns>
        public XmlElementWrapper AppendChild(string el)
        {
            var addEl = this._el.AppendChild(this._doc.CreateElement(el));
            return new XmlElementWrapper(addEl);
        }

        /// <summary>
        /// 値を設定
        /// </summary>
        /// <param name="value">値</param>
        public void SetValue(string value)
        {
            this._el.InnerText = value;
        }

        /// <summary>
        /// 値を取得
        /// </summary>
        /// <returns>値</returns>
        public string GetValue()
        {
            return this._el.InnerText;
        }

        /// <summary>
        /// 属性を設定
        /// </summary>
        /// <param name="attrName">属性名</param>
        /// <param name="value">属性値</param>
        public void SetAttribute(string attrName, string attrValue)
        {
            this._el.SetAttribute(attrName, attrValue);
        }

        /// <summary>
        /// 属性を取得
        /// </summary>
        /// <param name="attrName">属性名</param>
        /// <returns>属性値</returns>
        public string GetAttribute(string attrName)
        {
            return this._el.GetAttribute(attrName);
        }

        /// <summary>
        /// 要素検索
        /// </summary>
        /// <param name="xPath">XPath</param>
        /// <returns>XmlElementWrapper</returns>
        public XmlElementWrapper SelectSingleNode(string xPath)
        {
            var node = this._el.SelectSingleNode(xPath);
            return node == null ? null : new XmlElementWrapper(node);
        }
    }
}
using System;
using System.IO;
using TestProject.Extension;

namespace TestProject
{
    /// <summary>
    /// メインクラス
    /// </summary>
    public class Program
    {
        /// <summary>
        /// メインエントリ
        /// </summary>
        /// <param name="args">実行時引数</param>
        public static void Main(string[] args)
        {
            try
            {
                // XmlDocument拡張クラス
                var doc = new XmlDocumentExtension();

                // root要素作成
                var root = doc.CreateRootOfElementWrapper("root");

                // rootの直下にtestAを作成
                var testA = root.AppendChild("testA");

                // testAの下にtestBを作成
                var testB = testA.AppendChild("testB");

                // testBの下に同じタグを追加
                var testTag1 = testB.AppendChild("testTag");
                var testTag2 = testB.AppendChild("testTag");

                // 値設定
                var value1 = "valueTag1";
                var value2 = "valueTag2";
                testTag1.SetValue(value1);
                testTag2.SetValue(value2);

                // 属性設定
                testTag1.SetAttribute("testAttr", "testAttrValue");

                // XPath(あえてrootから)
                var findEl = root.SelectSingleNode($"//testB/testTag[text()='{value1}']");

                Console.WriteLine("SelectSingleNode結果 XMLタグ名:" + findEl?.Name);
                Console.WriteLine("属性testAttr:" + findEl?.GetAttribute("testAttr"));

                // xml保存
                doc.Save(Path.Combine(Environment.CurrentDirectory, "test.xml"));
            }
            catch(Exception err)
            {
                Console.WriteLine(err.Message);                
            }
            finally
            {
                Console.Read();
            }
        }
    }
}

image.png

<?xml version="1.0" encoding="utf-8"?>
<root>
  <testA>
    <testB>
      <testTag testAttr="testAttrValue">valueTag1</testTag>
      <testTag>valueTag2</testTag>
    </testB>
  </testA>
</root>

おわりに

ラップせずともXmlElementを継承して実現できるのであれば、各メソッドやプロパティを実装しなくてよいのですが・・・

アドバイス等ありましたら、いただけると幸いです。

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

ComputeShaderを使ってUVマップ画像を作成する

はじめに

自作のEditor拡張でUVマップを表示しました。
そのときにComputeShaderを使用したのでその詳細を記していきます。

ComputeShaderとは

HLSLやGLSLといった描画用のシェーダーがありますが、ComputeShaderはGPUを使った数値計算をする仕組みです。
GPUは単純な計算を並列実行できるので、処理によってはCPUに比べて高速に処理が実行できます。
Unityで使う場合には事前にC#のプログラム上で必要なデータや出力先を指定してComputeShaderを実行させます。
https://docs.unity3d.com/ja/2018.4/Manual/class-ComputeShader.html

メッシュのUVマップを取得する実際のコードを見ながら簡単に解説します。

実際のコード

実際のコードです。
今回のComputeShaderではポリゴン単位で並列で計算させています。

getUVMap.compute
#pragma kernel CSMain

// 出力先テクスチャ
RWTexture2D<float4> UVMap;

// 入力データ
StructuredBuffer<float2> UVs;
StructuredBuffer<int> Triangles;
int Width;
int Height;

CGPROGRAM
// 2点間に線を引く
void drawline(uint2 p1, uint2 p2, float4 color) {
    int2 diffp12 = int2(p2.x-p1.x, p2.y-p1.y);
    float distp12 = distance(p1, p2);
    for (int i = 0; i < distp12; i++) 
    {
        UVMap[p1 + diffp12 / distp12 * i] = color;  
    }
}
ENDCG

[numthreads(1,1,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
    // 3角ポリゴンをつくる3頂点のインデックスを取得
    int p1Index = Triangles[id.x * 3];
    int p2Index = Triangles[id.x * 3 + 1];
    int p3Index = Triangles[id.x * 3 + 2];

    // 3頂点に対応したuv座標を取得
    float2 uv1 = UVs[p1Index];
    float2 uv2 = UVs[p2Index];
    float2 uv3 = UVs[p3Index];

    // テクスチャの座標に変換
    uint2 p1Pos = uint2(uv1.x * Width, uv1.y * Height);
    uint2 p2Pos = uint2(uv2.x * Width, uv2.y * Height);
    uint2 p3Pos = uint2(uv3.x * Width, uv3.y * Height);

    float4 color = float4(1, 1, 1, 1);

    // 3頂点が示すテクスチャ上の点間に線を引く
    drawline(p1Pos, p2Pos, color);
    drawline(p2Pos, p3Pos, color);
    drawline(p3Pos, p1Pos, color);
}
cprog.cs
private Texture2D GetUVMap(Mesh mesh, int subMeshIndex, Texture2D texture)
{
    var triangles = mesh.GetTriangles(subMeshIndex);
    var uvs = mesh.uv;

    if (uvs.Count() <= 0) return null;

    ComputeShader cs = Instantiate(Resources.Load<ComputeShader>("getUVMap")) as ComputeShader;
    int kernel = cs.FindKernel("CSMain");

    RenderTexture uvMapRT = new RenderTexture(texture.width, texture.height, 0);
    uvMapRT.enableRandomWrite = true;
    uvMapRT.Create();

    var triangleBuffer = new ComputeBuffer(triangles.Count(), sizeof(int));
    var uvBuffer = new ComputeBuffer(uvs.Count(), Marshal.SizeOf(typeof(Vector2)));
    triangleBuffer.SetData(triangles);
    uvBuffer.SetData(uvs);

    cs.SetTexture(kernel, "UVMap", uvMapRT);
    cs.SetInt("Width", texture.width);
    cs.SetInt("Height", texture.height);
    cs.SetBuffer(kernel, "Triangles", triangleBuffer);
    cs.SetBuffer(kernel, "UVs", uvBuffer);

    cs.Dispatch(kernel, triangles.Length / 3, 1, 1);

    triangleBuffer.Release();
    uvBuffer.Release();

    var uvMapTex = new Texture2D(texture.width, texture.height, TextureFormat.RGB24, false);
    uvMapTex.name = texture.name;

    // RenderTextureからTexture2Dに変換
    var original = RenderTexture.active;
    RenderTexture.active = uvMapRT;
    uvMapTex.ReadPixels(new Rect(0, 0, uvMapRT.width, uvMapRT.height), 0, 0);
    uvMapTex.Apply();
    RenderTexture.active = original;

    uvMapRT.Release();

    return uvMapTex;
}

処理の解説

compute shader

このComputeShaderで実行される部分はCSMainの部分です。

[numthreads(1,1,1)]
void CSMain (uint3 id : SV_DispatchThreadID)

上についているnumthreadsは処理単位のブロックみたいなものですが、今回は特に考えないのですべて1にしています。

そうした場合、引数のidには並列処理ごとに割り振られた異なるidが入力されます。
[numthreads(1,1,1)]としたのでid.xが異なる値でyとzはすべて同じ値になっています。

そのidを元にメッシュの3頂点を特定するためのインデックスを取得します。

// 3角ポリゴンをつくる3頂点のインデックスを取得
int p1Index = Triangles[id.x * 3];
int p2Index = Triangles[id.x * 3 + 1];
int p3Index = Triangles[id.x * 3 + 2];

取得したインデックスを元にUV座標を取得してテクスチャ座標に変換します。

// 3頂点に対応したuv座標を取得
float2 uv1 = UVs[p1Index];
float2 uv2 = UVs[p2Index];
float2 uv3 = UVs[p3Index];

// テクスチャの座標に変換
uint2 p1Pos = uint2(uv1.x * Width, uv1.y * Height);
uint2 p2Pos = uint2(uv2.x * Width, uv2.y * Height);
uint2 p3Pos = uint2(uv3.x * Width, uv3.y * Height);

そして、2頂点間に線を引いていきます。

drawline(p1Pos, p2Pos, color);
drawline(p2Pos, p3Pos, color);
drawline(p3Pos, p1Pos, color);

2頂点間に線を引くコードはこちらです。

// 2点間に線を引く
void drawline(uint2 p1, uint2 p2, float4 color) {
    int2 diffp12 = int2(p2.x-p1.x, p2.y-p1.y);
    float distp12 = distance(p1, p2);
    for (int i = 0; i < distp12; i++) 
    {
        UVMap[p1 + diffp12 / distp12 * i] = color;  
    }
}

cshape

C#コード側ではこのComputeShaderに必要なデータを渡して、実行させています。

始めに使用するComputeShaderをResourcesフォルダから読み込んで、実行するKernelを取得します。

ComputeShader cs = Instantiate(Resources.Load<ComputeShader>("getUVMap")) as ComputeShader;
int kernel = cs.FindKernel("CSMain");

次に使用するデータを渡すためにBufferを確保して、データを設定します。

var triangleBuffer = new ComputeBuffer(triangles.Count(), sizeof(int));
var uvBuffer = new ComputeBuffer(uvs.Count(), Marshal.SizeOf(typeof(Vector2)));
triangleBuffer.SetData(triangles);
uvBuffer.SetData(uvs);

cs.SetTexture(kernel, "UVMap", uvMapRT);
cs.SetInt("Width", texture.width);
cs.SetInt("Height", texture.height);
cs.SetBuffer(kernel, "Triangles", triangleBuffer);
cs.SetBuffer(kernel, "UVs", uvBuffer);

そして、ComputeShaderを実行します。
Dispatch(kernel, x, y, z)はComputeShaderの[numthreads(x, y, z)]に対応しています。

cs.Dispatch(kernel, triangles.Length / 3, 1, 1);

これで計算結果がテクスチャとしてuvMapTexに出力されています。

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