アプリ開発の調査にかかる
時間を削減したい
内製化支援サービス
アプリを自分たちで
作成できるようになりたい
DX人材育成プログラム
プロに開発を依頼したい
アプリ開発導入支援サービス
機能拡張サービス
X-SP Feature
デザイン拡張サービス
X-SP Design
モダン化から運用管理までサポート
構築支援サービス
突然ですが皆さん、普段Power Appsでアプリを構築していて、「このコントロールの機能が少し物足りないんだよなぁ…」といった経験はございませんか?
ローコードで構築されるPower Appsは、どんな機能でも実現できる万能なツールというわけではないのも事実です。
そこで本記事では前回に引き続き、PCF(Power Apps Component Framework)によるカスタムコンポーネントの作成について解説してみたいと思います。
弊社はPower Platform(Power Apps・Power Automate)に関するアプリ開発や、
皆様が内製化を行う際の支援サービスを提供しておりますので、
Power Platformに関する内容でお悩みがある場合は、以下からぜひお問い合わせください。
動作イメージ
今回は渡された文字列が横幅を超える場合に自動でスクロールされるラベルコントロールを作ってみました!

必要なライセンス
今回ご紹介するPCFやカスタムコンポーネントの構築、利用ではプレミアムライセンスなどの特別なライセンスは不要ですが、
カスタムコンポーネント内でDataverseや外部のデータベース、APIなどと接続する場合にはプレミアムライセンスが必要となります。
そのため、プレミアムライセンスを契約せずにAPIなどを利用することはできない、という点にご注意ください。
また、Power Platform環境でPCFを有効化する際には環境管理者などの権限が必要になります。
ですので、企業などで利用される場合は事前にシステム管理者の方にご相談されるのが良いかと思われます。
カスタムコンポーネントの構築
それでは構築を進めていきます!
1. コンポーネントの構築
環境構築やプロジェクトの作成は前回の記事で解説しましたので、早速コンポーネントの構築を進めていきます。
2.1 マニフェストの編集
まずはマニフェストの編集から行っていきます。
マニフェストにはコンポーネントのメタデータやプロパティ、イベントなどの設定情報がXML形式で記載されています。
公式ドキュメント:マニフェストの実装
マニフェストの編集はプロジェクトフォルダ内のControlManifest.Input.xmlで行います。今回は以下のように記述しました。
<?xml version="1.0" encoding="utf-8" ?>
<manifest>
<control namespace="PCF" constructor="AutoScrollLabel" version="0.0.1" display-name-key="AutoScrollLabel" description-key="AutoScrollLabel description" control-type="standard" >
<external-service-usage enabled="false">
</external-service-usage>
<property name="ScrollText" display-name-key="テキスト" description-key="表示する文字列を入力します" of-type="SingleLine.Text" usage="bound" required="false" />
<resources>
<code path="index.ts" order="1"/>
</resources>
</control>
</manifest>
実際にスクロールさせるテキストを入力するためのプロパティとしてScrollTextを追加しました。
マニフェストの編集が完了したら以下のコマンドを実行しましょう!
※コマンドを実行する前に、マニフェストファイルを保存できているか確認しましょう!
npm run refreshTypes

このコマンドを実行すると、ControlManifest.Input.xmlで追加したプロパティがgeneratedフォルダ内のManifestTypes.d.tsに反映されます。

