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

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

【Power Apps】地理クイズアプリを作成してみよう!

はじめに

Power Apps で地理クイズを行える学習アプリを作成してみましたので、作成方法を纏めたいと思います。

お子様をお持ちのご家庭でかつHome 365 な誤家庭ご家庭の方は是非お子さんと一緒に作成してみて、アプリの作成と地理の学習という英才教育に役立ててください!

参考

今回利用したSVG 素材は以下からお借りしました。

github.com

アプリをダウンロードする場合

こちらに雑においてます。

.maspp ファイルをダウンロードしてください。

GitHub - koruneko/GeographyQize: for Power Apps

地理クイズアプリを作成する

日本地図を表示させる

まずは、下図のような日本地図を表示させたいと思います。

f:id:koruneko:20210101023332p:plain

画像コントロールを追加して以下のように設定します。

Screen.OnVisible

UpdateContext({scoring:false});
UpdateContext({nowSelect:0});
ClearCollect(
    Prefectures,
    {Num:1, Pref:"北海道", PrefCap:"札幌市", PrefAns:"", PrefCapAns:""},
    {Num:2, Pref:"青森県", PrefCap:"青森市", PrefAns:"", PrefCapAns:""},
    {Num:3, Pref:"岩手県", PrefCap:"盛岡市", PrefAns:"", PrefCapAns:""},
    {Num:4, Pref:"宮城県", PrefCap:"仙台市", PrefAns:"", PrefCapAns:""},
    {Num:5, Pref:"秋田県", PrefCap:"秋田市", PrefAns:"", PrefCapAns:""},
    {Num:6, Pref:"山形県", PrefCap:"山形市", PrefAns:"", PrefCapAns:""},
    {Num:7, Pref:"福島県", PrefCap:"福島市", PrefAns:"", PrefCapAns:""},
    {Num:8, Pref:"茨城県", PrefCap:"水戸市", PrefAns:"", PrefCapAns:""},
    {Num:9, Pref:"栃木県", PrefCap:"宇都宮市", PrefAns:"", PrefCapAns:""},
    {Num:10, Pref:"群馬県", PrefCap:"前橋市", PrefAns:"", PrefCapAns:""},
    {Num:11, Pref:"埼玉県", PrefCap:"さいたま市", PrefAns:"", PrefCapAns:""},
    {Num:12, Pref:"千葉県", PrefCap:"千葉市", PrefAns:"", PrefCapAns:""},
    {Num:13, Pref:"東京都", PrefCap:"新宿区", PrefAns:"", PrefCapAns:""},
    {Num:14, Pref:"神奈川県", PrefCap:"横浜市", PrefAns:"", PrefCapAns:""},
    {Num:15, Pref:"新潟県", PrefCap:"新潟市", PrefAns:"", PrefCapAns:""},
    {Num:16, Pref:"富山県", PrefCap:"富山市", PrefAns:"", PrefCapAns:""},
    {Num:17, Pref:"石川県", PrefCap:"金沢市", PrefAns:"", PrefCapAns:""},
    {Num:18, Pref:"福井県", PrefCap:"福井市", PrefAns:"", PrefCapAns:""},
    {Num:19, Pref:"山梨県", PrefCap:"甲府市", PrefAns:"", PrefCapAns:""},
    {Num:20, Pref:"長野県", PrefCap:"長野市", PrefAns:"", PrefCapAns:""},
    {Num:21, Pref:"岐阜県", PrefCap:"岐阜市", PrefAns:"", PrefCapAns:""},
    {Num:22, Pref:"静岡県", PrefCap:"静岡市", PrefAns:"", PrefCapAns:""},
    {Num:23, Pref:"愛知県", PrefCap:"名古屋市", PrefAns:"", PrefCapAns:""},
    {Num:24, Pref:"三重県", PrefCap:"津市", PrefAns:"", PrefCapAns:""},
    {Num:25, Pref:"滋賀県", PrefCap:"大津市", PrefAns:"", PrefCapAns:""},
    {Num:26, Pref:"京都府", PrefCap:"京都市", PrefAns:"", PrefCapAns:""},
    {Num:27, Pref:"大阪府", PrefCap:"大阪市", PrefAns:"", PrefCapAns:""},
    {Num:28, Pref:"兵庫県", PrefCap:"神戸市", PrefAns:"", PrefCapAns:""},
    {Num:29, Pref:"奈良県", PrefCap:"奈良市", PrefAns:"", PrefCapAns:""},
    {Num:30, Pref:"和歌山県", PrefCap:"和歌山市", PrefAns:"", PrefCapAns:""},
    {Num:31, Pref:"鳥取県", PrefCap:"鳥取市", PrefAns:"", PrefCapAns:""},
    {Num:32, Pref:"島根県", PrefCap:"松江市", PrefAns:"", PrefCapAns:""},
    {Num:33, Pref:"岡山県", PrefCap:"岡山市", PrefAns:"", PrefCapAns:""},
    {Num:34, Pref:"広島県", PrefCap:"広島市", PrefAns:"", PrefCapAns:""},
    {Num:35, Pref:"山口県", PrefCap:"山口市", PrefAns:"", PrefCapAns:""},
    {Num:36, Pref:"徳島県", PrefCap:"徳島市", PrefAns:"", PrefCapAns:""},
    {Num:37, Pref:"香川県", PrefCap:"高松市", PrefAns:"", PrefCapAns:""},
    {Num:38, Pref:"愛媛県", PrefCap:"松山市", PrefAns:"", PrefCapAns:""},
    {Num:39, Pref:"高知県", PrefCap:"高知市", PrefAns:"", PrefCapAns:""},
    {Num:40, Pref:"福岡県", PrefCap:"福岡市", PrefAns:"", PrefCapAns:""},
    {Num:41, Pref:"佐賀県", PrefCap:"佐賀市", PrefAns:"", PrefCapAns:""},
    {Num:42, Pref:"長崎県", PrefCap:"長崎市", PrefAns:"", PrefCapAns:""},
    {Num:43, Pref:"熊本県", PrefCap:"熊本市", PrefAns:"", PrefCapAns:""},
    {Num:44, Pref:"大分県", PrefCap:"大分市", PrefAns:"", PrefCapAns:""},
    {Num:45, Pref:"宮崎県", PrefCap:"宮崎市", PrefAns:"", PrefCapAns:""},
    {Num:46, Pref:"鹿児島県", PrefCap:"鹿児島市", PrefAns:"", PrefCapAns:""},
    {Num:47, Pref:"沖縄県", PrefCap:"那覇市", PrefAns:"", PrefCapAns:""}
)

Image.Image

