0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

スプレッドシートのデータをCSSを使って好きなデザインで印刷(PDF化)する

Posted at

はじめに

スプレッドシートを印刷するのは簡単なんですが、スタイリングはあまり自由じゃありませんよね。
かといえど、複数のデータはやはりスプレッドシートで管理する方が楽。。。。
ということで、今回はGoogleAppsScriptを使って、好きなスタイルのページを印刷できるようにしていこうと思います。

今時印刷?!と言われるかもしれませんが、PDF化としても利用できるので気になった方はぜひご覧くださいませ

記事にするほどのことなのか?

今回はGoogleAppsScriptを使って、WebAppとして表示した画面を印刷できるようにしていこうと思います。
これを聞くだけで、
Ctrl+PorCommand+Pで印刷できるじゃん?」
って方はいるでしょう。。。

ところがどっこい、GoogleAppsScriptのWebAppってiframeを使って公開してるんですよね。

スクリーンショット 2023-07-04 22.20.32.png

この状態で印刷するとこうなってしまうのです。

スクリーンショット 2023-07-04 22.25.16.png

ということで、この制限から逃れつつ、好きなCSSを使いつつ印刷していこうと思います。

トライ

ってことでやっていきましょー

印刷できるようにする

iframeだからできねーぞって書きました。
その通りです。なので、Client側のJavaScriptの機能であるURL.createObjectURLを使って回避します。

説明はMDN様にお任せいたしますが、
メモリを使って一時的なURLを生成することができます。
devToolを使った時に、動画のURLがblob:https://...のような形式をしていることを見ている方がいるといいなーという感じ。

ってことで、順序よくいきましょう。

