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

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

Power Apps で Teams を作成する

はじめに

随分前に作成して、作成方法を纏めていなかったので、備忘録を兼ねてPower Apps でのTeams の作成方法を纏めようと思います。

ただ作成したのが2020年の1月ごろなので、最新の書き方でない箇所が多々ある可能性があります...
ご了承ください。

完成イメージはこのようになります。

f:id:koruneko:20201213232356p:plain

Powe Apps とMicrosoft Teams を連携する

コネクタを追加する

Power Apps でTemas を連携するには、Microsoft Teams コネクタを利用します。

下図のように、"データ"より、"Microsoft Teams"を選択して、アプリ内にMicrosoft Teams コネクタを追加します。

f:id:koruneko:20201213231029p:plain

これで、Power Apps 内のアプリでTeams と連携できるようになりました!

f:id:koruneko:20201213231540p:plain

チーム一覧を表示する

自身が所属しているチームの一覧を表示するには、

MicrosoftTeams.GetAllTeams().value

を利用します。

ギャラリーを追加して、 Items に上記を設定します。

あとは、このギャラリー内にラベルを追加して、チームの表示名を表示させます。

Label.Text

ThisItem.displayName

これでチーム一覧が表示できました。

チャネル一覧を表示する

チャネル一覧を表示する

チャネルはTeams の下にぶら下がっているものです。
なので、先ほどのチームのギャラリー内にチャネル用のギャラリー設定します。

また、今回設定するチャネル用のギャラリーは、実際のアプリのように以下機能を実現させたいと思います。

  • チームを選択したときだけチャネル一覧が表示される
  • チャネルの数によって表示領域を可変にする

これを実現するために"高さ(伸縮可能、空)"を利用します。
高さ可変ギャラリーについては、Hiroさんのこちらの記事を参考にしてください。

ギャラリーを追加したら、 Items に以下を設定します。

Gyallary.Items

MicrosoftTeams.GetChannelsForGroup(ThisItem.id).value

ThisItem.id は親要素のチームになります。

続いてチーム一覧表示のときと同じようにチャネルの表示名を表示させます。

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

Label.Text

ThisItem.displayName

ギャラリーの高さをチャネルの数によって可変にする

このままでは、ギャラリーの高さが可変ではないのでチャネルの数によってギャラリーの高さを可変に設定したいと思います。

チャネルのギャラリーの高さを以下に設定します。

Gyallary.Height

45 * CountRows(MicrosoftTeams.GetChannelsForGroup(ThisItem.id).value.displayName)

45 としている箇所はチャネルのラベルの大きさによって適宜調整してください。

チームを選択したときだけチャネル一覧が表示される

チームを選択したときだけチャネルが表示されるように設定します。

選択されたかを判する方法には色々あるかと思いますが、今回は簡単にトグルを用いたいと思います。

チームのギャラリー内にトグルを追加します。

このトグルに対して特別な関数を設定する必要はありません。

続いてチャネルのギャラリーの Visible に以下を設定します。

Gyallery.Visible

Toggle.Value

これで実現したかった機能を実現することができました!

ちなみに他の方法として、チームのコレクションに「表示状態か?」を判断するための項目を追加して、チームのラベルを選択したときに、そのフラグを反転させるような設定にすれば、表示/非表示のためのアイテムを追加しなくてよくなると思います。

メッセージ一覧を表示する

メッセージの一覧表示ギャラリーは以下の要素に分かれています。

  • 投稿者のプロファイル画像
  • 投稿者の名前
  • 件名
  • 本文

メッセージの情報を取得する

メッセージ情報を取得するには、 MicrosoftTeams.GetMessagesFromChannel を利用します。

ただし、このメッセージ情報の取得には、対象のチームIDチャネルIDが必要です。

となると、先ほどのチャネル一覧表示のときのようにチャネルのギャラリー内にメッセージギャラリーを設定する必要があるように思えます。

ただそうしてしまうと、デザインの設定が面倒なのと、親の親のアイテムの情報を取得するのはちょっと面倒なので、今回はコレクションにメッセージの情報を設定したいと思います。

ユーザー操作から考えると、メッセージの情報を取得するタイミングはチャネルを選択したときですね。
なので、チャネル表示のラベルの OnSelect に以下を設定します。

Label.OnSelect

UpdateContext({_teamID:TeamGallery.Selected.id});
UpdateContext({_channelID:ThisItem.id});
ClearCollect(teamsMessage, MicrosoftTeams.GetMessagesFromChannel(_teamID, _channelID).value)

これで teamsMessage にメッセージ情報を設定できましたので、メッセージ表示用のギャラリーの ItemsteamsMessage を設定してください。

投稿者のプロファイル画像を表示する

投稿者のプロファイル画像を表示するには、メッセージ表示用のギャラリーに"画像"を追加して、以下のように設定します。

Image.Image

Office365ユーザー.UserPhoto(ThisItem.from.user.id)

teamsMessage に設定されたコレクションの中身をみてもずばり投稿者のプロファイル画像情報が設定されている項目はありません。
なので、投稿者のユーザーIDをもとに、Office365 ユーザー コネクタを使ってプロファイル画像を取得する必要があります。
こちらのコネクタもTeams コネクタを追加したときと同様の方法で追加しておきましょう。

投稿者の名前を表示する

投稿者の名前はプロファイル画像とは違い、コレクションに設定されていますね。

また、どうせなのでいつ投稿されたのかも一緒に表示してしまいましょう。

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

Label.Text

ThisItem.from.user.displayName & "  " & ThisItem.createdDateTime

件名を表示する

件名もコレクション内に存在するので、ラベルを追加して以下のように設定します。

Label.Text

ThisItem.subject

また、件名は設定されているときだけ表示されるようにしたいと思います。

Label.height

If(IsBlank(ThisItem.subject), 0, 30)

本文を表示する

メッセージの本文は ThisItem.body.content にて取得できます。

ただこちらの設定を、ラベルにてそのまま表示させてはいけません。

なぜかというと、Teams では文字に装飾(太文字や下線など)を行うことができますね。
この設定がなされているメッセージの場合、HTML タグが付与されたメッセージが設定されています。

これをそのまま表示させるために"HTML テキスト"を利用します。
HTML テキストの HTMLTextThisItem.body.content を設定してください。

これでメッセージがTeams で送られたメッセージそのまま表示されたかと思います。

また、文字が見切れるのを避けるためにメッセージの文字数によって、HTML テキストの高さを可変にしたいと思います。

HTMLText.Height

70 + 80 * 
Max(RoundDown((
// 半角英数字・記号
CountRows(MatchAll(HtmlTBody.HtmlText, "[\x21-\x7eE]")) +
// 全角文字
CountRows(MatchAll(HtmlTBody.HtmlText, "[^\x01-\x7E\xA1-\xDF]")) * 2 +
// 全角記号
CountRows(MatchAll(HtmlTBody.HtmlText, "[/[!-/:-@[-`{-~、-〜”’・]+/g]")) * 2
) / 100, 0),
// 改行
CountRows(MatchAll(HtmlTBody.HtmlText, "[\x0A]")))

MatchALL 関数を使って正規表現でコメントの文字が何文字設定されているかをカウントしています。
それによって、高さが調整されるようにしています。

ここの詳しい解説はまた別の機会に...

Microsoft Teams にメッセージを投稿する

メッセージを投稿する

メッセージを投稿するために、以下入力項目が必要です。

  • 件名入力欄
  • 本文入力欄

件名入力欄には、"テキスト入力"を利用します。
本文入力欄には、実際のTeams と同じように、太文字や下線など設定できるようにしたいので"リッチ テキスト エディター"を設定します。

これらに入力されて情報をTeams に投稿したいと思います。

メッセージ送信用のアイコンを追加して、 OnSelect に以下のように設定します。
(調べてみたら現在V3 が最新のようです(2020/12/13 時点))

Icon.OnSelect

UpdateContext({_isSubmit:
MicrosoftTeams.PostMessageToChannelV2(
    _teamID,
    _channelID,
    {
        content: BodyInput.HtmlText,
        contentType: "html"
    },
    {subject:SubjectInput.Text}
)});

以下を設定することで、HTML で装飾されたテキストを投稿することができます。

content: BodyInput.HtmlText,
contentType: "html"

また、「なぜ UpdateContext 関数を利用しているのか?」というと、メッセージの投稿が完了したら、Teams のメッセージ情報を更新して、先ほど投稿した、メッセージをアプリにも表示させたいからです。

メッセージが投稿されると、 _isSubmit には、 True が設定されます。

これで、メッセージが投稿されたか?を判断し、メッセージ情報の更新を行いたいと思います。

メッセージを投稿したら、メッセージ情報を更新する

_isSubmit を活用して、メッセージを投稿したら、メッセージ情報を更新するよう設定したいと思います。

トグルを追加して、以下のように設定します。

Toggle.Default

_isSubmit

Toggle.OnCheck

ClearCollect(teamsMessage, MicrosoftTeams.GetMessagesFromChannel(_teamID, _channelID).value);
UpdateContext({_isSubmit:false})

これでトグルが True になったとき、つまり、メッセージが投稿されたときメッセージ情報が更新され、投稿したメッセージがPower Apps 上でも確認できるようになったかと思います。

なお、タイマーなどで、定期的に情報を更新する場合はこの設定は必要ないですが、処理がちょっと重くなります。

定期更新を設定しないのであれば、手動更新を設定しておくといいかもですね。

この設定で実装できないこと

メンション

<at></at>で囲ってあげてもメンションとして識別してくれません。
これは、送信、受信両者においてです。

個人チャット情報の操作

コネクタの情報をみてもらえばわかる通り、取得及び操作できるのはチームのみで、チャットについては取得も投稿もできません。

チャットの仕組みはOutlook に近いものがあるので、どうにか工夫すれば、もしかしたら取得することも可能???
わからない...

画像の表示

Teams で投稿された画像の表示を行うことはできません。

これは投稿された画像がblob などに格納されてしまうからですね。

以下に表示できる画像、表示できない画像の早見表を作成しましたので、参考にしてください。

アイテム種類 表示可/表示不可 備考
ローカルから投稿した画像 表示不可 blob に保存される
インターネットから貼り付けた画像 表示不可 Graph API を叩けば取得できる??
このアプリから投稿した画像 表示可 Blogger に保存される
プライズ 表示不可 id でしか設定されていないので、Teams アプリでないと表示は不可
ステッカー 表示不可 Graph API を叩けば取得できる??
GIF 表示可 GIPHY に保存される

認識間違いある場合、教えていただけると助かります...

Power Apps でのSVG 表現メモ

はじめに

この記事は自身への備忘録やコピペ用に作成した記事です。

Power Apps でSVG を用いて図形を描画する際のサンプルコードを列挙します。
解説などは行いません。

Power Apps のと記載しましたが、もちろんSVG を記載するうえで参考になる部分はあると思います。

SVG のリファレンスに関してはこちらをご確認ください。

developer.mozilla.org

SVG

"data:image/svg+xml,"& 
EncodeUrl(
    "<svg viewBox='0 0 "& Self.Width & " " & Self.Height & "' xmlns='http://www.w3.org/2000/svg'>      
        <circle cx='50' cy='50' r='50' fill='black'/>
    </svg>"
)

四角

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

四角⇔円に変化するアニメーション

"data:image/svg+xml,"& 
EncodeUrl(
    "<svg viewBox='0 0 "& Self.Width & " " & Self.Height & "' xmlns='http://www.w3.org/2000/svg'>
      <rect width='100' height='100'>
        <animate attributeName='rx' values='0;100;0;' dur='5s' repeatCount='indefinite' />
      </rect>
    </svg>"
)

円の大きさが変化するアニメーション

"data:image/svg+xml,"& 
EncodeUrl(
    "<svg viewBox='0 0 "& Self.Width & " " & Self.Height & "' xmlns='http://www.w3.org/2000/svg'>
        <circle cx='50' cy='50' fill='black'>
            <animate attributeName='r' values='0;50;0;' dur='5s' repeatCount='indefinite' />
        </circle>
    </svg>"
)

四角形の幅が変化するアニメーション

"data:image/svg+xml,"& 
EncodeUrl(
    "<svg viewBox='0 0 "& Self.Width & " " & Self.Height & "' xmlns='http://www.w3.org/2000/svg'>
        <rect height='100'>
            <animate attributeName='width' values='0;100;0;' dur='5s' repeatCount='indefinite' />
        </rect>
    </svg>"
)

四角形の高さが変化するアニメーション

