Примеры использования wxWidgets

1 апреля 2011

Как и обещал, пишу продолжение своей заметки про wxWidgets. В этом посте будет описана установка wxWidgets и Code::Blocks под различными операционными системами, основы создания GUI с помощью wxSmith, а также приведены небольшие примеры кода.

1. Установка wxWidgets и Code::Blocks

Поскольку мы имеем дело с кроссплатформенной разработкой, тут все зависит от используемой нами ОС. Под Windows нужно скачать wxPack и компилятор MinGW отсюда, последнюю версию Code::Blocks без MinGW отсюда и утилиту wx-config.exe отсюда. Устанавливаем wxPack, затем MinGW, затем Code::Blocks. Утилиту wx-config.exe копируем в «C:\Windows\» или где там у вас стоит Винда.

Дополнение: Как выяснилось, можно поставить и более свежую версию MinGW, скажем, если вы хотите использовать в своем проекте возможности C++0x. Даже wxPack не нужен, достаточно поставить пакет wxMSW отсюда. Только учтите, что кое-где в wxWidgets используются расширения GCC, не входящие в стандарт C++0x, поэтому в Project → Build Options → Compiler Settings → Other Options нужно дописать флаг -std=gnu++0x. Флаг -std=c++0x, который Code::Blocks позволяет выставить с помощью галочки в соседней вкладке Compiler Flags, не годится.

В Ubuntu/Debian установка происходит следующим образом:

sudo apt-get install codeblocks codeblocks-contrib g++
# на счет libwxgtk2.6-dev есть сомнения, попробуйте сначала без него
sudo apt-get install wx2.8-headers libwxgtk2.8-0 \
  libwxgtk2.8-dev libwxgtk2.6-dev
sudo ln -s /usr/include/wx-2.8/wx /usr/include/wx
# если файла /usr/include/wx/setup.h нет, тогда говорим
sudo ln -s /usr/include/wx-2.6/wx/deprecated/setup.h \
  /usr/include/wx/setup.h

Проверка:

wx-config --cxxflags
wx-config --libs

Установка во FreeBSD намного проще, ибо все зависимости тянутся автоматом:

pkg_add -r codeblocks

Запускаем IDE, идем в меню «Settings → Compiler and debugger…». В «Compiler Sittings → Other Options» пишем:

`wxgtk2u-2.8-config --cppflags`

В «Linker Settings → Other linker options» прописываем:

`wxgtk2u-2.8-config --libs`

Все приведенное выше писалось по памяти и небольшой шпаргалке, сохраненной на GMail, так что в случае возникновения проблем, пожалуйста, отпишитесь в комментариях. С вероятностью 90% решение будет заключаться в прописывании правильных путей в настройках Code::Blocks или правке переменных окружения.

При портировании проекта из *nix в Windows, нужно убедиться, что в «Project → Build Options → Compiler/Linker Settings» wx-config вызывается с параметром --static=yes.

Дополнение: Есть сведения, что в репозитории Ubuntu лежит устаревшая и содержащая ошибки версия Code::Blocks, в связи с чем рекомендуется использовать ночные сборки или компилировать Code::Blocks из исходников. Подробности можно найти здесь.

2. Создание графического интерфейса

В комплекте с Code::Blocks идет плагин wxSmith, предназначенный для редактирования GUI. Увидеть его можно, открыв в окне Management (слева) вкладку Resources. Плагин не сильно похож на редактор GUI в Borland Delphi, но разобраться в нем просто.

Для меня самым сложным было понять, что в 90% случаев весь лайаут строится на основе wxBoxSizer. Для примера рассмотрим мои наброски интерфейса mp3-плеера:

eaxPlayer

Красные прямоугольники — это границы элементов wxBoxSizer. Самый большой прямоугольник имеет ориентацию wxVERTICAL. Все добавляемые в него элементы будут располагаться один под другим. Вложенные сайзеры имеют ориентацию wxHORIZONTAL. Расположенные в них элементы выстраиваются в одну строчку (см нижний ряд кнопок).

Вот как выглядит дерево элементов управления:

Дерево элементов управления

С помощью свойств Proportion, Shaped, Expand, Placement и Border можно добиться практически любого вообразимого расположения элементов. Настроить эти свойства можно в Quick Properties Panel (нижняя кнопка с буквой Q в правой части wxSmith). На словах довольно сложно объяснить, каким свойствам какие значения нужно присваивать для достижения определенного результата. Поэкспериментируйте немного и сами все поймете.

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

