Wiki

Определение высоты по ширине

Jasmin Blanchette (перевод Andi Peredri)

С момента выхода Qt 2.0 компоновщики виджетов стали неотъемлемой частью программирования с Qt. Компоновщики виджетов избавляют программиста от необходимости ручного позиционирования всех интерфейсных элементов форм и обеспечивают при этом более лучший результат. В этой статье рассматривается одна возникающая при работе с ними проблема, для которой не существует универсального решения: компоновка элементов, высота которых зависит от ширины (height-for-width). Здесь также представлен исходный код дружественного по отношению к рыбам и компоновщикам виджета Aquarium.


Парадокс минимального размера

Для упрощения задачи компоновки виджеты Qt предлагают функции sizeHint() и minimumSizeHint(). Например, рекомендуемый для кнопки "OK" размер (40, 25) сообщает компоновщику, что нужно предоставить, как минимум, 40 пикселов по горизонтали и 25 пикселов по вертикали.

Однако имеется более требовательная к вопросам компоновки категория виджетов. Например, ниже представлены три скриншота QMenuBar с различными размерами:

Menubar

Скриншоты наглядно демонстрируют, что высота виджета QMenuBar не всегда равна высоте одной строки меню. Аналогично себя ведет виджет QLabel в режиме переноса слов:

Alibaba-Diag

На первых двух скриншотах видно, что в зависимости от доступного пространства в одном из направлений, размер текстовой области может быть сокращен либо до 102 пикселей по горизонтали, либо до 55 пикселей по вертикали. Третий скриншот показывает, что происходит, когда размер текстовой области сокращается до минимума в обоих направлениях.

Хотелось бы, чтобы у виджета QLabel была возможность сообщить компоновщику виджетов о приемлемости результатов в первых двух случаях и о недопустимости третьего. Однако этого нельзя добиться с помощью лишь функций sizeHint() и minimumSizeHint(), поэтому Qt предлагает вспомогательный механизм height-for-width.

Каждый виджет имеет собственную политику QSizePolicy, которая, в свою очередь, содержит флаг boolean height-for-width, определяющий для виджета зависимость его высоты от ширины. Для определения высоты виджета по его известной ширине компоновщик может вызвать виртуальную функцию QWidget::heightForWidth().

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

  • menuBar()->heightForWidth(285) возвращает 21.
  • menuBar()->heightForWidth(159) возвращает 39.
  • menuBar()->heightForWidth(130) возвращает 57.
Аналогичное решение для QLabel позволит избежать результата, представленного на третьем скриншоте.

Все это автоматически обеспечивает Qt, поэтому вам не придется вмешиваться.

Проблемы с окнами верхнего уровня

Все намного усложняется, когда виджеты с высотой, зависящей от ширины, являются окнами верхнего уровня. Редко, когда такими виджетами являются QLabel, QMenuBar или QTextEdit. Гораздо чаще это - диалоговые окна на базе компоновщиков виджетов, которые содержат компоновщики второго порядка, которые, в свою очередь, уже содержат QLabel, QMenuBar или QTextEdit. Размеры диалоговых окон определяются размерами их дочерних виджетов, поэтому в некоторых случаях это может приводить к нежелательным результатам. Вот пример такого Setup-диалога:

Tele-Diag

В первых двух случаях результаты приемлемы. На третьем скриншоте отсутствуют кнопки и часть текста. Лучший способ добиться хорошего результата для таких диалогов - это разрешить пользователю изменять их размер. Такой подход используется в большинстве современных приложений.

До версии Qt 3.1 менеджеры компоновки определяли минимальный размер диалоговых окон на основании их рекомендуемого минимального размера, что часто приводило к неприемлемым результатам. Эта проблема решалась добавлением в текст программы следующей строки:

dialog->layout()->setResizeMode( QLayout::FreeResize );

Начиная с Qt 3.1, менеджеры компоновки в качестве режима изменения размера по-умолчанию используют FreeResize.

Создание виджета с высотой, зависящей от ширины

