Passo a passo: aplicativo leitor (DirectX e XAML)

Os aplicativos que incorporam uma experiência de leitura estão se popularizando rapidamente à medida que os computadores tablet se tornam mais predominantes. Neste artigo, mostramos como criar um aplicativo leitor de documentos avançado com DirectX, C++ e XAML. Descrevemos a estrutura, as tecnologias e as práticas recomendadas para esse tipo de aplicativo, incluindo como usar um Virtual Surface Image Source para criar interoperabilidade entre o código de desenho do DirectX e o gerenciamento de superfícies do XAML, usar o DirectWrite para carregar fontes, usar o WIC para carregar imagens e aplicar efeitos gráficos Direct2D a imagens.

Explicamos grande parte do código do aplicativo de exemplo de revista em DirectX com interoperabilidade com XAML e analisamos os diversos componentes que formam o aplicativo.

Estrutura do exemplo de revista

Os arquivos no exemplo podem ser divididos em três grupos: XML, XAML e DirectX. Os arquivos no grupo XML são responsáveis por representar cada uma das marcas no arquivo XML que contém todas as informações sobre um artigo. Os arquivos no grupo XAML são responsáveis por configurar os elementos XAML e funcionar com o Virtual Surface Image Source. Por fim, os arquivos no grupo DirectX são responsáveis por processos como o carregamento de fontes personalizadas do DirectWrite, efeitos gráficos Direct2D e codificação de imagens WIC.

NomeTipo
BindablePropertyXAML
ContentImageSourceXAML
DesignXML
DocumentXML
ElementXML
FontFileStreamDirectX
FontLoaderDirectX
ImageXML
ImageFileXML
ImageFrameXML
LayerXML
ListXML
PageXML
PageModelXAML
PageRollXML
RectangleXML
ResourceXML
StoryXML
TextXML
TextFrameXML
TreeIteratorXML

 

Virtual Surface Image Source

O Virtual Surface Image Source (VSIS) é uma superfície de renderização gerenciada do XAML que é bastante útil quando se deseja criar um aplicativo que incorpore o movimento panorâmico e o ajuste de zoom. Essa superfície é como qualquer outra origem de bitmap do DirectX, exceto pelo fato de que o XAML gerencia as manipulações entre o DirectX e a origem da imagem.

Para usar um VSIS em seu aplicativo, você precisará:

  • Determinar o tamanho do conteúdo.
  • Criar o VSIS que é o tamanho da imagem.
  • Definir o dispositivo DXGI como o dispositivo no VSIS.
  • Registrar o retorno de chamada da origem da imagem para receber mensagens quando precisar renderizar conteúdo.

Este código mostra como executar esses procedimentos.


    // Measure the content and store its size.
    Measure(&m_contentSize);

    // Create an image source at the initial pixel size.
    m_imageSource = ref new VirtualSurfaceImageSource(m_contentSize.cx, m_contentSize.cy);

    ComPtr<IUnknown> unknown(reinterpret_cast<IUnknown*>(m_imageSource));
    unknown.As(&m_imageSourceNative);

    auto renderer = m_document->GetRenderer();

    // Set DXGI device to the image source
    ComPtr<IDXGIDevice> dxgiDevice;
    renderer->GetDXGIDevice(&dxgiDevice);
    m_imageSourceNative->SetDevice(dxgiDevice.Get());

    // Register image source's update callback so update can be made to it.
    m_imageSourceNative->RegisterForUpdatesNeeded(this);


Agora, você tem um VSIS e sua classe está registrada para receber informações de retorno de chamada da superfície virtual. Em seguida, você deverá implementar os métodos de retorno de chamada que o VSIS chamará quando precisar atualizar o conteúdo.

Quando o usuário manipular o VSIS rolando o conteúdo, o VSIS chamará o método UpdatesNeeded da sua classe registrada. Assim, você deverá implementar o método de retorno de chamada UpdatesNeeded.

Este código mostra como implementar o método de retorno de chamada UpdatesNeeded e o método auxiliar Draw. Quando o VSIS chamar esse método de retorno de chamada na classe ContentImageSource, o método recuperará o retângulo atualizado que o VSIS está renderizando com o método VSISGetUpdateRectCount. Em seguida, você chamará o método de desenho com essa área atualizada.


