0
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?

Electron+Webix を使う

Posted at

はじめに

デスクトップアプリを作るための開発環境 Electron と、ウェブアプリを作るためのライブラリ Webix を使っています。

Electron を始める #JavaScript - Qiita
Electron を始めるの続き #JavaScript - Qiita

Webix を使ってみた #JavaScript - Qiita
Webix を使ってみたの続き #JavaScript - Qiita

この二つを一緒に使ってみたいと思います。

Electron+Webix を使う

Electron のプロジェクトを新規作成する

まず、Electron のプロジェクトを新規作成します。

ワークスペースのフォルダおよびファイルは以下のようにしました。

package.json
src
    main.js
    preload.js
    index.html
    style.css
    renderer.js
node_modules

Webix のライブラリを組込する

作成したプロジェクトに Webix のライブラリを組込します。

以下のページからダウンロードします。
Download a JavaScript UI Widgets Library Webix Based on HTML5 and CSS3 Standards

以下のファイルをワークスペースにコピーします。

codebase
    webix.min.css
    webix.min.js

配置するのはプロジェクトの以下のフォルダにしました。

src
    lib
        webix
            webix.min.css
            webix.min.js

index.html に以下のコードを追記します。

index.html
    <link rel="stylesheet" href="./lib/webix/webix.css" type="text/css"> 
    <script src="./lib/webix/webix.js" type="text/javascript"></script>  

Electron 公式サイトにある index.htmlContent-Security-Policy を変更しておかないと Webix のスクリプトが実行されませんでした。↓

index.html
        <meta
        http-equiv="Content-Security-Policy"
        content="default-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-eval'"
        />
        <meta
        http-equiv="X-Content-Security-Policy"
        content="default-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-eval'"
        />

併せて、style.css の設定は以下のようにしておきます。

index.html
    <link rel="stylesheet" href="./style.css" type="text/css">
style.css
html {
    background-color: white;
}

body {
    margin: 0; padding: 0;
}

body {
    height: 100vh; width: 100%;
}

アプリの画面を記述する

Webix ライブラリを使ってアプリの画面を記述します。

Getting Started - Webix Docs

Webix の UI コンポーネントは JavaScript コードで生成します。renderer.js に記述します。

renderer.js
    webix.ui({
        rows:[
            {
                type:'header', view:'template', template:"Try Webix",
            },
            {
                view:'form',
                elements:[
                    {
                        view:'text', id:"name", label:"Name", name:"name"
                    },
                    {
                        view:'button', id:"clickme", value:"Click me.",
                    },
                    {
                        view:'text', id:"message", label:"Message", name:"message"
                    }
                ],
            },
        ],
    });

Webix のコンポーネントは基本的に、親要素の領域一杯に表示されます。container プロパティに HTML タグを指定して、その領域一杯に表示させることもできます。
<body></body> に何も置かないで container の指定しないと、コンポーネントはブラウザのウィンドウ一杯に表示されます。↑

上記のコードは、通常は以下の初期化ブロックに記述します。

