2
1

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.

electronでQRコードリーダーを作った

Last updated at Posted at 2022-11-25

概要

PC上でQRコードをスクショして読み取るアプリがなかったのでelectronで作りました。

QRをsnipping toolなどでキャプチャ(コピーだけでいい)→アプリ起動→クリップボードを参照→QRの内容を表示
といった流れです。

QRの読み取りはjsqrというライブラリを使います。
electronのチュートリアルは割愛します。

動作

usage.gif

環境

node v16.13.1
electron 21.3.1
jsqr 1.4.0

ipc通信とpreloader

本記事では、ipc通信とpreload.jsによるAPIアクセスを利用しています。
Electron ipc (プロセス間通信) ipcMain, ipcRenderer 使い方
こちらの記事がすごくわかりやすかったのでおすすめです。

コード全文

main.js
const { app, BrowserWindow ,ipcMain , clipboard ,shell  } = require('electron');
const path = require('path');
const jsqr = require('jsqr');


const createWindow = () => {
  const mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js')
    }
  })
  
  mainWindow.loadFile('index.html');
  mainWindow.setPosition(1000, 0);
  // mainWindow.webContents.openDevTools()

  mainWindow.webContents.setWindowOpenHandler(details =>{
    shell.openExternal(details.url);
    return { action: "deny" };
  });
  
  ipcMain.on('reload', () => {
    mainWindow.reload();
  })
  
}

app.whenReady().then(() => {
  createWindow();
  app.on('activate', () => {
    if (BrowserWindow.getAllWindows().length === 0)
      createWindow();
  })
})


app.on('window-all-closed', () => {
  if (process.platform !== 'darwin')
    app.quit();
})

ipcMain.handle('getImg', async (event, ...args) => {
  return clipboard.readImage().toPNG();
})
ipcMain.handle('getQrValue', async (event, data,width, height) => {
  let code = jsqr(data, width, height);
  if(code){
    return code.data;
  }else{
    return 0;
  }
})
preload.js
const {contextBridge ,ipcRenderer} = require('electron');

contextBridge.exposeInMainWorld(
  'myApi', {
    getImg: async () => await ipcRenderer.invoke('getImg'),
    getQrValue: async (...args) => await ipcRenderer.invoke('getQrValue', ...args),
    reload: () => ipcRenderer.send('reload'),
})
renderer.js
window.addEventListener('DOMContentLoaded', async() => {
  document.querySelector("button").addEventListener('click', ()=>{
    window.myApi.reload();
  })
  await setImg();
  setQrValue();
})
const handleErr = () => {
  const errorMsgEle = document.querySelector(".errorMsg");
  errorMsgEle.innerHTML = "エラー:画像をコピーしてください"
}
const setImg = async() => {
  const imgUrl = await window.myApi.getImg();
  const blob = new Blob([imgUrl], { type: "image/png" });
  const canvas = document.querySelector("canvas");
  const bitmap = await createImageBitmap(blob).catch(handleErr);
  canvas.width = bitmap.width;
  canvas.height = bitmap.height;
  canvas.getContext('2d')?.drawImage(bitmap, 0, 0);
  canvas.style = "border:3px dashed black";

  
}
  
const setQrValue = async() => {
  const canvas = document.querySelector("canvas");
  const ctx = canvas.getContext("2d");
  const img = ctx.getImageData(0, 0, canvas.width, canvas.height);
  const qrValue = await window.myApi.getQrValue(img.data,img.width, img.height);
  const qrValueEle = document.querySelector(".qrValue a");
  if(qrValue != 0){
    qrValueEle.href = qrValue;
    qrValueEle.innerHTML = qrValue;
  }else{
    qrValueEle.innerHTML = "エラー:QRコードは1画像につき1枚まで";
  }
}
index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <meta http-equiv="Content-Security-Policy" content="default-src 'self' data: gap: https://ssl.gstatic.com; connect-src *; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline'; img-src * blob:; media-src *">
    <title>QRreader</title>
  </head>
  <body>
    <div><button>リロード</button> 画像をクリップボードにコピーしてリロード</div>
    
    <div class="errorMsg" style="margin-top:1em;"></div>
    <p class="qrValue"><a target="_blank"></a></p>
    <canvas></canvas>
    <script src="./renderer.js"></script>
  </body>
</html>

解説

main.js

const { app, BrowserWindow ,ipcMain , clipboard ,shell  } = require('electron');
const path = require('path');
const jsqr = require('jsqr');

いつものapp,BrowserWindowの他に、ipc通信のipcMain、clipboardから画像を取得するclipboard、electronのリンクを既定ブラウザで開くためのshellを定義する。

mainWindow.webContents.setWindowOpenHandler(details =>{
    shell.openExternal(details.url);
    return { action: "deny" };
  });

リンクがふまれたときの挙動を定義する。
今回はブラウザで開いたほうが都合が良いので、shell.openExternal()を使っています。
return { action: "deny" };はelectron側でもウィンドウを作ろうとするのを拒否するための処理です。

