九保すこひ@フリーランスエンジニア|累計300万PVのブログ運営中
さてさて、ここのところElectron
の開発記事を書いているからか、デスクトップのアプリを使っていても「あ、ここはこうしたいな」と自然と改善点が思い浮かんだりしているのですが、その中でも画像ビューアはとても多機能なものが多いですが、もっとシンプルでもいいのではと感じることがよくあります。
そこで、いつかは自分で自分自身のために画像ビューアをつくりたいなと考えていたのですが、せっかく今Electron
開発の流れがあるので、この勢いで基本となる部分だけでも作ってみることにしました。
もちろんUbuntu
に標準で搭載されているようなレベルのアプリケーションがすぐ作れるわけではありませんが、残りは少しずつ自分好みの改造をしていこうかなと考えています。
内容は誰でもわかるようにしているので、ビギナーの方でも参考になるはずです。
ぜひ楽しんで読んでみてくださいね。
※ 開発環境: Electron 2.0
目次
やりたいこと
- 画像を一覧表示して、フォルダの移動もできる
- 画像の一覧は4つの並べ替えができる。{ 名前(昇順、降順)/更新日時(昇順、降順) }
- クリックすると別ウィンドウで画像を表示
では実際に開発をしていきましょう!
Electronのインストール
インストールは、初心者向き!electron で簡単なメモ帳をつくってみようの “electronのインストール” に詳しく書いてあるので、そちらを見てください。
フォルダ名はmy-viewer
です。
アプリの設定をする
では、まずはmain.js
の中でアプリ設定をします。
今回は、画像の一覧を表示するので少し大きめにウィンドウを開くことにしました。
mainWindow = new BrowserWindow({ width: 800, height: 600, x: 0, y: 0, autoHideMenuBar: true, webPreferences: { preload: path.join(__dirname, 'preload.js'), nodeIntegration: true } })
【追記:2020.3.19】Electron v5
以降のためにwebPreferences
を追加しました。
JS、CSSのフレームワークをインストールする
レイアウトやJavaScriptコード必要になるので、CSSフレームワークのbootstrap
、JavaScriptフレームワークのVue
をインストールしておきましょう。
npm i bootstrap --save
npm i vue --save
レイアウトをつくる
先ほどインストールしたbootstrap
(のCSSだけ)を使ってレイアウトを作っていきます。
実際のHTMLコードはこちらです。
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>My画像ビューア!</title> <link rel="stylesheet" href="./node_modules/bootstrap/dist/css/bootstrap.min.css"> <style> body { padding: 15px 0; } #images { border: 5px solid #eee; border-radius: 10px; margin-top: 20px; padding: 15px; min-height: 500px; } #images img { height: 70px; } .image-container { width: 15%; height: 150px; margin: 5px; text-align: center; float:left; } .image-container a { color: #222; text-decoration: none; } .image-container img { margin-bottom: 5px; } .filename { font-size: 80%; word-break: break-all; } </style> </head> <body> <div id="app" class="container"> <div class="row"> <div class="col-9"> <input type="text" class="form-control" value="/home/****/images"> </div> <div class="col-3"> <select class="form-control"> <option>名前(昇順)</option> </select> </div> </div> <div id="images"> <div class="image-container"> <a href="#"> <img src="assets/images/computer_folder.png"> <div class="filename">computer_folder_computer_folder</div> </a> </div> <div class="image-container"> <a href="#"> <img src="assets/images/example.png"> <div class="filename">example.png</div> </a> </div> <div class="image-container"> <a href="#"> <img src="assets/images/example_2.png"> <div class="filename">example_2.png</div> </a> </div> <div class="clearfix"></div> </div> </div> <script> require('./renderer.js') </script> </body> </html>
まず、アドレスバーになる<input>
、並べ替えを選択する<select>
、そして最後にファイル一覧を表示する<div>
タグがありますが、この部分をVueでレンダリングすることになります。
なお、気をつけるべきは
<div class="clearfix"></div>
の部分です。
これがないとfloat:left
が効いているのでレイアウトが崩れるからです。
では、実際のレイアウトです。
※ なお、フォルダ画像はいらすとやさんの画像を使わせていただいてます。m_ _m
では、このレイアウトを使って次からJavaScriptを作っていきましょう。
JavaScript部分をつくる
Vueの基本形をつくる
まずはVueの基本形です。
本体を読み込んで new Vue()
でインスタンスを作っています。
<script src="./node_modules/vue/dist/vue.min.js"></script> <script> new Vue({ el: '#app' }) </script>
アドレスバーの部分をつくる
まずはアドレスバーから作っていきます。
Vueの変数data
に現在のアドレスを格納するpath
を登録します。
new Vue({ el: '#app', data: { path: '' } })
そして、アドレスバーを表示することなっている<input>
タグのvalue
をv-model
へ変更してpath
をバインディングします。
<input type="text" class="form-control" value="/home/****/images">
↓↓↓
<input type="text" class="form-control" v-model="path">
そして、getHomePath()
を作ってホームディレクトリが取得できるようにし、ページが読み込まれたら、すぐにホームディレクトリへ移動するようにします。
methods: { getHomePath() { const key = (process.platform == 'win32') ? 'USERPROFILE' : 'HOME' return process.env[key]; } }, mounted() { this.path = this.getHomePath() }
getHomePath()
では、Windowsとその他ではホームフォルダのパスが格納されている環境変数の場所が違うのでキーを切り替えて取得しています。
これでページが表示されたら、このようにホームフォルダのパスが自動で表示されるようになります。
並べ替えの部分をつくる
では次にページ右上にある並べ替え部分をつくっていきます。
まずは並べ替えに必要な変数です。
data
に、選択肢のインデックスを保持するsort
と、並べ替えの選択肢データsortOptions
をそれぞれ登録します。
data: { // 省略 sort: 0, sortOptions: [ { value: 'name asc', text: '名前(昇順)' }, { value: 'name desc', text: '名前(降順)' }, { value: 'modified asc', text: '変更日時(昇順)' }, { value: 'modified desc', text: '変更日時(降順)' }, ] },
続いて変数のバインディングです。
<select class="form-control" v-model="sort"> <option v-for="(option,index) in sortOptions" v-text="option.text" :value="index"></option> </select>
まず<select>
には選択されたsort
を、そして<option>
ではv-for
でsortOptions
をループさせ各データを設定します。
これをVueでレンダリングすると以下のようになります。
<select class="form-control"> <option value="0">名前(昇順)</option> <option value="1">名前(降順)</option> <option value="2">変更日時(昇順)</option> <option value="3">変更日時(降順)</option> </select>
フォルダの中身を表示する
続いて、アドレスバーのパスが変更になったタイミングで、ファイル一覧の表示を変更する部分をつくっていきましょう。
このアプリではフォルダ移動ができるようにしたいので、表示すべきものは次の2つです。
- フォルダ
- 画像ファイル
ファイルとフォルダを読み込む準備をする
では、フォルダ内を読み込んで取得した「フォルダ名」と「画像ファイル名」を保持しておく変数images
とfolders
をdata
に登録しましょう。
data: { path: '', images: [], folders: [] },
そして、ファイルとパスを管理するモジュールfs
とpath
を読み込んで使えるようにしておきます。
const fs = require('fs') const path = require('path')
アドレスが変更になったら実行するコードをつくる
では、ここからpath
に変更があった時のコードになります。
まずwatch
にpath
を登録して内容に変更があったらloadFolder()
を実行するようにします。
watch: { path() { this.loadFolder() } },
※ change
イベントやinput
イベントではmounted()
内でホームディレクトリが変更されても起動できないためwatch
にしています。
loadFolder()
の中身はこうなります。
loadFolder() { this.images = [] this.folders = [] if(fs.existsSync(this.path)) { const extensions = ['.png', '.jpg', '.jpeg', '.gif', '.bpm', '.webp'] const files = fs.readdirSync(this.path) .map(name => { const stat = fs.statSync(this.path +'/'+ name) const extension = path.extname(name) return { name: name, extension: extension, path: this.path +'/'+ name, type: stat.isFile() ? 'file' : 'folder', time: stat.mtime.getTime() } }) .filter(file => { if(file.type == 'file') { return (extensions.includes(file.extension)) } return (!file.name.startsWith('.')) }) for(file of files) { if(file.type == 'folder') { this.folders.push(file) } else { this.images.push(file) } } } }
まずこのメソッドが呼ばれたらすぐにimages
とfolders
を初期化します。
そして、existsSync()
で現在のパスが存在しているかをチェックし、もし存在していれば、その中にある全てのファイルとフォルダを取得します。
ただし、取得したファイル、フォルダは名前だけが分かっている状態ですので、map()
を使って全てのデータをstatSync()
で取得し、「名前」「拡張子」「パス」「ファイル or フォルダ」「更新時間」の5データをオブジェクト化します。
さらに、今回は不要となるファイルを次の2つの条件で除外します。
- ファイルの場合 ・・・ 拡張子が「.png」など画像のものである。
- フォルダの場合 ・・・ 隠しフォルダではない(つまりドット
.
から始まらない)
これでfiles
の中にはフォルダと画像ファイルのデータのみになりましたので、後はfor
ループでひとつひとつ画像ならimages
へ、フォルダならfolders
へそれぞれ格納していきます。
画像ファイル、フォルダの並べ替えをする
では「画像ファイル」と「フォルダ」を現在選択されている形式で並べ替えましょう。
まずは実際に並べ替えをするメソッドsortFiles()を作ります。
sortFiles(files) { const sort = this.sortOptions[this.sort].value let sorted if(sort == 'name asc') { sorted = files.sort((a, b) => { return (b.name < a.name) ? 1 : -1 }) } else if(sort == 'name desc') { sorted = files.sort((a, b) => { return (b.name < a.name) ? -1 : 1 }) } else if(sort == 'modified asc') { sorted = files.sort((a, b) => { return a.time - b.time }) } else if(sort == 'modified desc') { sorted = files.sort((a, b) => { return b.time - a.time }) } return sorted }
※ この部分のコードはElectron でファイル名を連番でリネームするアプリをつくるで開発したものを使っています。
ではこのメソッドを使って並べ替え済みデータを作ります。なお、並べ替えはキャッシュが効いていた方が高速表示に有利なのでcomputed
に擬似変数として登録します。
computed: { sortedImages() { return this.sortFiles(this.images) }, sortedFolders() { return this.sortFiles(this.folders) } }
これで、sortedImages
とsortedFolders
で並べ替え済みのファイルデータが取得できるようになりました。
フォルダを表示する
では、まずはfolders
を使ってフォルダを表示してみます。
<div class="image-container" v-for="folder in sortedFolders"> <a href="#" @click.prevent="changePath(folder)"> <img src="assets/images/computer_folder.png"> <div class="filename" v-text="folder.name"></div> </a> </div>
まずv-for
で並べ替え済みのフォルダをループさせます。そして、ループの中ではクリックされたときにフォルダを移動するメソッドchangePath()
を呼び、また、v-text
を使ってフォルダ名も表示します。
changePath()
の中身はこうなります。
changePath(folder) { if(this.path.endsWith('/')) { this.path += folder.name } else { this.path += '/'+ folder.name } }
内容としては、現在のパスが/
で終わっている場合はそのまま、それ以外の場合は/
を足して新しいパスを作っています。
実際に実行した画像がこちらです。
クリックするとアドレスバーのパスが変更になり、さらに下層のフォルダ内容が表示されるようになっています。
画像ファイルを表示する
では画像ファイルも同じやり方で表示できるようにしましょう。
<div class="image-container" v-for="image in sortedImages"> <a href="#" @click.prevent="openImage(image)"> <img :src="image.path"> <div class="filename" v-text="image.name"></div> </a> </div>
先ほどとほとんど同じですが、<img>
タグ内の:src
に各画像のパスを設定しています。また、画像がクリックされたときはopenImage()
を呼ぶようにしています。
openImage(image) { // ここで画像を開く }
これを実行した画像がこちらです。
画像を開く部分をつくる
では画像がクリックされたときにウィンドウを開き、その中に画像を表示するようにしてみましょう。
まずウィンドウを開くために必要なモジュールを読み込み、ウィンドウを保持する変数viewer
をdata
へ追加します。
const electron = require('electron'); const BrowserWindow = electron.remote.BrowserWindow;
data: { // 省略 viewer: null },
そして、先ほど作ったopenImage()
内で次のようにします。
openImage(image) { const viewer = new BrowserWindow({ width: 300, height: 300, center: true, autoHideMenuBar: true, }) viewer.loadFile(image.path) }
やっているのは、新しいウィンドウをBrowserWindow
で作成し、loadFile()
を使って画像のパスを呼び出しているだけです。
これで画像がクリックされると新しいウィンドウ内で画像が表示されるようになりました。
実際の例はこちらです。(透過画像なので背景が黒になっています。)
お疲れ様でした!
おまけ
さすがに「上の階層へ移動する」ボタンがないとテストも不便なのでつけることにしました。また、せっかくなので、「ホームボタン」もつくります。
まず、以下のように2つのグループ化されたボタンをつくり、それぞれクリックイベントにupPath()
とhomePath()
を実行できるようにします。
<div class="col-2"> <div class="btn-group" data-toggle="buttons"> <button type="button" class="btn btn-outline-warning" @click="upPath()">☝</button> <button type="button" class="btn btn-outline-warning" @click="homePath()">🏠</button> </div> </div>
そして、実行されるメソッドはこうなります。
upPath() { this.path = path.dirname(this.path) }, homePath() { this.path = this.getHomePath() },
それぞれ、対象になるパスを取得してpath
の内容を変更しているだけです。watch
が効いているのでこれを変更するだけで表示内容が自動で変更されます。
実際の画像はこちらです。
これでホームディレクトリにも上の階層への移動もクリック1つでOKになりました。
教材ソースコードをダウンロードする
今回実際に開発したソースコード一式を以下からダウンロードすることができます。
※ ただし、npmのインストールやフォルダ画像はご自身で用意してください。
※ また、今回は基本的な部分の説明ということで省略していますが、本来画像はメモリの使用量を減らすため、サムネイルを作ってそちらを表示すべきです。そのため、あまりにも多く画像がある場所を表示してしまうと処理が止まってしまう可能性がありますのでご注意ください。