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



    По работе пришлось сделать несколько своих компонентов на 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;
    }
    
    Метки:
    • +10
    • 5,9k
    • 7
    Поделиться публикацией
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 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)

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