// This method is called when the framework needs to update region managed by
// the virtual surface image source.
HRESULT STDMETHODCALLTYPE ContentImageSource::UpdatesNeeded()
{
    HRESULT hr = S_OK;

    try
    {
        ULONG drawingBoundsCount = 0;

        DX::ThrowIfFailed(
            m_imageSourceNative->GetUpdateRectCount(&drawingBoundsCount)
            );

        std::unique_ptr<RECT[]> drawingBounds(new RECT[drawingBoundsCount]);

        DX::ThrowIfFailed(
            m_imageSourceNative->GetUpdateRects(drawingBounds.get(), drawingBoundsCount)
            );

        // This code doesn't try to coalesce multiple drawing bounds into one. Although that
        // extra process  reduces the number of draw calls, it requires the virtual surface
        // image source to manage non-uniform tile size, which requires it to make extra copy
        // operations to the compositor. By using the drawing bounds it directly returns, which are
        //  non-overlapping  tiles of the same size, the compositor can use these tiles directly,
        // which can greatly reduce the amount of memory needed by the virtual surface image source.
        // This results in more draw calls, but Direct2D can accommodate them
        // without significant impact on presentation frame rate.
        for (ULONG i = 0; i < drawingBoundsCount; ++i)
        {
            if (Draw(drawingBounds[i]))
            {
                // Drawing isn't complete. This can happen when the content is still being
                // asynchronously loaded. Inform the image source to invalidate the drawing
                // bounds so that it calls back to redraw.
                DX::ThrowIfFailed(
                    m_imageSourceNative->Invalidate(drawingBounds[i])
                    );
            }
        }
    }
    catch (Platform::Exception^ exception)
    {
        hr = exception->HResult;
    }

    return hr;
}

bool ContentImageSource::Draw(RECT const& drawingBounds)
{
    ComPtr<IDXGISurface> dxgiSurface;
    POINT surfaceOffset = {0};

    DX::ThrowIfFailed(
        m_imageSourceNative->BeginDraw(
            drawingBounds,
            &dxgiSurface,
            &surfaceOffset
            )
        );

    auto renderer = m_document->GetRenderer();

    ComPtr<ID2D1DeviceContext> d2dDeviceContext;
    renderer->GetD2DDeviceContext(&d2dDeviceContext);

    ComPtr<ID2D1Bitmap1> bitmap;
    DX::ThrowIfFailed(
        d2dDeviceContext->CreateBitmapFromDxgiSurface(
            dxgiSurface.Get(),
            nullptr,
            &bitmap
            )
        );

    // Begin the drawing batch
    d2dDeviceContext->BeginDraw();

    // Scale content design coordinate to the display coordinate,
    // then translate the drawing to the designated place on the surface.
    D2D1::Matrix3x2F transform =
        D2D1::Matrix3x2F::Scale(
            m_document->DesignToDisplayWidth(1.0f),
            m_document->DesignToDisplayHeight(1.0f)
            ) *
        D2D1::Matrix3x2F::Translation(
            static_cast<float>(surfaceOffset.x - drawingBounds.left),
            static_cast<float>(surfaceOffset.y - drawingBounds.top)
            );

    // Prepare to draw content. This is the appropriate time for content element
    // to draw to an intermediate if there is any. It is important for performance
    // reason that you don't call SetTarget too often. Preparing the intermediates
    // upfront reduces the number of times the render target switches back and forth.
    bool needRedraw = m_content->PrepareToDraw(
        m_document,
        transform
        );

    if (!needRedraw)
    {
        // Set the render target to surface given by the framework
        d2dDeviceContext->SetTarget(bitmap.Get());

        d2dDeviceContext->SetTransform(D2D1::IdentityMatrix());

        // Constrain the drawing to the designated portion of the surface
        d2dDeviceContext->PushAxisAlignedClip(
            D2D1::RectF(
                static_cast<float>(surfaceOffset.x),
                static_cast<float>(surfaceOffset.y),
                static_cast<float>(surfaceOffset.x + (drawingBounds.right - drawingBounds.left)),
                static_cast<float>(surfaceOffset.y + (drawingBounds.bottom - drawingBounds.top))
                ),
            D2D1_ANTIALIAS_MODE_ALIASED
            );

        // The Clear call must follow the PushAxisAlignedClip call.
        // Placing the Clear call before the clip is set violates the contract of the
        // virtual surface image source in that the app draws outside the
        // designated portion of the surface the image source hands over to it. This
        // violation won't actually cause the content to spill outside the designated
        // area because the image source will safeguard it. But this extra protection
        // has a runtime cost associated with it, and in some drivers this cost can be
        // very expensive. So the best performance strategy here is to never create a
        // situation where this protection is required. Not drawing outside the appropriate
        // clip does that the right way.
        d2dDeviceContext->Clear(D2D1::ColorF(0, 0));

        // Draw the content
        needRedraw = m_content->Draw(
            m_document,
            transform
            );

        d2dDeviceContext->PopAxisAlignedClip();

        d2dDeviceContext->SetTarget(nullptr);
    }

    // End the drawing
    DX::ThrowIfFailed(
        d2dDeviceContext->EndDraw()
        );

    // Submit the completed drawing to the framework
    DX::ThrowIfFailed(
        m_imageSourceNative->EndDraw()
        );

    return needRedraw;
}


Com os retornos de chamada implementados, seu aplicativo agora receberá informações do VSIS sobre a região que você precisa desenhar. Sua função de desenho só precisa obter esse retângulo e desenhar no VSIS na área correta. Ao implementar esses dois retornos de chamada e registrar a classe no VSIS, o aplicativo pode renderizar na interface virtual e atualizar a área necessária quando o usuário manipular o conteúdo para revelar mais a superfície.

Carregamento de conteúdo e vinculação de dados com XML

As informações que nosso aplicativo exibe na revista são todas armazenadas no arquivo Sample.story. Esse arquivo é um documento XML que contém informações sobre o conteúdo, o título, a fonte, a imagem de tela de fundo e outros atributos do artigo. Cada informação importante que o aplicativo precisa interpretar tem uma marca. Com marcas, você pode facilmente expandir os artigos, criar objetos a partir dessas informações marcadas e vincular esses dados ao XAML no aplicativo.

Como as informações no arquivo .story estão em um esquema XML personalizado, a classe em TreeIterator.h ajuda a analisar esses dados XML e os coloca nas classes especializadas do aplicativo.

Este é um exemplo das informações contidas no arquivo .story usado pelo aplicativo.


<?xml version="1.0" encoding="utf-8"?>
<story>
    <resource>
        <image name="grasshopper-image" file="grasshopper.jpg"/>
        <image name="butterfly-image" file="butterfly.jpg"/>
        <text name="section" font-family="Arial" font-stretch="2" font-size="22">A BUG'S LIFE</text>
        <text name="grasshopper-title" font-family="Gill Sans" font-stretch="3" font-weight="700" font-size="70">GRASSHOPPER</text>
        <text name="grasshopper-quote" font-family="Pericles" font-size="18">a slender plant-eating flying and jumping insect that produces a buzzing sound by rubbing its back legs against its forewings</text>
        <text name="grasshopper-body" font-family="Kootenay" font-size="16"><bold>LOREM</bold> ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. Integer tincidunt. Cras dapibus. Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, porttitor eu, consequat vitae, eleifend ac, enim. Aliquam lorem ante, dapibus in, viverra quis, feugiat a, tellus. Phasellus viverra nulla ut metus varius laoreet. Quisque rutrum. Aenean imperdiet. Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies nisi. Nam eget dui.
In hac habitasse platea dictumst. Curabitur at lacus ac velit ornare lobortis. Curabitur a felis in nunc fringilla tristique. Morbi mattis ullamcorper velit. Phasellus gravida semper nisi. Nullam vel sem. Pellentesque libero tortor, tincidunt et, tincidunt eget, semper nec, quam. Sed hendrerit. Morbi ac felis. Nunc egestas, augue at pellentesque laoreet, felis eros vehicula leo, at malesuada velit leo quis pede. Donec interdum, metus et hendrerit aliquet, dolor diam sagittis ligula, eget egestas libero turpis vel mi. Nunc nulla. Fusce risus nisl, viverra et, tempor et, pretium in, sapien. Donec venenatis vulputate lorem.</text>


Como mostra o trecho de XML, as marcas das informações correspondem diretamente a algumas das classes XML no aplicativo. Quando o iterador analisa o arquivo XML, é criada para cada marca uma classe de mesmo nome que armazena as informações dentro da marca.

Depois que retirarmos todas as informações do arquivo XML e inseri-las no aplicativo, você vinculará os dados a elementos na interface do usuário. Essa vinculação de dados existe em dois locais: na marcação XAML da página e no arquivo cpp que acompanha a página de marcação XAML. Neste exemplo, ela existe em MainPage.xaml e MainPage.cpp.

Na marcação XAML, crie instâncias das classes FlipView, DataTemplate e ScrollViewer. Em seguida, vincule as informações a esses itens de marcação XAML. Este é o código de marcação XAML desses elementos.


<FlipView x:Name="FlipView" ManipulationMode="TranslateInertia">
        <FlipView.ItemTemplate>
            <DataTemplate>
                <ScrollViewer ZoomMode="Disabled" VerticalScrollMode="Enabled" IsVerticalRailEnabled="True">
                    <ScrollViewer.Background>
                        <ImageBrush ImageSource="{Binding Background}"/>
                    </ScrollViewer.Background>
                    <Image 
                        Source="{Binding Content}" 
                        Width="{Binding ContentWidth}" 
                        Height="{Binding ContentHeight}" 
                        HorizontalAlignment="Left"
                        />
                </ScrollViewer>
            </DataTemplate>
        </FlipView.ItemTemplate>
    </FlipView>


Esse código mostra que a tela de fundo e a imagem têm vinculações de dados que correspondem aos dados no código-fonte XML. Agora você precisa popular essas vinculações com as informações disponíveis. Você colocará o código no arquivo cpp de mesmo nome. Este é o código em MainPage.cpp que ajuda na vinculação de dados.


void MainPage::DocumentLoaded(_In_ Document^ document)
{
    // Parse the document into an element tree.
    document->Parse();

    auto contentRoot = document->GetContentRoot();

    if (contentRoot != nullptr)
    {
        // Create a collection of content element to bind to the view
        auto items = ref new Platform::Collections::Vector<Platform::Object^>();

        auto pageContent = contentRoot->GetFirstChild();

        while (pageContent != nullptr)
        {
            items->Append(ref new PageModel(pageContent, document));

            pageContent = pageContent->GetNextSibling();
        }

        FlipView->ItemsSource = items;

        // Load the saved document state if any
        LoadState(ApplicationData::Current->LocalSettings->Values);

        m_document = document;
    }
}


Esse código analisa o documento XML e, em seguida, cria um vetor de elementos de conteúdo para vinculá-los aos elementos XAML na interface do usuário.

Carregamento de fontes personalizadas

Para usar fontes personalizadas nesse aplicativo DirectX, você precisará implementar uma coleção e um carregador de fontes assíncrono no DirectWrite. Em um aplicativo DirectX, se você desejar fornecer uma fonte personalizada para ser usada em elementos de texto, precisará usar o DirectWrite para obter as novas fontes que não estão instaladas no sistema do usuário. No exemplo, o código que carrega e disponibiliza essas novas fontes encontra-se nas classes FontFileStream e FontLoader.

A classe FontFileStream é uma implementação da interface IDWriteFontFileStream e obtém o próprio arquivo de fonte e o carrega em um formato que o aplicativo possa interpretar. Isso envolve métodos que manipulam a leitura de fragmentos do arquivo, a liberação de fragmentos do arquivo, a obtenção do tamanho do arquivo e da hora da última edição.

Este é o código para ler e liberar o fragmento do arquivo de fonte.