前提として、WebAppのデプロイ方法は省きます。
わからない方は下記を参照。

  1. URL.createObjectURLを使って、新しいタブで開けるようにする

    code.gs
    function doGet(){
      return HtmlService.createHtmlOutputFromFile("top").setTitle("GASでプリント");
    }
    
    
    top.html
    <!DOCTYPE html>
    <html>
      <head>
        <base target="_top">
        <style>
          .text-center {
            text-align: center;
          }
        </style>
      </head>
      <body>
        <div class="text-center">
          <button id="openPage">印刷ページを表示する</button>
        </div>
        <script>
          document.querySelector("#openPage").addEventListener("click", () => {
            const blob = new Blob(['hello world'],{type:"text/plain"});
            const url = URL.createObjectURL(blob);
            window.open(url);
            URL.revokeObjectURL(url);
          });
        </script>
      </body>
    </html>
    

    code.gsについては、説明いらないですね。
    GETメソッドでアクセスが来たら、HTMLのページを返すおまじないのようなものです
    top.htmlについても、簡単に。
    ボタンをクリックするイベントをJavaScriptで記載しています。
    最初はHTMLとか関係なしに、テキストでhello worldが表示されるようにURLを作成して、新しいタブで開かれます。
    その後、URL.revokeObjectURLでメモリを破棄しています。
    そのため、新しく開かれたタブは正常に開かれますが、再度読み込むとファイルにアクセスできませんでしたと表示されます。

    top.html クリック後 blobページを再度読み込み
    スクリーンショット 2023-07-04 22.47.48.png スクリーンショット 2023-07-04 22.48.50.png スクリーンショット 2023-07-04 22.49.09.png
  2. GASで用意したHTMLが表示されるようにする
    先ほどの例では、普通のテキストで表示させていましたが、CSSで印刷するためにもHTMLで表示できるようにしましょう。
    先ほどのコードの箇所を下記のように変えても良いのですが、自分が編集したいときに見ずらいったらありゃしないですよね。(CSSも使いづらいし)

    - const blob = new Blob(['hello world'],{type:"text/plain"});
    + const blob = new Blob(['<h1>hello world</h1>'],{type:"text/html"});
    

    ということで、テンプレートはGASのHTML上で管理できるようにしましょう。
    新しくプリント用のテンプレートを用意します
    (日本語を使う場合は念のためにmetaタグでutf-8を指定しておきましょう)

    print.html
    <!DOCTYPE html>
    <html>
      <head>
        <base target="_top">
        <meta charset="UTF-8">
      </head>
      <body>
        <h1>プリントアウト用</h1>
      </body>
    </html>
    

    そして、top.htmlでクリックした際にテンプレートを取得して、その内容を表示できるようにしましょう。

    code.gs
    function doGet(){
      return HtmlService.createHtmlOutputFromFile("top").setTitle("GASでプリント");
    }
    
    function getTemplate(){
      return HtmlService.createHtmlOutputFromFile("print").getContent();
    }
    
    top.html
    <!DOCTYPE html>
    <html>
      <head>
        <base target="_top">
        <meta charset="UTF-8">
        <style>
          .text-center {
            text-align: center;
          }
        </style>
      </head>
      <body>
        <div class="text-center">
          <button id="openPage">印刷ページを表示する</button>
        </div>
        <script>
          document.querySelector("#openPage").addEventListener("click", () => {
            google.script.run.withSuccessHandler((templateText) => {
              const blob = new Blob([templateText],{type:"text/html"});
              const url = URL.createObjectURL(blob);
              window.open(url);
              URL.revokeObjectURL(url);
            }).getTemplate();
          });
        </script>
      </body>
    </html>
    

    code.gsには新しくgetTemplate関数を追加して、print.htmlの内容を返す内容にしましょう。ただし、返り値はStringが好ましいので、getContentで返します。
    top.htmlには新しくgoogle.script.runを使って、GASのgetTemplate関数を呼び出します。
    withSuccessHandlerを使って、返り値を引数に、blobを作成します。
    流れとしては同じですが、今回MIME Typeはtext/htmlを指定することで、HTMLページを開くことができます

    完成したら読み込んでみましょう!
    クリックすると数秒のラグが発生します(GASの関数を呼び出しているため)が、
    クリック後は下記のような画面が表示されると思います。

    スクリーンショット 2023-07-04 23.07.10.png

    後ほど説明しますが、印刷用のCSSを当てるとこんな感じで、1枚目、2枚目と表示することができます
    スクリーンショット 2023-07-04 23.09.50.png

    ということで、印刷できるようになりましたね!

スプレッドシートからデータを表示してみよう

次のステップです。CSSを使っても良いのですが、先にスプレッドシートからデータを取り出せるようにしましょう。

どのような印刷物にするか考えていなかった、、、
適当にチケット形式でいきましょう!(笑)
ささっとこんな感じのデータを作ってみました。

