技術情報ブログ
Power Platform
2026.01.28

Power Apps:自由を手に入れよう!カスタムコンポーネントを構築してみる【実装編】

Power Apps:自由を手に入れよう!カスタムコンポーネントを構築してみる【実装編】
伊礼圭吾

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

突然ですが皆さん、普段Power Appsでアプリを構築していて、「このコントロールの機能が少し物足りないんだよなぁ…」といった経験はございませんか?

ローコードで構築されるPower Appsは、どんな機能でも実現できる万能なツールというわけではないのも事実です。

そこで本記事では前回に引き続き、PCF(Power Apps Component Framework)によるカスタムコンポーネントの作成について解説してみたいと思います。

弊社はPower Platform(Power Apps・Power Automate)に関するアプリ開発や、
皆様が内製化を行う際の支援サービスを提供しておりますので、
Power Platformに関する内容でお悩みがある場合は、以下からぜひお問い合わせください。

 

動作イメージ

今回は渡された文字列が横幅を超える場合に自動でスクロールされるラベルコントロールを作ってみました!

PCF_01

 

必要なライセンス

今回ご紹介する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
PCF_13

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

PCF_14

以上でマニフェストの編集は完了です。

 

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_15

前回の記事で解説したように、ブラウザ上でPCFのテスト環境が立ち上がり、構築したコンポーネントの動作確認を行うことができます!

画面右にはプロパティ値が入力できるようになっていて、マニフェストで設定したScrollTextプロパティもありますね。

それではラベルに表示されている文字列のスクロールが行えるか、少し長めのテキストを入力してみましょう。

PCF_16

できました!ラベルの文字列が自動でスクロールされています!

