Пишем сайт с новостями на Wt

в 15:30, , рубрики: Без рубрики

Для начала расскажу немного о себе. Я студент 4 курса МАИ специальности «ЭВМ, комплексы, системы и сети», а также начинающий программист на С++/Qt. Очень давно я хотел научиться веб-разработке, но до изучения всех необходимых языков и технологий руки никак не доходили. Недавно на Хабре появилась статья о создании веб-приложений с помощью C++ библиотеки Wt, и я решил поближе познакомиться с этой библиотекой.

Подготовка рабочего места

Весь код я писал под Gentoo-linux в Qt Creator. Если вы решите делать так же, то для начала придётся подготовить окружение под наши нужды.
Первое, что надо сделать — это создать проект. Я использовал «Простой проект на С++» без поддержки Qt. Хотя Wt можно использовать совместно с Qt, в данном случае нам это не понадобится.
Пишем сайт с новостями на Wt

Я назвал проект MyApp. Далее откроем файл MyApp.pro и немного его отредактируем. А именно, в моём случае, надо добавить такие строки:

LIBS += -L/usr/lib -lwt -lwthttp -lwtdbo -lwtdbosqlite3 -I/usr/local/include
LIBS += -L/usr/local/lib -lwthttp -lwt -lboost_regex -lboost_signals
LIBS += -lboost_system -lboost_thread -lboost_filesystem -lboost_date_time -lpthread -lcrypt

Ещё нам надо будет слинковать папку resources в то место, откуда мы будем запускать наш сайт. Например, так:
ln -s /usr/share/Wt/resources/ ~/build-MyApp-Qt5-Отладка/

Подготовку на этом можно считать законченной.

Итак, начнём...

Чтобы запустить пустое приложение, достаточно будет написать такой код:

#include <iostream>
#include <Wt/WServer>
#include <Wt/WApplication>

using namespace Wt;

WApplication *createWidget(const WEnvironment& env)
{
    WApplication *app=new WApplication(env); //Создаём пустое веб-приложение
    app->setTitle("My Site"); //Устанавливаем заголовок страницы (то же, что в html <title></title>)
    app->setTheme(new Wt::WBootstrapTheme());  //Будем использовать стили bootstrap
    return app;
}

int main(int argc, char **argv)
{
  Wt::WServer server(argv[0]); //Создаём сервер

  server.setServerConfiguration(argc, argv, WTHTTP_CONFIGURATION); //Конфигурируем его

  server.addEntryPoint(Wt::Application,createWidget); //Добавляем "точку входа"

  if (server.start()) {
    int sig = Wt::WServer::waitForShutdown(); //Если заработало, ждём завершения
    std::cerr << "Shutting down: (signal = " << sig << ")" << std::endl;
    server.stop();
  }
}

Мы можем запустить всё это на localhost на порту 8080 командой:

./myapp --docroot . --http-address 0.0.0.0 --http-port 8080

Процесс запуска можно автоматизировать, для этого параметры запуска нужно прописать в строке «Проекты->Запуск->Параметры»

Пишем сайт с новостями на Wt

Таким образом, мы получаем веб-сервер с пустой страничкой, доступ к которой можно получить по адресу http://localhost:8080/

Добавим меню.

Приступим к заполнению. Для начала сделаем меню для нашего сайта. Для этого создадим новый класс, я назвал его «Page».
page.h:

#ifndef PAGE_H
#define PAGE_H

#include <Wt/WContainerWidget>
#include <Wt/WText>
#include <Wt/WString>
#include <Wt/WNavigationBar>
#include <Wt/WMenu>
#include <Wt/WStackedWidget>

class Page : public Wt::WContainerWidget
{
public:
    Page(WContainerWidget *parent = 0);
private:
    Wt::WStackedWidget *contentsStack;
    Wt::WNavigationBar *NavBar;
    Wt::WMenu *LeftMenu;
};

#endif // PAGE_H

page.cpp:

#include "page.h"

using namespace Wt;