"data:image/svg+xml,"& 
EncodeUrl(
    "<svg viewBox='0 0 "& Self.Width & " " & Self.Height & "' xmlns='http://www.w3.org/2000/svg'>
        <rect width='100'>
            <animate attributeName='height' values='0;100;0;' dur='5s' repeatCount='indefinite' />
        </rect>
    </svg>"
)

四角形の幅と高さが変化するアニメーション

"data:image/svg+xml,"& 
EncodeUrl(
    "<svg viewBox='0 0 "& Self.Width & " " & Self.Height & "' xmlns='http://www.w3.org/2000/svg'>
        <rect width='100' height='100'>
            <animate attributeName='width' values='0;100;0;' dur='5s' repeatCount='indefinite' />
            <animate attributeName='height' values='0;100;0;' dur='5s' repeatCount='indefinite' />
      </rect>
    </svg>"
)

ひし形

"data:image/svg+xml,"& 
EncodeUrl(
    "<svg viewBox='0 0 "& Self.Width & " " & Self.Height & "' xmlns='http://www.w3.org/2000/svg'>
          <polygon points='50 10, 90 50, 50 90, 10 50' fill='#99f' />
    </svg>"
)

楕円

"data:image/svg+xml,"& 
EncodeUrl(
    "<svg viewBox='0 0 "& Self.Width & " " & Self.Height & "' xmlns='http://www.w3.org/2000/svg'>
        <ellipse cx='50' cy='50' rx='30' ry='18' stroke='black' fill='#fff' stroke-width='2' />
    </svg>"
)

トライアングル

"data:image/svg+xml,"& 
EncodeUrl(
    "<svg viewBox='0 0 "& Self.Width & " " & Self.Height & "' xmlns='http://www.w3.org/2000/svg'>
        <path d='M 5 70 L 50 25 L 95 70 L 60 70' stroke='black' fill='transparent' stroke-width='2' />
    </svg>"
)

モーションパスに沿って移動するアニメーション

"data:image/svg+xml,"& 
EncodeUrl(
    "<svg width='120' height='120' viewBox='0 0 120 120'
    xmlns='http://www.w3.org/2000/svg' version='1.1'
    xmlns:xlink='http://www.w3.org/1999/xlink'>

  <!-- Draw the outline of the motion path in grey, along
       with 2 small circles at key points -->
  <path d='M10,110 A120,120 -45 0,1 110 10 A120,120 -45 0,1 10,110'
      stroke='lightgrey' stroke-width='2' 
      fill='none' id='theMotionPath'/>
  <circle cx='10' cy='110' r='3' fill='lightgrey'  />
  <circle cx='110' cy='10' r='3' fill='lightgrey'  />

  <!-- Red circle which will be moved along the motion path. -->
  <circle cx='' cy='' r='5' fill='red'>

    <!-- Define the motion path animation -->
    <animateMotion dur='6s' repeatCount='indefinite'>
      <mpath xlink:href='#theMotionPath'/>
    </animateMotion>
  </circle>
</svg>"
)

回転する三角形

"data:image/svg+xml,"& 
EncodeUrl(
    "<?xml version='1.0'?>
<svg width='120' height='120'  viewBox='0 0 120 120'
     xmlns='http://www.w3.org/2000/svg' version='1.1'
     xmlns:xlink='http://www.w3.org/1999/xlink' >

    <polygon points='60,30 90,90 30,90'>
        <animateTransform attributeName='transform'
                          attributeType='XML'
                          type='rotate'
                          from='0 60 70'
                          to='360 60 70'
                          dur='10s'
                          repeatCount='indefinite'/>
    </polygon>
</svg>"
)

動く四角形

"data:image/svg+xml,"& 
EncodeUrl(
    "<svg viewBox='0 0 "& Self.Width & " " & Self.Height & "' xmlns='http://www.w3.org/2000/svg'>
      <rect x='0' y='0' width='" & Self.Height & "' height='" & Self.Height & "' fill='black'>
        <animate attributeName='x' from='-100' to='" & Self.Width & "' dur='4s' repeatCount='indefinite' />
      </rect>
    </svg>"
)

格子上の模様をつける

"data:image/svg+xml,"& 
EncodeUrl(
    "<svg viewBox='0 0 230 100' xmlns='http://www.w3.org/2000/svg'>
  <defs>
    <pattern id='grid' viewBox='0,0,10,10' width='10%' height='10%'>
      <polygon points='0,0 2,5 0,10 5,8 10,10 8,5 10,0 5,2'/>
    </pattern>
  </defs>

  <circle cx='50'  cy='50' r='50' fill='url(#grid)'/>
  <circle cx='180' cy='50' r='40' fill='none' stroke-width='20' stroke='url(#grid)'/>
</svg>"
)

ハート

"data:image/svg+xml,"& 
EncodeUrl(
    "<svg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'>
  <path d='M 10,30
           A 20,20 0,0,1 50,30
           A 20,20 0,0,1 90,30
           Q 90,60 50,90
           Q 10,60 10,30 z'/>
</svg>"
)

マスク

"data:image/svg+xml,"& 
EncodeUrl(
    "<svg viewBox='0 0 "& Self.Width & " " & Self.Height & "' xmlns='http://www.w3.org/2000/svg'> 
  <mask id='myMask'>
    <!-- Everything under a white pixel will be visible -->
    <rect x='0' y='0' width='100' height='100' fill='white' />

    <!-- Everything under a black pixel will be invisible -->
    <path d='M10,35 A20,20,0,0,1,50,35 A20,20,0,0,1,90,35 Q90,65,50,95 Q10,65,10,35 Z' fill='black' />
  </mask>
 
  <polygon points='-10,110 110,110 110,-10' fill='orange' />

  <!-- with this mask applied, we 'punch' a heart shape hole into the circle -->
  <circle cx='50' cy='50' r='50' mask='url(#myMask)' />
</svg>"
)

ぼかす

"data:image/svg+xml,"& 
EncodeUrl(
"<svg viewBox='0 0 "&Self.Width&" "&Self.Height&"' xmlns='http://www.w3.org/2000/svg'>       
<defs>
    <filter id='blur'>
        <feGaussianBlur stdDeviation='5'/>
    </filter>
 </defs>
 
 <circle cx='50' cy='50' r='50' fill='blue'  filter='url(#blur)'/>
</svg>"
)

影をつける

"data:image/svg+xml,"& 
EncodeUrl(
"<svg viewBox='0 0 "&Self.Width&" "&Self.Height&"' xmlns='http://www.w3.org/2000/svg'>       
    <defs>
        <filter id='shadow'>
            <feDropShadow dx='10' dy='10' stdDeviation='5'/>
        </filter>
    </defs>
 
    <circle cx='50' cy='50' r='50' fill='blue' filter='url(#shadow)'/>
</svg>"
)

回転する星

"data:image/svg+xml,"& 
EncodeUrl(
"<svg viewBox='0 0 "& Self.Width & " " & Self.Height & "' xmlns='http://www.w3.org/2000/svg'>
    <polygon id='myPoly' fill='#FFF000' stroke='#FF0000' points='72.278,58.394 78.338,91.04 48.74,75.993 19.565,91.844 24.729,59.045 0.639,36.196 33.428,30.972 47.715,1 62.814,30.57 95.734,34.895 ' transform='rotation'>
    
    <animateTransform attributeName='transform'
                      begin='0s'
                      dur='3s'
                      type='rotate'
                      from='360 50 50'
                      to='0 50 50'
                      repeatCount='indefinite' />
    </polygon>
</svg>"
)

渦巻

"data:image/svg+xml,"& 
EncodeUrl(
    "<svg viewBox='0 0 "& Self.Width & " " & Self.Height & "' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'>
        <path d='m 71.815483,79.375008 c 0.379024,4.661976 -6.097254,2.810971 -7.748511,0.629963 -4.474806,-5.910399 0.64058,-13.727975 6.488585,-16.126986 10.460708,-4.291269 21.500027,3.544086 24.505461,13.607133 C 99.471613,92.253044 88.608573,106.89197 74.335336,110.36905 55.311355,115.00345 36.969959,101.01106 33.072925,82.524823 28.168906,59.261785 45.334033,37.175352 68.035704,32.883938 95.530469,27.686461 121.38386,48.046561 126.05506,74.965266 131.55991,106.68803 107.99196,136.32124 76.855188,141.3631 40.906556,147.18413 7.4853081,120.4002 2.0788793,85.044676 -4.0642488,44.871487 25.941108,7.6566196 65.515852,1.8898923 109.91273,-4.5795043 150.92519,28.65111 157.04911,72.445413 163.84782,121.06539 127.38915,165.87837 79.375041,172.35715' stroke='lightgrey' fill='transparent' stroke-width='2' />
    </svg>"
)

3Dキューブ

"data:image/svg+xml,"& 
EncodeUrl(
    "<svg viewBox='0 0 "& Self.Width & " " & Self.Height & "' xmlns='http://www.w3.org/2000/svg'>
        <path d='M 14.973676,89.153138 50.784897,25.914845 V 101.3499 l -35.811221,32.4797 z' stroke='black' fill='#e9e9ff' stroke-width='2' fill-opacity='0.5' />
        <path d='M 0.75595247,80.886905 V 129.584 L 14.973676,133.8296 V 89.153138 Z' stroke='black' fill='#353564' stroke-width='2' fill-opacity='0.5' />
        <path d='M 0.75595247,80.886905 30.994048,0.79166829 50.784897,25.914845 14.973676,89.153138 Z' stroke='black' fill='#4d4d9f' stroke-width='2' fill-opacity='0.5' />
        <path d='M 0.75595247,129.584 30.994048,88.446429 50.784897,101.3499 14.973676,133.8296 Z' stroke='black' fill='#afafde' stroke-width='2' fill-opacity='0.5' />
        <path d='M 30.994048,0.79166829 V 88.446429 L 50.784897,101.3499 V 25.914845 Z' stroke='black' fill='#d7d7ff' stroke-width='2' fill-opacity='0.5' />
        <path d='M 0.75595247,80.886905 30.994048,0.79166829 V 88.446429 L 0.75595247,129.584 Z' stroke='black' fill='#8686bf' stroke-width='2' fill-opacity='0.5' />
    </svg>"
)

回転する3Dキューブ(その1)

"data:image/svg+xml,"& 
EncodeUrl(
    "<svg viewBox='0 0 "& Self.Width & " " & Self.Height & "' xmlns='http://www.w3.org/2000/svg'>
        <path d='M 73.642864,83.30019 131.17597,71.637295 V 132.38138 L 73.642864,122.08462 Z' stroke='black' fill='#e9e9ff' stroke-width='2' fill-opacity='0.5' transform='rotation' >
            <animateTransform attributeName='transform'
                      begin='0s'
                      dur='3s'
                      type='rotate'
                      from='0 100 100'
                      to='360 100 100'
                      repeatCount='indefinite' />
        </path>
        <path d='m 31.576366,75.30557 v 53.83722 l 42.066498,-7.05817 V 83.30019 Z' stroke='black' fill='#353564' stroke-width='2' fill-opacity='0.5' transform='rotation'>
            <animateTransform attributeName='transform'
                      begin='0s'
                      dur='3s'
                      type='rotate'
                      from='0 100 100'
                      to='360 100 100'
                      repeatCount='indefinite' />
        </path>
        <path d='M 31.576366,129.14279 91.557712,154.57107 131.17597,132.38138 73.642864,122.08462 Z' stroke='black' fill='#4d4d9f' stroke-width='2' fill-opacity='0.5' transform='rotation'>
            <animateTransform attributeName='transform'
                      begin='0s'
                      dur='3s'
                      type='rotate'
                      from='0 100 100'
                      to='360 100 100'
                      repeatCount='indefinite' />
        </path>
        <path d='M 31.576366,75.30557 91.557712,46.503569 131.17597,71.637295 73.642864,83.30019 Z' stroke='black' fill='#afafde' stroke-width='2' fill-opacity='0.5' transform='rotation'>
            <animateTransform attributeName='transform'
                      begin='0s'
                      dur='3s'
                      type='rotate'
                      from='0 100 100'
                      to='360 100 100'
                      repeatCount='indefinite' />
        </path>
        <path d='M 91.557712,46.503569 V 154.57107 L 131.17597,132.38138 V 71.637295 Z' stroke='black' fill='#d7d7ff' stroke-width='2' fill-opacity='0.5' transform='rotation'>
            <animateTransform attributeName='transform'
                      begin='0s'
                      dur='3s'
                      type='rotate'
                      from='0 100 100'
                      to='360 100 100'
                      repeatCount='indefinite' />
        </path>
        <path d='M 31.576366,75.30557 91.557712,46.503569 V 154.57107 L 31.576366,129.14279 Z' stroke='black' fill='#8686bf' stroke-width='2' fill-opacity='0.5' transform='rotation'>
            <animateTransform attributeName='transform'
                      begin='0s'
                      dur='3s'
                      type='rotate'
                      from='0 100 100'
                      to='360 100 100'
                      repeatCount='indefinite' />
        </path>
    </svg>"
)