スクリーンショット 2023-07-04 23.18.27.png

  1. データを取得するGASコードを書く

    code.gs
    function doGet(){
      return HtmlService.createHtmlOutputFromFile("top").setTitle("GASでプリント");
    }
    
    function getTemplate(){
      return HtmlService.createHtmlOutputFromFile("print").getContent();
    }
    
    function getSheetData(){
      const sheet = SpreadsheetApp.openById("1YtiB4LDCl8jAaXJdbHhG6OKsCWYpUNxSXQ38RZX6zEA").getSheetByName("ticket-data");
      const data = sheet.getRange(`A2:C${sheet.getLastRow()}`).getValues()
        .map(line=>{
          const [ticketId, name, price] = line;
          return {ticketId, name, price}
        });
      return data;
    }
    

    新しくgetSheetData関数を作成します。
    使いやすいように、getValuesで取得したデータをJSON形式で返せるように少しいじります。

  2. データをHTMLに反映させる

    getTemplateの関数の中で取得できるようにしましょう。

    code.gs
    function doGet(){
      return HtmlService.createHtmlOutputFromFile("top").setTitle("GASでプリント");
    }
    
    function getTemplate(){
      const data = getSheetData();
      const html =  HtmlService.createTemplateFromFile("print");
      html.data = data
      return html.evaluate().getContent();
    }
    
    function getSheetData(){
      const sheet = SpreadsheetApp.openById("1YtiB4LDCl8jAaXJdbHhG6OKsCWYpUNxSXQ38RZX6zEA").getSheetByName("ticket-data");
      const data = sheet.getRange(`A2:C${sheet.getLastRow()}`).getValues()
        .map(line=>{
          const [ticketId, name, price] = line;
          return {ticketId, name, price}
        });
      return data;
    }
    

    最初にgetSheetDataを呼び出して、スプレッドシートのデータを取得します。
    今回はHtmlService.createTemplateFromFileを使って、テンプレートで呼び出します。
    HTMLのdataに、シートのデータを挿入し、
    evaluateでデータをHTMLに変換、getContentでテキストにします。

    今度はprint.htmldataを表示させるように変更しましょう
    とりあえず名前だけ表示させます。

    print.html
    <!DOCTYPE html>
    <html>
      <head>
        <base target="_top">
        <meta charset="UTF-8">
      </head>
      <body>
        <h1>プリントアウト用</h1>
        <? for( const {ticketId, name, price} of data){ ?>
        <p><?= name ?></p>
        <? } ?>
      </body>
    </html>
    

    この状態で実行してみましょう。
    ボタンを押した後、新しいタブが開くのが少し遅くなります。
    エラーが出なければ新しいタブが自動で開かれるでしょう。(今回エラーが出た時の対処はしていないので悪しからず)

    この後CSSを適用していくといい感じのページを作成することができます!
    スクリーンショット 2023-07-04 23.50.02.png

CSSを凝ってみる

ってことで、やることはほぼ終わりました。
CSSを凝ろう!的な内容を書いていますが、そんな技術ありませんのでデザインはネットから拾ってきます

普段はTailwindCSSやCSS in JSを書いてる人間なので、CSSを当てはめるにあたってのClassの命名規則がやばいのは目を瞑っていただけたらと。。。
デザイン力も皆無のためこんな見た目ですが( ̄▽ ̄;)
スクリーンショット 2023-07-05 0.46.47.png

print.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <meta charset="UTF-8">
    <style>
      @media print {
        body {
          -webkit-print-color-adjust: exact;
          color-adjust: exact;
        }

        .page {
          height: 100vh;
          page-break-after: always;
          display: flex;
          justify-content: center;
          align-items: center;
        }
      }

      @media screen {
        .page {
          padding: 30px 0px;
        }
      }

      .ticket {
        width: 400px;
        height: 100px;
        margin: auto;
        border: 2px solid black;
        border-radius: 10px;
        display: flex;
        align-content: center;
      }

      .ticket__left {
        display: flex;
        align-items: center;
        justify-content: center;
        border-radius: 10px 0px 0px 10px;
        padding: 10px 20px;
        background-color: rgba(93, 184, 93, 0.5);
        border-right: 2px black dashed;
      }

      .ticket__right {
        display: flex;
        flex-grow: 1;
        flex-direction: column;
        justify-content: center;
        padding: 10px;
      }

      .ticket__event {
        font-size: large;
        text-decoration: solid underline;
      }

      .ticket__name {
        display: flex;
        align-items: center;
      }

      .ticket__ticketId {
        display: flex;
        align-items: end;
        justify-content: end;
        font-size: small;
        color: gray;
      }
    </style>
  </head>
  <body>
    <? for( const {ticketId, name, price} of data){ ?>
    <div class="page">
      <div class="ticket">
        <div class="ticket__left">
          <span class="ticket__price"><?= price ?></span>
        </div>
        <div class="ticket__right">
          <div class="ticket__top">
            <span class="ticket__event">Event Name</span>
          </div>
          <div class="ticket__bottom">
            <span class="ticket__name"><?= name ?></span>
            <span class="ticket__ticketId"><?= ticketId ?></span>
          </div>
        </div>
      </div>
    </div>
    <? } ?>
  </body>