rebderer.js
webix.ready(function(){
    webix.ui({
    (以下略)

ビルドして実行してみます。

サンプル①.png

イベントハンドラを記述する

ボタンをクリックすると実行するイベントハンドラを記述します。↓

renderer.js
    $$('clickme').attachEvent('onItemClick', function(){
        var name = $$('name').getValue();
        if (name == "") {
            webix.message("Please enter your name.");
            return;
        }
        $$('message').setValue("Hello, " + name);
    });

Event Handling of Guides, Interacting with Users Webix Docs

バックエンドのプログラムを呼出する

バックエンドの main.js に以下の関数を用意します。

main.js
// Greet returns a greeting for the given name
function greet(name) {
    if (name == "") {
        throw new Error("argument 'name' is empty");
    }
    return `Hello, ${name}!`;
}

以下の設定を用意します。

main.js
ipcMain.handle('greet', (e, arg) => {
    try {
        const result = greet(arg);
        return [result, null];
    }
    catch (err) {
        return [null, err.message];
    }
});
preload.js
const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('main', {
    greet: (arg) => {
        return ipcRenderer.invoke('greet', arg);
    }
}

この関数をフロントエンドのコードで呼出します。

renderer.js
    $$('clickme').attachEvent('onItemClick', function(){
        (中略)
     /* $$('message').setValue("Hello, " + name);
        ↓ */
        var [result, err] = await main.greet(name);
        if (!err) {
            $$('message').setValue(result);
        }
        else {
            webix.message("Function failed: " + err);
        }
    });

Electron+NeDB を使ってみる

アプリのデータは NeDB データベースに記録するようにしたいと思います。

Node.js の NeDb パッケージを組込する

Electron の Node.js プログラムで NeDB データベースを利用できるよう用意します。

NeDB を使ってみたの続き #JavaScript - Qiita

Node.js アプリのワークスペースにインストールします。

npm install @seald-io/nedb --save 

Electron の Node.js プログラムに以下のコードを追加します。

main.js
const Datastore = require('@seald-io/nedb');

アプリの起動時にデータベースを作成する

実行ファイルと同じフォルダにデータファイル data.db を作ることにします。さらに、テーブル list を作成します。それにデータを追加しておきます。↓

main.js
const dbfile = path.join(path.resolve("."), "data.db");

const list = new Datastore({ filename: dbfile });

async function initDb() {
    await list.loadDatabase();

    var doc = { id:1, title:"The Shawshank Redemption", year:1994, votes:678790, rating:9.2, rank:1 };
    var docs = await list.findAsync({ id: doc.id });
    if (docs.length == 0) {
        await list.insertAsync(doc);
    }
    doc = { id:2, title:"The Godfather", year:1972, votes:511495, rating:9.2, rank:2 };
    docs = await list.findAsync({ id: doc.id });
    if (docs.length == 0) {
        await list.insertAsync(doc);
    }
}

initDb() に書かないで main.js にフラットに書いてもよさそうですが、処理でエラー発生したときにフロントエンドのプログラムでメッセージ表示したいでしょう。initDb() をフロントエンドのプログラムで呼出します。↓

main.js
ipcMain.handle('initDb', async (e, arg) => {
    try {
        await initDb();
        return [null];
    }
    catch (err) {
        return [err.message];
    }
});
preload.js
contextBridge.exposeInMainWorld('main', {
    initDb: () => {
        return ipcRenderer.invoke('initDb');
    },
rederer.js
    var [err] = await main.initDb();
    if (!err) {
        webix.message("Database initialized.");
    }
    else {
        webix.message("Database init failed: " + err);
    } 

Webix の datatable を使ってみる①

datatable コンポーネントを記述する

まず、datatable コンポーネントを画面に表示します。

renderer.js
webix.ui({
    rows:[
        {   
            view:'datatable',
            id:"list",
            columns:[
                { id:'no', header:"", width:30 },
                { id:'title', header:"Title", width:300 },
                { id:'year', header:"Released", width:80 },
                { id:'votes', header:"Votes", width:100 }
            ],
        },
    ]
});

この datatable コンポーネントにデータをセットして表示します。

サンプル②.png

データを取得するメソッドを用意する

NeDB からデータを取得するメソッドを用意します。

main.js
async function getItems() {
    const docs = await list.findAsync({});
    return docs;
}

ipcMain.handle('getItems', async (e, arg) => {
    try {
        const docs = await getItems();
        return [docs, null];
    }
    catch (err) {
        return [null, err.message];
    }
})
renderer.js
contextBridge.exposeInMainWorld('main', {
    getItems: () => {
        return ipcRenderer.invoke('getItems');
    },

テーブル list に記録されている全件を取得します。↑

datatable にデータをセットする

これをフロントエンドのプログラムで呼出します。

renderer.js
    var [docs, err] = await main.getItems();
    if (!err) {
        $$('list').parse(docs);
    }
    else {
        webix.message(err);
    }

parse メソッドを使って datatable にデータをセットします。↑

参考 https://snippet.webix.com/598f77de

initDb()getItems()async 関数なので、await で呼出します。そのため、webix.ready(function(){

renderer.js
webix.ready(async function(){

    (中略)

    var [err] = await main.initDb();
    (中略)

    var [docs, err] = await main.getItems();
    (中略)
})

Webix の datatable を使ってみる②

datatable を編集可能にする

editable プロパティを指定すると、セル単位でインライン編集できるようになります。

renderer.js
        {   
            view:'datatable',
            id:"list",
            (中略)
            columns:[
                { id:'rank', header:"", width:30 },
                { id:'title', header:"Title", width:300, editor:'text' },
                { id:'year', header:"Released", width:80, editor:'text' },
                { id:'votes', header:"Votes", width:100, editor:'text' }
            ],
            editable:true,
        },

参考 https://snippet.webix.com/f1edcadc

さらに、データの追加と削除できるように入力フォームを用意しましょう。

サンプル③.png

datatable にデータを追加する

add メソッドを使って datatable に追加できます。

まず、入力フォームを追加します。「追加」ボタンを用意しています。

renderer.js
        { 
            view:'datatable', 
            id:"list",
            (中略)
        },
        { 
            view:'form',
            id:"input",
            elements:[
                { view:'text', name:"title", value:"New Movie",inputWidth:200 },
                { view:'text', name:"year", value:"2025", inputWidth:200 },
                { view:"text", name:"votes", value:"999999", inputWidth:200 },
                { cols:[
                    { view:'button', width:100, value:"Add", click:addData }, 
                ] }
            ]
        }

「追加」ボタンをクリックすると関数 addData を実行するようにしています。ここで datatable に行を追加します。↓

    function addData(){
        var values = $$('input').getValues();
        $$('list').add({
            title: values['title'],
            year: values['year'],
            votes: values['votes']
        });
    }

参考 https://snippet.webix.com/247a9541

datatable にデータを削除する

remove メソッドを使って datatable から行を削除できます。

まず、入力フォームに「削除」ボタンを追加します。

renderer.js
        { 
            view:'datatable', 
            id:"list",
            (中略)
            select:'row',
        },
        { 
            view:'form',
            (中略)
                    { view:"button", width:160, value:"Remove selected", click:removeData }, 
                ] }
            ]
        }
(中略)

「追加」ボタンをクリックすると関数 removeData を実行するようにしています。ここで datatable から行を削除します。

    function removeData(){
        var id = $$('list').getSelectedId()
        if (!id) {
            return;
        }
        $$('list').remove(id);
    }

選択した行を削除するので、datatableselect:'row' を指定して行単位で選択できるようにしておきます。↓

        {   
            view:'datatable',
            id:"list",
            (中略)
            select:`row`,
        },

参考 https://snippet.webix.com/247a9541

データの変更を受けるメソッドを用意する①

データの追加を受けるメソッドを用意します。指定されたデータ item をテーブル list に追加します。

main.js
async function insertItem(item) {
    await list.insertAsync(item);
}

ipcMain.handle('insertItem', async (e, arg) => {
    try {
        await insertItem(arg);
        return [null];
    }
    catch (err) {
        return [err.message];
    }
})
preload.js
contextBridge.exposeInMainWorld('main', {
    insertItem: (item) => {
        return ipcRenderer.invoke('insertItem', item);
    },

データを変更するメソッドを呼出する①

データが変更されたイベントを受けて、上記のメソッドをフロントエンドのプログラムで呼出します。

datatable に行が追加されるタイミングで onBeforeAdd イベントが発生します。

renderer.js
    $$('list').attachEvent('onBeforeAdd', function(id, data, index){
       var [err] = await main.insertItem(data);
        if (!err) {
            webix.message("Inserted.")
        }
        else {
            webix.message(err)
        }
    });

データの変更を受けるメソッドを用意する②

データの変更を受けるメソッドを用意します。指定されたデータ item をテーブル list に更新します。

main.js
async function updateItem(item) {
    await list.updateAsync({ 'id':item.id }, item);
}

ipcMain.handle('updateItem', async (e, arg) => {
    try {
        await updateItem(arg);
        return [null];
    }
    catch (err) {
        return [err.message];
    }
})
preload.js
contextBridge.exposeInMainWorld('main', {
    updateItem: (item) => {
        return ipcRenderer.invoke('updateItem', item);
    },

データを変更するメソッドを呼出する②

データが変更されたイベントを受けて、上記のメソッドをフロントエンドのプログラムで呼出します。

datatable で表示の内容が変更されると onDataUpdate イベントが発生します。

main.js
    $$('list').attachEvent('onDataUpdate', function(id, data, old){
        var [err] = await main.updateItem(data);
        if (!err) {
            webix.message("Updated.")
        }
        else {
            webix.message(err)
        }
    });

データの変更を受けるメソッドを用意する③

データの変更を受けるメソッドを用意します。指定されたデータ item をテーブル list から削除します。

main.js
async function deleteItem(item) {
    await list.removeAsync({ 'id':item.id });
}

ipcMain.handle('deleteItem', async (e, arg) => {
    try {
        await deleteItem(arg);
        return [null];
    }
    catch (err) {
        return [err.message];
    }
})
preload.js
contextBridge.exposeInMainWorld('main', {
    deleteItem: (item) => {
        return ipcRenderer.invoke('deleteItem', item);
    },

データを変更するメソッドを呼出する③

データが変更されたイベントを受けて、上記のメソッドをフロントエンドのプログラムで呼出します。

datatable で行が削除されるタイミングで onBeforeDelete イベントが発生します。

renderer.js
    $$('list').attachEvent('onBeforeDelete', async function(id){
        var dt = this;
        var [err] = await main.deleteItem(dt.getItem(id));
        if (!err) {
            webix.message("Deleted.");
        }
        else {
            dt.undo();
            webix.message(err);
        }
    });

DaleteItem メソッドが失敗したときは datatable コンポーネントで行の削除をアンドゥします。↑
アンドゥできるように datatable コンポーネントに指定しておきます。↓

        {   
            view:'datatable',
            id:"list",
            (中略)
            undo:true,
        },

Webix の datatable を使ってみる③

新規追加の空行を追加する

一覧表示の最終に空行を表示してデータを入力して追加できると便利ですね。

サンプル④.png

onBeforeRender イベントで空行を追加します。

renderer.js
        {   
            view:'datatable',
            id:"list",
            columns:[
                { id:'no', header:"", width:30 },
                { id:'title', header:"Title", width:300, editor:'text' },
                { id:'year', header:"Released", width:80, editor:'text' },
                { id:'votes', header:"Votes", width:100, editor:'text' }
            ],
            editable:true,
        },
    (中略)

    $$('list').attachEvent('onBeforeRender', function(){
        var dt = this;
        if (dt.count() == 0){
            dt.add({});
        }
        if (canAdd(dt.getLastId())) {
            dt.add({});
        }
        dt.data.each(function(obj, i){
            obj['no'] = i + 1;
        });
    });

先頭の列を「no」にして、連番を表示するようにしています。↑

一覧にデータを追加したとき上記のハンドラが呼出されるよう、以下のコードを加えておきます。

    var [docs, err] = await main.getItems();
    if (!err) {
        $$('list').parse(docs);
        $$('list').render();  // 強制的に描画し直す
    (以下略)

追加していいか判定する

上記のコードで canAdd 関数を用意して使っています。指定された行の、全てのセルが空でないか確認して、空がなければ行の追加を可能と判断します。↓

    function canAdd(id) {
        return $$('list').getColumns(true).every(function(obj){
            return ($$('list').getItem(id)[obj.id])
        });
    }

データを変更するメソッドを呼出する②

onBeforeAdd イベントが発生する空行を追加した時点では全ての項目は空なので、ここで記録する処理しても意味ありません。
onDataUpdate イベントでも全ての項目がセットされていないと、処理してはだめでしょう。canAdd を使って判定します。

renderer.js
    $$('list').attachEvent('onBeforeAdd', function(id, data){
        // 何もしない
    });
    
    $$('list').attachEvent('onDataUpdate', function(id, data){
        if (!canAdd(id)) {
            return;
        }
        // ここでデータ更新するメソッドを呼出
    });

データ更新するメソッドを呼出しますが、この時点で追加か更新か判別して処理できません。↑

データの変更を受けるメソッドを用意する②

id を使ってサーバ側で登録済か確認して、未登録なら追加、そうでなければ更新の処理するのがいいでしょう。

main.js
async function upsertItem(item) {
    await list.updateAsync({ 'id':item.id }, item, { upsert: true });
}

ipcMain.handle('upsertItem', async (e, arg) => {
    try {
        await upsertItem(arg);
        return [null];
    }
    catch (err) {
        return [err.message];
    }
})
preload.js
contextBridge.exposeInMainWorld('main', {
    upsertItem: (item) => {
        return ipcRenderer.invoke('upsertItem', item);
    }

いわゆる「UPSERT」処理ですね。NeDB では updateAsync({ upsert: true }) で書けます。↑

これをフロントエンドのプログラムで呼出します。↓

renderer.js
    $$('list').attachEvent('onDataUpdate', async function(id, data){
        if (!canAdd(id)) {
            return;
        }
        var [err] = await main.upsertItem(data);
        if (!err) {
            webix.message("Upserted.")
        }
        else {
            webix.message(err)
        }
    });
0
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
0
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?