Как стать автором
Обновить

Создатель динамических обоев на языке Vala

Время на прочтение10 мин
Количество просмотров2.3K

Привет! Для создания динамических обоев в дистрибутивах GNU/Linux в большинстве случаев применяются специальные xml-файлы. Я решил создать программу, которая генерирует такой файл. Конечно, для этого есть готовые скрипты или даже можно вручную создать такой файл, но куда удобнее работать в программе с графическим интерфейсом. Здесь я тоже не первый, так как такие программы уже имеются в репозиториях, но почему бы не написать свой вариант?

Небольшой обзор файла

Программа должна создавать файл с заданными пользователем параметрами. Уже готовый файл пользователь указывает в настройках в качестве обоев. В среде GNOME для этого понадобится приложение GNOME Tweaks. Вот пример готового файла:

<background>
  <starttime>
    <year>2021</year>
    <month>03</month>
    <day>15</day>
    <hour>14</hour>
    <minute>0</minute>
    <second>0</second>
  </starttime>
<static>
    <duration>3595.0</duration>
    <file>/home/alex/DW_MACOSX/mojave_dynamic_1.jpeg</file>
  </static>
  <transition>
    <duration>5.0</duration>
    <from>/home/alex/DW_MACOSX/mojave_dynamic_1.jpeg</from>
<to>/home/alex/DW_MACOSX/mojave_dynamic_2.jpeg</to>
  </transition>
  <static>
    <duration>3595.0</duration>
    <file>/home/alex/DW_MACOSX/mojave_dynamic_2.jpeg</file>
  </static>
  <transition>
    <duration>5.0</duration>
    <from>/home/alex/DW_MACOSX/mojave_dynamic_2.jpeg</from>
<to>/home/alex/DW_MACOSX/mojave_dynamic_3.jpeg</to>
  </transition>
  <static>
    <duration>3595.0</duration>
    <file>/home/alex/DW_MACOSX/mojave_dynamic_3.jpeg</file>
  </static>
  <transition>
    <duration>5.0</duration>
    <from>/home/alex/DW_MACOSX/mojave_dynamic_3.jpeg</from>
<to>/home/alex/DW_MACOSX/mojave_dynamic_4.jpeg</to>
  </transition>
  <static>
    <duration>3595.0</duration>
    <file>/home/alex/DW_MACOSX/mojave_dynamic_4.jpeg</file>
  </static>
  <transition>
    <duration>5.0</duration>
    <from>/home/alex/DW_MACOSX/mojave_dynamic_4.jpeg</from>
<to>/home/alex/DW_MACOSX/mojave_dynamic_1.jpeg</to>
  </transition>
</background>

Как видим файл совсем не сложный. В starttime указаны дата и время начала цикла показа обоев. Тег static содержит время показа отдельного изображения и путь к нему. В теге transition указано время перехода от одного изображения к другому и пути к соответствующим изображениям. Время указывается в секундах.

Используем GNOME Builder

Решил попробовать поработать в GNOME Builder. Устанавливать лучше flatpak-версию из центра приложений или с сайта Flathub. Среда сама должна скачать необходимые Sdk и платформы. У классической deb-версии мною были замечены проблемы с загрузкой платформ и инструментов, также могут быть проблемы с запуском проекта. И еще я почему-то не нашел в этой версии конструктор интерфейсов, но может быть в старой версии его нет. У меня Debian 10, стабильная ветка, соответственно пакет среды будет довольно древний, а flatpak-пакет должен быть всегда самый новый.

Создаем приложение GNOME на главной странице. Вводим название проекта, выбираем язык и лицензию. После того как откроется проект, нас должны интересовать только два файла. Это window.vala и window.ui. В первом будет находится вся логика приложения, а во втором описание графического интерфейса. Есть еще main.vala, но вносить в него какие-либо изменения нам не потребуется. Вот его содержимое:

int main (string[] args) {
	var app = new Gtk.Application ("org.example.App", ApplicationFlags.FLAGS_NONE);
	app.activate.connect (() => {
		var win = app.active_window;
		if (win == null) {
			win = new Dwxmlcreator.Window (app);
		}
		win.present ();
	});

	return app.run (args);
}

GUI

Интерфейс приложения делал во встроенном дизайнере. Приводить содержимое ui-файла я не буду, так как он довольно монструозный и занимает больше шестисот строк. В конце поста будут ссылки на репозитории приложения в сервисах GitHub и SourceForge.

