IscanderChe
IscanderCheJuly 30, 2019, 3:06 a.m.

Simple Tracker project. Part 6: server. Server slots

We will consider slots that are directly related to the server in conjunction with the client. For now, let's focus on those slots that relate to project and task management and general application slots.

Let's start with connections. I moved them into a separate method void TrackerServer::createConnections() .

// trackerserver.cpp

// Метод создания подключений
void TrackerServer::createConnections()
{
    // Установка текущего проекта
    connect(projectView, &QListView::clicked, this, &TrackerServer::slotSetCurrentProject);
    connect(projectView, &QListView::activated, this, &TrackerServer::slotSetCurrentProject);

    // Подключение сигнала о смене статуса к обработчику
    connect(statusDelegate, &ComboBoxDelegate::commitData, this, &TrackerServer::slotChangeTaskStatus);

    // Создание новой задачи
    connect(newTaskButton, &QPushButton::clicked, this, &TrackerServer::slotNewTask);

    // Редактирование существующей задачи
    connect(editTaskButton, &QPushButton::clicked, this, &TrackerServer::slotEditTask);
    connect(taskView, &QTableView::doubleClicked, this, &TrackerServer::slotEditTask);

    // Удаление существующей задачи
    connect(deleteTaskButton, &QPushButton::clicked, this, &TrackerServer::slotDeleteTask);

    // Создание нового проекта
    connect(newProjectButton, &QPushButton::clicked, this, &TrackerServer::slotNewProject);

    // Архивация существующего проекта
    connect(archiveProjectButton, &QPushButton::clicked, this, &TrackerServer::slotArchiveProject);

    // Открытие архива проектов
    connect(openArchiveButton, &QPushButton::clicked, this, &TrackerServer::slotOpenArchive);

    // Открытие окна трекера
    connect(actionOpenWindow, &QAction::triggered, this, &TrackerServer::slotOpenTrackerWindow);

    // Открытие окна трекера по двойному клику на иконке в трее
    connect(trayIcon, &QSystemTrayIcon::activated, this, &TrackerServer::slotDoubleClickOpenTrackerWindow);

    // Завершение работы трекера
    connect(actionQuit, &QAction::triggered, qApp, &QApplication::quit);

    // Открытие окна настроек приложения
    connect(actionSettings, &QAction::triggered, this, &TrackerServer::slotOpenSettingsWindow);
}

To simplify working with project list items, we define an enumeration for Qt::DisplayRole and additional roles Qt::UserRole .

// trackerserver.h

enum ProjectProperty
{
    VisibleName = Qt::DisplayRole,
    Name = Qt::UserRole + 1,
    Id = Qt::UserRole + 2,
    Manual = Qt::UserRole + 3,
    Arch = Qt::UserRole + 4
};
// trackerserver.cpp

// Слот установки текущего проекта
void TrackerServer::slotSetCurrentProject(QModelIndex index)
{
    // Получаем элемент модели, соответствующий полученному индексу
    QStandardItem* item = projectListModel->itemFromIndex(index);
    // Получаем id текущего проекта
    projectCurrentId = item->data(ProjectProperty::Id).toInt();
    // Устанавливаем условие фильтрации таблицы задач по текущему id проекта
    taskFilterModel->setFilterFixedString(QString("%1").arg(projectCurrentId));

    // Обновляем представление таблицы задач
    updateTaskView();

    // Считаем количество строк в модели задач
    int rowNumber = taskFilterModel->rowCount();

    // Если количество строк больше нуля
    if(rowNumber > 0)
    {
        // Получаем индекс нулевого элемента модели задач
        int row = 0;
        QModelIndex index = taskFilterModel->index(row, CustomSortFilterProxyModel::E_COLUMN_NUMBER);
        // Устанавливаем в представлении текущий элемент с этим индексом
        taskView->setCurrentIndex(index);
        // Делаем доступными кнопки редактирования и удаления задач
        editTaskButton->setEnabled(true);
        deleteTaskButton->setEnabled(true);
    }
    else
    {
        // Иначе блокируем кнопки редактирования и удаления задач
        editTaskButton->setDisabled(true);
        deleteTaskButton->setDisabled(true);
    }

    // Делаем доступной кнопку создания новой задачи
    newTaskButton->setEnabled(true);

    // Проверяем, закрыты ли все задачи текущего проекта
    if(database->isClosedAllTasks(projectCurrentId))
        // Если да, то делаем доступной кнопку архивирования проекта
        archiveProjectButton->setEnabled(true);
    else
        // Если нет, то блокируем кнопку архивирования проекта
        archiveProjectButton->setDisabled(true);

    // Устанавливаем в виде текущим элемент с полученным индексом
    projectView->setCurrentIndex(index);
}