Наконец-то мы дошли до примеров кода. Начну с моих любимых регулярных выражений. Классическая ситуация — пользователь вводит что-то в поле ввода и нажимает кнопку «ОК». Программа должна как-то обработать введенные данные, но сначала следовало бы проверить их корректность.

    // получаем имя функции (из wxTextCtrl)
    wxString funcName = txtFuncName->GetValue();

    // создаем объект "регулярное выражение"
    wxRegEx reFuncName(_T("^[a-z_][a-z0-9_]+$"), wxRE_ICASE);
    // проверяем его валидность (только в Debug-версии)
    wxASSERT(reFuncName.IsValid());

    // введенные данные соответствует регулярному выражению?
    if(!reFuncName.Matches(funcName)) {
        // выводим сообщение об ошибке
        wxMessageBox(_("Invalid function name '")+funcName+_T("'!"));
        // очищаем поле ввода
        txtFuncName->Clear();
        // выходим из события OnClick
        return;
    }
    // ...

Здесь все довольно очевидно, если не считать макросов _T() и _(). Первый используется при объявлении unicode-строк, второй — при объявлении строк, которые мы можем захотеть перевести на другой язык (см следующий пример). Тот или иной макрос нужно использовать везде, где мы хотим получить объект wxString вместо массива char. Учитывая, что мы пишем с использованием wxWidgets, это 99% случаев. Необходимость использовать макросы при объявлении каждой строки компенсируется способностью Code::Blocks подставлять парные скобки и кавычки. У макроса _T() есть синоним wxT(), но он на один символ длиннее. Просто примите к сведенью и не удивляйтесь, если увидите wxT() в чужом коде.

Еще кое-что, касающееся регулярных выражений в wxWidgets, вы найдете в пункте 5 далее по тексту.

4. Интернационализация в wxWidgets

Интернационализация (сокращенно i18n, от «internationalization») — это когда мы один раз пишем программу, после чего для перевода на другой язык нам достаточно просто снабдить ее файлами перевода. Тут главная фишка в том, что нам не требуется перекомпилировать программу для каждого языка. Традиционно перевод осуществляется с помощью программы PoEdit.

PoEdit анализирует исходный код нашей программы, выдирая из нее строки, окруженные макросом _(), который на самом деле есть вызов функции gettext(). Для каждой строки вводим перевод. Таблица строк сохраняется в файл .po (отсюда и название программы). Когда перевод закончен, файл .po нужно «скомпилировать» в формат .mo. Это и есть файл перевода.

Осталось дописать несколько строк в коде программы:

// ---- файл ExampleApp.h ----

class ExampleApp : public wxApp {
    public:
        virtual bool OnInit();
    private:
        wxLocale m_Locale; // <-- дописали вот это
};

// ---- файл ExampleApp.cpp ----

// ...

bool ExampleApp::OnInit() {
    //(*AppInitialize
    bool wxsOK = true;
    wxInitAllImageHandlers();
    if ( wxsOK ) {
        // vvv дописали вот это vvv

        // добавляем каталог для поиска .mo файлов
        m_Locale.AddCatalogLookupPathPrefix(_T("lang"));
        // инициализация объекта
        m_Locale.Init();
        // подгружаем .mo файл, соответствующий текущей локали
        // (если такой файл есть)
        // например, если текущая локаль - ru_RU и в каталоге ./lang
        // лежит файл ru_RU.mo, он будет использован для перевода
        m_Locale.AddCatalog(m_Locale.GetCanonicalName());
        // для локали ru_RU аналогично вызову
        // m_Locale.AddCatalog(_T("ru_RU"));

        // ^^^ дописали вот это ^^^

        // ...
    }
    //*)
    return wxsOK;
}

Теперь бросаем в каталог ./lang файлы с переведенными строками, для русского языка — ru_RU.mo, для немецкого — de_DE.mo и так далее. Программа будет автоматически отображать тот язык, на котором говорит пользователь. В мире UNIX, например, этот язык определяется с помощью переменной окружения $LANG.

