Диалог выбора файлов на 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 m_impl;
};
При реализации древовидной модели удобно иметь само дерево. Элемент дерева представлен структурой TreeNode
struct TreeNode
{
enum Type
{
File, Directory
};
std::string filename;
std::string path;
TreeNode* parent;
std::vector 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->filenamefilename;
});
return count;
} catch (const filesystem_error&)
{
return 0;
}
}
};
Метод loadChildren осуществляет чтение файловой системы и подгрузку узлов. Узлы будут подгружаться по требованию, а именно тогда, когда у модели запросят rowCount. Корень дерева создается в конструкторе FileSystemModelPrivate
struct FileSystemModelPrivate
{
FileSystemModelPrivate() :
root(new TreeNode)
{
root->filename = "/";
root->path = "/";
}
std::unique_ptr 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(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(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(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(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(
indx.data(FileSystemModel::Path));
list << pt;
}
return list;
}