回転する3Dキューブ(その2)

"data:image/svg+xml,"& 
"data:image/svg+xml,"& 
EncodeUrl(
    "<svg viewBox='0 0 "& Self.Width & " " & Self.Height & "' xmlns='http://www.w3.org/2000/svg'>
        <path d='M 73.642864,83.30019 131.17597,71.637295 V 132.38138 L 73.642864,122.08462 Z' stroke='black' fill='#e9e9ff' stroke-width='2' fill-opacity='0.5' transform='rotation' >
            <animateTransform attributeName='transform'
                      begin='0s'
                      dur='3s'
                      type='rotate'
                      from='0 100 100'
                      to='360 100 100'
                      repeatCount='indefinite' />
        </path>
        <path d='m 31.576366,75.30557 v 53.83722 l 42.066498,-7.05817 V 83.30019 Z' stroke='black' fill='#353564' stroke-width='2' fill-opacity='0.5' transform='rotation'>
            <animateTransform attributeName='transform'
                      begin='0s'
                      dur='3s'
                      type='rotate'
                      from='0 50 50'
                      to='360 100 100'
                      repeatCount='indefinite' />
        </path>
        <path d='M 31.576366,129.14279 91.557712,154.57107 131.17597,132.38138 73.642864,122.08462 Z' stroke='black' fill='#4d4d9f' stroke-width='2' fill-opacity='0.5' transform='rotation'>
            <animateTransform attributeName='transform'
                      begin='0s'
                      dur='3s'
                      type='rotate'
                      from='0 80 80'
                      to='360 100 100'
                      repeatCount='indefinite' />
        </path>
        <path d='M 31.576366,75.30557 91.557712,46.503569 131.17597,71.637295 73.642864,83.30019 Z' stroke='black' fill='#afafde' stroke-width='2' fill-opacity='0.5' transform='rotation'>
            <animateTransform attributeName='transform'
                      begin='0s'
                      dur='3s'
                      type='rotate'
                      from='0 120 120'
                      to='360 100 100'
                      repeatCount='indefinite' />
        </path>
        <path d='M 91.557712,46.503569 V 154.57107 L 131.17597,132.38138 V 71.637295 Z' stroke='black' fill='#d7d7ff' stroke-width='2' fill-opacity='0.5' transform='rotation'>
            <animateTransform attributeName='transform'
                      begin='0s'
                      dur='3s'
                      type='rotate'
                      from='0 30 30'
                      to='360 100 100'
                      repeatCount='indefinite' />
        </path>
        <path d='M 31.576366,75.30557 91.557712,46.503569 V 154.57107 L 31.576366,129.14279 Z' stroke='black' fill='#8686bf' stroke-width='2' fill-opacity='0.5' transform='rotation'>
            <animateTransform attributeName='transform'
                      begin='0s'
                      dur='3s'
                      type='rotate'
                      from='0 140 140'
                      to='360 100 100'
                      repeatCount='indefinite' />
        </path>
    </svg>"
)

跳ねる3つのボール

"data:image/svg+xml,"& 
EncodeUrl(
    "<svg viewBox='0 0 "& Self.Width & " " & Self.Height & "' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'>
        <path d='M 25 80 L 25 10 L 25 80' stroke='transparent' fill='transparent' stroke-width='2' id='theMotionPath1' />
        <path d='M 50 60 L 50 10 L 50 80 L 50 60' stroke='transparent' fill='transparent' stroke-width='2' id='theMotionPath2' />
        <path d='M 75 40 L 75 10 L 75 80 L 75 40' stroke='transparent' fill='transparent' stroke-width='2' id='theMotionPath3' />

        <circle cx='' cy='' r='10' fill='red'>
            <animateMotion dur='1s' repeatCount='indefinite'>
        <mpath xlink:href='#theMotionPath1'/>
            </animateMotion>
        </circle>
        <circle cx='' cy='' r='10' fill='red'>
            <animateMotion dur='1s' repeatCount='indefinite'>
        <mpath xlink:href='#theMotionPath2'/>
            </animateMotion>
        </circle>
        <circle cx='' cy='' r='10' fill='red'>
            <animateMotion dur='1s' repeatCount='indefinite'>
        <mpath xlink:href='#theMotionPath3'/>
            </animateMotion>
        </circle>
    </svg>"
)

虹色で塗りつぶし

"data:image/svg+xml,"& 
EncodeUrl(
    "<svg viewBox='0 0 "& Self.Width & " " & Self.Height & "' xmlns='http://www.w3.org/2000/svg'>
      <defs>
       <linearGradient id='grad'  x1='0%' y1='0%' x2='100%' y2='0%'>
         <stop  offset='0%' stop-color='#ff0000' />
         <stop  offset='16.7%' stop-color='#ffff00'/>
         <stop  offset='33.3%' stop-color='#00ff00'/>
         <stop  offset='50.0%' stop-color='#00ffff'/>
         <stop  offset='66.7%' stop-color='#0000ff'/>
         <stop  offset='83.3%' stop-color='#ff00ff'/>
         <stop  offset='100%' stop-color='#ff0000'/>
       </linearGradient>
      </defs>
      <rect x='0' y='0' width='100' height='100' fill='url(#grad)' />
      <circle cx='150' cy='50' r='40' fill='none' stroke-width='15' stroke='url(#grad)'/>
    </svg>"
)

手書き風模様

"data:image/svg+xml,"& 
EncodeUrl(
    "<svg viewBox='0 0 "& Self.Width & " " & Self.Height & "' xmlns='http://www.w3.org/2000/svg'>
        <path d='M 83.910714,34.773809 C 83.65873,33.765873 84.131165,32.105057 83.15476,31.75 c -2.36813,-0.86114 -5.03968,0 -7.559522,0 -17.593328,0 -41.23065,0.258532 -45.357144,21.922618 -0.917128,4.814921 -0.456907,9.519994 2.267857,13.607142 6.94281,10.414214 32.757441,18.478672 43.089287,21.922621 9.5284,3.176132 11.477366,1.897133 19.654761,6.803569 4.696698,2.818022 5.291671,7.25328 5.291671,12.09524 0,0.75595 0.29778,1.57303 0,2.26786 -2.046509,4.77518 -9.374798,13.58418 -14.363099,15.11904 -6.040265,1.85855 -12.590655,1.89422 -18.898811,1.51191 -6.130877,-0.37157 -14.757582,-2.48253 -19.654761,-6.80357 -6.694275,-5.90672 -7.559524,-15.27572 -7.559524,-23.434525 0,-0.755955 -0.312814,-1.579666 0,-2.267858 0.983697,-2.164135 2.274422,-4.207758 3.779764,-6.047621 4.296706,-5.251529 11.430555,-5.116938 17.386903,-6.047618 23.373619,-3.652128 4.016097,-1.564129 31.749999,-3.023809 2.038498,-0.107289 13.900559,-0.55576 15.874999,-3.023809 1.55481,-1.943513 1.03006,-9.146434 0.75595,-11.339285 -0.20857,-1.66856 -1.67767,-3.914582 -2.26785,-5.291667 -1.31737,-3.073852 -2.17167,-10.487141 -4.53572,-12.85119 -0.17818,-0.178181 -0.57777,0.178178 -0.75595,0 -5.238481,-5.238483 5.96405,4.452138 -2.267856,-3.779764 C 99.143276,42.44685 98.09949,42.275341 97.517856,41.577381 96.796428,40.711667 96.748025,39.401654 96.00595,38.553572 92.805532,34.895951 88.554636,32.18208 84.666666,30.238094 79.940908,27.875216 71.601631,49.142956 71.059523,50.648809 c -6.440503,17.890283 -10.594459,36.186696 -15.875,54.428571 -2.92605,10.10818 -6.393354,20.06141 -9.071429,30.23809 -5.029446,19.1119 -0.242301,3.72168 -3.02381,20.41072 -0.262001,1.572 -1.226817,2.96773 -1.511903,4.53571 -0.306721,1.68697 0,4.18411 0,6.04762' stroke='black' fill='transparent' stroke-width='2' />
    </svg>"
)

グラデーション

"data:image/svg+xml,"& 
EncodeUrl(
    "<svg viewBox='0 0 "& Self.Width & " " & Self.Height & "' xmlns='http://www.w3.org/2000/svg'>
        <defs>
        <linearGradient id='atg' gradientUnits='objectBoundingBox' x1='0' y1='50%' x2='100%' y2='50%'>
            <stop offset='0%' stop-color='red'/>
            <stop offset='50%' stop-color='white'/>
            <stop offset='100%' stop-color='red'/>
            <animateTransform attributeName='gradientTransform' type='rotate' from='0,0.5,0.5' to='360,0.5,0.5' begin='0s' dur='6s' repeatCount='indefinite'/>
        </linearGradient>
        <pattern id='atp' patternUnits='userSpaceOnUse' x='0' y='0' width='50' height='50'>
            <rect x='0' y='0' width='50' height='50' fill='yellow'/>
            <circle cx='25' cy='25' r='25' fill='orange'/>
            <animateTransform attributeName='patternTransform' type='translate' from='0,0' to='100,100' begin='0s' dur='6s' repeatCount='indefinite'/>
        </pattern>
    </defs>
    <rect x='5%' y='5%' width='90%' height='90%' fill='url(#atg)' stroke='url(#atp)' stroke-width='10%'/>
</svg>"
)

NEXT ボタン

NEXT

"data:image/svg+xml,"& 
EncodeUrl(
    "<svg viewBox='0 0 "& Self.Width & " " & Self.Height & "' xmlns='http://www.w3.org/2000/svg'>
        <defs>
           <filter id='shadow'>
               <feDropShadow dx='10' dy='2' stdDeviation='2'/>
            </filter>
        </defs>

        <path stroke='black' stroke-width='2' fill='lightpink'>
            <animate attributeName='d' 
                calcMode='linear' 
                from='M 40 35 L 40 60 L 120 60 L 120 85 L 160 50 L 120 15 L 120 35 L 40 35' 
                to='M 30 30 L 30 75 L 130 75 L 130 95 L 180 50 L 130 5 L 130 30 L 30 30' 
                begin='0s' 
                dur='0.5s' 
                repeatCount='indefinite'/>
        </path>
        
        <text font-family='Algerian' stroke-width='0.3' stroke='#0022e0ff' fill='#18eee0ff' filter='url(#shadow)' >
            NEXT
            <animate attributeName='font-size' 
                calcMode='linear' 
                from='30' 
                to='40' 
                begin='0s' 
                dur='0.5s' 
                repeatCount='indefinite'/>
            <animate attributeName='x' 
                calcMode='linear' 
                from='50' 
                to='40' 
                begin='0s' 
                dur='0.5s' 
                repeatCount='indefinite'/>
            <animate attributeName='y' 
                calcMode='linear' 
                from='60' 
                to='65' 
                begin='0s' 
                dur='0.5s' 
                repeatCount='indefinite'/>
        </text>
    </svg>"
)

【Power Apps】あみだくじを作成してみよう!-その4-

はじめに

この記事は、

の続きとなります。
まだ上記をご覧になられていない場合は、先に確認することをお勧めします。

これまでの記事では、あみだくじを作成しました。
今回あみだくじの結果の作成方法を解説したいと思います。

あみだくじの結果を表示する

結果のテキストを表示する

結果の種類は、最初の画面で設定したように

  • 番号
  • 〇×
  • カスタム

があります。

f:id:koruneko:20201129234251p:plain

この結果は result に登録されていますので、 result に格納されている値を表示させます。
表示形式は、参加者情報と同じようにしたいと思います。

f:id:koruneko:20201129234821p:plain

例えば種類に番号を選んだ場合は、上記のようになります。

Image.Image

Concat(
    ForAll(
        Sequence(ParticipantDropdown.Selected.Value) As roopcnt,
        "<g>
            <rect x='" & Amidakuji.Width * roopcnt.Value / (ParticipantDropdown.Selected.Value + 1) - 50 & "' y='665' width='100' height='30'>
            </rect>
            <text x='" & Amidakuji.Width * roopcnt.Value / (ParticipantDropdown.Selected.Value + 1) - 
                            Len(Last(FirstN(result, roopcnt.Value)).result) * 8 & "' 
                  y='687' 
                  font-size='20'
                  fill='white'
                  font-weight='bold'
             > " &
                 Last(FirstN(result, roopcnt.Value)).result & "
             </text>
        </g>"
    ),
    Value
)

