А так же о всякой фигне
В этот раз попробуем написать что-нить сложнее hello world. И разобраться с некоторыми моментами.
Вёрстка в gtk чем-то похожа на веб. Размеры окна и элементов автоматически подстраиваются и автоматически выравниваются, в зависимости от того, какой контейнер используется:
Gtk.Box - горизонтальный контейнер, элементы добавленные в Gtk.Box, выстраиваются по горизонтале.
Gtk.Grid - что-то вроде html таблицы, с колонками и строками.
Gtk.ListBox - вертикальный контейнер
Gtk.Stack и Gtk.StackSwitcher - я тут ещё не разобрался нахер оно надо, но очень интересно.
Gtk.FlowBox - что-то вроде флексбокса, когда колонки автоматически выстраиваются, в зависимости от свободного пространства.
Нам понадобится простой Gtk.Box, который мы разобьём на 2 части, в левую поместим фрейм, во фрейм графику, в правую то же фрейм но со списком.
#!/usr/bin/gjs const Gtk = imports.gi.Gtk; const GLib = imports.gi.GLib; GLib.set_prgname('Advanced Hello World on gjs'); let app = new Gtk.Application({ application_id: 'org.gtk.advancedHello' }); app.connect('activate', () => { log('App started'); let mainWin = new Gtk.ApplicationWindow({ application: app }); let container = new Gtk.Box({}); let leftFrame = new Gtk.Frame({}); let rightFrame = new Gtk.Frame({}); container.add(leftFrame); container.add(rightFrame); leftFrame.set_size_request(500, 300); rightFrame.set_size_request(250, 300); mainWin.add(container); mainWin.show_all(); }); app.run([]);
Результат:
Теперь идём на https://github.com/optimisme/gjs-examples смотрим примеры, находим egList.js и берём его за основу, для построения тестового списка.
function cellFuncText1(col, cell, model, iter) { cell.editable = false; cell.text = model.get_value(iter, 1); }; function cellFuncText2(col, cell, model, iter) { cell.editable = false; cell.text = model.get_value(iter, 2); }; function getBody(parent) { parent.scroll = new Gtk.ScrolledWindow({ vexpand: true }); parent.store = new Gtk.ListStore(); parent.store.set_column_types([GObj.TYPE_INT, GObj.TYPE_STRING, GObj.TYPE_STRING, GObj.TYPE_BOOLEAN]); parent.store.set(parent.store.append(), [0, 1, 2, 3], [0, '0A', 'Name 0', false]); parent.store.set(parent.store.append(), [0, 1, 2, 3], [1, '1B', 'Name 1', false]); parent.store.set(parent.store.append(), [0, 1, 2, 3], [2, '2C', 'Name 2', false]); parent.store.set(parent.store.append(), [0, 1, 2, 3], [3, '3D', 'Name 3', false]); parent.tree = new Gtk.TreeView({ headers_visible: false, vexpand: true, hexpand: true }); parent.tree.set_model(parent.store); parent.scroll.add(parent.tree); parent.col = new Gtk.TreeViewColumn(); parent.tree.append_column(parent.col); let text1 = new Gtk.CellRendererText(); parent.col.pack_start(text1, true); parent.col.set_cell_data_func(text1, (col, cell, model, iter) => { cellFuncText1(col, cell, model, iter); }); let text2 = new Gtk.CellRendererText(); parent.col.pack_start(text2, true); parent.col.set_cell_data_func(text2, (col, cell, model, iter) => { cellFuncText2(col, cell, model, iter); }); return parent.scroll; }; ... rightFrame.add(getBody(this));
Результат:
Т.к. gjs- это не nodejs, то для работы с файлами применяется своя библиотека - GLib, и похоже, функции сюда перекочевали из php, например, для синхронного чтения файла существует функция file_get_contents.
Давайте напишим парсилку cpuinfo, а затем в колонки раскидаем инфу о частоте процессора:
function parseCPUInfo(text) { let result = []; let lines = text.toString().split(' '); let curProcessor = 0; for(let line of lines) { let parts = line.split(':'); if( parts.length <= 1 ) continue; let key = parts[ 0 ].trim(); let val = parts[ 1 ].trim(); if( key === 'processor' ) curProcessor = parseInt(val); if( !result[ curProcessor ] ) result[ curProcessor ] = {}; result[ curProcessor ][ key ] = val; } return result; } ... function getBody(parent, cpuInfo) { parent.scroll = new Gtk.ScrolledWindow({ vexpand: true }); parent.store = new Gtk.ListStore(); parent.store.set_column_types([GObj.TYPE_INT, GObj.TYPE_STRING, GObj.TYPE_STRING, GObj.TYPE_BOOLEAN]); for(let index=0; index<cpuInfo.length; index++) parent.store.set(parent.store.append(), [0, 1, 2, 3], [index, `C${cpuInfo[index].processor}`, cpuInfo[index]['cpu MHz'], false]); parent.tree = new Gtk.TreeView({ headers_visible: true, vexpand: true, hexpand: true }); parent.tree.set_model(parent.store); parent.scroll.add(parent.tree); parent.col = new Gtk.TreeViewColumn({ title: 'CPU' }); parent.col2 = new Gtk.TreeViewColumn({ title: 'MHz' }); parent.tree.append_column(parent.col); parent.tree.append_column(parent.col2); parent.text1 = new Gtk.CellRendererText(); parent.col.pack_start(parent.text1, true); parent.col.set_cell_data_func(parent.text1, (col, cell, model, iter) => { cellFuncText1(col, cell, model, iter, cpuInfo); }); parent.text2 = new Gtk.CellRendererText(); parent.col2.pack_start(parent.text2, true); parent.col2.set_cell_data_func(parent.text2, (col, cell, model, iter) => { cellFuncText2(col, cell, model, iter, cpuInfo); }); return parent.scroll; };
Результат:
Ещё нам нужно как-то обновлять данные в уже созданных ячейках. Для этого есть функция ListStore.foreach(), которая последовательно пройдёт по всем строкам и вызовет функцию с параметрами (model, box, iter)
function updateCells(parent, cpuInfo) { parent.store.foreach(function(model, box, iter) { let index = model.get_value(iter, 0); parent.store.set(iter, [1, 2], [`C${cpuInfo[index].processor}`, cpuInfo[index]['cpu MHz']]); }); }
У нас стало много кода. Давайте разобьём проект на модули, которые сложим в в отдельный коталог "modules".
В отличии от nodejs, в экспорт попадают все переменные и функции, но остаются они в своём неймспейсе, не нужно юзать module.exports.
// ./modules/cpuinfo.js const GLib = imports.gi.GLib; const procPath = '/proc/cpuinfo'; function parseCPUInfo(text) { let result = []; let lines = text.toString().split(' '); let curProcessor = 0; for(let line of lines) { let parts = line.split(':'); if( parts.length <= 1 ) continue; let key = parts[ 0 ].trim(); let val = parts[ 1 ].trim(); if( key === 'processor' ) curProcessor = parseInt(val); if( !result[ curProcessor ] ) result[ curProcessor ] = {}; result[ curProcessor ][ key ] = val; } return result; } function getCPUInfo() { let [ok, contents] = GLib.file_get_contents(procPath); return parseCPUInfo(contents); }
А чтобы подключить модули, нужно сперва указать каталог, где кастомные модули распаложены, и уже потом можно подключить модуль:
imports.searchPath.push('./'); const getCPUInfo = imports.modules.cpuinfo.getCPUInfo;
getBody и всё что с ним связано тоже уносим в отдельный модуль. В котором мы будем использовать getCPUInfo. И вот уже внутри сабмодуля, чтобы подключить свой модуль, ещё раз указывать imports.searchPath.push('./'); не нужно, работает родительская декларация:
Теперь наш app.js будет такой:
#!/usr/bin/gjs const Gtk = imports.gi.Gtk; const GLib = imports.gi.GLib; const GObj = imports.gi.GObject; imports.searchPath.push('./'); const getCPUInfo = imports.modules.cpuinfo.getCPUInfo; const getBody = imports.modules.getBody.getBody; let app = new Gtk.Application({ application_id: 'org.gtk.advancedHello' }); app.connect('activate', () => { log('App started'); let mainWin = new Gtk.ApplicationWindow({ application: app, window_position: Gtk.WindowPosition.CENTER, title: 'Advanced Hello World on gjs' }); let container = new Gtk.Box({}); let leftFrame = new Gtk.Frame({}); let rightFrame = new Gtk.Frame({}); container.add(leftFrame); container.add(rightFrame); leftFrame.set_size_request(500, 300); rightFrame.set_size_request(250, 300); rightFrame.add(getBody(this)); mainWin.add(container); mainWin.show_all(); }); app.run([]);
А вот нет этих функций. Их придётся реализовать самим, а точнее ещё раз честно спиздить с примеров: https://github.com/optimisme/gjs-examples/blob/master/assets/timers.js.
Теперь нужно повесить таймер, по которому раз в секунду будем обновлять инфу в табличке.
... const setInterval = imports.modules.timers.setInterval; ... app.connect('activate', () => { ... let updateInterval = setInterval( () => { let cpuInfo = getCPUInfo(); updateCells(this, cpuInfo); }, 1000); ... });
И происходит чудо!
Наступает самые интересный и сложный момент. Сложный потому, что я не имею понятия как работать с графикой, интересный, потому, что графика- это всегда интересно.
После нескольких часов гугления и разбора примеров, я решил, что буду использовать cairo, просто потому, что нашлось хотя бы 2.5 примера использования его именно с gjs, а не чистым gtk.
Сперва переведём из C++ в gjs этот пример:
// C++ public static int main (string[] args) { // Create a context: Cairo.ImageSurface surface = new Cairo.ImageSurface (Cairo.Format.ARGB32, 80, 80); Cairo.Context context = new Cairo.Context (surface); // Draw a rectangle: context.rectangle (10, 10, 50, 50); context.set_source_rgba (0, 0, 0, 1); context.set_line_width (8); context.fill (); // Change some style settings inside save/restore: context.set_operator (Cairo.Operator.CLEAR); context.set_source_rgba (1, 0, 0, 1); context.rectangle (20, 20, 50, 50); context.fill (); // Save the image: surface.write_to_png ("img.png"); return 0; } // Gjs const Gdk = imports.gi.Gdk; const cairo = imports.cairo; function main() { let surface = new cairo.ImageSurface (cairo.Format.ARGB32, 80, 80); let context = new cairo.Context (surface); context.rectangle (10, 10, 50, 50); context.setSourceRGBA (0, 0, 0, 1); context.setLineWidth (8); context.fill (); context.setOperator (cairo.Operator.CLEAR); context.setSourceRGBA (1, 0, 0, 1); context.rectangle (20, 20, 50, 50); context.fill (); // Save the image: surface.writeToPNG ("img.png"); return false; } main();
Программа создасть маленькую пнгшечку "img.png":
Т.к. нам нужно выводить изображение не в файл, а в окно приложения, то cairo контекст нужно создать по другому:
let context = Gdk.cairo_create(drawing_area.get_window()); // drawing_area - это какой-либо GTK UI элемент, в моём случае это let drawArea = new Gtk.DrawingArea(); drawArea.connect('draw', chart.drawChart); //которую я вешаю на leftFrame.add(drawArea);
И ещё немного покурив доку находим всё необходимое. Контекст очищается с помощью такой конструкции:
context.operator = cairo.Operator.CLEAR; context.paint(); context.operator = cairo.Operator.OVER;
Цвет линии устанавливается оператором context.setSourceRGBA(r, g, b,a), а линия рисуется вот так:
context.setLineWidth (1); context.moveTo(xStart, yStart); context.lineTo(x, y ); context.stroke();
Теперь остаётся делом техники собрать всё воедино:
// ./modules/chart.js const Gdk = imports.gi.Gdk; const cairo = imports.cairo; const colors = [ [190, 0, 0, 0.5], [0, 190, 0, 0.5], [0, 0, 190, 0.5], [190, 0, 190, 0.5], [190, 190, 0, 0.5], [0, 190, 190, 0.5], [255, 0, 0, 0.5], [0, 255, 0, 0.5], [0, 0, 255, 0.5], [128, 0, 255, 0.5], [0, 128, 255, 0.5], [255, 128, 0, 0.5], [255, 0, 128, 0.5], [0, 255, 128, 0.5], [128, 255, 0, 0.5], [128, 255, 128, 0.5], ]; function cleanSurface(drawArea) { let surface = drawArea.get_window(); let context = Gdk.cairo_create(surface); context.operator = cairo.Operator.CLEAR; context.paint(); context.operator = cairo.Operator.OVER; } function getMaxMinVal(chartStore, maxWidth) { let maxVal = 0; let minVal = 9999999; for(let index=(chartStore.length-1); (index>=0 && (index>=(chartStore.length-maxWidth))); index--) { for(let cpuIndex=0; cpuIndex<chartStore[index].length; cpuIndex++) { if( chartStore[ index ][ cpuIndex ][ 'cpu MHz' ] > maxVal ) maxVal = chartStore[ index ][ cpuIndex ][ 'cpu MHz' ]; if( chartStore[ index ][ cpuIndex ][ 'cpu MHz' ] < minVal ) minVal = chartStore[ index ][ cpuIndex ][ 'cpu MHz' ]; } } return [ maxVal, minVal ]; } function calcHeight(val, maxVal, minVal, surfaceHeight) { let realLength = val - minVal; let realMaxLength = maxVal - minVal; return surfaceHeight - parseInt( (realLength/realMaxLength) * surfaceHeight ); } function drawChart(drawArea, parentFrame, chartStore, cpuCount) { if( chartStore.length <2 ) return; let surface = drawArea.get_window(); let context = Gdk.cairo_create(surface); let [ width, height ] = parentFrame.get_size_request(); context.operator = cairo.Operator.CLEAR; context.paint(); context.operator = cairo.Operator.OVER; let [ maxVal, minVal ] = getMaxMinVal(chartStore, width); context.setLineWidth (1); let colorIndex = 0; for(let cpuIndex=0; cpuIndex<cpuCount; cpuIndex++) { context.setSourceRGBA (...colors[ colorIndex ]); let startVal = chartStore[ chartStore.length-1 ][ cpuIndex ]['cpu MHz']; context.moveTo(width - 1, calcHeight(startVal, maxVal, minVal, height)); let xOffset = 1; for(let index=chartStore.length-1; ( (index>=0) && (index>(chartStore.length-width)) ); index--) { let val = chartStore[ index ][ cpuIndex ]['cpu MHz']; let pHeight = calcHeight(val, maxVal, minVal, height); context.lineTo(width - xOffset, pHeight ); xOffset ++; } context.stroke(); colorIndex ++; if( colorIndex >= colors.length ) colorIndex = 0; } return false; }
Результат:
ЧИтается всё это быстро, но на самом деле времени чтобы разобраться в каждой мелочи я потратил много, всё таки без знания основ gtk трудно взять и начать кодить, впрочем, так с любым фремворком, хоть с реактом, хоть с вуём.
Да и получился какой-то уж очень продвинутый hello world, и, наверное, я его буду постепенно допиливать до совсем красивого состояния.
Исходники лежать тут: https://gitlab.com/hololoev/gjs_cpufreqinfo
gtk + javascript = gjs, делаем "hello world"
Давно хотел посмотреть какой-нибудь гуёвый фреймворк, а тут оказалось, что под gtk можно писать на javascripte. Мне показалось это на много интереснее чем electron.
Новый шаблон Hapi.js REST сервера
Я уже фигачил пачку статей про REST сервер на hapi. Но с тех пор прошло много времени. Обновились модули, обновился мой опыт. Вот на основе нового опыта я и собрал новый шаблон, с ещё более переиспользуемым кодом и с обновлёнными модулями.