Нативные интерфейсы в Qt

    Qt Logo

    Эта статья посвящена программированию GUI на базе фреймворка Qt от Nokia. Мы рассмотрим способы разработки интерфейсов для нативного отображения на платформах Windows, Linux и Mac OS X.

    Презентацию[1] вёл Йенс Бач-Вииг (Jens Bache-Wiig) — разработчик Qt (с 2005 года), занимающийся интерфейсами (look and feel).

    От переводчика: далее, поскольку это слайд-шоу, по мере возможностей буду давать пояснения к слайдам. Какие то, на мой взгляд менее важные моменты будут опускаться, что-то будет рассмотрено более подробно.

    Итак, приступим. Каждое графическое окружение имеет свой свод правил (User Interface Guidelines, UIG) по созданию интерфейсов, предназначенных для этих окружений. Из основных можно отметить такие руководства как Windows User Experience Interaction Guideline, Apple Human Interface Guideline, KDE User Interface Guideline и GNOME HID. Каждое из этих руководств «определяет положение кнопочек и рюшечек» конкретного окружения. Фреймворк Qt, в свою очередь, осуществляет поддержку всех этих руководств, предоставляя разработчику возможность создавать программы с использованием виджетов, «подстраивающихся» под окружение.



    QStyle



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

    Интерфейсы Windows 7 и Ubuntu 10.10 соответственно:
    Windows & Ubuntu

    QStyle отвечает за внешний вид, размеры, форму и любые специфичные для конкретной платформы параметры отображения стиля (темы оформления).

    Для модификации существующих стилей в своих проектах существует класс QStyleOption. Пример использования:
    QStyleOption opt;<br/>
    opt.rect = arrowRect;<br/>
    opt.state = QStyle::Sate_Item | QStyle::State_Children;<br/>

    if (expanded)<br/>
        opt.state |= QStyle::State_Open;<br/>
    style()->drawPrimitive(QStyle::PE_IndicatorBranch, &opt, painter, this);


    Класс QStyleOption хранит параметры, используемые функциями QStyle.

    Если нужно модифицировать стиль, нет необходимости наследовать его. Можно и нужно использовать QProxyStyle (введён с Qt 4.6). Он позволяет легко модифицировать системный стиль не нарушая его.
    class MyProxyStyle: public QProxyStyle<br/>

    {<br/>
    public:<br/>
        int styleHint(StyleHint hint, const QStyleOption *option = 0,<br/>

                      const QWidget *widget = 0, QStyleHintReturn *returnData = 0) const<br/>
        {<br/>
            if (hint == QStyle::SH_UnderlineShortcut)<br/>

                return 0;<br/>
            return QProxyStyle::styleHint(hint, option, widget, returnData);<br/>

        }<br/>
    };<br/>
     <br/>
    int main(int argc, char **argv)<br/>
    {<br/>
        QApplication a(argc, argv);<br/>

        a.setStyle(new MyProxyStyle);<br/>
        //...<br/>
    }


    Таблицы стилей



    К использованию таблиц стилей стоит подходить с осторожностью, так как их использование может испортить внешний вид. Следует пытаться смешивать их с системной палитрой: использовать полупрозрачность и системную палитру.

    Следует избегать кодирования цвета:
    QString css = "QLabel { color:palette(highlight); }";


    А если требуется собственный цвет:
    QColor color(255, 0, 0);<br/>

    QString css = QString("QLabel { color: %1; }").arg(color.name());


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

    QWidget *segBtn = new QWidget(this);<br/>
    QToolButton *backBtn = new QToolButton;<br/>

    QToolButton *forwardBtn = new QToolButton;<br/>
    segBtn->setLayout(new QHBoxLayout);<br/>
    segBtn->layout()->setSpacing(0);<br/>

    segBtn->layout()->setMargin(0);<br/>
    backBtn->setIcon(QIcon(":/imgs/arrow-back.png"));<br/>
    backBtn->setFixedSize(29, 32);<br/>

    backBtn->setStyleSheet("QToolButton { border-image: url(:/imgs/button-left.png); }"<br/>
                           "QToolButton:hover { border-image: url(:/imgs/button-left-hover.png); }");<br/>
    forwardBtn->setIcon(QIcon(":/imgs/arrow-forward.png"));<br/>

    forwardBtn->setFixedSize(29, 32);<br/>
    forwardBtn->setStyleSheet("QToolButton { border-image: url(:/imgs/button-right.png); }"<br/>

                              "QToolButton:hover { border-image: url(:/imgs/button-right-hover.png); }");<br/>
    segBtn->layout()->addWidget(backBtn);<br/>
    segBtn->layout()->addWidget(forwardBtn);<br/>

    mainToolBar->addWidget(segBtn);

    Segmented buttons


    Как видно из примера (позаимствован из стати «Using Blur Behind on Windows»[2]), псевдоклассы тоже есть в наличии.

    С помощью селекторов атрибутов можно настроить отображение под каждый системный стиль:
    "QToolButton[style=QMacStyle] { border-image: url(:/imgs/button-mac-right.png); }"<br/>
    "QToolButton[style=QWindowsVistaStyle] { border-image: url(:/imgs/button-vista-right.png); }"


    Диалоги



    Согласно руководствам, у каждой операционной системы (графического окружения) есть свои представления почему кнопки «Сохранить», «Отмена», «Ок» и так далее должны находиться «именно там, а не тут». И так далее в этом же ключе про системные иконки на кнопках, разные надписи и т. д. Собственно, здесь мы обсуждаем не UIG, а говорим о правильном использовании виджетов…

    Использование стандартных кнопок в диалогах с учётом UIG окружения достигается при использовании QDialogButtonBox.
    QDialogButtonBox box(QDialogButtonBox::Cancel | QDialogButtonBox::Help |<br/>

                         QDialogButtonBox::Ok | QDialogButtonBox::Save);

    Этот код в Windows

    Этот код в Ubuntu


    Также можно добавить собственные кнопки с обозначением их роли:
    QDialogButtonBox box;<br/>

    box.addButton(myButton, QDialogButtonBox::AcceptRole);


    Значимость указания роли кнопки хорошо просматривается на примере простого модального диалога:
    MyQDialogSubcass dialog;<br/>
    //...<br/>

    if (dialog.exec == QDialog::Accept) {<br/>
        //Make some noise?<br/>
    }


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

    Что же происходит при использовании модальных диалогов?
    Window Modal Dialog


    (Пример[3] основан на коде sdi из примеров, поставляемых с библиотеками Qt)

    Есть несколько способов показать диалоговое окно:
    • QDialog::show() — не модально;
    • QDialog::exec() — модально к программе;
    • QDialog::open() — модально к окну.


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

    Так же, не редкость когда диалог является формой ввода каких-либо данных. Формы, в понимании конкретно формы, например ввода контактной информации или ещё чего-либо. Здесь так же, в соответствии с UIG, будет разное отображение:

    Ещё один момент, показывающий разницу в руководствах — использование разметки для форм ввода (QFormLayout — компоновка в 2 колонки). Всё отлично видно и понятно:

    QFormLayout в Mac OS X и Windows


    Код подобной формы легче написать самому, чем использовать дизайнер:
    QFormLayout *layout = new QFormLayout();<br/>
        layout->addRow(tr("Имя пользователя:"),<br/>

                       userName);<br/>
        layout->addRow(tr("Электропочта:"),<br/>
                       eMail);<br/>
        layout->addRow(tr("Пароль:"),<br/>

                       passWord);<br/>
        layout->addRow(tr("Закрепим успех:"),<br/>
                       passWordAgain);


    Кроссплатформенные заметки



    Как показать пользователю, что текущий документ был модифицирован?
    setWindowModified(ture);<br/>
    setWindowFilePath("Untitled 1.txt");

    Модифицирован

    Использовать относительные пути к файлам (к примеру, к документации: «/home/erik/myapp/bin/docs/index.html" → «docs/index.html»)

    Как привлечь внимание пользователя?
    void QApplication::alert(QWidget *widget, int msec = 0) [static]

    В Mac иконка в доке начнёт прыгать, а в Windows — начнёт мигать кнопка окна приложения на панели задач.

    Превлечение внимания польщователя в Mac и Windows

    Чтобы узнать адреса хранилищ (фото, видео, временных файлов...) используйте QDesktopServices::storageLocation().

    Чтобы отправить электронное письмо из своего приложения, открыть страницу в браузере или какой-то файл в приложении по умолчанию, установленного в системе, следует использовать QDesktopServices::openUrl():
    QDesktopServices::openUrl("mailto:support@habrahabr.ru");<br/>

    QDesktopServices::openUrl("habrahabr.ru");<br/>
    QDesktopServices::openUrl(QUrl::fromLocalFile(...));


    Чтобы использовать в приложении горячие клавиши, лучше всего использовать стандартные комбинации. Некоторые комбинации на разных платформах различаются, с этим стоит считаться (конечно никто не запрещает добавлять свои комбинации, к примеру, для выхода по Ctrl+Q в Windows — удобно же, но не стандартно)

    // Получаем полный список стандартных сочетаний для действия "вырезать"<br/>
    QList<QKeySequence> keys = QKeySequence::keyBindings(QKeySequence::Cut);<br/>

    foreach (QKeySequence key, keys) {<br/>
        logStream << key.toString(QKeySequence::PortableText);<br/>
        // ...<br/>

    }


    Рекомендуется использовать похожую на оригинальную тему иконок, чтобы они не сильно различались с окружением ОС. С Linux — легче. Есть поддержка тем:
    QIcon::fromTheme("edit-cut", QIcon(":/edit-cut.png"));


    Если в системе будет найдена иконка для действия «вырезать» (edit-cut), то будет использована системная. Если нет — подгрузит из ресурсов.
    Спецификация наименований иконок от FreeDesktop доступна здесь.
    Использование системной темы в Linux


    Выставляйте приоритеты для QAction. Это позволит выделить наиболее приоритетные действия в интерфейсе пользователя. К примеру, когда для панели инструментов установлен режим Qt::ToolButtonTextBesideIcon, то действия с низким приоритетом (LowPriority) будут отображены без подписей
    void QAction::setPriority(Priority priority)


    Приоритеты QAction и Qt::ToolButtonTextBesideIcon

    Иконки в меню не используются в Mac, но используются в Windows и KDE. В GNOME — зависит от настроек. Чтобы «переписать» это в помощь нам есть:

    void QAction::setIconVisibleInMenu(bool visible)<br/>
    QApplication::setAttribute(Qt::AA_DontShowIconsInMenus);


    Пользователи GNOME и Mac привыкли, что настройки применяются сразу по их изменению, а пользователей Windows и KDE балуют возможностью применить (и отменить) сделанные изменения, не применяя настройки до нажатия соответствующей кнопки

    Про MDI-интерфейсы: Mac и GTK+ не поддерживают их. В Windows их всё ещё используют, но как-то это уже не вписывается в образ современной ОС.

    Если необходимо использовать какие-то платформо-зависимые функции (WinAPI, Cocoa...), то существуют макросы Q_WS_WIN, W_WS_MAC, Q_WS_X11, ...
    #ifdef Q_WS_WIN<br/>

        actionClose->setShortcut(QKeySequence("Ctrl+Q"));<br/>
    #endif


    Так же для этих задач может понадобиться QWidget::winId(), который возвращает системный указатель HWND, NSView* или X11 handle (на Windows, Mac и Linux соответственно). При этом следует не забыть, что идентификатор окна может измениться во время выполнения программы — будет получено событие QEvent::WinIdChange.

    Платформо-зависимые заметки



    Mac


    Используйте иконку приложения высокого разрешения.

    Для области уведомлений следует использовать чёрно-белую иконку.

    Создавайте QMenuBar без родителя, он ему не нужен. Первый созданный QMenuBar будет использован по умолчанию.

    Опция QMainWindow::setUnifiedTitleAndToolBarOnMac(bool set) объединит ваши панели в одно целое с заголовком окна. Но следует помнить, что это приведёт к
    — невозможности перемещать тулбары;
    — будет только 1 строка без разрывов (было несколько панелей друг над другом — станет одна);
    — в полноэкранном режиме панели не будут видны;
    Совмещение панелей и заголовка окна в Mac

    В доке тоже есть меню. Чтобы использовать его в своём приложении:
    QMenu *menu = new QMenu;<br/>

    // Добавляем элементы меню...<br/>
    extern void qt_mac_set_dock_menu(QMenu *);<br/>
    qt_mac_set_dock_menu(menu);

    Меню в доке Mac

    Qt автоматически переставит (и переименует, если что) пункты вашего меню на их законные места в Mac, если выставить им роли с помощью QAction:setMenuRole(MenuRole menuRole). К примеру, меню настойки будет перемещено в «Программа → Настройки…»

    Правильное меню в Mac

    X11


    Здесь основным пунктом будет следование стандартам freedesktop.org — это:
    • спецификации меню;
    • темы иконок;
    • автостарт;
    • закладки;
    • файл .desktop.


    Простой файл.desktop конфигурации содержит:
    • иконку приложения;
    • кнопку для меню;
    • регистрацию mime-типов;


    Как определить, использует ли пользователь KDE или GNOME? Собственно, 100% пути нет. Можно попробовать проверить переменную «DESKTOP_SESSION».
    echo $DESKTOP_SESSION<br/>

    gnome


    Конечно, не забываем про разное поведение приложений в GNOME и KDE. Тестируем.

    Windows


    QSettings в Windows использует реестр. Здесь его можно использовать для чтения системных настроек.
    QSettings settings("HKEY_CURRENT_USER\\...\\Explorer\\Advanced", QSettings::NativeFormat);<br/>

    bool result = settings.value("EnableBalloonTips", true).toBool();


    Ещё можно использовать темы оформления типа DotNet или Explorer. Всё это можно найти в решениях (ftp).

    Также можно включить Blur Behind(2), но Qt пока не предоставляет API для этого. Однако можно использовать WinAPI напрямую, установив атрибут WA_TranslucentBackground и не забыв включить WA_NoSystemBackground у виджета.

    Сноски



        [1] — Презентация и её транскрипция;
        [2] — Статья «Using Blur Behind on Windows» от Qt Labs (и статья «Создание смазывания фона под окном в Windows» здесь);
        [3] — Исходный код переписанного примера sdi.

    Успехов в новой 365-ти-дневке!

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 9

      +18
      Отличная статья, труд если не титанический, но весьма объемный и добротный.
      Спасибо!
        +4
        Спасибо, особенно понравилось сравнение в различных операционных системах :)
          +1
          Используя QWidget:winId() нужно помнить, что он может измениться во время работы программы. В этом случае виджету будет послано событие типа QEvent::WinIdChange.
            +1
            Спасибо. Подправил.
            +2
            Спасибо, как раз интересовался нативным отображением Qt-программ под mac os.
              +5
              Хабр радует статьями в этом году.
                +3
                Сильно. Отправил инвайт.
                  0
                  Отличная работа, видел исходную презентацию, но на родном языке намного проще воспринимать информацию, спасибо
                    0
                    Спасибо! Было интересно читать даже мне, незнакомому с QT.

                    Only users with full accounts can post comments. Log in, please.