В программе есть три страницы. На первой странице пользователь вводит дату и время начала показа обоев. На второй странице добавляет изображения для показа и на третьей вводит данные для сохранения готового xml-файла. Вот первая страница:

Вторая страница:

И третья страница:

Интерфейс немного не каноничный, если говорить о расположении текстовых меток, но выглядит, по моему мнению, довольно симпатично.

Логика

Первые строчки

Здесь все как обычно. Объявляем необходимые объекты, компоненты интерфейса и переменные. Добавляем в текстовые поля значки выбора файлов. Связываем кнопки с соответствующими методами и прописываем логику автоматического заполнения полей для удобства пользователя.

namespace Dwxmlcreator {
	[GtkTemplate (ui = "/org/example/App/window.ui")]
	public class Window : Gtk.ApplicationWindow {
		[GtkChild]
		Gtk.Stack stack;
		[GtkChild]
		Gtk.Box start_time_box;
        [GtkChild]
        Gtk.Box add_box;
        [GtkChild]
        Gtk.Box create_xml_box;//id в ui-файле
        [GtkChild]
        Gtk.Button add_start_time_button;
        [GtkChild]
        Gtk.Button add_button;
        [GtkChild]
        Gtk.Button add_image_button;
        [GtkChild]
        Gtk.Button create_button;
        [GtkChild]
        Gtk.Button back_button;
        [GtkChild]
        Gtk.Entry path_to_image;
        [GtkChild]
        Gtk.Entry transition_duration;
        [GtkChild]
        Gtk.Entry static_duration;
        [GtkChild]
        Gtk.Entry path_to_xml_directory;
        [GtkChild]
        Gtk.Entry xml_name;
        [GtkChild]
        Gtk.Entry day;
        [GtkChild]
        Gtk.Entry month;
        [GtkChild]
        Gtk.Entry year;
        [GtkChild]
        Gtk.Entry hours;
        [GtkChild]
        Gtk.Entry minutes;
        [GtkChild]
        Gtk.Entry seconds;
        [GtkChild]
        Gtk.Label image_counter;

        StringBuilder builder;

        string last_folder;
        string main_part;
        string start_time;
        string first_image;
        string image;

        int counter = 0;
        int duration;