// Слот создания нового проекта
void TrackerServer::slotNewProject()
{
    // Объявляем диалог создания нового проекта
    QSharedPointer<NewProjectDialog> dialog(new NewProjectDialog());
    // Созадаём диалоговое окно
    if(dialog->exec() == QDialog::Accepted)
    {
        // Если в диалоге нажата кнопка "ОК",
        // получаем из диалога наименование проекта
        QString projectName = dialog->getProjectName();
        // Если наименование проекта не пустое
        if(!projectName.isEmpty())
        {
            // Добавляем проект со свойство "без контроля СКВ"
            database->insertNewProject(projectName, ProjectVCS::WithoutVCS);
            // Обновляем модель проектов
            projectTableModel->select();

            // Устанавливаем свойства элемента
            QString visibleProjectName = projectName;
            int row = projectTableModel->rowCount() - 1;
            int projectId =
                projectTableModel->record(row).value(idProjectCol).toInt();
            int projectManual = ProjectVCS::WithoutVCS;
            int projectArch = ProjectArchive::Active;

            visibleProjectName = "(M) " + projectName;

            QStandardItem* item = new QStandardItem();
            // Загружаем данные в элемент
            item->setData(visibleProjectName, ProjectProperty::VisibleName);
            item->setData(projectName, ProjectProperty::Name);
            item->setData(projectId, ProjectProperty::Id);
            item->setData(projectManual, ProjectProperty::Manual);
            item->setData(projectArch, ProjectProperty::Arch);

            // Загружаем элемент в модель представления проектов
            projectListModel->appendRow(item);

            // Устанавливаем текущий id проекта равный id нового проекта
            projectCurrentId = projectId;
            // Получаем индекс элемента нового проекта в модели представления
            int column = 0;
            QModelIndex currentProjectIndex = projectListModel->index(row, column);
            // Устанавливаем условие фильтрации таблицы задач по текущему id проекта
            taskFilterModel->setFilterFixedString(QString("%1").arg(projectCurrentId));
            // Устанвливаем текущий проект по индексу
            slotSetCurrentProject(currentProjectIndex);

            // Поскольку новый проект пустой, блокируем кнопки удаления и редактирования задач,
            // и делаем доступной кнопку создания новой задачи
            editTaskButton->setDisabled(true);
            deleteTaskButton->setDisabled(true);
            newTaskButton->setEnabled(true);
        }
    }
}