これで結果を表示できます。
仕組みは参加者の表示と同様ですので、今回は割愛します。

結果を順に表示させてしまうと、アニメーションを開始する前から自分の結果を辿ることができてしまいます。
別に悪いことではないですが、せっかくアニメーションを作ったのでこちらがアニメーションを行うまで参加者には結果の予測をさせたくないですw
よって、以下のようにします。

  • 結果のシャッフル
  • 参加者が結果まで到達したタイミングで結果を表示する

結果の順表示順をシャッフルする

表示結果をシャッフルするためには shuffle 関数 を用います。

shuffle 関数 を利用するタイミングには注意が必要です。

例えば、 Last(FirstN(Shuffle(result), roopcnt.Value)).result としてしまうと、この式は ForAll 関数 内にありますので、処理毎にシャッフルが行われてしまうので、表示されない結果やダブって表示される結果がでてきてしまいます。

ですので、Shuffle は ForAll を行う前に行いましょう。

ForAll 関数の前にシャッフルを行うとなると、シャッフルした結果をどこかに保存しておく必要があります。
Collect 関数 で保存する方法もありますが、一時的な利用なため、 With 関数 を今回は利用します。

Image.Image

With({s_result:Shuffle(result)},
   Concat(
       ForAll(
           Sequence(ParticipantDropdown.Selected.Value) As roopcnt,
           "<g>
               <rect x='" & Amidakuji.Width * roopcnt.Value / (ParticipantDropdown.Selected.Value + 1) - 50 & "' y='665' width='100' height='30'>
               </rect>
               <text x='" & Amidakuji.Width * roopcnt.Value / (ParticipantDropdown.Selected.Value + 1) - 
                               Len(Last(FirstN(s_result, roopcnt.Value)).result) * 8 & "' 
                     y='687' 
                     font-size='20'
                     fill='white'
                     font-weight='bold'
                > " &
                    Last(FirstN(s_result, roopcnt.Value)).result & "
                </text>
           </g>"
       ),
       Value
   )
)

これで結果がシャッフルされて表示されたかと思います。

参加者が結果まで到達したタイミングで結果を表示する

参加者が結果まで到達したタイミングで結果を表示するためには、 <animate>要素 を利用します。

参加者が結果に到達するのは、Start ボタンを押してから7秒後なので結果のアニメーション開始はStart ボタンを押してから7秒後とします。

Image.Image

With({s_result:Shuffle(result)},
    Concat(
        ForAll(
            Sequence(ParticipantDropdown.Selected.Value) As roopcnt,
            "<g>
                    <rect x='" & Amidakuji.Width * roopcnt.Value / (ParticipantDropdown.Selected.Value + 1) - 50 & "' y='665' width='100' height='30' fill='none'>" &
                    If(
                        isStart,
                        "<animate attributeName='fill' begin='7s' dur='3s' from='none' to='black' repeatCount='1' fill='freeze' />"
                    ) & "
                    </rect>
                    <text x='" & Amidakuji.Width * roopcnt.Value / (ParticipantDropdown.Selected.Value + 1) - 
                                Len(Last(FirstN(s_result, roopcnt.Value)).result) * 8 & "' 
                          y='687' 
                          font-size='20'
                          fill='none'
                          font-weight='bold'
                          > " &
                        Last(FirstN(s_result, roopcnt.Value)).result &
                        If(
                            isStart,
                            "<animate attributeName='fill' begin='7s' dur='5s' from='none' to='white' repeatCount='1' fill='freeze' />"
                        ) & "
                    </text>
                </g>"
        ),
        Value
    )
)

アニメーションの開始を7秒後と設定している箇所は begin='7s' です。

これで、参加者が結果に到達したタイミングで結果が表示されるようになりました!

おわりに

長くなりましたが、以上であみだくじの作成完了です!
SVG を利用せずに実現することも可能かと思いますが、処理がより複雑になり、また重くなってしまうかと思います。

こういったグラフィックが条件によって複雑に変化するようなものはSVG を利用することをおすすめします。

【Power Apps】あみだくじを作成してみよう!-その3-

はじめに

この記事は、

の続きとなります。
まだ上記をご覧になられていない場合は、先に確認することをお勧めします。

これまでの記事では、あみだくじの梯子部分を作成しました。
今回は参加者を表示させ、その参加者たちがあみだくじに沿って結果まで動く。というアニメーションの作成方法を解説したいと思います。

あみだくじに沿って参加者を動かすアニメーションを作成する

各参加者の移動ルートを計算する

animateMotion というSVG の要素を利用することで、指定の要素をモーションパスに沿って移動させるアニメーションを作成することが可能です。

これを利用して、参加者をあみだくじに沿って動かすアニメーションを作成したいと思います。

そのためにまず、各参加者が辿るあみだくじのルートを計算したいと思います。

このルートの計算は、あみだくじ画面が表示されたときに行うようにしたいと思います。

Amidakuji.OnVisible

Clear(route);
Clear(tmpCol);
ForAll(
    Sequence(ParticipantDropdown.Selected.Value, 1) As selectNum,
    Collect(tmpCol, {val:selectNum.Value, now:selectNum.Value});
    ForAll(
        Sequence(CountRows(amidakuji)) As countNum,
        If(
            LookUp(amidakuji, cnt = countNum.Value && num = Last(tmpCol).now, isline),
            Collect(tmpCol, {val:selectNum.Value, now:Last(tmpCol).now + 1});
            Collect(
                route,
                {num:selectNum.Value, cnt:countNum.Value, change:Last(tmpCol).now}
            ),
            LookUp(amidakuji, cnt = countNum.Value && num = Last(tmpCol).now - 1, isline),
            Collect(tmpCol, {val:selectNum.Value, now:Last(tmpCol).now - 1});
            Collect(
                route,
                {num:selectNum.Value, cnt:countNum.Value, change:Last(tmpCol).now}
            );
        );
    );
);

route というコレクションに各参加者が辿る経路を保存しています。
tmpCol というコレクションは、一時保存用のコレクションです。
このコレクションは最後のレコードを確認すれば、現在みているのが何人目の参加者用の経路で、現在何番目の縦線をみているのかを判断できるようになっています。

それぞれのコレクションには以下のような値が入ることを想定しています。

route

アイテム名 役割
num x人目の参加者
cnt y番目の箇所
change 変更後の縦線番号

tmpCol

アイテム名 役割
val x人目の参加者
now 現在みている縦線番号

なぜ tmpCol を使用しているか?というと、ForAll 関数 では UpdateContext 関数Set 関数 を用いることができないからです。

ForAll 関数の「アクションの実行」セクション を確認してみましょう。

順序の依存関係を避けるように注意してください。 したがって、UpdateContext、Clear、および ClearCollect 関数は、効果の影響を受けやすい変数を保持するために簡単に使用できるため、 ForAll 関数で使用することはできません。 Collect を使用することはできますが、レコードが追加される順序は定義されていません。

Collect、Remove、および Update を含むデータ ソースを変更する複数の関数は、変更されたデータ ソースを戻り値として返します。 これらの戻り値は大きく、ForAll テーブルのすべてのレコードに対して返された場合、かなりのリソースを使用する可能性があります。 また、ForAll が並行して動作でき、結果の取得からこれらの関数の副作用を切り離す場合があるので、これらの戻り値が予想どおりのものではないこともあります。 幸いにも、ForAll からの戻り値が実際には使用されない場合 (データ変更関数の場合によく見られます)、戻り値は作成されないため、リソースまたは順序付けに関する懸念事項はありません。 ただし、ForAll の結果およびデータ ソースを返す関数のいずれかを使用している場合、結果の構造方法について十分検討し、最初は小さいデータ セットで試してください。

上記のようにデータソースを更新するような関数は利用できないと明記されています。(2020年11月現在)

そこで今回 ForAll 関数 内で疑似的に変数を更新する方法として tmpCol を利用しているというわけです。

今回のルート計算では、ForAll 関数入れ子で利用しています。
なので、現在処理されているレコードの名前解決を行うためにAs 演算子 を利用しています。

まず最初のループ処理ですが、参加者の人数分処理を行います。

その際、 tmpCol にこれから計算を行う参加者の番号を設定します。
この tmpCol に設定された最新の値を取得するには、 Last 関数 を利用します。

子要素のForAll 関数では、 amidakuji コレクションのレコード数分、つまり'参加人数 × 15' 回処理を行います。
これにより、各参加者に対して、一要素ずつ横線が引かれているか?引かれていないか?を判断することができます。

今確認を行っている縦線の y 位置に対して、右方向に横線が引かれているか?は

LookUp(amidakuji, cnt = countNum.Value && num = Last(tmpCol).now, isline)

で確認を行っています。

左方向に横線が引かれているか?は

LookUp(amidakuji, cnt = countNum.Value && num = Last(tmpCol).now - 1, isline)

で判断しています。

まず横線が引かれているか否かをみるためには、 amidakuji コレクションを確認すればいいですね。
この amidakuji コレクションで線が引かれているかは isline を確認すればわかります。

なので、LookUp 関数 を用いて対象箇所(対象レコード)に線が引かれているかをそれぞれ判定しています。

さて、レコードを絞る条件ですが、まず num = Last(tmpCol).now により、現在みている縦線要素を絞っています。
続いて横線要素は cnt = countNum.Value で絞っています。
ただしこれで絞れるのは"右方向に横線が引かれているか?"です。
あみだくじでは、左方向に線が引かれているか?もみなくてはいけません。
この問題を解決する方法として、"ひとつ前の縦線から右方向に横線が引かれているか?"を判断することにより、左方向の横線の有無を判断しています。

ひとつ前の縦線をみたいですから、 num = Last(tmpCol).now - 1 で絞り込めますね。

あとは条件にあったときに、以下でコレクションに値をセットするだけですね。

Collect(tmpCol, {val:selectNum.Value, now:Last(tmpCol).now + 1});
Collect(
    route,
    {num:selectNum.Value, cnt:countNum.Value, change:Last(tmpCol).now}
)

これにより、各参加者が辿る横線をを把握することができました。

余談ですが、今回 amidakuji コレクションのレコード数分だけループしましたが、Filter 関数 を用いて、無駄なループ回数を減らしたほうが処理速度の面から考えるといいかもですね。
ただ、Filter 処理を行うときも内部ではループ処理を行っていると思うので、どちらが本当に性能がいいのか?というのはちょっとわかりません・・・
Power Apps で性能を検証するツールとかあるのだろうか?というよりそこまでしたい需要あるのですかね?

参加者を表示する

参加者の名前は最初の画面で入力された情報を用います。
参加者の名前は ParticipantGallery のテキスト入力に記載されていますので、そこから取得します。

参加者の名前の外観ですが、黒で塗りつぶした四角形に白文字で参加者の名前を表示するようにしたいと思います。

f:id:koruneko:20201127003719p:plain

このような感じにします。

Image.Image

Concat(
    ForAll(
        Sequence(ParticipantDropdown.Selected.Value) As roopcnt,
         "<g>
             <rect x='" & Amidakuji.Width * roopcnt.Value / (ParticipantDropdown.Selected.Value + 1) - 50 & "' y='85' width='100' height='30' fill='black'/>
             <text x='" & Amidakuji.Width * roopcnt.Value / (ParticipantDropdown.Selected.Value + 1) - 
                         Len(Last(FirstN(ParticipantGallery.AllItems, roopcnt.Value)).ParticipantTextInput.Text) * 10 & "' 
                   y='106' 
                   font-size='20' 
                   fill='white' 
                   font-weight='bold'
                   > " &
                 Last(FirstN(ParticipantGallery.AllItems, roopcnt.Value)).ParticipantTextInput.Text & "
             </text>
         </g>"
    ),
    Value
)

まずループの回数ですが、参加人数分だけループを行いたいので、参加人数を選択するドロップダウンで選択された数だけループさせます。 ParticipantDropdown.Selected.Value

<g>要素 他のSVG 要素をグループ化するために用いています。
こちらの要素がなくとも問題なく表示はされます。
しかし、参加者情報はあみだくじに沿って動くようにしたいです。
その際、<rect>要素<text>要素 両方に同じ設定を記載するのは無駄ですね。
なので、これらはグループ化してあげて纏めてアニメーションを設定してあげましょう!

<g>要素 の中で記載されている<rect>要素 は黒塗りの四角を、<text>要素 では参加者の名前をそれぞれ表示するよう記載しています。

文字の表示は Last(FirstN(ParticipantGallery.AllItems, roopcnt.Value)).ParticipantTextInput.Text で行っています。
これにより、最初の画面で記載された順に参加者の名前が表示されますね。

参加者が辿るルートを表示する

