コルネの進捗や備忘録が記されたなにか

進捗や成果物や備忘録てきななにかを雑に更新していきます。


スポンサードリンク

Power Apps のカスタム関数で入力パラメータにレコード / テーブルを利用する際の覚書

はじめに

この記事では、Power Apps のカスタム関数でパラメータにレコード、及びテーブルを利用する際の覚書になります。
もし誤っている内容や、私の理解が足りないような記載がありましたら指摘してもらえると助かります。

また、現時点(2021/02/28)ではカスタム関数はPreview 機能なため、この記事をご覧になっている際には機能などが大幅に変更になっている可能性がありますので、 ご注意ください。

カスタム関数とは

カスタム関数ってなに?という方は、以前私が簡単にまとめた記事があるので、まずはそちらをご覧になってください。

koruneko.hatenablog.com

パラメータを利用してみる

レコード

入力、及び出力のデータ型にレコードを設定した場合の動作についてです。

以下のように設定します。

f:id:koruneko:20210228181307p:plain

左上のプロパティを選択すると先ほど作成した、関数とパラメータが存在します。

f:id:koruneko:20210228181551p:plain

Record_record の式をみてみると以下のように設定されています。

Record_record

{SampleStringField: "SampleText", SampleNumberField: 10, SampleBooleanField: true}

この式がなにを表しているのか?というと、関数を呼び出す際のパラメータに渡すレコードのフィールドを定義しています。

ここで定義したようなレコード以外を渡すとエラーになってしまうので、注意が必要です。

アウトプットは、 Record で定義します。

Record

{SampleStringField: "[" & record.SampleStringField & "]", SampleNumberField: record.SampleNumberField * 2, SampleBooleanField: true}

今回はテストのため、与えられたレコードを少し加工して返すだけにしています。

画面に戻り、この関数を呼び出してみましょう。

Gallery.Items

Component1_1.Record({SampleStringField: "Text", SampleNumberField: 100, SampleBooleanField: true})

f:id:koruneko:20210228185947p:plain

先ほど作成した関数に予め定義されたようなレコードを渡すと、 SampleStringField の値は前後に"[]"を付けて、 SampleNumberField の値は2を乗算した値が返ってきていることが確認できます。

また、下記の画像のように異なるフィールドのレコードを渡すとエラーになってしまうということも確認できました。

f:id:koruneko:20210228190428p:plain

テーブル

続いてパラメータの型にテーブルを設定した場合です。

基本的にレコードの時と考え方は同じです。

以下のように設定します。

f:id:koruneko:20210228191246p:plain

テーブルのときもレコードのときと同様にフィールドの定義が必要です。

Tabele_table

Table({SampleStringField: "SampleText", SampleNumberField: 10, SampleBooleanField: true})

出力には以下のように加工したテーブルを返します。

Table

ForAll(
    table,
    {
        SampleStringField: "{" & SampleStringField & "}",
        SampleNumberField: SampleNumberField / 2,
        SampleBooleanField: SampleBooleanField
    }
)

SampleStringField では、渡された文字列を"{}"で囲って返し、 SampleNumberField では渡された値を2で割った値を返すようにしています。

画面のほうで、実際に動作確認してみましょう。

Gallery.Items

Component1_1.Tabel(
    Table(
        {SampleStringField: "a", SampleNumberField: 10, SampleBooleanField: false},
        {SampleStringField: "b", SampleNumberField: 50, SampleBooleanField: true}
    )
)

f:id:koruneko:20210228192951p:plain

ちゃんと動いていますね。

テーブルでは、すべてのレコードで定義したフィールドが存在している必要はありません。

f:id:koruneko:20210228193117p:plain

f:id:koruneko:20210228193310p:plain

ただし、どこかのレコードで少なくとも1つは設定されたフィールドが含まれている必要があります。

f:id:koruneko:20210228193510p:plain

これはコレクションを作成したときの動作と同様、対象フィールドが設定されていないレコードはそのフィールドが0や空白、false などとして扱われるからですね。
つまり実際はそのレコードには省略しているフィールドも存在しているというわけです。

動作が不安定?

簡単な使い方は上記でわかりました。

これからは、使用していた中でちょっと動作が怪しいな?と思ったものを纏めました。
なお、冒頭でも述べている通りこれはまだPreview 機能なので修正などの処置が行われる可能性があります。

テーブルの入力フィールドが更新されない

テーブルの入力フィールドを以下のように修正しました。
SampleNumberField を削除しています。

Table_table

Table({SampleStringField: "SampleText", SampleBooleanField: true})

Table

ForAll(
    table,
    {
        SampleStringField: "{" & SampleStringField & "}",
        SampleNumberField: 10,
        SampleBooleanField: SampleBooleanField
    }
)

これに伴い出力も変更しています。

この修正を行ったうえで画面でこのカスタム関数を呼び出している式を修正しましょう。

SampleNumberField が不要になったので削除します。

f:id:koruneko:20210228194247p:plain

すると上記のようにフィールドが異なる旨のエラーが出力されてしまいました。

これは変更が反映されていないのが原因です。
現時点での対策として、カスタム関数を保存し直すという手があります。

説明を適当に修正して保存し直しましょう。

これでも直らなかった場合は、まずはアプリを保存しましょう。

やると不味い行動として、
対象のコンポーネントを削除 -> 再度対象コンポーネントを追加
というのがあります。

これをするとどうなるか?というと以下のようなエラーが表示され、アプリがクラッシュします。(しました)

f:id:koruneko:20210228195057p:plain