Page::Page(WContainerWidget *parent):WContainerWidget(parent)
{
    setStyleClass("container");
    contentsStack = new Wt::WStackedWidget(); //В этом "контейнере" будет появляться виджет, соответствующий текущему пункту меню
    contentsStack->setStyleClass("container");
    contentsStack->setPadding(48,Wt::Top); //Потому, что меню будет фиксированным сверху и закроет собой часть этого "контейнера"

    NavBar = new WNavigationBar(this); //Сама полосочка меню
    NavBar->setTitle("MyApp","http://localhost:8080"); //Красивый заголовок перед пунктами меню, а заодно ссылка
    NavBar->setResponsive(true); //Наша полоска будет адаптироваться под размер экрана
    NavBar->addStyleClass("container"); //Стиль bootstrap
    NavBar->setPositionScheme(Wt::Fixed); //Делаем полоску меню фиксированной
    LeftMenu=new WMenu(contentsStack,this); //Создаём новое меню. Заметьте, это именно пункты меню.
    NavBar->addMenu(LeftMenu); //Вставляем пункты меню в полоску

    LeftMenu->addItem(WString::fromUTF8("Новости"),new WText(WString::fromUTF8("Здесь будут новости"))); //Добавляем в меню пункт "Новости".

    addWidget(contentsStack); //Мы создавали контейнер без родителя, и он не будет отображаться, пока мы его куда-нибудь не добавим.
}

Заодно надо добавить строчку в main.cpp

...
WApplication *createWidget(const WEnvironment& env)
{
    WApplication *app=new WApplication(env);
    app->setTitle("My Site"); 
    app->setTheme(new Wt::WBootstrapTheme());  
    new Page(app->root()); //Вот эту!
    return app;
}
...

Проверяем:

Пишем сайт с новостями на Wt

Допустим. А где же новости?

Сейчас и до них дойдём. Для начала нам надо научиться работать с БД.
В Wt работа с БД, как по мне, реализована довольно странно, но, когда я разобрался с принципом, особых проблем и сложностей у меня больше не возникало.

Первое, что необходимо — это создать шаблон нашей новости в виде класса.

news.h:

#ifndef NEWS_H
#define NEWS_H

#include <Wt/WDateTime>
#include <Wt/Dbo/Types>
#include <Wt/Dbo/WtSqlTraits>

class News
{
public:
    News();

    std::string title; //Заголовок новости
    std::string text; //Текст новости
    std::string author; //Автор новости
    Wt::WDateTime created; //Дата создания

    template<class Action> //А это наш шаблон для записей БД
    void persist(Action& a)
    {
      Wt::Dbo::field(a, title, "title");
      Wt::Dbo::field(a, text, "text");
      Wt::Dbo::field(a, author, "aurhor");
      Wt::Dbo::field(a, created, "created");
    }
};

DBO_EXTERN_TEMPLATES(News);

#endif // NEWS_H

news.cpp:

#include "news.h"

#include <Wt/Dbo/Impl>

DBO_INSTANTIATE_TEMPLATES(News);

News::News():
    created(Wt::WDateTime::currentDateTime()) //Здесь можно указать поля, заполняемые автоматически, например дата и время создания новости.
{}

«Хм, и как этим пользоваться?» — спросите вы. Всё очень просто. Я использовал sqlite3 по некоторым причинам (переносимость вместе с проектом и потому, что поддержка MySQL в Wt у меня по неизвестным причинам не собралась), но аналогично должно работать с любой поддерживаемой БД.

Переписываем page.cpp:

#include "page.h"
#include "news.h"
#include <Wt/Dbo/backend/Sqlite3>
#include <Wt/WApplication>
#include <Wt/WPanel>

using namespace Wt;

