Масштабирование изображения в QWidget

В этой теме 0 ответов, 1 участник, последнее обновление  Васильев Владимир Сергеевич 1 месяц, 2 нед. назад.

  • Автор
    Сообщения
  • #4261

    В библиотеке Qt есть класс QLabel, который умеет отображать не только текст, но и картинки. Однако, загруженное изображение не будет масштабироваться при изменении размеров QLabel, кроме того не получится задать размер QLabel меньший, чем размер изображения (для больших картинок это вообще проблема). В этой заметке показано как решить проблему, мы напишем программу, функционал которой показан на рисунке:

    Для понимания материала рекомендую прочитать следующие статьи:

    Начнем с функции main — ее задача создать экземпляр главного окна и запустить обработку событий:

    #include "mainwindow.h"
    #include <QApplication>
    int main(int argc, char *argv[]) {
        QApplication a(argc, argv);
        MainWindow w;
        w.show();
        return a.exec();
    }

    В проект добавим форму Qt Designer — MainWindow, класс которой наследует QMainWindow. На форму кинем кнопку (QPushButton) с именем load и объект для отображения картинки (экземпляр QLabel). Немного перепишем содержимое сгенерированных файлов.

    В заголовочном файле:

    namespace Ui {
      class MainWindow;
    }
    class MainWindow : public QMainWindow {
        Q_OBJECT
    public:
        explicit MainWindow(QWidget *parent = 0);
        ~MainWindow();
    private slots:
        void load_file();
    private:
        Ui::MainWindow* ui;
    };

    Добавился приватный слот load_file, который будет вызываться при нажатии на кнопку.

    В файле реализации:

    #include <QFileDialog>
    
    MainWindow::MainWindow(QWidget *parent) :
        QMainWindow(parent), ui(new Ui::MainWindow) {
        ui->setupUi(this);
        connect(ui->open, SIGNAL(clicked(bool)), SLOT(load_file()));
    }
    void MainWindow::load_file() {
      auto path = QFileDialog::getOpenFileName(
            this, "Open Dialog", "", "*.png *.jpg *.bmp *.JPG");
      if (path.isEmpty())
        return;
      QPixmap pixmap(path);
      ui->image->setPixmap(pixmap);
    }
    MainWindow::~MainWindow() {
      delete ui;
    }

    В конструктор добавлено соединения сигнала от кнопки со слотом-обработчиком. Слот создает стандартный диалог выбора файла, одним из аргументов при этом задаются расширения открываемых файлов. Функция выбора файла возвращает путь, выбранный пользователем (или пустую строку если пользователь решил ничего не выбирать). Путь используется при конструировании объекта типа QPixmap, который передается в функцию setPixmap объекта на форме.

    В результате мы получили приложение, которое умеет открывать картинку и отображать ее на QLabel, однако картинка не масштабируется (ведь на настоящий момент наш код использует стандартный QLabel), чтобы решить проблему нам нужен новый класс.

    Посмотрим как работает стандартный QLabel. Этот класс имеет метод setPixmap, который копирует переданную картинку в свой внутренний буфер и запускает обновление (метод paintEvent). Функция paintEvent (виртуальная, можно написать свою реализацию) смотрит что для QLabel была выставлена картинка и отрисовывает ее (не так как нам надо), но кроме этого она делает еще много всего, например рисует текст, анимацию (QMovie) и т. п. Таким образом, если мы создадим наследника класс QLabel — то задавать нужное нам поведение надо будет в методе paintEvent. Однако, если пользователь видит, что наш класс является наследником QLabel, а значит имеет все соответствующие методы — то он может попытаться добавить на него например анимацию. Чтобы отображение этой анимации работало корректно (наш класс вел себя как полноценный QLabel) в методе paintEvent нам будет надо вызвать QLabel::paintEvent, однако при этом все наши старания по отображению картинки пойдут на смарку — как было описано выше, он посмотрит, что был выставлен pixmap и отрисует его (не так, как мы хотим). Вывод этого сложного абзаца в том, что наследовать класс QLabel нам не стоит.

    Итак, нам нужен класс, который будет уметь отображать картинку и мастабироваться, ну а еще, возможно отображать всякие стандартный рамочки и т. п. Весь нужный нам функционал кроме картинок есть в QWidget, его и наследуем:

    #include <QWidget>
    class ScaledPixmap : public QWidget {
    public:
      ScaledPixmap(QWidget *parent = 0);
      void setScaledPixmap(const QPixmap &pixmap);
    protected:
      void paintEvent(QPaintEvent *event);
    private:
      QPixmap m_pixmap;
    };

    В класс добавлена картинка, которую будем добавлять и метод для ее установки. Реализуем методы:

    ScaledPixmap::ScaledPixmap(QWidget *parent): QWidget(parent) {
    }
    void ScaledPixmap::setScaledPixmap(const QPixmap &pixmap) {
      m_pixmap = pixmap;
      update();
    }
    void ScaledPixmap::paintEvent(QPaintEvent *event) {
      QPainter painter(this);
      if (false == m_pixmap.isNull()) {
        QSize widgetSize = size();
        QPixmap scaledPixmap = m_pixmap.scaled(widgetSize, Qt::KeepAspectRatio);
        QPoint center((widgetSize.width() - scaledPixmap.width())/2,
                      (widgetSize.height() - scaledPixmap.height())/2);
        painter.drawPixmap(center, scaledPixmap);
      }
      QWidget::paintEvent(event);
    }

    После установки картинки, вызываем функцию обновления (чтобы перерисовать виджет). В функции paintEvent проверяем что картинка не пуста, получаем размер виджета, создаем новую картинку, подогнанную под размер виджета без искажения пропорций (KeepAspectRatio), определяем координаты центра виджета и отрисываем по центру картинку. После всех этих действий запускаем отрисовку рамочек, обновление части виджета и т. п. (QWidget::paintEvent)

    Вернемся к QLabel. Ведь мы можем для него точно также добавить метод setScaledPixmap и хранить еще одну картинку внутри него, которую и отрисовывать в paintEvent… (я бы не писал про это, если бы не встретил такую реализацию). Да потому что наш класс ведет себя не так, как QLabel, что будет если пользователь вызовет сначала setScaledPixmap, а потом QLabel::setPixmap? – либо нашу картинку вообще не будет видно (поверх нее будет другая), либо (если сначала вызвать QLabel::paintEvent, а потом нарисовать то, что нам надо) – нарисовано будет две разных картинки. Все это нарушает принцип подстановки Лисков:

    поведение наследуемых классов должно быть ожидаемым для кода, использующего переменную базового типа.

    И так, мы описали наш класс, который умеет отображать картинку и перерисовывает ее при изменении своего размера. Чтобы отобразить его на нашей форме — заменяем в QtDesigner объект типа QLabel на объект QWidget. Нажимаем на него правой кнопкой мыши, выбираем «преобразование виджетов». Добавляем туда информацию о нашем новом классе (наследует QWidget, называется ScaledPixmap, находится в файле scaledpixmap.h). Осталось только изменить обращение к нему в слоте загрузки картинки:

    void MainWindow::load_file() {
      auto path = QFileDialog::getOpenFileName(
            this, "Open Dialog", "", "*.png *.jpg *.bmp *.JPG");
      if (path.isEmpty())
        return;
      QPixmap pixmap(path);
      ui->image->setScaledPixmap(pixmap);
    }

    Запускаем и наслаждаемся результатом.

    Взять исходники можно в репозитории.

Для ответа в этой теме необходимо авторизоваться.