参加者があみだくじを辿るアニメーションを作成するには、冒頭でanimateMotion というSVG の要素を利用すると説明しましたが、こいつを利用するためには、アニメーションの軌跡が作成されている必要があります。
ですので、「各参加者の移動ルートを計算する」で得られた情報をもとに各参加者の軌跡を作成したいと思います。

なおこの線は見えなくてよいため透明な線で作成します。

Image.Image

Concat(
        ForAll(
            Sequence(ParticipantDropdown.Selected.Value) As roopcnt,
            "<path d='M " & Amidakuji.Width * roopcnt.Value / (ParticipantDropdown.Selected.Value + 1) & " 100 " &
             "L " & Amidakuji.Width * roopcnt.Value / (ParticipantDropdown.Selected.Value + 1) & " " &
              First(Filter(route, num = roopcnt.Value)).cnt * 30 + 100 &
            Concat(
                ForAll(
                    Sequence(CountRows(Filter(route, num = roopcnt.Value))) As routeNum,
                    " L " &  Amidakuji.Width * Last(FirstN(Filter(route, num = roopcnt.Value), routeNum.Value)).change / (ParticipantDropdown.Selected.Value + 1) & " " &
                      Last(FirstN(Filter(route, num = roopcnt.Value), routeNum.Value)).cnt * 30 + 100 &
                       "L " & Amidakuji.Width * Last(FirstN(Filter(route, num = roopcnt.Value), routeNum.Value)).change / (ParticipantDropdown.Selected.Value + 1) & " " &
                        Last(FirstN(Filter(route, num = roopcnt.Value), routeNum.Value + 1)).cnt * 30 + 100
                ),
                Value
            ) & 
            " L " & Amidakuji.Width * Last(Filter(route, num = roopcnt.Value)).change / (ParticipantDropdown.Selected.Value + 1) & " 650'" &
             " stroke='none' fill='transparent' stroke-width='1'  id='theMotionPath " & roopcnt.Value &"' />"
        ),
        Value
)

親要素のループでは、1人目の参加者、2人目の参加者・・・といった順でループするようにしています。

まずはあみだくじの最初の横線が引かれるヶ所まで縦線を引きます。

Sequence(ParticipantDropdown.Selected.Value) As roopcnt,
"<path d='M " & Amidakuji.Width * roopcnt.Value / (ParticipantDropdown.Selected.Value + 1) & " 100 " &
 "L " & Amidakuji.Width * roopcnt.Value / (ParticipantDropdown.Selected.Value + 1) & " " &
  First(Filter(route, num = roopcnt.Value)).cnt * 30 + 100

子要素のループでは、 計算したルートが格納されている route コレクションの、現在ルートを作成しているX人目の参加者分だけループを行うようにしています。

このループでは直線コマンド L が2つ存在します。

まず1つめの L ですが、これは横線を引いています。

" L " &  Amidakuji.Width * Last(FirstN(Filter(route, num = roopcnt.Value), routeNum.Value)).change / (ParticipantDropdown.Selected.Value + 1) & " " &
  Last(FirstN(Filter(route, num = roopcnt.Value), routeNum.Value)).cnt * 30 + 100

2つめの L ですが、これは縦線を引いています。

"L " & Amidakuji.Width * Last(FirstN(Filter(route, num = roopcnt.Value), routeNum.Value)).change / (ParticipantDropdown.Selected.Value + 1) & " " &
 Last(FirstN(Filter(route, num = roopcnt.Value), routeNum.Value + 1)).cnt * 30 + 100

これを繰り返すことで、格子部分の軌跡を作成できますね。

最後に親要素の入れ子の最後であみだくじの最後まで縦線を引きます。

" L " & Amidakuji.Width * Last(Filter(route, num = roopcnt.Value)).change / (ParticipantDropdown.Selected.Value + 1) & " 650'"

色は無しにしたいので、 stroke='none' を、また、これらをアニメーションの軌跡に設定したいので id='theMotionPath " & roopcnt.Value と設定しましょう。
この id で、アニメーションの軌跡を判断します。

参加者をあみだくじに沿って動かす

参加者の名前をあみだくじに沿って動かすようにします。
先ほど作成した参加者の名前を表示するための記載を以下のように置き換えてください。

Image.Image

Concat(
    ForAll(
        Sequence(ParticipantDropdown.Selected.Value) As roopcnt,
        If(
            isStart,
            "<g>
                <rect x='-50' y='-15' width='100' height='30' fill='black'/>
                <text x='-" & Len(Last(FirstN(ParticipantGallery.AllItems, roopcnt.Value)).ParticipantTextInput.Text) * 10 & "' 
                      y='6' 
                      font-size='20' 
                      fill='white' 
                      font-weight='bold'
                      > " &
                    Last(FirstN(ParticipantGallery.AllItems, roopcnt.Value)).ParticipantTextInput.Text & "
                </text>
                <animateMotion dur='7s' repeatCount='1' fill='freeze' >
                    <mpath xlink:href='#theMotionPath " & roopcnt.Value &"' />
                </animateMotion>
            </g>",
            "<g>
                <rect x='" & Amidakuji.Width * roopcnt.Value / (ParticipantDropdown.Selected.Value + 1) - 50 & "' y='85' width='100' height='30' fill='black'/>
                <text x='" & Amidakuji.Width * roopcnt.Value / (ParticipantDropdown.Selected.Value + 1) - 
                            Len(Last(FirstN(ParticipantGallery.AllItems, roopcnt.Value)).ParticipantTextInput.Text) * 10 & "' 
                      y='106' 
                      font-size='20' 
                      fill='white' 
                      font-weight='bold'
                      > " &
                    Last(FirstN(ParticipantGallery.AllItems, roopcnt.Value)).ParticipantTextInput.Text & "
                </text>
            </g>"
        )
    ),
    Value
)

アニメーションはボタンが押されたときに開始したいので、If 関数 を用います。

ボタンを配置して、 OnSelectUpdateContext({isStart:true}); を設定しておいてください。

まずボタンが押されていないとき、つまり、If 関数の判定がfalse のときの記載は先ほどと同様なので省きます。

ボタンが押されたとき、先ほどの軌跡に沿ってアニメーションを行わせたいので

<animateMotion dur='7s' repeatCount='1' fill='freeze' >
    <mpath xlink:href='#theMotionPath " & roopcnt.Value &"' />
</animateMotion>

を追加しています。

<mpath xlink:href='#theMotionPath " & roopcnt.Value &"' /> でどの軌跡に沿ってアニメーションを行うか判断しています。

また、アニメーション終了後終了地点で止まってほしいので、 fill='freeze' としています。

四角形やテキストに設定している座標も異なっていますが、これは先ほど作成した軌跡との相対座標を設定しているからです。

さぁ動かしてみよう!

ではここまでの設定が完了しましたら、ボタンを押して、いざアニメーションを動作させてみましょう!!

・・・

如何でしょうか?
想定通り動きましたか?

恐らくボタンを押すととあみだくじが消えてしまったかと思います。

xmlns:xlink='http://www.w3.org/1999/xlink' を、 svg viewBox に追記してください。

<svg viewBox='0 0 "& Self.Width & " " & Self.Height & "' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'> 

こちらを追記するとアニメーションが動作します。
気になる人は是非調べてみてください。

おわりに

次回はあみだくじの結果を表示する方法を纏めようと思います。
このシリーズもいよいよ最後です。

もし、SVG が上手く表示されないーという場合はスペースもきちんと記載しているか確認してください。
スペースの設定もSVG の表示には重要な要素です。

【Power Apps】あみだくじを作成してみよう!-その2-

はじめに

この記事は前回投稿した、【Power Apps】あみだくじを作成してみよう!-その1-の続きです。
まだご覧になられていない方は是非ご確認ください。

前回はあみだくじ作成に必要な情報を作成しました。
今回はいよいよあみだくじの作成を行っていこうと思います!

あみだくじを作成する

とりあえず全容

まずはとりあえずコピペ用に全容を貼ります。

長いので折りたたんで表示しています。 "data:image/svg+xml,"& EncodeUrl( "<svg viewBox='0 0 "& Self.Width & " " & Self.Height & "' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' > " & Concat( ForAll( Sequence(ParticipantDropdown.Selected.Value, 1), "<path d='M " & Amidakuji.Width * Value / (ParticipantDropdown.Selected.Value + 1) & " 100 L " & Amidakuji.Width * Value / (ParticipantDropdown.Selected.Value + 1) & " 650' stroke='black' fill='transparent' stroke-width='2' />" ), Value ) & Concat( ForAll( Sequence(CountRows(amidakuji)), If( Last(FirstN(amidakuji, Value)).isline, "<path d='M " & Amidakuji.Width * Last(FirstN(amidakuji, Value)).num / (ParticipantDropdown.Selected.Value + 1) & " " & Last(FirstN(amidakuji, Value)).cnt * 30 + 100 & " L " & Amidakuji.Width * (Last(FirstN(amidakuji, Value)).num + 1) / (ParticipantDropdown.Selected.Value + 1) & " " & Last(FirstN(amidakuji, Value)).cnt * 30 + 100 & "' stroke='black' fill='transparent' stroke-width='2' />" ) ), Value ) & Concat( ForAll( Sequence(ParticipantDropdown.Selected.Value) As roopcnt, "<path d='M " & Amidakuji.Width * roopcnt.Value / (ParticipantDropdown.Selected.Value + 1) & " 100 " & "L " & Amidakuji.Width * roopcnt.Value / (ParticipantDropdown.Selected.Value + 1) & " " & First(Filter(route, num = roopcnt.Value)).cnt * 30 + 100 & Concat( ForAll( Sequence(CountRows(Filter(route, num = roopcnt.Value))) As routeNum, " L " & Amidakuji.Width * Last(FirstN(Filter(route, num = roopcnt.Value), routeNum.Value)).change / (ParticipantDropdown.Selected.Value + 1) & " " & Last(FirstN(Filter(route, num = roopcnt.Value), routeNum.Value)).cnt * 30 + 100 & "L " & Amidakuji.Width * Last(FirstN(Filter(route, num = roopcnt.Value), routeNum.Value)).change / (ParticipantDropdown.Selected.Value + 1) & " " & Last(FirstN(Filter(route, num = roopcnt.Value), routeNum.Value + 1)).cnt * 30 + 100 ), Value ) & " L " & Amidakuji.Width * Last(Filter(route, num = roopcnt.Value)).change / (ParticipantDropdown.Selected.Value + 1) & " 650'" & " stroke='none' fill='transparent' stroke-width='1' id='theMotionPath " & roopcnt.Value &"' />" ), Value ) & Concat( ForAll( Sequence(ParticipantDropdown.Selected.Value) As roopcnt, If( isStart, "<g> <rect x='-50' y='-15' width='100' height='30' fill='black'/> <text x='-" & Len(Last(FirstN(ParticipantGallery.AllItems, roopcnt.Value)).ParticipantTextInput.Text) * 10 & "' y='6' font-size='20' fill='white' font-weight='bold' > " & Last(FirstN(ParticipantGallery.AllItems, roopcnt.Value)).ParticipantTextInput.Text & " </text> <animateMotion dur='7s' repeatCount='1' fill='freeze' > <mpath xlink:href='#theMotionPath " & roopcnt.Value &"' /> </animateMotion> </g>", "<g> <rect x='" & Amidakuji.Width * roopcnt.Value / (ParticipantDropdown.Selected.Value + 1) - 50 & "' y='85' width='100' height='30' fill='black'/> <text x='" & Amidakuji.Width * roopcnt.Value / (ParticipantDropdown.Selected.Value + 1) - Len(Last(FirstN(ParticipantGallery.AllItems, roopcnt.Value)).ParticipantTextInput.Text) * 10 & "' y='106' font-size='20' fill='white' font-weight='bold' > " & Last(FirstN(ParticipantGallery.AllItems, roopcnt.Value)).ParticipantTextInput.Text & " </text> </g>" ) ), Value ) & With({s_result:Shuffle(result)}, Concat( ForAll( Sequence(ParticipantDropdown.Selected.Value) As roopcnt, "<g> <rect x='" & Amidakuji.Width * roopcnt.Value / (ParticipantDropdown.Selected.Value + 1) - 50 & "' y='665' width='100' height='30' fill='none'>" & If( isStart, "<animate attributeName='fill' begin='7s' dur='3s' from='none' to='black' repeatCount='1' fill='freeze' />" ) & " </rect> <text x='" & Amidakuji.Width * roopcnt.Value / (ParticipantDropdown.Selected.Value + 1) - Len(Last(FirstN(s_result, roopcnt.Value)).result) * 8 & "' y='687' font-size='20' fill='none' font-weight='bold' > " & Last(FirstN(s_result, roopcnt.Value)).result & If( isStart, "<animate attributeName='fill' begin='7s' dur='5s' from='none' to='white' repeatCount='1' fill='freeze' />" ) & " </text> </g>" ), Value ) ) & "</svg>" )