Page::Page(WContainerWidget *parent):WContainerWidget(parent)
{
    Wt::Dbo::backend::Sqlite3 DataBase=WApplication::instance()->appRoot() + "myapp.db"; //Задаём файл БД
    Wt::Dbo::Session DBSession; //Создаём сессию
    DBSession.setConnection(DataBase); //Подключаемся

    DBSession.mapClass<News>("news"); //Ассоциируем наш класс с таблицей news в БД

    Wt::Dbo::Transaction transaction(DBSession); //Создаём транзакцию
    try {
        DBSession.createTables(); //Если таблицы в БД пока нет, создаём её таким вот простым образом. Все поля будут созданы автоматически.
        Wt::log("info") << "Database created";
    } catch (...) {
        Wt::log("info") << "Using existing database";
    }
    transaction.commit();//Записываем изменения

    Wt::WTable *NewsTable=new Wt::WTable();//Создаём таблицу для вывода новостей
    NewsTable->addStyleClass("container");//Делаем таблицу нормальной ширины

    Wt::Dbo::Transaction transaction1(DBSession);//Ещё одна транзакция. Их нужно по одной на каждый запрос, насколько я понял.
    Wt::Dbo::collection<Wt::Dbo::ptr<News> > a=DBSession.find<News>().orderBy("created desc");//Забираем наши новости из БД, отсортированные по дате и времени создания.
    int j=0;
    for (Wt::Dbo::collection<Wt::Dbo::ptr<News> >::const_iterator i = a.begin(); i != a.end(); ++i) {//Заполняем таблицу новостями. Элементы в таблице заранее создавать не надо, они создаются автоматически при первом обращении к ним. Почти бэйсик =)
        Wt::Dbo::ptr<News> Article = *i;
        Wt::WPanel *panel = new Wt::WPanel();
        panel->setTitle(Article.get()->title);
        panel->setCentralWidget(new Wt::WText(WString::fromUTF8("<p>Автор:")+Article.get()->author+"</p><p>"+Article.get()->text+"</p>"));
        NewsTable->elementAt(j,0)->addWidget(panel);
        j++;
    }
    transaction1.commit();//Закрыли транзакцию. Кстати, повторно транзакции использовать нельзя.

    setStyleClass("container");
    contentsStack = new Wt::WStackedWidget();
    contentsStack->setStyleClass("container");
    contentsStack->setPadding(48,Wt::Top);

    NavBar = new WNavigationBar(this);
    NavBar->setTitle("MyApp","http://cursed.redegrade.net");
    NavBar->setResponsive(true);
    NavBar->addStyleClass("container");
    NavBar->setPositionScheme(Wt::Fixed);
    LeftMenu=new WMenu(contentsStack,this);
    NavBar->addMenu(LeftMenu);

    LeftMenu->addItem(WString::fromUTF8("Новости"),NewsTable); //Прикручиваем нашу табличку с новостями к пункту меню.

    this->addWidget(contentsStack);
}

Ну, по поводу вывода новостей всё понятно, а как же создание?

Хм, и правда. Куда без создания. Сейчас прикрутим. Придётся переписать класс Page ещё раз, заключительный в пределах статьи.

page.h:

#ifndef PAGE_H
#define PAGE_H

#include <Wt/WContainerWidget>
#include <Wt/WText>
#include <Wt/WString>
#include <Wt/WNavigationBar>
#include <Wt/WMenu>
#include <Wt/WTable>
#include <Wt/WStackedWidget>
#include <Wt/Dbo/Session>

class Page : public Wt::WContainerWidget
{
public:
    Page(WContainerWidget *parent = 0);
private:
    Wt::WStackedWidget *contentsStack;
    Wt::WNavigationBar *NavBar;
    Wt::WMenu *LeftMenu;
    Wt::WTable *CreateArticle;
    Wt::WLineEdit *Title;
    Wt::WLineEdit *Author;
    Wt::WTextArea *Text;
    Wt::WPushButton *AddNews;
    Wt::Dbo::Session DBSession;
    void AddArticle();
};

#endif // PAGE_H

page.cpp

#include "page.h"
#include "news.h"
#include <Wt/WApplication>
#include <Wt/WPanel>
#include <Wt/WLineEdit>
#include <Wt/WTextEdit>
#include <Wt/WPushButton>
#include <Wt/Dbo/backend/Sqlite3>

using namespace Wt;

