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

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

Power Apps でタスク管理アプリを作成したおはなし


スポンサードリンク

はじめに

Power Apps でタスク管理作成してみましたのでその作成方法を簡単にまとめます。

こちらのアプリの作成した経緯はAkira さんからバトンをもらったからです。
面白い試み?企画?ですねー

Akira さんはmk さんからバトンが回ってきて作成したようです。

関連記事

Akira さん

hanakuso365.hatenablog.com

mk さん

note.com

タスク管理アプリを作成する

ヘッダを作成する

ヘッダ部分ですが以下みたいなちょっとポップなフォントにして、縁取りしたかったのですが

f:id:koruneko:20210912234133p:plain

これはPower Apps のラベルコントロールだけでは実現できません。

フォントの問題に関しては、Power Apps のフォントプロパティの選択肢だと以下しか選べませんが

f:id:koruneko:20210912235612p:plain

このようにフォント名を直接指定することで、 Font であらかじめ定義されているフォント以外も指定することができます。

f:id:koruneko:20210912235728p:plain

フォント名にスペースを含む場合は ' (シングルクォート)で囲めばよいです。

f:id:koruneko:20210913000120p:plain

ただ、文字の縁取りは現状Power Apps のラベルコントロールでは行うことができません。

なので今回はSVG を使って文字を表示させたいと思います。

SVG は画像コントロールImage

Image

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

みたいに記載することで利用できます。

ほかの表現方法は

koruneko.hatenablog.com

をご覧ください。

画像コントロールを追加し、以下のように式を記載します。

TitleCharImage.Image

"data:image/svg+xml,"& 
EncodeUrl(
    "<svg viewBox='0 0 "& Self.Width & " " & Self.Height & "' xmlns='http://www.w3.org/2000/svg'>
    <rect width='"& Self.Width & "' height='" & Self.Height & "' fill='aqua' />
    <text 
            x='"& (Self.Width - AuthorImage.Width) / 2 &"' 
            y='"& Self.Height / 2 - 10 &"' 
            font-family='HGPSoeiKakupoptai' 
            font-size='60px'
            font-weight='bold'
            stroke='black'
            stroke-width='2px'
            fill='white'
            text-anchor='middle'
        >
            今から
            <tspan dx='-100' dy='70'>なにする?</tspan>
        </text>
    </svg>"
)

<rect> 部分が背景の塗りつぶし。
<text> 部分が文字の設定。
となっています。

今回は細かい説明は省きます。

ToDoList を作成する

ToDoList は以前Redo & Undo の実装方法の説明で紹介したもののほぼ使いまわしです。

koruneko.hatenablog.com

少し変えた点として、Redo & Undo 時、詳細を表示するかの判定は含めないので、現在のコレクションの値を参照するように変更しました。

UndoIcon.OnSelect

