хуйу нас не матерятся
В этот раз попробуем написать что-нить сложнее 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