Пользовательские потоки данных

Введение

SFML имеет несколько ресурсных классов: для картинок, шрифтов, звуков и т.д. В большинстве программ эти ресурсы будут загружены из файлов, при помощи своих функций loadFromFile(). В некоторых других случаях, ресурсы будут упакованы в исполняемый файл или в большой файл данных, и тогда будут загружаться из памяти с помощью loadFromMemory(). Эти функции покрывают практически все возможные варианты использования — но не все.

Иногда вы хотите загрузить файлы их необычного места, такого как сжатый/зашифрованный архив или удалённая сеть, например. Для подобных специальных случаев SFML предоставляет третью функцию загрузки: loadFromStream(). Эта функция считывает данные из абстрактного класса sf::InputStream, который позволяет определить собственные реализации.

В этом уроке вы узнаете как написать и использовать свой собственный производный входной поток.

А как же стандартные потоки?

Как и большинство других языков, С++ уже имеет класс для входного потока данных: std::istream. На самом деле их даже два: std::istream это только внешний абстрактный интерфейс для пользовательских данных std::streambuf.

К сожалению, эти классы не очень удобные, и способны сильно усложнить вам жизнь, если вы захотите реализовать что-то не тривиальное. Библиотека Boost.Iostreams пытается обеспечить более простой интерфейс для стандартных потоков, но Boost это большая зависимость и SFML не может себе позволить зависеть от неё.

Вот почему SFML предоставляет свой собственный интерфейс для потоков, который, мы надеемся, является более простым и быстрым.

InputStream

Класс sf::InputStream объявляет четыре виртуальные функции:

class InputStream
{
public :

    virtual ~InputStream() {}

    virtual Int64 read(void* data, Int64 size) = 0;

    virtual Int64 seek(Int64 position) = 0;

    virtual Int64 tell() = 0;

    virtual Int64 getSize() = 0;
};

read должна извлекать size байтов данных из потока и копировать их в указанный адрес data; она возвращает число прочитанных байт или -1 в случае ошибки.

seek должна изменять текущую позицию чтения в потоке; её аргумент position обозначает абсолютное число байт смещения для перехода (относительно начала данных, а не относительно текущей позиции); она возвращает значение новой позиции или -1 в случае ошибки.

tell должна возвращать текущую позицию чтения (в байтах) в потоке или -1 в случае ошибки.

getSize должна возвращать суммарный размер дынных (в байтах), который содержится в потоке или -1 в случае ошибки.

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

Пример

Здесь приведена полная и рабочая реализация пользовательского входного потока. И она не очень пригодная. :) Мы напишем поток FileStream, который считывает данные из файла. Главное, что это достаточно просто, и вы сможете сосредоточится на том, как работает код, не потерявшись при этом в деталях.

Для начала, взглянем на объявление:

#include <SFML/System.hpp>
#include <string>
#include <cstdio>

class FileStream : public sf::InputStream
{
public :

    FileStream();

    ~FileStream();

    bool open(const std::string& filename);

    virtual sf::Int64 read(void* data, sf::Int64 size);

    virtual sf::Int64 seek(sf::Int64 position);

    virtual sf::Int64 tell();

    virtual sf::Int64 getSize();

private :

    std::FILE* m_file;
};

В этом примере мы будем использовать старый добрый Cи`шный API для файлов, потому то у нас и есть член std::FILE*. Так же мы добавим конструктор, деструктор и функцию открытия файла.

А вот и реализация класса:

FileStream::FileStream() :
m_file(NULL)
{
}

FileStream::~FileStream()
{
    if (m_file)
        std::fclose(m_file);
}

bool FileStream::open(const std::string& filename)
{
    if (m_file)
        std::fclose(m_file);

    m_file = std::fopen(filename.c_str(), "rb");

    return m_file != NULL;
}

sf::Int64 FileStream::read(void* data, sf::Int64 size)
{
    if (m_file)
        return std::fread(data, 1, static_cast<std::size_t>(size), m_file);
    else
        return -1;
}

sf::Int64 FileStream::seek(sf::Int64 position)
{
    if (m_file)
    {
        std::fseek(m_file, static_cast<std::size_t>(position), SEEK_SET);
        return tell();
    }
    else
    {
        return -1;
    }
}

sf::Int64 FileStream::tell()
{
    if (m_file)
        return std::ftell(m_file);
    else
        return -1;
}

sf::Int64 FileStream::getSize()
{
    if (m_file)
    {
        sf::Int64 position = tell();
        std::fseek(m_file, 0, SEEK_END);
        sf::Int64 size = tell();
        seek(position);
        return size;
    }
    else
    {
        return -1;
    }
}

Обратите внимание на то, что в случае ошибки, все функции возвращают -1, об этом требовании уже упоминалось выше.

Вы можете проверить форум и wiki на оф.сайте, есть большая вероятность что кто-то уже успел написать свой класс на основе sf::InputStream, который соответствует вашим нуждам. А если вы напишите свой и посчитаете что он кому-то может пригодиться, не стесняетесь и делитесь всё на том же оф.сайте! :)

Использование вашего потока

Использование пользовательского потока прямолинейно: создание экземпляра класса и передача его функции loadFromStream() (или функции openFromStream()) того объекта, который вы хотите загрузить.

FileStream stream;
stream.open("image.png");

sf::Texture texture;
texture.loadFromStream(stream);

Типичные ошибки

Некоторые классы ресурсов не загружаются полностью после того, как была вызвана loadFromStream(). Вместо этого, они продолжают считывать свои данные так долго, как они используются. Так происходит с классом sf::Music, образцы музыки которого, продолжают воспроизводиться и классом sf::Font который загружает символы на лету в зависимости от содержания текстов.

Как следствие, экземпляр потока который вы используете для загрузки музыки или шрифта, а так же источник данных, должны существовать до тех пор, пока используются этот ресурс. Если они будут уничтожены, то поведение станет неопределённым (приложение рухнет, перестанет отображаться или произойдёт потеря данных).

Ещё одна распространённая ошибка заключается в возвращаемом значении внутренней функции. Например в примере выше, может возникнуть соблазн написать функцию seek() следующим образом:

sf::Int64 FileStream::seek(sf::Int64 position)
{
    return std::fseek(m_file, position, SEEK_SET);
}

Этот код неверный, поскольку std::fseek() возвращает ноль в случае успеха, в то время как SFML ожидает возвращения нового положения.

1 Комментарий

  1. Чих Пых

    Напиши КАК СМЕНИТЬ СТИЛЬ И РАЗМЕР ОКНА уже работающего пл3, нужно же сначала узнать его имя, размер и стиль, чтобы их менять при работе ? как это сделать ?

Оставить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Капча * Лимит времени истёк. Пожалуйста, перезагрузите CAPTCHA.

Этот сайт использует Akismet для борьбы со спамом. Узнайте как обрабатываются ваши данные комментариев.