Set(nowHistoryID, nowHistoryID - 1);
With(
    {
        colVal: MatchAll(LookUp(History, _id = nowHistoryID, data), "(?<="":).*?(?=(,|}))")
    },
    If(
        Mod(CountRows(colVal), 9) = 0,
        ClearCollect(
            Tasks,
            ForAll(
                Sequence(CountRows(colVal) / 9),
                {
                    _id:        Value(Last(FirstN(colVal, 9 * ThisRecord.Value - 8)).FullMatch),
                    achievement:Value(Last(FirstN(colVal, 9 * ThisRecord.Value - 7)).FullMatch),
                    details:    Substitute(Last(FirstN(colVal, 9 * ThisRecord.Value - 6)).FullMatch, """", ""),
                    endDate:    Value(Last(FirstN(colVal, 9 * ThisRecord.Value - 5)).FullMatch),
                    level:      Substitute(Last(FirstN(colVal, 9 * ThisRecord.Value - 4)).FullMatch, """", ""),
                    startDate:  Value(Last(FirstN(colVal, 9 * ThisRecord.Value - 3)).FullMatch),
                    status:     Substitute(Last(FirstN(colVal, 9 * ThisRecord.Value - 2)).FullMatch, """", ""),
                    task:       Substitute(Last(FirstN(colVal, 9 * ThisRecord.Value - 1)).FullMatch, """", ""),
-                   viewDetail: Substitute(Last(FirstN(colVal, 9 * ThisRecord.Value - 0)).FullMatch, """", "") = "true"
+                   // 詳細の表示/非表示は履歴に含めないので元コレクション参照
+                   viewDetail: Last(FirstN(Tasks, ThisRecord.Value)).viewDetail
                }
            )
        )
    )
)

また、スクリーンを遷移しても変わらず設定したタスクを記憶しておきたかったので、 Screen.OnVisible に記載されていた式は App.OnSelect に変更しました。

App.OnSelect

ClearCollect(
    Tasks,
    {
        _id:1,
        task:"",
        details:"",
        startDate:Today(),
        endDate:Today() + 1,
        level:"Normal",
        status:"未開始",
        achievement:0,
        viewDetail:false
    }
);
ClearCollect(
    History, 
    {
        _id:1, 
        data:JSON(Tasks, JSONFormat.IgnoreBinaryData)
    }
);
Set(nowHistoryID, 1)

これにより、 nowHistoryID がコンテキスト変数からグローバル変数に変更になったのでそれに伴って他で利用しているコンテキスト変数もグローバル変数を更新するように変更しておきましょう。
(RedoIcon.OnSelect , UndoIcon.OnSelect , SetHistory.OnSelect が対象のはずです。)

サイコロを作成する

サイコロは以下のようなサイコロっぽいレイアウトで作成します。

f:id:koruneko:20210913004354p:plain

Power Apps のアイコンコントロールに存在する、Rectangle だけではこのようなレイアウトを作成することはちょっとできないので、ここもSVG で表現します。

DiceImage.Image

"data:image/svg+xml,"& 
EncodeUrl(
    "<svg viewBox='0 0 "& Self.Width & " " & Self.Height & "' xmlns='http://www.w3.org/2000/svg'>
      <defs>
          <g id='cube' class='cube-unit'>
              <rect 
                    width='" & 31.5 * RoundDown(Self.Width / 75, 0) &"' 
                    height='" & 36 * RoundDown(Self.Height / 75, 0) &"' 
                    fill='#CCCCCC' 
                    stroke='#000000' 
                  transform='skewY(30) scale(1,1) rotate(0) translate(0 0)'
                />
              <rect 
                    width='" & 31.5 * RoundDown(Self.Width / 75, 0) &"' 
                    height='" & 36 * RoundDown(Self.Height / 75, 0) &"' 
                    fill='#EEEEEE'
                    stroke='#000000'  
                  transform='skewY(-30) scale(1,1) rotate(0) translate(" & 31.5 * RoundDown(Self.Width / 75, 0) &" " & 36 * RoundDown(Self.Height / 75, 0) &".3)'
                />
              <rect 
                    width='" & 31.5 * RoundDown(Self.Width / 75, 0) &"' 
                    height='" & 31.5 * RoundDown(Self.Height / 75, 0) &"' 
                    fill='#FFFFFF'
                    stroke='#000000'   
                  transform='skewY(0) scale(1.41,.81) rotate(45) translate(0 -" & 31.5 * RoundDown(Self.Width / 75, 0) &")'
                />
          </g>
      </defs>
      <use 
          href='#cube' 
          x='" & 5 * RoundDown(Self.Width / 75, 0) &"' 
          y='" & 20 * RoundDown(Self.Height / 75, 0) &"'
      />
        <text 
            x='" & (35 + Len(Text(Last(FirstN(Objectives, 1))._id))) * RoundDown(Self.Width / 75, 0) &"' 
            y='" & 23 * RoundDown(Self.Height / 75, 0) &"' 
            font-family='Verdana' 
            font-size='35'
            text-anchor='middle'
        >
            "& Last(FirstN(Objectives, 1))._id &"
        </text>
        <text 
            x='" & (20 + Len(Text(Last(FirstN(Objectives, 2))._id))) * RoundDown(Self.Width / 75, 0) &"' 
            y='" & 39 * RoundDown(Self.Height / 75, 0) &"' 
            font-family='Verdana' 
            font-size='35'
            text-anchor='middle'
            transform='skewY(30) scale(1,1) rotate(0) translate(0 0)'
        >
            "& Last(FirstN(Objectives, 2))._id &"
        </text>
        <text 
            x='" & (20 + Len(Text(Last(FirstN(Objectives, 3))._id))) * RoundDown(Self.Width / 75, 0) &"' 
            y='" & 45 * RoundDown(Self.Height / 75, 0) &"' 
            font-family='Verdana' 
            font-size='35'
            text-anchor='middle'
            transform='skewY(-30) scale(1,1) rotate(0) translate(" & 31.5 * RoundDown(Self.Width / 75, 0) &" " & 36 * RoundDown(Self.Height / 75, 0) &".3)'
        >
            "& Last(FirstN(Objectives, 3))._id &"
        </text>
    </svg>"
)

<rect> 要素は3つありますが、上から順に2, 3, 1(画像の数字)の四角形となっています。

<text> 要素も3つありますが、こちらは上から順に1, 2, 3(画像の数字)のテキストとなっています。

テキストで表示している数字はTodoList で作成したタスクの内部ID を表しています。(タスクのタイトルを表示してしまったら文字サイズや位置の調整などが面倒ですからね。。。)

ただ内部ID を表示されてもユーザはなにかわからないのでこのID がどのタスクを表しているのかわかるように参照できるリストを表示させましょう。

f:id:koruneko:20210913010909p:plain

これは、ヘッダ部分はコンテナで作成し、リスト部分はギャラリーで作成しています。

f:id:koruneko:20210913011003p:plain

サイコロでタスクの抽選を行う

「サイコロを動かす」というタイトルにしたかったのですが、サイコロを振るようなアニメーションをSVG で表現するのは大変面倒だったので、今回は辞めました。。。

animateTransform を利用するのではなく、立方体の向きが異なるイメージを数パターン用意しておいてそれをパラパラ漫画のように表示させる方式であればサイコロを振っているようにみえるかもです。

今回は数字をランダムに変えることで抽選を行うことにしたいと思います。(それサイコロの意味ある?という指摘は無しでwww)

数字は Objectives コレクションの最初の3つの _id を表示するようにしているのでこのコレクションの中身をShuffle 関数でランダムに変更させます。

まず初期表示時にもランダムな数値を表示させたいので、 OnVisible に以下式を設定します。
また、 OnVisible では、抽選中かどうかを判断させるための変数を宣言しておきます。

Screen.OnVisible

ClearCollect(Objectives, Shuffle(Filter(Tasks, !(_id in todayTasks._id))));
UpdateContext({isRoll:false})

Shuffle(Filter(Tasks, !(_id in todayTasks._id))) の解説は後程行います。

抽選を行ったり止めたりするためのボタンを用意して、以下式を設定します。

RollBtn.OnSelect

UpdateContext({isRoll:!isRoll});

このボタンが押されたら(isRolltrue になったら)タイマーをスタートさせ、繰り返し処理を行われるようにして、 Objectives の中身をシャッフルさせます。

RollTimer.Start

isRoll

RollTimer.OnTimerEnd

ClearCollect(Objectives, Shuffle(Filter(Tasks, !(_id in todayTasks._id))));
Select(TaskReferenceGallery, First(Objectives)._id)

Duration はサイコロの数字の表示が切り替わる間隔になります。
私の設定では1 に設定しています。

OnTimerEnd にて Select(TaskReferenceGallery, First(Objectives)._id) という式を設定していますが、これはサイコロの右に設定しているID とタスクを紐づけために表示を行っているギャラリーでサイコロの一番上の数字(ID)のレコードを選択するための式となっています。
(このため、対象のギャラリーは現在選択されているレコードを塗りつぶすように設定しています。)

抽選されたタスクを記録する

抽選されたタスクを記録するためのコレクションを用意します。

タスクを記録するタイミングは抽選終了時なので、抽選ボタンの OnSelect で設定します。
ただし、抽選を行うのは抽選を開始したタイミングではなく抽選を終了したタイミングということに注意が必要です。

RollBtn.OnSelect

UpdateContext({isRoll:!isRoll});
If(
    !isRoll,
    Collect(
        todayTasks,
        AddColumns(
            Table(First(Objectives)),
            "ToDo",
            Last(Sort(todayTasks, SortOrder.Descending)).ToDo + 1
        )
    )
)

このとき、抽選されたタスクをそのまま設定するだけでなく ToDo フィールドを追加して"・やることそのX"という文字列を表示できるようにしておきます。

f:id:koruneko:20210913020003p:plain

抽選で選択されたタスクは抽選結果で選ばれないようにする

一度選ばれたタスクはもう抽選されてほしくないので、除外するようにします。

それが先ほど後で解説するといった、 Shuffle(Filter(Tasks, !(_id in todayTasks._id))) です。

in 演算子 では、 Tasks._id の値の中に存在する todayTasks._id を選択します。
ただ、今回は存在しないものを選択するようにしたいので ! で否定を取っています。

これで、選択されたタスクは抽選に含まれないようになりました。

ToDo フィールドを連番にする

追加されたToDo の値は連番で追加されていくようになっていますが、削除した場合はToDo の値は連番ではなく抜けてしまいます。

なので、削除したレコード以降のレコードのToDo の値を更新してあげる必要があります。

DeleteIcon の OnSelect にて選択されたレコードを削除し、そのレコードの値以降にToDo フィールドの値を更新するようにします。

DeleteIcon_1.OnSelect

RemoveIf(todayTasks, _id = ThisItem._id);
UpdateIf(todayTasks, ToDo >= ThisItem.ToDo, {ToDo:ToDo - 1})

おわりに

バトン企画楽しいですねー
同じようなテーマのアプリでも人によって作成するアプリが異なってきますし、実装方法も人それぞれなのが大変面白いと思いました。

他テーマでもやってみたいですねー

次はよーよんさん に(勝手に)バトン渡しましたが、どんなアプリが作成されるのか今から楽しみです!(ハードルを上げていく)


スポンサードリンク