以上でマニフェストの編集は完了です。
2.2 処理の作成
いよいよ処理の実装を行っていきます。プロジェクトフォルダ内のindex.tsにTypeScriptで記述していきます。
import {IInputs, IOutputs} from "./generated/ManifestTypes";
export class AutoScrollLabel implements ComponentFramework.StandardControl<IInputs, IOutputs> {
private container: HTMLDivElement;
private labelElement: HTMLDivElement;
private scrollPos: number = 0;
private scrollSpeed: number = 1;
private animationFrameId: number | null = null;
constructor() {}
public init(
context: ComponentFramework.Context<IInputs>,
notifyOutputChanged: () => void,
state: ComponentFramework.Dictionary,
container: HTMLDivElement
): void {
// コンポーネントのコンテナを初期化
this.container = container;
// ラベル要素を作成
this.labelElement = document.createElement("div");
this.labelElement.style.whiteSpace = "nowrap";
this.labelElement.style.overflow = "hidden";
this.labelElement.style.display = "block";
this.labelElement.style.width = "100%";
// スタイル設定
this.labelElement.style.border = "1px solid #ccc";
// 初期テキスト
this.labelElement.innerText = context.parameters.ScrollText.raw || "Scrolling Label";
this.container.appendChild(this.labelElement);
// スクロールを開始
this.startScrolling();
}
public updateView(context: ComponentFramework.Context<IInputs>): void {
// テキストの更新
const newText = context.parameters.ScrollText.raw || "";
if (newText !== this.labelElement.innerText) {
this.labelElement.innerText = newText;
this.scrollPos = 0; // リセット
}
}
public getOutputs(): IOutputs {
return {};
}
public destroy(): void {
// クリーンアップ
if (this.animationFrameId) {
cancelAnimationFrame(this.animationFrameId);
}
}
private startScrolling(): void {
const scroll = () => {
if (this.labelElement.scrollWidth > this.labelElement.clientWidth) {
this.scrollPos = (this.scrollPos + this.scrollSpeed) % this.labelElement.scrollWidth;
this.labelElement.scrollLeft = this.scrollPos;
}
this.animationFrameId = requestAnimationFrame(scroll);
};
scroll();
}
}
まずは以下のコマンドでビルドします。
npm run build
ビルドが完了したらさらに次のコマンドを実行します。
npm start

前回の記事で解説したように、ブラウザ上でPCFのテスト環境が立ち上がり、構築したコンポーネントの動作確認を行うことができます!
画面右にはプロパティ値が入力できるようになっていて、マニフェストで設定したScrollTextプロパティもありますね。
それではラベルに表示されている文字列のスクロールが行えるか、少し長めのテキストを入力してみましょう。