長い!!
いやー長いですね...
ローコードとは...

ここで注目して欲しいのはこのあみだくじの作成には、SVG という画像フォーマット技術を利用している。ということです。
SVG に関しては下記のリファレンスをご覧ください。(また今度SVG について纏めた記事書きたいと思います。)

developer.mozilla.org

また、コードをよくみてみると同じような塊になっているということがわかるかと思います。

ForAll 関数で繰り返し処理を行いコレクションを作成し、そのコレクションのカラムたちをConcat 関数で文字列として繋げていますね。
これによりそれぞれある要素を作成しているということです。

それでは細かく処理をみていきましょう!

SVGを記載するための準備

Power Apps でSVG を表示するためには、イメージコントロールを利用します。
「メディア」より「画像」を選択して、以下のように設定してみてください。

Image.Image

"data:image/svg+xml,"& 
EncodeUrl(
    "<svg viewBox='0 0 "& Self.Width & " " & Self.Height & "' xmlns='http://www.w3.org/2000/svg' >       
        <circle cx='50' cy='50' r='50' fill='black'/>
    </svg>"
)

これにより、「画像」コントロールの左上隅に黒い丸が表示されたかと思います。

黒い丸を表している箇所が4行目になります。

下記のような記述方法を基本として、SVG を作成していきます。

"data:image/svg+xml,"& 
EncodeUrl(
    "<svg viewBox='0 0 "& Self.Width & " " & Self.Height & "' xmlns='http://www.w3.org/2000/svg' >       
        [ここにSVG 要素を記述する]
    </svg>"
)

「画像」コントロールは画面いっぱいに設定しておいてください。

SVG をより詳しく見たい方はこちらをご確認ください。

developer.mozilla.org

あみだくじの縦線を作成する

まずはあみだくじの縦線を作成します。
縦線は、以下のように作成します。

Image.Image

"data:image/svg+xml,"& 
EncodeUrl(
    "<svg viewBox='0 0 "& Self.Width & " " & Self.Height & "' xmlns='http://www.w3.org/2000/svg' >       
        <path d='M 200 100 L 200 650'
          stroke='black' fill='transparent' stroke-width='2' />
    </svg>"
)

これで縦線が1本表示されるようになったかと思います。

ただ縦線は参加人数分作成したいですね。
そのためには上記のような記載を参加人数分作成したいです。
そこで利用するのが、ForAll 関数です。

ただ、ForAll 関数で得られる結果はコレクションです。
ここに設定する値は文字列です。
そんな問題を解決するのがConcat 関数です。

上記を踏まえて以下のように記載します。

Image.Image

"data:image/svg+xml,"& 
EncodeUrl(
    "<svg viewBox='0 0 "& Self.Width & " " & Self.Height & "' xmlns='http://www.w3.org/2000/svg' > " &
        Concat(
                ForAll(
                    Sequence(ParticipantDropdown.Selected.Value, 1),
                    "<path d='M " & Amidakuji.Width * Value / (ParticipantDropdown.Selected.Value + 1) & " 100 
                     L " &  Amidakuji.Width * Value / (ParticipantDropdown.Selected.Value + 1) & " 650'
                      stroke='black' fill='transparent' stroke-width='2' />"
                ),
                Value
        ) & "
    </svg>"
)

簡単に解説します。

繰り返し処理を行う回数は Sequence(ParticipantDropdown.Selected.Value, 1) 回です。
ParticipantDropdown.Selected.Value では参加人数が選択されていますね。

線の位置はそれぞれx座標の場所が異なりますね。
なのでループごとにx座標の値が等間隔で変化するように、また、参加人数によって線の間隔が変わるように式を作成しています。( Amidakuji.Width * Value / (ParticipantDropdown.Selected.Value + 1) )
ここで Amidakuji.Width は画面の幅を指定しています。

y座標の始点/終点は固定なので、始点:100、終点:650 で固定しています。

ちなみに始点の設定がM、終点の設定がLだと思ってください。

あみだくじの横線を作成する

続いてあみだくじの横線を作成します。

あみだくじの横線要素は以前 amidakuji というコレクションに作成しましたね。 このコレクションには、

アイテム名 役割
num x番目の箇所
cnt y番目の箇所(15分割されている)
isline 線を引くかどうか

が設定されています。

このコレクションに設定されているレコード数だけループ処理を行う必要があります。
なので、 Sequence(CountRows(amidakuji)) により、 amidakuji のレコード数分処理を行います。

上記を踏まえて先ほどの Concat() に続けて、& で繋げて以下のように記載します。

        Concat(
            ForAll(
                Sequence(CountRows(amidakuji)),
                If(
                    Last(FirstN(amidakuji, Value)).isline,
                    "<path d='M " & Amidakuji.Width * Last(FirstN(amidakuji, Value)).num / (ParticipantDropdown.Selected.Value + 1) & " 
                     " & Last(FirstN(amidakuji, Value)).cnt * 30 + 100 & " 
                      L " &  Amidakuji.Width * (Last(FirstN(amidakuji, Value)).num + 1) / (ParticipantDropdown.Selected.Value + 1) & " 
                       " & Last(FirstN(amidakuji, Value)).cnt * 30 + 100 & "'
                        stroke='black' fill='transparent' stroke-width='2' />"
                )
            ),
            Value
        )

現在みるべきレコードは、 Last(FirstN(amidakuji, Value)) で取得しています。

横線を引くのは、 islinetrue のときだけなので、If 関数により判断を行っています。

横線のx座標の式は縦線のx座標の式とほぼ同様です。

[横線の式]
Amidakuji.Width * 
+ Last(FirstN(amidakuji, Value)).num
 / (ParticipantDropdown.Selected.Value + 1)

[縦線の式]
Amidakuji.Width * 
- Value
 / (ParticipantDropdown.Selected.Value + 1)

このように valueLast(FirstN(amidakuji, Value)).num が異なっています。

横線の value はなにを表していたでしょうか?

縦線のときの value は、x本目の縦線ということを表していました。
これを今回の横線の作成の式に当てはめると、 amidakujinum 要素になります。
よって、 Last(FirstN(amidakuji, Value)).num となっています。

始点がx本目の縦線からはじまると、終点はx + 1本目の縦線になりますね。
なので、 Amidakuji.Width * (Last(FirstN(amidakuji, Value)).num + 1) / (ParticipantDropdown.Selected.Value + 1) となります。

最後にy座標ですが、これは、 amidakujicnt 要素に設定されています。
なので Last(FirstN(amidakuji, Value)).cnt * 30 + 100 としています。
* 30 は線の間隔を、 + 100 は一番上の横線が始まるy座標の初期位置をそれぞれ調整しています。

おわり

これで、あみだくじの梯子が作成できたかと思います。

f:id:koruneko:20201108034616p:plain

お疲れさまでした!

次回以降は参加者の名前をあみだくじに表示し、その名前があみだくじに沿って結果まで動くというアニメーションの作成方法を解説したいと思います。

【Power Apps】あみだくじを作成してみよう!-その1-

今回作成するもの

今回は以下のようなあみだくじをPower Apps で作成しようと思います!

使用している要素

Power Apps のオブジェクトとして利用しているのはたったのこれだけです。

f:id:koruneko:20201101224935p:plain

ではどうやってあみだくじを作成したり、アニメーションをつけているかというと、SVG を活用しています。
SVG の詳しい解説はこのシリーズでは行う予定はありません。
その解説についてはまたの機会に行おうと思いますー。(私も最近少し触ってみただけですが...)

ホーム画面を作成する

まずは最初に表示されたホーム画面を作成しようと思います。

f:id:koruneko:20201101225539p:plain

この画面では特に難しい操作は行っておらず、SVG も利用していません。

参加者入力欄を作成する

参加者入力欄を作成するために以下のオブジェクトを配置します。

f:id:koruneko:20201101230113p:plain

それぞれ以下のような役割をもっています。

オブジェクト 役割
ParticipantTitleLabel 参加人数入力欄用ラベル
ParticipantDropdown 参加人数を選択するためのドロップダウン
ParticipantGallery 参加者の名前入力用ギャラリー
ParticipantLabel X人目の参加者入力用ラベル
ParticipantTextInput X人目の参加者名入力欄

それぞれ以下のように設定しましょう。
(座標などの細かい設定値の説明は今回省きます。)

まずドロップダウンは2~15まで選択可能なようにしたいので以下のように設定します。

ParticipantDropdown.Items

Sequence(14, 2)

Sequence 関数に関しては以前纏めたこちらの記事をご覧ください。
【Power Apps】Sequence 関数を使ってみよう!

参加者の名前入力欄はドロップダウンで選択した値と同じ数だけ表示させたいです。
なので以下のように設定します。

ParticipantGallery.Items

Sequence(ParticipantDropdown.Selected.Value)

以上で参加者入力欄の作成は完了です。
あとは適宜座標やサイズを調整してください。

結果入力欄を作成する

結果入力欄を作成するために以下のオブジェクトを配置します。

f:id:koruneko:20201101232058p:plain

それぞれ以下のような役割をもっています。

オブジェクト 役割
ResultTitleLabel 結果のタイプ選択用ラベル
ResultDropdown 結果のタイプ選択用ドロップダウン
True-FalseLabel 〇の数入力欄用ラベル
True-FalseDropdown 〇の数を選択するためのドロップダウン
ResultGallery 結果(カスタム値)入力用ギャラリー
ResultLabel X個目の結果入力用ラベル
ResultTextInput X個目の結果入力欄

参加者入力欄とはぼ同じですが、以下のように設定します。

結果のタイプには、"番号", "〇×", "カスタム" の3種類の選択肢を用意したいと思うので、以下のように設定します。

ResultDropdown.Items

["番号", "〇×", "カスタム"]

このドロップダウンで選択された結果によって、〇の数入力欄とカスタム結果入力欄の表示を制御したいと思います。
そのため以下のように設定します。

True-FalseLabel.Visible & True-FalseDropdown.Visible

ResultDropdown.SelectedText.Value = "〇×"

ResultGallery.Visible

ResultDropdown.SelectedText.Value = "カスタム"

結果に設定する〇の数は最低1で最大が参加者の人数 - 1となるかと思います。
なので以下のように設定します。

True-FalseDropdown.Items

Sequence(ParticipantDropdown.Selected.Value - 1)

カスタム結果入力欄は参加人数と同じだけ用意する必要があります。
なので以下のように設定します。

ResultGallery.Items

Sequence(ParticipantDropdown.Selected.Value)

以上で結果入力欄の作成は完了です。
あとは適宜座標やサイズを調整してください。

あみだくじ!ボタンを作成する

あみだくじ!ボタンを押すとあみだくじの画面に遷移します。
なのでこのボタンが押せる状態にある = ホーム画面で必要な値の入力/選択がすべて完了している という状態にしたいと思います。

では、「ホーム画面で必要な値の入力/選択がすべて完了している」とはどのような状態でしょうか?

今回は以下のような2つの条件だと思います。

  • 参加者名が参加人数分全て埋まっている。
  • 結果のタイプで「カスタム」が選択されていた場合、カスタム値が参加人数分全て埋まっている。

上記条件を式に表すと例えば以下のように表現することができます。

ボタンを配置して以下のように設定します。

AmidakujiButton.DisplayMode

If(
    CountIf(
        ForAll(
            Sequence(ParticipantDropdown.Selected.Value),
            Last(FirstN(ParticipantGallery.AllItems, Value)).ParticipantTextInput.Text
        ),
        Value <> ""
    ) = ParticipantDropdown.Selected.Value &&
    If(
        ResultDropdown.SelectedText.Value = "カスタム",
        CountIf(
            ForAll(
                Sequence(ParticipantDropdown.Selected.Value),
                Last(FirstN(ResultGallery.AllItems, Value)).ResultTextInput.Text
            ),
            Value <> ""
        ) = ParticipantDropdown.Selected.Value,
        true
    ),
    DisplayMode.Edit,
    DisplayMode.Disabled
)

ForAll 関数内ではギャラリー内のテキスト入力の値を1~参加人数分まで取得しています。
これにより、参加人数分のギャラリー内のテキスト入力の値が設定されたコレクションが作成されます。

Countif 関数内ではForAll 関数で作成されたコレクションに対して、値が空白("")でないカラムが何個あるのか数えています。
この値が、参加人数と同じだった = 参加人数分テキスト入力が入力されている と判定できますね。