Также можете распространять вместе с программой и .po файл. Чисто на случай, если найдутся добровольцы, желающие перевести ее на свой родной язык. Чтобы облегчить им работу, изначально используйте английский. Вряд ли в Японии или Египте многие владеют русским языком.

5. Пример использования wxHTTP

В wxWidgets имеется много полезных классов, среди которых мне особенно хотелось бы отметить wxHTTP. Как несложно догадаться по названию, он представляет собой готовый HTTP-клиент. Пример использования:

// скачать веб-страницу (в UTF-8!) и сохранить ее в rslt
// true в случае успеха, false в случае ошибки
bool ExampleFrame::HttpGet(
  const wxString url, wxString& rslt, size_t maxLen)
{
    const unsigned int DEFAULT_MAX_LEN = 128*1024;
    const unsigned int HTTP_TIMEOUT = 10;
    if(maxLen == 0) {
        // некорректный аргумент или значение по умолчанию
        maxLen = DEFAULT_MAX_LEN;
    }

    // регулярное выражение для url
    wxRegEx reUrl(wxT("^(?:http://)?([^/]+)(/.*)$"), wxRE_ADVANCED);
    wxASSERT(reUrl.IsValid());

    if(!reUrl.Matches(url)) {
        // неверный формат URL
        // возможно, не хватает слэша после доменного имени
        return false;
    }

    // получаем доменное имя и get-запрос
    wxString server = reUrl.GetMatch(url, 1);
    wxString query = reUrl.GetMatch(url, 2);

    // создаем http-клиент
    wxHTTP http;
    http.SetTimeout(HTTP_TIMEOUT);
    http.SetHeader(
        wxT("Content-type"),
        wxT("text/html; charset=utf-8")
      );
    http.SetHeader(
        wxT("User-Agent"),
        wxT("Mozilla/5.0 (X11; U; FreeBSD i386; ru-RU; "
            "rv:1.9.2.13) Gecko/20110202 Firefox/3.6.13")
      );

    // соединяемся с сервером
    if(!http.Connect(server)) {
        // this->LogAdd(_("Connection failed."));
        return false;
    }

    // получаем поток данных
    wxInputStream* searchRslt = http.GetInputStream(query);
    if(http.GetError() != wxPROTO_NOERR) {
        // this->LogAdd(_("Download failed, get.GetError() == ")
        //   + http.GetError());
        // что-то вроде if(x) { delete x; x = NULL; }
        wxDELETE(searchRslt);
        return false;
    }

    // выделяем память для временного буфера
    char* binaryData;
    try {
       binaryData = new char[maxLen];
    } catch(bad_alloc& error) {
        // нехватка памяти
        wxDELETE(searchRslt);
        return false;
    }

    // считываем данные из потока в буфер
    searchRslt->Read(binaryData, maxLen);
    wxDELETE(searchRslt);

    // преобразуем данные из буфера в строку
    rslt = wxString::FromUTF8(binaryData, maxLen);
    // что-то вроде if(x) { delete[] x; x = NULL; }
    wxDELETEA(binaryData);
    return true;
}

Обратите внимание на то, как выделяется память для временного буфера. В современном C++ в случае нехватки памяти оператор new вызывает исключение std::bad_alloc. Я вот, к примеру, очень долго этого не знал и после вызова new делал проверку в стиле if(ptr != 0).

Из недостатков класса wxHTTP хотелось бы отметить варнинги компилятора при сборке Release-версии программы. Надеюсь, в wxWidgets 2.9 это исправят.

6. Многопоточность в wxWidgets

Рассмотрим простейший пример. Есть поток, который в цикле инкрементирует значение счетчика до тех пор, пока значение некого флага не будет установлено равным true. Когда это происходит, поток завершает работу. Доступ к флагу регулируется с помощью мьютекса. По-моему, проще некуда.

class CounterThread: public wxThread {
    private:
        unsigned int m_cnt; // наш счетчик
        wxMutex* m_mtx; // мьютекс
        bool* m_exitFlag; // указатель на флаг
        // закрытый метод для чтения флага
        bool GetExitFlag() {
            bool exitFlag;
            m_mtx->Lock();
            exitFlag = *m_exitFlag;
            m_mtx->Unlock();
            return exitFlag;
        }
    public:
        CounterThread(wxThreadKind kind = wxTHREAD_DETACHED):
            wxThread(kind) {
            // do nothing
        }
        // инициализируем класс
        void Create(wxMutex* mtx, bool* exitFlag) {
            m_cnt = 0;
            m_mtx = mtx;
            m_exitFlag = exitFlag;
            wxThread::Create();
        }
        // получаем значение счетчика (ПОСЛЕ остановки потока!)
        unsigned int GetCounter() { return m_cnt; }
        // полезная нагрузка
        virtual void* Entry() {
            while(this->GetExitFlag() != true) {
                m_cnt++;
            }
            return 0;
        }
};

