В этой статье, я покажу вам, несколько с иной стороны, насколько удобна QtE.
Дело в том, что несмотря на то, что в этой замечательной привязке к библиотеке Qt5, есть много элементов графического интерфейса, иногда этого набора не хватает… Хоть это и случается редко, но порой возникает задача, при которой требуется создать какой-то свой графический элемент.
Вот тут то, начинается самое интересное и при этом не трудное!
Когда, я работал с DFL2 и dformlib, я очень часто использовал такой графический элемент, как PictureBox. Этот элемент позволял загружать картинки прямо в окно и даже рисовать на них! Однако, в QtE5, похожего элемента мне не удалось найти, но Qt5 разрешает рисовать практически на любых элементах интерфейса. Именно это, а также практическая необходимость, позволили дойти своим ходом до идеи создания своего собственного PictureBox’а, который понимает кучу форматов изображений (спасибо, Qt5!), в отличие от аналогичного компонента в DFL2/dformlib!
Вообще, создать собственный виджет (именно так тут называются визуальные элементы GUI) достаточно просто: нужно лишь произвести наследование от класса QWidget, задать ряд нужных свойств в конструкторе получившегося класса и написать необходимое поведение, используя «переходники» (блок extern(C), как вы поняли) к обработчикам нужных событий внутри класса.
Итак, осуществляем наследование:
class RPictureBox : QWidget { private { QWidget parent; QImage image; int pictureWidth; int pictureHeight; string fileName; } // конструктор : длина картинки на ширину обязательно должны быть указаны ! this(QWidget parent, int pictureWidth, int pictureHeight) { super(this); this.parent = parent; this.pictureWidth = pictureWidth; this.pictureHeight = pictureHeight; this.image = new QImage(pictureWidth, pictureHeight, QImage.Format.Format_ARGB32_Premultiplied); setStyleSheet("background : white;"); setPaintEvent(&onPaintPictureBox, aThis()); } }
Тут все достаточно просто: в конструкторе класса мы обращаемся сначала к базовому классу (при помощи ключевого слова super), затем сохраняем во внутренние поля класса длину и ширину картинки, а также ссылку на родительский класс (хоть, это мы напрямую не используем, однако, кое-что из этого нам может пригодиться). Аргументы, которые попадают в конструктор класса задают длину и ширину исходной картинки, что в перспективе позволит нам легко манипулировать самыми разными параметрами изображения, однако, сейчас они нам нужны для несколько иных целей…
Для рисования картинки нам потребуется некий объект, в котором мы будем это делать или же объект, который умеет в себе хранить изображения, и выводить требуемую картинку на экран. К счастью, в QtE относительно недавно была добавлена очень умная и прикольная вещь под названием QImage, которая понимает очень много форматов изображении и взаимодействуя с событием перерисовки, эта штучка позволяет выводить в окно практически любые картинки (да еще и с автомасштабированием под окно!).
Используя QImage, мы должны помнить о следующих вещах:
- а) необходимо задать длину и ширину изображения (разумеется, она указывается в пикселах);
- б) изображение, помещается напрямую в оперативную память, и поэтому необходимо практически сразу сконструировать нужный объект и задать в каком формате будет храниться картинка (это задается с помощью QImage.Format, которое является перечислением, заданным внутри класса QImage. Не знаете, какой формат использовать? Используйте QImage.Format.Format_ARGB32_Premultiplied — этого формата практически всегда хватает, т.к. он позволяет хранить картинки в виде RGB-триплетов с дополнительным каналом прозрачности).
Учитывая выше перечисленное, мы создаем внутренний объект класса QImage и помещаем в него новый объект этого класса, который позже задействуем внутри события перерисовки.
Дальнейшие инструкции внутри конструктора вполне банальны: с помощью setStyleSheet задается QSS стиль (напоминаю, что такие стили мало чем отличаются от стандартных CSS стилей), а затем устанавливается связь между событием перерисовки виджета и «переходником» на него (метод setPaintEvent базового класса). При этом, не забываем про сам «переходник», код которого выглядит примерно так:
// переходник к обработчику события отрисовки extern(C) { void onPaintPictureBox(RPictureBox* pictureBox, void* eventPointer, void* painterPointer) { (*pictureBox).runPaint(eventPointer, painterPointer); } }
Код «переходника» обязывает нас добавить в класс RPictureBox метод runPaint с сигнатурой:
void runPaint(void* eventPointer, void* painterPointer)
Однако, про этот метод я расскажу несколько позднее, поскольку я хотел бы сейчас обратить ваше внимание на одно из внутренних приватных полей нашего класса. Как вы уже поняли, я говорю про поле fileName, которое позволит нам хранить имя файла картинки и использовать его для смены изображения в импровизированном PictureBox’е, однако, это поле помечено модификатором private, что делает поле недоступным для простого присвоения. Следовательно, чтобы не нарушать инкапсуляции и всегда иметь доступ к значению fileName, добавим два метода в класс — сеттер (метод, который будет устанавливать значение для переменной fileName) и геттер (метод, который всегда позволит нам получать значение переменной fileName):
// имя файла для отображения (сеттер) void setFileName(string fileName) { this.fileName = fileName; if (fileName.empty || !fileName.exists) { image.fill(QtE.GlobalColor.white); } else { image.load(fileName); } } // получить имя файла (геттер) string getFileName() { return fileName; }
Работают эти методы просто: setFileName устанавливает значение внутренней переменной fileName и проводит при этом ряд проверок: если имя файла вдруг окажется пустым (fileName.empty) или же файл с заданным именем не существует (!fileName.exists), то объект image типа QImage будет просто заполнен некоторым цветом (в данном случае, это белый цвет, что следует из использования в методе fill перечисления QtE.GlobalColor со значением QtE.GlobalColor.White); иначе будет загружено интересующее нас изображение. Метод getFileName просто возвращает текущее значение, которое содержится в поле fileName.
После этого, можно спокойно перейти к реализации метода runPaint, который работает на удивление просто — получает указатель на текущий отрисовщик (QPainter), создает из него соответствующий объект и создавая новую прямоугольную область (а в ней — новый прямоугольник), рисует в ней наше изображение:
// событие отрисовки void runPaint(void* eventPointer, void* painterPointer) { QPainter painter; with (painter = new QPainter('+', painterPointer)) { drawImage(contentsRect(new QRect), image); end; } }
Теперь, осталось все это испытать и для этого нам потребуется создать свое окно, поместить в него RPictureBox:
// Область с картинкой class RPictureBox : QWidget { private { QWidget parent; QImage image; int pictureWidth; int pictureHeight; string fileName; } // конструктор : длина картинки на ширину обязательно должны быть указаны ! this(QWidget parent, int pictureWidth, int pictureHeight) { super(this); this.parent = parent; this.pictureWidth = pictureWidth; this.pictureHeight = pictureHeight; this.image = new QImage(pictureWidth, pictureHeight, QImage.Format.Format_ARGB32_Premultiplied); setStyleSheet("background : white;"); setPaintEvent(&onPaintPictureBox, aThis()); } // имя файла для отображения void setFileName(string fileName) { this.fileName = fileName; if (fileName.empty || !fileName.exists) { image.fill(QtE.GlobalColor.white); } else { image.load(fileName); } } // получить имя файла string getFileName() { return fileName; } // событие отрисовки void runPaint(void* eventPointer, void* painterPointer) { QPainter painter; with (painter = new QPainter('+', painterPointer)) { drawImage(contentsRect(new QRect), image); end; } } }
Здесь все просто: создается форма MainForm с помощью наследования от QWidget (да, не удивляйтесь, и так тоже можно), затем в конструкторе задается ряд базовых параметров и создается один вертикальный сайзер (выравниватель, если кто забыл). Потом, создается объект RPictureBox, в который передается указатель на текущий объект (т.е используется this), устанавливается файл из которого будет загружаться изображение и, дальше (внимание!), с помощью метода saveThis в самом pictureBox сохраняется указатель на себя, который требуется для правильной работы события отрисовки (если этого не сделать — приложение на QtE без лишних слов и восклицаний либо просто зависнет, либо просто «упадет»). После необходимых манипуляций с настройкой PictureBox’а, просто размещаем его в сайзере и устанавливаем verticalSizer как ведущий элемент компоновки окна с помощью метода setLayout.
Как видите, все на редкость просто и приятно:
Напоследок, если вы рискнете (как и я) разработать свой виджет, то вот вам ряд бездельных советов:
- Помните, что QtE5 не то же самое, что Qt 5. Здесь несколько иные правила, и есть ряд вещей, которые можете разработать вы.
- Именуя виджеты, постарайтесь давать им ясные и простые названия. Наша коллаборация рекомендует вам в качестве первой буквы в имени виджета использовать букву R: эта буква следующая за буквой Q, с которой начинаются имена виджетов в QtE, и ее использование поможет вам (и возможно, другим людям) отличить сторонний виджет от стандартного для QtE5.
- Внимательно изучите файлы qte5.d и ascii1251.d, которые поставляются вместе с QtE5. Возможно, вам не потребуется писать свой собственный виджет, т.к. сама библиотека содержит необходимый и минимальный набор графических элементов, но даже если вдруг потребуется написать свой виджет, то у вас перед глазами будет ряд хороших примеров с качественной документацией и продуманной структурой (которую стоит перенять).
- Чаще проверяйте официальный репозиторий QtE и, по возможности, активно принимайте участие в его наполнении и работе. Вдруг что-то изменилось или что-то добавилось ?!
- Не забывайте про метод saveThis для ваших виджетов и обращайте внимание на работу всех элементов QtE.
[accordion][panel intro=»На этом все, а полный код примера, как обычно, под спойлером.»]
module app; import core.runtime; import std.file; import std.range; import qte5; alias WindowType = QtE.WindowType; alias normalWindow = WindowType.Window; // переходник к обработчику события отрисовки extern(C) { void onPaintPictureBox(RPictureBox* pictureBox, void* eventPointer, void* painterPointer) { (*pictureBox).runPaint(eventPointer, painterPointer); } } // Область с картинкой class RPictureBox : QWidget { private { QWidget parent; QImage image; int pictureWidth; int pictureHeight; string fileName; } // конструктор : длина картинки на ширину обязательно должны быть указаны ! this(QWidget parent, int pictureWidth, int pictureHeight) { super(this); this.parent = parent; this.pictureWidth = pictureWidth; this.pictureHeight = pictureHeight; this.image = new QImage(pictureWidth, pictureHeight, QImage.Format.Format_ARGB32_Premultiplied); setStyleSheet("background : white;"); setPaintEvent(&onPaintPictureBox, aThis()); } // имя файла для отображения void setFileName(string fileName) { this.fileName = fileName; if (fileName.empty || !fileName.exists) { image.fill(QtE.GlobalColor.white); } else { image.load(fileName); } } // получить имя файла string getFileName() { return fileName; } // событие отрисовки void runPaint(void* eventPointer, void* painterPointer) { QPainter painter; with (painter = new QPainter('+', painterPointer)) { drawImage(contentsRect(new QRect), image); end; } } } class MainForm : QWidget { private { QVBoxLayout verticalSizer; RPictureBox pictureBox; } this(QWidget parent, WindowType windowType) { super(parent, windowType); resize(500, 500); setWindowTitle("RPictureBox demo"); setStyleSheet("background : white"); verticalSizer = new QVBoxLayout(this); pictureBox = new RPictureBox(this, 256, 256); // здесь надо задать путь к своему файлу pictureBox.setFileName(`/home/aquaratixc/Загрузки/-kGOHOrMGIY.jpg`); pictureBox.saveThis(&pictureBox); verticalSizer.addWidget(pictureBox); setLayout(verticalSizer); } } auto QtEDebugInfo(bool debugFlag) { if (LoadQt(dll.QtE5Widgets, debugFlag)) { return 1; } else { return 0; } } int main(string[] args) { MainForm mainForm; QtEDebugInfo(true); QApplication app = new QApplication(&Runtime.cArgs.argc, Runtime.cArgs.argv, 1); with (mainForm = new MainForm(null, normalWindow)) { show; saveThis(&mainForm); } return app.exec; }
[/panel][/accordion]
P.S: На снимке изображен я, собственной персоной.