さて、自動スクロール機能は実装できましたが、ラベルコントロールとして運用するには少し物足りない部分がありそうですね。

  • 文字列が中央寄せになっているが、スクロールを前提とすると左寄せにしたい

  • 文字列が初期位置に戻った後すぐにスクロールされて読みづらいので、スクロールに待機時間がほしい

  • フォントやサイズを設定できるようにしたい

  • 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)`;
  }
}

結果…

PCF_17

コンポーネントに対して両端に余白を持たせたり、フォントやサイズを変更できるようになりました!これでコンポーネントの構築が完了です!


3. 環境へのインポート

良い感じにコンポーネントを構築することができました!
せっかく作った自作のカスタムコンポーネント、普段開発している環境内のAppsにも適用してみたいですよね。
さっそく環境へのインポートを行いましょう。

環境へのインポートを行う手段はいくつかありますが、今回はMicrosoftの公式ドキュメントでも紹介されている、ターミナルから環境に直接プッシュする方法で進めたいと思います。
公式ドキュメント:コード コンポーネントをパッケージ化する


3.1 PCFの有効化

まず大前提として、Power Platform管理センターより環境内でのPCFを有効化してあげる必要があります。

  • Power Platform管理センターで環境を選択する

    PCF_18
  • カスタムコンポーネントをインポートしたい環境を選択する

  • PCF_19
  • 画面上部のタブから設定を選択する
    また、この画面で環境URLをメモしておきましょう

    PCF_20
  • 製品から機能を開く

    PCF_21
  • 「コード コンポーネントでキャンバス アプリの公開を許可する」をオンにする

    PCF_22


3.2 環境への接続

続いてターミナルから環境へ接続します。先ほどメモしておいた環境URLを使用します。

pac auth create --url [環境URL]

Microsoftのサインイン画面がポップアップするので、任意の環境に接続できるアカウントでログインします。

PCF_23

正常に認証されれば環境への接続は完了です。


3.3 コンポーネントをプッシュ

では環境へコンポーネントをプッシュしていきましょう。以下のコマンドを実行します。

pac pcf push --publisher-prefix [任意の公開元の接頭辞]

実行には数分かかる場合があります。完了すると、以下のようなメッセージが表示されて、コンポーネントのプッシュが完了します。

PCF_24


4. アプリへの適用

これで自作したカスタムコンポーネントが皆さんの環境へデプロイされました!それでは早速、アプリで使用してみましょう!


4.1 コンポーネントのインポート

  • まずは任意のアプリのエディターを開きます。

    PCF_25
  • 続いてツリービューのコンポーネントでインポートを選択します。

    PCF_26
  • コンポーネントの選択画面が開きます。コードタブを選択すると、先ほど環境へプッシュしたコンポーネントがいるので選択してインポートしましょう!

    PCF_27
  • この状態でコンポーネント一覧を開くと、コードコンポーネントの中にインポートしたカスタムコンポーネントがいますね!これでカスタムコンポーネントのインポートは完了です!

    PCF_28


4.2 アプリで使ってみる

あとはいつも通りアプリへコントロールを追加して、プロパティを入力するだけです!

PCF_29

これで…

PCF_01

自作のカスタムコンポーネントをアプリに適用することができました!!


おわり

以上、PCFを使用してカスタムコンポーネントを構築し、環境内のアプリへ適用までを行ってみました。

一通り触ってみた感想としては以下の通りです。

  • マニフェスト周りの設定は、最初はどう記述すべきか分からず困惑しやすい

  • Microsoftの公式ドキュメントなどもありますが、翻訳が微妙なところもあり理解するのが少し大変…

  • この辺のナレッジについて日本ではまだディープな記事が少ないので、海外の有志のブログやコミュニティ、動画を漁るのがよさそう

  • 完全にローコードではなくプログラミング知識を要するため、構築したカスタムコンポーネントをどう保守していくのかについてはよく検討する必要がある

少しネガティブな印象ばかり書いてしまいましたが、それを差し置いても自分が欲しいコンポーネントを好きに自作してアプリに適用できるのは便利ですし、何より楽しいです!

ぜひ皆さんもオリジナルのカスタムコンポーネントを構築して、ワンランク上のPower Appsアプリを開発してみていただければと思います!

Microsoftクラウド関連

シェアする
記事カテゴリ
最新記事
2026.01.28

Power Apps:自由を手に入れよう!カスタムコンポーネントを構築してみる【実装編】

2026.01.21

Power Apps:PCFって何?カスタムコンポーネントを構築してみる【環境構築編】

2026.01.14

【2026年1月更新】Power Automate 初心者 ~ 中級者 向けロードマップ

2026.01.07

【2026年1月更新】Power Apps の実践的なノウハウ まとめ

2025.12.10

Power Automate×Word:Wordテンプレートを用いた資料の自動作成(基礎編)

JSON書式保守性アクセシビリティPCFSharePointEF CoreMarker Clusterer中級者DXカスタマイズ委任自動化したクラウド フロー運用開発環境filter query管理システム列StyleDLPポリシー地方自治体MLテンプレート化DX推進Wordテンプレート環境構築ExcelマイグレーションRANK()関数キャンバスアプリノウハウcomponentVBAフローの種類選択肢列環境sortガバナンス登録日StudioTestCopilot Studiot共有リンクサイト複製作り方業務自動化カスタムコンポーネントPower AutomateFramework CoreDynamics 365 SalesDatePicker情報技術ダイアログエラーインスタント クラウド フロー参照列本番環境ソートerror notification更新者AICanvas自治体DXレポート化PnP PowerShellページ承認効率化Power Platform CLIC#Attribute directivesMicrosoft TranslatorDropdownメッセージIDコンポーネントエクセルスケジュール済みクラウド フローChatGPTライセンスmultiple itemエラー通知更新日生成系AITest Studio生成AI自治体APIカスタムスクリプトドキュメント管理資料作成開発手順attributeO/Rマッパーマーカークラスタリングライブラリviewメールdialogerrorレスポンシブ レイアウトOpenAI環境構築手順複数項目削除変更Copilotテスト事例HTTP リクエスト業務効率化IT管理初心者向け拡張機能validationazure sql databasetailwindcssビューfirst()関数Tips復元responsive layoutオープンAIpipelineシェアポイントフォルダ外部DBlicenseテストスタジオ活用ワーケーションファイル保存申請システムハウツービルドローコードCase式マルチテナントアクセス制限nest新機能restoreデータ行の制限チャットGPTCI/CD便利機能ゴミ箱連携添付ファイルコントロール使い方サイトブランド化名古屋エンティティワークフロー自動化画像挿入プロジェクト作成AngularHTTP Requestドロップダウンメニューノーコード入れ子変数Power BI引き継ぎgalleryパイプラインカレンダー完全削除接続ファイルサイズ基本知識フォントカスタマイズ体験記フォルダ構成設定複数レコードPCFギャラリーAccessCSSBreakpointObserver承認動的リスト検索個人列退職ギャラリーDevOpsCalendarモデル駆動型データフローフルリモートワークPowerAutomateブランドセンター感想Microsoft Learn DocsテーマカラーPDF変換業務システムInfoPathxUnitメディアクエリリマインドcollectionMicrosoft 365グループユーザー列所有者を変更スクロールMicrosoft 365Teamsセキュリティロールrecycle binアーティサンX-SP Designテーマ作成チームサイトカスタムコネクタダークモード資料自動作成キャンバスアプリ 違いMatTable.Net Core 3.1スマホSetコレクションセキュリティグループSharePoint Online異動コンテナ簡易在庫管理ローコード開発ビジネスルールアクセス許可Artisanスライドショーデザイン拡張コミュニケーションサイトOpenAPIFormulasプロパティフロー設計Power Apps 導入Angular MaterialVSCodePCForAll複数の添付ファイル送信元リストLoopショートカットキー時間外非エンジニアDataverseSharePoint Framework転職Slide showMicrosoft365サイトの種類MCPサーバーカラーセットテンプレート活用Power Apps 比較データ構造.Net Core Test ExplorerレスポンシブUpdateContext承認フローメールの送信非表示Microsoftshortcut key通知体験談JavaScriptSPFx主キー比較移行要件定義FAQエージェントカラーユニバーサルデザイン自動化事例モデル駆動型 とはモデル駆動型アプリSortByColumns関数Dataverse for TeamsDynamics 365ロードマップform差出人アプリdesignconcat関数ファイル勉強表示サンプルCopilot Studio社内ポータル多言語化サイト構成AIチャットボットアプリデザインNode.jsシステム構築Power AppsTypeScriptitem関数入門技術エクスポートインスタントクラウドフロー[市民開発者JSON文字制限フィルター クエリ内製化切替samplePowerAppsグループウェアMUI権限設計実運用UI/UXVisual Studio CodePower PlatformHTMLGoogle Maps初心者Itインポート自動化したクラウドフロー構築デザインフロー実行ドキュメント ライブラリ市民開発登録者X-SPNFCタグエンゲージメントMultilingualデータ移行
PageTop
ページトップに戻る