// Слот архивации проекта
void TrackerServer::slotArchiveProject()
{
    // Определяем индекс текущего проекта
    QModelIndex currentProjectIndex = projectView->currentIndex();
    int row = currentProjectIndex.row();

    // Скрываем проект в представлении
    projectView->setRowHidden(row, true);

    // Устанавливаем для элемента модели пресдтавления свойство архивации проекта
    QStandardItem* item = projectListModel->item(row);
    item->setData(ProjectArchive::Archive, ProjectProperty::Arch);
    projectListModel->setItem(row, item);

    // Получаем из элемента модели представления id проекта
    int projectId = item->data(ProjectProperty::Id).toInt();
    // Архивируем проект
    database->archiveProject(projectId);

    // Блокируем кнопку архивации проектов
    archiveProjectButton->setDisabled(true);

    // Определяем, остались ли видимые элементы в списке проектов
    int numberRows = projectListModel->rowCount();
    int numberRowsNotHidden = 0;
    int firstRowNotHidden = 0;

    for(int row = 0; row < numberRows; ++row)
    {
        if(!projectView->isRowHidden(row))
        {
            ++numberRowsNotHidden;
            firstRowNotHidden = row;
            break;
        }
    }

    if(numberRowsNotHidden > 0)
    {
        // Если видимые элементы остались, определем индекс первого
        QModelIndex index = projectListModel->index(firstRowNotHidden, 0);
        // Устанавливаем этот элемент текущим проектом
        slotSetCurrentProject(index);
    }
    else
    {
        // Если видимых элементов нет, обнуляем текущий id проекта
        projectCurrentId = 0;
        // Устанавливаем условие фильтрации таблицы задач по текущему id проекта
        taskFilterModel->setFilterFixedString(QString("%1").arg(projectCurrentId));
        // Обновляем представление таблицы задач
        updateTaskView();
    }

}

// Слот открытия архива
void TrackerServer::slotOpenArchive()
{
    // Объявляем диалог открытия архива
    QSharedPointer<OpenArchiveDialog> dialog(new OpenArchiveDialog(projectListModel));
    // Если подтверждено извлечение из архива проекта/проектов
    if(dialog->exec() == QDialog::Accepted)
    {
        // Получаем список id извлекаемых из архива проектов
        QList<int> projectIdList = dialog->getListProjectId();
        int lastId = projectIdList.last();
        // Извлекаем проекты из архива
        foreach(int id, projectIdList)
            database->extractProject(id);

        // Обновляем модель списка проектов
        updateProjectListModel();
        // Обновляем виджет списка проектов
        updateProjectView();

        int numberRows = projectListModel->rowCount();

        // Определяем индес последнего извлечённого проекта
        QModelIndex lastIndex;
        for(int row = 0; row < numberRows; ++row)
        {
            QStandardItem* item = projectListModel->item(row);
            int projectId = item->data(ProjectProperty::Id).toInt();
            if(projectId == lastId)
                lastIndex = projectListModel->indexFromItem(item);
        }

        // Устанавливаем текущим последний извлечённый проект
        slotSetCurrentProject(lastIndex);
    }
}

// Слот изменения состояния задачи
void TrackerServer::slotChangeTaskStatus(QWidget* /* editor */)
{
    // Обновляем модель SQL-таблицы задач
    taskSqlTableModel->select();

    if(database->isClosedAllTasks(projectCurrentId))
        // Если все задачи закрыты, делаем доступной кнопку архивации проектов
        archiveProjectButton->setEnabled(true);
    else
        // Если нет, блокируем кнопку архивации проектов
        archiveProjectButton->setDisabled(true);
}

// Слот создания новой задачи
void TrackerServer::slotNewTask()
{
    // Объявляем диалог создания / редактирования задачи
    QSharedPointer<NewEditTaskDialog> dialog(new NewEditTaskDialog());
    // Если создание новой задачи подтверждено
    if(dialog->exec() == QDialog::Accepted)
    {
        // Получаем из диалога тип и описание задачи
        QString type = dialog->getType();
        QString description = dialog->getDescription();
        // Если тип задачи и описание не пустые
        if(!type.isEmpty() && !description.isEmpty())
        {
            // Добавляем новую задачу
            database->insertNewTask(projectCurrentId, type, description);
            // Обновляем модель SQL-таблицы задач
            taskSqlTableModel->select();
            // Обновляем представление задач
            updateTaskView();
            // Определяем номер строки созданной задачи
            int row = taskFilterModel->rowCount() - 1;
            // Определяем индекс новой задачи в модели представления задач
            QModelIndex index = taskFilterModel->index(row, CustomSortFilterProxyModel::E_COLUMN_NUMBER);
            // Устанавливаем эту задачу текущей в представлении
            taskView->setCurrentIndex(index);
            // Делаем доступными кнопки редактирования и удаления задач
            editTaskButton->setEnabled(true);
            deleteTaskButton->setEnabled(true);
        }
    }
}