長いので折りたたんで表示しています。 "data:image/svg+xml,"& EncodeUrl( "<svg viewBox='-700 0 1700 1000' xmlns='http://www.w3.org/2000/svg'> <title>Japanese Prefectures</title> <desc>Created by Geolonia (https://geolonia.com/).</desc> <g class='svg-map' transform='matrix(1.028807, 0, 0, 1.028807, -47.544239, -28.806583)'> <g class='prefectures' transform='matrix(1, 0, 0, 1, 6, 18)'> <g class='okinawa kyusyu-okinawa prefecture' data-code='47' stroke-linejoin='round' fill='" & If(nowSelect = 47, "#FFFF00", If(LookUp(Prefectures, Num = 47, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(52.000000, 193.000000)'> <title>沖縄 / Okinawa</title> <polygon points='4 109 6 110 4 111 0 110'/> <polygon points='48 121 55 123 51 129 39 124 42 122 44 125 46 118'/> <polygon points='132 113 130 110 132 110 130 108 133 105 132 100 135 108 142 114'/> <polygon points='225 23 224 28 219 23 223 21'/> <polygon points='73 117 77 112 79 113 73 120 72 126 66 127 64 125 67 122 63 119 71 120'/> <polygon points='287 20 291 17 286 15 285 8 292 10 292 14 299 13 298 11 307 0 309 5 309 9 305 15 300 15 299 19 293 19 294 21 288 25 281 25 286 34 281 31 276 39 280 41 270 46 270 38 277 32 275 24 280 25'/> <polygon points='127 106 126 103 128 104'/> <polygon points='279 8 279 6 283 8'/> <polygon points='293 11 294 13 292 12'/> </g> <g class='kagoshima kyusyu kyusyu-okinawa prefecture' data-code='46' stroke-linejoin='round' fill='" & If(nowSelect = 46, "#FFFF00", If(LookUp(Prefectures, Num = 46, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(96.000000, 17.000000)'> <title>鹿児島 / Kagoshima</title> <polygon points='23 949 26 951 23 952'/> <polygon points='33 960 32 954 39 950 48 956 47 961 41 965 35 964'/> <polygon points='64 953 64 958 59 959 58 952 62 949 63 939 70 929 71 938'/> <polygon points='38 844 43 848 52 844 57 850 56 852 61 859 66 864 65 869 70 871 73 880 80 881 80 888 78 891 75 890 70 896 75 899 73 903 76 902 69 905 66 910 50 918 50 913 55 910 59 898 53 889 54 884 48 881 53 879 54 883 57 884 61 878 54 872 50 874 44 887 46 898 51 901 49 906 43 907 40 901 27 901 28 897 25 896 27 894 22 890 31 890 35 880 32 871 27 867 30 860 28 848 31 846 35 848'/> <polygon points='31 837 34 836 33 838'/> <polygon points='27 848 24 842 29 840 30 844'/> <polygon points='4 868 6 865 7 867 2 875 0 874'/> <polygon points='12 864 9 861 14 861'/> <polygon points='284 149 280 150 279 146 289 144'/> <polygon points='301 126 301 118 306 118 305 122 309 126 308 129 303 132 300 128'/> <polygon points='363 98 360 99 359 97 365 93'/> <polygon points='344 90 331 98 335 101 330 104 327 103 329 107 323 102 325 99 316 97 324 97 325 95 321 94 335 88 337 90 339 86 344 85 342 89 344 87 345 89 347 82 349 88'/> <polygon points='324 108 322 106 320 108 318 101 323 102 322 105 328 107 325 109 326 107'/> <polygon points='355 12 352 16 352 13'/> <polygon points='361 1 363 0 365 4'/> </g> <g class='miyazaki kyusyu kyusyu-okinawa prefecture' data-code='45' stroke-linejoin='round' fill='" & If(nowSelect = 45, "#FFFF00", If(LookUp(Prefectures, Num = 45, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(152.000000, 824.000000)'> <title>宮崎 / Miyazaki</title> <polygon points='36 0 38 1 43 0 48 5 56 5 59 1 63 2 63 7 65 7 54 17 53 21 56 22 52 23 51 25 53 26 47 34 41 51 39 74 34 79 32 91 27 89 22 84 24 81 24 74 17 73 14 64 9 62 10 57 5 52 0 45 1 43 22 41 19 35 23 30 18 23 18 16 23 14 32 0'/> </g> <g class='oita kyusyu kyusyu-okinawa prefecture' data-code='44' stroke-linejoin='round' fill='" & If(nowSelect = 44, "#FFFF00", If(LookUp(Prefectures, Num = 44, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(163.000000, 771.000000)'> <title>大分 / Oita</title> <polygon points='0 34 3 29 0 26 2 24 1 19 5 13 12 9 19 10 20 3 33 7 38 0 47 4 49 10 46 18 43 17 43 20 35 22 36 26 40 28 56 27 50 36 56 36 53 38 56 40 61 38 62 41 57 41 55 45 65 49 59 49 61 51 57 54 60 55 54 56 54 60 52 60 52 55 48 54 45 58 37 58 32 53 27 54 25 53 21 49 22 43 15 30 9 30 10 36 8 39'/> </g> <g class='kumamoto kyusyu kyusyu-okinawa prefecture' data-code='43' stroke-linejoin='round' fill='" & If(nowSelect = 43, "#FFFF00", If(LookUp(Prefectures, Num = 43, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(115.000000, 800.000000)'> <title>熊本 / kumamoto</title> <path d='M8,37 L13,38 L13,48 L5,57 L1,58 L1,52 L5,51 L0,50 L4,40 L3,37 L8,37 Z M20,47 L14,47 L12,43 L19,39 L26,39 L23,47 L20,47 Z M24,34 L26,34 L26,38 L23,38 L24,34 Z M38,67 L33,61 L24,65 L19,61 L32,46 L30,40 L32,41 L31,39 L37,34 L26,34 L34,28 L35,23 L27,15 L26,10 L31,10 L30,7 L35,3 L39,4 L40,0 L48,5 L56,10 L58,7 L57,1 L63,1 L70,14 L69,20 L73,24 L69,24 L60,38 L55,40 L55,47 L60,54 L56,59 L59,65 L38,67 Z'/> </g> <g class='nagasaki kyusyu kyusyu-okinawa prefecture' data-code='42' stroke-linejoin='round' fill='" & If(nowSelect = 42, "#FFFF00", If(LookUp(Prefectures, Num = 42, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(44.000000, 700.000000)'> <title>長崎 / Nagasaki</title> <path d='M53,1 L55,0 L57,2 L54,5 L55,9 L49,15 L48,19 L51,18 L49,22 L51,21 L48,25 L48,20 L46,22 L46,19 L45,22 L42,20 L44,20 L45,15 L47,15 L45,14 L49,10 L46,8 L48,3 L52,3 L53,1 Z M46,29 L43,35 L37,36 L40,22 L41,24 L45,25 L44,22 L45,26 L48,25 L46,29 Z M67,59 L68,61 L64,63 L60,59 L62,59 L61,56 L64,53 L67,55 L66,57 L68,58 L67,59 Z M28,110 L25,115 L26,110 L23,107 L26,106 L26,102 L28,102 L30,94 L28,105 L33,106 L30,108 L28,106 L28,110 Z M24,112 L21,111 L21,109 L23,111 L22,108 L24,112 Z M19,113 L20,110 L19,116 L16,111 L19,113 Z M6,118 L9,117 L10,119 L12,115 L16,125 L9,124 L9,129 L0,126 L1,124 L3,126 L4,125 L2,125 L5,122 L4,116 L6,118 Z M65,80 L62,81 L64,78 L65,80 Z M67,82 L70,84 L67,86 L67,82 Z M29,88 L31,85 L33,87 L29,88 Z M54,76 L51,77 L55,74 L54,76 Z M66,86 L64,89 L66,96 L73,98 L72,102 L78,109 L86,112 L79,117 L92,118 L94,123 L92,130 L82,134 L80,128 L85,124 L84,122 L79,121 L71,123 L68,129 L60,134 L64,129 L63,126 L65,127 L67,123 L64,125 L63,118 L59,117 L56,111 L59,100 L63,104 L62,108 L63,106 L66,108 L64,115 L75,118 L71,112 L72,107 L69,103 L66,105 L65,99 L62,99 L62,97 L58,100 L60,98 L57,96 L57,93 L57,95 L53,93 L57,87 L54,86 L55,83 L60,82 L60,85 L65,84 L66,86 Z M49,88 L48,92 L42,94 L47,91 L45,89 L48,84 L52,84 L54,81 L55,84 L49,88 Z M15,115 L15,113 L18,116 L15,118 L15,115 Z M64,101 L63,104 L62,101 L64,98 L64,101 Z'/> </g> <g class='saga kyusyu kyusyu-okinawa prefecture' data-code='41' stroke-linejoin='round' fill='" & If(nowSelect = 41, "#FFFF00", If(LookUp(Prefectures, Num = 41, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(108.000000, 773.000000)'> <title>佐賀 / Saga</title> <polygon points='15 6 28 6 34 12 41 10 40 16 32 21 30 28 25 23 19 29 22 39 14 36 8 29 9 25 2 23 0 16 2 13 5 17 4 13 6 9 2 6 3 4 6 6 6 0 7 2 11 2 12 7'/> </g> <g class='fukuoka kyusyu kyusyu-okinawa prefecture' data-code='40' stroke-linejoin='round' fill='" & If(nowSelect = 40, "#FFFF00", If(LookUp(Prefectures, Num = 40, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(123.000000, 752.000000)'> <title>福岡 / Fukuoka</title> <polygon points='40 53 32 48 31 52 27 51 22 55 23 58 18 58 18 52 15 49 17 42 25 37 26 31 19 33 13 27 0 27 7 23 3 20 10 15 13 21 20 20 21 15 17 17 14 14 19 15 23 12 25 5 35 3 36 0 42 1 38 5 41 5 43 2 46 4 53 0 49 8 52 10 50 11 55 22 60 22 59 29 52 28 45 32 41 38 42 43 40 45 43 48'/> </g> <g class='kochi shikoku prefecture' data-code='39' stroke-linejoin='round' fill='" & If(nowSelect = 39, "#FFFF00", If(LookUp(Prefectures, Num = 39, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(251.000000, 765.000000)'> <title>高知 / Kochi</title> <path d='M61,0 L74,6 L80,4 L81,12 L88,13 L86,17 L88,21 L94,22 L87,41 L75,25 L65,21 L55,23 L55,21 L56,24 L44,28 L50,28 L44,29 L42,32 L41,29 L38,33 L39,38 L37,44 L34,44 L30,52 L25,53 L25,61 L22,64 L25,70 L21,66 L14,68 L9,65 L5,67 L6,61 L10,60 L9,57 L6,57 L8,54 L5,41 L9,44 L15,36 L20,33 L16,24 L27,23 L30,13 L37,4 L61,0 Z M1,69 L0,70 L0,67 L1,69 Z'/> </g> <g class='ehime sikoku prefecture' data-code='38' stroke-linejoin='round' fill='" & If(nowSelect = 38, "#FFFF00", If(LookUp(Prefectures, Num = 38, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(225.000000, 737.000000)'> <title>愛媛 / Ehime</title> <path d='M34,21 L31,20 L35,17 L34,21 Z M55,6 L51,6 L55,0 L57,3 L55,6 Z M60,5 L61,5 L60,7 L57,6 L60,5 Z M58,10 L54,12 L55,7 L60,8 L58,10 Z M32,85 L25,83 L25,86 L23,86 L24,81 L27,82 L21,76 L24,76 L22,72 L25,72 L22,71 L23,69 L20,67 L24,70 L28,66 L28,63 L24,63 L27,60 L19,60 L21,57 L19,56 L22,51 L16,50 L8,56 L0,57 L35,37 L41,18 L50,15 L48,11 L51,10 L59,24 L70,20 L80,22 L84,18 L89,20 L87,28 L63,32 L56,41 L53,51 L42,52 L46,61 L41,64 L35,72 L31,69 L34,82 L32,85 Z'/> </g> <g class='kagawa shikoku prefecture' data-code='37' stroke-linejoin='round' fill='" & If(nowSelect = 37, "#FFFF00", If(LookUp(Prefectures, Num = 37, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(308.000000, 724.000000)'> <title>香川 / Kagawa</title> <path d='M6,33 L1,31 L4,21 L0,17 L6,20 L19,10 L26,13 L30,9 L31,14 L34,11 L37,14 L36,17 L45,22 L44,25 L31,23 L22,30 L15,27 L6,33 Z M27,5 L25,4 L29,4 L27,5 Z M40,0 L42,0 L40,8 L38,7 L38,5 L35,9 L35,5 L31,3 L40,0 Z'/> </g> <g class='tokushima shikoku prefecture' data-code='36' stroke-linejoin='round' fill='" & If(nowSelect = 36, "#FFFF00", If(LookUp(Prefectures, Num = 36, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(312.000000, 744.000000)'> <title>徳島 / Tokushima</title> <path d='M50,2 L52,0 L51,4 L50,2 Z M41,2 L49,0 L49,3 L52,4 L48,13 L55,20 L50,24 L57,26 L38,36 L33,43 L27,42 L25,38 L27,34 L20,33 L19,25 L13,27 L0,21 L2,13 L11,7 L18,10 L27,3 L40,5 L41,2 Z'/> </g> <g class='yamaguchi chugoku prefecture' data-code='35' stroke-linejoin='round' fill='" & If(nowSelect = 35, "#FFFF00", If(LookUp(Prefectures, Num = 35, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(168.000000, 710.000000)'> <title>山口 / Yamaguchi</title> <path d='M45,0 L47,6 L43,11 L45,15 L50,15 L48,20 L50,24 L56,22 L58,24 L61,20 L60,16 L64,14 L64,21 L66,23 L67,29 L72,31 L72,36 L69,36 L70,44 L65,46 L65,54 L61,48 L57,48 L52,42 L47,44 L50,41 L46,38 L35,42 L32,39 L30,42 L29,40 L27,42 L27,38 L25,43 L21,45 L19,43 L16,45 L15,40 L9,36 L3,43 L3,36 L0,32 L4,27 L1,21 L4,17 L10,17 L5,15 L7,12 L16,14 L17,17 L24,17 L30,15 L29,12 L41,1 L43,2 L45,0 Z M64,54 L59,56 L62,52 L64,54 Z M75,50 L83,48 L78,50 L77,53 L73,50 L69,53 L67,48 L70,46 L75,50 Z M18,15 L18,13 L22,14 L18,15 Z M51,46 L48,46 L51,43 L51,46 Z'/> </g> <g class='hiroshima chugoku prefecture' data-code='34' stroke-linejoin='round' fill='" & If(nowSelect = 34, "#FFFF00", If(LookUp(Prefectures, Num = 34, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(230.000000, 687.000000)'> <title>広島 / Hiroshima</title> <path d='M73,40 L72,42 L69,40 L72,43 L70,42 L68,47 L63,45 L63,42 L54,44 L51,48 L39,49 L37,53 L30,55 L28,53 L25,56 L26,53 L23,47 L25,45 L17,44 L9,51 L10,54 L5,52 L4,46 L2,44 L2,37 L0,35 L5,30 L7,24 L6,22 L12,17 L20,19 L22,16 L27,18 L35,16 L32,11 L38,8 L45,0 L58,2 L65,4 L68,7 L66,14 L70,19 L69,24 L73,40 Z M45,53 L43,56 L41,53 L46,51 L45,53 Z M21,54 L20,50 L23,51 L22,59 L19,59 L21,56 L17,51 L21,54 Z M15,48 L16,50 L12,52 L15,48 Z M35,57 L33,56 L36,56 L35,57 Z M24,58 L26,58 L25,61 L28,61 L26,63 L20,62 L23,56 L26,56 L24,58 Z M59,49 L59,52 L56,49 L59,49 Z M57,50 L53,52 L54,50 L57,50 Z M60,44 L62,45 L59,47 L60,44 Z'/> </g> <g class='okayama chugoku prefecture' data-code='33' stroke-linejoin='round' fill='" & If(nowSelect = 33, "#FFFF00", If(LookUp(Prefectures, Num = 33, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(295.000000, 673.000000)'> <title>岡山 / Okayama</title> <polygon points='58 8 59 14 51 22 52 29 50 32 54 38 53 41 46 39 49 42 43 49 34 48 39 49 36 53 34 52 33 57 27 55 25 58 23 52 23 55 19 51 15 55 10 52 12 56 8 54 4 38 5 33 1 28 3 21 0 18 1 15 8 14 7 10 14 10 13 7 15 6 18 0 25 2 29 7 39 1 38 3 45 6 46 12'/> </g> <g class='shimane chugoku prefecture' data-code='32' stroke-linejoin='round' fill='" & If(nowSelect = 32, "#FFFF00", If(LookUp(Prefectures, Num = 32, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(211.000000, 610.000000)'> <title>島根 / Shimane</title> <path d='M74,14 L78,13 L75,17 L72,15 L73,19 L71,16 L74,14 Z M79,16 L77,19 L77,15 L79,14 L81,17 L79,16 Z M83,5 L88,0 L93,5 L92,9 L89,9 L91,11 L86,12 L83,5 Z M2,100 L10,98 L25,84 L35,78 L41,70 L52,65 L55,59 L53,55 L70,51 L76,46 L79,49 L87,48 L84,50 L82,51 L88,59 L87,68 L79,70 L81,73 L77,79 L64,77 L57,85 L51,88 L54,93 L46,95 L41,93 L39,96 L31,94 L25,99 L26,101 L24,107 L19,112 L21,114 L17,116 L18,120 L15,124 L13,122 L7,124 L5,120 L7,115 L2,115 L0,111 L4,106 L2,100 Z'/> </g> <g class='tottori chugoku prefecture' data-code='31' stroke-linejoin='round' fill='" & If(nowSelect = 31, "#FFFF00", If(LookUp(Prefectures, Num = 31, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(288.000000, 658.000000)'> <title>鳥取 / Tottori</title> <polygon points='7 33 0 31 4 25 2 22 10 20 11 11 5 3 7 2 9 6 15 8 23 4 34 6 55 5 64 0 71 17 71 21 65 23 53 27 52 21 45 18 46 16 36 22 32 17 25 15 22 21 20 22 21 25 14 25 15 29 8 30'/> </g> <g class='wakayama kinki prefecture' data-code='30' stroke-linejoin='round' fill='" & If(nowSelect = 30, "#FFFF00", If(LookUp(Prefectures, Num = 30, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(385.000000, 737.000000)'> <title>和歌山 / Wakayama</title> <path d='M41,32 L44,38 L49,41 L45,46 L46,50 L38,55 L37,59 L35,59 L36,56 L19,51 L14,44 L17,41 L9,37 L4,30 L0,30 L3,25 L0,24 L6,21 L1,18 L5,16 L4,14 L8,14 L0,6 L2,3 L5,6 L31,0 L35,9 L31,10 L25,17 L30,24 L28,30 L41,32 Z M49,23 L49,26 L44,29 L44,26 L49,23 Z'/> </g> <g class='nara kinki prefecture' data-code='29' stroke-linejoin='round' fill='" & If(nowSelect = 29, "#FFFF00", If(LookUp(Prefectures, Num = 29, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(410.000000, 712.000000)'> <title>奈良 / Nara</title> <polygon points='24 48 19 51 19 54 17 54 17 57 16 57 3 55 5 49 0 42 6 35 10 34 6 25 8 20 6 11 11 0 17 5 21 2 25 5 27 3 29 7 26 8 28 9 27 13 36 18 28 24 31 29 29 30 31 33 29 37 30 44 29 47'/> </g> <g class='hyogo kinki prefecture' data-code='28' stroke-linejoin='round' fill='" & If(nowSelect = 28, "#FFFF00", If(LookUp(Prefectures, Num = 28, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(345.000000, 655.000000)'> <title>兵庫 / Hyogo</title> <path d='M31,90 L23,93 L23,88 L20,89 L20,86 L38,67 L39,69 L32,79 L35,88 L31,90 Z M3,59 L4,56 L0,50 L2,47 L1,40 L9,32 L8,26 L14,24 L14,20 L7,3 L16,0 L33,1 L32,5 L36,10 L41,9 L42,11 L42,17 L36,17 L35,23 L42,28 L47,26 L49,33 L59,35 L58,40 L55,41 L56,45 L62,48 L60,49 L62,60 L59,62 L53,60 L40,66 L25,56 L11,57 L11,53 L8,58 L5,56 L3,59 Z'/> </g> <g class='osaka kinki prefecture' data-code='27' stroke-linejoin='round' fill='" & If(nowSelect = 27, "#FFFF00", If(LookUp(Prefectures, Num = 27, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(387.000000, 695.000000)'> <title>大阪 / Osaka</title> <polygon points='16 0 24 8 28 5 27 8 31 9 34 17 29 28 31 37 29 42 3 48 0 45 7 44 14 37 17 28 19 30 17 22 20 20 18 9 20 8 14 5 13 1'/> </g> <g class='kyoto kinki prefecture' data-code='26' stroke-linejoin='round' fill='" & If(nowSelect = 26, "#FFFF00", If(LookUp(Prefectures, Num = 26, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(377.000000, 649.000000)'> <title>京都 / Kyoto</title> <polygon points='44 63 41 55 37 54 38 51 34 54 26 46 27 41 17 39 15 32 10 34 3 29 4 23 10 23 10 17 9 15 4 16 0 11 1 7 3 10 2 8 19 0 23 7 15 14 18 15 20 11 20 15 24 17 23 21 25 18 27 19 25 14 31 11 32 14 31 20 34 25 46 27 51 32 48 46 51 56 55 56 59 62 60 66 58 68 54 65 50 68'/> </g> <g class='shiga kinki prefecture' data-code='25' stroke-linejoin='round' fill='" & If(nowSelect = 25, "#FFFF00", If(LookUp(Prefectures, Num = 25, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(423.000000, 655.000000)'> <title>滋賀 / Shiga</title> <path d='M20,0 L27,3 L28,9 L31,9 L34,16 L35,19 L31,29 L33,30 L34,40 L30,50 L24,53 L17,50 L16,55 L13,56 L9,50 L5,50 L2,40 L5,26 L0,21 L3,17 L6,18 L9,11 L12,13 L18,10 L18,7 L21,8 L20,0 Z M20,13 L20,16 L18,13 L14,16 L16,24 L8,32 L8,36 L5,42 L7,45 L9,36 L15,35 L16,31 L25,25 L26,21 L20,13 Z'/> </g> <g class='mie kinki prefecture' data-code='24' stroke-linejoin='round' fill='" & If(nowSelect = 24, "#FFFF00", If(LookUp(Prefectures, Num = 24, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(426.000000, 683.000000)'> <title>三重 / Mie</title> <polygon points='8 95 3 92 0 86 1 86 1 83 3 83 8 80 8 77 13 76 14 73 13 66 15 62 13 59 15 58 12 53 20 47 11 42 12 38 10 37 13 36 11 32 10 28 13 27 14 22 21 25 27 22 31 12 30 2 36 0 43 7 48 15 42 16 43 20 36 32 35 40 51 47 53 52 56 51 55 55 50 55 55 56 55 61 51 62 53 60 50 58 49 60 43 61 46 57 42 57 39 62 39 60 36 63 34 61 34 64 32 62 25 66 22 68 23 73 21 72 22 70 18 73 22 77 20 77 22 80 19 78 20 81 13 85'/> </g> <g class='aichi chubu prefecture' data-code='23' stroke-linejoin='round' fill='" & If(nowSelect = 23, "#FFFF00", If(LookUp(Prefectures, Num = 23, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(469.000000, 673.000000)'> <title>愛知 / Aichi</title> <polygon points='42 46 18 52 20 47 23 49 31 43 33 46 35 43 34 40 28 37 26 41 18 40 14 37 16 29 12 40 16 45 13 45 9 41 10 36 8 33 8 28 12 21 5 25 0 17 1 11 5 3 15 0 21 8 26 10 33 8 39 12 46 8 47 14 52 11 60 13 50 33 42 38'/> </g> <g class='shizuoka chubu prefecture' data-code='22' stroke-linejoin='round' fill='" & If(nowSelect = 22, "#FFFF00", If(LookUp(Prefectures, Num = 22, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(511.000000, 659.000000)'> <title>静岡 / Shizuoka</title> <polygon points='73 14 77 14 77 27 83 31 81 36 85 42 85 46 78 55 77 61 70 65 64 59 67 45 65 41 66 38 72 37 62 31 55 33 51 38 53 41 44 45 42 54 36 61 38 65 25 61 0 60 0 52 8 47 18 27 33 17 33 5 37 0 40 8 39 19 44 20 48 29 53 27 52 20 56 12 61 17'/> </g> <g class='gifu chubu prefecture' data-code='21' stroke-linejoin='round' fill='" & If(nowSelect = 21, "#FFFF00", If(LookUp(Prefectures, Num = 21, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(450.000000, 608.000000)'> <title>岐阜 / Gifu</title> <polygon points='66 4 69 10 64 20 67 24 66 28 60 35 55 35 53 40 59 43 64 50 62 53 68 59 66 63 68 66 65 66 67 70 65 73 58 77 52 73 45 75 40 73 34 65 24 68 20 76 19 82 12 75 6 77 4 76 8 66 7 63 4 56 1 56 0 50 5 41 11 44 12 42 25 41 28 37 22 29 24 23 29 13 26 10 30 5 34 7 35 11 44 0 47 2 52 0 52 2 56 0'/> </g> <g class='nagano chubu prefecture' data-code='20' stroke-linejoin='round' fill='" & If(nowSelect = 20, "#FFFF00", If(LookUp(Prefectures, Num = 20, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(503.000000, 572.000000)'> <title>長野 / Nagano</title> <polygon points='68 18 59 21 60 23 55 27 54 34 57 39 66 39 67 45 64 47 66 53 63 54 66 57 66 63 70 65 71 70 68 72 64 69 58 71 53 66 43 77 46 80 43 82 45 87 41 92 41 104 26 114 18 112 13 115 12 109 14 106 12 102 15 102 13 99 15 95 9 89 11 86 6 79 0 76 2 71 7 71 13 64 14 60 11 56 16 46 13 40 19 32 18 29 21 28 22 17 27 10 27 7 34 8 34 13 36 14 44 10 48 12 48 7 53 2 60 0 63 2 63 7 68 11'/> </g> <g class='yamanashi chubu prefecture' data-code='19' stroke-linejoin='round' fill='" & If(nowSelect = 19, "#FFFF00", If(LookUp(Prefectures, Num = 19, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(546.000000, 638.000000)'> <title>山梨 / Yamanashi</title> <polygon points='2 21 0 16 3 14 0 11 10 0 15 5 21 3 25 6 28 4 39 7 43 15 48 18 47 27 38 35 26 38 21 33 17 41 18 48 13 50 9 41 4 40 5 29'/> </g> <g class='fukui chubu prefecture' data-code='18' stroke-linejoin='round' fill='" & If(nowSelect = 18, "#FFFF00", If(LookUp(Prefectures, Num = 18, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(408.000000, 618.000000)'> <title>福井 / Fukui</title> <polygon points='40 0 46 8 56 9 61 14 66 13 64 19 70 27 67 31 54 32 53 34 47 31 42 40 35 37 36 45 33 44 33 47 27 50 24 48 21 55 18 54 15 58 3 56 0 51 1 45 3 49 10 46 6 49 13 50 15 47 13 48 13 45 19 47 17 45 20 43 18 40 26 41 25 35 28 33 29 38 31 39 32 32 25 19 34 6 34 2'/> </g> <g class='ishikawa chubu prefecture' data-code='17' stroke-linejoin='round' fill='" & If(nowSelect = 17, "#FFFF00", If(LookUp(Prefectures, Num = 17, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(448.000000, 541.000000)'> <title>石川 / Ishikawa</title> <path d='M37,26 L34,23 L41,23 L37,26 Z M28,77 L31,80 L26,90 L21,91 L16,86 L6,85 L0,77 L15,62 L26,44 L27,33 L25,29 L25,24 L22,23 L27,10 L52,0 L56,1 L57,5 L51,6 L50,15 L46,14 L39,22 L37,18 L34,19 L32,28 L38,30 L41,26 L41,36 L33,38 L31,48 L28,50 L30,54 L28,57 L29,61 L27,69 L28,77 Z'/> </g> <g class='toyama chubu prefecture' data-code='16' stroke-linejoin='round' fill='" & If(nowSelect = 16, "#FFFF00", If(LookUp(Prefectures, Num = 16, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(475.000000, 575.000000)'> <title>富山 / Toyama</title> <polygon points='41 37 31 33 27 35 27 33 22 35 19 33 10 44 9 40 5 38 1 43 0 35 2 27 1 23 3 20 1 16 4 14 6 4 14 2 11 8 23 15 31 11 33 3 43 0 47 3 50 14 49 25 46 26 47 29'/> </g> <g class='niigata chubu prefecture' data-code='15' stroke-linejoin='round' fill='" & If(nowSelect = 15, "#FFFF00", If(LookUp(Prefectures, Num = 15, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(518.000000, 476.000000)'> <title>新潟 / Niigata</title> <path d='M7,113 L4,102 L0,99 L14,94 L23,87 L30,87 L45,75 L55,60 L60,46 L79,34 L80,36 L79,35 L85,28 L94,0 L102,3 L102,10 L111,16 L109,20 L101,23 L101,31 L98,39 L101,44 L104,46 L95,56 L97,65 L89,65 L88,68 L79,69 L78,72 L80,76 L76,82 L81,87 L80,101 L73,94 L71,98 L67,99 L68,104 L65,105 L65,108 L53,114 L53,107 L48,103 L48,98 L45,96 L38,98 L33,103 L33,108 L29,106 L21,110 L19,109 L19,104 L12,103 L12,106 L7,113 Z M36,46 L28,47 L35,38 L33,35 L30,37 L30,31 L34,23 L43,14 L39,30 L46,31 L43,40 L36,46 Z'/> </g> <g class='kanagawa kanto prefecture' data-code='14' stroke-linejoin='round' fill='" & If(nowSelect = 14, "#FFFF00", If(LookUp(Prefectures, Num = 14, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(584.000000, 656.000000)'> <title>神奈川 / Kanagawa</title> <polygon points='10 0 24 6 28 11 28 6 30 6 27 4 30 2 44 9 40 13 36 12 39 16 36 16 36 23 42 26 38 30 39 33 35 33 36 28 33 24 26 22 15 25 11 28 12 34 10 34 4 30 4 17 0 17 9 9'/> </g> <g class='tokyo kanto prefecture' data-code='13' stroke-linejoin='round' fill='" & If(nowSelect = 13, "#FFFF00", If(LookUp(Prefectures, Num = 13, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(585.000000, 642.000000)'> <title>東京 / Tokyo</title> <path d='M49,173 L49,178 L44,171 L49,173 Z M34,113 L32,115 L29,114 L31,111 L34,113 Z M11,104 L13,106 L11,107 L11,104 Z M18,98 L16,96 L18,92 L18,98 Z M22,75 L22,69 L26,70 L26,76 L22,75 Z M48,7 L49,12 L47,16 L41,15 L43,19 L39,20 L43,20 L43,23 L29,16 L26,18 L29,20 L27,20 L27,25 L23,20 L9,14 L4,11 L0,3 L3,0 L18,4 L23,8 L30,5 L30,9 L36,6 L40,7 L42,5 L48,7 Z'/> </g> <g class='chiba kanto prefecture' data-code='12' stroke-linejoin='round' fill='" & If(nowSelect = 12, "#FFFF00", If(LookUp(Prefectures, Num = 12, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(627.000000, 629.000000)'> <title>千葉 / Chiba</title> <polygon points='5 29 7 25 6 20 6 13 0 0 8 10 19 15 37 11 37 8 55 20 55 24 45 24 34 35 32 57 19 61 7 75 0 71 5 69 3 66 4 63 3 58 5 53 1 49 4 45 7 46 7 42 13 39 18 33 11 26 6 30'/> </g> <g class='saitama kanto prefecture' data-code='11' stroke-linejoin='round' fill='" & If(nowSelect = 11, "#FFFF00", If(LookUp(Prefectures, Num = 11, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(573.000000, 618.000000)'> <title>埼玉 / Saitama</title> <polygon points='48 4 49 5 51 12 54 11 54 11 60 24 60 31 54 29 52 31 48 30 42 33 42 29 35 32 30 28 15 24 12 27 1 24 0 19 16 10 21 0 32 2 38 6'/> </g> <g class='gunma kanto prefecture' data-code='10' stroke-linejoin='round' fill='" & If(nowSelect = 10, "#FFFF00", If(LookUp(Prefectures, Num = 10, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(557.000000, 570.000000)'> <title>群馬 / Gunma</title> <polygon points='64 52 54 54 48 50 37 48 32 58 16 67 12 65 12 59 9 56 12 55 10 49 13 47 12 41 3 41 0 36 1 29 6 25 5 23 14 20 26 14 26 11 29 10 28 5 32 4 34 0 41 7 49 9 47 12 50 14 47 17 46 26 54 29 48 42 53 49 62 48'/> </g> <g class='tochigi kanto prefecture' data-code='9' stroke-linejoin='round' fill='" & If(nowSelect = 9, "#FFFF00", If(LookUp(Prefectures, Num = 9, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(603.000000, 563.000000)'> <title>栃木 / Tochigi</title> <polygon points='19 60 18 59 16 55 7 56 2 49 8 36 0 33 1 24 4 21 1 19 3 16 28 0 40 3 46 7 47 13 47 24 48 27 45 29 47 39 44 46 33 48 30 53 27 53 26 57'/> </g> <g class='ibaraki kanto prefecture' data-code='8' stroke-linejoin='round' fill='" & If(nowSelect = 8, "#FFFF00", If(LookUp(Prefectures, Num = 8, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(622.000000, 575.000000)'> <title>茨城 / Ibaraki</title> <polygon points='5 54 5 54 2 55 0 48 7 45 8 41 11 41 14 36 25 34 28 27 26 17 29 15 28 12 28 1 38 10 44 4 44 0 54 5 46 29 47 35 44 42 46 52 51 61 49 63 51 66 52 62 60 74 42 62 42 65 24 69 13 64'/> </g> <g class='fukushima tohoku prefecture' data-code='7' stroke-linejoin='round' fill='" & If(nowSelect = 7, "#FFFF00", If(LookUp(Prefectures, Num = 7, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(594.000000, 511.000000)'> <title>福島 / Fukushima</title> <polygon points='82 69 72 64 72 68 66 74 56 65 55 59 49 55 37 52 12 68 4 66 5 52 0 47 4 41 2 37 3 34 12 33 13 30 21 30 19 21 28 11 25 9 32 11 38 10 40 14 44 13 47 15 53 14 56 10 54 9 55 0 61 0 65 4 74 4 75 10 80 12 79 10 83 10 83 4 87 4 93 18 93 45 91 61'/> </g> <g class='yamagata tohoku prefecture' data-code='6' stroke-linejoin='round' fill='" & If(nowSelect = 6, "#FFFF00", If(LookUp(Prefectures, Num = 6, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(612.000000, 439.000000)'> <title>山形 / Yamagata</title> <polygon points='14 1 24 0 28 5 42 8 44 13 48 14 53 22 51 30 48 30 52 41 45 53 46 59 43 65 36 67 37 72 36 81 38 82 35 86 29 87 26 85 22 86 20 82 14 83 7 81 4 76 7 68 7 60 15 57 17 53 8 47 8 40 0 37 13 14'/> </g> <g class='akita tohoku prefecture' data-code='5' stroke-linejoin='round' fill='" & If(nowSelect = 5, "#FFFF00", If(LookUp(Prefectures, Num = 5, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(617.000000, 352.000000)'> <title>秋田 / Akita</title> <polygon points='54 97 47 102 43 101 39 100 37 95 23 92 19 87 9 88 11 78 16 69 17 47 11 39 3 42 0 33 6 36 12 28 15 19 15 11 10 6 14 7 17 3 29 5 32 2 39 7 42 5 45 7 55 0 55 5 60 5 59 16 55 21 57 39 52 40 55 44 52 48 54 53 50 59 47 70 55 83 52 86 55 91'/> </g> <g class='miyagi tohoku prefecture' data-code='4' stroke-linejoin='round' fill='" & If(nowSelect = 4, "#FFFF00", If(LookUp(Prefectures, Num = 4, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(648.000000, 445.000000)'> <title>宮城 / Miyagi</title> <path d='M33,70 L29,70 L29,76 L25,76 L26,78 L21,76 L20,70 L11,70 L7,66 L1,66 L0,61 L7,59 L10,53 L9,47 L16,35 L12,24 L15,24 L17,16 L12,8 L16,9 L23,4 L33,9 L41,8 L39,12 L45,16 L49,11 L55,14 L57,0 L64,1 L67,8 L62,5 L63,10 L59,14 L62,19 L60,17 L56,20 L60,22 L57,28 L61,28 L61,32 L58,30 L60,34 L57,35 L58,38 L61,37 L59,39 L61,41 L61,45 L57,43 L58,40 L55,41 L57,39 L56,37 L39,40 L36,43 L40,45 L35,46 L38,47 L32,57 L33,70 Z M43,41 L44,43 L42,43 L43,41 Z'/> </g> <g class='iwate tohoku prefecture' data-code='3' stroke-linejoin='round' fill='" & If(nowSelect = 3, "#FFFF00", If(LookUp(Prefectures, Num = 3, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(664.000000, 354.000000)'> <title>岩手 / Iwate</title> <polygon points='48 92 41 91 39 105 33 102 29 107 23 103 25 99 17 100 7 95 8 89 5 84 8 81 0 68 3 57 7 51 5 46 8 42 5 38 10 37 8 19 12 14 16 15 29 5 31 8 35 5 40 7 46 0 54 11 52 16 57 19 55 23 61 28 64 43 62 53 65 49 66 54 68 55 62 60 64 62 67 59 67 63 63 63 63 68 60 69 65 68 60 71 62 73 60 75 64 74 58 81 62 83 57 83 60 86 57 86 59 88 53 89 52 86 54 91 52 91 52 94 50 90'/> </g> <g class='aomori tohoku prefecture' data-code='2' stroke-linejoin='round' fill='" & If(nowSelect = 2, "#FFFF00", If(LookUp(Prefectures, Num = 2, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(624.000000, 287.000000)'> <title>青森 / Aomori</title> <polygon points='3 71 3 63 0 60 6 51 12 51 18 47 21 31 17 27 20 26 21 19 27 23 31 20 35 23 37 38 40 45 46 41 45 37 47 33 58 42 61 39 65 23 61 16 55 22 41 25 47 0 64 11 73 6 71 37 73 51 77 62 81 61 86 67 80 74 75 72 71 75 69 72 56 82 52 81 53 70 48 70 48 65 38 72 35 70 32 72 25 67 22 70 10 68 7 72'/> </g> <g class='hokkaido prefecture' data-code='1' stroke-linejoin='round' fill='" & If(nowSelect = 1, "#FFFF00", If(LookUp(Prefectures, Num = 1, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(602.000000, 10.000000)'> <title>北海道 / Hokkaido</title> <path d='M4,240 L3,245 L0,246 L0,237 L6,235 L4,240 Z M33,261 L32,250 L28,243 L23,242 L21,237 L17,236 L15,231 L19,223 L17,212 L19,209 L28,207 L34,198 L37,202 L39,201 L43,192 L49,187 L39,173 L40,166 L47,164 L60,174 L71,171 L71,174 L78,177 L83,174 L89,165 L83,140 L86,135 L93,132 L97,126 L96,103 L100,95 L101,85 L98,67 L90,48 L93,39 L92,33 L94,36 L99,35 L105,28 L131,55 L139,68 L155,85 L184,104 L213,109 L214,113 L219,118 L238,118 L260,91 L262,96 L252,119 L252,129 L255,135 L265,138 L263,140 L264,137 L258,137 L263,149 L269,156 L273,157 L280,149 L287,148 L277,156 L275,163 L258,166 L256,172 L252,177 L247,177 L245,175 L246,173 L243,172 L240,178 L243,180 L228,181 L220,178 L205,186 L191,202 L182,216 L179,225 L180,240 L178,248 L164,237 L141,228 L113,211 L100,209 L103,206 L88,214 L72,230 L69,227 L73,227 L68,226 L66,220 L58,212 L47,213 L42,220 L39,230 L40,234 L52,242 L62,242 L71,254 L80,257 L82,260 L76,265 L72,267 L63,263 L60,265 L60,261 L57,260 L55,265 L48,269 L48,278 L40,282 L37,287 L30,284 L27,278 L28,269 L33,261 Z M71,48 L73,45 L77,47 L79,52 L75,55 L71,52 L71,48 Z M65,35 L65,33 L67,35 L66,45 L63,35 L65,35 Z M369,17 L367,13 L365,13 L363,17 L365,23 L364,26 L359,28 L357,35 L350,34 L351,41 L341,50 L341,54 L335,54 L335,56 L339,58 L339,62 L336,65 L332,64 L332,69 L329,66 L330,71 L327,78 L331,79 L336,70 L341,69 L346,59 L356,50 L358,40 L363,43 L369,41 L384,24 L397,15 L406,13 L407,10 L404,6 L407,2 L402,0 L396,2 L384,20 L373,22 L369,17 Z M290,99 L295,93 L303,91 L308,84 L311,85 L314,79 L304,82 L296,77 L293,79 L289,89 L280,104 L280,107 L266,122 L268,129 L273,128 L273,132 L274,119 L282,114 L283,109 L286,109 L287,102 L290,99 Z M322,125 L334,115 L329,113 L325,116 L326,117 L319,119 L319,123 L321,122 L322,125 Z M300,142 L304,137 L297,139 L300,142 Z M291,146 L293,143 L289,144 L291,146 Z'/> </g> </g> <g class='boundary-line' stroke='#EEEEEE' stroke-width='12' stroke-linejoin='round' style=''> <line x1='320.227' y1='361.996' x2='582.351' y2='109.378' style=''/> <line x1='277.337' y1='380.162' x2='46.213' y2='380.162' style=''/> </g> </g> </svg>" )

追加した画像コントロールは画面いっぱいに表示しておいてください。
これで日本地図が画面右側に表示することができました。

fill='" & If(nowSelect = 47, "#FFFF00", If(LookUp(Prefectures, Num = 47, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "'

としているのは、対象の県の色をユーザーの操作の状態で変化させるためです。

対象の県が選択されているときは黄色      
対象の県の答えが既に入力されているときは赤色 
上記以外の時は灰色              

をそれぞれ表示されるように設定しています。

OnVisibele で設定した、変数 / コレクションですが、それぞれ以下の用途で使用しています。

変数名 種類 使用用途
scoring 変数 スコア表示を行うか
nowSelect 変数 現在選択されている県の番号
Prefectures コレクション 都道府県の番号、名称、ユーザーが入力した回答

回答入力欄を作成する

ユーザーが回答を入力するようの回答入力欄を作成します。

こちらの回答入力欄ですが、画像のように
01 11
︙ ︙
10 20
のようにN 順で回答欄を今回は作成させたいと思います。

しかし、ギャラリー(縦方向)の Items に先ほどの Prefecture を設定し、 WrapCount にて折り返し表示を行いたい数だけ任意の数値を設定した場合、
01…10
11…20
のようにZ 順で表示されてしまいます。

ギャラリー(横方向)を利用し、 WrapCount を設定した場合は上記のようにN 順で回答欄を作成することが可能です。
ただし WrapCount にて設定できる数は最大で 10 です。
今回47都道府県を3行で表示させたかったので、一列あたりに表示させる数は最大で16ですので、条件を満たせません。

また、県の他に県庁所在地の入力欄もユーザーの選択したモードで 表示 / 非表示 を制御したいので、「伸縮可能」なギャラリーを用いる必要があります。
この伸縮可能ギャラリーは縦方向のものしか現時点(2021/1/1)では存在していません。
よって縦方向のギャラリーでどうにか実現する必要があります。

なお伸縮可能ギャラリーに関しては、Hiro さんのこちらの記事を参考にしてください。
Flexible Height Gallery (高さ可変ギャラリー)のススメ

そこで今回は各列ごとにギャラリーを設定することにしました。
今回は3行表示させたいので、ギャラリーを3つ設定することになりますね。

伸縮可能ギャラリーを1つ追加して Items に以下を設定します。

Gallery.Items

Filter(Prefectures, Num <= 16)

他2つのギャラリーと設定が異なるのは、この Items の項目と座標の設定だけですので、先に1つ目の列を作成して残りはコピペで作成しましょう。

ギャラリーには以下のコントロールを設定します。

f:id:koruneko:20210101031911p:plain

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

コントロール 役割
NumLabel 番号を表示
PrefTextInput 都道県回答入力欄
PrefCapTextInput 県庁所在地回答入力欄
PrefAns 都道府県回答
PrefCapAns 県庁所在地回答

追加したらそれぞれ以下のように設定しましょう!
(座標の設定は省いていますので、各自適宜揃えてください。)

NumLabel

NumLabel.Label

ThisItem.Num

PrefTextInput

PrefTextInput.Default

ThisItem.PrefAns

PrefTextInput.HintText

"都道府県名"

PrefTextInput.DisplayMode

If(!scoring, DisplayMode.Edit, View)

PrefTextInput.OnChange

UpdateIf(Prefectures, Num = ThisItem.Num, {PrefAns:Self.Text});
If(!Toggle1.Value, UpdateContext({nowSelect:ThisItem.Num + 1}))

PrefTextInput.OnSelect

Select(Parent);
UpdateContext({nowSelect:ThisItem.Num})

PrefCapTextInput

PrefCapTextInput.Default

ThisItem.PrefCapAns

PrefCapTextInput.HintText

"県庁所在地"

PrefCapTextInput.DisplayMode

If(!scoring, DisplayMode.Edit, View)

PrefCapTextInput.OnChange

UpdateIf(Prefectures, Num = ThisItem.Num, {PrefCapAns:Self.Text});
UpdateContext({nowSelect:ThisItem.Num + 1})

PrefCapTextInput.OnSelect

Select(Parent);
UpdateContext({nowSelect:ThisItem.Num})

PrefCapTextInput.Visible

Toggle1.Value

PrefAns
PrefAns.Text

ThisItem.Pref

PrefAns.Visible

scoring

PrefAns.Color

RGBA(0, 0, 0, 0)

PrefAns.Fill

RGBA(0, 0, 0, 0)

PrefAns.HoverColor

If(ThisItem.Pref = ThisItem.PrefAns, Self.Color, RGBA(0, 0, 0, 1))

PrefAns.HoverFill

If(ThisItem.Pref = ThisItem.PrefAns, Self.Fill, RGBA(220, 20, 60, 1))

PrefAns.OnSelect

UpdateContext({nowSelect:ThisItem.Num})

PrefCapAns
PrefCapAns.Text

ThisItem.PrefCap

PrefCapAns.Visible

scoring

PrefCapAns.Color

RGBA(0, 0, 0, 0)

PrefCapAns.Fill

RGBA(0, 0, 0, 0)

PrefCapAns.HoverColor

If(ThisItem.PrefCap = ThisItem.PrefCapAns, Self.Color, RGBA(0, 0, 0, 1))

PrefCapAns.HoverFill

If(ThisItem.PrefCap = ThisItem.PrefCapAns, Self.Fill, RGBA(220, 20, 60, 1))

PrefCapAns.OnSelect

UpdateContext({nowSelect:ThisItem.Num})

Toggle は県庁所在地を問題に表示するか否かの選択肢です。
私の場合は初回のHome画面で設定しています。

テキスト入力欄の OnChangeUpdateContext({nowSelect:ThisItem.Num + 1}) としているのは、タブ遷移を行って次の入力欄に移った時 OnSelect が働かないからです。
タブ遷移を行ったときは次の入力欄にいきたいということなので、疑似的に実現するために上記のように設定しています。

Power Apps でのタブ遷移の動きについては備忘録も兼ねて今度纏めたいと思います。

回答を表示しているラベルはテキスト入力の座標、大きさとそれぞれ揃えて設定してください。
回答はマウスホバーしたときに表示されるようにしたいので、デフォルト時に表示される色は透明とし、ホバー時に色が設定されるようにしています。

これで回答入力欄は完成なので、残り2列分もコピーして、座標を合わせましょう。
Items にはそれぞれ以下のように設定します。

Gallery_2.Items

Filter(Prefectures, Num > 16, Num <= 32)

Gallery_3.Items

Filter(Prefectures, Num > 32)

採点ボタンを作成する

最後に採点ボタンと採点結果を表示するラベルを追加しましょう。

採点ボタンでは以下のように設定します。

Button.OnSelect

UpdateContext({scoring:true});

採点結果ラベルには以下のように設定します。

Label.Text

"都道府県 :" & If(!scoring, "    ", Text(CountIf(Prefectures, Pref = PrefAns), "[$-ja]00")) & "/ 47" & Char(10) &
If(Toggle1.Value, "県庁所在地:" & If(!scoring, "    ", Text(CountIf(Prefectures, PrefCap = PrefCapAns), "[$-ja]00")) & "/ 47" & Char(10)) &
If(scoring, "カーソルを合わせると回答を表示します。")

Char(10) で改行を表します。

以上で日本地図のクイズの作成は完了です!

おわりに

これを利用すれば他チリのクイズも作成が可能です。
またスコアをShare Point リストに設定して、記録するようにするのも面白そうですね。

もし作成してみた方がいらっしゃいましたら教えていただけますと嬉しいです!

ローカルのExcel ファイルをSharePoint リストにアップロードしてPower Apps で表示する

はじめに

以下のようなコメントをいただきましたのでやり方を簡単に紹介したいと思います。

PowerAppsでローカルのエクセルをSharepointリストやDBにアップロードしてそれをPowerAppsで表示したいです。やり方などもし分かればご紹介してほしいです。(PowerAutomateを呼び出して使う?)

SharePoint リストにアップする際のExcel ファイルの前提条件

ローカルのExcel ファイルをSharePoint リストにアップロードする際、Excel ファイルはある前提条件を満たしている必要があります。
それは、対象データが"テーブル"内に存在している必要があるということです。

対象ファイルにテーブルが存在しない場合、以下のようなメッセージが表示されます。

f:id:koruneko:20201231230405p:plain

Excel ファイルにテーブルを設定する

では、Excel ファイルにテーブルを設定する方法を紹介します。
知っている方は飛ばしてもらって大丈夫です。

例えば以下のような表がExcel に存在していたとします。

f:id:koruneko:20201231230601p:plain

この表をテーブルに設定してみます。

  1. まずは対象の列(今回であれば $A$1:$C$14 )して「挿入 > テーブル」を選択、もしくは"Ctrl + T"を押します。
    f:id:koruneko:20201231234439p:plain
  2. すると以下のようなメッセージがでてくるので"OK"を選択します。
    先頭行もデータとして利用するのであれば、「先頭行をテーブルの見出しとして使用する(M)」のチェックを外してください。
    f:id:koruneko:20201231234518p:plain
  3. これでテーブルの設定が完了しました。
    デザインやテーブル名を変更したいときは、「テーブルデザイン」より変更してください。
    f:id:koruneko:20201231232921p:plain

以上でテーブルの設定は完了です。

なお、注意として A:C のように選択してテーブルに設定するのはやめましょう。
読み込みの際に、読み込み範囲のエラーが表示されてしまいます。

Power Automate などで読み込む場合もテーブルの設定が必要なのでこのやり方は是非覚えておきましょう。

ローカルのExcel ファイルをSharePoint リストにアップロードする

  1. リストを作成したいSharePoint サイトを開いて「新規 > リスト」を選択します。
    f:id:koruneko:20201231233256p:plain
  2. Excel から」を選択します。
    f:id:koruneko:20201231233531p:plain
  3. 今回ローカルからアップロードしたいので「ファイルをアップロードする」を選択します。
    f:id:koruneko:20201231233717p:plain
  4. 問題なければ「次へ」を選択します。
    f:id:koruneko:20201231234753p:plain
  5. 名前などを設定したら、「作成」を選択します。
    f:id:koruneko:20201231234859p:plain

以上でローカルのExcel ファイルをSharePoint リストにアップロードすることができました!

f:id:koruneko:20201231234949p:plain

Power Apps でSharePoint リストを表示する

Power Apps でSharePoint リストを表示する方法は何通りかありますが、一番簡単な方法を紹介します。

先ほど作成したリストより、「PowerApps > アプリの作成」を選択します。

f:id:koruneko:20201231235624p:plain

適当な名前をつけます。

f:id:koruneko:20201231235744p:plain

たったこれだけで、アプリの作成ができます!!

f:id:koruneko:20201231235900p:plain

おわりに

以上で紹介は終了です。
他質問あればコメントにてお願いしますー。

ではよいお年を。

【Power Apps】糸通しゲームを作成してみる

はじめに

みなさん"糸通し"というアプリをご存じでしょうか?

play.google.com

元祖糸通し

元祖糸通し

  • Spicysoft Corp.
  • ゲーム
  • 無料
apps.apple.com

今回は上記のようなアプリをPower Apps で作成してみましたので作成方法を纏めてみようと思います!

糸通しゲームを作成する

糸を描画する

糸のアニメーションはSVG で描画させたいと思います。

糸の情報は trajectory というコレクションに格納したいと思います。

trajectory には1ms単位のx座標、y座標、ボタンが押されたか?の情報を保持させようと思います。

n - 1 レコード目とn レコード目、n + 1レコード目の点をそれぞれ繋ぐように線を引くことで糸を作成しています。

以下のようにコレクションの初期化、糸の描画のための記載をしましょう。

PlayScreen.OnVisible

ClearCollect(trajectory, {x:0, y:PlayScreen.Height / 2, press:false});

Image.Image

"data:image/svg+xml,"& 
EncodeUrl(
    "<svg viewBox='0 0 "& Self.Width & " " & Self.Height & "' xmlns='http://www.w3.org/2000/svg'>
        <path d='" &
        Concat(
            ForAll(
                Sequence(CountRows(trajectory)),
                If(
                    Value = 1,
                    "M " & First(trajectory).x & " " & First(trajectory).y,
                    If(Last(FirstN(trajectory, Value)).press = Last(FirstN(trajectory, Value + 1)).press,
                        " L" & Last(FirstN(trajectory, Value)).x & " " & Last(FirstN(trajectory, Value)).y,
                        " S" & Last(FirstN(trajectory, Value)).x & " " & 
                               Last(FirstN(trajectory, Value)).y & " " &
                               Last(FirstN(trajectory, Value + 1)).x & " " & 
                               Last(FirstN(trajectory, Value + 1)).y
                    )
                )
            ),
            Value  
        )
        & "
    </svg>"
)

初期位置はx座標は0、y座標は画面の中心にくるように設定しています。

心の糸の描画箇所ですが、先ほども記載したように点と点を繋ぐようにして描画させています。

ただ点と点を繋いだだけでは線のUp/Down の切り替えタイミングが滑らかな線にならず切替点が尖ったものになってしまいますので、切替点は丸く、滑らかになるようにしたいと思います。
それを実現しているのが、

" S" & Last(FirstN(trajectory, Value)).x & " " & 
       Last(FirstN(trajectory, Value)).y & " " &
       Last(FirstN(trajectory, Value + 1)).x & " " & 
       Last(FirstN(trajectory, Value + 1)).y

です。

Up/Down が切り替わるのは、Nレコード目とN + 1レコード目のPress が異なっていた場合です。
それを、 Last(FirstN(trajectory, Value)).press = Last(FirstN(trajectory, Value + 1)).press を条件にして上記を実現させています。

trajectory にレコードを追加する

trajectory の情報によって糸は作成されます。

このコレクションのレコードは1ms単位で更新していきたいと思います。

なので、Duration:1のタイマーを追加し、以下のように設定します。

Timer.OnTimerStart

Collect(
    trajectory, 
    {
        x:If(Last(trajectory).x >= PlayScreen.Width / 2, Last(trajectory).x, Last(trajectory).x + PlayScreen.Width / 100), 
        y:Last(trajectory).y + If(Button1.Pressed, -1, 1) * PlayScreen.Height / 300,
        press:Button1.Pressed
    }
);

また、Up/Down を切り替えるためのボタンも作成しておいてください。

糸が動いているようにみせるために、xは時間経過で最後のレコードのx座標にプラスした値を設定していきます。
ただし、ずっとx座標の値をプラスし続けていると、画面からはみでてしまいます。
なので画面中央に来たタイミングでx座標の値のプラスは行わないようにしています。

y座標は最後の値にボタンが押されていないときはプラスし、ボタンが押されているときはマイナスします。

このままでは画面中央まで糸が到達した後、線が想定通り引かれません。

よってタイマー終了時、糸が画面中央まできていた場合以下のようにしてx座標の値を更新したいと思います

Timer.OnTimerEnd

If(
    CountRows(trajectory) >= 50, 
    RemoveIf(trajectory, x = First(trajectory).x, y = First(trajectory).y);
    UpdateIf(trajectory, true, {x:x - PlayScreen.Width / 100})
);

上記のようにすることにより、糸の点の情報が左方向にずれていくため、相対的に糸が右に進んでいるように見えるというわけです。

また、画面外にずれた情報は不要ですので削除しちゃいましょう。

これで糸のアニメーションを作成することができました!

リングを作成する

糸が通過するためのリングを作成したいと思います。

リングの情報は ring に持ちたいと思います。

このコレクションにはリングの座標と、リングの大きさを保持しています。

PlayScreen.OnVisible

ClearCollect(ring, {cx:PlayScreen.Width + 15, cy:PlayScreen.Height / 2, ry:50});

このコレクション情報をもとにリングを描画したいと思います。

Image.Image

' stroke='black' fill='transparent' stroke-width='2' />" & 
        Concat(
            ForAll(
                Sequence(CountRows(ring)),
                "<ellipse 
                    cx='" & Last(FirstN(ring, Value)).cx & "' 
                    cy='" & Last(FirstN(ring, Value)).cy & "' 
                    rx='15' 
                    ry='" & Last(FirstN(ring, Value)).ry & "' 
                stroke='red' fill='none' stroke-width='2' />"
            ),
            Value
        )

これで赤い楕円のリングを描画することができます。

続いて、糸の情報と同じように1msごとにコレクションの情報を更新したいと思います。

Timer.OnTimerStart

If(
    Last(ring).cx < PlayScreen.Width * 2 / 3,
    Collect(
        ring,
        {
            cx:PlayScreen.Width,
            cy:
                With(
                    {val:Last(ring).cy + Rand() * PlayScreen.Height * 2 / 9 - PlayScreen.Height * 2 / 18},
                    If(val > PlayScreen.Height - 150, 
                           PlayScreen.Height - 150,
                       val < 150,
                           150,
                       val
                    )
                ),
            ry:50 / (Rand() * 1 / 2 + 1)
        }
    )
)

Timer1.OnTimerEnd

UpdateIf(ring, true, {cx:cx - PlayScreen.Width / 100});
RemoveIf(ring, cx < -15);

リングは1msごとに作成する必要はないので、If 関数で作成する条件を制御しています。
今回、条件はリングが画面の2 / 3まできたら新規で追加するようにしています。

追加されたリングは1msごとに左にずれるようx座標を更新し、画面外に出た情報は削除するようにしています。

これでリング部分の実装が完成です!

糸がリングを通過したか判定を行う

糸がリングを通過したか判定を行うのには、トグルを利用します。

"切り替え"を追加して、以下のように設定します。

Toggle.Default

CountRows(
    Filter(
        ring,
        cx - 15 <= Last(trajectory).x,
        cx + 15 >= Last(trajectory).x,
        cy - ry > Last(trajectory).y ||
        cy + ry < Last(trajectory).y
    )
) <> 0

トグルが true になると、糸がリングを通過していないのでゲーム終了となります。

上記条件を簡単に解説します。

まず、判断を行うのは糸の先端ですので Last(trajectory) で糸の先端部分の座標を取得しています。
cx - 15 としているのは、リング横幅を15で固定しているためです。

x座標はリング内にいたときにいたときをフィルタリングしたのに対し、y座標はリング外にいたときでフィルタリングする必要があります。

y座標がリング外にいるというのは以下2つの場合が存在します。

  • リング上部より上にいるとき
  • リング下部より下にいるとき

書き出してみると当たり前じゃん。という感じですが、「どういったときにやりたいことを満たせる条件になるか」を考えてみると実装が少し楽になったりします。

1つ目の条件を満たしているのは、 cy - ry > Last(trajectory).y
2つ目の条件を満たしているのが、 cy + ry < Last(trajectory).y
です。

これでPower Apps での糸通しゲームの完成です!

おわりに

ご覧のとおり結構単純な実装で糸通しゲームが作成できました!

この技術を応用すれば横スクロールゲームも作成できそうですね。

長い文字列を変数に保存した際にPower Apps が落ちる?(検証中)

はじめに

Power Apps でアプリ作成中に文字列(150万文字程)を保存しようとしたところ下記のようにページが応答しなくなったので、色々試したみた記事になります。

ただし具体的な制限などはまだみつけられていません...
自身のメモ書き程度に記載された記事です。
具体的な結論はまだみつかっていません。
ご容赦ください。

f:id:koruneko:20201221002448p:plain

事象

Power Apps にて、150万文字程度の文字列をSet 関数を用いて変数に保存しようとしたところ下のように応答なしとなってしまった。

f:id:koruneko:20201221003448p:plain

(スクショミスって応答なしが含まれていませんが操作できなくなっています。)

この事象が、一体何文字から発生するのか?検証すべく10万文字単位で文字数を増やしてみたのですが、なんと170万文字を超えても保存できてしまったんですよね...

再現性のない事象はやめてほしいですね...

以下は10万文字単位で検証した動画ではないですが、150万文字程度の文字列の貼り付け、表示、及び保存を行ってときのPower Apps の挙動です。

動画からもわかる通り、一応応答は返ってきてはいますが、結構動作がもっさりしてしまっていますね。

もしかすると、応答なしとなる一歩手前な状態で、応答なしとなるかどうかはそのときのネットワークやPCの状態によるのかもしれません。

引き続き調査し、なにかわかりましたら追記したいと思います。

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 の解説がメインになりそうです。

それではーノシ


スポンサードリンク