Page::Page(WContainerWidget *parent):WContainerWidget(parent)
{
    Wt::Dbo::backend::Sqlite3 DataBase=WApplication::instance()->appRoot() + "myapp.db";
    DBSession.setConnection(DataBase);

    DBSession.mapClass<News>("news");

    Wt::Dbo::Transaction transaction(DBSession);
    try {
        DBSession.createTables();
        Wt::log("info") << "Database created";
    } catch (...) {
        Wt::log("info") << "Using existing database";
    }
    transaction.commit();

    Wt::WTable *NewsTable=new Wt::WTable();
    NewsTable->addStyleClass("container");

    Wt::Dbo::Transaction transaction1(DBSession);
    Wt::Dbo::collection<Wt::Dbo::ptr<News> > a=DBSession.find<News>().orderBy("created desc");
    int j=0;
    for (Wt::Dbo::collection<Wt::Dbo::ptr<News> >::const_iterator i = a.begin(); i != a.end(); ++i) {
        Wt::Dbo::ptr<News> Article = *i;
        Wt::WPanel *panel = new Wt::WPanel();
        panel->setTitle(WString::fromUTF8(Article.get()->title));
        panel->setCentralWidget(new Wt::WText(WString::fromUTF8("<p>Автор:")+WString::fromUTF8(Article.get()->author)+"</p><p>"+WString::fromUTF8(Article.get()->text)+"</p>"));//Немного исправил вывод. Из коробки норовит выводить русский текст знаками вопроса.
        NewsTable->elementAt(j,0)->addWidget(panel);
        NewsTable->elementAt(j,0)->setLoadLaterWhenInvisible(true);
        j++;
    }
    transaction1.commit();

    setStyleClass("container");
    contentsStack = new Wt::WStackedWidget();
    contentsStack->setStyleClass("container");
    contentsStack->setPadding(48,Wt::Top);

    NavBar = new WNavigationBar(this);
    NavBar->setTitle("MyApp","http://localhost:8080");
    NavBar->setResponsive(true);
    NavBar->addStyleClass("container");
    NavBar->setPositionScheme(Wt::Fixed);
    LeftMenu=new WMenu(contentsStack,this);
    NavBar->addMenu(LeftMenu);

    LeftMenu->addItem(WString::fromUTF8("Новости"),NewsTable);

    CreateArticle=new Wt::WTable(); //Табличка для формы создания новости
    Title=new Wt::WLineEdit(WString::fromUTF8("Заголовок новости")); //Всякие поля
    Author=new Wt::WLineEdit(WString::fromUTF8("Автор"));
    Text=new Wt::WTextArea(WString::fromUTF8("Текст новости"));
    AddNews=new Wt::WPushButton(WString::fromUTF8("Добавить новость"));

    CreateArticle->addStyleClass("container");

    CreateArticle->elementAt(0,0)->addWidget(Title); //Добавляем поля в табличку
    CreateArticle->elementAt(1,0)->addWidget(Author);
    CreateArticle->elementAt(2,0)->addWidget(Text);
    CreateArticle->elementAt(3,0)->addWidget(AddNews);

    LeftMenu->addItem(WString::fromUTF8("Создать новость"),CreateArticle); //Создаём кнопку и привязываем метод Page::AddArticle к её нажатию
    AddNews->clicked().connect(this,&Page::AddArticle);

    this->addWidget(contentsStack);
}

void Page::AddArticle()
{
    Wt::Dbo::backend::Sqlite3 DataBase=WApplication::instance()->appRoot() + "myapp.db";
    DBSession.setConnection(DataBase); //Переподключаемся (долго с этим бился, возможно, стоило сделать всё переменными класса, а не локальными)
    Wt::Dbo::Transaction transaction(DBSession); //Повторюсь: для каждой операции с БД нужна живая транзакция.
    News *a=new News(); //Создаём новость
    a->title=Title->text().toUTF8();
    a->author=Author->text().toUTF8();
    a->text=Text->text().toUTF8();
    Wt::Dbo::ptr<News> Article=DBSession.add(a); //Заносим новость в БД
}

Результат:

Пишем сайт с новостями на Wt
Пишем сайт с новостями на Wt

P.S. Если необходимы дополнительные пояснения, пишите в комментариях, постараюсь рассказать подробней. Я только начал изучать Wt, поэтому, вполне возможно, где-то неправ. Буду рад, если поправите.
P.P.S. Если вам интересны статьи по Wt, пишите в комментариях, что ещё вы хотели бы о нём узнать, и я постараюсь написать об этом!

Автор: Ruckus

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js