// Слот редактирования задачи
void TrackerServer::slotEditTask()
{
    // Объявляем диалог создания / редактирования задачи
    QSharedPointer<NewEditTaskDialog> dialog(new NewEditTaskDialog());

    // Извлекаем из текущего индекса задачи id, тип и описание задачи
    QModelIndex currentIndex = taskView->currentIndex();
    QModelIndex idIndex = taskFilterModel->index(currentIndex.row(), CustomSortFilterProxyModel::E_COLUMN_NUMBER);
    QModelIndex typeIndex = taskFilterModel->index(currentIndex.row(), CustomSortFilterProxyModel::E_COLUMN_TYPE);
    QModelIndex descriptionIndex = taskFilterModel->index(currentIndex.row(), CustomSortFilterProxyModel::E_COLUMN_DESCRIPTION);

    QString type = taskFilterModel->data(typeIndex, Qt::DisplayRole).toString();
    QString description = taskFilterModel->data(descriptionIndex, Qt::DisplayRole).toString();
    int id = taskFilterModel->data(idIndex, Qt::DisplayRole).toInt();

    // Передаём в диалог тип и описание задачи
    dialog->setType(type);
    dialog->setDescription(description);

    // Если редактирование задачи подтверждено
    if(dialog->exec() == QDialog::Accepted)
    {
        // Если тип или описание задачи отредактированы
        if(type != dialog->getType() || description != dialog->getDescription())
        {
            // Редактируем задачу в базе
            database->editTask(id, dialog->getType(), dialog->getDescription());
            // Обновляем модель SQL-таблицы задач
            taskSqlTableModel->select();
            // Обновляем представление задач
            updateTaskView();
            // Устанавливаем отредактированную задачу текущей в представлении
            taskView->setCurrentIndex(currentIndex);
        }
    }
}

// Слот удаления задачи
void TrackerServer::slotDeleteTask()
{
    // Объявляем диалог удаления задачи
    QSharedPointer<DeleteTaskDialog> dialog(new DeleteTaskDialog());

    // Извлекаем из индекса id задачи
    QModelIndex currentIndex = taskView->currentIndex();
    QModelIndex idIndex = taskFilterModel->index(currentIndex.row(), CustomSortFilterProxyModel::E_COLUMN_NUMBER);
    int id = taskFilterModel->data(idIndex, Qt::DisplayRole).toInt();

    // Если удаление задачи подтверждено
    if(dialog->exec() == QDialog::Accepted)
    {
        // Удаляем задачу из базы
        database->deleteTask(id);
        // Обновляем модель SQL-таблицы задач
        taskSqlTableModel->select();
        // Обновляем представление задач
        updateTaskView();
        // Определяем, остались ли видимые задачи
        int numberRows = taskFilterModel->rowCount();
        if(numberRows > 0)
        {
            // Если остались, делаем текущей первую
            int row = 0;
            QModelIndex index = taskFilterModel->index(row, CustomSortFilterProxyModel::E_COLUMN_NUMBER);
            taskView->setCurrentIndex(index);
        }
        else if(numberRows == 0)
        {
            // Если нет, блокируем кнопки редактирования и удаления задач
            editTaskButton->setDisabled(true);
            deleteTaskButton->setDisabled(true);
        }
    }
}

// Слот открытия окна трекера
void TrackerServer::slotOpenTrackerWindow()
{
    setVisible(true);
}

// Слот открытия окна трекера по двойному клику на иконке в трее

