Electron で独自の画像ビューアをつくってみよう(ダウンロード可)

さてさて、ここのところ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
})

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>タグのvaluev-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-forsortOptionsをループさせ各データを設定します。

これをVueでレンダリングすると以下のようになります。

<select class="form-control">
    <option value="0">名前(昇順)</option>
    <option value="1">名前(降順)</option>
    <option value="2">変更日時(昇順)</option>
    <option value="3">変更日時(降順)</option>
</select>

フォルダの中身を表示する

続いて、アドレスバーのパスが変更になったタイミングで、ファイル一覧の表示を変更する部分をつくっていきましょう。

このアプリではフォルダ移動ができるようにしたいので、表示すべきものは次の2つです。

  • フォルダ
  • 画像ファイル

ファイルとフォルダを読み込む準備をする

では、フォルダ内を読み込んで取得した「フォルダ名」と「画像ファイル名」を保持しておく変数imagesfoldersdataに登録しましょう。

data: {
    path: '',
    images: [],
    folders: []
},

そして、ファイルとパスを管理するモジュールfspathを読み込んで使えるようにしておきます。

const fs = require('fs')
const path = require('path')

アドレスが変更になったら実行するコードをつくる

では、ここからpathに変更があった時のコードになります。

まずwatchpathを登録して内容に変更があったら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)

            }

        }

    }

}

まずこのメソッドが呼ばれたらすぐにimagesfoldersを初期化します。

そして、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)
    }
}

これで、sortedImagessortedFoldersで並べ替え済みのファイルデータが取得できるようになりました。

フォルダを表示する

では、まずは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) {

    // ここで画像を開く

}

これを実行した画像がこちらです。

画像を開く部分をつくる

では画像がクリックされたときにウィンドウを開き、その中に画像を表示するようにしてみましょう。

まずウィンドウを開くために必要なモジュールを読み込み、ウィンドウを保持する変数viewerdataへ追加します。

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()">&#x261D;</button>
        <button type="button" class="btn btn-outline-warning" @click="homePath()">&#x1F3E0;</button>
    </div>
</div>

そして、実行されるメソッドはこうなります。

upPath() {

    this.path = path.dirname(this.path)

},
homePath() {

    this.path = this.getHomePath()

},

それぞれ、対象になるパスを取得してpathの内容を変更しているだけです。watchが効いているのでこれを変更するだけで表示内容が自動で変更されます。

実際の画像はこちらです。

これでホームディレクトリにも上の階層への移動もクリック1つでOKになりました。

教材ソースコードをダウンロードする

今回実際に開発したソースコード一式を以下からダウンロードすることができます。

※ ただし、npmのインストールやフォルダ画像はご自身で用意してください。
※ また、今回は基本的な部分の説明ということで省略していますが、本来画像はメモリの使用量を減らすため、サムネイルを作ってそちらを表示すべきです。そのため、あまりにも多く画像がある場所を表示してしまうと処理が止まってしまう可能性がありますのでご注意ください。

Electron で画像ビューアをつくってみよう