できました!ラベルの文字列が自動でスクロールされています!
さて、自動スクロール機能は実装できましたが、ラベルコントロールとして運用するには少し物足りない部分がありそうですね。
-
文字列が中央寄せになっているが、スクロールを前提とすると左寄せにしたい
-
文字列が初期位置に戻った後すぐにスクロールされて読みづらいので、スクロールに待機時間がほしい
-
フォントやサイズを設定できるようにしたい
-
PaddingやMarginなどのプロパティを設定できると余白を作れて見やすそう
この辺が一般的なラベルコントロールにも実装されているポイントでしょうか。
ということで簡単に手直ししていきます。
↓※プルダウンで表示できるようにしています。↓
ControlManifest.Input.xml
<?xml version="1.0" encoding="utf-8" ?>
<manifest>
<control namespace="reirePCF" constructor="AutoScrollLabel" version="0.0.1" display-name-key="AutoScrollLabel" description-key="AutoScrollLabel description" control-type="standard">
<external-service-usage enabled="false">
</external-service-usage>
<property name="ScrollText" display-name-key="テキスト" description-key="表示する文字列を入力します" of-type="SingleLine.Text" usage="bound" required="false" />
<property name="DelayTime" display-name-key="遅延時間" description-key="スクロールが始まるまでの時間を指定します" of-type="Whole.None" usage="bound" required="false" default-value="3000" />
<property name="FontSize" display-name-key="フォントサイズ" description-key="文字の大きさを指定します" of-type="Whole.None" usage="bound" required="false" default-value="13" />
<property name="Font" display-name-key="フォント" description-key="文字のフォントを指定します" of-type="Enum" usage="bound" required="false" default-value="1">
<value name="Arial" display-name-key="Arial">0</value>
<value name="Open Sans" display-name-key="Open Sans">1</value>
<value name="Verdana" display-name-key="Verdana">2</value>
<value name="Courier New" display-name-key="Courier New">3</value>
<value name="Georgia" display-name-key="Georgia">4</value>
</property>
<property name="LPadding" display-name-key="パディング(左)" description-key="左のパディングを指定します" of-type="Whole.None" usage="bound" required="false" default-value="0" />
<property name="RPadding" display-name-key="パディング(右)" description-key="右のパディングを指定します" of-type="Whole.None" usage="bound" required="false" default-value="0" />
<property name="TPadding" display-name-key="パディング(上)" description-key="上のパディングを指定します" of-type="Whole.None" usage="bound" required="false" default-value="0" />
<property name="BPadding" display-name-key="パディング(下)" description-key="下のパディングを指定します" of-type="Whole.None" usage="bound" required="false" default-value="0" />
<property name="LMargin" display-name-key="マージン(左)" description-key="左のマージンを指定します" of-type="Whole.None" usage="bound" required="false" default-value="0" />
<property name="RMargin" display-name-key="マージン(右)" description-key="右のマージンを指定します" of-type="Whole.None" usage="bound" required="false" default-value="0" />
<resources>
<code path="index.ts" order="1" />
</resources>
</control>
</manifest>
index.ts
import { IInputs, IOutputs } from "./generated/ManifestTypes";
export class AutoScrollLabel implements ComponentFramework.StandardControl<IInputs, IOutputs> {
private container: HTMLDivElement;
private labelElement: HTMLDivElement;
private scrollPos: number = 0;
private scrollSpeed: number = 1;
private animationFrameId: number | null = null;
private delayTime: number = 5000; // デフォルト値(5秒)
private fontfamily: string[] = ["Arial", "Open Sans", "Verdana", "Courier New", "Georgia"];
constructor() {}
public init(
context: ComponentFramework.Context<IInputs>,
notifyOutputChanged: () => void,
state: ComponentFramework.Dictionary,
container: HTMLDivElement
): void {
// コンポーネントのコンテナを初期化
this.container = container;
// ラベル要素を作成
this.labelElement = document.createElement("div");
this.labelElement.style.whiteSpace = "nowrap";
this.labelElement.style.overflow = "hidden";
this.labelElement.style.display = "block";
this.labelElement.style.boxSizing = "border-box";
this.labelElement.style.height = "100%";
this.labelElement.style.textAlign = "left";
this.labelElement.style.border = "none";
this.labelElement.style.borderWidth = "0px";
// スタイル設定
this.styleReset(context);
// 初期テキスト
this.labelElement.innerText = context.parameters.ScrollText.raw || "Scrolling Label";
this.container.appendChild(this.labelElement);
// ディレイタイムを取得(デフォルト値: 5000ms)
this.delayTime = context.parameters.DelayTime.raw || 5000;
// スクロールを開始
this.startScrolling(this.delayTime);
}
public updateView(context: ComponentFramework.Context<IInputs>): void {
// テキストの更新
const newText = context.parameters.ScrollText.raw || "";
// ディレイタイムを更新
const newdelayTime = context.parameters.DelayTime.raw || 5000;
// スタイル設定
this.styleReset(context);
if (newText !== this.labelElement.innerText || newdelayTime !== this.delayTime) {
this.labelElement.innerText = newText;
this.scrollPos = 0; // リセット
// ディレイタイムを更新
this.delayTime = newdelayTime;
// 前回のスクロールを停止
if (this.animationFrameId) {
cancelAnimationFrame(this.animationFrameId);
}
// 再度スクロールを開始
this.startScrolling(this.delayTime);
}
}
public getOutputs(): IOutputs {
return {};
}
public destroy(): void {
// クリーンアップ
if (this.animationFrameId) {
cancelAnimationFrame(this.animationFrameId);
}
}
private startScrolling(delay: number): void {
const scroll = () => {
if (this.labelElement.scrollWidth > this.labelElement.clientWidth) {
this.scrollPos += this.scrollSpeed;
// テキストが右端を通過した場合に初期位置に戻る
if (this.scrollPos >= this.labelElement.scrollWidth) {
this.scrollPos = 0;
this.labelElement.scrollLeft = this.scrollPos;
// ディレイを適用
setTimeout(() => {
this.animationFrameId = requestAnimationFrame(scroll);
}, delay);
return;
}
this.labelElement.scrollLeft = this.scrollPos;
}
this.animationFrameId = requestAnimationFrame(scroll);
};
scroll();
}
private styleReset(context: ComponentFramework.Context<IInputs>): void {
const totalmargin = (context.parameters.LMargin.raw || 0) + (context.parameters.RMargin.raw || 0);
this.labelElement.style.fontSize = (context.parameters.FontSize.raw || 16) + "px";
this.labelElement.style.fontFamily = this.fontfamily[Number(context.parameters.Font.raw) || 1];
this.labelElement.style.paddingLeft = (context.parameters.LPadding.raw || 0) + "px";
this.labelElement.style.paddingRight = (context.parameters.RPadding.raw || 0) + "px";
this.labelElement.style.paddingTop = (context.parameters.TPadding.raw || 0) + "px";
this.labelElement.style.paddingBottom = (context.parameters.BPadding.raw || 0) + "px";
this.labelElement.style.marginLeft = (context.parameters.LMargin.raw || 0) + "px";
this.labelElement.style.marginRight = (context.parameters.RMargin.raw || 0) + "px";
this.labelElement.style.width = `calc(100% - ${totalmargin}px)`;
}
}
結果…