void TrackerServer::slotDoubleClickOpenTrackerWindow(QSystemTrayIcon::ActivationReason reason)
{
    if(reason == QSystemTrayIcon::DoubleClick)
        setVisible(true);
}

// Слот открытия окна настроек программы
void TrackerServer::slotOpenSettingsWindow()
{
    QSharedPointer<SettingsDialog> dialog(new SettingsDialog());
    dialog->exec();
}

Creating a project through the tracker is limited only to projects without VCS control. This is done to simplify the functionality of the tracker itself. Projects under SLE control are tracked by the tracker using the following code.

// trackerserver.cpp

// Метод инициализации проектов под СКВ
void TrackerServer::initProjectsWithVCS()
{
    // В переменную записываем путь к репозиториям из объекта settings класса QSettings
    QString pathToProjects = settings.value("PathToProjects", "").toString();
    if(!pathToProjects.isEmpty())
    {
        // Если путь не пустой, формируем из списка репозиториев список проектов под СКВ
        QDir dir(pathToProjects);
        QStringList projectFileList;

        foreach(QFileInfo item, dir.entryInfoList())
        {
            if(item.isDir())
            {
                QString dirPath = item.absoluteFilePath();
                QString dirName = item.fileName();
                // Критерием проекта под СКВ является наличие в репозитории в папке hooks
                // файла log.txt, куда хуки pre-commit и post-commit вносят информацию
                // о номере коммита и текст коммита с номером закрываемой задачи
                QString fileNamePath = dirPath + "/hooks/log.txt";
                QFile file(fileNamePath);
                if(file.exists())
                    projectFileList << dirName;
            }
        }

        if(!projectFileList.isEmpty())
        {
            // Если список проектов под СКВ не пустой, извлекаем из базы данных
            // список проектов и соответствующих им id проектов
            QStringList projectNameList;
            QList<int> projectIdList;

            projectTableModel->select();

            int numberRows = projectTableModel->rowCount();

            for(int row = 0; row < numberRows; ++row)
            {
                int projectId =
                    projectTableModel->record(row).value(idProjectCol).toInt();
                QString projectName =
                    projectTableModel->record(row).value(visibleNameProjectCol).toString();
                int manualFlag =
                    projectTableModel->record(row).value(manualPropertyCol).toInt();
                if(manualFlag == ProjectVCS::WithVCS)
                {
                    projectIdList << projectId;
                    projectNameList << projectName;
                }
            }

            // Формируем список новых проектов, отсутствующих в базе
            QStringList newProjectList;

            foreach(QString file, projectFileList)
            {
                int count = 0;
                foreach(QString project, projectNameList)
                {
                    if(file == project)
                        ++count;
                }
                if(count == 0)
                    newProjectList << file;
            }

            // Формируем список проектов, подлежащих автоматической архивации,
            // если репозитории этих проектов были удалены
            QStringList archiveProjectList;

            foreach(QString project, projectNameList)
            {
                int count = 1;
                foreach(QString file, projectFileList)
                {
                    if(project == file)
                        --count;
                }
                if(count > 0)
                    archiveProjectList << project;
            }

            // Вносим новые проекты по списку
            foreach(QString project, newProjectList)
                    database->insertNewProject(project, ProjectVCS::WithVCS);

            // Архивируем проекты по списку
            foreach(QString project, archiveProjectList)
            {
                for(int i = 0; i < projectNameList.size(); ++i)
                {
                    if(project == projectNameList.at(i))
                    {
                        database->archiveProject(projectIdList.at(i));
                        // Получае перечень задач для данного проекта
                        QList<int> taskIdList = database->getIdListForProject(projectIdList.at(i));
                        foreach(int taskId, taskIdList)
                        {
                            if(!database->isClosed(taskId))
                                // Если задача не закрыта, закрываем её с пометкой "forced"
                                database->closeTask(taskId, "forced");
                        }
                    }
                }
            }

            // Обновляем SQL-модель таблицы задач
            taskSqlTableModel->select();
        }
    }
}
We recommend hosting TIMEWEB
We recommend hosting TIMEWEB
Stable hosting, on which the social network EVILEG is located. For projects on Django we recommend VDS hosting.

