はじめに
スプレッドシートを印刷するのは簡単なんですが、スタイリングはあまり自由じゃありませんよね。
かといえど、複数のデータはやはりスプレッドシートで管理する方が楽。。。。
ということで、今回はGoogleAppsScriptを使って、好きなスタイルのページを印刷できるようにしていこうと思います。
今時印刷?!と言われるかもしれませんが、PDF化としても利用できるので気になった方はぜひご覧くださいませ
記事にするほどのことなのか?
今回はGoogleAppsScriptを使って、WebAppとして表示した画面を印刷できるようにしていこうと思います。
これを聞くだけで、
「Ctrl+P
orCommand+P
で印刷できるじゃん?」
って方はいるでしょう。。。
ところがどっこい、GoogleAppsScriptのWebAppってiframeを使って公開してるんですよね。
この状態で印刷するとこうなってしまうのです。
ということで、この制限から逃れつつ、好きなCSSを使いつつ印刷していこうと思います。
トライ
ってことでやっていきましょー
印刷できるようにする
iframeだからできねーぞって書きました。
その通りです。なので、Client側のJavaScriptの機能であるURL.createObjectURL
を使って回避します。
説明はMDN様にお任せいたしますが、
メモリを使って一時的なURLを生成することができます。
devToolを使った時に、動画のURLがblob:https://...
のような形式をしていることを見ている方がいるといいなーという感じ。
ってことで、順序よくいきましょう。
前提として、WebAppのデプロイ方法は省きます。
わからない方は下記を参照。
-
URL.createObjectURL
を使って、新しいタブで開けるようにするcode.gsfunction 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ページを再度読み込み -
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.gsfunction 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の関数を呼び出しているため)が、
クリック後は下記のような画面が表示されると思います。後ほど説明しますが、印刷用のCSSを当てるとこんな感じで、1枚目、2枚目と表示することができます
ということで、印刷できるようになりましたね!
スプレッドシートからデータを表示してみよう
次のステップです。CSSを使っても良いのですが、先にスプレッドシートからデータを取り出せるようにしましょう。
どのような印刷物にするか考えていなかった、、、
適当にチケット形式でいきましょう!(笑)
ささっとこんな感じのデータを作ってみました。
-
データを取得するGASコードを書く
code.gsfunction 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形式で返せるように少しいじります。 -
データをHTMLに反映させる
getTemplate
の関数の中で取得できるようにしましょう。code.gsfunction 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.html
でdata
を表示させるように変更しましょう
とりあえず名前だけ表示させます。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を凝ってみる
ってことで、やることはほぼ終わりました。
CSSを凝ろう!的な内容を書いていますが、そんな技術ありませんのでデザインはネットから拾ってきます
普段はTailwindCSSやCSS in JSを書いてる人間なので、CSSを当てはめるにあたってのClassの命名規則がやばいのは目を瞑っていただけたらと。。。
デザイン力も皆無のためこんな見た目ですが( ̄▽ ̄;)
<!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>
ちなみになぜか2枚目が空白なので、5枚のチケットに対して6枚印刷できるようになっていました。。。
なんで笑
今回重要じゃないんで放置!
最終形のコード
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
<!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
<!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ライフを~(*>∀<)ノ))