WebAuthoring abnormal termination. Client date/time: 2021-02-28T10:47:51.149Z Version: 3.21023.28 (v3.21023.28.179268482) Session ID: ed3cfda3-7c17-44c3-948e-67cf1293852e description: {"exception":{"message":"XMLHttpRequest error(string). {\"traceLevel\":3,\"message\":\"An error occurred in Document API document.createcompletecontrol, activity id 572d8111-aa18-4ba1-8aca-e417fa34a389\",\"status\":500}","name":"UnhandledError","isCritical":true,"detail":{"exception":{}}},"error":"[circular]","promise":{"oncancel":null,"nextState":null,"state":{"name":"error","enter":"[function]","cancel":"[function]","done":null,"then":null,"completed":"[function]","error":"[function]","notify":"[function]","progress":"[function]","setCompleteValue":"[function]","setErrorValue":"[function]"},"listeners":null,"value":"[circular]","isException":false,"errorId":16,"done":"[function]","then":"[function]"},"id":16,"setPromise":"[function]"} stack: UnhandledError: XMLHttpRequest error(string). {"traceLevel":3,"message":"An error occurred in Document API document.createcompletecontrol, activity id 572d8111-aa18-4ba1-8aca-e417fa34a389","status":500} at t [as constructor] (https://cdn-paaprodejp.azureedge.net/v3.21023.28.179268482/studio/js/Core.js?v=3.21023.28.179268482:1:205175) at new t (https://cdn-paaprodejp.azureedge.net/v3.21023.28.179268482/studio/js/Core.js?v=3.21023.28.179268482:1:245836) at o.translateError (https://cdn-paaprodejp.azureedge.net/v3.21023.28.179268482/studio/js/AppMagic.WebAuthoring.js?v=3.21023.28.179268482:1:509997) at o.raiseTerminalError (https://cdn-paaprodejp.azureedge.net/v3.21023.28.179268482/studio/js/AppMagic.WebAuthoring.js?v=3.21023.28.179268482:1:509279) at o.unhandledException (https://cdn-paaprodejp.azureedge.net/v3.21023.28.179268482/studio/js/AppMagic.WebAuthoring.js?v=3.21023.28.179268482:1:509192) at o. (https://cdn-paaprodejp.azureedge.net/v3.21023.28.179268482/studio/js/AppMagic.WebAuthoring.js?v=3.21023.28.179268482:1:507788) at https://cdn-paaprodejp.azureedge.net/v3.21023.28.179268482/studio/js/Core.js?v=3.21023.28.179268482:1:307766 at Object.throw (https://cdn-paaprodejp.azureedge.net/v3.21023.28.179268482/studio/js/Core.js?v=3.21023.28.179268482:1:307872) at u (https://cdn-paaprodejp.azureedge.net/v3.21023.28.179268482/studio/js/Core.js?v=3.21023.28.179268482:1:306691) at Object.q [as notify] (https://cdn-paaprodejp.azureedge.net/v3.21023.28.179268482/studio/openSource/modified/winjs/js/base.js?v=3.21023.28.179268482:2:38070) errorNumber: 0 errorMessage: XMLHttpRequest error(string). {"traceLevel":3,"message":"An error occurred in Document API document.createcompletecontrol, activity id 572d8111-aa18-4ba1-8aca-e417fa34a389","status":500} callStack: UnhandledError: XMLHttpRequest error(string). {"traceLevel":3,"message":"An error occurred in Document API document.createcompletecontrol, activity id 572d8111-aa18-4ba1-8aca-e417fa34a389","status":500} at t [as constructor] (https://cdn-paaprodejp.azureedge.net/v3.21023.28.179268482/studio/js/Core.js?v=3.21023.28.179268482:1:205175) at new t (https://cdn-paaprodejp.azureedge.net/v3.21023.28.179268482/studio/js/Core.js?v=3.21023.28.179268482:1:245836) at o.translateError (https://cdn-paaprodejp.azureedge.net/v3.21023.28.179268482/studio/js/AppMagic.WebAuthoring.js?v=3.21023.28.179268482:1:509997) at o.raiseTerminalError (https://cdn-paaprodejp.azureedge.net/v3.21023.28.179268482/studio/js/AppMagic.WebAuthoring.js?v=3.21023.28.179268482:1:509279) at o.unhandledException (https://cdn-paaprodejp.azureedge.net/v3.21023.28.179268482/studio/js/AppMagic.WebAuthoring.js?v=3.21023.28.179268482:1:509192) at o. (https://cdn-paaprodejp.azureedge.net/v3.21023.28.179268482/studio/js/AppMagic.WebAuthoring.js?v=3.21023.28.179268482:1:507788) at https://cdn-paaprodejp.azureedge.net/v3.21023.28.179268482/studio/js/Core.js?v=3.21023.28.179268482:1:307766 at Object.throw (https://cdn-paaprodejp.azureedge.net/v3.21023.28.179268482/studio/js/Core.js?v=3.21023.28.179268482:1:307872) at u (https://cdn-paaprodejp.azureedge.net/v3.21023.28.179268482/studio/js/Core.js?v=3.21023.28.179268482:1:306691) at Object.q [as notify] (https://cdn-paaprodejp.azureedge.net/v3.21023.28.179268482/studio/openSource/modified/winjs/js/base.js?v=3.21023.28.179268482:2:38070)

他操作も私が確認できていないだけで、やると不味いことが多々ありそうなので、保存するなりで対策しましょう。
また、修正した入力パラメータを元に戻しておくのもよさそうですね。
恐らく裏で不正なデータ扱いされていて非常に不安定な状態だと思いますので。。。

なおこの問題は型:レコードでは確認できませんでした。

型が勝手にテーブルからレコードに変更される

仕様??わからないですが、事実としての動作を記しておきます。

パラメータをテーブルではなくレコードにしてみます。

Table_tabel

{SampleStringField: "SampleText", SampleBooleanField: true}

このカスタム関数を呼び出している箇所を確認してみると以下エラーが表示されています。

f:id:koruneko:20210228200102p:plain

対象カスタム関数のパラメータをみてみると、以下のようにテーブル -> レコードに型が変わっています。

f:id:koruneko:20210228200259p:plain

仕様??型解決してくれてる??(正直余計な事しないでほしい)

f:id:koruneko:20210228200404p:plain

ただ、逆に入力項目の型:レコードの項目にテーブルを設定しようとすると、上記のようなエラーが出力されるので、恐らく、型:テーブルを設定しているにも関わらずレコード型を設定できることがよくないんでしょうね。

型:テーブルを設定した際の入力パラメータの型チェックに漏れがあるのですかね。
実際のソースみることができないので憶測になりますが、そんな感じがします。

おわりに

冒頭でも述べている通り、カスタム関数の作成はPreview 機能なので今後機能が変更になることがあります。

また、なにか誤っていたりした場合は教えていただけると助かります。

【Power Apps】カスタム関数が作成できるようになりました!

はじめに

2021年2月18日日の発表でカスタム関数の作成機能をプレビューでの公開を開始した。といった旨の記事が公開されました。

powerapps.microsoft.com

この発表によると、これまでは、 OnSelect のイベントのみ実行可能でしたが、今回のアップデートで OnChange イベントを実行できるようになったと記載されています。
例えば、クエリや関数を作成して、それにパラメータを渡すことで内部で計算させて結果を出力させるという関数のようなものを独自に作成できるようになったようですね。

とはいえなにやら制限はあるっぽい?ですが私の英語力と知識が足りないので、なにを伝えたいのかよくわからないですね。。。

such as output properties must be pure without side effects

"出力プロパティは副作用のない純粋なものでなければならない"
と直訳するとなるのですかね?どういうことだ??

なおこの機能はPreview 機能のため、今後更に機能追加が行われていく一方これまで使えていた機能が使えなくなる。仕様変更が行われる。などの可能性があるためご注意ください。

カスタム関数を作成できるようにする

カスタム関数の作成機能は現段階ではPreview 機能なため、設定により対象機能を有効化してあげる必要があります。

ファイル > 設定 > 詳細設定 > 実験的な機能 より、"拡張コンポーネントのプロパティ"を"オン"にします。

f:id:koruneko:20210224000115p:plain

これでカスタム関数作成の準備ができました!

カスタム関数を作成してみる

今回は Enhanced component properties に記載されている例に倣ってカスタム関数を作成していこうと思います。

動作プロパティ

"コンポーネント"より、"新しいコンポーネント"を選択して、コンポーネントを作成します。

f:id:koruneko:20210224001708p:plain

記事のように、"日付の選択"と"ラベル"を2つずつ作成します。

f:id:koruneko:20210224002257p:plain

これから作成するカスタム関数では、"日付の選択が変更されると、通知を行う"といったものを作成するようです。

"Component1"を選択して、"新しいカスタム プロパティ"より、カスタムプロパティを定義します。

f:id:koruneko:20210224002729p:plain

今回設定した項目は以下項目です。
表示名:OnChange
説明:Notify when User Change DataPicker
プロパティの型:動作

設定したら、"作成"を行います。

先ほど作成した"日付の選択"を2つ選択し、 OnChange にて以下のように設定します。

f:id:koruneko:20210224003641p:plain

OnChange

Component1.OnChange()

ここで記載した、 OnChange() は先ほどカスタムプロパティで設定した名前となります。

では実際に作成したカスタム関数の動作確認を行ってみましょう。

画面に戻り、"カスタム"より先ほど作成した Component1 を追加します。

f:id:koruneko:20210224004337p:plain

追加した Component1OnChange プロパティに Notify() 関数を設定します。

f:id:koruneko:20210224004604p:plain

Component1_1.OnChange

Notify("You have changed date.")

それでは日付を変更してみましょう。
日付を変更してみると、先ほど設定した通知が画面上部に表示されたかと思います。

Great! Easy so far.

すごい!今のところ簡単だそうです。

プロパティパラメータ

続いて OnChange の際にパラメータを渡してみます。

先ほど作成したコンポーネントに戻り、カスタムプロパティの編集画面を表示します。
先ほど作成したカスタムプロパティの右側(画像だと"テキスト")を選択すると表示できます。

f:id:koruneko:20210224005416p:plain

"新しいパラメーター"を選択して、カスタム関数にパラメータ(引数)を定義します。

f:id:koruneko:20210224005928p:plain

今回設定した項目は以下項目です。
パラメータ名:Begin
説明:a parameter
データ型:日時

作成を行うと、作成したパラメータが追加されます。

f:id:koruneko:20210224010137p:plain

同様の手順でパラメータ名:End のパラメータも作成しましょう。

f:id:koruneko:20210224010307p:plain

保存を行ったら、"日付の選択"を2つ選択して OnChange プロパティを編集して、呼び出している OnChange 関数に先ほど作成したパラメータを渡すように設定します。

f:id:koruneko:20210224010633p:plain

OnChange

Component1.OnChange(DatePicker1.SelectedDate, DatePicker1_1.SelectedDate)

画面に戻り、作成したパラメータを通知で表示するように設定します。

f:id:koruneko:20210224011008p:plain

Component1_1.OnChange

Notify("You have changed date. Begin is " & Begin & ", End is " & End)

先ほど、 Component1_1OnChange に設定したはずの関数が消えているかと思いますが、これはバグで修正予定だそうです。

Note that because we added the parameters to the event, the formula’s value in the app will have returned to its default and our earlier customization will have been lost. This is a bug that we will fix soon (sorry, we’re still experimental).

設定を行い、実際に動作させてみると、選択した日付が表示されたかと思います。

純粋関数

最後に、入力プロパティと出力プロパティを利用してみます。

Power Apps ではExcelRANDBETWEEN 関数 という、指定された範囲内の整数の乱数を返すような関数は提供されていません。
今回はこのRANDBETWEEN 関数をカスタム関数で作成してみます。

新規で MathUtils というコンポーネントを作成して、 RandBetween というカスタムプロパティを定義します。

f:id:koruneko:20210224012207p:plain

今回設定した項目は以下項目です。
表示名:RandBetween
説明:RandBetween
プロパティの型:出力
データ型:数値

続いてパラメータを2つ作成します。
作成するパラメータは数値型の BottomTop です。
これらは必須パラメータとしたいので、"必須"にチェックを入れます。

f:id:koruneko:20210224013211p:plain

f:id:koruneko:20210224013315p:plain

MathUtils を選択し、設定項目を確認してみると、先ほど作成したカスタム関数を確認することができます。

f:id:koruneko:20210224013447p:plain

RandBetween に以下式を記載します。

MathUtils.RandBetween

If(
    Top >= Bottom, 
    Round(Rand() * (Top - Bottom) + Bottom, 0),
    Blank()
)

簡単に式の解説を記載しておきます。
まず、前提として作成する乱数の範囲は 最大値 >= 最小値 となっている必要があるので Top >= Bottom で判定を行っています。

Round 関数 は、簡単にいうと指定した桁数になるように四捨五入を行う関数です。
Rand 関数 は、0 以上 1 未満の疑似乱数を返す関数です。

Rand() * X とすると、0~X までの値が返ってくることになりますね。
このX が最大値である Top です。 また、最低値は0ではなく Bottom で設定された値としたいです。
なので Bottom の数値を加算するわけですが、これでは Top + Bottom の数値が最大返ってきます。
これでは想定通りの動作にならないので、 Top - Bottom を行っているというわけです。

以上でカスタム関数の作成は完了したので、画面に追加してカスタム関数の動作確認を行ってみます。

画面に先ほど作成した、 MathUtils コンポーネントを追加し、 BottomTop を設定するためのスライダー、 RandBetween 関数の出力結果を表示するためのラベル、また、それらがなにを表すのかを記載したラベルを追加します。

f:id:koruneko:20210224015039p:plain

RandBetween 関数の出力結果を表示するためのラベルには以下のように設定し、 RandBetween 関数を呼び出すようにします。

Text

MathUtils_1.RandBetween(Slider1.Value, Slider1_1.Value)

スライダーの値を変更してみると、Bottom ~ Top までの間でランダムな数値が返されることが確認できるかと思います。

おわりに

発表されたブログの日本語訳と簡単な解説の補足だけになってしまいましたが、以上でカスタム関数の作成の紹介は終わりです。
これを利用すれば、よく使うような式を関数化して使いまわすようにしたり、開発を行う際に関数作成チームと画面作成チームという所謂フロントとエンドに分かれての開発が可能になってくるかもですね。

ただ、ブログでも記載されている通り、いくつか問題があるようです。

  • RandBetween is a pure function – it does its work based purely on its input parameters. It can’t read state in the app, such as global variables or data sources.
  • RandBetween is a data flow property. It cannot change state within the component or the app.
  • The component had to be instanced. It is a UX component that has to be placed on the screen, where in fact MathUtils has no UI at all. It would be better if RandBetween was simply a function in a MathUtils namespace and not object oriented.

We are actively working to build on this foundation, to make this all cleaner, and address the limitations.

私たちは、この基盤上に構築を行い、これをすべてクリーンにして、制限に対処するために積極的に取り組んでいます。

もし利用していく中で、意見などがありましたら、Power Apps community forum で投げてみましょう。

【PowerShell Runbook】 入力パラメータの型検証

はじめに

この記事は、PoweShell Runbook で、入力パラメータにて値を渡したときその値の型はどうなるのか?
を検証した記事になります。

誤っている箇所があればご指摘いただければと思います。

型検証を行おうと思ったきっかけはこちらの記事で、Power Automate からJSON 値を渡したはずがConvertFrom-Json でなぜか失敗してしまったことがきっかけです。
Microsoft 365 ユーザーアカウントの作成を自動化する

PowerShell Runbook での入力パラメータの構成方法

PowerShell Runbook で入力パラメータを構成するには、以下のように記述します。

Param
(
  [Parameter (Mandatory= $true/$false)]
  [Type] $Name1 = <Default value>,

  [Parameter (Mandatory= $true/$false)]
  [Type] $Name2 = <Default value>
)

詳しくは以下の公式doc をご確認ください。

docs.microsoft.com

検証

PowerShell Runbook の作成

今回作成したPowerShell Runbook には以下のような検証用シェルを作成しました。
PowerShell Runbook の詳しい作成方法は今回省きます。

Param
(
    $str_1,[string]$str_2,
    $num_1,[int]$num_2,
    $json_1,$json_2,$json_3
)

$str = "str"
$num = 1
$json = '{ "name": "korune" }'

echo ("str:" + $str)
echo (" - type:" + $str.GetType().FullName)

echo  "`r`n"

echo ("num:" + $num)
echo (" - type:" + $num.GetType().FullName)

echo  "`r`n"

echo ("json:" + $json)
echo (" - type:" + $json.GetType().FullName)

echo  "`r`n"

echo ("str_1:" + $str_1)
echo (" - type:" + $str_1.GetType().FullName)
echo ("str_2:" + $str_2)
echo (" - type:" + $str_2.GetType().FullName)

echo  "`r`n"

echo ("num_1:" + $num_1)
echo (" - type:" + $num_1.GetType().FullName)
echo ("num_2:" + $num_2)
echo (" - type:" + $num_2.GetType().FullName)

echo  "`r`n"

echo ("json_1:" + $json_1)
echo (" - type:" + $json_1.GetType().FullName)
echo ("json_2:" + $json_2)
echo (" - type:" + $json_2.GetType().FullName)
echo ("json_3:" + $json_3)
echo (" - type:" + $json_3.GetType().FullName)

echo  "`r`n"

echo ("`r`n---ConvertFrom-Json----`r`n")

$convert_json = ConvertFrom-Json $json
echo ("json:" + $convert_json)
echo (" - type:" + $convert_json.GetType().FullName)
echo  "`r`n"

$convert_json_1 = ConvertFrom-Json $json_1
echo ("json_1:" + $convert_json_1)
echo (" - type:" + $convert_json_1.GetType().FullName)
echo  "`r`n"

$convert_json_2 = ConvertFrom-Json $json_2
echo ("json_2:" + $convert_json_2)
echo (" - type:" + $convert_json_2.GetType().FullName)
echo  "`r`n"

$convert_json_3 = ConvertFrom-Json $json_3
echo ("json_3:" + $convert_json_3)
echo (" - type:" + $convert_json_3.GetType().FullName)
echo  "`r`n"

echo ("`r`n---ConvertFrom-Json(not in param)----`r`n")

echo ("json")
ConvertFrom-Json $json
echo  "`r`n"

echo ("json_1")
ConvertFrom-Json $json_1
echo  "`r`n"

echo ("json_2")
ConvertFrom-Json $json_2
echo  "`r`n"

echo ("json_3")
ConvertFrom-Json $json_3

ただ実際変数に設定されている値と、その型がわかり、またConvertFrom-Json で失敗するのはどういったケースなのか?
がわかればいいので、書き方は適当です。

テスト実行

それぞれ以下の画像のような変数を設定して実行してみました。

f:id:koruneko:20210217014859p:plain

その際の出力は以下のようになりました。

str:str
 - type:System.String

num:1
 - type:System.Int32

json:{ "name": "korune" }
 - type:System.String

str_1:a
 - type:System.String
str_2:b
 - type:System.String

num_1:1
 - type:System.String
num_2:2
 - type:System.Int32

json_1:{ "name": "korune" }
 - type:System.String
json_2:'{ "name": "korune" }'
 - type:System.String
json_3:[{ "name": "korune" }]
 - type:System.String


---ConvertFrom-Json----
json:@{name=korune}
 - type:System.Management.Automation.PSCustomObject

json_1:@{name=korune}
 - type:System.Management.Automation.PSCustomObject

json_2:{ "name": "korune" }
 - type:System.String

json_3:
 - type:System.Object[]


---ConvertFrom-Json(not in param)----
json

name  
----  
korune

json_1

name  
----  
korune

json_2
{ "name": "korune" }

json_3

Length         : 1
LongLength     : 1
Rank           : 1
SyncRoot       : {@{name=korune}}
IsReadOnly     : False
IsFixedSize    : True
IsSynchronized : False
Count          : 1

番号(_X)が変数の末についていないものは、変数の値をシェル内で定義しています。
変数の末が_1のものは、Param() にて型宣言を行わずに変数を定義しています。
変数の末が_2のものは、Param() にて型宣言を行って変数を定義しています。
* JSON 除く

JSON の型宣言を行っていないのはJSON 型というものが存在しないためです。

ちなみにPowerShell で利用可能な型は以下になります。

Type Name
[array] [System.Array]
[bool] [System.Boolean]
[byte] [System.Byte]
[char] [System.Char]
[datetime] [System.DateTime]
[decimal] [System.Decimal]
[double] [System.Double]
[guid] [System.Guid]
[hashtable] [System.Collections.Hashtable]
[int16] [System.Int16]
[int32][int] [System.Int32]
[int64][long] [System.Int64]
[nullable] [System.Nullable]
[psobject] [System.Management.Automation.PSObject]
[regex] [System.Text.RegularExpressions.Regex]
[sbyte] [System.SByte]
[scriptblock] [System.Management.Automation.ScriptBlock]
[single][float] [System.Single]
[string] [System.String]
[switch] [System.Management.Automation.SwitchParameter]
[timespan] [System.TimeSpan]
[type] [System.Type]
[uint16] [System.UInt16]
[uint32] [System.UInt32]
[uint64] [System.UInt64]
[xml] [System.Xml.XmlDocument]

出力結果を纏めると以下のようになります。

設定値と型の関係

変数名 備考 設定値
str シェル内定義 str System.String
str_1 型宣言なし a System.String
str_2 型宣言あり[string] b System.String
num シェル内定義 1 System.Int32
num_1 型宣言なし 1 System.String
num_2 型宣言あり[int] 2 System.Int32
json シェル内定義 { "name": "korune" } System.String
json_1 型宣言なし { "name": "korune" } System.String
json_2 型宣言なし '{ "name": "korune" }' System.String
json_3 型宣言なし [{ "name": "korune" }] System.String

ConvertFrom-Json の結果

変数名 設定値 出力
json { "name": "korune" } name
----
korune
System.Management.Automation.PSCustomObject
json_1 { "name": "korune" } name
----
korune
System.Management.Automation.PSCustomObject
json_2 '{ "name": "korune" }' { "name": "korune" } System.String
json_3 [{ "name": "korune" }] Length : 1
LongLength : 1
Rank : 1
SyncRoot : {@{name=korune}}
IsReadOnly : False
IsFixedSize : True
IsSynchronized : False
Count : 1
System.Object[]

ConvertFrom-Json の結果はドキュメントを確認すると、 System.Management.Automation.PSCustomObject となっている。
ConvertFrom-Json

json_2json_3 の結果は System.Management.Automation.PSCustomObject ではないが、何故エラーになっていない?
私の知識が足りないので、ここはもう少し調査を行う必要がある。。。

Power Automate からPowerShell Runbook に値を渡してみる

Power Automate からPowerShell Runbook に値を渡すようフローを作成します。
渡す値は先ほどと同じ値を渡したいと思います。

その結果が以下になります。

f:id:koruneko:20210222014457p:plain

json_2 にて "有効な JSON を入力してください。" とでているのはPowerShell 内で ConvertFrom-Json を利用しているのでその解析結果によるものかと思いますが、 str_1 で同様のエラーがでているのは何故??
Object 型から string 型への変換に失敗してこのエラーがでている?? こちらの公式ドキュメントのRunbook の入力パラメーターを構成する - Runbook に JSON オブジェクトを渡すをみてみると、

JSON データを受け入れるには、Runbook は、入力パラメーターとしてオブジェクトを受け取る必要があります

と記載されています。

これは私の推測なので詳細はわかりませんが、この"ジョブの作成"コネクタのエラーハンドリングが適切ではないのでは??と推測しました。
ここは中のソースを確認するなりサポートに確認するなりしないとわからないですね。

必須項目にはしていないので、該当項目を空白にして保存を行ったところ以下エラーが出力されました。

f:id:koruneko:20210222015746p:plain

フローの保存がコード 'OpenApiOperationParameterValidationFailed' およびメッセージ '入力パラメーター 'body' の検証がワークフロー操作 'ジョブの作成' で失敗しました: 種類/形式 'Integer' のパス 'body/properties/parameters/num_1' にある値 '1' のパラメーターは、種類/形式 'Object' に変換できません。' で失敗しました。

先ほどRunbook のテストを行うときにパラメータ入力のスクショを貼り付けましたが、その際、型の宣言を行わなかった変数の型は 'Object' と表示されていたかと思います。

f:id:koruneko:20210222020308p:plain

このことから型のデフォルト設定値は Object だということがわかりますが、公式ドキュメントみるとそもそもPowerShell Runbook の入力パラメーターの Type は必須になっていて、デフォルト値がなにか記載とかないんですよね...
Runbook の入力パラメーターを構成する - PowerShell Runbook の入力パラメーターを構成する

まぁ、ちゃんと型宣言しろよ。ってことかもしれませんが、これPowerShell Runbook 側でエラーとかださなくていいんですかね?もしくはドキュメントの記載を変えるか。
PowerShell の構文としては問題ないので、ドキュメントの記載方法変えたほうがいいような...と思いますが、私がなにか勘違いしているのでしょうかね。

とりあえずこのことからも、型の宣言はきちんと行っておくべきだということがわかりますね。

対象項目を空白にしましたが、問題はまだあります。

f:id:koruneko:20210222021114p:plain

フローの保存がコード 'OpenApiOperationParameterValidationFailed' およびメッセージ '入力パラメーター 'body' の検証がワークフロー操作 'ジョブの作成' で失敗しました: API 操作 'CreateJob' では、プロパティ 'body/properties/parameters/json_3' の種類を 'Object' にする必要がありますが、種類が 'Array' です。' で失敗しました。

指摘の通り、 [{ "name": "korune" }] は配列なのでエラーがでています。
一旦上の3つの項目、

  • str_2
  • num_2
  • json_1

だけを設定して実行してみます。

その結果が以下になります。

str:str
 - type:System.String

num:1
 - type:System.Int32

json:{ "name": "korune" }
 - type:System.String

str_1:
str_2:b
 - type:System.String

num_1:
num_2:1
 - type:System.Int32

json_1:@{name=korune}
 - type:System.Management.Automation.PSCustomObject
json_2:
json_3:

---ConvertFrom-Json----
json:@{name=korune}
 - type:System.Management.Automation.PSCustomObject

json_1:

json_2:

json_3:

---ConvertFrom-Json(not in param)----
json

name  
----  
korune


json_1

json_2

json_3

ConvertTo-Json できてないじゃん...

入力値を確認してみます。

f:id:koruneko:20210222022040p:plain

入力値としては想定通りの値が渡ってきています。

ログをみてみます。

f:id:koruneko:20210222022306p:plain

f:id:koruneko:20210222022330p:plain

Invalid JSON primitive: .

つまりJSON じゃないといわれていますね。

出力結果をみてみると、 json_1 は以下のように出力されています。

json_1:@{name=korune}
 - type:System.Management.Automation.PSCustomObject

このことから、どうやらPower Automate からPowerShell Runbook へJSON を渡したときは ConvertTo-Json が行われた結果が変数に設定されるようです。

これは、shibatea さんがMicrosoft 365 ユーザーアカウントの作成を自動化するのコメントでも記載してくれている通り、Power Automate の"ジョブの作成"のRunbook のパラメーターの型は dynamic (動的) なので、渡したパラメーターを自動的に型解決してくれているのだと思います。

Azure Automation - Connectors | Microsoft Docs

このことから考えてもちゃんと型宣言しておくべきですね。

おわりに

以上、PowerShell Runbook の入力パラメータの型検証結果でした。

私の推測より記載している部分が多々ありますので、あくまでも参考程度にしていただければと思います。
また、誤っている情報がありましたら、指摘していただけると助かります。

【GPPB Virtual Bootcamp 2021 Sapporo】30分で作成!?解説しながら作る横スクアクションゲーム

はじめに

この記事はGPPB Virtual Bootcamp 2021 Sapporo での私のセッション【30分で作成!?解説しながら作る横スクアクションゲーム】の登壇資料です。

本イベントの動画は下記にて公開されておりますので併せてご確認ください!

当日配信URL
こちら よりご確認ください

チャンネルURL

www.youtube.com

横スクアクションゲームの作成

概要

今回作成するゲームは、強制横スクロールを行い、画面をタップするとプレイヤーがジャンプして障害物を避けるというゲームです。

アニメーション部分にはSVG を利用します。
これはPower Apps のコントロールを駆使してアニメーションの描画を行うことも可能ではありますが、どうしてもアイテム数が増えてくると動作がもっさりしてしまい、ゲームをプレイしたときの爽快感が薄れてしまうからです。

ステージを作成する

ステージは画面がスクロールすることによって変化するのでSVG を利用して描画します。

まずは今回利用するSVG の基本形となる画面に四角形の図形を表示するためのSVG をコピペして作成します。
画像コントロールは画面領域いっぱいに表示されるようにします。

StageImage.Image

"data:image/svg+xml,"& 
EncodeUrl(
    "<svg viewBox='0 0 "& Self.Width & " " & Self.Height & "' xmlns='http://www.w3.org/2000/svg'>
        <rect 
            x='"& (Self.Width - 200) / 2 & "' 
            y='" & (Self.Height - 200) / 2 & "' 
            width='200' 
            height='200' 
        />
    </svg>"
)

これで画面中央に黒色の四角形が表示されたかと思います。
これが今回作成するステージの基本形となります。

続いて、ステージが"どこに"、"どれだけの大きさで"存在するかを表した情報を作成します。
この情報は stage というコレクションに設定したいと思います。

開発を簡単にするために、 initButton というボタンを作成して、そのボタンが押されたときに各種変数類が初期化されるようにしておきます。

initButton.OnsSelect

ClearCollect(
    stage,
    {x:0, y:Parent.Height - 150, width:Parent.Width, height:150}
)

initButton.OnsSelect = Screen1.OnVisible となります。
開発が完了したらこのボタンは削除し、設定した式は OnVisibel に記載することにします。

上記の式でステージの初期情報を作成することができました。
なのでステージの見た目もこのコレクションの内容と同じものを表示させるようにしたいと思います。

これを実現するために、コレクションのレコード情報をForAll 関数で取得し、その結果をConcat 関数で文字列の結合を行うようにします。

StageImage.Image

"data:image/svg+xml,"& 
EncodeUrl(
    "<svg viewBox='0 0 "& Self.Width & " " & Self.Height & "' xmlns='http://www.w3.org/2000/svg'>" &
        Concat(
            ForAll(
                stage,
                "<rect 
                    x='" & x &"' 
                    y='" & y &"' 
                    width='" & width &"' 
                    height='" & height &"' 
                />"
            ),
            Value
        ) & "
    </svg>"
)

これで、画面下部にスクリーンの幅いっぱいで高さ150の四角形を作成することができました。

これでステージの作成は完了です。

プレイヤーを表示する

プレイヤーもSVG で表示させます。

今回プレイヤーは複雑なアニメーションをさせないので、普通に画像を読み込んで表示させてもよかったのですが、画像の引用元を明らかにしておきたかった( <metadata> にて引用元を記載しています。)のと、機能の追加でプレイヤーにもアニメーションを適用(ジャンプなどで画像が傾くなど)することを想定してSVG で表示させています。

大きさ100の画像コントロールを追加して、以下を設定します。

PlayerImage.Image

"data:image/svg+xml,"& 
EncodeUrl(
    "<svg version='1.0' xmlns='http://www.w3.org/2000/svg'
 width='1198.000000pt' height='1280.000000pt' viewBox='0 0 1198.000000 1280.000000'
 preserveAspectRatio='xMidYMid meet'>
<metadata>
Created by potrace 1.15, written by Peter Selinger 2001-2017
'https://svgsilh.com/ja/image/2029531.html'
</metadata>
    <g
     transform='matrix(-0.1,0,0,-0.1,1198.3267,1280) rotate(0 0 0)'
     fill='#000000'
     stroke='none'
     >
    <path
       d='m 8235,12784 c -140,-31 -244,-82 -481,-236 -180,-117 -202,-120 -244,-34 -25,54 -36,64 -75,68 -26,3 -31,-1 -46,-42 -24,-60 -23,-103 2,-151 33,-65 27,-102 -31,-211 -28,-54 -61,-105 -73,-113 -12,-8 -39,-15 -60,-15 -42,0 -77,28 -109,88 -34,63 -108,29 -138,-64 -16,-46 -9,-85 29,-172 17,-39 31,-77 31,-84 0,-8 -13,-27 -30,-43 -34,-32 -38,-60 -14,-94 14,-20 23,-22 56,-18 64,9 103,-9 181,-82 40,-37 101,-86 134,-109 111,-76 138,-136 128,-294 l -6,-106 30,-34 c 16,-19 69,-58 118,-88 111,-68 192,-144 232,-220 57,-107 101,-127 206,-94 48,15 55,15 110,0 62,-19 115,-61 115,-94 0,-33 -36,-67 -111,-107 -81,-42 -109,-67 -109,-94 0,-29 47,-79 107,-113 29,-16 53,-36 53,-44 0,-8 -21,-24 -47,-35 -26,-11 -79,-44 -118,-73 -38,-29 -91,-63 -117,-75 -47,-22 -70,-51 -123,-156 -31,-60 -74,-97 -171,-147 -113,-58 -191,-88 -340,-133 -179,-55 -361,-129 -544,-223 -373,-192 -431,-217 -596,-251 -197,-41 -395,-50 -689,-30 -179,12 -210,20 -230,60 -24,46 -18,73 37,155 75,112 127,164 166,164 35,0 62,22 62,51 0,24 -43,69 -65,69 -47,0 -149,-81 -330,-259 -178,-177 -193,-189 -242,-201 -90,-21 -126,-8 -209,74 -41,40 -76,84 -80,100 -9,36 3,84 55,218 58,151 75,241 68,358 -7,118 -33,298 -53,367 -19,70 -137,227 -269,359 -81,81 -116,108 -182,141 -136,68 -216,68 -372,-1 -42,-18 -110,-42 -151,-52 -41,-11 -120,-43 -175,-72 -55,-29 -150,-77 -210,-107 -134,-66 -186,-108 -220,-180 -14,-30 -30,-55 -35,-55 -21,0 -40,-34 -40,-73 0,-40 -41,-175 -90,-299 -22,-57 -23,-58 -6,-163 9,-58 19,-147 23,-198 7,-101 -3,-85 106,-179 35,-30 39,-39 47,-102 10,-80 33,-118 78,-127 71,-14 100,-51 121,-154 25,-119 66,-162 231,-245 95,-47 190,-112 235,-160 42,-44 96,-70 149,-70 37,0 53,7 103,46 136,106 180,124 254,104 22,-6 60,-28 82,-49 23,-21 62,-51 87,-68 111,-75 171,-173 197,-321 14,-75 14,-111 4,-257 -7,-93 -16,-240 -21,-325 -11,-184 -32,-274 -95,-408 -61,-128 -158,-284 -200,-321 -19,-16 -51,-33 -72,-37 -56,-10 -562,4 -608,17 -37,11 -51,23 -143,126 -43,47 -127,69 -201,53 -53,-12 -62,-19 -191,-150 -111,-113 -144,-140 -185,-155 -108,-38 -173,-78 -273,-166 -57,-50 -112,-105 -123,-123 -23,-36 -94,-261 -103,-324 -20,-136 61,-298 198,-399 77,-57 123,-80 263,-133 160,-59 203,-70 336,-80 65,-5 125,-14 133,-20 34,-29 20,-90 -103,-465 -118,-361 -196,-542 -251,-585 -34,-27 -60,-25 -187,15 -556,178 -1137,86 -1668,-263 C 1007,4101 893,4007 757,3872 372,3491 121,3016 29,2493 -4,2312 -4,1981 28,1806 132,1242 424,797 956,397 1163,242 1248,197 1481,122 1706,49 1914,13 2165,3 c 471,-18 950,108 1347,354 500,309 858,785 997,1326 54,208 66,317 62,567 -2,124 -4,270 -5,325 l -1,101 38,51 c 42,58 60,105 77,208 24,138 88,211 208,236 144,29 338,-74 409,-219 23,-47 28,-70 28,-132 0,-87 -22,-138 -79,-187 -39,-35 -74,-48 -164,-62 -163,-26 -196,-107 -138,-341 44,-177 35,-221 -65,-328 -64,-68 -98,-129 -99,-173 0,-73 52,-128 108,-114 39,10 275,129 340,172 31,21 88,68 127,104 73,69 102,89 131,89 9,0 55,-17 103,-39 217,-97 426,-119 926,-97 278,12 281,10 305,-149 34,-227 195,-552 400,-810 203,-254 491,-492 770,-634 465,-237 1026,-301 1556,-176 706,166 1315,672 1597,1325 120,278 166,489 174,796 6,220 -4,339 -42,522 -126,596 -520,1146 -1070,1493 -119,74 -326,177 -436,215 -44,15 -85,34 -90,41 -8,9 -8,45 1,130 37,342 -51,701 -238,967 -33,48 -55,90 -55,105 4,159 17,254 89,636 15,80 45,145 80,176 11,9 62,40 114,69 52,28 169,101 260,162 269,181 287,190 710,350 856,323 974,379 1030,489 17,33 20,59 20,186 -1,617 -262,1099 -830,1532 -217,165 -443,308 -653,412 -127,64 -151,85 -127,114 25,30 109,18 333,-50 360,-107 503,-135 701,-135 201,0 268,29 302,133 24,72 40,94 81,108 93,30 232,-9 352,-100 30,-22 62,-41 72,-41 10,0 27,12 39,28 19,23 22,35 17,92 -11,125 -4,123 -329,118 -296,-4 -303,-6 -389,-98 -55,-59 -57,-60 -109,-60 -112,0 -617,99 -791,156 -62,20 -104,64 -95,99 4,15 18,38 31,51 23,23 29,24 178,24 153,0 155,0 278,41 68,22 159,56 201,75 101,44 307,175 363,231 56,55 110,162 100,195 -5,12 -38,55 -76,93 -45,47 -81,97 -108,150 -24,48 -65,106 -102,145 -33,36 -68,79 -77,95 -22,43 -31,102 -21,140 13,47 132,154 211,191 329,153 445,236 554,399 39,58 52,125 35,178 -19,57 -75,62 -101,9 -62,-123 -199,-273 -326,-355 -39,-25 -112,-62 -163,-83 -167,-67 -288,-169 -352,-299 -55,-112 -50,-191 15,-277 47,-61 73,-115 97,-205 27,-101 31,-107 100,-180 67,-70 73,-96 32,-136 -40,-38 -114,-68 -328,-132 -105,-31 -252,-81 -327,-111 -76,-30 -142,-54 -147,-54 -29,0 -157,-52 -193,-79 -52,-39 -89,-40 -117,-5 -31,40 -27,91 14,170 38,73 44,117 19,152 -23,33 -58,27 -140,-24 -91,-56 -106,-74 -172,-201 -93,-175 -113,-180 -223,-45 -40,48 -128,144 -196,213 -145,148 -147,155 -74,217 27,23 52,51 56,62 9,29 -12,60 -45,67 -46,9 -64,37 -70,107 -5,56 -7,61 -30,64 -20,2 -45,-15 -116,-81 -50,-46 -103,-87 -117,-91 -95,-24 -183,117 -194,311 -10,160 36,224 223,321 56,28 107,59 115,68 26,31 91,54 157,54 78,0 118,18 172,76 33,36 41,51 38,77 -3,32 -3,32 -66,35 -81,4 -87,18 -33,77 56,61 55,85 -9,131 -27,20 -50,42 -50,49 0,8 29,28 64,45 70,33 99,68 111,131 14,77 -42,179 -98,179 -13,0 -52,-19 -86,-42 -79,-53 -134,-58 -179,-15 -16,15 -34,38 -40,50 -6,12 -35,74 -65,138 -46,97 -65,124 -115,170 -67,62 -183,126 -287,159 -93,29 -254,36 -350,14 z m -96,-3551 c 95,-47 123,-115 102,-238 -27,-158 -35,-397 -23,-667 42,-888 211,-1647 460,-2061 89,-147 90,-148 81,-502 -4,-160 5,-221 93,-580 47,-189 52,-222 52,-326 1,-112 0,-117 -29,-165 -43,-69 -112,-114 -176,-114 -28,0 -64,6 -82,14 -40,16 -95,77 -103,114 -21,99 -36,105 -263,112 -239,7 -235,5 -309,155 -29,60 -79,168 -109,239 -30,72 -92,217 -138,321 -46,105 -90,212 -99,238 -36,106 -12,154 89,182 33,9 101,35 150,57 141,63 180,56 231,-44 26,-52 41,-58 72,-30 43,38 56,153 23,208 -24,40 -86,70 -183,88 -135,26 -151,51 -117,178 38,143 48,210 49,315 0,177 -44,321 -146,476 -30,44 -54,86 -54,93 0,6 8,22 19,35 15,19 27,23 72,22 79,-2 91,3 87,39 -3,28 -8,32 -73,52 -169,54 -186,88 -117,232 33,71 37,87 37,159 -1,93 -15,142 -51,176 -23,22 -36,24 -152,29 -126,6 -128,6 -212,50 -125,65 -349,219 -562,384 -103,80 -229,174 -280,209 -99,67 -115,92 -82,128 30,33 140,59 359,84 421,49 616,105 923,266 83,44 178,86 210,94 83,21 182,13 251,-22 z M 5565,7315 c 50,-30 121,-68 158,-84 167,-75 293,-170 328,-246 13,-28 19,-64 19,-118 0,-88 17,-139 52,-158 12,-7 52,-15 88,-18 59,-5 65,-8 68,-30 2,-12 1,-34 -2,-47 -6,-24 -110,-109 -217,-177 -31,-20 -94,-68 -140,-107 -46,-39 -122,-94 -169,-122 -47,-28 -107,-72 -133,-97 l -48,-46 -277,-4 c -242,-3 -664,11 -729,25 -40,8 -28,48 50,167 87,133 126,201 282,492 65,121 142,254 173,295 61,83 289,315 324,330 37,15 76,3 173,-55 z M 4218,6678 c 19,-19 14,-108 -9,-178 -69,-210 -241,-446 -372,-509 -38,-18 -62,-21 -183,-21 -190,0 -227,12 -311,96 -35,35 -70,64 -77,64 -7,0 -28,-10 -47,-23 -19,-13 -52,-36 -73,-50 -113,-78 -276,18 -276,161 0,64 23,106 97,184 156,161 356,250 623,277 138,14 614,13 628,-1 z M 2740,6375 c 8,-10 10,-31 5,-64 -14,-100 37,-225 127,-313 60,-59 126,-90 257,-124 97,-25 108,-26 176,-14 63,11 76,10 103,-4 65,-34 71,-99 14,-156 -51,-51 -101,-64 -227,-57 -241,14 -465,94 -565,204 -99,108 -118,234 -61,406 28,83 38,102 65,118 37,23 89,25 106,4 z m 1700,-510 c 36,-19 44,-48 36,-119 -7,-51 -5,-57 23,-88 28,-32 33,-33 113,-36 46,-2 132,3 191,12 59,9 132,16 163,16 65,0 118,20 134,51 8,15 11,45 7,86 -8,81 3,88 117,79 158,-13 162,-39 32,-234 -102,-154 -123,-208 -111,-290 14,-100 82,-207 178,-280 47,-36 145,-93 312,-180 17,-9 52,-17 80,-18 65,-4 75,-19 75,-110 0,-107 -17,-169 -70,-264 -76,-133 -100,-249 -120,-565 -6,-99 -18,-211 -26,-250 -8,-38 -28,-140 -44,-225 -51,-266 -68,-317 -109,-340 -32,-16 -39,-13 -94,47 -76,85 -192,205 -812,848 -674,698 -684,709 -816,858 -57,66 -124,141 -148,168 -24,28 -43,59 -43,72 3,101 21,257 31,277 37,69 77,90 196,100 63,5 106,15 136,30 85,43 288,231 324,299 12,24 49,46 105,65 33,10 113,5 140,-9 z m 2955,-57 c 33,-15 54,-51 103,-173 63,-156 162,-375 251,-552 44,-90 81,-170 81,-178 0,-9 7,-29 14,-44 13,-25 13,-31 0,-50 -12,-17 -25,-21 -68,-21 -76,0 -198,34 -291,80 -98,49 -371,243 -380,270 -4,13 5,44 24,84 17,34 52,118 77,187 25,68 56,145 70,172 19,38 24,64 24,121 0,59 4,76 20,94 24,25 38,27 75,10 z m -275,-88 c 7,-44 -24,-190 -60,-292 -45,-125 -124,-228 -173,-228 -11,0 -46,18 -78,39 -67,45 -146,77 -236,96 -34,7 -66,16 -72,20 -14,8 -14,60 -1,85 12,22 68,40 126,40 64,0 146,48 289,170 116,98 134,109 165,107 32,-2 35,-5 40,-37 z M 3495,4934 c 9,-4 30,-34 47,-68 57,-114 110,-178 352,-429 350,-363 465,-484 614,-649 73,-81 162,-173 198,-205 63,-55 134,-154 134,-186 0,-20 -104,-56 -157,-54 -50,2 -87,-27 -97,-75 -8,-40 -85,-118 -117,-118 -40,0 -144,120 -233,269 -174,290 -395,502 -798,764 -80,52 -154,107 -164,122 -17,26 -16,31 50,248 37,122 73,238 78,257 13,44 36,90 56,113 17,20 15,19 37,11 z m 3148,-476 c 87,-18 86,-36 -27,-301 -91,-216 -197,-511 -242,-675 -19,-69 -48,-158 -63,-197 -15,-38 -31,-88 -35,-110 -11,-57 -50,-120 -93,-146 -28,-17 -56,-24 -118,-27 -106,-6 -130,5 -121,56 10,67 103,233 191,346 47,60 102,143 122,185 20,42 56,103 80,136 57,78 99,165 119,251 21,86 23,278 5,392 -18,108 -11,117 74,107 33,-3 81,-11 108,-17 z m 464,-33 c 158,-20 288,-48 425,-91 224,-71 224,-105 3,-269 -158,-117 -254,-205 -362,-330 -218,-253 -360,-529 -414,-806 -10,-55 -19,-115 -19,-134 0,-67 -48,-82 -137,-41 -38,18 -59,36 -79,68 -14,24 -44,60 -66,80 l -40,38 4,67 c 3,43 16,100 36,153 17,47 54,157 83,245 28,88 78,219 110,292 32,73 78,204 104,295 66,233 125,410 143,431 19,21 55,22 209,2 z m 1981,-147 c 6,-7 26,-87 46,-178 30,-143 60,-296 158,-810 25,-135 27,-281 4,-335 -31,-72 -68,-115 -155,-178 -74,-54 -92,-62 -128,-61 -53,1 -70,15 -83,68 -30,118 -69,227 -135,376 -40,91 -105,240 -144,333 l -71,167 v 299 c 0,259 2,302 16,315 13,14 50,16 248,16 175,0 236,-3 244,-12 z m -6448,-91 c 166,-36 224,-57 228,-84 2,-11 -13,-54 -33,-93 -20,-40 -52,-118 -72,-174 -19,-55 -96,-234 -170,-396 -75,-162 -155,-339 -178,-393 -24,-53 -70,-143 -104,-200 -33,-56 -78,-136 -99,-177 l -39,-75 -81,-5 c -61,-4 -86,-10 -103,-25 -32,-29 -49,-77 -49,-140 0,-48 4,-59 34,-93 19,-20 39,-56 45,-80 22,-81 -29,-76 801,-85 464,-5 847,-4 1040,2 311,11 379,7 408,-23 37,-37 0,-310 -69,-517 C 4017,1076 3551,616 2970,415 2735,334 2555,305 2290,306 2012,307 1795,349 1555,447 685,802 184,1742 371,2664 c 56,277 196,573 377,801 297,372 736,640 1187,724 157,29 159,29 385,26 173,-3 223,-7 320,-28 z m 7237,-58 c 471,-244 828,-639 1043,-1154 47,-115 140,-399 140,-431 0,-12 -8,-32 -19,-45 -26,-32 -132,-45 -506,-60 -274,-11 -309,-10 -410,6 -60,10 -144,20 -185,22 -46,3 -81,11 -92,20 -40,35 -118,304 -145,497 -23,164 -27,1153 -4,1179 24,30 75,20 178,-34 z m -1837,-79 c 27,-15 328,-420 399,-538 138,-230 315,-580 367,-728 16,-45 16,-55 4,-85 -19,-44 -84,-109 -134,-133 -35,-16 -49,-17 -145,-7 -58,6 -187,15 -286,21 -210,12 -269,19 -545,65 -113,19 -259,41 -325,50 -271,35 -337,85 -301,224 18,69 137,315 216,445 66,110 178,261 259,348 107,116 291,261 416,327 45,24 50,25 75,11 z M 3307,3944 c 168,-101 275,-184 423,-333 186,-187 304,-359 406,-591 134,-304 200,-614 144,-670 -19,-19 -33,-20 -522,-19 -848,2 -1289,12 -1315,29 -43,28 -47,60 -20,141 68,202 322,738 612,1290 103,195 118,219 145,219 9,0 67,-30 127,-66 z M 6088,2870 c 69,-42 75,-110 27,-306 -25,-107 -41,-129 -101,-140 -76,-14 -180,60 -204,145 -18,64 -24,187 -10,220 11,28 56,53 170,94 33,12 87,6 118,-13 z m 345,-121 c 50,-26 67,-51 67,-99 0,-137 -35,-200 -111,-200 -78,0 -129,37 -129,92 0,40 47,168 71,194 25,26 66,31 102,13 z m 864,-129 c 333,-65 546,-97 838,-125 84,-8 159,-17 168,-21 10,-3 17,-16 17,-29 0,-50 8,-50 -594,-50 -503,0 -569,2 -636,18 -108,25 -105,22 -105,123 1,78 3,88 24,105 32,26 52,25 288,-21 z m 2333,-316 c 86,-12 221,-15 754,-13 619,2 649,2 658,-16 23,-41 -5,-255 -62,-475 -219,-847 -887,-1437 -1732,-1529 -458,-50 -935,70 -1345,338 -249,163 -433,344 -586,576 -98,149 -267,515 -267,579 0,35 31,64 77,71 21,3 92,23 158,45 163,54 1182,304 1563,384 67,14 155,26 195,27 39,0 117,9 172,19 113,22 236,20 415,-6 z m -3095,-34 c 20,-19 25,-33 25,-74 0,-49 -2,-52 -62,-109 -92,-87 -188,-156 -226,-163 -20,-4 -49,0 -75,10 -29,11 -71,15 -134,14 -81,-1 -98,2 -147,26 -80,39 -118,95 -83,124 22,19 381,154 472,178 114,31 193,28 230,-6 z m 1589,14 c 24,-9 19,-51 -8,-69 -13,-9 -75,-27 -137,-40 -63,-13 -222,-50 -354,-84 -256,-64 -452,-105 -551,-116 -46,-5 -63,-3 -72,8 -7,8 -17,59 -22,113 -13,130 -13,143 5,171 l 15,23 h 554 c 305,0 561,-3 570,-6 z M 6741,2184 c 8,-9 15,-51 17,-103 4,-82 3,-88 -22,-118 -55,-66 -129,-63 -169,5 -18,31 -18,35 -3,75 19,51 68,113 110,138 38,23 50,24 67,3 z'/>
</g>
</svg>"
)

以上でプレイヤーの表示は完了です。

重力を作成する

重力を作成して、プレイヤーが時間経過によって、下に落ちるようにします。

今回時間経過処理にはスライダーを用います。
ゲーム実行中はスライダーコントロールOnChange が呼ばれるようにしてループ処理を実現します。

なので、スライダーコントロールを追加してください。

まずは変数を初期化します。
今回追加する初期化変数は、"プレイヤーの座標" PlayerX, PlayerY 、"スライダーの値" sliderVal 、"ゲーム開始を判定する変数" playGame です。

InitButton.OnSelect

UpdateContext({PlayerX:50, PlayerY:470});
UpdateContext({sliderVal:0});
UpdateContext({playGame:false});

PlayerX, PlayerYPlayerImageXY にそれぞれ設定しておき、 sliderVal はスライダーコントロールDefault に設定しておきましょう。

ゲームの開始はStart ボタンが押されたら開始することにします。
ボタンを追加して以下のように設定します。

StartButton.OnSelect

If(
    playGame,
    UpdateContext({playGame:false});
    Reset(Slider1),
    UpdateContext({sliderVal:sliderVal + 1});
    UpdateContext({playGame:true})
)

ゲームがまだ開始していない場合は、スライダーの OnChange を呼び出すようにして、ゲームが開始している場合にボタンが押されたときはゲームを中断するようにしています。

わかりやすいよう、ボタンに表示される文字も変更しておきましょう。

StartButton.Text

If(!playGame,"Start","Stop")

最後に重力を作成します。
重力 = プレイヤーが下方向に移動する。
なので、 OnChange 処理で PlayerY の値を増やしてあげればよいです。

Slider1.OnChange

If(
    playGame,
    Reset(Slider1);
    UpdateContext({PlayerY:PlayerY + 10});
)

これで重力を実装することができました。
ただこのままでは、プレイヤーがステージを貫通して落ちてしまうので、プレイヤーとステージとの当たり判定を作成します。

プレイヤーとステージとの間に当たり判定を実装する

プレイヤーとステージとの当たり判定にはコントロール名、切り替えのトグルを利用します。

このトグルの値が True の場合がプレイヤーとステージは接触していると判定することにしたいと思います。

当たり判定は以下のようにPlayer とStage を定義したとき

f:id:koruneko:20210214163618p:plain

以下のような式であらわすことができます。

((PlayerX > StageX && PlayerX < StageX + StageWidth) ||
(StageX > PlayerX && StageX < PlayerX + PlayerWidth))  &&
((PlayerY > StageY && PlayerY < StageY + StageHeight) ||
(StageY > PlayerY && StageY < PlayerY + PlayerHeight))

まず2つの図形が"重なっている"というのは、下図のようにX成分でみたときもY成分でみたときも重なっている。
というのが条件になります。

f:id:koruneko:20210214164617p:plain

上の式でいいますと、
この式がX成分の重なり判定で

((PlayerX > StageX && PlayerX < StageX + StageWidth) ||
(StageX > PlayerX && StageX < PlayerX + PlayerWidth))

この式がY成分の重なり判定です。

((PlayerY > StageY && PlayerY < StageY + StageHeight) ||
(StageY > PlayerY && StageY < PlayerY + PlayerHeight))

これら2つの条件を満たしているとき、重なっていると判断できるので && でそれぞれの式を繋げているわけです。

ただし、今回この当たり判定を実装するうえで1点注意しなくてはいけないことがあります。

今回当たり判定を実装したいのは、プレイヤーとステージが接しているか?です。
これをもっと細かく定義すると、
"プレイヤーの下部"と"ステージの上部"が接しているか?
です。

なぜ、ここまで細かく定義するか?というと、上式のまま判定を作成してしまうと、プレイヤーがステージの横方向から突っ込んだ場合でもプレイヤーとステージが接している。と判定してしまうことになるからです。
これはこのゲームを作成するうえで意図した実装ではないので、"プレイヤーの下部"と"ステージの上部"が接しているか?で判定式を作成したいと思います。

これらを踏まえ判定式を作成すると以下のようになります。

isGrand.Default

CountIf(
    ForAll(
        stage,
        ((PlayerImage.X > x && PlayerImage.X < x + width) ||
        (x > PlayerImage.X && x < PlayerImage.X + PlayerImage.Width))  &&
        ((PlayerImage.Y + PlayerImage.Height - 5 > y && PlayerImage.Y + PlayerImage.Height - 5 < y + 5) ||
        (y > PlayerImage.Y + PlayerImage.Height - 5 && y < PlayerImage.Y + PlayerImage.Height))
    ),
    Value = true
) > 0

プレイヤー、ステージそれぞれ高さで定義しています。
プレイヤーの場合、Y座標も調整が必要ですので、そのことも式に盛り込んでいます。

また、判定を行うのはコレクションの存在するステージすべてですので ForAll 関数を用いており、うち1つでも True になれば、ステージのどれかにプレイヤーが接しているということなので、 CountIf 関数で0より大きい場合という条件を指定しています。

プレイヤーがステージと接している場合、プレイヤーのY 座標を加算してはいけないので、重力を設定していた式を以下のように編集します。

Slider1.OnChange

If(
    playGame,
    Reset(Slider1);
    If(!isGrand.Value, UpdateContext({PlayerY:PlayerY + 10}));
)

ジャンプ機能を追加する

画面をクリック(タップ)したときにプレイヤーがジャンプするようにしたいと思います。

ジャンプ機能を追加するにあたり、今回は以下の仕組みを使用しようと思います。

  • ジャンプができる条件:プレイヤーが地面にいる場合。
  • 画面が押されたら:ジャンプができる状態だった場合、変数に一定数値(今回は15)を加算する。
  • ループ処理:変数の値が0より大きければ上昇させる。変数の値を減算する。

これらの条件を満たすことにより、

  • 2重ジャンプの禁止
  • プレイヤーが上昇し続けるバグを抑制

することが可能となります。

では早速実装を行っていきます。

まずは変数の宣言です。

ジャンプをしていない状態 = 0 とするので以下のように宣言します。

InitButton.OnSelect

UpdateContext({PlayerJump:0});

画面が押されたときの処理は、ステージの画像を最終的に最前面に持ってくる StageImage.OnSelect に記載します。

StageImage.OnSelect

If(isGrand.Value, UpdateContext({PlayerJump:15}))

ループ処理はスライダーコントロールOnChange に該当するので、以下のように設定します。

Slider1.OnChange

If(
    playGame,
    Reset(Slider1);
    If(!isGrand.Value && PlayerJump < 0, UpdateContext({PlayerY:PlayerY + 10}));
    If(PlayerJump >= 0, UpdateContext({PlayerY:PlayerY - 10, PlayerJump:PlayerJump - 1}));
)

これで先に述べた機能を満たすことができましたので、ジャンプ機能の作成完了です!

横スクロール機能を作成する

横スクロールはプレイヤーの座標を変更するのではなく、ステージをプレイヤーの進行方向とは反対方向に動かすことで横スクロールしているように見せたいと思います。
* 初回移動時だけプレイヤーを動かします。

プレイヤーは画面左から右へ進んでいるようにみせたいので、反対にステージは右から左へ移動させる。つまりx 座標を減らしてあげればよいです。

これは下記のようにすることで実現可能です。

If(PlayerX > Parent.Width / 3 || !isGrand, UpdateIf(stage, true, {x:x - 10}), UpdateContext({PlayerX:PlayerX + 5}));

PlayerX > Parent.Width / 3 || !isGrand これはゲーム開始時はプレイヤーを画面の1 / 3進めるための処理です。
初回地面と接触するまではプレイヤーのx 成分を動かさないようにしています。
これはこのほうが動きが自然にみえるな。と考えたためです。

ただこのまま減らしただけでは初期で宣言したステージの幅より先はなにもないので、ゲームが終了してしまいます。

なのでステージの自動生成も追加したいと思います。

ステージを自動生成するタイミング = ステージとステージ間の溝

になります。

これは最大でもプレイヤーがギリギリジャンプ可能な位置からジャンプして次のステージまで到達可能な幅にしなくてはいけません。

この計算には、

  • プレイヤーのジャンプ力
  • プレイヤーのx 方向への進む速度
  • ステージ間の高さの差

が分かれば、厳密に計算を行おうと思えば行うことも可能ですが、今回は感覚で設定しています。

また、生成されるステージはランダム性を持たせたいので、 Rand 関数を用います。

ステージのy 座標の値は高さによって決定するので、高さの値はWith 関数で先に決定しておきます。

また、ステージがだんだんずれていくので、コレクション内には不要になったレコードがでてきます。
性能面も考えてこの不要なレコードは削除するようにしましょう。
不要なレコードというのは、画面外にでたステージですね。

上記を踏まえて式を追加すると、以下のようになります。

Slider1.OnChange

If(
    playGame,
    Reset(Slider1);
    If(!isGrand.Value && PlayerJump < 0, UpdateContext({PlayerY:PlayerY + 10}));
    If(PlayerJump >= 0, UpdateContext({PlayerY:PlayerY - 10, PlayerJump:PlayerJump - 1}));
    If(PlayerX > Parent.Width / 3 || !isGrand, UpdateIf(stage, true, {x:x - 10}), UpdateContext({PlayerX:PlayerX + 5}));
    If(
        Parent.Width - (Last(stage).x + Last(stage).width) > 200 + Rand() * 50,
        With(
            {H:Last(stage).height + Rand() * 50 * If(Last(stage).height > 130, If(Rand() > 0.5, 1, -1), 1)},
            Collect(
                stage,
                {
                    x:Parent.Width,
                    y:Parent.Height - H,
                    width:200 + Rand() * 200,
                    height:H
                }
            )
        )
    );
    RemoveIf(stage, x + width < 0)
)

高さを設定している箇所は以下です。

H:Last(stage).height + Rand() * 50 * If(Last(stage).height > 130, If(Rand() > 0.5, 1, -1), 1)

この式を日本語にすると、「追加前のステージの高さが130よりも大きかった場合、50%の確率で最大で高さを50増減させます。130よりも小さかった場合は最大50増やします。」となります。

いくらランダムとはいえ画面外にステージが形成されてはいけないからですね。

スコア表示を行う

ゲーム性を高めるためにスコア表示を行おうと思います。

この機能の実装は簡単にユーザーがゲームをプレーしている間だんだん加算される仕組みにしたいと思います。

InitButton.OnSelect

UpdateContext({Score:0});

Slider1.OnChange(ゲーム実行中に処理されるif 文内に作成)

UpdateContext({Score:Score + 50});

この変数の値を画面に表示させてあげましょう。

ScoreLabel.Text

"Score:" & Score

ゲームオーバー画面を作成する

このゲームの終了条件はプレイヤーが画面外へ落ちてしまうことだけで、クリア条件はありません。
よって今回作成するのはゲームオーバー画面のみです。

ゲームオーバーの条件は、プレイヤーが完全に画面外にでたときとしたいと思います。

地面に接しているときの判定と同じように判定にはトグルを利用します。

トグルの Default に以下の式を適用します。

isGameOver.Default

PlayerY > Parent.Height

ゲームオーバーとなった場合、ゲームの実行を止めたいので以下のように設定します。

isGameOver.OnCheck

UpdateContext({playGame:false});
Reset(Slider1)

これでシステム的にはゲームオーバー判定となりましたが、ゲームをプレーしているユーザーにもゲームオーバーとなったことを知らせてあげましょう。

GameOverLabel.Text

"Game Over..."

GameOverLabel.Visible

isGameOver.Value

このラベルを選択したら、再度ゲームがプレーできる状態としたいため以下のように初期化処理を行うようにします。

GameOverLabel.OnSelect

UpdateContext({PlayerX:50, PlayerY:470});
UpdateContext({sliderVal:0});
UpdateContext({playGame:false});
UpdateContext({PlayerJump:0});
UpdateContext({Score:0});
ClearCollect(
    stage,
    {x:0, y:Self.Height - 150, width:Self.Width, height:150}
)

表示順やVisible を調整

最後に不要なコントロールを非表示に設定します。
不要 = ユーザーには表示する必要がないコントロール
ですので、以下コントロールを非表示にします。

  • Slider1 (スライダー)
  • isGrand (切り替え)
  • isGameOver (切り替え)

また、開発用に作成していた initButtonOnSelect の値を Screen1.OnVisible に移して削除しておきます。

また、コントロールはそれぞれ以下の順序になるように設定します。(値が小さいものが前面)

  1. GameOverLabel
  2. StartButton
  3. StageImage

他コントロールの順序はこれより背面に設定されているのであれば、特に気にしなくて大丈夫です。

以上で横スクアクションゲームの作成完了です!!

おわりに

もし作成してみてわからなかったことや、こんな機能追加したいがどうすればできるか?
などありましたらご気軽にお尋ねくださいー。

Microsoft 365 ユーザーアカウントの作成を自動化する

はじめに

今回は、Microsoft 365 ユーザーアカウントの作成の自動化方法を纏めたいと思います。

ケースとしては、新入社員や中途社員が増えた時や派遣社員の方やBPの方がプロジェクトに参入したときなどにMicrosoft 365 ユーザーアカウントを発行しなくてはならない。
などがあるかと思います。

よって今回のシナリオは以下となるように作成します。

  1. ユーザーの作成を依頼
  2. その依頼を担当者が承認
  3. Microfot 365 ユーザーアカウントを作成
  4. 依頼を行った相手に作成したアカウント情報を払い出し

を想定して作成します。

ユーザー作成依頼フォームを作成する

ユーザー作成を依頼するためのフォームの作成方法として、今回のケースで最も単純で簡単なものは Microsoft Forms かと思います。
ただMicrosoft Forms ですと、ユーザーが入力項目を動的に増やすということを行うことができません。
なので今回はユーザー作成依頼フォームをPower Apps で作成してみようと思います。

* この記事では私がPower Apps が好きなので入力フォームをPower Apps で作成することにしていますが、もちろんMicrosoft Forms でも十分運用可能なレベルかと思います。
  実際に運用される際は必要な機能や作成にかかる労力などから費用対効果を十分に考えたうえで作成することをお勧めします。

Power Apps でユーザー作成フォームを作成する

作成する入力フォームは以下のようなイメージです。

f:id:koruneko:20210131151838p:plain

+ ボタンを押すと以下のように入力フォームが増えて、ごみ箱のアイコンを押すと対象の入力フォームが消えます。
Request ボタンは全ての入力項目が入力されていないと押せない仕様にしています。

f:id:koruneko:20210131152036p:plain

今回このフォームの作成はメインでないため簡単に説明します。

まず入力フォームとなるギャラリーですが、以下の要素で構成されています。

f:id:koruneko:20210131152208p:plain

このコレクションに設定する ItemsApp.OnStart で定義しています。

App.OnStart

ClearCollect(
    userRequest,
    {
        ID:1,
        DisplayName:"",
        UserPrincipalName:"",
        FirstName:"",
        LastName:""
    }
)

ギャラリー内に設置されたテキストボックスは、コレクション userRequest と同期させたいです。
なのでテキストボックスの値が変更されたら都度それぞれ対応する項目を更新するようにします。

TextInput.OnChange

UpdateIf(userRequest, ID = ThisItem.ID, {LastName:Self.Text})

続いて削除アイコンです。
これは

  • 対象の項目のみを削除
  • 入力項目が1つしかない場合は非表示

としたいです。
なので以下のように設定します。

DeleteIcon.OnSelect

Remove(userRequest, ThisItem)

DeleteIcon.Visible

CountRows(userRequest) > 1

これでギャラリー内の要素の作成は完了です。

続いて入力項目を増やすためのボタンの作成です。
これはコレクションを増やすだけでよいので、以下のように設定します。

AddIcon.OnSelect

Collect(
    userRequest,
    {
        ID:Last(userRequest).ID + 1,
        DisplayName:"",
        UserPrincipalName:"",
        FirstName:"",
        LastName:""
    }
)

最後にリクエストボタンを作成します。
これには以下機能を実施します。

  • 入力項目が全て入力されていないときは非活性
  • ボタンが選択されると申請を行うフローを起動して、完了画面へ遷移

「入力項目が全て入力されていないときは非活性」というのは以下で実現できます。

RequestButton.DisplayMode

If(
    CountIf(
        ForAll(
            userRequest,
            IsBlank(DisplayName) || IsBlank(UserPrincipalName) || IsBlank(FirstName) || IsBlank(LastName)
        ),
        Value = true
     ) = 0,
     DisplayMode.Edit,
     DisplayMode.Disabled
)

「ボタンが選択されると申請を行うフローを起動して、完了画面へ遷移」ですが、フローの作成は後の章で行うのでここでは説明を一旦割愛します。
完了画面への遷移の作成は簡単です。

まず完了画面の作成ですが、これはテンプレートを用いましょう。

f:id:koruneko:20210131153606p:plain

「新しい画面」 > 「成功」を選択します。
これだけでそれっぽい画面の完成です。

f:id:koruneko:20210131153702p:plain

あとはボタンが選択されたときの動作にこの画面への遷移を定義するだけでよいので、

RequestBotton.OnSelect

Navigate(Screen2)

以上で入力フォームの作成は完了です。

ユーザー作成処理を作成する

Azure Automation

ここは以前紹介した、【Microsoft Teams】申請されたユーザーを指定のTeams のチームに自動で追加する と一部説明が被りますが再度紹介します。

Azure Automation のデプロイ

  1. Azure Portal にログインを行い、「リソースの作成」を選択します。
    f:id:koruneko:20200719174315p:plain

  2. 「オートメーション」を作成します。
    f:id:koruneko:20200719174423p:plain

  3. 必要項目を入力します。
    f:id:koruneko:20200719174647p:plain

    • 名前:任意の名前を入力します。
    • サブスクリプション:任意のサブスクリプションを選択します。
    • リソースグループ:任意のリソースグループを選択、もしくは新規作成します。
    • 場所:任意の場所を選択します。今回は、「東日本」を選択します。
    • Azure 実行アカウントの作成:「はい」を選択します。(詳細はこちら
  4. 入力が完了したら、「作成」を選択し、デプロイを行います。

  5. デプロイが無事完了しました。
    f:id:koruneko:20200719175301p:plain

モジュールを追加する

  1. 「共有リソース」より、「モジュール」を選択します。
    f:id:koruneko:20210204233452p:plain

  2. ギャラリーを参照を選択します。
    f:id:koruneko:20200719175739p:plain

  3. 検索窓より「AzureAD」と検索して、「AzureAD」を選択します。 f:id:koruneko:20210205005137p:plain

  4. インポートを行います。 f:id:koruneko:20210205005342p:plain f:id:koruneko:20210205005402p:plain

  5. モジュールが使用可能になりました。 f:id:koruneko:20210205013714p:plain

  6. 同様の手順で「MSOnline」も使用可能な状態にします。 f:id:koruneko:20210205015007p:plain

変数を利用する

共有リソースより、「変数」を選択します。

f:id:koruneko:20200722013829p:plain

「変数の追加」を選択して、変数の追加を行います。

f:id:koruneko:20200722014213p:plain

必要項目を入力します。
今回追加する変数は domain です。
自身のテナントのドメイン、"@xxx.co.jp"を設定します。

f:id:koruneko:20200722015132p:plain

  • 名前:任意の変数名を入力します。
  • 説明:変数の説明を記載します。
  • タイプ:変数の型をしていします。今回は「文字列」を指定します。
  • 値:変数の値を入力します。
  • 暗号化:暗号化を行うか選択します。

変数の設定が完了したら、PowerShell Runbook に戻ります。

先ほどのスクリプトに先ほど追加した変数の読み込みを追加します。

変数の読み込みは「Get-AutomationVariable -Name 'VariableName'」で行うことができます。

資格情報の追加

Microsoft 365 ユーザーの追加を行うことができるadmin のユーザー情報を記録します。

  1. 共有リソースより、「資格情報」を選択します。
    f:id:koruneko:20200719181415p:plain

  2. 「資格情報の追加」を選択します。
    f:id:koruneko:20200719181555p:plain

  3. 必要項目を入力します。
    f:id:koruneko:20200719181810p:plain

    • 名前:任意の名前を入力します。
    • 説明:後から確認したときにわかりやすいよう、対象の資格情報の説明を入力します。
    • ユーザー名:ユーザー名(上記説明の権限のあるユーザー)を入力します。
    • パスワード:パスワード(上記説明の権限のあるユーザーのパスワード)を入力します。
    • パスワードの確認入力:先ほど入力したパスワードをもう一度入力します。
  4. 「作成」を選択して完了です。

PowerShell スクリプトの登録

  1. プロセス オートメーションより「Runbook」を選択します。
    f:id:koruneko:20200719222338p:plain

  2. 「Runbook の作成」を選択します。
    f:id:koruneko:20200719222519p:plain

  3. 必要項目を入力します。
    f:id:koruneko:20200719222800p:plain

    • 名前:任意の名前を入力します。
    • Runbook の種類:「PowerShell」を選択します。
    • 説明:後から確認したときにわかりやすいよう、対象のRunbook の説明を入力します。
  4. 作成が完了するとPowerShell の編集画面が表示されます。
    f:id:koruneko:20200719223309p:plain

  5. Runbook には以下のように記載します。

Param
(
    [Parameter (Mandatory= $true)]
    $userRequest
)

# 変数の読込
$domain = Get-AutomationVariable -Name 'domain'

# 資格情報を読込
$myCredential = Get-AutomationPSCredential -Name 'Microsoft 365 admin Login'

# Microsoft 365 サブスクリプション用の Azure AD に接続する
Connect-MsolService -Credential $myCredential

# ユーザーの作成
$userRequest | ForEach-Object{
    $DisplayName = $_.DisplayName
    $FirstName = $_.FirstName
    $LastName = $_.LastName
    $UserPrincipalName = $_.UserPrincipalName + $domain

    New-MsolUser -DisplayName $DisplayName `
        -FirstName $FirstName `
        -LastName $LastName `
        -UserPrincipalName $UserPrincipalName `
        -UsageLocation JP `
        -LicenseAssignment xxx:O365_BUSINESS
}

送られてくる値はObject なので ForEach-Object でループ処理を行っています。

ユーザーの作成は、New-MsolUser で行っています。

使用可能なライセンス( -LicenseAssignment )は、Get-MsolAccountSku で確認を行ってください。

モジュールのインストールを行っていない場合は、PowerShell を使用して Microsoft 365 に接続する を参考にモジュールのインストールを行ってください。

なお、エラー処理は今回特に行っていないので、各自適宜行うようにしてください。

  1. テストを行う
    スクリプトの作成が完了したら、一度、テストを行っておきましょう。
    「テスト ウィンドウ」を選択して、パラメーターを入力後「開始」を選択します。

f:id:koruneko:20210208011752p:plain

f:id:koruneko:20210208011936p:plain

  1. 公開を行う
    作成が完了したら、「公開」を行ってください。
    これを行わないと、変更が反映されませんので、注意です。

f:id:koruneko:20210208012037p:plain

Power Automate

最後にPower Apps で入力された値を先ほど作成したPowerShell Runbook に渡すようにします。
また、このPower Automate ないで"承認"処理を行うようにします。

  1. Power Apps から呼び出されるので、トリガーには Power Apps を利用します。

f:id:koruneko:20210208012902p:plain

  1. Power Apps からはコレクションの内容をJSON で渡すようにするので、"JSON の解析"で内容を取得します。

f:id:koruneko:20210208013150p:plain

ちなみにスキーマには以下のように入力します。

{
    "type": "array",
    "items": {
        "type": "object",
        "properties": {
            "DisplayName": {
                "type": "string"
            },
            "FirstName": {
                "type": "string"
            },
            "ID": {
                "type": "integer"
            },
            "LastName": {
                "type": "string"
            },
            "UserPrincipalName": {
                "type": "string"
            }
        }
    }
}
  1. 承認者に申請内容が分かりやすいように申請内容を表示させたいと思います。
    以下のように申請内容の文字列を作成します。

f:id:koruneko:20210208013458p:plain

"値"は以下のように設定しています。

表示名:@{items('Apply_to_each')?['DisplayName']}@{uriComponentToString('%0A')}
姓:@{items('Apply_to_each')?['FirstName']}@{uriComponentToString('%0A')}
名:@{items('Apply_to_each')?['LastName']}@{uriComponentToString('%0A')}
アカウント名:@{items('Apply_to_each')?['UserPrincipalName']}@{uriComponentToString('%0A')}
@{uriComponentToString('%0A')}

uriComponentToString('%0A') は改行を表しています。

  1. 承認を作成します。
    詳細には先ほど作成した文字列を設定します。

f:id:koruneko:20210208013804p:plain

なおこの詳細メッセージはOutlook やPower Automate で表示したときは改行されて表示されますが、Teams で表示したときは改行して表示されないため注意が必要です。

  1. 承認の結果によって処理を分けたいと思います。

f:id:koruneko:20210208014118p:plain

承認された場合は、PowerShell Runbook を呼び出します。
却下された場合は、Teams で申請者にその旨を知らせます。

  1. 承認された場合はPowerShell Runbook を実行したいので"ジョブの作成"を作成します。

f:id:koruneko:20210208014404p:plain

  • サブスクリプション:Azure Automation 作成時に使用したサブスクリプションと同様のものを選択
  • リソースグループ:Azure Automation 作成時に使用したサブスクリプションと同様のものを選択
  • Automation アカウント:先ほど作成したAutomation アカウントを選択
  • Runbook 名:先ほど作成したPowerShell Runbook を選択
  • ジョブの待機:「はい」を選択
  • Runbook Parameter:作成したパラメーターがこちらに表示されます。今回は"JSONの解析_コンテンツ"を選択します。

なおこのコネクタはPremium コネクタのため注意してください。

  1. 却下された場合は申請者にTeams でメッセージを送りたいので"メッセージをフローボットとしてユーザーに投稿する"を作成します。

f:id:koruneko:20210208014849p:plain

わかりにくいですが、メッセージにはMarkdown が利用できるため、文の末尾にスペースを2つ入力しています。
これで改行になります。

〇応答コメント  
@{items('Apply_to_each_2')?['comments']}  

〇要求内容  
@{variables('request')}

Power Apps からPower Automate を呼び出す

最後にPower Apps からPower Automate を呼び出す処理を作成します。

"アクション"から"Power Automate"を選択して、先ほど作成したフローを追加します。

f:id:koruneko:20210208015413p:plain

Power Apps からPower Automate を呼び出すときには、

  • コレクションの内容をJSON にしたもの
  • 申請者のe-mail

を渡します。

Request ボタンの OnSelect に以下のように設定します。

RequestButton.OnSelect

Create_Microsoft365_User.Run(JSON(userRequest, JSONFormat.Compact), User().Email);
Navigate(Screen2)

以上でMicrosoft 365 ユーザー作成依頼を作成することができました!!

ここまで作成してみて、恐らく皆さんはPowerShell Runbook で受けた値の処理方法が適切ではない(エラーとなる)のでは?と思われているかと思います。
次の章で解説します。

Power Automate からPowerShell Runbook へJSON を送った場合の型について

Power Automate からPowerShell Runbook へJSON を送った場合、上記の場合、変数 $userRequest にはJSON が設定されるので、以下のように記載すべきかと思います。
(私も最初そう考えて処理を記載していましたが、 Invalid JSON エラーが表示されて詰まっていました...)

$userRequest | ConvertFrom-Json | ForEach-Object {
    $fields = $_
    
    $fields | ForEach-Object{
        $DisplayName = $_.DisplayName
        $FirstName = $_.FirstName
        $LastName = $_.LastName
        $UserPrincipalName = $_.UserPrincipalName + $domain

        New-MsolUser -DisplayName $DisplayName `
            -FirstName $FirstName `
            -LastName $LastName `
            -UserPrincipalName $UserPrincipalName `
            -UsageLocation JP `
            -LicenseAssignment xxx:O365_BUSINESS
    }
}

ただこの場合、上でも述べている通り、 ConvertFrom-JsonInvalid JSON エラーが表示されてしまいます。

Runbook の実行結果より、"入力"の値を確認したところ、値は確かにJSON で渡ってきており、念のためフォーマットも確認しましたが、正しい形式でした。

そこで、 echo などで実際に変数 $userRequest に設定されている値を確認した結果、どうやらオブジェクトとして変数には設定されているようでした。

なので上記のように設定しているというわけです。

何故オブジェクトになるのか???については現在調査中です。
もしわかる方や参考になりそうなdocs などありましたら教えていただけますと助かります。

---2021/02/23 追記---

作成されたユーザー情報を連携する

大事な処理を作成するのを忘れていました。。。
作成されたMicrosoft 365 ユーザーの情報を申請者へ連携しなくてはなりません。

ユーザー作成の結果はPowerShell Runbook の出力から確認することができます。

ただこの結果は確認していただければわかる通り、 System.Object[] 型で出力されています。

このままの結果をPower Automate に渡しても加工が難しいのでJSON に変換したいと思います。
JSON に変換するためには、 ConvertTo-Json を利用します。

Param
(
    [Parameter (Mandatory= $true)]
    $userRequest
)

$domain = Get-AutomationVariable -Name 'domain'

# 資格情報を読込
$myCredential = Get-AutomationPSCredential -Name 'Microsoft 365 admin Login'

# Microsoft 365 サブスクリプション用の Azure AD に接続する
Connect-MsolService -Credential $myCredential

# ユーザーの作成
$userRequest | ForEach-Object{
    $DisplayName = $_.DisplayName
    $FirstName = $_.FirstName
    $LastName = $_.LastName
    $UserPrincipalName = $_.UserPrincipalName + $domain

    New-MsolUser -DisplayName $DisplayName `
        -FirstName $FirstName `
        -LastName $LastName `
        -UserPrincipalName $UserPrincipalName `
        -UsageLocation JP `
        -LicenseAssignment korune:DEVELOPERPACK
} | ConvertTo-Json -Compress

これで結果がJSON で出力されるようになりました。

続いてPower Automate でのフローの編集です。

呼び出したPowerShell Runbook の出力結果を取得するには"ジョブの出力を取得します"というコネクタを利用する必要があります。

f:id:koruneko:20210223110131p:plain

その際、Runbook を呼び出した"ジョブの作成"コネクタの"ジョブの待機"は"はい"を選びましょう。

これでユーザーを作成した結果の情報をPower Automate でも扱えるようになったので、あとはデータを加工して必要な情報だけを申請者に共有するようにします。

まずはデータの加工です。
結果はJSON で送られてきているので、JSON の加工を行います。

f:id:koruneko:20210223111047p:plain

コンテンツ

@{body('ジョブの出力を取得します')?['body']}

スキーマ

{
    "type": "array",
    "items": {
        "type": "object",
        "properties": {
            "Password": {
                "type": "string"
            },
            "ExtensionData": {
                "type": "object",
                "properties": {}
            },
            "AlternateEmailAddresses": {
                "type": "array"
            },
            "AlternateMobilePhones": {
                "type": "array"
            },
            "AlternativeSecurityIds": {},
            "BlockCredential": {
                "type": "boolean"
            },
            "City": {},
            "CloudExchangeRecipientDisplayType": {},
            "Country": {},
            "Department": {},
            "DirSyncProvisioningErrors": {},
            "DisplayName": {
                "type": "string"
            },
            "Errors": {},
            "Fax": {},
            "FirstName": {
                "type": "string"
            },
            "ImmutableId": {},
            "IndirectLicenseErrors": {},
            "IsBlackberryUser": {
                "type": "boolean"
            },
            "IsLicensed": {
                "type": "boolean"
            },
            "LastDirSyncTime": {},
            "LastName": {
                "type": "string"
            },
            "LastPasswordChangeTimestamp": {
                "type": "string"
            },
            "LicenseAssignmentDetails": {
                "type": "array",
                "items": {
                    "type": "string"
                }
            },
            "LicenseReconciliationNeeded": {
                "type": "boolean"
            },
            "Licenses": {
                "type": "array",
                "items": {
                    "type": "string"
                }
            },
            "LiveId": {
                "type": "string"
            },
            "MSExchRecipientTypeDetails": {},
            "MSRtcSipDeploymentLocator": {},
            "MSRtcSipPrimaryUserAddress": {},
            "MobilePhone": {},
            "ObjectId": {
                "type": "string"
            },
            "Office": {},
            "OverallProvisioningStatus": {
                "type": "integer"
            },
            "PasswordNeverExpires": {
                "type": "boolean"
            },
            "PasswordResetNotRequiredDuringActivate": {},
            "PhoneNumber": {},
            "PortalSettings": {},
            "PostalCode": {},
            "PreferredDataLocation": {},
            "PreferredLanguage": {},
            "ProxyAddresses": {},
            "ReleaseTrack": {},
            "ServiceInformation": {},
            "SignInName": {
                "type": "string"
            },
            "SoftDeletionTimestamp": {},
            "State": {},
            "StreetAddress": {},
            "StrongAuthenticationMethods": {
                "type": "array"
            },
            "StrongAuthenticationPhoneAppDetails": {
                "type": "array"
            },
            "StrongAuthenticationProofupTime": {},
            "StrongAuthenticationRequirements": {
                "type": "array"
            },
            "StrongAuthenticationUserDetails": {},
            "StrongPasswordRequired": {
                "type": "boolean"
            },
            "StsRefreshTokensValidFrom": {
                "type": "string"
            },
            "Title": {},
            "UsageLocation": {
                "type": "string"
            },
            "UserLandingPageIdentifierForO365Shell": {},
            "UserPrincipalName": {
                "type": "string"
            },
            "UserThemeIdentifierForO365Shell": {},
            "UserType": {
                "type": "integer"
            },
            "ValidationStatus": {
                "type": "integer"
            },
            "WhenCreated": {}
        }
    }
}

上記の結果を申請者 / 管理者に送るためのメッセージに加工します。

f:id:koruneko:20210223111303p:plain

@{uriComponentToString('%0A')}-----------------------------------------------@{uriComponentToString('%0A')}
DisplayName:@{items('Apply_to_each_3')?['DisplayName']}@{uriComponentToString('%0A')}
FirstName:@{items('Apply_to_each_3')?['FirstName']}@{uriComponentToString('%0A')}
LastName:@{items('Apply_to_each_3')?['LastName']}@{uriComponentToString('%0A')}
UserPrincipalName:@{items('Apply_to_each_3')?['UserPrincipalName']}@{uriComponentToString('%0A')}
Password:@{items('Apply_to_each_3')?['Password']}@{uriComponentToString('%0A')}

@{uriComponentToString('%0A')} は改行です。

あとはこのメッセージをTeams で共有してあげれば完了です!

f:id:koruneko:20210223111428p:plain

Power Automate からPowerShell Runbook への値渡しについて

【PowerShell Runbook】 入力パラメータの型検証にて検証結果を纏めてありますのでもしよければ参考にしてください。

shibatea さんコメントなどでのアドバイスありがとうございました!

【Power Apps】パズルゲームを作成してみよう!

はじめに

今回は随分前に作成して動画もアップするとか宣言しておいてアップしていなかったPower Apps で作成したパズルゲームの作成方法について纏めようと思います。
動画は収録していたものがありましたが、今回改めて撮りなおしました。

2020年のやり残し(他多数)を2021年に行うことになるとは...

YouTube

動画での紹介はこちらになります。

youtu.be

よろしければ、 チャンネル登録、高評価お願いします!!

パズルゲームを作成する

ホーム画面を作成する

ホーム画面は単純で

  • ラベル
  • ボタン

の2つで構成されています。

まずはタイトルラベルを作成します。

ラベルを追加して画面上部に配置します。

Label.Text

"Puzzle Game"

Label.X

0

Label.Y

0

Label.Width

Parent.Width

Label.Height

200

Label.Color

RGBA(255, 255, 0, 1)

Label.Fill

RGBA(0, 121, 187, 1)

続いてゲーム開始を行うためのボタンを作成します。

このボタンには以下2つの役割があります。

  1. パズルの盤面を作成する
  2. ゲーム画面へ遷移する

1 の盤面を作成するために、ホーム画面が呼び出されたタイミングで盤面を初期化し、パズルの回答(どこを選択すればよいのか)を設定します。

上記を実現するために OnVisibel に以下を設定します。

Screen1.OnVisible

ClearCollect(
    originBoard,
    {No:1,  isFront:true},
    {No:2,  isFront:true},
    {No:3,  isFront:true},
    {No:4,  isFront:true},
    {No:5,  isFront:true},
    {No:6,  isFront:true},
    {No:7,  isFront:true},
    {No:8,  isFront:true},
    {No:9,  isFront:true},
    {No:10, isFront:true},
    {No:11, isFront:true},
    {No:12, isFront:true},
    {No:13, isFront:true},
    {No:14, isFront:true},
    {No:15, isFront:true},
    {No:16, isFront:true}
);
ClearCollect(
    answer,
    {No:1,  isSelect:Rand() > 0.5},
    {No:2,  isSelect:Rand() > 0.5},
    {No:3,  isSelect:Rand() > 0.5},
    {No:4,  isSelect:Rand() > 0.5},
    {No:5,  isSelect:Rand() > 0.5},
    {No:6,  isSelect:Rand() > 0.5},
    {No:7,  isSelect:Rand() > 0.5},
    {No:8,  isSelect:Rand() > 0.5},
    {No:9,  isSelect:Rand() > 0.5},
    {No:10, isSelect:Rand() > 0.5},
    {No:11, isSelect:Rand() > 0.5},
    {No:12, isSelect:Rand() > 0.5},
    {No:13, isSelect:Rand() > 0.5},
    {No:14, isSelect:Rand() > 0.5},
    {No:15, isSelect:Rand() > 0.5},
    {No:16, isSelect:Rand() > 0.5}
)

originBoard が問題の盤面です。
まずは初期化を行うのですべて true (表)としています。

answer がパズルの答えです。
1 / 2 の確率で対象のマスが選択すべきマスとして設定されることになります。

今回のパズルゲームのルールとして、選択したマスとそのマスに隣するすべてのマスの裏表が反転するというルールなので、 answer で作成されて答えをもとに、 originBoard のマスを作成しようと思います。

ボタンが選択されたときのアクションに以下を追加します。

Button.OnSelect

ForAll(
    RenameColumns(answer, "No", "ansNo"),
    If(
        isSelect,
        If(Mod(ansNo, 4) <> 1, UpdateIf(originBoard, No = ansNo - 5, {isFront:!LookUp(originBoard, No = ansNo - 5, isFront)}));
        UpdateIf(originBoard, No = ansNo - 4, {isFront:!LookUp(originBoard, No = ansNo - 4, isFront)});
        If(Mod(ansNo, 4) <> 0, UpdateIf(originBoard, No = ansNo - 3, {isFront:!LookUp(originBoard, No = ansNo - 3, isFront)}));
        If(Mod(ansNo, 4) <> 1, UpdateIf(originBoard, No = ansNo - 1, {isFront:!LookUp(originBoard, No = ansNo - 1, isFront)}));
        UpdateIf(originBoard, No = ansNo, {isFront:!isFront});
        If(Mod(ansNo, 4) <> 0, UpdateIf(originBoard, No = ansNo + 1, {isFront:!LookUp(originBoard, No = ansNo + 1, isFront)}));
        If(Mod(ansNo, 4) <> 1, UpdateIf(originBoard, No = ansNo + 3, {isFront:!LookUp(originBoard, No = ansNo + 3, isFront)}));
        UpdateIf(originBoard, No = ansNo + 4, {isFront:!LookUp(originBoard, No = ansNo + 4, isFront)});
        If(Mod(ansNo, 4) <> 0, UpdateIf(originBoard, No = ansNo + 5, {isFront:!LookUp(originBoard, No = ansNo + 5, isFront)}));
    )
);
Navigate(PlayScreen, ScreenTransition.Cover)

盤面の作成方法は単純で答えで選択するマスとして設定されたマスとそれに隣するマスの裏表を逆転させればいいですね。

盤面の作成が完了したので次はパズル画面の作成を行いたいと思います。

パズル画面を作成する

パズルの盤面を作成する

パズルの盤面を作成します。
盤面の作成にはギャラリーを利用します。

Items には先ほど作成した originBoard を利用したいですが、そのまま使用してしまうと"やり直し"ができなくなってしまいます。
なので originBoardplayBoard にコピーして、 playBoardItems に設定したいと思います。

コレクションの内容のコピーは以下のように行います。

Screen2.OnVisible

Clear(playBoard);
ForAll(
    originBoard,
    Collect(
        playBoard,
        {
            No:No,
            isFront:isFront
        }
    )
)

なおこの OnVisible では以下のように他変数も初期化しておいてください。
使用用途はその都度説明します。

Screen2.OnVisible

UpdateContext({_dispAns:false});
UpdateContext({_ansnum:1});
UpdateContext({_count:0});
UpdateContext({_isHint:false});
UpdateContext({_isRetire:false});
Clear(playBoard);
ForAll(
    originBoard,
    Collect(
        playBoard,
        {
            No:No,
            isFront:isFront
        }
    )
)

ギャラリーを設定して以下のように設定します。

Gallery.Items

playBoard

Gallery.X

(Parent.Width - Self.Width) / 2

Gallery.Y

(Parent.Height - Self.Height) / 2

Gallery.Width

Self.TemplateHeight * 4

Gallery.Height

Self.TemplateHeight * 4

Gallery.WrapCount

4

Gallery.TemplateSize

150

Gallery.TemplatePadding

0

ギャラリー内にラベルを配置し、以下のように設定します。

Label.Text

If(
    ThisItem.isFront,
    "〇",
    "×"
)

Label.Color

If(
    ThisItem.isFront,
    RGBA(255, 0, 0, 1),
    RGBA(0, 0, 255, 1)
)

Label.Fill

If(
    ThisItem.isFront,
    Pink,
    Aqua
)

Label.Size

80

このラベル(マス)が選択されたとき、そのマスとそれに隣するマス全ての裏表を逆転させたいので以下のように設定します。

Label.OnSelect

If(Mod(ThisItem.No, 4) <> 1, UpdateIf(playBoard, No = ThisItem.No - 5, {isFront:!LookUp(playBoard, No = ThisItem.No - 5, isFront)}));
UpdateIf(playBoard, No = ThisItem.No - 4, {isFront:!LookUp(playBoard, No = ThisItem.No - 4, isFront)});
If(Mod(ThisItem.No, 4) <> 0, UpdateIf(playBoard, No = ThisItem.No - 3, {isFront:!LookUp(playBoard, No = ThisItem.No - 3, isFront)}));
If(Mod(ThisItem.No, 4) <> 1, UpdateIf(playBoard, No = ThisItem.No - 1, {isFront:!LookUp(playBoard, No = ThisItem.No - 1, isFront)}));
UpdateIf(playBoard, No = ThisItem.No, {isFront:!ThisItem.isFront});
If(Mod(ThisItem.No, 4) <> 0, UpdateIf(playBoard, No = ThisItem.No + 1, {isFront:!LookUp(playBoard, No = ThisItem.No + 1, isFront)}));
If(Mod(ThisItem.No, 4) <> 1, UpdateIf(playBoard, No = ThisItem.No + 3, {isFront:!LookUp(playBoard, No = ThisItem.No + 3, isFront)}));
UpdateIf(playBoard, No = ThisItem.No + 4, {isFront:!LookUp(playBoard, No = ThisItem.No + 4, isFront)});
If(Mod(ThisItem.No, 4) <> 0, UpdateIf(playBoard, No = ThisItem.No + 5, {isFront:!LookUp(playBoard, No = ThisItem.No + 5, isFront)}));
UpdateContext({_count:_count + 1})

UpdateContext({_count:_count + 1}) では、ユーザーの手数をカウントしています。
OnVisble で初期化を行っていますね。

以上で盤面の作成は完了です。

経過ターンを表示する

経過ターンを表示するためのラベルを作成します。

ラベルを配置して以下のように設定します。

Label.Text

_count & "ターン経過" & If(_isHint, "(最短" & CountRows(Filter(answer, isSelect = true)) & "回)")

Label.Width

Parent.Width

Label.Height

Gallery.Y

If(_isHint, "(最短" & CountRows(Filter(answer, isSelect = true)) & "回)") は後に作成するヒント機能の実装部分になります。

ホームボタンを作成する

ホーム画面に戻るボタンを作成します。

家のアイコンを追加して以下のように設定します。

HomeIcon.OnSelect

Back()

今回画面は2画面だけで、この画面はホーム画面からのみ遷移されてくるので、 Back() で戻ることとします。

リセットボタンを作成する

リセットボタンには OnVisible と同じ内容を設定すればいいですね。

再読み込みのアイコンを設定して以下のように設定します。

ResetIcon.OnSelect

UpdateContext({_dispAns:false});
UpdateContext({_ansnum:1});
UpdateContext({_count:0});
UpdateContext({_isHint:false});
UpdateContext({_isRetire:false});
Clear(playBoard);
ForAll(
    originBoard,
    Collect(
        playBoard,
        {
            No:No,
            isFront:isFront
        }
    )
)

ResetIcon.Y

HomeIcon.Y + HomeIcon.Height + 10

ヒントボタンを作成する

ヒントボタンは、押されると最短手数を表示するようにします。
先ほど作成した経過ターンのラベルですね。

isHinttrue に設定すれば、表示されるよう先ほど設定していたのでそのように設定します。

星のアイコンを設定して以下のように設定します。

HintIcon.OnSelect

UpdateContext({_isHint:true});

HintIcon.Y

ResetIcon.Y + ResetIcon.Height + 10

答えを表示する

答えを表示するためのボタンを設定して以下のように設定します。

Button.OnSelect

UpdateContext({_count:0});
UpdateContext({_isRetire:true});
Clear(playBoard);
ForAll(
    originBoard, 
    Collect(
        playBoard,
        {
            No:No,
            isFront:isFront
        }
    )
);
UpdateContext({_dispAns:true});

答えを表示するために、一度カウントと盤面をリセットしています。
また、答え表示のためのフラグ _dispAns とユーザーが自力で解けずリタイアしたことを表すためのフラグ _isRetiretrue に設定しています。

答え表示のためのフラグ _dispAnstrue に設定されたことをトリガーにユーザーに答えを示したいと思います。

答えの表示処理にはタイマーを利用します。

タイマーを設定して以下のように設定します。

Timer.Duration

1000

Timer.OnTimerEnd

If(
    CountRows(Filter(answer, isSelect = true)) >= _ansnum,
    UpdateContext({_dispAns:true});
    Select(BoardGallery, Last(FirstN(Filter(answer, isSelect = true), _ansnum)).No, Panel),
    UpdateContext({_dispAns:false})
);
UpdateContext({_ansnum:_ansnum + 1});

TimerAutoStart

CountRows(Filter(answer, isSelect = true)) >= _ansnum && _dispAns

Timer.Repeat

CountRows(Filter(answer, isSelect = true)) >= _ansnum && _dispAns

タイマーは1000ms 間隔で繰り返し処理を行うようにします。

タイマーの処理終了時、 _ansnum の値が answertrue の数以下(選択すべきマス以下)であった場合、 _ansnum 番目の選択すべきマスを選択しています。
_ansnum の値が answertrue の数以上であった場合は、 _dispAnsfalse に設定しています。

答えの表示が全て終了したらループを止めたいので、リピート処理を上記のように設定しています。

このままでは、ユーザーがどのマスを選択すればよかったのか?視覚的にわかりにくいので、答えの表示時に選択されたマスを赤丸で示したいと思います。

ギャラリー内にサークルを追加して、以下のように設定します。

Circle.X

(Panel.Width - Self.Width) / 2

Circle.Y

(Panel.Height - Self.Height) / 2

Circle.Fill

RGBA(255, 0, 0, 1)

Circle.OnVisible

ThisItem.No = Last(FirstN(Filter(answer, isSelect = true), _ansnum - 1)).No && _dispAns && CountRows(Filter(answer, isSelect = true)) >= _ansnum - 1

(Panel は Label です。)

これで答えの表示を実装することができました!

クリア画面を作成する

最後にクリア画面を作成したいと思います。

このクリア画面は、答え表示で全てのパネルが表になったときではなく、ユーザーが自身で選択したことによって全てのマスが表になったときに表示するようにします。

Label.Text

"Clear!!"

Label.OnVisible

CountIf(playBoard, isFront = true) = CountRows(playBoard) && !_isRetire

Label.Size

250

最後にホーム画面へ戻るアイコンたちを最前面にして完成です!
これをし忘れると、クリアラベルが表示されたとき、アイコンたちより上にいた場合アイコンが選択できなくて画面を操作できなくなって詰みます。

おわりに

以上でパズルアプリの作成方法は終了です!

最近はSVG を利用したアプリが多めでしたが、こんな風にPower Apps の標準的?な機能だけで簡単にゲームが作成できてしまうので良い時代になりましたね。 Power Apps はそういった用途のアプリじゃないですが‼

【Power Apps】タブ遷移に関して

はじめに

今回の記事はTips てきな記事になります。

Power Apps ではアクセシビリティを向上させるためのTabIndex プロパティが存在します。
この設定を行うことにより、ユーザーがキーボードだけで操作を行うことができるようになります。

今回はこのTabIndex プロパティについて纏めようと思います。

TabIndex プロパティを設定してみる

TabIndex プロパティは各コントロールに設定することが可能です。

TabIndex プロパティは外面上部にある数式バーか、画面右にあるコントロールのプロパティから変更が可能です。

f:id:koruneko:20210103004157p:plain

f:id:koruneko:20210103003900p:plain

日本語名称では、"タブ遷移順"となっていますが、ここに設定する数値は0 or -1が推奨されています。
それぞれ以下の動作となります。

TabIndex 値 動作 既定
0 コントロールはキーボード ナビゲーションに関与します。 Button、Text input、Combo box、およびその他の一般的な対話型コントロール
-1 コントロールはキーボード ナビゲーションに関与できません。 Label、Image、Icon、およびその他の一般的な非対話型コントロール

簡単に説明すると、
0を設定するとタブ遷移を行い -1を設定するとタブ遷移を行いません。

このタブ遷移は特に特別な設定を行っていなかった場合、Z順で遷移を行います。

詳しくはこちらをご確認ください。

docs.microsoft.com

デフォルト値が-1のコントロールに関しても、もちろん0に設定することでタブ遷移を行えるようにすることが可能です。
その場合、 FocusedBorderColor に任意の色を設定し、 FocusedBorderThickness に0より大きい値を設定してください。
この設定を行わないと、現在フォーカス設定されているコントロールがユーザーからわからなくなってしまいます。

TabIndex のデフォルト値が0のコントトールは上記プロパティに自身のボーダーカラーと線の太さが0より大きい値が設定されていますのでフォーカスされているコントロールがわかるようになっています。

実際の動作

まずは以下のようにテキストボックスを5つ配置したときのタブ遷移です。

ヒントのテキストに数値が設定されているテキストボックスの TabIndex には0が設定されており、×が設定されている TabIndex には-1が設定されています。

続いてギャラリーのタブ遷移です。

ギャラリーは WrapCount で折り返しの数を指定することが可能です。
この数値を設定することで1行(1列)あたりに表示されるコントロールの数を指定できます。

今回は WrapCount に3を設定しているので3行ずつ表示されています。
この状態でタブ遷移を行うと以下のようになります。

上記のようにZ順でテキストボックスにタブ遷移が行われます。
ラベルには TabIndex に-1を設定しているのでタブ遷移が行われません。

続いてギャラリー内でタブ遷移を行ったときに、ギャラリー外から選択されている項目を参照するとどのような動作になるのかみてみましょう!

上記のようにタブ遷移でユーザーがコントロールへ移動した場合、外部からはユーザーが現在どのコントロールを選択しているのかわかりません。
これは恐らくタブ遷移ではコントロールを選択するのではなく、あくまでフォーカスを行っているだけだからかと思われます。

もし、ギャラリー内でユーザーが選択している項目によってなにか変化させるような実装を行いたい場合は、このような制限事項があるので注意が必要です。
現時点(2021/1/2)ではこの問題を回避するベストな方法が存在しないので、ケースバイケースで対応もしくは許容する必要があります。

最後にタブ遷移をZ順ではなくN順で行わせる方法についてです。

まずは下記のように、 TabIndex のみを設定した場合は以下のようにZ順でタブ遷移を行います。

上記のように上から下へ、左から右にタブ遷移を行っていますね。

これを、「テキストボックス → トグル」といった風にN順でタブ遷移を行うように設定してみます。
上記を実現するためにはグループをコンテナー内に設定します。

これにより、上記のようにN順でタブ遷移が行われるようになりましたね。

1点注意として、N順でタブ遷移をしたい場合は、グループ化ではなくコンテナ化する必要があるという点です。
グループ化とした場合は以下のようにZ順でタブ遷移を行います。

これらの違いに気を付けて各コントロールを設定しましょう。


スポンサードリンク