HRESULT STDMETHODCALLTYPE FontFileStream::ReadFileFragment(
    _Outptr_result_bytebuffer_(fragmentSize) void const** fragmentStart,
    UINT64 fileOffset,
    UINT64 fragmentSize,
    _Out_ void** fragmentContext
    )
{
    // The loader is responsible for doing a bounds check.
    if (    fileOffset <= m_data->Length
        &&  fragmentSize + fileOffset <= m_data->Length
        )
    {
        *fragmentStart = m_data->Data + static_cast<ULONG>(fileOffset);
        *fragmentContext = nullptr;
        return S_OK;
    }
    else
    {
        *fragmentStart = nullptr;
        *fragmentContext = nullptr;
        return E_FAIL;
    }
}

void STDMETHODCALLTYPE FontFileStream::ReleaseFileFragment(
    _In_ void* fragmentContext
    )
{
}


Este é o código para recuperar o tamanho do arquivo e a hora da última edição.


HRESULT STDMETHODCALLTYPE FontFileStream::GetFileSize(
    _Out_ UINT64* fileSize
    )
{
    *fileSize = m_data->Length;
    return S_OK;
}

HRESULT STDMETHODCALLTYPE FontFileStream::GetLastWriteTime(
    _Out_ UINT64* lastWriteTime
    )
{
    // The concept of last write time does not apply to this loader.
    *lastWriteTime = 0;
    return E_NOTIMPL;
}


Depois que todas as partes estiverem em seus devidos locais e você puder acessar as informações no arquivo de fonte, será necessário criar o código que permite construir uma coleção de fontes personalizadas e os objetos de fonte a partir do fluxo de arquivo. Esse código pode ser encontrado no arquivo FontLoader.cpp.

Primeiramente, você precisa de uma função que carregue e enumere as fontes que deseja usar. Este código é referente à função LoadAsync que analisa o diretório de fontes no exemplo de revista e enumera uma lista dos arquivos de fonte .ttf presentes nesse diretório.


task<void> FontLoader::LoadAsync()
{
    // Locate the "fonts" subfolder within the document folder
    return task<void>([this]()
    {
        task<StorageFolder^>(m_location->GetFolderAsync("fonts")).then([=](StorageFolder^ folder)
        {
            // Enumerate a list of .TTF files in the storage location
            auto filters = ref new Platform::Collections::Vector<Platform::String^>();
            filters->Append(".ttf");

            auto queryOptions = ref new QueryOptions(CommonFileQuery::DefaultQuery, filters);
            auto queryResult = folder->CreateFileQueryWithOptions(queryOptions);

            return queryResult->GetFilesAsync();

        }).then([=](IVectorView<StorageFile^>^ files)
        {
            m_fontFileCount = files->Size;

            std::vector<task<IBuffer^>> tasks;

            for (uint32 i = 0; i < m_fontFileCount; ++i)
            {
                auto file = dynamic_cast<StorageFile^>(files->GetAt(i));

                tasks.push_back(task<IBuffer^>(FileIO::ReadBufferAsync(file)));
            }

            return when_all(tasks.begin(), tasks.end());

        }).then([=](std::vector<IBuffer^> buffers)
        {
            for each (IBuffer^ buffer in buffers)
            {
                auto fileData = ref new Platform::Array<byte>(buffer->Length);
                DataReader::FromBuffer(buffer)->ReadBytes(fileData);

                ComPtr<FontFileStream> fontFileStream(new FontFileStream(fileData));
                m_fontFileStreams.push_back(fontFileStream);
            }

        }).wait();
    });
}


Agora, crie outro método que obtenha a chave da coleção de fontes que será passada quando o DirectWrite criar uma coleção de fontes personalizadas. Esse método obterá esse valor e retornará um enumerador de arquivo de fonte.


HRESULT STDMETHODCALLTYPE FontLoader::CreateEnumeratorFromKey(
    _In_ IDWriteFactory* factory,
    _In_reads_bytes_(fontCollectionKeySize) void const* fontCollectionKey,
    uint32 fontCollectionKeySize,
    _Outptr_ IDWriteFontFileEnumerator** fontFileEnumerator
    )
{
    *fontFileEnumerator = ComPtr<IDWriteFontFileEnumerator>(this).Detach();
    return S_OK;
}


Esse método recebe a mesma chave de coleção e retorna um fluxo do arquivo de fonte.


