Создание своего виджета в QtE5

В этой статье, я покажу вам, несколько с иной стороны, насколько удобна 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.

На этом все, а полный код примера, как обычно, под спойлером.

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;
}


P.S: На снимке изображен я, собственной персоной.

Добавить комментарий