ipcMain.on('reload', () => {
    mainWindow.reload();
  })
ipcMain.handle('getImg', async (event, ...args) => {
  return clipboard.readImage().toPNG();
})
ipcMain.handle('getQrValue', async (event, data,width, height) => {
  let code = jsqr(data, width, height);
  if(code){
    return code.data;
  }else{
    return 0;
  }
})

ipc通信のハンドル登録をする。
レンダラー側からAPIを通じて呼ばれる側の処理を書いています。
上から、全体のリロード、クリップボードの画像をpngに変換して返す、qrの内容を返すといった内容です。

preload.js

const {contextBridge ,ipcRenderer} = require('electron');

ipc通信のipcRenderer、API接続のためのcontextBridgeを定義する。

contextBridge.exposeInMainWorld(
  'myApi', {
    getImg: async () => await ipcRenderer.invoke('getImg'),
    getQrValue: async (...args) => await ipcRenderer.invoke('getQrValue', ...args),
    reload: () => ipcRenderer.send('reload'),
})

APIを登録する。
invoke()は非同期なのでasyncを付けます。

renderer.js

window.addEventListener('DOMContentLoaded', async() => {
  document.querySelector("button").addEventListener('click', ()=>{
    window.myApi.reload();
  })
  await setImg();
  setQrValue();
})

後述するindex.htmlのボタンが押されたらAPI経由でリロードするので、イベントを登録しておく。
setImg()はrenderer側からクリップボードの画像をメインプロセスに要求し、その画像をcanvasに表示します。
setQrValue()はcanvasの画像の情報をメインプロセスに渡して、QRの内容を表示します。
setImg()が完了してからsetQrValue()を呼びたいのでawaitしておきます。(もしかしたら要らないかもしれない)

const setImg = async() => {
  const imgUrl = await window.myApi.getImg();
  const blob = new Blob([imgUrl], { type: "image/png" });
  const canvas = document.querySelector("canvas");
  const bitmap = await createImageBitmap(blob).catch(handleErr);
  canvas.width = bitmap.width;
  canvas.height = bitmap.height;
  canvas.getContext('2d')?.drawImage(bitmap, 0, 0);
  canvas.style = "border:3px dashed black";  
}

API経由でgetImg()を呼んでblobに変換、createImageBitmap()及びdrawImage()でcanvasに画像を描画する。

const handleErr = () => {
  const errorMsgEle = document.querySelector(".errorMsg");
  errorMsgEle.innerHTML = "エラー:画像をコピーしてください"
}

createImageBitmap()するときに、クリップボードに画像がないとerrorを吐くので、そのハンドリングをする。

const setQrValue = async() => {
  const canvas = document.querySelector("canvas");
  const ctx = canvas.getContext("2d");
  const img = ctx.getImageData(0, 0, canvas.width, canvas.height);
  const qrValue = await window.myApi.getQrValue(img.data,img.width, img.height);
  const qrValueEle = document.querySelector(".qrValue a");
  if(qrValue != 0){
    qrValueEle.href = qrValue;
    qrValueEle.innerHTML = qrValue;
  }else{
    qrValueEle.innerHTML = "エラー:QRコードは1画像につき1枚まで";
  }
}

canvasの画像の情報をメインプロセスのgetQrValue()に渡して、QRの内容を表示する。
画像にQRがない or 2枚以上QRがある と0が返ってくるので分岐しておきます。

index.html

    <div><button>リロード</button> 画像をクリップボードにコピーしてリロード</div>

リロードボタンと説明文

    <div class="errorMsg" style="margin-top:1em;"></div>

エラーはいたときメッセージを格納するdiv

    <p class="qrValue"><a target="_blank"></a></p>

qrの内容(今回の場合だとURL)を表示する。
aタグにしてクリックしたらブラウザで開くようにしました。target="_blank"じゃないとelectron内で開こうとするので注意してください。

まとめ

zoomとかでQRを画面共有されたとき、いちいちスマホを探す手間をこのアプリで省ける と思って作成しました。
electronは前に使ったことがありましたが、ipc通信のあたりとかめっちゃ変わっててわりと苦労しました。それでもelectronは簡単にデスクトップアプリ作れるのはいいですね。
QRjsを初めて使ったけど、すごく簡単にQRを処理できるのでjsでQR読みたいならこれ一択だなって思いました(そんな機会があるかはさておき)。2枚以上あると認識できないけど、画像処理ライブラリとかで分割できるかもしれないです。
クリップボードだけじゃなくて、ローカルに保存されてる画像とか、URLでアクセスできる画像とかも対応したほうがいいかもしれないですね。

参考にしたサイト

Electron ipc (プロセス間通信) ipcMain, ipcRenderer 使い方
Electron入門 ~ Webの技術でつくるデスクトップアプリ
ElectronでのIPCの例(send, sendSync, invoke, etc.)
jsQRであっさりQRコードリーダ/メーカ
Electron の Webview で新規タブを開く場合もページ遷移させる
[Electron] リンクをクリックすると標準Webブラウザで開く

link

QRreader Github

2
1
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
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?