</html>

印刷するとこんな感じ
スクリーンショット 2023-07-05 0.47.56.png

ちなみになぜか2枚目が空白なので、5枚のチケットに対して6枚印刷できるようになっていました。。。
なんで笑

今回重要じゃないんで放置!

最終形のコード

code.gs
code.gs
function doGet(){
  return HtmlService.createHtmlOutputFromFile("top").setTitle("GASでプリント");
}

function getTemplate(){
  const data = getSheetData();
  const html =  HtmlService.createTemplateFromFile("print");
  html.data = data
  return html.evaluate().getContent();
}

function getSheetData(){
  const sheet = SpreadsheetApp.openById("1YtiB4LDCl8jAaXJdbHhG6OKsCWYpUNxSXQ38RZX6zEA").getSheetByName("ticket-data");
  const data = sheet.getRange(`A2:C${sheet.getLastRow()}`).getValues()
    .map(line=>{
      const [ticketId, name, price] = line;
      return {ticketId, name, price}
    });
  return data;
}
top.html
top.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <meta charset="UTF-8">
    <style>
      .text-center {
        text-align: center;
      }
    </style>
  </head>
  <body>
    <div class="text-center">
      <button id="openPage">印刷ページを表示する</button>
    </div>
    <script>
      document.querySelector("#openPage").addEventListener("click", () => {
        google.script.run.withSuccessHandler((templateText) => {
          const blob = new Blob([templateText],{type:"text/html"});
          const url = URL.createObjectURL(blob);
          window.open(url);
          URL.revokeObjectURL(url);
        }).getTemplate();
      });
    </script>
  </body>
</html>

print.html
print.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <meta charset="UTF-8">
    <style>
      @media print {
        body {
          -webkit-print-color-adjust: exact;
          color-adjust: exact;
        }

        .page {
          height: 100vh;
          page-break-after: always;
          display: flex;
          justify-content: center;
          align-items: center;
        }
      }

      @media screen {
        .page {
          padding: 30px 0px;
        }
      }

      .ticket {
        width: 400px;
        height: 100px;
        margin: auto;
        border: 2px solid black;
        border-radius: 10px;
        display: flex;
        align-content: center;
      }

      .ticket__left {
        display: flex;
        align-items: center;
        justify-content: center;
        border-radius: 10px 0px 0px 10px;
        padding: 10px 20px;
        background-color: rgba(93, 184, 93, 0.5);
        border-right: 2px black dashed;
      }

      .ticket__right {
        display: flex;
        flex-grow: 1;
        flex-direction: column;
        justify-content: center;
        padding: 10px;
      }

      .ticket__event {
        font-size: large;
        text-decoration: solid underline;
      }

      .ticket__name {
        display: flex;
        align-items: center;
      }

      .ticket__ticketId {
        display: flex;
        align-items: end;
        justify-content: end;
        font-size: small;
        color: gray;
      }
    </style>
  </head>
  <body>
    <? for( const {ticketId, name, price} of data){ ?>
    <div class="page">
      <div class="ticket">
        <div class="ticket__left">
          <span class="ticket__price"><?= price ?></span>
        </div>
        <div class="ticket__right">
          <div class="ticket__top">
            <span class="ticket__event">Event Name</span>
          </div>
          <div class="ticket__bottom">
            <span class="ticket__name"><?= name ?></span>
            <span class="ticket__ticketId"><?= ticketId ?></span>
          </div>
        </div>
      </div>
    </div>
    <? } ?>
  </body>
</html>

さいごに

ということでいかがでしたでしょうか。
スプレッドシートでも印刷はできるのですが、好きなスタイルが当てられないということで、少しずる賢い方法で実装してみました。

あまり使い所はないかもしれませんが、よきGASライフを~(*>∀<)ノ))

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?