Сейчас мы создадим виджет Aquarium с высотой, зависящей от его ширины. Виджет Aquarium содержит некоторое число рыб. Для того, чтобы выжить, каждой рыбе необходимо определенное пространство. Таким образом, нам придется воспользоваться механизмом height-for-width: широкий аквариум не должен быть глубоким, а глубокий аквариум не должен быть широким.

Aqua1  Aqua2  Aqua3

Скриншоты демонстрируют изменение размеров виджета Aquarium после каждого добавления рыбы.

Класс Aquarium наследует от класса QFrame и переопределяет три виртуальные функции. Вот его объявление:

class Aquarium : public QFrame
{
    Q_OBJECT
public:
    Aquarium( int numFish, QWidget *parent = 0, const
char *name = 0 );
 
    virtual int heightForWidth( int width ) const;
    virtual QSize sizeHint() const;
 
public slots:
    void setCapacity( int numFish );
 
protected:
    virtual void drawContents( QPainter *painter );
 
    QPixmap fish;
    int capacity;
};

В качестве параметра конструктор принимает вместимость аквариума и устанавливает для виджета цвет, стиль и политику QSizePolicy:

Aquarium::Aquarium( int numFish, QWidget *parent, const char *name )
    : QFrame( parent, name ), fish( "fish.png" ),
      capacity( numFish )
{
    setPalette( QPalette( QColor("light blue")) );
    setFrameStyle( Box | Raised );
    setSizePolicy( QSizePolicy(QSizePolicy::Preferred,
	QSizePolicy::Preferred, TRUE) );
}

Аргумент TRUE конструктора QSizePolicy означает, что высота виджета зависит от его ширины.

Функция heightForWidth() класса QWidget переопределяется таким образом, чтобы возвращать высоту аквариума в зависимости от числа рыб:

int Aquarium::heightForWidth( int width ) const
{
    return 10000 * capacity / QMAX( width, 1 );
}

Мы принимаем, что каждой рыбе необходимо пространство в 10000 пикселов, хотя в действительности это зависит от ее вида. Мы используем QMAX(), чтобы избежать деления на нуль.

Функция sizeHint() класса QWidget переопределяется таким образом, чтобы возвращать подходящий размер для виджета:

QSize Aquarium::sizeHint() const
{
    int w = (int) ( 100 * sqrt(capacity) );
    return QSize( w, heightForWidth(w) );
}

Функция setCapacity() задает число рыб:

void Aquarium::setCapacity( int numFish )
{
    if ( capacity != numFish ) {
        capacity = numFish;
        updateGeometry();
        update();
    }
}

Мы вызываем QWidget::updateGeometry(), чтобы сообщить менеджеру компоновки об изменении размеров. Это нужно делать во всех случаях, когда изменяется sizeHint(), minimumSizeHint() или heightForWidth(). Затем мы вызываем QWidget::update(), чтобы перерисовать аквариум с корректным числом рыб.

Функция drawContents() класса QFrame переопределяется таким образом, чтобы отрисовывать рыбу:

void Aquarium::drawContents( QPainter *painter )
{
    srand( capacity );
    QSize size( width() - fish.width(), height() - fish.height() );
    size = size.expandedTo( QSize(1, 1) );
    for ( int i = 0; i < capacity; i++ ) {
        int x = rand() % size.width();
        int y = rand() % size.height();
        painter->drawPixmap( x, y, fish );
    }
}

Вызов srand() гарантирует повторяемость последовательности случайных чисел, используемых для определения положений рыб в аквариуме. Таким образом, для данного числа рыб их местоположение будет всегда фиксировано.

Подведение итогов

Функция heightForWidth() базового класса QWidget всегда возвращает 0, что означает, что высота виджета не зависит от его ширины. Это свойственно большинству подклассов QWidget. В классах QLabel, QMenuBar и QTextEdit функция heightForWidth() переопределена.

Переопределить функцию heightForWidth() несложно. В случае класса Aquarium мы переопределили heightForWidth(), как обратно- пропорциональную функцию. Однако Qt не накладывает каких-либо ограничений на этот счет, поэтому мы можем переопределить heightForWidth() самым произвольным образом, например, удерживать высоту виджета фиксированной.


Copyright © 2002 Trolltech. Trademarks