続いてこのボタンが押されたときの処理です。
このボタンが押されたときには以下の処理をしたいと思います。

  1. あみだくじのイメージ(どこに横線が引かれるか?)を判定するためのコレクションを作成する。
  2. 結果を格納したコレクションを作成する。
  3. あみだくじ画面へ遷移する。

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

AmidakujiButton.OnSelect

Clear(amidakuji);
ForAll(
    Sequence(15) As cnt,
    ForAll(
        Sequence(ParticipantDropdown.Selected.Value - 1) As num,
        Collect(
            amidakuji,
            {num:num.Value, cnt:cnt.Value, isline:If(!Last(amidakuji).isline, RoundDown(Rand() * 5, 0) = 0)}
        )
    )
);
Clear(result);
ForAll(
    Sequence(ParticipantDropdown.Selected.Value),
    Switch(
        ResultDropdown.SelectedText.Value,
        "番号", Collect(result, {result:Text(Value)}),
        "〇×", Collect(result, {result:If(Value <= 'True-FalseDropdown'.Selected.Value, "〇", "×")}),
        "カスタム", Collect(result, {result:Last(FirstN(ResultGallery.AllItems, Value)).ResultTextInput.Text})
    )
);
Navigate(Amidakuji, ScreenTransition.Cover)

急に式がちょっと難しくなっちゃいましたが、1つずつみていきましょう。

まずは、「あみだくじのイメージ(どこに横線が引かれるか?)を判定するためのコレクションを作成する。」です。
これは、

Clear(amidakuji);
ForAll(
    Sequence(15) As cnt,
    ForAll(
        Sequence(ParticipantDropdown.Selected.Value - 1) As num,
        Collect(
            amidakuji,
            {num:num.Value, cnt:cnt.Value, isline:If(!Last(amidakuji).isline, RoundDown(Rand() * 5, 0) = 0)}
        )
    )
);

ここの式で表現しています。
ForAll 関数が入れ子にねっちゃっていますね...

まず今回作成するあみだくじですが、以下ルールに基づいて作成を行っています。

  • 横線は一つの縦線につき最大15本引かれる
  • 横線は隣接する網掛けとは重ならないようにする
  • 横線がどこに引かれるかはランダムとする

まず最初の条件の「横線は一つの縦線につき最大15本引かれる」ですが、これを実現するためにまず最初のForAll で15回ループするようにしています。
Sequence(15) As cnt
このときなぜAs 句を用いているかというと、その次に宣言するForAll 関数で値が被らないようにするためです。
この1つ目のForAll の値を用いたい場合は cnt.Value と記載することで利用することができます。

続いて2つ目のForAll 関数では、横線を引くべき行分だけループさせています。
文章では伝え難いので図で説明すると以下の箇所のことを指しています。

f:id:koruneko:20201102002411p:plain

これは、縦線の数(参加者の数) - 1個存在すると思います。
なので、
Sequence(ParticipantDropdown.Selected.Value - 1) As num
としています。

これらのループ内でコレクションを作成します。

amidakuji というコレクション名には、num, cnt, isline というアイテムが存在します。
それぞれ以下のような値が入ることを想定しています。

アイテム名 役割
num x番目の箇所
cnt y番目の箇所
isline 線を引くかどうか

これにより、どこに線が引かれるか判断できますね。

num にはx番目の箇所を設定したいので、2つ目のループの num.Value を設定します。

cnt にはy番目の箇所を設定したいので、1つ目のループの cnt.Value を設定します。

isline には上で説明したようなルールを適用したいので、1つ前のレコードの値を確認して線が引かれているようなら線を引かず、引かれていないようなら、5分の1の確率で線を引くようにします。
これを式に表すと以下のようになります。
If(!Last(amidakuji).isline, RoundDown(Rand() * 5, 0) = 0)

これで、これまで説明した要件を満たせますが、この実装では以下のような問題もあります。

  • 右端に横線が引かれた場合、その1つ下の左端には線が確定で引かれなくなる。
  • (4 / 5)15 * ([参加人数] - 1) の確率でどこにも線が引かれない。

今回簡単に作成するため、また、上記問題はそこまであみだくじの作成に影響しないため無視しましたが、気になる方は上記問題を解決した実装を行ってみてください!

続いて、「結果を格納したコレクションを作成する。」ですが、これは以下で行っています。

Clear(result);
ForAll(
    Sequence(ParticipantDropdown.Selected.Value),
    Switch(
        ResultDropdown.SelectedText.Value,
        "番号", Collect(result, {result:Text(Value)}),
        "〇×", Collect(result, {result:If(Value <= 'True-FalseDropdown'.Selected.Value, "〇", "×")}),
        "カスタム", Collect(result, {result:Last(FirstN(ResultGallery.AllItems, Value)).ResultTextInput.Text})
    )
);

結果は、

  • 参加者と同じ数だけ用意
  • ドロップダウで選択されたタイプによって設定する値が異なる

という特性があります。

まず、「参加者と同じ数だけ用意」ですが、これは参加者の数分だけループを実施すればよいですね。
Sequence(ParticipantDropdown.Selected.Value)

続いて、「ドロップダウで選択されたタイプによって設定する値が異なる」ですが、これはswitch 関数を活用すれば実現できます。

「番号」が選択されたときは、ループのカウントを設定すればOKです。
ただし、他のタイプが選択されたときは、数値型だけでなく文字型が入るということに注意が必要です。
そのため、数値型ではなく文字型にするためにText 関数を利用しましょう。
Text(Value)

「〇×」が選択されたときは、〇の数を決めるドロップダウンで選択された値の数とループのカウントを比較し、ループのカウントが〇の数を決めるドロップダウンで選択された値の数以下だった場合は〇を、より大きかった場合は×をそれぞれ設定すれば、要件を満たすことができます。
If(Value <= 'True-FalseDropdown'.Selected.Value, "〇", "×")

「カスタム」が選択されたときは、ギャラリー内のテキスト入力の値を設定するだけで、OKです。
以下のように設定すれば要件を満たすことが可能です。
Last(FirstN(ResultGallery.AllItems, Value)).ResultTextInput.Text

これまで説明したForAll の前には、それぞれClear 関数を用いて対象のコレクションをクリアしていますが、これはそのままの意味で、ここに設定する値は毎回新規で作成したいためです。

最後にNavigate 関数であみだくじ画面へ遷移を行っています。
Navigate(Amidakuji, ScreenTransition.Cover)

おわり

以上でホーム画面の作成は完了です!
おつかれさまですっ!!!

これであみだくじの作成に必要な情報はほぼほぼ作成することがきました。

次回はいよいよあみだくじの作成方法を解説したいと思います。
SVG の解説がメインになりそうです。

それではーノシ

【Power Apps】超簡単!Power Apps でクリック/選択された座標を取得する

今回やること

今回は下記のようにクリック/選択された座標をPower Apps で取得する方法を紹介したいと思います。

方針

今回は以下の方針でクリックされた座標を取得できるようにしたいと思います。

  • 複雑なロジックは極力しようしない
  • 動作が重くなるような実装は避ける

Power Apps はローコードを売りにしていますので、上記方針で作成しています。

クリック/選択された座標をPower Apps で取得する

まずは「縦方向(空)」のギャラリーを配置して以下のように設定してください。

Gallery1.X & Gallery1.Y

0

Gallery1.Width

Parent.Width

Gallery1.Height

Parent.Height

Gallery1.TemplatePadding

0

ギャラリーの配下に「評価」を追加して以下のように設定します。

Rating1.X & Rating1.Y

0

Rating1.Width

Parent.Width

Rating1.Default

0

Rating1.ShowValue

false

X座標の選択された座標を取得する仕組みを作成する

X座標の選択された座標を取得する仕組みには先ほど作成した「評価」を用います。
以下のように設定を変更してください。

Rating1.Max

100

ここでMaxに設定した値が大きければ大きいほど選択したX座標の精度が高くなります。
ここの値を大きくしてもそれほど性能に影響しないので、今回は100に設定しています。
なおここに設定できる値は最大でも100までなので注意しましょう。

上記のようにどこを選択しているのか判定できますね。
あとはこの値をもとに座標を算出してあげればよいというわけです。

Y座標の選択された座標を取得する仕組みを作成する

Y座標の選択された座標を取得する仕組みにはギャラリーを用います。
今回は以下のように設定することとします。

Gallery1.TemplateSize

40

Gallery1.Items

Sequence(Self.Height / Self.TemplateHeight)

上記の設定により、TemplateSize に設定した値によって表示されるカラムの数が変化します。
具体的にいうと、TemplateSize の値が小さいほど、カラムの数は増えますし、値が大きいほどカラムの数は減ります。

当然カラムの数が多いほうが、Y座標の判定の精度は高くなりますが、その分アプリが重くなってしまいます。
ここはY座標の精度をとるか、アプリの重さをとるかのトレードオフとなりますね。

取得した値を実際の座標に変換する

選択した座標がきちんと取得/計算できている判断するために、適当なアイコンでも設定しましょう。
このときギャラリーが最前面にくるように設定しなければならないことに注意してください。

取得した値を計算するには以下のように設定します。

Icon1.X

Gallery1.Selected.Rating1.Value * Gallery1.Width / Rating1.Max

Icon1.Y

Gallery1.Selected.Value * Gallery1.TemplateHeight - Rating1.Height / 2

アイコンを選択された位置の中心に持ってきたい場合は調整が必要ですね。

Icon1.X

Gallery1.Selected.Rating1.Value * Gallery1.Width / Rating1.Max - Self.Width / 2

Icon1.Y

Gallery1.Selected.Value * Gallery1.TemplateHeight - Rating1.Height / 2 - Self.Height / 2

まずはX座標の計算についての解説です。
Gallery1.Selected.Rating1.Value では選択された評価の値を取得しています。
このままの値では座標の値ではないので座標に変換を行います。
評価の数値1あたりの座標の数値を算出するには、ギャラリーの幅 / 評価の最大値で算出できますね。 これを式にすると Gallery1.Width / Rating1.Max となるわけです。

続いてY座標の計算についての解説です。
Gallery1.Selected.Value では選択されたカラムの数を取得しています。
X座標のときと同様この数値を座標に変換します。
同様の理論でカラム1つあたりの座標を算出するための式を作成します。
それが Gallery1.TemplateHeight にあたるというわけですね。

おわり

以上でPower Apps でなるべく簡単なロジックでクリック/選択された座標を取得するための仕組みを作成することができました!
願わくばこの機能がいつか標準で取得できるようになるといいですね...

また、今回「評価」を用いましたが、「スライダー」でも同様に実現可能です。

【Power Automate】Share Point ドキュメントのディレクトリ構成を取得する

はじめに

棚卸作業などで、Share Point Online のドキュメントライブラリのディレクトリ構成の棚卸を行いたい!と考えてことはありませんか?
今回はそのようなときに利用できるフローをPower Automate で作成してみました。

なお、標準機能でディレクトリ構成の出力を行いたい場合や、普段利用しているツールと同様の方法でディレクトリ構成の出力を行いたい場合は、もくだいさんのドキュメントライブラリのフォルダ、ファイル一覧を取得したいをご参照ください。

さっそく作成してみる

今回利用する変数

今回利用する変数は以下です。
最初に変数の初期化を行いましょう。

f:id:koruneko:20200919115628p:plain

f:id:koruneko:20200919115725p:plain

名前 種類
InitParent 文字列 [ディレクトリ構成を作成したい親ディレクトリ]
ex ) /Shared Documents/General
Parent 文字列 @{variables('InitParent')}
Index 整数 1
resultDirectory アレイ
Directory アレイ @{body('選択')}
※後述

InitParent で設定したディレクトリのフォルダ一覧を取得する

InitParent で設定したディレクトリ以下のフォルダ/ファイルの一覧を取得します。
「フォルダーの一覧」を選択し、対象のサイトを選択後、先ほど作成した「initParent」をファイル識別子に設定します。
その際、【Power Automate】Share Point コネクタのファイル識別子に動的コンテンツを利用するでも説明したように、replace 関数を利用して値を設定しましょう!

ファイル識別子

replace(variables('Parent'), '/', '%2F')

続いて「選択」アクションを選択し、「フォルダーの一覧」で取得した結果で必要なものだけを抜き出しましょう。
今回は以下のように設定しました。
Parent には、変数のParent を用いています。

マップ

{
  "Parent": @{variables('Parent')},
  "isFolder": @{item()?['IsFolder']},
  "DisplayName": @{item()?['DisplayName']},
  "Path": @{item()?['Path']},
  "isDone": @{false}
}

f:id:koruneko:20200919120719p:plain

