Amazonで商品を見るセール会場へ

Googleスプレッドシート│ドライブ内の画像をサムネ付きギャラリー表示する方法

Googleドライブ内の画像を一覧表示するギャラリーを作成しました。

条件を指定した絞り込み検索にも対応しています。

本構成のメリットは以下の通りです。

  • Googleの標準サービスを利用するため追加コストがかからない
  • アクセス権を持つGoogleユーザーのみ閲覧可能でセキュア
  • GASで開発するためカスタマイズ性が高く&動的にギャラリーを生成できる
  • スプレッドシート上でデータを編集・管理しやすい
  • Googleドライブ上の画像データを直接利用できる&ストレージ容量も確保しやすい
Googleドライブ上の画像データを活用して作成したサムネイル付きギャラリーの画面イメージ。
目次

ギャラリー表示機能の概要

作成したギャラリーの基本機能を紹介します。

各要素の連携と画面上の動作イメージを確認できます。

1. Googleドライブの設定

専用フォルダを作成し、ギャラリーに表示したい画像ファイルを格納しておきます。

ギャラリー表示に使用する画像ファイルを格納したGoogleドライブの専用フォルダ。

2. スプレッドシートへのデータ入力

スプレッドシートをデータベースとして利用します。

IDや名称に加え、絞り込みに使用するタグや種別などの情報を入力します。

IDや名称、絞り込み用タグなどのメタデータを入力したGoogleスプレッドシートの画面。

※画像のパス取得には、別途用意した専用機能を利用します。

スプレッドシート側で画像のパス情報を取得・管理するための設定画面。

3. Webアプリ上でのギャラリー表示

GASで構築したWebアプリがスプレッドシートのデータを読み込み、ギャラリー形式で画面に描画します。

スプレッドシートのデータを読み込んで描画されたWebアプリのギャラリー表示画面。

画像カードにカーソルを合わせると詳細情報が表示されます。

ギャラリー上の画像カードにカーソルを合わせて詳細情報が表示された様子。

絞り込み条件を指定して検索ボタンを押すと、該当する画像のみに絞り込まれます。

検索条件を指定して絞り込みを実行したあとのギャラリー表示結果。

サムネイル付きギャラリーの実装手順

スプレッドシートのメニュー「拡張機能」から「Apps Script」を選択し、スクリプトエディタを起動します。

プロジェクトは以下の5つのファイルで構成します。

拡張機能から起動したApps Scriptのエディタと構成するファイル一覧。
main.gsバックエンドの処理実装
index.html画面の基本HTML構造
css.htmlレイアウトやホバー演出のスタイル
js.html表示制御や絞り込み検索のスクリプト
plugin.html外部ライブラリの読み込み設定

以下は実装の詳細なコードと解説です。

※本コードはベースサンプルです。

要件に応じてカスタマイズしてご活用ください。

Googleスプレッドシートの関連テクニックは、まとめ記事でも紹介しています。

5つのファイルにコードを記述したあと、ウェブアプリとしてデプロイして動作を確認する手順で進めます。

plugin.htmlの実装

実装を効率化するため、外部ライブラリをCDN経由で読み込みます。

<!-- jQuery full -->
<script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>

<!-- iziModal -->
<link  rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/izimodal/1.6.0/css/iziModal.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/izimodal/1.6.0/js/iziModal.min.js"></script>

<!-- Underscore / lodash -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.13.6/underscore-min.js"></script>

<!-- Bootstrap & select -->
<link  rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
<link  rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-select@1.13.10/dist/css/bootstrap-select.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.13.10/js/bootstrap-select.min.js"></script>

<!-- twbs-pagination -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/twbs-pagination/1.4.2/jquery.twbsPagination.min.js"></script>

css.htmlの実装

デザインとスタイルの設定です。

画像ホバー時にメタ情報を表示するスタイルなどを指定しています。

<style>
/* =====================  共通リセット  ===================== */
* { margin: 0; padding: 0; box-sizing: border-box; }

/* =====================  操作エリア  ===================== */
#header { display: flex; flex-wrap: wrap; gap: .5rem; margin-bottom: 1.5rem; }
.selectpicker { width: 200px; }