HRESULT STDMETHODCALLTYPE FontLoader::CreateStreamFromKey(
    _In_reads_bytes_(fontFileReferenceKeySize) void const* fontFileReferenceKey,
    uint32 fontFileReferenceKeySize,
    _Outptr_ IDWriteFontFileStream** fontFileStream
    )
{
    if (fontFileReferenceKeySize != sizeof(size_t))
    {
        return E_INVALIDARG;
    }

    size_t fontFileStreamIndex = *(static_cast<size_t const*>(fontFileReferenceKey));

    *fontFileStream = ComPtr<IDWriteFontFileStream>(m_fontFileStreams.at(fontFileStreamIndex).Get()).Detach();

    return S_OK;
}


Os próximos dois métodos são auxiliares. A primeira função passa para o próximo arquivo de fonte no fluxo de arquivo e a outra funciona como um getter simples do arquivo de fonte atual. Este é o código para esses dois métodos.


HRESULT STDMETHODCALLTYPE FontLoader::MoveNext(OUT BOOL* hasCurrentFile)
{
    *hasCurrentFile = FALSE;

    if (m_fontFileStreamIndex < m_fontFileCount)
    {
        DX::ThrowIfFailed(
            m_dwriteFactory->CreateCustomFontFileReference(
                &m_fontFileStreamIndex,
                sizeof(size_t),
                this,
                &m_currentFontFile
                )
            );

        *hasCurrentFile = TRUE;
        ++m_fontFileStreamIndex;
    }

    return S_OK;
}

HRESULT STDMETHODCALLTYPE FontLoader::GetCurrentFontFile(OUT IDWriteFontFile** currentFontFile)
{
    *currentFontFile = ComPtr<IDWriteFontFile>(m_currentFontFile.Get()).Detach();
    return S_OK;
}


Com o código completo, agora você tem um carregador de arquivo de fonte ativo que funciona assincronamente com o aplicativo. Essas duas classes funcionam em conjunto para enumerar os arquivos no sistema, carregá-los em um fluxo de arquivo de fonte e criar uma coleção de fontes personalizadas com essas fontes para poderem ser usadas.

Práticas recomendadas sobre o uso do VSIS

O Windows 8 oferece suporte a Surface Image Source e Virtual Surface Image Source quando você deseja executar interoperabilidade entre DirectX e XAML, mas qual será usado dependerá do que você deseja fazer.

Existem três principais cenários, cada um com uma opção de interoperabilidade com XAML.

  • Para desenhar um elemento como uma textura como um bitmap em seu aplicativo.
  • Para desenhar um bitmap maior do que a tela, será necessário o movimento panorâmico.
  • Para melhorar o desempenho da manipulação de toque.

O Surface Image Source é indicado se você precisar gerenciar uma imagem estática ou atualizar o conteúdo do SIS esporadicamente. Um bom exemplo de quando o SIS é útil é se você deseja desenhar um bitmap como um elemento da interface do usuário XAML. Pode ser necessário atualizar esse elemento para exibir informações diferentes, mas ele é em grande parte estático. O SIS funciona bem porque as atualizações que seu aplicativo faz no SIS são sincronizadas com o thread da interface do usuário XAML, portanto, ele geralmente funciona melhor em cenários em que precisa de um desempenho equivalente ao da interface do usuário do aplicativo.

Porém, se você precisar de uma área de conteúdo DirectX ou bitmap grande que precise ser manipulada diretamente (com rolagem ou movimento panorâmico), use um VSIS. Um VSIS é útil quando as informações que você deseja exibir não cabem na tela ou no elemento. Um exemplo do uso adequado de um VSIS é um aplicativo de leitura com texto rolável ou um aplicativo de mapas que exige movimento panorâmico ou ajuste de zoom para explorar o mapa.

Se nenhum desses cenários corresponder ao caso de uso do seu aplicativo, talvez VSIS ou SIS não seja adequado para ele. Em termos específicos, se o desempenho for importante para o aplicativo, o elemento SwapChainBackgroundPanel em XAML pode ser mais indicado. Para saber mais, veja Windows.UI::Xaml::Controls::SwapChainBackgroundPanel.

 

 

Mostrar:
© 2014 Microsoft