Do you like it? Share on social networks!

Evgenii Legotckoi
  • July 30, 2019, 4:24 a.m.

В общем-то достаточно написать так

// trackerserver.h

enum ProjectProperty
{
    VisibleName = Qt::DisplayRole,
    Name = Qt::UserRole + 1,
    Id,
    Manual,
    Arch
};

Все enum после Name будут и так увеличиваться на единицу.

Объявление переменной row не нужно

        int row = 0;
        QModelIndex index = taskFilterModel->index(row, CustomSortFilterProxyModel::E_COLUMN_NUMBER);
        // Устанавливаем в представлении текущий элемент с этим индексом
        taskView->setCurrentIndex(index);

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

        taskView->setCurrentIndex(taskFilterModel->index(0, CustomSortFilterProxyModel::E_COLUMN_NUMBER));

foreach выполняет глубокое копирование , лучше переходите полноценно на современные стандарты C++. То есть вместо этого

        foreach(int id, projectIdList)
            database->extractProject(id);

Можно написать так

        for(int id : projectIdList)
            database->extractProject(id);

К тому же foreach - это макрос, а значит получаем работу препроцессора, которае в случае конструкций языка, например for, не происходит. Конечно есть случае, где foreach ещё оправдан, но их мало на самом деле.

Там где у вас QModelIndex берутся, также можно переписать в одну строку, как в предыдущем варианте. В конечном счёте и так ясно, что за это индексы по enum CustomSortFilterProxyModel::E_COLUMN_TYPE например.

Думаю, что вот это

int numberRows = taskFilterModel->rowCount();
        if(numberRows > 0)
        {
            // Если остались, делаем текущей первую
            int row = 0;
            QModelIndex index = taskFilterModel->index(row, CustomSortFilterProxyModel::E_COLUMN_NUMBER);
            taskView->setCurrentIndex(index);
        }
        else if(numberRows == 0)
        {
            // Если нет, блокируем кнопки редактирования и удаления задач
            editTaskButton->setDisabled(true);
            deleteTaskButton->setDisabled(true);
        }

Можно переписать так

// rowCount не должен возвращать -1 в принципе, поэтому второе условие просто лишнее
if (taskFilterModel->rowCount() > 0)
{
    // Одной строкой достаточно, и так ясно, что происходит благодаря CustomSortFilterProxyModel::E_COLUMN_NUMBER
    taskView->setCurrentIndex(taskFilterModel->index(0, CustomSortFilterProxyModel::E_COLUMN_NUMBER));
}
else
{
    editTaskButton->setDisabled(true);
    deleteTaskButton->setDisabled(true);
}

Вот это лишнее переусложнение

void TrackerServer::slotOpenSettingsWindow()
{
    QSharedPointer<SettingsDialog> dialog(new SettingsDialog());
    dialog->exec();
}

Просто создайте на стеке и тогда по окончанию работы диалог сам уничтожиться, не говоря уже о том, что в предыдущем варианте лишнее создание умного указателя с созданием объекта диалога и его копированием.

void TrackerServer::slotOpenSettingsWindow()
{
    SettingsDialog dialog;
    dialog.exec();
}

Вот это

QList<int> taskIdList = database->getIdListForProject(projectIdList.at(i));
foreach(int taskId, taskIdList)
{
    if(!database->isClosed(taskId))
        // Если задача не закрыта, закрываем её с пометкой "forced"
        database->closeTask(taskId, "forced");
}

Можно ак переписать

for (int taskId :  database->getIdListForProject(projectIdList.at(i)))
{
    if (!database->isClosed(taskId))
        database->closeTask(taskId, "forced");
}