		public Window (Gtk.Application app) {
			Object (application: app);
			path_to_image.set_icon_from_icon_name (Gtk.EntryIconPosition.SECONDARY, "document-open-symbolic");
			path_to_image.icon_press.connect ((pos, event) => {
        if (pos == Gtk.EntryIconPosition.SECONDARY) {
              on_path_to_image();//выбор изображения
           }
        });
        path_to_xml_directory.set_icon_from_icon_name (Gtk.EntryIconPosition.SECONDARY, "document-open-symbolic");
			path_to_xml_directory.icon_press.connect ((pos, event) => {
        if (pos == Gtk.EntryIconPosition.SECONDARY) {
              on_path_to_xml_directory();//выбор директории сохранения
           }
          });
			add_start_time_button.clicked.connect(add_start_time);
			add_image_button.clicked.connect(add_image);
			add_button.clicked.connect(go_to_create_xml_page);
			create_button.clicked.connect(create_xml);
			back_button.clicked.connect(go_to_back);
			set_widget_visible(back_button, false);//скрываем кнопку "назад" в хидербаре
			builder = new StringBuilder();
			var date_time = new DateTime.now_local();
			day.set_text(date_time.format("%d"));//день
			month.set_text(date_time.format("%m"));//месяц
			year.set_text(date_time.format("%Y"));//год
            if(int.parse(date_time.format("%M"))>=55){//если кол-во минут больше или равно 55-и
                hours.set_text((int.parse(date_time.format("%H"))+1).to_string());//увеличиваем часы на единицу
            }else{
                hours.set_text(date_time.format("%H"));//показываем часы
            }
		}

Автоматически заполняются только все три поля даты и поле ввода часов. Количество часов увеличивается на единицу, если количество минут больше или равно 55-и. Зачем это? Мне показалось, что если не спешить, то за пять минут можно не успеть составить более-менее вменяемый по объему документ. В любом случае, решать вам. Можете закомментировать или вообще удалить эти строки.

Кнопка "назад"

В хидербаре только одна кнопка. Кнопка "назад", которая должна появляться только на второй странице, а при возврате на первую исчезать. При нажатии на кнопку срабатывает следующий метод:

private void go_to_back(){
		    if (stack.get_visible_child_name()=="page1"){
                stack.visible_child = start_time_box;//page0
                set_widget_visible(back_button, false);
		    }else{
		        stack.visible_child = add_box;
		    }
		}

Определяем на какой странице находимся и если это страница с именем "page1", то есть вторая страница, то показываем первую (page0). Если условие не выполняется, то значит мы на третьей странице и идем на вторую. На первой странице кнопки нет.

Время старта

За создание первой части нашего файла отвечает метод add_start_time. Вот он:

private void add_start_time(){
		if(is_empty(year.get_text())||is_empty(month.get_text())||is_empty(day.get_text())||
		is_empty(hours.get_text())||is_empty(minutes.get_text())||is_empty(seconds.get_text())){
            alert("Enter correct data in all fields!");
            return;
		}
           start_time ="<background>
  <starttime>
    <year>"+year.get_text()+"</year>
    <month>"+month.get_text()+"</month>
    <day>"+day.get_text()+"</day>
    <hour>"+hours.get_text()+"</hour>
    <minute>"+minutes.get_text()+"</minute>
    <second>"+seconds.get_text()+"</second>
  </starttime>\n";
		    stack.visible_child = add_box;//идем на вторую страницу
		    set_widget_visible(back_button, true);//делаем видимой кнопку в хидербаре
		}

После проверки всех шести полей на пустоту записываем в переменную start_time содержимое этих полей с необходимыми тегами в начале и конце. Далее, переходим на вторую страницу, не забыв сделать кнопку возвращения в хидербаре видимой.

Добавление изображений

Основная часть документа конструируется с помощью метода add_image:

private void add_image(){
          if(is_empty(path_to_image.get_text())||is_empty(transition_duration.get_text())||
          is_empty(static_duration.get_text())){
              alert("Enter correct data in all fields!");
              return;
          }
          counter++;//счетчик для определения первого и последующих нажатий
          duration = int.parse(static_duration.get_text())*60-int.parse(transition_duration.get_text());
          if(counter != 1){//если не первое нажатие
             image = path_to_image.get_text();
             main_part ="<to>"+image+"</to>
  </transition>
  <static>
    <duration>"+duration.to_string()+".0"+"</duration>
    <file>"+image+"</file>
  </static>
  <transition>
    <duration>"+transition_duration.get_text()+".0"+"</duration>
    <from>"+image+"</from>\n";
    }else{//если первое нажатие
    first_image = path_to_image.get_text();//путь до первого изображения
             main_part = "<static>
    <duration>"+duration.to_string()+".0"+"</duration>
    <file>"+first_image+"</file>
  </static>
  <transition>
    <duration>"+transition_duration.get_text()+".0"+"</duration>
    <from>"+first_image+"</from>\n";
    }
  image_counter.set_text(counter.to_string()+" images were added");//отображаем счетчик в текстовой метке
  path_to_image.set_text("");//очищаем поле
  builder.append(main_part);//добавляем в строковый билдер основную часть
   }

Если пользователь добавляет первое изображение, то путь до него записывается в отдельную переменную first_image. Она понадобится потом для зацикливания показа обоев. Пути до остальных изображений записываются в переменную image.

На последнюю страницу

Когда пользователь добавит все нужные ему изображения, он может перейти к следующей, последней странице. За это отвечает метод go_to_create_xml_page:

private void go_to_create_xml_page(){
		  if(counter < 2){
		      alert("Add images");
		      return;
		  }
		 stack.visible_child = create_xml_box;
		 path_to_xml_directory.set_text(Environment.get_home_dir());
		 xml_name.set_text("dynamic_wallpaper_"+Random.int_range(100,10000).to_string());
		}

Переход возможен, если добавлено как минимум два изображения. После перехода в поле выбора директории будет отображаться путь до вашей домашней папки. В поле имени будет предложен некоторый вариант имени файла, состоящий из фразы "dynamic_wallpaper" и рандомного числа от 100 до 10000.

Создание документа

Метод creat_xml завершает создание файла. Он дописывает недостающие строчки, собирает все части документа в одну кучу и создает файл в указанном месте.

private void create_xml(){
           if(builder.str==""){
               alert("Nothing to create");
               return;
           }
           if(is_empty(path_to_xml_directory.get_text())||is_empty(xml_name.get_text())){
               alert("Enter correct data in all fields!");
               return;
           }
           string end_xml = "<to>"+first_image+"</to>
  </transition>
</background>";//зацикливаем показ, добавив путь до первого изображения и создаем конец документа
           builder.append(end_xml);//добавляем конец к основной части
           builder.insert(0, start_time);//вставляем начало
           GLib.File file = GLib.File.new_for_path(path_to_xml_directory.get_text()+"/"+xml_name.get_text()+".xml");
            try {
              FileUtils.set_contents (file.get_path(), builder.str);//создаем файл
           } catch (Error e) {
            stderr.printf ("Error: %s\n", e.message);
          }
            if(file.query_exists()){
                alert("File created successfully");
            }else{
                alert("An unknown error has occurred! Failed to create file");
            }
            counter = 0;
            builder = new StringBuilder();
            xml_name.set_text("");
            image_counter.set_text("Add images");
        }

Выбор изображения

Любая программа должна быть удобной. По возможности. В случае с диалогом выбора изображений, помимо фильтра, в нем присутствует отображение предпросмотра изображений и запоминание последней открытой папки. Без этих удобств пользователю приходилось бы каждый раз при открытии диалога переходить в папку изображений и при выборе ориентироваться на имена файлов и мелкие миниатюры изображений в списке.

private void on_path_to_image(){
		     var file_chooser = new Gtk.FileChooserDialog ("Select image file", this, Gtk.FileChooserAction.OPEN, "_Cancel", Gtk.ResponseType.CANCEL, "_Open", Gtk.ResponseType.ACCEPT);
	    Gtk.FileFilter filter = new Gtk.FileFilter ();
		file_chooser.set_filter (filter);//добавляем фильтр
		filter.add_mime_type ("image/jpeg");
        filter.add_mime_type ("image/png");
        Gtk.Image preview_area = new Gtk.Image ();
		file_chooser.set_preview_widget (preview_area);//устанавливаем область для предпросмотра
		file_chooser.update_preview.connect (() => {
			string uri = file_chooser.get_preview_uri ();
			string path = file_chooser.get_preview_filename();
			if (uri != null && uri.has_prefix ("file://") == true) {
				try {
					Gdk.Pixbuf pixbuf = new Gdk.Pixbuf.from_file_at_scale (path, 250, 250, true);
					preview_area.set_from_pixbuf (pixbuf);
					preview_area.show ();
				} catch (Error e) {
					preview_area.hide ();
				}
			} else {
				preview_area.hide ();
			}
		});
		if (last_folder != null) {
            file_chooser.set_current_folder (last_folder);//открываем последнюю папку
        }
        if (file_chooser.run () == Gtk.ResponseType.ACCEPT) {
            last_folder = file_chooser.get_current_folder ();//запоминаем текущую папку
            path_to_image.set_text(file_chooser.get_filename());
        }
        file_chooser.destroy ();
		}

Остальные методы

Диалог выбора папки для сохранения файла:

private void on_path_to_xml_directory(){
             var file_chooser = new Gtk.FileChooserDialog ("Choose a directory", this, Gtk.FileChooserAction.SELECT_FOLDER, "_Cancel", Gtk.ResponseType.CANCEL, "_Open", Gtk.ResponseType.ACCEPT);
        if (file_chooser.run () == Gtk.ResponseType.ACCEPT) {
            path_to_xml_directory.set_text(file_chooser.get_filename());
        }
        file_chooser.destroy ();
		}

Скрыть или показать какой-либо виджет (для кнопки "назад"):

private void set_widget_visible (Gtk.Widget widget, bool visible) {
         widget.no_show_all = !visible;
         widget.visible = visible;
       }

Проверить текстовое поле на пустоту:

private bool is_empty(string str){
        return str.strip().length == 0;
        }

Показать сообщение пользователю:

private void alert (string str){
          var dialog_alert = new Gtk.MessageDialog(this, Gtk.DialogFlags.MODAL, Gtk.MessageType.INFO, Gtk.ButtonsType.OK, str);
          dialog_alert.set_title("Message");
          dialog_alert.run();
          dialog_alert.destroy();
       }

Сборка

Для сборки приложения нужно нажать Export Bundle в окне, где отображается текущий статус проекта. Чтобы кнопка стала активной надо хотя бы раз запустить приложение. По умолчанию собираются flatpak-пакеты. В консоли после удачной сборки будет указан путь до готового файла. Также должен запуститься файловый менеджер, в котором будет открыта указанная в консоли директория. Если вам нужен исполняемый файл, то его следует искать там же в директории bin.

Ссылка на GitHub: https://github.com/alexkdeveloper/dwxmlcreator

Ссылка на SourceForge: https://sourceforge.net/projects/dwxmlcreator/

Всем спасибо за внимание! До встречи в следующих постах!

Теги:
Хабы:
Всего голосов 4: ↑4 и ↓0+4
Комментарии3

Публикации

Истории

Ближайшие события

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань