Инженер-программист
0,5
рейтинг
25 января в 23:15

Разработка → Диалог выбора файлов на Wt

C++*


По работе пришлось сделать несколько своих компонентов на Wt: визард, диалог выбора каталогов и файлов на устройстве. Решил выложить на GitHub, может кому-нибудь понадобится. Под катом будет простой диалог выбора файлов на Wt+Boost.

Wt виджет-ориентированный фреймворк, который по API похож на Qt. То что Wt «похож» на Qt, но не Qt!

Модель данных


В основе абстрактная модель Wt::WAbstractItemModel(почти как QAbstractItemModel):

class FileSystemModel: public Wt::WAbstractItemModel
{
public:

    enum Roles
    {
        Name = Wt::UserRole + 1, Path = Wt::UserRole
    };

    FileSystemModel(Wt::WObject* parent = nullptr);

    bool isFile(const Wt::WModelIndex& index) const;
    bool isDir(const Wt::WModelIndex& index) const;

    virtual int columnCount(const Wt::WModelIndex& parent =
            Wt::WModelIndex()) const;

    virtual int rowCount(const Wt::WModelIndex& parent =
            Wt::WModelIndex()) const;

    virtual Wt::WModelIndex parent(const Wt::WModelIndex& index) const;

    virtual boost::any data(const Wt::WModelIndex& index, int role =
            Wt::DisplayRole) const;

    virtual Wt::WModelIndex index(int row, int column,
            const Wt::WModelIndex& parent = Wt::WModelIndex()) const;

    virtual boost::any headerData(int section, Orientation orientation =
            Horizontal, int role = DisplayRole) const;
private:

    std::unique_ptr<FileSystemModelPrivate> m_impl;
};

При реализации древовидной модели удобно иметь само дерево. Элемент дерева представлен структурой TreeNode:

struct TreeNode
{
    enum Type
    {
        File, Directory
    };

    std::string filename;
    std::string path;
    TreeNode* parent;
    std::vector<TreeNode*> children;
    Type type;
    bool childrenLoaded;

    TreeNode(TreeNode* prnt = nullptr) :
            parent(prnt),
            type(Directory),
            childrenLoaded(false)
    {
        if (parent)
        {
            parent->children.push_back(this);
        }
    }

    ~TreeNode()
    {
        parent = nullptr;
        for (TreeNode* child : children)
        {
            delete child;
        }
        children.clear();
    }

    size_t loadChildren()
    {
        if (childrenLoaded)
        {
            return children.size();
        }

        boost::filesystem::path p(path);

        childrenLoaded = true;
        size_t count = 0;

        try
        {
            for (directory_iterator iter(p), end; iter != end; ++iter)
            {
                auto itm = new TreeNode(this);
                itm->filename = iter->path().filename().string();
                itm->path = iter->path().string();
                itm->type =
                        is_directory(iter->path()) ?
                                TreeNode::Directory : TreeNode::File;
                ++count;
            }

            std::sort(children.begin(), children.end(),
                    [](const TreeNode* a, const TreeNode* b)
                    {
                        return a->filename<b->filename;
                    });

            return count;
        } catch (const filesystem_error&)
        {
            return 0;
        }
    }
};

Метод loadChildren осуществляет чтение файловой системы и подгрузку узлов. Узлы будут подгружаться по требованию, а именно тогда, когда у модели запросят rowCount. Корень дерева создается в конструкторе FileSystemModelPrivate:

struct FileSystemModelPrivate
{
    FileSystemModelPrivate() :
            root(new TreeNode)
    {
        root->filename = "/";
        root->path = "/";
    }

    std::unique_ptr<TreeNode> root;
};

В Wt так же как и в Qt есть метод createIndex создающий модельный индекс(WModelIndex) и позволяющий передавать указатель на TreeNode.

Остальной код очень прост:

FileSystemModel::FileSystemModel(WObject* parent) :
        WAbstractItemModel(parent),
        m_impl(new FileSystemModelPrivate)
{
}

int FileSystemModel::columnCount(const WModelIndex& parent) const
{
    return 1;
}

int FileSystemModel::rowCount(const WModelIndex& parent) const
{
    if (parent.isValid())
    {

        TreeNode* node = static_cast<TreeNode*>(parent.internalPointer());
        if (node == nullptr || node->type == TreeNode::File)
        {
            return 0;
        }

        return node->childrenLoaded ?
                node->children.size() : node->loadChildren();

    }
    else
    {
        //Unix root '/'
        return 1;
    }

    return 0;
}

WModelIndex FileSystemModel::parent(const WModelIndex& index) const
{
    if (!index.isValid())
    {
        return WModelIndex();
    }

    auto node = static_cast<TreeNode*>(index.internalPointer());
    if (node->parent == nullptr)
    {
        return WModelIndex();
    }

    if (node->parent->parent == nullptr)
    {
        return createIndex(0, 0, m_impl->root.get());
    }

    const auto grand = node->parent->parent;
    const auto parent = node->parent;

    const auto res = std::lower_bound(grand->children.cbegin(),
            grand->children.cend(), parent);

    const size_t row = std::distance(grand->children.cbegin(), res);

    return createIndex(row, 0, parent);
}