P/S/ Вы можете в статье добавлять рекомендуемые статьи, например, список всех статей по Simple Tracker. Тогда пользователи смогу лучше ориентироваться по всем вашим статьям.

IscanderChe
  • July 30, 2019, 5:19 a.m.

Все enum после Name будут и так увеличиваться на единицу.

Это мне самому нужно было, чтобы при переписываниии с Qt::UserRole + n на enum не запутаться. Сам себе проблему придумал, поскольку сразу не ввёл enum.

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

Тоже пишу так для себя, поскольку ещё не до окнца освоился с QModelIndex.

lang-cpp for(int id : projectIdList)

Понял. Да, спасибо.

// rowCount не должен возвращать -1 в принципе, поэтому второе условие просто лишнее

Согласен.

Просто создайте на стеке и тогда по окончанию работы диалог сам уничтожиться, не говоря уже о том, что в предыдущем варианте лишнее создание умного указателя с созданием объекта диалога и его копированием.

Понял.

Comments

Only authorized users can post comments.
Please, Log in or Sign up
AD

C ++ - Test 004. Pointers, Arrays and Loops

  • Result:50points,
  • Rating points-4
m

C ++ - Test 004. Pointers, Arrays and Loops

  • Result:80points,
  • Rating points4
m

C ++ - Test 004. Pointers, Arrays and Loops

  • Result:20points,
  • Rating points-10
Last comments
ИМ
Игорь МаксимовNov. 22, 2024, 11:51 a.m.
Django - Tutorial 017. Customize the login page to Django Добрый вечер Евгений! Я сделал себе авторизацию аналогичную вашей, все работает, кроме возврата к предидущей странице. Редеректит всегда на главную, хотя в логах сервера вижу запросы на правильн…
Evgenii Legotckoi
Evgenii LegotckoiOct. 31, 2024, 2:37 p.m.
Django - Lesson 064. How to write a Python Markdown extension Добрый день. Да, можно. Либо через такие же плагины, либо с постобработкой через python библиотеку Beautiful Soup
A
ALO1ZEOct. 19, 2024, 8:19 a.m.
Fb3 file reader on Qt Creator Подскажите как это запустить? Я не шарю в программировании и кодинге. Скачал и установаил Qt, но куча ошибок выдается и не запустить. А очень надо fb3 переконвертировать в html
ИМ
Игорь МаксимовOct. 5, 2024, 7:51 a.m.
Django - Lesson 064. How to write a Python Markdown extension Приветствую Евгений! У меня вопрос. Можно ли вставлять свои классы в разметку редактора markdown? Допустим имея стандартную разметку: <ul> <li></li> <li></l…
d
dblas5July 5, 2024, 11:02 a.m.
QML - Lesson 016. SQLite database and the working with it in QML Qt Здравствуйте, возникает такая проблема (я новичок): ApplicationWindow неизвестный элемент. (М300) для TextField и Button аналогично. Могу предположить, что из-за более новой верси…
Now discuss on the forum
m
moogoNov. 22, 2024, 7:17 a.m.
Mosquito Spray System Effective Mosquito Systems for Backyard | Eco-Friendly Misting Control Device & Repellent Spray - Moogo ; Upgrade your backyard with our mosquito-repellent device! Our misters conce…
Evgenii Legotckoi
Evgenii LegotckoiJune 24, 2024, 3:11 p.m.
добавить qlineseries в функции Я тут. Работы оень много. Отправил его в бан.
t
tonypeachey1Nov. 15, 2024, 6:04 a.m.
google domain [url=https://google.com/]domain[/url] domain [http://www.example.com link title]
NSProject
NSProjectJune 4, 2022, 3:49 a.m.
Всё ещё разбираюсь с кешем. В следствии прочтения данной статьи. Я принял для себя решение сделать кеширование свойств менеджера модели LikeDislike. И так как установка evileg_core для меня не была возможна, ибо он писался…

Follow us in social networks