コンポーネントに対して両端に余白を持たせたり、フォントやサイズを変更できるようになりました!これでコンポーネントの構築が完了です!
3. 環境へのインポート
良い感じにコンポーネントを構築することができました!
せっかく作った自作のカスタムコンポーネント、普段開発している環境内のAppsにも適用してみたいですよね。
さっそく環境へのインポートを行いましょう。
環境へのインポートを行う手段はいくつかありますが、今回はMicrosoftの公式ドキュメントでも紹介されている、ターミナルから環境に直接プッシュする方法で進めたいと思います。
公式ドキュメント:コード コンポーネントをパッケージ化する
3.1 PCFの有効化
まず大前提として、Power Platform管理センターより環境内でのPCFを有効化してあげる必要があります。
-
Power Platform管理センターで環境を選択する
-
カスタムコンポーネントをインポートしたい環境を選択する
-
-
画面上部のタブから設定を選択する
また、この画面で環境URLをメモしておきましょう -
製品から機能を開く
-
「コード コンポーネントでキャンバス アプリの公開を許可する」をオンにする
3.2 環境への接続
続いてターミナルから環境へ接続します。先ほどメモしておいた環境URLを使用します。
pac auth create --url [環境URL]
Microsoftのサインイン画面がポップアップするので、任意の環境に接続できるアカウントでログインします。

正常に認証されれば環境への接続は完了です。
3.3 コンポーネントをプッシュ
では環境へコンポーネントをプッシュしていきましょう。以下のコマンドを実行します。
pac pcf push --publisher-prefix [任意の公開元の接頭辞]
実行には数分かかる場合があります。完了すると、以下のようなメッセージが表示されて、コンポーネントのプッシュが完了します。

4. アプリへの適用
これで自作したカスタムコンポーネントが皆さんの環境へデプロイされました!それでは早速、アプリで使用してみましょう!
4.1 コンポーネントのインポート
-
まずは任意のアプリのエディターを開きます。
-
続いてツリービューのコンポーネントでインポートを選択します。
-
コンポーネントの選択画面が開きます。コードタブを選択すると、先ほど環境へプッシュしたコンポーネントがいるので選択してインポートしましょう!
-
この状態でコンポーネント一覧を開くと、コードコンポーネントの中にインポートしたカスタムコンポーネントがいますね!これでカスタムコンポーネントのインポートは完了です!
4.2 アプリで使ってみる
あとはいつも通りアプリへコントロールを追加して、プロパティを入力するだけです!

これで…

自作のカスタムコンポーネントをアプリに適用することができました!!
おわり
以上、PCFを使用してカスタムコンポーネントを構築し、環境内のアプリへ適用までを行ってみました。
一通り触ってみた感想としては以下の通りです。
-
マニフェスト周りの設定は、最初はどう記述すべきか分からず困惑しやすい
-
Microsoftの公式ドキュメントなどもありますが、翻訳が微妙なところもあり理解するのが少し大変…
-
この辺のナレッジについて日本ではまだディープな記事が少ないので、海外の有志のブログやコミュニティ、動画を漁るのがよさそう
-
完全にローコードではなくプログラミング知識を要するため、構築したカスタムコンポーネントをどう保守していくのかについてはよく検討する必要がある
少しネガティブな印象ばかり書いてしまいましたが、それを差し置いても自分が欲しいコンポーネントを好きに自作してアプリに適用できるのは便利ですし、何より楽しいです!
ぜひ皆さんもオリジナルのカスタムコンポーネントを構築して、ワンランク上のPower Appsアプリを開発してみていただければと思います!









こんにちは。アーティサン株式会社の伊礼(いれい)です。