Об Ultimate++ ( https://www.ultimatepp.org/ ) я узнал совершенно случайно, когда прочитал в айтишной новостной RSS-ленте об очередном обновлении этого фреймворка. Что ж, подумал я, раз мои пен-проекты пока в заторможенном состоянии, почему бы не попробовать что-то новое.
Ultimate++ (сокращённо U++ или UPP) представляет собой кроссплатформенный фреймворк для разработки GUI-приложений на основе C++. Кроме кода самого фреймворка в состав входит собственный сборщик BLITZ и среда разработки TheIDE для редактирования кода и элементов GUI.
Попробуем написать простейший блокнот на основе U++.
Скачиваем с сайта zip-архив с фрейворком, распаковываем в любую удобную директорию и запускаем среду разработки TheIDE.
Первое, что предлагается средой — выбрать пакет (package) или создать новый пакет приложения. Есть ещё вариант — создать новую коллекцию (assembly) пакетов. Кроме того, можно выбрать существующие коллекции. Из существующих коллекций доступны различные примеры.
Итак, создадим новую коллекцию. Для этого в поле коллекций нужно вызвать контекстное меню и выбрать пункт «New assembly» (Новая коллекция). В открывшемся окне имеется три строчных поля ввода: «Package nests» (Гнёзда пакетов), «Output directory» (Директория вывода), «Assembly name» (Имя коллекции). В «Package nests» уже внесённые пути к пакетам лучше не удалять. А вот добавить свой путь необходимо. Вызываем редактор гнёзд пакетов, нажав на кнопку с многоточием справа от поля ввода. В открывшемся окне вносим удобный путь к своим файлам, перемещаем кнопками со стрелками этот путь на первое место и нажимаем «ОК». В «Output directory», в свою очередь, вносим удобный путь для выходных файлов после компиляции и линковки. И пишем в «Assembly name» имя, которым коллекция будет представлена в списке коллекций. Всё, жмём «ОК».
Теперь создадим новый пакет. Для этого нажимаем кнопку «New package», расположенную в нижнем правом углу основного окна TheIDE.
В открывшемся диалоговом окне есть несколько вариантов создания пакета. Выбираем «U++ CtrlLib application with main window», с использованием библиотеки CtrlLib, куда, помимо прочего, входят интересующие нас экранные виджеты, и лэйаутом главного окна. Вводим в соответствующее поле имя пакета и нажимаем «OK».
После этого открывается основное окно TheIDE.
Слева сверху расположено окно, где отображаются подключённые к пакету другие пакеты. Слева снизу — окно со сгенерированными только что файлами нашего пакета. Справа показывается структура текущего файла пакета. По центру — редактор кода.
Всего создано три файла:
UppNotepad2.h
,
main.cpp
и
UppNotepad2.lay
. Предупрежу вопросы, почему «2»: с первой версией блокнота я ошибся, и при выборе типа пакета указал простое приложение без главного окна, и файл *.lay не был создан, а без него нам не обойтись.
С первыми двумя файлами всё понятно, это исходный код нашего блокнота. Третий файл, UppNotepad2.lay – лэйаут главного окна приложения. Рассмотрим каждый из них подробно.
#ifndef _UppNotepad2_UppNotepad2_h #define _UppNotepad2_UppNotepad2_h #include <CtrlLib/CtrlLib.h> using namespace Upp; #define LAYOUTFILE <UppNotepad2/UppNotepad2.lay> #include <CtrlCore/lay.h> class UppNotepad2 : public WithUppNotepad2Layout<TopWindow> { public: typedef UppNotepad2 CLASSNAME; UppNotepad2(); }; #endif
#include "UppNotepad2.h" UppNotepad2::UppNotepad2() { CtrlLayout(*this); } GUI_APP_MAIN { UppNotepad2().Run(); }
Этот код генерируется автоматически и в редактировании не нуждается.
Для лэйаута существует два режима просмотра: в графическом и текстовом виде.
LAYOUT(ppNotepad2Layout, 200, 100) END_LAYOUT
Есть ещё четвёртый файл, который размещён в директории пакета — UppNotepad2.upp . Этот файл в TheIDE не показывается, он нужен только для сборки пакета.
Начнём разрабатывать наше приложение с лэйаута.
Курсор наводим на серое поле лэйаута и в контекстном меню последовательно выбираем: «Editors – DocEdit». На лэйауте таким образом появляется виджет редактирования текста. Настроим его. Для этого выделим виджет. В средней части внизу появляется окно со свойствами элемента. Справа от типа элемента есть поле ввода для имени виджета. Введём туда «editor».
Также настроим тип и размер шрифта. Для этого щёлкнем по полю ввода "SetFont" и открывшемся диалоговом окне укажем тип шрифта "Monospace", размер 12.
И настроим зависимость расположения элемента от размеров главного окна.
Теперь перейдём к заголовочному файлу.
Объявим элементы главного окна и переменные:
-
MenuBar menu
— строка выпадающих меню;
-
ToolBar tool
— панель инструментов;
-
StatusBar status
— строка состояния;
-
String fileName
— имя редактируемого файла;
-
String loadText
— загруженный текст файла.
Объявим следующие методы для работы с текстом и файлами:
-
void OpenCmd()
- открыть файл;
-
void SaveCmd()
- сохранить файл;
-
void SaveAsCmd()
- сохранить файл как;
-
void ExitCmd()
- закрыть приложение;
-
void CutCmd()
- вырезать текст;
-
void CopyCmd()
- копировать текст;
-
void PasteCmd()
- вставить текст;
-
void UndoCmd()
- отменить последнее действие;
-
void RedoCmd()
- повторить последнее действие.
Также переопределим метод virtual void Close() окна TopWindow для случая, когда нажата кнопка закрытия окна.
И определим методы, формирующие меню на основе библиотечного элемента
Bar
:
-
void FileBar(Bar& bar)
— меню/панель инструментов «Файл»;
-
void EditBar(Bar& bar)
— меню/панель инструментов «Редактировать»;
-
void MainBar(Bar& bar)
— панель инструментов;
-
void MainMenu(Bar& bar)
— выпадающее меню.
class UppNotepad2 : public WithUppNotepad2Layout<TopWindow> { public: typedef UppNotepad2 CLASSNAME; UppNotepad2(); MenuBar menu; ToolBar tool; StatusBar status; String fileName; String loadText; void OpenCmd(); void SaveCmd(); void SaveAsCmd(); void ExitCmd(); void CutCmd(); void CopyCmd(); void PasteCmd(); void UndoCmd(); void RedoCmd(); void FileBar(Bar& bar); void EditBar(Bar& bar); void MainBar(Bar& bar); void MainMenu(Bar& bar); virtual void Close(); };
Перейдём к файлу реализации.
Для открытия файлов служит метод
UppNotepad2::OpenCmd()
. В нём используется диалог
FileSel
для выбора файлов, метод
Type
диалога для настройки отображения файлов с расширением *.txt и метод
Get()
для получения полного пути к файлу. Кроме того, используется утилита
FileIn
для открытия файла на чтение и утилита
LoadFile
для загрузки файла в текстовый редактор.
void UppNotepad2::OpenCmd() { FileSel fs; fs.Type("Текстовые файлы (*.txt)", "*.txt"); if(fs.ExecuteOpen("Открыть файл")) { fileName = fs.Get(); FileIn fi(fileName); if(!fi) { PromptOK("Не могу открыть файл!"); return; } loadText = LoadFile(fileName); editor.Set(loadText); } }
В методах UppNotepad2::SaveCmd() и UppNotepad2::SaveAsCmd() используется утилита SaveFile для сохранения файла.
void UppNotepad2::SaveCmd() { if(!SaveFile(fileName, editor.Get())) PromptOK("Не могу сохранить файл!"); } void UppNotepad2::SaveAsCmd() { FileSel fs; fs.Type("Текстовые файлы (*.txt)", "*.txt"); if(fs.ExecuteSaveAs("Сохранить файл как")) { fileName = fs.Get(); if(!SaveFile(fileName, editor.Get())) PromptOK("Не могу сохранить файл!"); } }
В методе ExitCmd() проверяется, изменён ли текст, и если изменён, пользователю задаётся вопрос, сохранить ли текст. В случае, если пользователь соглашается сохранить текст, вызывается либо SaveCmd() , если файл был ранее открыт, либо SaveAsCmd() , если файл новый.
void UppNotepad2::ExitCmd() { String curText = editor.Get(); if(!(loadText == curText)) { if(PromptOKCancel("Сохранить файл?")) { if(!fileName.IsEmpty()) SaveCmd(); else SaveAsCmd(); } } Break(); } void UppNotepad2::Close() { ExitCmd(); }
Вырезание текста, а также копирование и его вставка, отмена действия и повторение действия выполняются с помощью соответствующих методов виджета DocEdit . В виджете же вшиты горячие клавиши для этих действий.
void UppNotepad2::CutCmd() { editor.Cut(); } void UppNotepad2::CopyCmd() { editor.Copy(); } void UppNotepad2::PasteCmd() { editor.Paste(); } void UppNotepad2::UndoCmd() { editor.Undo(); } void UppNotepad2::RedoCmd() { editor.Redo(); }
Рассмотрим код создания меню.
За добавление пункта выпадающего меню и кнопки панели инструментов отвечает метод
Add()
, причём работает он по разному в зависимости от аргументов. Если указан путь к иконке, как это сделано, например, для команды «Сохранить», то создастся и пункт выпадающего меню, и кнопка на панели инструментов. Если иконка не указана (см. команду «Сохранить как…»), то создаётся только пункт выпадающего меню. Макрос
THISBACK
позволяет подключить соответствующий метод к создаваемому элементу меню. В методе
Help()
вписывается строка контекстной помощи, отображающейся в строке состояния. Метод
Separator()
добавляет разделительную полосу как в выпадающее меню, так и в панель инструментов.
void UppNotepad2::FileBar(Bar& bar) { bar.Add("Открыть", StreamRaster::LoadFileAny("E:\\dev\\upp\\UppNotepad2\\images\\open.png"), THISBACK(OpenCmd)).Help("Открыть файл"); bar.Add("Сохранить", StreamRaster::LoadFileAny("E:\\dev\\upp\\UppNotepad2\\images\\save.png"), THISBACK(SaveCmd)).Help("Сохранить файл"); bar.Add("Сохранить как...", THISBACK(SaveAsCmd)).Help("Сохранить как"); bar.Add("Выйти", THISBACK(ExitCmd)).Help("Выход из программы"); } void UppNotepad2::EditBar(Bar& bar) { bar.Add("Вырезать (Ctrl + X)", StreamRaster::LoadFileAny("E:\\dev\\upp\\UppNotepad2\\images\\cut.png"), THISBACK(CutCmd)).Help("Вырезать выделенный текст в буфер"); bar.Add("Копировать (Ctrl + C)", StreamRaster::LoadFileAny("E:\\dev\\upp\\UppNotepad2\\images\\copy.png"), THISBACK(CopyCmd)).Help("Копировать выделенный текст в буфер"); bar.Add("Вставить (Ctrl + V)", StreamRaster::LoadFileAny("E:\\dev\\upp\\UppNotepad2\\images\\paste.png"), THISBACK(PasteCmd)).Help("Вставить текст из буфера"); bar.Separator(); bar.Add("Отменить (Ctrl + Z)", StreamRaster::LoadFileAny("E:\\dev\\upp\\UppNotepad2\\images\\undo.png"), THISBACK(UndoCmd)).Help("Отменить последнее действие"); bar.Add("Повторить (Ctrl + Shift + Z)", StreamRaster::LoadFileAny("E:\\dev\\upp\\UppNotepad2\\images\\redo.png"), THISBACK(RedoCmd)).Help("Повторить последнее действие"); } void UppNotepad2::MainBar(Bar& bar) { FileBar(bar); bar.Separator(); EditBar(bar); } void UppNotepad2::MainMenu(Bar& bar) { bar.Add("Файл", THISBACK(FileBar)); bar.Add("Редактировать", THISBACK(EditBar)); }
Теперь рассмотрим конструктор UppNotepad2 .
UppNotepad2::UppNotepad2() { // Устанавливаем лэйаут CtrlLayout(*this); loadText = ""; // Устанавливаем цвет текста editor.SetColor(TextCtrl::INK_NORMAL, Color(255, 255, 255)); // Устанавливаем цвет фона editor.SetColor(TextCtrl::PAPER_NORMAL, Color(0, 0, 0)); // Убираем горизонатльную полосу, обозначающую конец файла editor.EofLine(false); // Устанавливаем заголовок окна, указываем, что окно будет изменяемым, с кнопками минимизации и развёртывания окна // и что окно будет развёрнуто максимально Title("Upp Notepad 2").Sizeable().MinimizeBox().MaximizeBox().Maximize(true); // Устанавливаем иконку заголовка окна Icon(StreamRaster::LoadFileAny("E:\\dev\\upp\\UppNotepad2\\images\\editor.png")); // Добавляем в окно выпадающее меню AddFrame(menu); // Добавляем разделитель выпадающего меню и панели инструментов AddFrame(TopSeparatorFrame()); // Добавляем панель инструментов AddFrame(tool); // Добавляем строку состояния AddFrame(status); // Устанавливаем меню menu.Set(THISBACK(MainMenu)); // Перенаправляем вывод Help() в строку состояния menu.WhenHelp = status; // Устанавливаем панель инструментов tool.Set(THISBACK(MainBar)); tool.WhenHelp = status; }
Для сокрытия платформозависимых деталей реализации вместо функции main() используется макрос GUI_APP_MAIN .
GUI_APP_MAIN { UppNotepad2().Run(); }
Вот результат выполнения кода.
Чтобы добавить иконку приложения для рабочего стола, необходимо подключить файл *.rc. Для этого в любом текстовом редакторе пишем следующую строку:
9999 ICON "images/editor.ico"
и сохраняем её в файл UppNotepad.rc в каталоге с исходниками нашего приложения. Далее в поле файлов TheIDE вызываем контекстное меню и выбираем пункт «Insert package directory file(s)» и выбираем только что созданный файл. Это необходимо, чтобы файл *.rc попал в пакет и участвовал в сборке пакета.
Выводы
Достоинства фреймворка:
- не требуется установка дистрибутива, достаточно распаковать zip-архив в подходящую директорию, и все инструменты становятся доступны;
- обширная база примеров;
- простота освоения;
- хорошая документация;
- на выходе получается один исполняемый файл, не требующий никаких зависимостей.
Недостатки:
- на сайте легче воспользоваться поиском по сайту, чем в некоторых случаях переходить по битым ссылкам;
- одно открытое окно TheIDE поддерживает только один открытый головной проект-пакет. Это обходится тем, что можно открыть второй и последующие экземпляры TheIDE. В каком-то смысле так даже удобнее, переключаться между окнами можно с помощью Alt+Tab;
- в редакторе текстов TheIDE не работает автодополнение для ключевых слов и переменных, которые начинаешь вводить с новой строки;
- виджеты GUI можно создать только в лэйауте, через код их создавать нельзя;
- непереносимость кода с машины на машину. (Код редактора я писал на одном компьютере, повторную проверку работоспособности кода проверял на другой машине. В результате «отвалился» rc-файл и выдавал ошибку при компиляции. Это связано с тем, что в файле *.upp был прописан абсолютный путь к файлу. Пришлось подключать повторно.)
Это довольно сильный недостаток. Может всё-таки возможно из кода производить редактирование?
Там наверняка можно задать относительный путь. Как бы это довольно детский недостаток для фреймворка. Должно было быть вылечено ещё на ранних стадиях.
Не смотрели, что там внутри макроса? Там обычный callback на шаблонах?
Не смотрели, как обстоит дело с поддержкой мультиязычности?
По примерам - только так. Но некоторые, сложные виджеты, реализованы как раз через код. После Qt, конечно, тёмный лес. :))
Возможно, что я не той опцией на первой машине воспользовался. Сейчас стоит как относительный путь.
Не смотрел.
Мультиязычность есть, но я подробно не разбирался.
Фигово, что подключение функций реализовано на макросах. Напоминает Qt4 с их SIGNAL и SLOT макросами.