変数「Directory」の設定

「変数」>「変数の初期化」を選択し、「Directory」というアレイ型の変数を作成します。
値には、先ほど「選択」で出力された結果を選択します。

f:id:koruneko:20200922161557p:plain

サブフォルダを含めてフォルダーの一覧を取得する

上記の方法だけでは、サブフォルダーも含めてフォルダーの一覧を取得することはできませんでした。

ここでは上記で取得したフォルダー一覧をもとにサブフォルダーも含めてフォルダーの一覧を取得していこうと思います。

サブフォルダーまで探索するためのループ条件ですが、今回は
「Directory 変数に格納された値の長さ(数) と ループ処理を行った回数」 で比較を行いたいと思います。

f:id:koruneko:20200922162053p:plain

Do until のループ条件

@greaterOrEquals(length(variables('Directory')), variables('Index'))

今回ループ処理回数のカウントに「Index」という変数を用いていますが、iterationIndexes という関数を利用してもいいかもですね。

JSON 解析を用いて値を参照しやすくする

続いて、「JSON の解析」を用いて「Directory」に格納されている値を順に参照していきます。

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

コンテンツ

last(take(variables('Directory'), variables('Index')))

スキーマ

{
    "type": "object",
    "properties": {
        "Parent": {
            "type": "string"
        },
        "isFolder": {
            "type": "boolean"
        },
        "DisplayName": {
            "type": "string"
        },
        "Path": {
            "type": "string"
        },
        "isDone": {
            "type": "boolean"
        }
    }
}

これにより、Index 番目の配列が参照されますね。

フォルダー一覧を取得する

続いて、「条件」コントロールを追加します。
この条件には、現在の対象となっているディレクトリが、ファイルか?フォルダーか?を判断するようにします。
もしフォルダーであれば、対象フォルダー以下のフォルダー一覧を取得し、「Directory」変数に値を追加します。
ファイルであれば、特になにも処理を行いません。
これにより、フォルダーが存在している場合「Directory」の値が増えるためループ回数が増えますね。

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

@body('JSON_の解析')?['isFolder']    次の値に等しい    @true

フォルダーであった場合には、フォルダー一覧を取得し、「Directory」に追加したいので以下のように設定します。

  • フォルダー一覧

サイトのアドレス

任意のアドレス

ファイル識別子

@replace(body('JSON_の解析')?['Path'], '/', '%2F')
  • 変数の設定

変数

Parent

@{body('JSON_の解析')?['Parent']}
  • 選択

開始

@{body('フォルダーの一覧_2')}

マップ

{
  "Parent": @{variables('Parent')},
  "DisplayName": @{item()?['DisplayName']},
  "isFolder": @{item()?['IsFolder']},
  "Path": @{item()?['Path']},
  "isDone": @{body('JSON_の解析')?['isDone']}
}
  • Apply to each
@{body('選択_2')}
  • 配列変数に追加

変数

Directory

@{items('Apply_to_each_2')}

f:id:koruneko:20200922180457p:plain

ここで、「Directory」に値を追加する際、Apply to each の処理が必要になってきます。
Apply to each なしで「選択」で出力された値を配列変数に追加しようとすると、エラーになるかと思います。
追加を行う際は、「選択」で取得した結果を1つずつ配列変数に追加する必要があるのでこのようになっています。

インデックの増加

ループ処理の最後に、「Index」の値を1増やす処理を追加しましょう!
これにより、対象ディレクトリのサブフォルダを含めたディレクトリー構成を取得することができます。

f:id:koruneko:20200922181412p:plain

取得したディレクトリ構成に属性を追加する

これまでの処理で取得された結果は、

  • フォルダー名
  • パス
  • 親フォルダー
  • フォルダーかファイルか

のみでした。

これらに加え更に

  • サイズ
  • 最終更新日

まで取得できるようにしたいと思います。

以下のような構成のフローを作成します。

f:id:koruneko:20200922181631p:plain

メタデータを取得する

Apply to each処理には以下を設定します。

@{variables('Directory')}

メタデータの取得ですが、これはファイルとフォルダーでそれぞれアクションが異なってきます。
なので「条件」を追加し、以前同様フォルダーか?ファイルか?を判定する条件を追加します。

設定するパラメータはほぼ同じなので、今回はフォルダーのメタデータの取得について解説します。

サイトのアドレス

任意のアドレス

ファイル識別子

@replace(items('Apply_to_each')?['Path'], '/', '%2F')
  • 作成
{
  "isDone": @{items('Apply_to_each')?['isDone']},
  "Parent": @{items('Apply_to_each')?['Parent']},
  "isFolder": @{items('Apply_to_each')?['isFolder']},
  "DisplayName": @{items('Apply_to_each')?['DisplayName']},
  "Path": @{items('Apply_to_each')?['Path']},
  "Size": @{body('フォルダー_メタデータの取得')?['Size']},
  "LastModified": @{body('フォルダー_メタデータの取得')?['LastModified']}
}
  • 配列変数に追加

名前

resultDirectory

@{outputs('作成_2')}

これで、メタデータの取得で得られた結果を踏まえた配列を取得することができるようになりました!

他の情報も取得したい場合は同様の方法で取得を行ってみてください。

ディレクトリ構成を見やすいかたちで出力する(作成まだ)

こちらは作成完了できたら追記します。
どなたかよい案があればください!!

おわりに

今回紹介したやり方を応用すれば、対象ディレクトリ配下のサブフォルダー含むすべてのファイルに対して、特定の処理を行う。
ということもできるようになります。

興味のある方はそちらも試してみるとよいですね。

【Power Automate】Share Point コネクタのファイル識別子に動的コンテンツを利用する

はじめに

この記事では、Power Automate にてShare Point コネクタ利用時に「ファイル識別子」に別のアクションで取得した、パスなどの動的コンテンツを設定したい場合のやり方について纏めています。

別のアクションで取得したパスを「ファイル識別子」に設定する場合、取得した値を少し加工してあげる必要があるのですが若干初見殺し感があるので備忘録も兼ねて纏めてみることにします。

「ファイルパス」と「ファイル識別子」

Power Automate からShare Point のドキュメントを指定するには、対象の「サイトのアドレス」と「ファイルパス」もしくは「ファイル識別子」の指定が必要なものがほとんどです。

では、「ファイルパス」と「ファイル識別子」の違いは何でしょうか?
実際に動かしてみて挙動をみてみましょう!

下記のようなフローを用意し、実際に動かしてみます。
2つとも同じ結果を取得するようなアクションですね。
違いは、「ファイルパス」か「ファイル識別子」という点です。

f:id:koruneko:20200918104026p:plain

そしてこちらが上記フローを実行した結果です。
「ファイル識別子」を指定したフローだけが失敗していますね。

f:id:koruneko:20200918104249p:plain

エラーとしては、下記のようなものが出力されています。

{
  "status": 400,
  "message": "Route did not match\r\nclientRequestId: 6d2fa675-2883-41b6-bd93-d6d7a73ac3f1\r\nserviceRequestId: 6d2fa675-2883-41b6-bd93-d6d7a73ac3f1"
}

ルートが一致しないと怒られていますね。

原因を調査してみる

「未加工入力の表示」で「ファイル識別子」に動的なコンテンツを設定した場合と、GUI 操作で直接パスを設定した場合の違いを比較してみましょう。
「未加工入力の表示」は実行履歴の以下より取得可能です。

f:id:koruneko:20200918104948p:plain

ここで取得した結果をWinMarge という比較・マージ用ツールを用いて差分を比較してみます。

f:id:koruneko:20200918110124p:plain

左がGUI 操作で直接パスを設定した場合、右が動的なコンテンツを設定した場合です。
比較の結果「id」に指定されている値が違うということがわかりました。

「/」が「%252f」に置換されていますね。
これでピンとこない方は

  • / %252f
  • %252f uri

などで調べてください。
※%252f ではなく%2F が検索結果に引っかかるかもしれませんが、どちらでも同じ挙動をします。

「ファイル識別子」に動的コンテンツを指定するには

これらの結果をもとに「ファイル識別子」に指定している動的コンテンツを加工してアクションが正常に実施されるように修正します。
修正内容は「/」を「%252f」(もしくは「%2F」)に置換してあげればよいです。

置換を行うには、「replace」関数を用います。

replace(items('Apply_to_each')?['Path'], '/', '%252F')

もしくは

replace(items('Apply_to_each')?['Path'], '/', '%2F')

これで正常にフローを実行することができました!

おわりに

今回は、問題解決アプローチも記事に組み込んで紹介してみました。
調べても出てこないような問題もPower Automate やPower Apps でアプリを作成しているかもしれません。
そのような場合は、自分でトライ&エラーを繰り返してみて原因を特定し、解決できるよう目指してみましょう!

【Power Apps】Sequence 関数を使ってみよう!

この記事で紹介すること

この記事ではPower Apps のSequence 関数について紹介したいと思います。
若干今更感ありますがそこはいわないで!

Sequence 関数を使うとなにができるの?

Sequence 関数を使うことにより、簡単な数列のテーブルを作成することができます。

docs.microsoft.com

実際に使用してみる

では、実際に使用してみましょう。

連続する番号のテーブルを作成する

1-10の連続する番号のテーブルを作成してみます。

ListBox1.Items

Sequence(10)

f:id:koruneko:20200907000257p:plain

これにより、1-10の連続する番号のテーブルを作成することができました。
この関数が登場するまでは、

[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

のように記載しなくてはいけなかったので大変楽になりましたね!

開始番号を任意の数に設定する

先ほど作成したテーブルは1から開始されていました。
今度は10からテーブルを開始し、20までの連続するテーブルを作成してみようと思います。

ListBox1.Items

Sequence(11, 10)

f:id:koruneko:20200907001011p:plain

このように第2引数に数値を指定した場合、その数値から連続するテーブルを作成することができます。
なお開始する番号は規定では1に設定されているので、指定しなかった場合は1から開始されます。

連続する偶数を10個作成する

今度は連続する偶数を10個作成してみようと思います。

ListBox1.Items

Sequence(10, 2, 2)

f:id:koruneko:20200907001641p:plain

このように第3引数に数値を指定した場合、その数値分だけ値が増えたテーブルを作成することができます。
なおこのステップ数は規定では1に設定されているので、指定しなかった場合は1から開始されます。

数値がだんだん減っていくテーブルを作成する

ステップ数にマイナスの値を設定することにより、カウントダウンを行うようなテーブルを作成できます。

ListBox1.Items

Sequence(10, 5, -1)

f:id:koruneko:20200907002857p:plain

3の倍数だけ除外されたテーブルを作成する

これを実現するためには、Filter 関数を利用すればよいです。

ListBox1.Items

Filter(Sequence(10), Mod(ThisRecord.Value, 3) <> 0)

f:id:koruneko:20200907003344p:plain

ThisRecord.Value の部分はValue だけでも大丈夫です。

ForAll と組み合わせて利用する

恐らく、Sequence 関数を利用していく場合、ForAll 関数とともに利用するケースが多いかと思います。
ForAll 関数とまだ仲良くなれていない方はこの機会に是非仲良くなってみましょう!

例えば公式doc のように乱数を10個生成してみようかと思います。

ListBox1.Items

ForAll(Sequence(10), Rand())

f:id:koruneko:20200907004940p:plain

【おまけ】フィボナッチ数列を作成してみる

ForAll 関数とSequence 関数を活用してPower Apps でフィボナッチ数列を作成してみようと思います!
フィボナッチ数列ってなに?という方は、こちらをご確認ください。
フィボナッチ数列をわかりやすく解説!一般項の求め方をマスターしよう

簡単に説明すると、「2つ前の項と1つ前の項を足し合わせていくことでできる数列」で
0, 1, 1, 2, 3, 5, 8, 13, 21…
といったかたちの数列になります。綺麗ですね!

これをPower Apps で作成しようとすると、例えば以下のようになります。

Button1.OnSelect

Clear(fibo);
Collect(fibo, [0, 1]);
ForAll(
    Sequence(10),
    Collect(
        fibo,
        {Value:Last(FirstN(fibo, Value)).Value + Last(FirstN(fibo, Value + 1)).Value}
    )
)

f:id:koruneko:20200907011603p:plain

簡単に解説を行うと、まずは最初に第一項と第二項を宣言します。
次にForAll 関数ないで、fibo レコードを再帰的に呼び出してフィボナッチ数列を作成しています。
Sequence 関数で指定した数値 + 2項フィボナッチ数列が作成されていますね。

こちらの再帰的に呼び出している箇所については、【Power Apps】コレクションにインデックスを付与するで似たような解説を行っていますので、よろしければ参考にしてください!


スポンサードリンク