/* ===== 画像グリッド ===== */
#btn-search {
    background-color: #333;
    border-color: #333;
}
.btn.btn-info {
    background-color: #28a745;
    border-color: #28a745;
}

/* ===== 画像グリッド ===== */
#grid{
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: 20px;
  list-style: none;
  padding: 0;
}

/* 画面幅に応じて列数を自動調整 */
@media (max-width: 1280px){ #grid{ grid-template-columns: repeat(4, minmax(0,1fr)); } }
@media (max-width: 992px) { #grid{ grid-template-columns: repeat(3, minmax(0,1fr)); } }
@media (max-width: 768px) { #grid{ grid-template-columns: repeat(2, minmax(0,1fr)); } }

/* ===== カード ===== */
#grid .list_item{ position: relative; }

#grid img.card{
  width: 100%;
  height: auto;
  object-fit: contain;
  transition: filter .5s;
}

/* ===== ホバー時のエフェクト ===== */
#grid .list_item:hover img.card{ filter: brightness(50%); }

#grid .list_item:hover::after {
  content: attr(data-alt) '\A' attr(data-name) '\A ID : ' attr(data-id);
  white-space: pre-wrap;
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  text-align: center;
  width: 90%;
  color: #fff;
  pointer-events: none;
}

/* =====================  モーダル画像  ===================== */
.iziModal{
  position: fixed !important;          /* 念のため固定 */
  left: 50% !important;
  top : 50% !important;
  transform: translate(-50%,-50%) !important;
  margin: 0 !important;
  max-width : 90vw !important;         /* 画面幅 90% まで拡大可 */
  width     : auto   !important;       /* 幅は内容に合わせて伸縮 */
}
.iziModal-content {
  padding: 1em!important;
}
.modal_card {
    width: 100%;
}
</style>

main.gsの実装

スプレッドシートやドライブとの連携を処理するサーバーサイドコードです。

コード内の定数定義に以下の設定値を指定します。

  • 対象スプレッドシートのID
  • 画像を格納しているGoogleドライブフォルダのID
  • 画像パスを出力するシート名
  • メタデータを管理しているシート名

各項目をお手元の環境に合わせて設定してください。

シート上から各機能を呼び出せるよう、カスタムメニューも追加しています。

各種機能の呼び出し用カスタムメニューが追加されたスプレッドシートの画面。

※公開したWebアプリのURLは、管理用シートの所定セルに転記して管理します。

公開したWebアプリのURLを管理用シートの所定セルに転記して管理している様子。
/* =====================================================================
 * 定数定義
 * ===================================================================*/
const SS_ID           = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
const FOLDER_ID       = 'xxxxxxxxxxxxxxxxxxxxxxxxx';
const SHEET_IMG_PATH  = '画像パスの取得';
const SHEET_DATA_MAIN = '管理シート';

/* =====================================================================
 * 画像ファイル一覧をスプレッドシートへ書き込む
 * ===================================================================*/
function exportDriveImages() {
  const rootFolder = DriveApp.getFolderById(FOLDER_ID);
  const list = [];

  getFilesRecursively(rootFolder, rootFolder.getName(), list);

  if (!list.length) return;

  const sheet = getSheet(SHEET_IMG_PATH);
  sheet.clearContents();
  sheet.getRange(1, 1, 1, 3).setValues([['ファイル名', '画像URL', 'フォルダパス']]);
  sheet.getRange(2, 1, list.length, 3).setValues(list);
}

function getFilesRecursively(folder, path, list) {
  const files = folder.getFiles();
  while (files.hasNext()) {
    const file = files.next();
    const id = file.getId();
    const thumbUrl = `https://drive.google.com/thumbnail?id=${id}`;
    list.push([file.getName(), thumbUrl, path]);
  }

  const folders = folder.getFolders();
  while (folders.hasNext()) {
    const sub = folders.next();
    const subPath = `${path}/${sub.getName()}`;
    getFilesRecursively(sub, subPath, list);
  }
}

/* =====================================================================
 * Web アプリ公開
 * ===================================================================*/
function doGet () {
  return HtmlService
    .createTemplateFromFile('index')
    .evaluate();
}

/* 新しいタブで Web アプリを開く(メニュー用)*/
function openViewer () {
  const url = SpreadsheetApp.getActiveSpreadsheet()
               .getSheetByName('ギャラリー')
               .getRange('A1')
               .getValue();

  const safeUrl = JSON.stringify(url);
  const script = `
    <script>
      window.open(${safeUrl}, '_blank').focus();
      google.script.host.close();
    </script>`;

  SpreadsheetApp.getUi()
    .showModalDialog(HtmlService.createHtmlOutput(script), 'CardIllustViewer');
}

/* =====================================================================
 * カスタムメニュー
 * ===================================================================*/
function onOpen () {
  SpreadsheetApp.getUi().createMenu('カスタムメニュー')
    .addItem('ギャラリーを表示',      'openViewer')
    .addItem('画像パスの取得', 'exportDriveImages')
    .addToUi();
}

/* =====================================================================
 * データ取得 API(HTML から呼び出し)
 * ===================================================================*/
function getSSValues () {
  return JSON.stringify(readSheetObjects(SHEET_DATA_MAIN));
}

/* =====================================================================
 * 共通ユーティリティ
 * ===================================================================*/
/** シート取得(キャッシュ付き)*/
function getSheet (name) {
  const ss = SpreadsheetApp.openById(SS_ID);
  return ss.getSheetByName(name);
}

/** ヘッダー行をキーに行データをオブジェクト配列化 */
function readSheetObjects (sheetName) {
  const sheet = getSheet(sheetName);
  const [header, ...rows] = sheet.getDataRange().getDisplayValues();
  return rows.map(row =>
    header.reduce((obj, key, idx) => {
      obj[String(key)] = String(row[idx]);
      return obj;
    }, {}));
}

/** HTML 部品の include 用 */
function include (file) {
  return HtmlService.createHtmlOutputFromFile(file).getContent();
}

index.htmlの実装

ギャラリー画面を描画するメインのHTMLファイルです。

コード内のシートURL指定箇所は、ご自身の環境に合わせて書き換えてください。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <title>画像一覧</title>

    <!-- CSS & プラグイン読込  -->
    <?!= include('plugin') ?>
    <?!= include('css') ?>
  </head>
  <body>
  <!-- ===== フィルター・操作エリア ===== -->
  <header id="header" class="d-flex flex-wrap gap-2 mb-3">
    <select id="filter"        class="selectpicker" multiple title="フィルター"
            data-actions-box="true" data-selected-text-format="count > 3" size="4"></select>

    <select id="select_chara"  class="selectpicker" multiple title="アイテム名"
            data-actions-box="true" data-selected-text-format="count > 3"></select>

    <select id="select_tag"    class="selectpicker" multiple title="タグ"
            data-live-search="true" data-actions-box="true"
            data-live-search-placeholder="Search" data-selected-text-format="count > 3"></select>

    <button id="btn-search" class="btn btn-success">検索</button>
    <button class="btn btn-info" onclick="window.open('https://docs.google.com/spreadsheets/d/xxxxxxxxx/edit?gid=0#gid=0')">シート</button>
  </header>
  <!-- ================= ページャ ================= -->
  <nav id="pager">
    <ul id="pagenation"></ul>
  </nav>
    <!-- ================= 画像グリッド ================= -->
   <main>
    <ul>
      <div id="grid"></div>
    </ul>
  </main>
  
    <!-- ================= 画像モーダル ================= -->
    <div class="iziModal"></div>

    <!-- ================= JavaScript ================= -->
    <?!= include('js') ?>
</body>
</html>

js.htmlの実装

フロントエンド側の表示制御を行うJavaScriptコードです。

ホバー時に表示するデータ項目など、描画ロジックは必要に応じて変更してください。

<script>
(() => {
  /* ------------------------------------------------------------------
   * 1. 定数・キャッシュ
   * ------------------------------------------------------------------*/
  const PER_PAGE      = 18;

  // jQuery オブジェクトのキャッシュ
  const $grid   = $('#grid');           // 画像グリッド
  const $pager  = $('#pagenation');     // ページャ
  const $filter = $('#filter');
  const $chara  = $('#select_chara');
  const $tags   = $('#select_tag');
  const $modal  = $('.iziModal');

  /* ------------------------------------------------------------------
   * 2. 状態変数
   * ------------------------------------------------------------------*/
  let allData   = [];   // スプレッドシート全件
  let listData  = [];   // 画像あり・有効フラグ付きデータ
  let filterKeys = ["rare", "type"];
  let chunks    = [];   // ページング用
  let selFilter = [], selChara = [], selTags = [];

  /* ------------------------------------------------------------------
   * 3. 初期化
   * ------------------------------------------------------------------*/
  $(async () => {
    $('#btn-search').on('click', onSearchClick);

    initModal();
    await loadSheetData();
    buildUI();
  });

  /* ------------------------------------------------------------------
   * 4. モーダル初期化
   * ------------------------------------------------------------------*/
  function initModal () {
    const $modal = $('.iziModal');
    const $grid  = $('#grid');

    $modal.iziModal({
      maxWidth : 0,        // 0 = 制限なし(プラグイン推奨値)
      maxHeight: '90%',    // 高さは 90% に制限
      width    : null,     // 自動
      transitionIn: 'fadeIn',
      onClosed   : () => $modal.iziModal('setContent', '')
    });

    $grid.on('click', 'img.card', function () {
      const full = $(this).data('full');
      const alt  = this.alt;
      const $modal = $('.iziModal');

      const img = new Image();
      img.onload = function () {
        const html = `<img src="${full}" alt="${alt}" class="modal_card">`;
        $modal.iziModal('setContent', html);
        $modal.iziModal('open');
      };
      img.src = full;
    });

  }

  /* ------------------------------------------------------------------
   * 5. データ取得
   * ------------------------------------------------------------------*/
    async function loadSheetData () {
      const json = await new Promise(resolve => {
        google.script.run
          .withSuccessHandler(resolve)
          .withFailureHandler(() => alert('データ取得失敗'))
          .getSSValues();
      });

      allData = JSON.parse(json);

      listData = allData.filter(d =>
        getThumb(d) && d.item_name
      );
    }

  /* ------------------------------------------------------------------
   * 6. UI 構築
   * ------------------------------------------------------------------*/
  async function buildUI () {
    /* プルダウン ---------------- */
    const charaSet = uniqCompact(propArray(allData, 'item_name'));
    populateSelect($chara, charaSet);

    /* タグプルダウン ------------------ */
    const tagSet = uniqCompact(
      flatMap(listData, d => splitCSV(d.tag))
    );
    populateSelect($tags, tagSet);

    /* 絞り込みキー -------------------- */
    refreshFilterSelect();

    /* 一覧 & ページング --------------- */
    listData = _.sortBy(listData, d => -d.id);
    renderPage(listData);
  }

  /* ------------------------------------------------------------------
   * 7. 検索処理
   * ------------------------------------------------------------------*/
  function onSearchClick () {
    selFilter = $filter.val() || [];
    selChara  = $chara.val()  || [];
    selTags   = $tags.val()   || [];

    const filtered = listData
      .filter(d => !selChara.length || selChara.includes(d.item_name))
      .filter(d => filterRow(d, selFilter))
      .filter(d => tagMatch(d.tag, selTags));

    renderPage(filtered);
  }

  function filterRow (row, selected) {
    if (!selected.length) return true;

    return filterKeys.every(key => {
      const keyOpts = getColumnValues(key);
      const hit     = _.intersection(selected, keyOpts);
      if (!hit.length) return true;

      const rowVals = splitCSV(row[key]);
      return !rowVals.length || rowVals.some(v => hit.includes(v));
    });
  }

  function tagMatch (tagStr, selected) {
    if (!selected.length) return true;
    const tagArr = splitCSV(tagStr);
    return tagArr.some(t => selected.includes(t));
  }

  /* ------------------------------------------------------------------
   * 8. 描画関連
   * ------------------------------------------------------------------*/
  function renderPage (data) {
    const firstPage = setupPagination(data);
    renderGrid(firstPage);
  }

  function renderGrid(data) {
    $grid.empty();

    data.forEach(d => {
      const $li = $('<li>', {
        class: 'list_item',
        name : d.file_name,
        alt  : d.item_name,
        'data-id': d.id,
        'data-name': d.file_name,
        'data-alt': d.item_name
      });

      const thumb = getThumb(d); 
      const full  = getFull(d);

      if (!thumb) return;

      $('<img>', {
        class : 'card',
        src   : thumb,
        'data-full': full,
        alt   : d.file_name,
        loading : 'lazy'
      }).appendTo($li);

      $grid.append($li);
    });
  }

  function setupPagination (data) {
    if ($pager.data('twbs-pagination')) $pager.twbsPagination('destroy');

    if (data.length <= PER_PAGE) {
      $pager.hide();
      return data;
    }

    chunks = _.chunk(data, PER_PAGE);
    $pager.twbsPagination({
      totalPages  : chunks.length,
      visiblePages: 5,
      onPageClick : (_, page) => renderGrid(chunks[page-1])
    });
    $pager.show();
    return chunks[0];
  }

  /* ------------------------------------------------------------------
   * 9. フィルターセレクト生成
   * ------------------------------------------------------------------*/
  async function getFilterHeader () {
    const json = await new Promise(resolve => {
      google.script.run
        .withSuccessHandler(resolve)
        .withFailureHandler(() => alert('フィルターキー取得失敗'))
        .getFilterKeys();
    });
    return JSON.parse(json);
  }

  function refreshFilterSelect () {
    $filter.empty();
    filterKeys.forEach(key => {
      const opts = getColumnValues(key);
      const $group = $('<optgroup>', { label: key });
      opts.forEach(v => $('<option>', { value:v, text:v }).appendTo($group));
      $group.appendTo($filter);
    });
    $filter.selectpicker('refresh');
  }

  /* ------------------------------------------------------------------
   * 10. ユーティリティ
   * ------------------------------------------------------------------*/
  const flatMap     = (arr, fn) => [].concat(...arr.map(fn));   // lodash 代替
  const uniqCompact = arr => _.uniq(arr.filter(Boolean));       // 空文字除外+重複排除
  const propArray   = (arr, key) => arr.map(o => o[key]);       // プロパティ抽出
  const toBool = v => String(v).toLowerCase() === 'true';
  const splitCSV = str => (str ? str.split(',') : []);
  const getThumb = d => d.image_path ? `${d.image_path}&sz=w400` : '';
  const getFull  = d => d.image_path ? `${d.image_path}&sz=w1920` : '';

  function populateSelect ($sel, items) {
    $sel.empty();
    items.forEach(v => $('<option>', { value:v, text:v }).appendTo($sel));
    $sel.selectpicker('refresh');
  }

  function getColumnValues (key) {
    return uniqCompact(
      flatMap(listData, d => splitCSV(d[key]))
    );
  }

})();
</script>

動作確認とデプロイ

すべてのコードを保存後、「新しいデプロイ」からウェブアプリとして公開し、URLにアクセスして表示を確認します。

画像一覧の表示だけでなく、モーダル表示、ホバー時の情報表示、絞り込み検索が正常に動作するか確認してください。

Webアプリとしてデプロイし、画像一覧や絞り込み検索の動作を確認しているブラウザ画面。

以上で、Googleスプレッドシートとドライブを連携した画像ギャラリーの構築は完了です。

コードとギャラリーのプレビューを並べて作業する

GASのエディタとWebアプリの動作確認画面を同時に表示しづらい場合は、ブラウザの分割表示やサブモニターの導入が便利です。

運営メディア「デザノマ」のコードとプレビューを並べるモニターの選び方に関する記事も参考にしてみてください。

この記事で紹介しているコードは、GitHubのMOTOKI-LLC/cg-method-codeにもまとめています。

GASでウェブアプリケーションを作る記事は、ほかにもあります。

次に学ぶ・作業環境を選ぶ

学習を続けたい方や、作業環境を整えたい方は、目的に合うガイドをご覧ください。

スプレッドシート・GASのおすすめ書籍

作業環境の作り方

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次