LoginSignup
64
47

More than 3 years have passed since last update.

【誰得】普段目にする、レシート印刷の実装方法

Last updated at Posted at 2019-06-21

なぜこの記事?

自分は幸運にも、一人で伝票システムアプリを作る機会が得られました!
(それについての記事はこちら:
八丈島のホテルで、運用費用0円の伝票システムアプリ作って、業務改善した話。

さらにその中で普段(少なくとも個人開発)では学ぶことがないであろうレシートを作る実装を学べました。

その時はGoogleで調べてももちろん参考にする記事など無く(あるのかもだけど)、プリンターのドキュメントを頼りに進めていきました。

なのでこの記事でまとめておきます。
今後レシート印刷を実装する人にとって、この記事が開発の高速道路を進む手助けになれば幸いです。

また、この部分を知ることで普段受け取るレシートに目が行くようになり、日常の見え方が変わったので知識として知っておくのも良いかもしれません:nerd:

(※自分の体験を基にしたので、正解としてではなく、参考程度に読んでください!)

実装の流れ

  • (プリンターを選ぶ)
  • (レイアウトの構想)
  • レシートデータ作成関数をつくる
    • レイアウトを整える
    • builderにバイトデータを格納
    • プリンターのportを開ける
    • 初期化

プリンターを選ぶ

この段階では、自分の目的を明確にする必要があります。
具体的には、

  • 何を (レシートか、写真か、クーポンか..)
  • どうやって(通信規格:Bluetooth かイーサネットか..)
  • どのように (欲しい機能や性能)
  • プラットフォーム (webかandroidかiOSか..)

です。
そして必要な機能を絞ったら後は値段とパフォーマンスとの天秤にかけます。

自分の場合は、

  • 何を --- 会計レシートを
  • どのように --- 速く
  • どうやって --- Bluetoothで
  • プラットフォーム --- Android上で でした!

これにプラスで、自分はドキュメントやサンプルが豊かかどうかも考慮に入れました。

その結果、プリンターはStarPrntのTSP650iiを購入しました。
製作しているのは、スター精密という自動車や時計の精密機器を扱っている日本の会社で、プリンター界(?)でも老舗だと思います!信頼性抜群!

image.png:公式サイトより

決め手は以下👇のような点です!

  • iOSモードと標準モード(Android/Windows等)に対応
  • Bluetoothインターフェースモデル
  • スター最速印字が可能(最大300㎜/s)
  • StarIO(通信サポートソフト)を利用すれば、iOS、Androidなど多様なOS上でのアプリケーション開発における工数大幅削減が可能
  • SDKが多機能で使いやすそう。ドキュメントも丁寧

個人でなにかプリンター接続で遊びたい、などの目的では安いモデル(3~5000円程)もありましたが、大抵はWindowsのみ対応だったり、専用のモバイルアプリのみ対応だったりします。

自分でプリンターを組み込んでアプリを作る場合はお金が許すのならスター精密が一番だと思います!(設定・接続もわや楽)

また、プリンターの種類は勿論、インクを使用する機器ではなく、熱を使うサーマルプリンターの方が圧倒的に楽で良いと思います。

ここからは、スター精密のSDKを使っている前提で話していきます。
👇
👇

レイアウトの構想

次にレイアウトの構想ですが、最初の段階で自由にレイアウトを描いてはいけないのがコツです。
では最初になにをやるというと、

  1. 横1行に入る最大全角文字数を測る。
  2. レシートに記載する必要情報をまとめる。

です!(②の説明は省略)
①でなぜ全角なのかというと、これまで自分は知らなかったのですが、
半角は全角の半分ではないのです!

詳しく言うとディスプレイに表示される文字には大きく2種類、
等幅フォントと、プロポーショナルフォントがあります。

等幅フォントは原稿用紙のマス目みたいに横幅が決まっており、半角文字は全角文字の半分の幅になっています。

対して、プロポーショナルフォントは、文字一つ一つが綺麗にみえるために文字の大きさによって幅が変わってきます。

2019-06-18.png
: 参考【「全角」と「半角」、「等幅」と「プロポーショナル」】

印刷物などではプロポーショナルフォントが採用されていることが多く、TSP650も例外ではありません。

なので、固定されているレイアウトは良いのですが、注文名などの文字数が可変的な情報は全ての文字を全角 or 半角 で統一する必要があります。
(レイアウトを整えるという点で)

👇例えば、紙の横1行が全角で24文字入り、最後に何文字か残したい場合は、こうします。(自分の例では数量を入れたかったので)

  //注文名をすべて全角に
  //1行に収まるように調節
       var order = Transliterator.getInstance("Halfwidth-Fullwidth").transliterate(orderList[x])
       if (order.count() >= 18){
            order = order.substring(0, 18)
        }

        val size = order.count()
        val rest = 22 - size
        data = (order + " ".repeat(rest - 3) + qList[x] + "\n")    //行数合わせのコード

(変数orderには注文名が入っている。)

半角→全角(全角→半角)の変換はライブラリ「ICU」を使えば1行で変換できます!
(参考: Javaにおける文字列の全角⇔半角変換について)

ここまで来たら紙でも何でも使って、自由にデザインを決められます。
最初から紙を使うのは、自由度が高すぎるので、出来ること必要なことを蔑ろにして、楽しくお絵かきをしてしまう危険性があります(笑)

自分はここの段階では、コードを変えて色々レイアウトを試しました。
そしてユーザー(自分の場合、ホテルスタッフや支配人)からのフィードバックを得て、最適なレイアウトにどんどん近づいていきました!

👇最初はこんな感じでひどかったのですが...
ooooi.png

👇最終的にはこのレイアウトに行きつきました。
iei.png

文字の大きさは、縦(横)だけn倍縦横n倍を使って分かりやすく、また文字の太さ中央寄せも利用して見やすいレイアウトを心がけました!

自分はシンプル&見やすさを一番に考えたのですが、スター精密のプリンターは本当に多機能で、ロゴを入れたり、QRコードやバーコードももちろんつけれたりで、自作でもお洒落なレシートも簡単に作れそうです。

レシートデータ作成関数をつくる

ここからが主にコードの問題になります。
自分は手をつける前ここが鬼門だと思っていました...。
しかしいざドキュメントを見ると、、、

2019-06-21.png

引用: 公式ドキュメントより

(;''∀'')めっちゃ簡単そう!!!
実際簡単でした!

自分で実装しないといけない箇所は
レイアウトをコード内に記して、
byte型に変換してbuilderにデータを入れる
の2つです。
(上記の画像では変数commandがbuilderになっています)

レイアウトを整えるコード

まず上に貼った、自分が最終的に作ったレシートレイアウトを作るコードを貼っておきます。
(文字数が可変的なものは、変数に格納されており、すでに全角&適切な文字数になっている)
実際のレシートと見比べるとわかりやすいと思います!

レシートレイアウト
 //レシート情報の作成
   val time = getToday()

   val content0 =
            ("レストラン タルタルーガ\n").toByteArray(charset("SJIS"))
   val content1 =
            ("--$time--\n\n").toByteArray(charset("SJIS"))
   val content2 =
            ("お会計はチェックアウトの際にお願いします。" + "\n" + "\n" + "\n").toByteArray(charset("SJIS"))
   val content3 =
            ( "テーブル番号   $tableNum\n" +
                                "お部屋番号      $room\n").toByteArray(charset("SJIS"))
   val content4 =
         ("\n【$tablet】\n注文内容    (@単価)         数量\n"
                                + "------------------------------------------------\n").toByteArray(charset("SJIS"))

//5.6は下でmake_order()を呼び出して作っている。

    val content8 =
          ("      合計    \\ $total\n").toByteArray(charset("SJIS"))
    val content9 =
          ("  --サインはこちらへ--\n" + "\n" + "\n" + "\n" + "\n").toByteArray(charset("SJIS"))

(👆空白はスペース、改行は "\n" で表されています。あとなぜかSJISに一回しないと通りませんでした。)

最初のgetToday()はおなじみのこのコードです👇

   fun getToday(): String {
        val date = Date()
        val format = SimpleDateFormat("yyyy/MM/dd/ HH:mm", Locale.getDefault())
        return format.format(date)
    }

builderにバイトデータを格納

あとは、builder()にデータを入れていきます

   val builder = StarIoExt.createCommandBuilder(StarIoExt.Emulation.StarLine)
   builder.beginDocument()
   builder.appendEmphasis(true)

     //write content0

   builder.appendAlignment(ICommandBuilder.AlignmentPosition.Center)
   builder.appendMultipleHeight(content0, 2)

                //1
   builder.appendEmphasis(false)

   builder.appendAlignment(ICommandBuilder.AlignmentPosition.Center)      
   builder.append(content1)

                //2
   builder.appendEmphasis(true)
   builder.appendMultipleHeight(content2, 2)

                //3

   builder.appendAlignment(ICommandBuilder.AlignmentPosition.Left)
   builder.appendMultiple(content3, 2, 2)
                //4
   builder.append(content4)
   ・・・続く

👆この段階で文字の大きさ・太さ・中央寄せなどを記します。

ちなみに自分が作った伝票システムでは、注文ごとをブロック(線)で区切る必要があり,以下のコードを書きました。(複雑なので説明はカットします(ノД`)

    //ブロック毎に("------")をつけるためのコード
  if (x + 1 == inBlockList[y] + z){
        z += inBlockList[y]
        val a =   ( "------------------------------------------------").toByteArray(charset("SJIS"))
        builder.append(a)
         y += 1
   }

このようなコードも全て自分で書く必要があります。
(最初は全部SDKが自動でやってくれるものかと...)

そして最後に紙を切る処理を入れて、builderに入っているデータをまとめます。

   builder.appendCutPaper(ICommandBuilder.CutPaperAction.PartialCutWithFeed)
   builder.endDocument()

   val cmd = builder.commands

プリンターのPortをopen

あとは、プリンターのPortをopenして、ドキュメントの通りにデータをプリンターに送ります。

 //プリンターのportオープン
  var port: StarIOPort? = null

  try {
        port = StarIOPort.getPort("BT:", ";d3", 3000)
        var status = port.beginCheckedBlock(
        port.writePort(cmd, 0, cmd.size)
        status = port.endCheckedBlock()

        if (!status.offline) {
           // 印刷正常終了(プリンタオンライン)
            toast("印刷が完了しました。")
        } else {
           // 印刷異常終了(紙無し、プリンタカバーオープンなど)
           // ユーザーに通知
        }
  } catch (e: StarIOPortException) {
          toast("$e")
          Log.e("startPortのエラー", "$e")
         // エラー発生
  } finally {
            try {
                // ポートクローズ
               StarIOPort.releasePort(port)
             } catch (e: StarIOPortException) {
                   toast("エラー2: $e")
                 }
   }

(※エラー部分は適時変えておいてください。)

そして、初期化

最後にこのレシート情報作成関数で使用したリストなどを初期化しましょう!
初歩的なことですが大事です。
自分はこれを忘れていて何分か無駄にしました(';')

おまけ

レシート作成は自分で書いたコードが画面上ではなく、リアルに反映されるのでなかなか楽しかったです。
レイアウトを考える作業なんかも職人気分で没頭して、10時間ぶっとうしで作りました。

通してわかったコツは、とにかく試行錯誤の回数だと思います。
レシート作成に限らず、ここの泥臭さを楽しめたら一気に進むと思います!
    (80Lぐらいのゴミ袋がパンパンになったの写真👇)
123.png

最後までありがとうございました!

64
47
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
64
47