Теперь — как это использовать:

// ---- файл wxThreadsMain.h -----
// ...
class wxThreadsFrame: public wxFrame {
    public:
        // ...
    private:
        wxMutex m_Mutex;
        bool m_ExitFlag;
        CounterThread* m_CounterThreads[8];
        // ...
};
// ...
// ---- файл wxThreadsMain.cpp ----
// ...
void wxThreadsFrame::OnButton1Click(wxCommandEvent& event) {
    // число потоков
    const int ThreadsNumber =
        sizeof(m_CounterThreads)/sizeof(m_CounterThreads[0]);
    // флаг завершения работы потоков
    m_ExitFlag = false;
    // создаем потоки
    for(int i = 0; i < ThreadsNumber; i++) {
        try {
          m_CounterThreads[i] = new CounterThread(wxTHREAD_JOINABLE);
        } catch(bad_alloc& error) {
            // не удалось создать поток из-за нехватки памяти
            if(i > 0) {
                for(j = 0; j < i; j++) {
                    wxDELETE(m_CounterThreads[j]);
                }
            }
            wxMessageBox(_("Failed to start thread #") +
                         wxString::Format(_T("%d"), i));
            return;
        }

        m_CounterThreads[i]->Create(&m_Mutex, &m_ExitFlag);
    }
    // все потоки созданы, теперь запускаем их
    for(int i = 0; i < ThreadsNumber; i++) {
        m_CounterThreads[i]->Run();
    }
    // ждем одну секунду
    wxSleep(1);
    // устанавливаем флаг выхода
    m_Mutex.Lock();
    m_ExitFlag = true;
    m_Mutex.Unlock();
    // сюда будут сохранены значения счетчиков
    unsigned int counters[ThreadsNumber];
    // а сюда - конкатенация их младших бит
    wxString randomNumber;
    // ждем завершения потоков, освобождаем память
    for(int i = 0; i < ThreadsNumber; i++) {
        // ждем завершения
        m_CounterThreads[i]->Wait();
        // получаем значение счетчика
        counters[i] = m_CounterThreads[i]->GetCounter();
        // поток больше не нужен
        wxDELETE(m_CounterThreads[i]);
        // записываем в строку значение младшего бита счетчика
        randomNumber += wxString::Format(_T("%d"), counters[i] & 1);
    }
    // выводим результат
    ListBox1->Insert(randomNumber, 0);
}
// ...

Пример может показаться несколько бессмысленным, но на самом деле это не совсем так. Значения счетчиков сильно зависят от загруженности системы. Если добавить в метод CounterThread::Entry() какие-нибудь действия с оперативной памятью (например, сортировку массива «пузырьком») и с жестким диском (чтение/запись временного файла), счетчик будет вести себя еще более непредсказуемо. А случайные числа бывают очень полезны (например, в криптографии) и зачастую их не так уж просто получить.

Оптимальное число потоков равно числу имеющихся на борту ядер процессора, которое можно определить с помощью функции wxThread::GetCPUCount(). Предварительно следует задать достаточно большой размер массива m_CounterThreads или использовать vector.

7. Где взять дополнительную инфу?

Я продемонстрировал лишь некоторые возможности wxWidgets. За более подробной информацией обращайтесь к официальной документации на Doxygen, а также к wiki.

Из русскоязычных сайтов я бы рекомендовал блог Николая Тюшкова и сайт wxWidgets.info. Первый ведет и регулярно обновляет практикующий программист, использующие в своей работе, помимо прочего, wxWidgets. Второй не обновлялся с 2009 года, но содержит множество актуальных статей по теме.

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

Дополнение: Также вас могут заинтересовать заметки GUI-проиложение, написанное на Go с использованием GTK и Пишем GUI-приложение при помощи Python, GTK и Glade.

Метки: , , , .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.