boost::any FileSystemModel::data(const WModelIndex &index, int role) const
{
    if (!index.isValid())
    {
        return boost::any();
    }

    auto node = static_cast<TreeNode*>(index.internalPointer());
    if (node == nullptr)
    {
        return boost::any();
    }

    switch (role)
    {
    case DisplayRole:
    {
        return node->filename;
    }
    case Path:
    {
        return node->path;
    }
    case DecorationRole:
    {
        try
        {
            return FILE_SYSTEM_ICONS.at(node->type);
        } catch (...)
        {

            return boost::any();
        }
    }
        break;
    default:
        return boost::any();
    }
}

WModelIndex FileSystemModel::index(int row, int column,
        const Wt::WModelIndex& parent) const
{
    if (!parent.isValid())
    {
        return createIndex(0, 0, m_impl->root.get());
    }
    TreeNode* pNode = static_cast<TreeNode*>(parent.internalPointer());
    if (pNode == nullptr)
    {
        return WModelIndex();
    }

    return createIndex(row, column, pNode->children[row]);
}
boost::any FileSystemModel::headerData(int section, Orientation orientation,
        int role) const
{
    if (role == DisplayRole && orientation == Horizontal)
    {
        return "File name";
    }

    return boost::any();
}


Диалог выбора файлов



Диалог выбора файлов наследуется от Wt::WDialog и имеет интерфейс:

class FileDialog: public Wt::WDialog
{
public:

    FileDialog(WObject* parent = nullptr);
    virtual void accept();

    Wt::WStringList selected() const;

private:

    Wt::WTreeView* m_view;
    FileSystemModel* m_fs;
};

Класс FileDialog содержит в себе нашу модель и древовидное представление Wt::WTreeView.

Рассмотрим конструктор:

FileDialog::FileDialog(WObject* parent) :
        WDialog(parent),
        m_view(new WTreeView()),
        m_fs(new FileSystemModel(this))

{
    setWindowTitle("Selecting files and directories");

    auto cancel = new WPushButton("Cancel", footer());
    cancel->clicked().connect(this, &WDialog::reject);

    m_view->setModel(m_fs);
    m_view->setSelectionBehavior(SelectItems);
    m_view->setSelectionMode(ExtendedSelection);

    auto select = new WPushButton("Select", footer());
    select->clicked().connect(this, &FileDialog::accept);
    m_view->setSortingEnabled(false);
    m_view->setHeaderHeight(0);
    m_view->expandToDepth(1);

    auto layout = new WVBoxLayout;
    layout->addWidget(m_view);
    contents()->setLayout(layout);

    resize(600, 500);
}

Внутри тела конструктора создаются две кнопки, которые размещаются в нижнем колонтитуле, вертикальный layout, который размещается на месте содержимого. Вертикальный layout нужен для выставления размеров WTreeView, иначе представление может выйти за пределы диалогового окна.

Конструкция
cancel->clicked().connect(this, &WDialog::reject);

это механизм сигнал/слотов на базе Boost.Signals2(или Boost.Signals, зависит от версии Boost). Два оставшихся методов тривиальны
void FileDialog::accept()
{
    const auto indxs = m_view->selectedIndexes();
    if (indxs.size() > 0)
    {
        WDialog::accept();
    }
}

Wt::WStringList FileDialog::selected() const
{
    WStringList list;
    const auto indxs = m_view->selectedIndexes();
    for (auto indx : indxs)
    {
        const WString pt = boost::any_cast<std::string>(
                indx.data(FileSystemModel::Path));
        list << pt;
    }

    return list;
}
Дмитрий @RPG18
карма
26,2
рейтинг 0,5
Инженер-программист
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (7)

  • НЛО прилетело и опубликовало эту надпись здесь
    • 0
      На стороне сервера. В моей задаче пользователь выбирает какие файлы и каталоги он хочет бекапить на своем NAS'е.
      • НЛО прилетело и опубликовало эту надпись здесь
        • 0
          Если я не ошибаюсь, диалог выбора файла у клиента всегда предоставляется браузером, ибо js песочнице доступ к файловой системе закрыт.
  • 0
    Как долго работаете с Wt? Можете поделиться впечатлениями о фреймворке? Ну там плюсы/минусы. Честно говоря веб разработка на плюсах даже с фреймворками кажется тяжким делом.
    • 0
      Пятый месяц. В принципе работает. Несколько раз приходилось обращаться к своим frontend разработчикам, что бы те сделали верстку на html/css, которую впоследствии шаблонизировал.

      Для меня плюс это схожесть с Qt, что позволило сделать Wizard

    • 0
      Из минусов: немного кривая поддержка Bootstrap 3, и нужно быть готовым что, что то может свалится в реализации сервера(многопоточный на базе Boost.Asio).

      Если бы хорошо знал бы фронтенд разработку, то взял бы Tufão т. к. изначально использовался Qt.

      Повторюсь, что задача не создать Web сайт, а приложение с Web интерфейсом, которое крутится на NAS(Synology, QNAP)

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